From 1b4b10f8cdfb2e04f0acba0d6f9f674e8773ecbf Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Tue, 23 Jul 2019 23:45:27 -0500 Subject: [PATCH 0001/1197] Update docs/installation.md Address that numpy is required before `python3 -m pip install -r requirements.txt` can run. --- docs/installation.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/installation.md b/docs/installation.md index 657273e2f..35cdcda62 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -175,6 +175,7 @@ cp config.json.example config.json ``` bash python3 -m pip install --upgrade pip +pip install numpy python3 -m pip install -r requirements.txt python3 -m pip install -e . ``` From 54bde6ac11a183be414a8faa37e588eafa269aaf Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 16:34:23 +0100 Subject: [PATCH 0002/1197] verify date on new candle before producing signal --- freqtrade/freqtradebot.py | 9 ++++++--- freqtrade/strategy/interface.py | 9 +++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 914b8d9cd..02d8b5b56 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -390,14 +390,17 @@ class FreqtradeBot: """ logger.debug(f"create_trade for pair {pair}") + dataframe = self.dataprovider.ohlcv(pair, self.strategy.ticker_interval) + latest = dataframe.iloc[-1] + # Check if dataframe is out of date + signal_date = arrow.get(latest['date']) + if self.strategy.is_pair_locked(pair): logger.info(f"Pair {pair} is currently locked.") return False # running get_signal on historical data fetched - (buy, sell) = self.strategy.get_signal( - pair, self.strategy.ticker_interval, - self.dataprovider.ohlcv(pair, self.strategy.ticker_interval)) + (buy, sell) = self.strategy.get_signal(pair, self.strategy.ticker_interval, dataframe) if buy and not sell: if not self.get_free_open_trades(): diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index d23af3f6e..765e23b0b 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -277,15 +277,20 @@ class IStrategy(ABC): latest = dataframe.iloc[-1] - # Check if dataframe is out of date + # Check if dataframe has new candle signal_date = arrow.get(latest['date']) interval_minutes = timeframe_to_minutes(interval) + if (arrow.utcnow() - signal_date).total_seconds() // 60 >= interval_minutes: + logger.warning('Old candle for pair %s. Last tick is %s minutes old', + pair, (arrow.utcnow() - signal_date).total_seconds() // 60) + + # Check if dataframe is out of date offset = self.config.get('exchange', {}).get('outdated_offset', 5) if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))): logger.warning( 'Outdated history for pair %s. Last tick is %s minutes old', pair, - (arrow.utcnow() - signal_date).seconds // 60 + (arrow.utcnow() - signal_date).total_seconds() // 60 ) return False, False From 4e45abbf139a08c79b2c1adee12cffbceda259c9 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 16:44:45 +0100 Subject: [PATCH 0003/1197] added return false, false --- freqtrade/strategy/interface.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 765e23b0b..a26e8edbd 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -283,6 +283,7 @@ class IStrategy(ABC): if (arrow.utcnow() - signal_date).total_seconds() // 60 >= interval_minutes: logger.warning('Old candle for pair %s. Last tick is %s minutes old', pair, (arrow.utcnow() - signal_date).total_seconds() // 60) + return False, False # Check if dataframe is out of date offset = self.config.get('exchange', {}).get('outdated_offset', 5) From d239e999043fe50b01b1e2a79b074bb3c9de707a Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 16:49:37 +0100 Subject: [PATCH 0004/1197] removed old code from create_trade --- freqtrade/freqtradebot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 02d8b5b56..90f2c0c02 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -393,7 +393,6 @@ class FreqtradeBot: dataframe = self.dataprovider.ohlcv(pair, self.strategy.ticker_interval) latest = dataframe.iloc[-1] # Check if dataframe is out of date - signal_date = arrow.get(latest['date']) if self.strategy.is_pair_locked(pair): logger.info(f"Pair {pair} is currently locked.") From a85d17327bebfc7432a968464ac9b15f587f2813 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 16:54:27 +0100 Subject: [PATCH 0005/1197] fix --- freqtrade/freqtradebot.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 90f2c0c02..fd4daf7aa 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -390,15 +390,13 @@ class FreqtradeBot: """ logger.debug(f"create_trade for pair {pair}") - dataframe = self.dataprovider.ohlcv(pair, self.strategy.ticker_interval) - latest = dataframe.iloc[-1] # Check if dataframe is out of date - if self.strategy.is_pair_locked(pair): logger.info(f"Pair {pair} is currently locked.") return False # running get_signal on historical data fetched + dataframe = self.dataprovider.ohlcv(pair, self.strategy.ticker_interval) (buy, sell) = self.strategy.get_signal(pair, self.strategy.ticker_interval, dataframe) if buy and not sell: From a82cdf0add7f0fca9f0a3a4d32b5510aa4b5b461 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 17:04:51 +0100 Subject: [PATCH 0006/1197] fixed test --- freqtrade/strategy/interface.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index a26e8edbd..47bdf781d 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -282,7 +282,7 @@ class IStrategy(ABC): interval_minutes = timeframe_to_minutes(interval) if (arrow.utcnow() - signal_date).total_seconds() // 60 >= interval_minutes: logger.warning('Old candle for pair %s. Last tick is %s minutes old', - pair, (arrow.utcnow() - signal_date).total_seconds() // 60) + pair, int(arrow.utcnow() - signal_date).total_seconds() // 60) return False, False # Check if dataframe is out of date @@ -291,7 +291,7 @@ class IStrategy(ABC): logger.warning( 'Outdated history for pair %s. Last tick is %s minutes old', pair, - (arrow.utcnow() - signal_date).total_seconds() // 60 + int((arrow.utcnow() - signal_date).total_seconds() // 60) ) return False, False From d667acb3081132adc7207c4dad396e184ebec544 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 17:10:57 +0100 Subject: [PATCH 0007/1197] fixed typo --- freqtrade/strategy/interface.py | 44 ++++++++++++++++----------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 47bdf781d..781bd7a54 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -282,11 +282,11 @@ class IStrategy(ABC): interval_minutes = timeframe_to_minutes(interval) if (arrow.utcnow() - signal_date).total_seconds() // 60 >= interval_minutes: logger.warning('Old candle for pair %s. Last tick is %s minutes old', - pair, int(arrow.utcnow() - signal_date).total_seconds() // 60) + pair, int((arrow.utcnow() - signal_date).total_seconds() // 60) return False, False # Check if dataframe is out of date - offset = self.config.get('exchange', {}).get('outdated_offset', 5) + offset=self.config.get('exchange', {}).get('outdated_offset', 5) if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))): logger.warning( 'Outdated history for pair %s. Last tick is %s minutes old', @@ -295,7 +295,7 @@ class IStrategy(ABC): ) return False, False - (buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1 + (buy, sell)=latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1 logger.debug( 'trigger: %s (pair=%s) buy=%s sell=%s', latest['date'], @@ -306,8 +306,8 @@ class IStrategy(ABC): return buy, sell def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, - sell: bool, low: float = None, high: float = None, - force_stoploss: float = 0) -> SellCheckTuple: + sell: bool, low: float=None, high: float=None, + force_stoploss: float=0) -> SellCheckTuple: """ This function evaluates if one of the conditions required to trigger a sell has been reached, which can either be a stop-loss, ROI or sell-signal. @@ -317,12 +317,12 @@ class IStrategy(ABC): :return: True if trade should be sold, False otherwise """ # Set current rate to low for backtesting sell - current_rate = low or rate - current_profit = trade.calc_profit_ratio(current_rate) + current_rate=low or rate + current_profit=trade.calc_profit_ratio(current_rate) trade.adjust_min_max_rates(high or current_rate) - stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, + stoplossflag=self.stop_loss_reached(current_rate=current_rate, trade=trade, current_time=date, current_profit=current_profit, force_stoploss=force_stoploss, high=high) @@ -332,9 +332,9 @@ class IStrategy(ABC): return stoplossflag # Set current rate to high for backtesting sell - current_rate = high or rate - current_profit = trade.calc_profit_ratio(current_rate) - config_ask_strategy = self.config.get('ask_strategy', {}) + current_rate=high or 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): # This one is noisy, commented out @@ -366,29 +366,29 @@ class IStrategy(ABC): def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime, current_profit: float, - force_stoploss: float, high: float = None) -> SellCheckTuple: + force_stoploss: float, high: float=None) -> SellCheckTuple: """ Based on current profit of the trade and configured (trailing) stoploss, decides to sell or not :param current_profit: current profit as ratio """ - stop_loss_value = force_stoploss if force_stoploss else self.stoploss + stop_loss_value=force_stoploss if force_stoploss else self.stoploss # Initiate stoploss with open_rate. Does nothing if stoploss is already set. trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) if self.trailing_stop: # trailing stoploss handling - sl_offset = self.trailing_stop_positive_offset + 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_ratio(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): # Specific handling for trailing_stop_positive if self.trailing_stop_positive is not None and high_profit > sl_offset: - stop_loss_value = self.trailing_stop_positive + stop_loss_value=self.trailing_stop_positive logger.debug(f"{trade.pair} - Using positive stoploss: {stop_loss_value} " f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%") @@ -401,11 +401,11 @@ class IStrategy(ABC): (trade.stop_loss >= current_rate) and (not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])): - sell_type = SellType.STOP_LOSS + sell_type=SellType.STOP_LOSS # If initial stoploss is not the same as current one then it is trailing. if trade.initial_stop_loss != trade.stop_loss: - sell_type = SellType.TRAILING_STOP_LOSS + sell_type=SellType.TRAILING_STOP_LOSS logger.debug( f"{trade.pair} - HIT STOP: current price at {current_rate:.6f}, " f"stoploss is {trade.stop_loss:.6f}, " @@ -425,10 +425,10 @@ class IStrategy(ABC): :return: minimal ROI entry value or None if none proper ROI entry was found. """ # Get highest entry in ROI dict where key <= trade-duration - roi_list = list(filter(lambda x: x <= trade_dur, self.minimal_roi.keys())) + roi_list=list(filter(lambda x: x <= trade_dur, self.minimal_roi.keys())) if not roi_list: return None, None - roi_entry = max(roi_list) + roi_entry=max(roi_list) return roi_entry, self.minimal_roi[roi_entry] def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool: @@ -439,8 +439,8 @@ class IStrategy(ABC): :return: True if bot should sell at current rate """ # 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) + trade_dur=int((current_time.timestamp() - trade.open_date.timestamp()) // 60) + _, roi=self.min_roi_reached_entry(trade_dur) if roi is None: return False else: From 7754742459688e94b258ecf006883e57f0152b78 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 17:10:57 +0100 Subject: [PATCH 0008/1197] fix tests --- freqtrade/strategy/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 47bdf781d..23c16d83e 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -282,7 +282,7 @@ class IStrategy(ABC): interval_minutes = timeframe_to_minutes(interval) if (arrow.utcnow() - signal_date).total_seconds() // 60 >= interval_minutes: logger.warning('Old candle for pair %s. Last tick is %s minutes old', - pair, int(arrow.utcnow() - signal_date).total_seconds() // 60) + pair, int((arrow.utcnow() - signal_date).total_seconds() // 60)) return False, False # Check if dataframe is out of date From 2e679ee2ebee44fee6c445e568f03a5f58902aee Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 17:22:21 +0100 Subject: [PATCH 0009/1197] fixed log message --- freqtrade/strategy/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 23c16d83e..cf205c709 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -281,7 +281,7 @@ class IStrategy(ABC): signal_date = arrow.get(latest['date']) interval_minutes = timeframe_to_minutes(interval) if (arrow.utcnow() - signal_date).total_seconds() // 60 >= interval_minutes: - logger.warning('Old candle for pair %s. Last tick is %s minutes old', + logger.warning('Old candle for pair %s. Last candle is %s minutes old', pair, int((arrow.utcnow() - signal_date).total_seconds() // 60)) return False, False From d25cf1395b11f130877ef4864b12f480cf940ea9 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 17:22:21 +0100 Subject: [PATCH 0010/1197] fixed log message --- freqtrade/strategy/interface.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 23c16d83e..4d82c6c02 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -277,14 +277,6 @@ class IStrategy(ABC): latest = dataframe.iloc[-1] - # Check if dataframe has new candle - signal_date = arrow.get(latest['date']) - interval_minutes = timeframe_to_minutes(interval) - if (arrow.utcnow() - signal_date).total_seconds() // 60 >= interval_minutes: - logger.warning('Old candle for pair %s. Last tick is %s minutes old', - pair, int((arrow.utcnow() - signal_date).total_seconds() // 60)) - return False, False - # Check if dataframe is out of date offset = self.config.get('exchange', {}).get('outdated_offset', 5) if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))): @@ -295,6 +287,14 @@ class IStrategy(ABC): ) return False, False + # Check if dataframe has new candle + signal_date = arrow.get(latest['date']) + interval_minutes = timeframe_to_minutes(interval) + if (arrow.utcnow() - signal_date).total_seconds() // 60 >= interval_minutes: + logger.warning('Old candle for pair %s. Last candle is %s minutes old', + pair, int((arrow.utcnow() - signal_date).total_seconds() // 60)) + return False, False + (buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1 logger.debug( 'trigger: %s (pair=%s) buy=%s sell=%s', From ba596af636b2a5a725cb0f13e4e71d81fa0157ef Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 17:26:57 +0100 Subject: [PATCH 0011/1197] final? --- freqtrade/strategy/interface.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 4d82c6c02..a2a3c4f3a 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -275,8 +275,6 @@ class IStrategy(ABC): logger.warning('Empty dataframe for pair %s', pair) return False, False - latest = dataframe.iloc[-1] - # Check if dataframe is out of date offset = self.config.get('exchange', {}).get('outdated_offset', 5) if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))): @@ -288,6 +286,7 @@ class IStrategy(ABC): return False, False # Check if dataframe has new candle + latest = dataframe.iloc[-1] signal_date = arrow.get(latest['date']) interval_minutes = timeframe_to_minutes(interval) if (arrow.utcnow() - signal_date).total_seconds() // 60 >= interval_minutes: From c442913febf34cf610fa405bbb714667c45582a5 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 17:28:03 +0100 Subject: [PATCH 0012/1197] final --- freqtrade/strategy/interface.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index a2a3c4f3a..081127370 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -275,6 +275,9 @@ class IStrategy(ABC): logger.warning('Empty dataframe for pair %s', pair) return False, False + latest = dataframe.iloc[-1] + signal_date = arrow.get(latest['date']) + # Check if dataframe is out of date offset = self.config.get('exchange', {}).get('outdated_offset', 5) if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))): @@ -286,8 +289,6 @@ class IStrategy(ABC): return False, False # Check if dataframe has new candle - latest = dataframe.iloc[-1] - signal_date = arrow.get(latest['date']) interval_minutes = timeframe_to_minutes(interval) if (arrow.utcnow() - signal_date).total_seconds() // 60 >= interval_minutes: logger.warning('Old candle for pair %s. Last candle is %s minutes old', From 1395f658723dd5d4464026760adc00d701710907 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 17:29:22 +0100 Subject: [PATCH 0013/1197] meh --- freqtrade/strategy/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 081127370..d840e2aea 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -277,6 +277,7 @@ class IStrategy(ABC): latest = dataframe.iloc[-1] signal_date = arrow.get(latest['date']) + interval_minutes = timeframe_to_minutes(interval) # Check if dataframe is out of date offset = self.config.get('exchange', {}).get('outdated_offset', 5) @@ -289,7 +290,6 @@ class IStrategy(ABC): return False, False # Check if dataframe has new candle - interval_minutes = timeframe_to_minutes(interval) if (arrow.utcnow() - signal_date).total_seconds() // 60 >= interval_minutes: logger.warning('Old candle for pair %s. Last candle is %s minutes old', pair, int((arrow.utcnow() - signal_date).total_seconds() // 60)) From d752586b322b43afe652482ce0ddee0b46f0cac4 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Wed, 11 Mar 2020 17:44:03 +0100 Subject: [PATCH 0014/1197] added test --- tests/strategy/test_interface.py | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 86d0738c6..d476b64b0 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -79,6 +79,21 @@ def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ticker_history assert log_has('Empty dataframe for pair xyz', caplog) +def test_get_signal_old_candle(default_conf, mocker, caplog, ticker_history): + caplog.set_level(logging.INFO) + # default_conf defines a 5m interval. we check interval of previous candle + # this is necessary as the last candle is removed (partial candles) by default + oldtime = arrow.utcnow().shift(minutes=-10) + ticks = DataFrame([{'buy': 1, 'date': oldtime}]) + mocker.patch.object( + _STRATEGY, '_analyze_ticker_internal', + return_value=DataFrame(ticks) + ) + assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], + ticker_history) + assert log_has('Old candle for pair xyz. Last candle is 10 minutes old', caplog) + + def test_get_signal_old_dataframe(default_conf, mocker, caplog, ticker_history): caplog.set_level(logging.INFO) # default_conf defines a 5m interval. we check interval * 2 + 5m @@ -198,14 +213,14 @@ def test_min_roi_reached3(default_conf, fee) -> None: strategy = StrategyResolver.load_strategy(default_conf) strategy.minimal_roi = min_roi 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, - exchange='bittrex', - open_rate=1, + 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, + exchange='bittrex', + open_rate=1, ) assert not strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-56).datetime) From 1976aaf13e366da4c9b6008fc3f5ddd169bb6d1c Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Sun, 22 Mar 2020 02:22:06 +0100 Subject: [PATCH 0015/1197] initial push --- docs/utils.md | 10 +++++--- freqtrade/commands/arguments.py | 1 + freqtrade/commands/cli_options.py | 12 +++++++++ freqtrade/commands/hyperopt_commands.py | 32 ++++++++++++++++++++++-- freqtrade/configuration/configuration.py | 6 +++++ tests/commands/test_commands.py | 28 +++++++++++++++++++++ 6 files changed, 84 insertions(+), 5 deletions(-) diff --git a/docs/utils.md b/docs/utils.md index 269b9affd..03924c581 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -426,9 +426,9 @@ usage: freqtrade hyperopt-list [-h] [-v] [--logfile FILE] [-V] [-c PATH] [--max-trades INT] [--min-avg-time FLOAT] [--max-avg-time FLOAT] [--min-avg-profit FLOAT] [--max-avg-profit FLOAT] - [--min-total-profit FLOAT] - [--max-total-profit FLOAT] [--no-color] - [--print-json] [--no-details] + [--min-total-profit FLOAT] [--max-total-profit FLOAT] + [--min-objective FLOAT] [--max-objective FLOAT] + [--no-color] [--print-json] [--no-details] [--export-csv FILE] optional arguments: @@ -447,6 +447,10 @@ optional arguments: Select epochs on above total profit. --max-total-profit FLOAT Select epochs on below total profit. + --min-objective FLOAT + Select epochs on above objective (- is added by default). + --max-objective FLOAT + Select epochs on below objective (- is added by default). --no-color Disable colorization of hyperopt results. May be useful if you are redirecting output to a file. --print-json Print best result detailization in JSON format. diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 8c64c5857..9edd143a6 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -69,6 +69,7 @@ ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_list_min_avg_time", "hyperopt_list_max_avg_time", "hyperopt_list_min_avg_profit", "hyperopt_list_max_avg_profit", "hyperopt_list_min_total_profit", "hyperopt_list_max_total_profit", + "hyperopt_list_min_objective", "hyperopt_list_max_objective", "print_colorized", "print_json", "hyperopt_list_no_details", "export_csv"] diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 5cf1b7fce..1402e64ef 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -484,6 +484,18 @@ AVAILABLE_CLI_OPTIONS = { type=float, metavar='FLOAT', ), + "hyperopt_list_min_objective": Arg( + '--min-objective', + help='Select epochs on above objective.', + type=float, + metavar='FLOAT', + ), + "hyperopt_list_max_objective": Arg( + '--max-objective', + help='Select epochs on below objective.', + type=float, + metavar='FLOAT', + ), "hyperopt_list_no_details": Arg( '--no-details', help='Do not print best epoch details.', diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index 5b2388252..dd9de19e7 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -35,9 +35,16 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: 'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None), 'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None), 'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None), - 'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None) + 'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None), + 'filter_min_objective': config.get('hyperopt_list_min_objective', None), + 'filter_max_objective': config.get('hyperopt_list_max_objective', None) } + if filteroptions['filter_min_objective'] is not None: + filteroptions['filter_min_objective'] = -filteroptions['filter_min_objective'] + if filteroptions['filter_max_objective'] is not None: + filteroptions['filter_max_objective'] = -filteroptions['filter_max_objective'] + trials_file = (config['user_data_dir'] / 'hyperopt_results' / 'hyperopt_results.pickle') @@ -92,9 +99,16 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: 'filter_min_avg_profit': config.get('hyperopt_list_min_avg_profit', None), 'filter_max_avg_profit': config.get('hyperopt_list_max_avg_profit', None), 'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None), - 'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None) + 'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None), + 'filter_min_objective': config.get('hyperopt_list_min_objective', None), + 'filter_max_objective': config.get('hyperopt_list_max_objective', None) } + if filteroptions['filter_min_objective'] is not None: + filteroptions['filter_min_objective'] = -filteroptions['filter_min_objective'] + if filteroptions['filter_max_objective'] is not None: + filteroptions['filter_max_objective'] = -filteroptions['filter_max_objective'] + # Previous evaluations trials = Hyperopt.load_previous_results(trials_file) total_epochs = len(trials) @@ -175,6 +189,20 @@ def _hyperopt_filter_trials(trials: List, filteroptions: dict) -> List: x for x in trials if x['results_metrics']['profit'] < filteroptions['filter_max_total_profit'] ] + if filteroptions['filter_min_objective'] is not None: + trials = [x for x in trials if x['results_metrics']['trade_count'] > 0] + # trials = [x for x in trials if x['loss'] != 20] + trials = [ + x for x in trials + if x['loss'] < filteroptions['filter_min_objective'] + ] + if filteroptions['filter_max_objective'] is not None: + trials = [x for x in trials if x['results_metrics']['trade_count'] > 0] + # trials = [x for x in trials if x['loss'] != 20] + trials = [ + x for x in trials + if x['loss'] > filteroptions['filter_max_objective'] + ] logger.info(f"{len(trials)} " + ("best " if filteroptions['only_best'] else "") + diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index e5515670d..c26610336 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -334,6 +334,12 @@ class Configuration: self._args_to_config(config, argname='hyperopt_list_max_total_profit', logstring='Parameter --max-total-profit detected: {}') + self._args_to_config(config, argname='hyperopt_list_min_objective', + logstring='Parameter --min-objective detected: {}') + + self._args_to_config(config, argname='hyperopt_list_max_objective', + logstring='Parameter --max-objective detected: {}') + self._args_to_config(config, argname='hyperopt_list_no_details', logstring='Parameter --no-details detected: {}') diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 4530cd03d..2825a4679 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -868,6 +868,34 @@ def test_hyperopt_list(mocker, capsys, hyperopt_results): pargs['config'] = None start_hyperopt_list(pargs) captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12"]) + assert all(x not in captured.out + for x in [" 4/12", " 10/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--min-objective", "0.1" + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() + assert all(x in captured.out + for x in [" 10/12"]) + assert all(x not in captured.out + for x in [" 1/12", " 2/12", " 3/12", " 4/12", " 5/12", " 6/12", " 7/12", " 8/12", + " 9/12", " 11/12", " 12/12"]) + args = [ + "hyperopt-list", + "--no-details", + "--max-objective", "0.1" + ] + pargs = get_args(args) + pargs['config'] = None + start_hyperopt_list(pargs) + captured = capsys.readouterr() assert all(x in captured.out for x in [" 1/12", " 2/12", " 3/12", " 5/12", " 6/12", " 7/12", " 8/12", " 9/12", " 11/12"]) From bf96ef08e0b7bf5c9d47a297f386f64d905a90be Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Sun, 22 Mar 2020 09:39:38 +0100 Subject: [PATCH 0016/1197] added # flake8: noqa C901 --- freqtrade/commands/hyperopt_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index dd9de19e7..c5cf9a969 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -10,7 +10,7 @@ from freqtrade.state import RunMode logger = logging.getLogger(__name__) - +# flake8: noqa C901 def start_hyperopt_list(args: Dict[str, Any]) -> None: """ List hyperopt epochs previously evaluated From 7143cac64f7c9d3ccd5f14d2ae689016157dcee3 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Mon, 23 Mar 2020 09:41:01 +0100 Subject: [PATCH 0017/1197] fixed wording of all in cli_options --- freqtrade/commands/cli_options.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 1402e64ef..e1927a901 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -450,49 +450,49 @@ AVAILABLE_CLI_OPTIONS = { ), "hyperopt_list_min_avg_time": Arg( '--min-avg-time', - help='Select epochs on above average time.', + help='Select epochs above average time.', type=float, metavar='FLOAT', ), "hyperopt_list_max_avg_time": Arg( '--max-avg-time', - help='Select epochs on under average time.', + help='Select epochs under average time.', type=float, metavar='FLOAT', ), "hyperopt_list_min_avg_profit": Arg( '--min-avg-profit', - help='Select epochs on above average profit.', + help='Select epochs above average profit.', type=float, metavar='FLOAT', ), "hyperopt_list_max_avg_profit": Arg( '--max-avg-profit', - help='Select epochs on below average profit.', + help='Select epochs below average profit.', type=float, metavar='FLOAT', ), "hyperopt_list_min_total_profit": Arg( '--min-total-profit', - help='Select epochs on above total profit.', + help='Select epochs above total profit.', type=float, metavar='FLOAT', ), "hyperopt_list_max_total_profit": Arg( '--max-total-profit', - help='Select epochs on below total profit.', + help='Select epochs below total profit.', type=float, metavar='FLOAT', ), "hyperopt_list_min_objective": Arg( '--min-objective', - help='Select epochs on above objective.', + help='Select epochs above objective.', type=float, metavar='FLOAT', ), "hyperopt_list_max_objective": Arg( '--max-objective', - help='Select epochs on below objective.', + help='Select epochs below objective.', type=float, metavar='FLOAT', ), From 0a87fe76a363db2056aee42fed2d3dd4902b48fa Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Mon, 23 Mar 2020 11:17:56 +0100 Subject: [PATCH 0018/1197] unified language --- freqtrade/commands/cli_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index e1927a901..47e7187a8 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -456,7 +456,7 @@ AVAILABLE_CLI_OPTIONS = { ), "hyperopt_list_max_avg_time": Arg( '--max-avg-time', - help='Select epochs under average time.', + help='Select epochs below average time.', type=float, metavar='FLOAT', ), From ef4426a65c08f767314a1a789d3b06c7b9a98072 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Fri, 27 Mar 2020 03:01:51 +0100 Subject: [PATCH 0019/1197] added comma --- freqtrade/commands/hyperopt_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index c5cf9a969..33abb7823 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -37,7 +37,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: 'filter_min_total_profit': config.get('hyperopt_list_min_total_profit', None), 'filter_max_total_profit': config.get('hyperopt_list_max_total_profit', None), 'filter_min_objective': config.get('hyperopt_list_min_objective', None), - 'filter_max_objective': config.get('hyperopt_list_max_objective', None) + 'filter_max_objective': config.get('hyperopt_list_max_objective', None), } if filteroptions['filter_min_objective'] is not None: From 2fb3d94938e8d3bbe4759b8ef670f40e4c3356b0 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Sat, 22 Feb 2020 15:49:18 +0100 Subject: [PATCH 0020/1197] added wins/draws/losses --- freqtrade/optimize/hyperopt.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index fcf50af6a..3f704b33c 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -533,10 +533,14 @@ class Hyperopt: 'total_profit': total_profit, } - def _calculate_results_metrics(self, backtesting_results: DataFrame) -> Dict: + def _calculate_results_metrics(self, backtesting_results: DataFrame) -> Dict: return { 'trade_count': len(backtesting_results.index), + 'wins': len(backtesting_results[backtesting_results.profit_percent > 0]), + 'draws': len(backtesting_results[backtesting_results.profit_percent == 0]), + 'losses': len(backtesting_results[backtesting_results.profit_percent < 0]), 'avg_profit': backtesting_results.profit_percent.mean() * 100.0, + 'median_profit': backtesting_results.profit_percent.median() * 100.0, 'total_profit': backtesting_results.profit_abs.sum(), 'profit': backtesting_results.profit_percent.sum() * 100.0, 'duration': backtesting_results.trade_duration.mean(), @@ -548,7 +552,11 @@ class Hyperopt: """ stake_cur = self.config['stake_currency'] return (f"{results_metrics['trade_count']:6d} trades. " + f"{results_metrics['wins']:6d} wins. " + f"{results_metrics['draws']:6d} draws. " + f"{results_metrics['losses']:6d} losses. " f"Avg profit {results_metrics['avg_profit']: 6.2f}%. " + f"Median profit {results_metrics['median_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} min." From 6147498fd425d151f162e7ed0ebc855df4e1785d Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Sat, 22 Feb 2020 15:51:36 +0100 Subject: [PATCH 0021/1197] fixed indent --- 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 3f704b33c..f2221d6a7 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -533,7 +533,7 @@ class Hyperopt: 'total_profit': total_profit, } - def _calculate_results_metrics(self, backtesting_results: DataFrame) -> Dict: + def _calculate_results_metrics(self, backtesting_results: DataFrame) -> Dict: return { 'trade_count': len(backtesting_results.index), 'wins': len(backtesting_results[backtesting_results.profit_percent > 0]), From 72b088d85f9b4573d35c2d165d1504aeb83caec8 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Mon, 2 Mar 2020 02:50:27 +0100 Subject: [PATCH 0022/1197] added test --- tests/optimize/test_hyperopt.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index b5106be0c..cdbe7f161 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -740,8 +740,10 @@ 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 min.' + 'results_explanation': (' 1 trades. 1 wins. 0 draws. 0 losses. ' + 'Avg profit 2.31%. Median profit 2.31%. Total profit ' + '0.00023300 BTC ( 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, @@ -772,10 +774,14 @@ def test_generate_optimizer(mocker, default_conf) -> None: 'trailing_stop_positive_offset': 0.07}}, 'params_dict': optimizer_param, 'results_metrics': {'avg_profit': 2.3117, + 'draws': 0, 'duration': 100.0, + 'losses': 0, + 'median_profit': 2.3117, 'profit': 2.3117, 'total_profit': 0.000233, - 'trade_count': 1}, + 'trade_count': 1, + 'wins': 1}, 'total_profit': 0.00023300 } From 181b12b3a811bf461f602cfc7b111cc13ebaaaee Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Sat, 22 Feb 2020 15:49:18 +0100 Subject: [PATCH 0023/1197] added wins/draws/losses --- 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 f2221d6a7..3f704b33c 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -533,7 +533,7 @@ class Hyperopt: 'total_profit': total_profit, } - def _calculate_results_metrics(self, backtesting_results: DataFrame) -> Dict: + def _calculate_results_metrics(self, backtesting_results: DataFrame) -> Dict: return { 'trade_count': len(backtesting_results.index), 'wins': len(backtesting_results[backtesting_results.profit_percent > 0]), From c9711678fd414e9ff7bb82496fe1ec6c98a9f5a8 Mon Sep 17 00:00:00 2001 From: Yazeed Al Oyoun Date: Sat, 22 Feb 2020 15:51:36 +0100 Subject: [PATCH 0024/1197] fixed indent --- 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 3f704b33c..f2221d6a7 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -533,7 +533,7 @@ class Hyperopt: 'total_profit': total_profit, } - def _calculate_results_metrics(self, backtesting_results: DataFrame) -> Dict: + def _calculate_results_metrics(self, backtesting_results: DataFrame) -> Dict: return { 'trade_count': len(backtesting_results.index), 'wins': len(backtesting_results[backtesting_results.profit_percent > 0]), From bfa55f31c0aa51d6de3864a8dfe001d1aac6aa62 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Wed, 20 May 2020 17:45:27 +0300 Subject: [PATCH 0025/1197] Remove wrong comment --- freqtrade/freqtradebot.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index dd419f541..32662ae09 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -402,7 +402,6 @@ class FreqtradeBot: """ logger.debug(f"create_trade for pair {pair}") - # Check if dataframe is out of date if self.strategy.is_pair_locked(pair): logger.info(f"Pair {pair} is currently locked.") return False From f3824d970bb511d1a230fe6de713f735b81c80c7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 29 May 2020 20:18:38 +0200 Subject: [PATCH 0026/1197] Use dict for symbol_is_pair --- freqtrade/commands/list_commands.py | 2 +- freqtrade/exchange/exchange.py | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index e5131f9b2..bc4bd694f 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -163,7 +163,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: tabular_data.append({'Id': v['id'], 'Symbol': v['symbol'], 'Base': v['base'], 'Quote': v['quote'], 'Active': market_is_active(v), - **({'Is pair': symbol_is_pair(v['symbol'])} + **({'Is pair': symbol_is_pair(v)} if not pairs_only else {})}) if (args.get('print_one_column', False) or diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index af745e8d0..09f700bbb 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -214,7 +214,7 @@ class Exchange: if quote_currencies: markets = {k: v for k, v in markets.items() if v['quote'] in quote_currencies} if pairs_only: - markets = {k: v for k, v in markets.items() if symbol_is_pair(v['symbol'])} + markets = {k: v for k, v in markets.items() if symbol_is_pair(v)} if active_only: markets = {k: v for k, v in markets.items() if market_is_active(v)} return markets @@ -1210,7 +1210,7 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime: return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) -def symbol_is_pair(market_symbol: str, base_currency: str = None, +def symbol_is_pair(market_symbol: Dict[str, Any], base_currency: str = None, quote_currency: str = None) -> bool: """ Check if the market symbol is a pair, i.e. that its symbol consists of the base currency and the @@ -1218,10 +1218,12 @@ def symbol_is_pair(market_symbol: str, base_currency: str = None, it also checks that the symbol contains appropriate base and/or quote currency part before and after the separating character correspondingly. """ - symbol_parts = market_symbol.split('/') + symbol_parts = market_symbol['symbol'].split('/') return (len(symbol_parts) == 2 and - (symbol_parts[0] == base_currency if base_currency else len(symbol_parts[0]) > 0) and - (symbol_parts[1] == quote_currency if quote_currency else len(symbol_parts[1]) > 0)) + (market_symbol.get('base') == base_currency + if base_currency else len(symbol_parts[0]) > 0) and + (market_symbol.get('quote') == quote_currency + if quote_currency else len(symbol_parts[1]) > 0)) def market_is_active(market: Dict) -> bool: From ffa93377b4e66f7182fcfcee305c8635299b106b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Jun 2020 09:34:03 +0200 Subject: [PATCH 0027/1197] Test colorama init again (after the fixes done to progressbar) --- freqtrade/optimize/hyperopt.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 3a28de785..bd80f7069 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -4,26 +4,26 @@ This module contains the hyperopt logic """ +import io import locale import logging import random import warnings -from math import ceil from collections import OrderedDict +from math import ceil from operator import itemgetter from pathlib import Path from pprint import pprint from typing import Any, Dict, List, Optional +import progressbar import rapidjson +import tabulate from colorama import Fore, Style +from colorama import init as colorama_init from joblib import (Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects) -from pandas import DataFrame, json_normalize, isna -import progressbar -import tabulate -from os import path -import io +from pandas import DataFrame, isna, json_normalize from freqtrade.data.converter import trim_dataframe from freqtrade.data.history import get_timerange @@ -32,7 +32,8 @@ from freqtrade.misc import plural, round_dict from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 -from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401 +from freqtrade.optimize.hyperopt_loss_interface import \ + IHyperOptLoss # noqa: F401 from freqtrade.resolvers.hyperopt_resolver import (HyperOptLossResolver, HyperOptResolver) @@ -374,7 +375,7 @@ class Hyperopt: return # Verification for overwrite - if path.isfile(csv_file): + if Path(csv_file).is_file(): logger.error(f"CSV file already exists: {csv_file}") return @@ -603,7 +604,7 @@ class Hyperopt: data, timerange = self.backtesting.load_bt_data() preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data) - + colorama_init(autoreset=True) # Trim startup period from analyzed dataframe for pair, df in preprocessed.items(): preprocessed[pair] = trim_dataframe(df, timerange) From d9afef8fe19d1fee53cd3c0421cdd3ffcdb27a91 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Jun 2020 09:37:10 +0200 Subject: [PATCH 0028/1197] Move colorama_init to where it was --- freqtrade/optimize/hyperopt.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index bd80f7069..566ba5de8 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -604,7 +604,7 @@ class Hyperopt: data, timerange = self.backtesting.load_bt_data() preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data) - colorama_init(autoreset=True) + # Trim startup period from analyzed dataframe for pair, df in preprocessed.items(): preprocessed[pair] = trim_dataframe(df, timerange) @@ -629,6 +629,10 @@ class Hyperopt: self.dimensions: List[Dimension] = self.hyperopt_space() self.opt = self.get_optimizer(self.dimensions, config_jobs) + + if self.print_colorized: + colorama_init(autoreset=True) + try: with Parallel(n_jobs=config_jobs) as parallel: jobs = parallel._effective_n_jobs() From f6edb32a33c5ddc6a48c7bd9e3abeb01871a6054 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Jun 2020 09:55:52 +0200 Subject: [PATCH 0029/1197] Run hyperopt with --print-all --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 239576c61..2c6141344 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,7 +88,7 @@ jobs: run: | cp config.json.example config.json freqtrade create-userdir --userdir user_data - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt + freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --print-all - name: Flake8 run: | @@ -150,7 +150,7 @@ jobs: run: | cp config.json.example config.json freqtrade create-userdir --userdir user_data - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt + freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --print-all - name: Flake8 run: | From b2025597aa3f97f9f6a6b14e0402534dc8e2cdcc Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Jun 2020 20:15:48 +0200 Subject: [PATCH 0030/1197] Build-commands should write timeframe instead of ticker interval --- freqtrade/commands/build_config_commands.py | 4 ++-- freqtrade/templates/base_config.json.j2 | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index 87098f53c..0c98b2e55 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -75,8 +75,8 @@ def ask_user_config() -> Dict[str, Any]: }, { "type": "text", - "name": "ticker_interval", - "message": "Please insert your timeframe (ticker interval):", + "name": "timeframe", + "message": "Please insert your desired timeframe (e.g. 5m):", "default": "5m", }, { diff --git a/freqtrade/templates/base_config.json.j2 b/freqtrade/templates/base_config.json.j2 index 6d3174347..e47c32309 100644 --- a/freqtrade/templates/base_config.json.j2 +++ b/freqtrade/templates/base_config.json.j2 @@ -4,7 +4,7 @@ "stake_amount": {{ stake_amount }}, "tradable_balance_ratio": 0.99, "fiat_display_currency": "{{ fiat_display_currency }}", - "ticker_interval": "{{ ticker_interval }}", + "timeframe": "{{ timeframe }}", "dry_run": {{ dry_run | lower }}, "cancel_open_orders_on_exit": false, "unfilledtimeout": { From 009ea0639f90535a0f350311231c217d1664deab Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Jun 2020 20:33:26 +0200 Subject: [PATCH 0031/1197] Exchange some occurances of ticker_interval --- freqtrade/commands/arguments.py | 6 ++--- freqtrade/commands/cli_options.py | 4 +-- freqtrade/commands/list_commands.py | 4 +-- freqtrade/constants.py | 2 ++ tests/conftest.py | 2 +- tests/exchange/test_exchange.py | 8 +++--- tests/optimize/test_backtest_detail.py | 2 +- tests/optimize/test_backtesting.py | 34 +++++++++++++------------- tests/optimize/test_edge_cli.py | 8 +++--- tests/optimize/test_hyperopt.py | 8 +++--- 10 files changed, 40 insertions(+), 38 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 1b7bbfeb5..36e3dedf0 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -15,7 +15,7 @@ ARGS_STRATEGY = ["strategy", "strategy_path"] ARGS_TRADE = ["db_url", "sd_notify", "dry_run"] -ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange", +ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "max_open_trades", "stake_amount", "fee"] ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", @@ -59,10 +59,10 @@ ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchang ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", "trade_source", "export", "exportfilename", - "timerange", "ticker_interval", "no_trades"] + "timerange", "timeframe", "no_trades"] ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url", - "trade_source", "ticker_interval"] + "trade_source", "timeframe"] ARGS_SHOW_TRADES = ["db_url", "trade_ids", "print_json"] diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index ee9208c33..3ed2f81d1 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -110,8 +110,8 @@ AVAILABLE_CLI_OPTIONS = { action='store_true', ), # Optimize common - "ticker_interval": Arg( - '-i', '--ticker-interval', + "timeframe": Arg( + '-i', '--timeframe', '--ticker-interval', help='Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`).', ), "timerange": Arg( diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index e5131f9b2..b29aabe25 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -102,8 +102,8 @@ def start_list_timeframes(args: Dict[str, Any]) -> None: Print ticker intervals (timeframes) available on Exchange """ config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) - # Do not use ticker_interval set in the config - config['ticker_interval'] = None + # Do not use timeframe set in the config + config['timeframe'] = None # Init exchange exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 1984d4866..511c2993d 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -72,6 +72,7 @@ CONF_SCHEMA = { 'properties': { 'max_open_trades': {'type': ['integer', 'number'], 'minimum': -1}, 'ticker_interval': {'type': 'string'}, + 'timeframe': {'type': 'string'}, 'stake_currency': {'type': 'string'}, 'stake_amount': { 'type': ['number', 'string'], @@ -303,6 +304,7 @@ CONF_SCHEMA = { SCHEMA_TRADE_REQUIRED = [ 'exchange', + 'timeframe', 'max_open_trades', 'stake_currency', 'stake_amount', diff --git a/tests/conftest.py b/tests/conftest.py index 971f7a5fa..f62b18f98 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -247,7 +247,7 @@ def default_conf(testdatadir): "stake_currency": "BTC", "stake_amount": 0.001, "fiat_display_currency": "USD", - "ticker_interval": '5m', + "timeframe": '5m', "dry_run": True, "cancel_open_orders_on_exit": False, "minimal_roi": { diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 25aecba5c..6df94f356 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1350,7 +1350,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ # exchange = Exchange(default_conf) await async_ccxt_exception(mocker, default_conf, MagicMock(), "_async_get_candle_history", "fetch_ohlcv", - pair='ABCD/BTC', timeframe=default_conf['ticker_interval']) + pair='ABCD/BTC', timeframe=default_conf['timeframe']) api_mock = MagicMock() with pytest.raises(OperationalException, @@ -1480,7 +1480,7 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na exchange._api_async.fetch_ohlcv = get_mock_coro(ohlcv) sort_mock = mocker.patch('freqtrade.exchange.exchange.sorted', MagicMock(side_effect=sort_data)) # Test the OHLCV data sort - res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval']) + res = await exchange._async_get_candle_history('ETH/BTC', default_conf['timeframe']) assert res[0] == 'ETH/BTC' res_ohlcv = res[2] @@ -1517,9 +1517,9 @@ async def test___async_get_candle_history_sort(default_conf, mocker, exchange_na # Reset sort mock sort_mock = mocker.patch('freqtrade.exchange.sorted', MagicMock(side_effect=sort_data)) # Test the OHLCV data sort - res = await exchange._async_get_candle_history('ETH/BTC', default_conf['ticker_interval']) + res = await exchange._async_get_candle_history('ETH/BTC', default_conf['timeframe']) assert res[0] == 'ETH/BTC' - assert res[1] == default_conf['ticker_interval'] + assert res[1] == default_conf['timeframe'] res_ohlcv = res[2] # Sorted not called again - data is already in order assert sort_mock.call_count == 0 diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index e7bc76c1d..9b3043086 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -360,7 +360,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: """ default_conf["stoploss"] = data.stop_loss default_conf["minimal_roi"] = data.roi - default_conf["ticker_interval"] = tests_timeframe + default_conf["timeframe"] = tests_timeframe default_conf["trailing_stop"] = data.trailing_stop default_conf["trailing_only_offset_is_reached"] = data.trailing_only_offset_is_reached # Only add this to configuration If it's necessary diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index ace82d28b..407604d9c 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -81,7 +81,7 @@ def load_data_test(what, testdatadir): def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None: patch_exchange(mocker) - config['ticker_interval'] = '1m' + config['timeframe'] = '1m' backtesting = Backtesting(config) data = load_data_test(contour, testdatadir) @@ -165,7 +165,7 @@ def test_setup_optimize_configuration_without_arguments(mocker, default_conf, ca assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog) - assert 'ticker_interval' in config + assert 'timeframe' in config assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog) assert 'position_stacking' not in config @@ -189,7 +189,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> '--config', 'config.json', '--strategy', 'DefaultStrategy', '--datadir', '/foo/bar', - '--ticker-interval', '1m', + '--timeframe', '1m', '--enable-position-stacking', '--disable-max-market-positions', '--timerange', ':100', @@ -208,8 +208,8 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> assert config['runmode'] == RunMode.BACKTEST assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog) - assert 'ticker_interval' in config - assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', + assert 'timeframe' in config + assert log_has('Parameter -i/--timeframe detected ... Using timeframe: 1m ...', caplog) assert 'position_stacking' in config @@ -288,7 +288,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None: def test_backtesting_init_no_ticker_interval(mocker, default_conf, caplog) -> None: patch_exchange(mocker) - del default_conf['ticker_interval'] + del default_conf['timeframe'] default_conf['strategy_list'] = ['DefaultStrategy', 'SampleStrategy'] @@ -337,7 +337,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) - default_conf['ticker_interval'] = '1m' + default_conf['timeframe'] = '1m' default_conf['datadir'] = testdatadir default_conf['export'] = None default_conf['timerange'] = '-1510694220' @@ -367,7 +367,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) - default_conf['ticker_interval'] = "1m" + default_conf['timeframe'] = "1m" default_conf['datadir'] = testdatadir default_conf['export'] = None default_conf['timerange'] = '20180101-20180102' @@ -387,7 +387,7 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=[])) - default_conf['ticker_interval'] = "1m" + default_conf['timeframe'] = "1m" default_conf['datadir'] = testdatadir default_conf['export'] = None default_conf['timerange'] = '20180101-20180102' @@ -534,7 +534,7 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC', datadir=testdatadir) - default_conf['ticker_interval'] = '1m' + default_conf['timeframe'] = '1m' backtesting = Backtesting(default_conf) backtesting.strategy.advise_buy = _trend_alternate # Override backtesting.strategy.advise_sell = _trend_alternate # Override @@ -573,7 +573,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) # Remove data for one pair from the beginning of the data data[pair] = data[pair][tres:].reset_index() - default_conf['ticker_interval'] = '5m' + default_conf['timeframe'] = '5m' backtesting = Backtesting(default_conf) backtesting.strategy.advise_buy = _trend_alternate_hold # Override @@ -623,7 +623,7 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): '--config', 'config.json', '--strategy', 'DefaultStrategy', '--datadir', str(testdatadir), - '--ticker-interval', '1m', + '--timeframe', '1m', '--timerange', '1510694220-1510700340', '--enable-position-stacking', '--disable-max-market-positions' @@ -632,7 +632,7 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): start_backtesting(args) # check the logs, that will contain the backtest result exists = [ - 'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', + 'Parameter -i/--timeframe detected ... Using timeframe: 1m ...', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...', 'Parameter --timerange detected: 1510694220-1510700340 ...', f'Using data directory: {testdatadir} ...', @@ -676,7 +676,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): '--config', 'config.json', '--datadir', str(testdatadir), '--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'), - '--ticker-interval', '1m', + '--timeframe', '1m', '--timerange', '1510694220-1510700340', '--enable-position-stacking', '--disable-max-market-positions', @@ -695,7 +695,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): # check the logs, that will contain the backtest result exists = [ - 'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', + 'Parameter -i/--timeframe detected ... Using timeframe: 1m ...', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...', 'Parameter --timerange detected: 1510694220-1510700340 ...', f'Using data directory: {testdatadir} ...', @@ -765,7 +765,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat '--config', 'config.json', '--datadir', str(testdatadir), '--strategy-path', str(Path(__file__).parents[1] / 'strategy/strats'), - '--ticker-interval', '1m', + '--timeframe', '1m', '--timerange', '1510694220-1510700340', '--enable-position-stacking', '--disable-max-market-positions', @@ -778,7 +778,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat # check the logs, that will contain the backtest result exists = [ - 'Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', + 'Parameter -i/--timeframe detected ... Using timeframe: 1m ...', 'Ignoring max_open_trades (--disable-max-market-positions was used) ...', 'Parameter --timerange detected: 1510694220-1510700340 ...', f'Using data directory: {testdatadir} ...', diff --git a/tests/optimize/test_edge_cli.py b/tests/optimize/test_edge_cli.py index a5e468542..acec51f66 100644 --- a/tests/optimize/test_edge_cli.py +++ b/tests/optimize/test_edge_cli.py @@ -29,7 +29,7 @@ def test_setup_optimize_configuration_without_arguments(mocker, default_conf, ca assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog) - assert 'ticker_interval' in config + assert 'timeframe' in config assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog) assert 'timerange' not in config @@ -48,7 +48,7 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N '--config', 'config.json', '--strategy', 'DefaultStrategy', '--datadir', '/foo/bar', - '--ticker-interval', '1m', + '--timeframe', '1m', '--timerange', ':100', '--stoplosses=-0.01,-0.10,-0.001' ] @@ -62,8 +62,8 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N assert 'datadir' in config assert config['runmode'] == RunMode.EDGE assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog) - assert 'ticker_interval' in config - assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', + assert 'timeframe' in config + assert log_has('Parameter -i/--timeframe detected ... Using timeframe: 1m ...', caplog) assert 'timerange' in config diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 90e047954..f5b3b8909 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -94,7 +94,7 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog) - assert 'ticker_interval' in config + assert 'timeframe' in config assert not log_has_re('Parameter -i/--ticker-interval detected .*', caplog) assert 'position_stacking' not in config @@ -136,8 +136,8 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo assert config['runmode'] == RunMode.HYPEROPT assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog) - assert 'ticker_interval' in config - assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', + assert 'timeframe' in config + assert log_has('Parameter -i/--ticker-interval detected ... Using timeframe: 1m ...', caplog) assert 'position_stacking' in config @@ -544,7 +544,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None: ) patch_exchange(mocker) # Co-test loading timeframe from strategy - del default_conf['ticker_interval'] + del default_conf['timeframe'] default_conf.update({'config': 'config.json.example', 'hyperopt': 'DefaultHyperOpt', 'epochs': 1, From 898def7f6ca3a58163ad7811dca75c547bb13730 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Jun 2020 20:39:01 +0200 Subject: [PATCH 0032/1197] Remove ticker_interval from exchange --- freqtrade/exchange/exchange.py | 2 +- tests/exchange/test_exchange.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 7ef0d7750..3f1cdc568 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -115,7 +115,7 @@ class Exchange: if validate: # Check if timeframe is available - self.validate_timeframes(config.get('ticker_interval')) + self.validate_timeframes(config.get('timeframe')) # Initial markets load self._load_markets() diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 6df94f356..0d924882f 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -578,7 +578,7 @@ def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog): ('5m'), ("1m"), ("15m"), ("1h") ]) def test_validate_timeframes(default_conf, mocker, timeframe): - default_conf["ticker_interval"] = timeframe + default_conf["timeframe"] = timeframe api_mock = MagicMock() id_mock = PropertyMock(return_value='test_exchange') type(api_mock).id = id_mock @@ -596,7 +596,7 @@ def test_validate_timeframes(default_conf, mocker, timeframe): def test_validate_timeframes_failed(default_conf, mocker): - default_conf["ticker_interval"] = "3m" + default_conf["timeframe"] = "3m" api_mock = MagicMock() id_mock = PropertyMock(return_value='test_exchange') type(api_mock).id = id_mock @@ -613,7 +613,7 @@ def test_validate_timeframes_failed(default_conf, mocker): with pytest.raises(OperationalException, match=r"Invalid timeframe '3m'. This exchange supports.*"): Exchange(default_conf) - default_conf["ticker_interval"] = "15s" + default_conf["timeframe"] = "15s" with pytest.raises(OperationalException, match=r"Timeframes < 1m are currently not supported by Freqtrade."): @@ -621,7 +621,7 @@ def test_validate_timeframes_failed(default_conf, mocker): def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker): - default_conf["ticker_interval"] = "3m" + default_conf["timeframe"] = "3m" api_mock = MagicMock() id_mock = PropertyMock(return_value='test_exchange') type(api_mock).id = id_mock @@ -641,7 +641,7 @@ def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker): def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker): - default_conf["ticker_interval"] = "3m" + default_conf["timeframe"] = "3m" api_mock = MagicMock() id_mock = PropertyMock(return_value='test_exchange') type(api_mock).id = id_mock @@ -662,7 +662,7 @@ def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker): def test_validate_timeframes_not_in_config(default_conf, mocker): - del default_conf["ticker_interval"] + del default_conf["timeframe"] api_mock = MagicMock() id_mock = PropertyMock(return_value='test_exchange') type(api_mock).id = id_mock From b2c241e607317baf147806824c838952632e5722 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Jun 2020 20:43:20 +0200 Subject: [PATCH 0033/1197] Replace ticker_interval in all rpc files --- freqtrade/rpc/rpc.py | 3 ++- freqtrade/rpc/rpc_manager.py | 4 ++-- freqtrade/rpc/telegram.py | 2 +- tests/rpc/test_rpc.py | 2 ++ tests/rpc/test_rpc_apiserver.py | 2 ++ 5 files changed, 9 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 6addf18ba..e7d4a3be8 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -101,7 +101,8 @@ class RPC: 'trailing_stop_positive': config.get('trailing_stop_positive'), 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'), 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'), - 'ticker_interval': config['ticker_interval'], + 'ticker_interval': config['timeframe'], # DEPRECATED + 'timeframe': config['timeframe'], 'exchange': config['exchange']['name'], 'strategy': config['strategy'], 'forcebuy_enabled': config.get('forcebuy_enable', False), diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 670275991..2cb44fec8 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -72,7 +72,7 @@ class RPCManager: minimal_roi = config['minimal_roi'] stoploss = config['stoploss'] trailing_stop = config['trailing_stop'] - ticker_interval = config['ticker_interval'] + timeframe = config['timeframe'] exchange_name = config['exchange']['name'] strategy_name = config.get('strategy', '') self.send_msg({ @@ -81,7 +81,7 @@ class RPCManager: f'*Stake per trade:* `{stake_amount} {stake_currency}`\n' f'*Minimum ROI:* `{minimal_roi}`\n' f'*{"Trailing " if trailing_stop else ""}Stoploss:* `{stoploss}`\n' - f'*Ticker Interval:* `{ticker_interval}`\n' + f'*Timeframe:* `{timeframe}`\n' f'*Strategy:* `{strategy_name}`' }) self.send_msg({ diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 7eef18dfc..17354cdb0 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -639,7 +639,7 @@ class Telegram(RPC): f"*Max open Trades:* `{val['max_open_trades']}`\n" f"*Minimum ROI:* `{val['minimal_roi']}`\n" f"{sl_info}" - f"*Ticker Interval:* `{val['ticker_interval']}`\n" + f"*Timeframe:* `{val['timeframe']}`\n" f"*Strategy:* `{val['strategy']}`\n" f"*Current state:* `{val['state']}`" ) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 7b7824310..4950c4ea7 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -66,6 +66,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'max_rate': ANY, 'strategy': ANY, 'ticker_interval': ANY, + 'timeframe': ANY, 'open_order_id': ANY, 'close_date': None, 'close_date_hum': None, @@ -120,6 +121,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'max_rate': ANY, 'strategy': ANY, 'ticker_interval': ANY, + 'timeframe': ANY, 'open_order_id': ANY, 'close_date': None, 'close_date_hum': None, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index c7f937b0c..bccd12e18 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -544,6 +544,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'sell_order_status': None, 'strategy': 'DefaultStrategy', 'ticker_interval': 5, + 'timeframe': 5, 'exchange': 'bittrex', }] @@ -659,6 +660,7 @@ def test_api_forcebuy(botclient, mocker, fee): 'sell_order_status': None, 'strategy': None, 'ticker_interval': None, + 'timeframe': None, 'exchange': 'bittrex', } From 950f358982b881d0f8f7b5d4e665506380fe8f90 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Jun 2020 20:47:27 +0200 Subject: [PATCH 0034/1197] Replace occurances in test files --- tests/commands/test_build_config.py | 4 +- tests/config_test_comments.json | 2 +- tests/data/test_dataprovider.py | 40 +++++++++---------- tests/data/test_history.py | 6 +-- tests/optimize/test_hyperopt.py | 4 +- tests/strategy/test_interface.py | 12 +++--- tests/strategy/test_strategy.py | 8 ++-- tests/test_arguments.py | 2 +- tests/test_configuration.py | 2 +- tests/test_freqtradebot.py | 2 +- tests/test_main.py | 12 +++--- tests/test_persistence.py | 2 + tests/test_plotting.py | 2 +- tests/testdata/backtest-result_test copy.json | 7 ++++ 14 files changed, 57 insertions(+), 48 deletions(-) create mode 100644 tests/testdata/backtest-result_test copy.json diff --git a/tests/commands/test_build_config.py b/tests/commands/test_build_config.py index d4ebe1de2..69b277e3b 100644 --- a/tests/commands/test_build_config.py +++ b/tests/commands/test_build_config.py @@ -44,7 +44,7 @@ def test_start_new_config(mocker, caplog, exchange): 'stake_currency': 'USDT', 'stake_amount': 100, 'fiat_display_currency': 'EUR', - 'ticker_interval': '15m', + 'timeframe': '15m', 'dry_run': True, 'exchange_name': exchange, 'exchange_key': 'sampleKey', @@ -68,7 +68,7 @@ def test_start_new_config(mocker, caplog, exchange): result = rapidjson.loads(wt_mock.call_args_list[0][0][0], parse_mode=rapidjson.PM_COMMENTS | rapidjson.PM_TRAILING_COMMAS) assert result['exchange']['name'] == exchange - assert result['ticker_interval'] == '15m' + assert result['timeframe'] == '15m' def test_start_new_config_exists(mocker, caplog): diff --git a/tests/config_test_comments.json b/tests/config_test_comments.json index 8f41b08fa..b224d7602 100644 --- a/tests/config_test_comments.json +++ b/tests/config_test_comments.json @@ -9,7 +9,7 @@ "fiat_display_currency": "USD", // C++-style comment "amount_reserve_percent" : 0.05, // And more, tabs before this comment "dry_run": false, - "ticker_interval": "5m", + "timeframe": "5m", "trailing_stop": false, "trailing_stop_positive": 0.005, "trailing_stop_positive_offset": 0.0051, diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index c2d6e82f1..718060c5e 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -12,7 +12,7 @@ from tests.conftest import get_patched_exchange def test_ohlcv(mocker, default_conf, ohlcv_history): default_conf["runmode"] = RunMode.DRY_RUN - timeframe = default_conf["ticker_interval"] + timeframe = default_conf["timeframe"] exchange = get_patched_exchange(mocker, default_conf) exchange._klines[("XRP/BTC", timeframe)] = ohlcv_history exchange._klines[("UNITTEST/BTC", timeframe)] = ohlcv_history @@ -53,47 +53,47 @@ def test_historic_ohlcv(mocker, default_conf, ohlcv_history): def test_get_pair_dataframe(mocker, default_conf, ohlcv_history): default_conf["runmode"] = RunMode.DRY_RUN - ticker_interval = default_conf["ticker_interval"] + timeframe = default_conf["timeframe"] exchange = get_patched_exchange(mocker, default_conf) - exchange._klines[("XRP/BTC", ticker_interval)] = ohlcv_history - exchange._klines[("UNITTEST/BTC", ticker_interval)] = ohlcv_history + exchange._klines[("XRP/BTC", timeframe)] = ohlcv_history + exchange._klines[("UNITTEST/BTC", timeframe)] = ohlcv_history dp = DataProvider(default_conf, exchange) assert dp.runmode == RunMode.DRY_RUN - assert ohlcv_history.equals(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval)) - assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame) - assert dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval) is not ohlcv_history - assert not dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval).empty - assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty + assert ohlcv_history.equals(dp.get_pair_dataframe("UNITTEST/BTC", timeframe)) + assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", timeframe), DataFrame) + assert dp.get_pair_dataframe("UNITTEST/BTC", timeframe) is not ohlcv_history + assert not dp.get_pair_dataframe("UNITTEST/BTC", timeframe).empty + assert dp.get_pair_dataframe("NONESENSE/AAA", timeframe).empty # Test with and without parameter - assert dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval)\ + assert dp.get_pair_dataframe("UNITTEST/BTC", timeframe)\ .equals(dp.get_pair_dataframe("UNITTEST/BTC")) default_conf["runmode"] = RunMode.LIVE dp = DataProvider(default_conf, exchange) assert dp.runmode == RunMode.LIVE - assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame) - assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty + assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", timeframe), DataFrame) + assert dp.get_pair_dataframe("NONESENSE/AAA", timeframe).empty historymock = MagicMock(return_value=ohlcv_history) mocker.patch("freqtrade.data.dataprovider.load_pair_history", historymock) default_conf["runmode"] = RunMode.BACKTEST dp = DataProvider(default_conf, exchange) assert dp.runmode == RunMode.BACKTEST - assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", ticker_interval), DataFrame) - # assert dp.get_pair_dataframe("NONESENSE/AAA", ticker_interval).empty + assert isinstance(dp.get_pair_dataframe("UNITTEST/BTC", timeframe), DataFrame) + # assert dp.get_pair_dataframe("NONESENSE/AAA", timeframe).empty def test_available_pairs(mocker, default_conf, ohlcv_history): exchange = get_patched_exchange(mocker, default_conf) - ticker_interval = default_conf["ticker_interval"] - exchange._klines[("XRP/BTC", ticker_interval)] = ohlcv_history - exchange._klines[("UNITTEST/BTC", ticker_interval)] = ohlcv_history + timeframe = default_conf["timeframe"] + exchange._klines[("XRP/BTC", timeframe)] = ohlcv_history + exchange._klines[("UNITTEST/BTC", timeframe)] = ohlcv_history dp = DataProvider(default_conf, exchange) assert len(dp.available_pairs) == 2 - assert dp.available_pairs == [("XRP/BTC", ticker_interval), ("UNITTEST/BTC", ticker_interval), ] + assert dp.available_pairs == [("XRP/BTC", timeframe), ("UNITTEST/BTC", timeframe), ] def test_refresh(mocker, default_conf, ohlcv_history): @@ -101,8 +101,8 @@ def test_refresh(mocker, default_conf, ohlcv_history): mocker.patch("freqtrade.exchange.Exchange.refresh_latest_ohlcv", refresh_mock) exchange = get_patched_exchange(mocker, default_conf, id="binance") - ticker_interval = default_conf["ticker_interval"] - pairs = [("XRP/BTC", ticker_interval), ("UNITTEST/BTC", ticker_interval)] + timeframe = default_conf["timeframe"] + pairs = [("XRP/BTC", timeframe), ("UNITTEST/BTC", timeframe)] pairs_non_trad = [("ETH/USDT", ticker_interval), ("BTC/TUSD", "1h")] diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 6fd4d9569..c52163bbc 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -354,7 +354,7 @@ def test_init(default_conf, mocker) -> None: assert {} == load_data( datadir=Path(''), pairs=[], - timeframe=default_conf['ticker_interval'] + timeframe=default_conf['timeframe'] ) @@ -363,13 +363,13 @@ def test_init_with_refresh(default_conf, mocker) -> None: refresh_data( datadir=Path(''), pairs=[], - timeframe=default_conf['ticker_interval'], + timeframe=default_conf['timeframe'], exchange=exchange ) assert {} == load_data( datadir=Path(''), pairs=[], - timeframe=default_conf['ticker_interval'] + timeframe=default_conf['timeframe'] ) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index f5b3b8909..4f5b3983a 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -117,7 +117,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo '--config', 'config.json', '--hyperopt', 'DefaultHyperOpt', '--datadir', '/foo/bar', - '--ticker-interval', '1m', + '--timeframe', '1m', '--timerange', ':100', '--enable-position-stacking', '--disable-max-market-positions', @@ -137,7 +137,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog) assert 'timeframe' in config - assert log_has('Parameter -i/--ticker-interval detected ... Using timeframe: 1m ...', + assert log_has('Parameter -i/--timeframe detected ... Using timeframe: 1m ...', caplog) assert 'position_stacking' in config diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index e5539099b..59b4d5902 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -54,12 +54,12 @@ def test_returns_latest_signal(mocker, default_conf, ohlcv_history): def test_get_signal_empty(default_conf, mocker, caplog): - assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'], + assert (False, False) == _STRATEGY.get_signal('foo', default_conf['timeframe'], DataFrame()) assert log_has('Empty candle (OHLCV) data for pair foo', caplog) caplog.clear() - assert (False, False) == _STRATEGY.get_signal('bar', default_conf['ticker_interval'], + assert (False, False) == _STRATEGY.get_signal('bar', default_conf['timeframe'], []) assert log_has('Empty candle (OHLCV) data for pair bar', caplog) @@ -70,7 +70,7 @@ def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ohlcv_his _STRATEGY, '_analyze_ticker_internal', side_effect=ValueError('xyz') ) - assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'], + assert (False, False) == _STRATEGY.get_signal('foo', default_conf['timeframe'], ohlcv_history) assert log_has_re(r'Strategy caused the following exception: xyz.*', caplog) @@ -83,7 +83,7 @@ def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ohlcv_history) ) mocker.patch.object(_STRATEGY, 'assert_df') - assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], + assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe'], ohlcv_history) assert log_has('Empty dataframe for pair xyz', caplog) @@ -104,7 +104,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): return_value=mocked_history ) mocker.patch.object(_STRATEGY, 'assert_df') - assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], + assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe'], ohlcv_history) assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog) @@ -124,7 +124,7 @@ def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history): _STRATEGY, 'assert_df', side_effect=StrategyError('Dataframe returned...') ) - assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], + assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe'], ohlcv_history) assert log_has('Unable to analyze candle (OHLCV) data for pair xyz: Dataframe returned...', caplog) diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py index 13ca68bf0..5cb59cad0 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -106,7 +106,7 @@ def test_strategy(result, default_conf): assert default_conf['stoploss'] == -0.10 assert strategy.ticker_interval == '5m' - assert default_conf['ticker_interval'] == '5m' + assert default_conf['timeframe'] == '5m' df_indicators = strategy.advise_indicators(result, metadata=metadata) assert 'adx' in df_indicators @@ -176,19 +176,19 @@ def test_strategy_override_trailing_stop_positive(caplog, default_conf): caplog) -def test_strategy_override_ticker_interval(caplog, default_conf): +def test_strategy_override_timeframe(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ 'strategy': 'DefaultStrategy', - 'ticker_interval': 60, + 'timeframe': 60, 'stake_currency': 'ETH' }) strategy = StrategyResolver.load_strategy(default_conf) assert strategy.ticker_interval == 60 assert strategy.stake_currency == 'ETH' - assert log_has("Override strategy 'ticker_interval' with value in config file: 60.", + assert log_has("Override strategy 'timeframe' with value in config file: 60.", caplog) diff --git a/tests/test_arguments.py b/tests/test_arguments.py index 0052a61d0..457683598 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -131,7 +131,7 @@ def test_parse_args_backtesting_custom() -> None: assert call_args["verbosity"] == 0 assert call_args["command"] == 'backtesting' assert call_args["func"] is not None - assert call_args["ticker_interval"] == '1m' + assert call_args["timeframe"] == '1m' assert type(call_args["strategy_list"]) is list assert len(call_args["strategy_list"]) == 2 diff --git a/tests/test_configuration.py b/tests/test_configuration.py index edcbe4516..05074c258 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -87,7 +87,7 @@ def test_load_config_file_error_range(default_conf, mocker, caplog) -> None: assert isinstance(x, str) assert (x == '{"max_open_trades": 1, "stake_currency": "BTC", ' '"stake_amount": .001, "fiat_display_currency": "USD", ' - '"ticker_interval": "5m", "dry_run": true, ') + '"timeframe": "5m", "dry_run": true, ') def test__args_to_config(caplog): diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5e951b585..442917bb6 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -924,7 +924,7 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None: assert refresh_mock.call_count == 1 assert ("BTC/ETH", "1m") in refresh_mock.call_args[0][0] assert ("ETH/USDT", "1h") in refresh_mock.call_args[0][0] - assert ("ETH/BTC", default_conf["ticker_interval"]) in refresh_mock.call_args[0][0] + assert ("ETH/BTC", default_conf["timeframe"]) in refresh_mock.call_args[0][0] @pytest.mark.parametrize("side,ask,bid,last,last_ab,expected", [ diff --git a/tests/test_main.py b/tests/test_main.py index 11d0ede3a..5700df1ae 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -35,12 +35,12 @@ def test_parse_args_backtesting(mocker) -> None: main(['backtesting']) assert backtesting_mock.call_count == 1 call_args = backtesting_mock.call_args[0][0] - assert call_args["config"] == ['config.json'] - assert call_args["verbosity"] == 0 - assert call_args["command"] == 'backtesting' - assert call_args["func"] is not None - assert callable(call_args["func"]) - assert call_args["ticker_interval"] is None + assert call_args['config'] == ['config.json'] + assert call_args['verbosity'] == 0 + assert call_args['command'] == 'backtesting' + assert call_args['func'] is not None + assert callable(call_args['func']) + assert call_args['timeframe'] is None def test_main_start_hyperopt(mocker) -> None: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 3b1041fd8..c15936cf6 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -772,6 +772,7 @@ def test_to_json(default_conf, fee): 'max_rate': None, 'strategy': None, 'ticker_interval': None, + 'timeframe': None, 'exchange': 'bittrex', } @@ -829,6 +830,7 @@ def test_to_json(default_conf, fee): 'sell_order_status': None, 'strategy': None, 'ticker_interval': None, + 'timeframe': None, 'exchange': 'bittrex', } diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 5bb113784..af872d0c1 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -47,7 +47,7 @@ def generate_empty_figure(): def test_init_plotscript(default_conf, mocker, testdatadir): default_conf['timerange'] = "20180110-20180112" default_conf['trade_source'] = "file" - default_conf['ticker_interval'] = "5m" + default_conf['timeframe'] = "5m" default_conf["datadir"] = testdatadir default_conf['exportfilename'] = testdatadir / "backtest-result_test.json" ret = init_plotscript(default_conf) diff --git a/tests/testdata/backtest-result_test copy.json b/tests/testdata/backtest-result_test copy.json new file mode 100644 index 000000000..0395830d4 --- /dev/null +++ b/tests/testdata/backtest-result_test copy.json @@ -0,0 +1,7 @@ +{ + "ASDF": {, + "trades": [], + "metrics":[], + } +} +[["TRX/BTC",0.03990025,1515568500.0,1515568800.0,27,5,9.64e-05,0.00010074887218045112,false,"roi"],["ADA/BTC",0.03990025,1515568500.0,1515569400.0,27,15,4.756e-05,4.9705563909774425e-05,false,"roi"],["XLM/BTC",0.03990025,1515569100.0,1515569700.0,29,10,3.339e-05,3.489631578947368e-05,false,"roi"],["TRX/BTC",0.03990025,1515569100.0,1515570000.0,29,15,9.696e-05,0.00010133413533834584,false,"roi"],["ETH/BTC",-0.0,1515569700.0,1515573300.0,31,60,0.0943,0.09477268170426063,false,"roi"],["XMR/BTC",0.00997506,1515570000.0,1515571800.0,32,30,0.02719607,0.02760503345864661,false,"roi"],["ZEC/BTC",0.0,1515572100.0,1515578100.0,39,100,0.04634952,0.046581848421052625,false,"roi"],["NXT/BTC",-0.0,1515595500.0,1515599400.0,117,65,3.066e-05,3.081368421052631e-05,false,"roi"],["LTC/BTC",0.0,1515602100.0,1515604500.0,139,40,0.0168999,0.016984611278195488,false,"roi"],["ETH/BTC",-0.0,1515602400.0,1515604800.0,140,40,0.09132568,0.0917834528320802,false,"roi"],["ETH/BTC",-0.0,1515610200.0,1515613500.0,166,55,0.08898003,0.08942604518796991,false,"roi"],["ETH/BTC",0.0,1515622500.0,1515625200.0,207,45,0.08560008,0.08602915308270676,false,"roi"],["ETC/BTC",0.00997506,1515624600.0,1515626400.0,214,30,0.00249083,0.0025282860902255634,false,"roi"],["NXT/BTC",-0.0,1515626100.0,1515629700.0,219,60,3.022e-05,3.037147869674185e-05,false,"roi"],["ETC/BTC",0.01995012,1515627600.0,1515629100.0,224,25,0.002437,0.0024980776942355883,false,"roi"],["ZEC/BTC",0.00997506,1515628800.0,1515630900.0,228,35,0.04771803,0.04843559436090225,false,"roi"],["XLM/BTC",-0.10448878,1515642000.0,1515644700.0,272,45,3.651e-05,3.2859000000000005e-05,false,"stop_loss"],["ETH/BTC",0.00997506,1515642900.0,1515644700.0,275,30,0.08824105,0.08956798308270676,false,"roi"],["ETC/BTC",-0.0,1515643200.0,1515646200.0,276,50,0.00243,0.002442180451127819,false,"roi"],["ZEC/BTC",0.01995012,1515645000.0,1515646500.0,282,25,0.04545064,0.046589753784461146,false,"roi"],["XLM/BTC",0.01995012,1515645000.0,1515646200.0,282,20,3.372e-05,3.456511278195488e-05,false,"roi"],["XMR/BTC",0.01995012,1515646500.0,1515647700.0,287,20,0.02644,0.02710265664160401,false,"roi"],["ETH/BTC",-0.0,1515669600.0,1515672000.0,364,40,0.08812,0.08856170426065162,false,"roi"],["XMR/BTC",-0.0,1515670500.0,1515672900.0,367,40,0.02683577,0.026970285137844607,false,"roi"],["ADA/BTC",0.01995012,1515679200.0,1515680700.0,396,25,4.919e-05,5.04228320802005e-05,false,"roi"],["ETH/BTC",-0.0,1515698700.0,1515702900.0,461,70,0.08784896,0.08828930566416039,false,"roi"],["ADA/BTC",-0.0,1515710100.0,1515713400.0,499,55,5.105e-05,5.130588972431077e-05,false,"roi"],["XLM/BTC",0.00997506,1515711300.0,1515713100.0,503,30,3.96e-05,4.019548872180451e-05,false,"roi"],["NXT/BTC",-0.0,1515711300.0,1515713700.0,503,40,2.885e-05,2.899461152882205e-05,false,"roi"],["XMR/BTC",0.00997506,1515713400.0,1515715500.0,510,35,0.02645,0.026847744360902256,false,"roi"],["ZEC/BTC",-0.0,1515714900.0,1515719700.0,515,80,0.048,0.04824060150375939,false,"roi"],["XLM/BTC",0.01995012,1515791700.0,1515793200.0,771,25,4.692e-05,4.809593984962405e-05,false,"roi"],["ETC/BTC",-0.0,1515804900.0,1515824400.0,815,325,0.00256966,0.0025825405012531327,false,"roi"],["ADA/BTC",0.0,1515840900.0,1515843300.0,935,40,6.262e-05,6.293388471177944e-05,false,"roi"],["XLM/BTC",0.0,1515848700.0,1516025400.0,961,2945,4.73e-05,4.753709273182957e-05,false,"roi"],["ADA/BTC",-0.0,1515850200.0,1515854700.0,966,75,6.063e-05,6.0933909774436085e-05,false,"roi"],["TRX/BTC",-0.0,1515850800.0,1515886200.0,968,590,0.00011082,0.00011137548872180449,false,"roi"],["ADA/BTC",-0.0,1515856500.0,1515858900.0,987,40,5.93e-05,5.9597243107769415e-05,false,"roi"],["ZEC/BTC",-0.0,1515861000.0,1515863400.0,1002,40,0.04850003,0.04874313791979949,false,"roi"],["ETH/BTC",-0.0,1515881100.0,1515911100.0,1069,500,0.09825019,0.09874267215538847,false,"roi"],["ADA/BTC",0.0,1515889200.0,1515970500.0,1096,1355,6.018e-05,6.048165413533834e-05,false,"roi"],["ETH/BTC",-0.0,1515933900.0,1515936300.0,1245,40,0.09758999,0.0980791628822055,false,"roi"],["ETC/BTC",0.00997506,1515943800.0,1515945600.0,1278,30,0.00311,0.0031567669172932328,false,"roi"],["ETC/BTC",-0.0,1515962700.0,1515968100.0,1341,90,0.00312401,0.003139669197994987,false,"roi"],["LTC/BTC",0.0,1515972900.0,1515976200.0,1375,55,0.0174679,0.017555458395989976,false,"roi"],["DASH/BTC",-0.0,1515973500.0,1515975900.0,1377,40,0.07346846,0.07383672295739348,false,"roi"],["ETH/BTC",-0.0,1515983100.0,1515985500.0,1409,40,0.097994,0.09848519799498745,false,"roi"],["ETH/BTC",-0.0,1516000800.0,1516003200.0,1468,40,0.09659,0.09707416040100249,false,"roi"],["TRX/BTC",0.00997506,1516004400.0,1516006500.0,1480,35,9.987e-05,0.00010137180451127818,false,"roi"],["ETH/BTC",0.0,1516018200.0,1516071000.0,1526,880,0.0948969,0.09537257368421052,false,"roi"],["DASH/BTC",-0.0,1516025400.0,1516038000.0,1550,210,0.071,0.07135588972431077,false,"roi"],["ZEC/BTC",-0.0,1516026600.0,1516029000.0,1554,40,0.04600501,0.046235611553884705,false,"roi"],["TRX/BTC",-0.0,1516039800.0,1516044300.0,1598,75,9.438e-05,9.485308270676691e-05,false,"roi"],["XMR/BTC",-0.0,1516041300.0,1516043700.0,1603,40,0.03040001,0.030552391002506264,false,"roi"],["ADA/BTC",-0.10448878,1516047900.0,1516091100.0,1625,720,5.837e-05,5.2533e-05,false,"stop_loss"],["ZEC/BTC",-0.0,1516048800.0,1516053600.0,1628,80,0.046036,0.04626675689223057,false,"roi"],["ETC/BTC",-0.0,1516062600.0,1516065000.0,1674,40,0.0028685,0.0028828784461152877,false,"roi"],["DASH/BTC",0.0,1516065300.0,1516070100.0,1683,80,0.06731755,0.0676549813283208,false,"roi"],["ETH/BTC",0.0,1516088700.0,1516092000.0,1761,55,0.09217614,0.09263817578947368,false,"roi"],["LTC/BTC",0.01995012,1516091700.0,1516092900.0,1771,20,0.0165,0.016913533834586467,false,"roi"],["TRX/BTC",0.03990025,1516091700.0,1516092000.0,1771,5,7.953e-05,8.311781954887218e-05,false,"roi"],["ZEC/BTC",-0.0,1516092300.0,1516096200.0,1773,65,0.045202,0.04542857644110275,false,"roi"],["ADA/BTC",0.00997506,1516094100.0,1516095900.0,1779,30,5.248e-05,5.326917293233082e-05,false,"roi"],["XMR/BTC",0.0,1516094100.0,1516096500.0,1779,40,0.02892318,0.02906815834586466,false,"roi"],["ADA/BTC",0.01995012,1516096200.0,1516097400.0,1786,20,5.158e-05,5.287273182957392e-05,false,"roi"],["ZEC/BTC",0.00997506,1516097100.0,1516099200.0,1789,35,0.04357584,0.044231115789473675,false,"roi"],["XMR/BTC",0.00997506,1516097100.0,1516098900.0,1789,30,0.02828232,0.02870761804511278,false,"roi"],["ADA/BTC",0.00997506,1516110300.0,1516112400.0,1833,35,5.362e-05,5.4426315789473676e-05,false,"roi"],["ADA/BTC",-0.0,1516123800.0,1516127100.0,1878,55,5.302e-05,5.328576441102756e-05,false,"roi"],["ETH/BTC",0.00997506,1516126500.0,1516128300.0,1887,30,0.09129999,0.09267292218045112,false,"roi"],["XLM/BTC",0.01995012,1516126500.0,1516127700.0,1887,20,3.808e-05,3.903438596491228e-05,false,"roi"],["XMR/BTC",0.00997506,1516129200.0,1516131000.0,1896,30,0.02811012,0.028532828571428567,false,"roi"],["ETC/BTC",-0.10448878,1516137900.0,1516141500.0,1925,60,0.00258379,0.002325411,false,"stop_loss"],["NXT/BTC",-0.10448878,1516137900.0,1516142700.0,1925,80,2.559e-05,2.3031e-05,false,"stop_loss"],["TRX/BTC",-0.10448878,1516138500.0,1516141500.0,1927,50,7.62e-05,6.858e-05,false,"stop_loss"],["LTC/BTC",0.03990025,1516141800.0,1516142400.0,1938,10,0.0151,0.015781203007518795,false,"roi"],["ETC/BTC",0.03990025,1516141800.0,1516142100.0,1938,5,0.00229844,0.002402129022556391,false,"roi"],["ETC/BTC",0.03990025,1516142400.0,1516142700.0,1940,5,0.00235676,0.00246308,false,"roi"],["DASH/BTC",0.01995012,1516142700.0,1516143900.0,1941,20,0.0630692,0.06464988170426066,false,"roi"],["NXT/BTC",0.03990025,1516143000.0,1516143300.0,1942,5,2.2e-05,2.2992481203007514e-05,false,"roi"],["ADA/BTC",0.00997506,1516159800.0,1516161600.0,1998,30,4.974e-05,5.048796992481203e-05,false,"roi"],["TRX/BTC",0.01995012,1516161300.0,1516162500.0,2003,20,7.108e-05,7.28614536340852e-05,false,"roi"],["ZEC/BTC",-0.0,1516181700.0,1516184100.0,2071,40,0.04327,0.04348689223057644,false,"roi"],["ADA/BTC",-0.0,1516184400.0,1516208400.0,2080,400,4.997e-05,5.022047619047618e-05,false,"roi"],["DASH/BTC",-0.0,1516185000.0,1516188300.0,2082,55,0.06836818,0.06871087764411027,false,"roi"],["XLM/BTC",-0.0,1516185000.0,1516187400.0,2082,40,3.63e-05,3.648195488721804e-05,false,"roi"],["XMR/BTC",-0.0,1516192200.0,1516226700.0,2106,575,0.0281,0.02824085213032581,false,"roi"],["ETH/BTC",-0.0,1516192500.0,1516208100.0,2107,260,0.08651001,0.08694364413533832,false,"roi"],["ADA/BTC",-0.0,1516251600.0,1516254900.0,2304,55,5.633e-05,5.6612355889724306e-05,false,"roi"],["DASH/BTC",0.00997506,1516252800.0,1516254900.0,2308,35,0.06988494,0.07093584135338346,false,"roi"],["ADA/BTC",-0.0,1516260900.0,1516263300.0,2335,40,5.545e-05,5.572794486215538e-05,false,"roi"],["LTC/BTC",-0.0,1516266000.0,1516268400.0,2352,40,0.01633527,0.016417151052631574,false,"roi"],["ETC/BTC",-0.0,1516293600.0,1516296000.0,2444,40,0.00269734,0.0027108605012531326,false,"roi"],["XLM/BTC",0.01995012,1516298700.0,1516300200.0,2461,25,4.475e-05,4.587155388471177e-05,false,"roi"],["NXT/BTC",0.00997506,1516299900.0,1516301700.0,2465,30,2.79e-05,2.8319548872180444e-05,false,"roi"],["ZEC/BTC",0.0,1516306200.0,1516308600.0,2486,40,0.04439326,0.04461578260651629,false,"roi"],["XLM/BTC",0.0,1516311000.0,1516322100.0,2502,185,4.49e-05,4.51250626566416e-05,false,"roi"],["XMR/BTC",-0.0,1516312500.0,1516338300.0,2507,430,0.02855,0.028693107769423555,false,"roi"],["ADA/BTC",0.0,1516313400.0,1516315800.0,2510,40,5.796e-05,5.8250526315789473e-05,false,"roi"],["ZEC/BTC",0.0,1516319400.0,1516321800.0,2530,40,0.04340323,0.04362079005012531,false,"roi"],["ZEC/BTC",0.0,1516380300.0,1516383300.0,2733,50,0.04454455,0.04476783095238095,false,"roi"],["ADA/BTC",-0.0,1516382100.0,1516391700.0,2739,160,5.62e-05,5.648170426065162e-05,false,"roi"],["XLM/BTC",-0.0,1516382400.0,1516392900.0,2740,175,4.339e-05,4.360749373433584e-05,false,"roi"],["TRX/BTC",0.0,1516423500.0,1516469700.0,2877,770,0.0001009,0.00010140576441102757,false,"roi"],["ETC/BTC",-0.0,1516423800.0,1516461300.0,2878,625,0.00270505,0.002718609147869674,false,"roi"],["XMR/BTC",-0.0,1516423800.0,1516431600.0,2878,130,0.03000002,0.030150396040100245,false,"roi"],["ADA/BTC",-0.0,1516438800.0,1516441200.0,2928,40,5.46e-05,5.4873684210526304e-05,false,"roi"],["XMR/BTC",-0.10448878,1516472700.0,1516852200.0,3041,6325,0.03082222,0.027739998000000002,false,"stop_loss"],["ETH/BTC",-0.0,1516487100.0,1516490100.0,3089,50,0.08969999,0.09014961401002504,false,"roi"],["LTC/BTC",0.0,1516503000.0,1516545000.0,3142,700,0.01632501,0.01640683962406015,false,"roi"],["DASH/BTC",-0.0,1516530000.0,1516532400.0,3232,40,0.070538,0.07089157393483708,false,"roi"],["ADA/BTC",-0.0,1516549800.0,1516560300.0,3298,175,5.301e-05,5.3275714285714276e-05,false,"roi"],["XLM/BTC",0.0,1516551600.0,1516554000.0,3304,40,3.955e-05,3.9748245614035085e-05,false,"roi"],["ETC/BTC",0.00997506,1516569300.0,1516571100.0,3363,30,0.00258505,0.002623922932330827,false,"roi"],["XLM/BTC",-0.0,1516569300.0,1516571700.0,3363,40,3.903e-05,3.922563909774435e-05,false,"roi"],["ADA/BTC",-0.0,1516581300.0,1516617300.0,3403,600,5.236e-05,5.262245614035087e-05,false,"roi"],["TRX/BTC",0.0,1516584600.0,1516587000.0,3414,40,9.028e-05,9.073253132832079e-05,false,"roi"],["ETC/BTC",-0.0,1516623900.0,1516631700.0,3545,130,0.002687,0.002700468671679198,false,"roi"],["XLM/BTC",-0.0,1516626900.0,1516629300.0,3555,40,4.168e-05,4.1888922305764405e-05,false,"roi"],["TRX/BTC",0.00997506,1516629600.0,1516631400.0,3564,30,8.821e-05,8.953646616541353e-05,false,"roi"],["ADA/BTC",-0.0,1516636500.0,1516639200.0,3587,45,5.172e-05,5.1979248120300745e-05,false,"roi"],["NXT/BTC",0.01995012,1516637100.0,1516638300.0,3589,20,3.026e-05,3.101839598997494e-05,false,"roi"],["DASH/BTC",0.0,1516650600.0,1516666200.0,3634,260,0.07064,0.07099408521303258,false,"roi"],["LTC/BTC",0.0,1516656300.0,1516658700.0,3653,40,0.01644483,0.01652726022556391,false,"roi"],["XLM/BTC",0.00997506,1516665900.0,1516667700.0,3685,30,4.331e-05,4.3961278195488714e-05,false,"roi"],["NXT/BTC",0.01995012,1516672200.0,1516673700.0,3706,25,3.2e-05,3.2802005012531326e-05,false,"roi"],["ETH/BTC",0.0,1516681500.0,1516684500.0,3737,50,0.09167706,0.09213659413533835,false,"roi"],["DASH/BTC",0.0,1516692900.0,1516698000.0,3775,85,0.0692498,0.06959691679197995,false,"roi"],["NXT/BTC",0.0,1516704600.0,1516712700.0,3814,135,3.182e-05,3.197949874686716e-05,false,"roi"],["ZEC/BTC",-0.0,1516705500.0,1516723500.0,3817,300,0.04088,0.04108491228070175,false,"roi"],["ADA/BTC",-0.0,1516719300.0,1516721700.0,3863,40,5.15e-05,5.175814536340851e-05,false,"roi"],["ETH/BTC",0.0,1516725300.0,1516752300.0,3883,450,0.09071698,0.09117170170426064,false,"roi"],["NXT/BTC",-0.0,1516728300.0,1516733100.0,3893,80,3.128e-05,3.1436791979949865e-05,false,"roi"],["TRX/BTC",-0.0,1516738500.0,1516744800.0,3927,105,9.555e-05,9.602894736842104e-05,false,"roi"],["ZEC/BTC",-0.0,1516746600.0,1516749000.0,3954,40,0.04080001,0.041004521328320796,false,"roi"],["ADA/BTC",-0.0,1516751400.0,1516764900.0,3970,225,5.163e-05,5.1888796992481196e-05,false,"roi"],["ZEC/BTC",0.0,1516753200.0,1516758600.0,3976,90,0.04040781,0.04061035541353383,false,"roi"],["ADA/BTC",-0.0,1516776300.0,1516778700.0,4053,40,5.132e-05,5.157724310776942e-05,false,"roi"],["ADA/BTC",0.03990025,1516803300.0,1516803900.0,4143,10,5.198e-05,5.432496240601503e-05,false,"roi"],["NXT/BTC",-0.0,1516805400.0,1516811700.0,4150,105,3.054e-05,3.069308270676692e-05,false,"roi"],["TRX/BTC",0.0,1516806600.0,1516810500.0,4154,65,9.263e-05,9.309431077694235e-05,false,"roi"],["ADA/BTC",-0.0,1516833600.0,1516836300.0,4244,45,5.514e-05,5.5416390977443596e-05,false,"roi"],["XLM/BTC",0.0,1516841400.0,1516843800.0,4270,40,4.921e-05,4.9456666666666664e-05,false,"roi"],["ETC/BTC",0.0,1516868100.0,1516882500.0,4359,240,0.0026,0.002613032581453634,false,"roi"],["XMR/BTC",-0.0,1516875900.0,1516896900.0,4385,350,0.02799871,0.028139054411027563,false,"roi"],["ZEC/BTC",-0.0,1516878000.0,1516880700.0,4392,45,0.04078902,0.0409934762406015,false,"roi"],["NXT/BTC",-0.0,1516885500.0,1516887900.0,4417,40,2.89e-05,2.904486215538847e-05,false,"roi"],["ZEC/BTC",-0.0,1516886400.0,1516889100.0,4420,45,0.041103,0.041309030075187964,false,"roi"],["XLM/BTC",0.00997506,1516895100.0,1516896900.0,4449,30,5.428e-05,5.5096240601503756e-05,false,"roi"],["XLM/BTC",-0.0,1516902300.0,1516922100.0,4473,330,5.414e-05,5.441137844611528e-05,false,"roi"],["ZEC/BTC",-0.0,1516914900.0,1516917300.0,4515,40,0.04140777,0.0416153277443609,false,"roi"],["ETC/BTC",0.0,1516932300.0,1516934700.0,4573,40,0.00254309,0.002555837318295739,false,"roi"],["ADA/BTC",-0.0,1516935300.0,1516979400.0,4583,735,5.607e-05,5.6351052631578935e-05,false,"roi"],["ETC/BTC",0.0,1516947000.0,1516958700.0,4622,195,0.00253806,0.0025507821052631577,false,"roi"],["ZEC/BTC",-0.0,1516951500.0,1516960500.0,4637,150,0.0415,0.04170802005012531,false,"roi"],["XLM/BTC",0.00997506,1516960500.0,1516962300.0,4667,30,5.321e-05,5.401015037593984e-05,false,"roi"],["XMR/BTC",-0.0,1516982700.0,1516985100.0,4741,40,0.02772046,0.02785940967418546,false,"roi"],["ETH/BTC",0.0,1517009700.0,1517012100.0,4831,40,0.09461341,0.09508766268170425,false,"roi"],["XLM/BTC",-0.0,1517013300.0,1517016600.0,4843,55,5.615e-05,5.643145363408521e-05,false,"roi"],["ADA/BTC",-0.07877175,1517013900.0,1517287500.0,4845,4560,5.556e-05,5.144e-05,true,"force_sell"],["DASH/BTC",-0.0,1517020200.0,1517052300.0,4866,535,0.06900001,0.06934587471177944,false,"roi"],["ETH/BTC",-0.0,1517034300.0,1517036700.0,4913,40,0.09449985,0.09497353345864659,false,"roi"],["ZEC/BTC",-0.04815133,1517046000.0,1517287200.0,4952,4020,0.0410697,0.03928809,true,"force_sell"],["XMR/BTC",-0.0,1517053500.0,1517056200.0,4977,45,0.0285,0.02864285714285714,false,"roi"],["XMR/BTC",-0.0,1517056500.0,1517066700.0,4987,170,0.02866372,0.02880739779448621,false,"roi"],["ETH/BTC",-0.0,1517068200.0,1517071800.0,5026,60,0.095381,0.09585910025062655,false,"roi"],["DASH/BTC",-0.0,1517072700.0,1517075100.0,5041,40,0.06759092,0.06792972160401002,false,"roi"],["ETC/BTC",-0.0,1517096400.0,1517101500.0,5120,85,0.00258501,0.002597967443609022,false,"roi"],["DASH/BTC",-0.0,1517106300.0,1517127000.0,5153,345,0.06698502,0.0673207845112782,false,"roi"],["DASH/BTC",-0.0,1517135100.0,1517157000.0,5249,365,0.0677177,0.06805713709273183,false,"roi"],["XLM/BTC",0.0,1517171700.0,1517175300.0,5371,60,5.215e-05,5.2411403508771925e-05,false,"roi"],["ETC/BTC",0.00997506,1517176800.0,1517178600.0,5388,30,0.00273809,0.002779264285714285,false,"roi"],["ETC/BTC",0.00997506,1517184000.0,1517185800.0,5412,30,0.00274632,0.002787618045112782,false,"roi"],["LTC/BTC",0.0,1517192100.0,1517194800.0,5439,45,0.01622478,0.016306107218045113,false,"roi"],["DASH/BTC",-0.0,1517195100.0,1517197500.0,5449,40,0.069,0.06934586466165413,false,"roi"],["TRX/BTC",-0.0,1517203200.0,1517208900.0,5476,95,8.755e-05,8.798884711779448e-05,false,"roi"],["DASH/BTC",-0.0,1517209200.0,1517253900.0,5496,745,0.06825763,0.06859977350877192,false,"roi"],["DASH/BTC",-0.0,1517255100.0,1517257500.0,5649,40,0.06713892,0.06747545593984962,false,"roi"],["TRX/BTC",-0.0199116,1517268600.0,1517287500.0,5694,315,8.934e-05,8.8e-05,true,"force_sell"]] From 18913db99267a699a89b9fdddc6509980cff6775 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Jun 2020 20:47:36 +0200 Subject: [PATCH 0035/1197] Replace ticker_interval with timeframe in sample configs --- config.json.example | 2 +- config_binance.json.example | 2 +- config_full.json.example | 2 +- config_kraken.json.example | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config.json.example b/config.json.example index d37a6b336..545fcf1d4 100644 --- a/config.json.example +++ b/config.json.example @@ -4,7 +4,7 @@ "stake_amount": 0.05, "tradable_balance_ratio": 0.99, "fiat_display_currency": "USD", - "ticker_interval": "5m", + "timeframe": "5m", "dry_run": false, "cancel_open_orders_on_exit": false, "trailing_stop": false, diff --git a/config_binance.json.example b/config_binance.json.example index 5d7b6b656..98dada647 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -4,7 +4,7 @@ "stake_amount": 0.05, "tradable_balance_ratio": 0.99, "fiat_display_currency": "USD", - "ticker_interval": "5m", + "timeframe": "5m", "dry_run": true, "cancel_open_orders_on_exit": false, "trailing_stop": false, diff --git a/config_full.json.example b/config_full.json.example index 481742817..66b0d6a0d 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -9,7 +9,7 @@ "last_stake_amount_min_ratio": 0.5, "dry_run": false, "cancel_open_orders_on_exit": false, - "ticker_interval": "5m", + "timeframe": "5m", "trailing_stop": false, "trailing_stop_positive": 0.005, "trailing_stop_positive_offset": 0.0051, diff --git a/config_kraken.json.example b/config_kraken.json.example index 54fbf4a00..602e2ccf3 100644 --- a/config_kraken.json.example +++ b/config_kraken.json.example @@ -4,7 +4,7 @@ "stake_amount": 10, "tradable_balance_ratio": 0.99, "fiat_display_currency": "EUR", - "ticker_interval": "5m", + "timeframe": "5m", "dry_run": true, "cancel_open_orders_on_exit": false, "trailing_stop": false, From cadc50ce9b7dbedf50478cbd9431e5bdffa4ac8c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Jun 2020 20:49:40 +0200 Subject: [PATCH 0036/1197] Replace more occurances of ticker_interval with timeframe --- freqtrade/commands/pairlist_commands.py | 2 +- freqtrade/data/converter.py | 4 ++-- freqtrade/data/dataprovider.py | 4 ++-- freqtrade/freqtradebot.py | 6 +++--- freqtrade/optimize/backtesting.py | 6 +++--- freqtrade/optimize/hyperopt_interface.py | 8 +++++--- freqtrade/pairlist/pairlistmanager.py | 4 ++-- freqtrade/plot/plotting.py | 6 +++--- freqtrade/resolvers/hyperopt_resolver.py | 5 +++-- freqtrade/resolvers/strategy_resolver.py | 2 +- freqtrade/strategy/interface.py | 2 +- freqtrade/templates/base_strategy.py.j2 | 4 ++-- tests/strategy/test_strategy.py | 2 +- 13 files changed, 29 insertions(+), 26 deletions(-) diff --git a/freqtrade/commands/pairlist_commands.py b/freqtrade/commands/pairlist_commands.py index bf0b217a5..dffe0c82e 100644 --- a/freqtrade/commands/pairlist_commands.py +++ b/freqtrade/commands/pairlist_commands.py @@ -25,7 +25,7 @@ def start_test_pairlist(args: Dict[str, Any]) -> None: results = {} for curr in quote_currencies: config['stake_currency'] = curr - # Do not use ticker_interval set in the config + # Do not use timeframe set in the config pairlists = PairListManager(exchange, config) pairlists.refresh_pairlist() results[curr] = pairlists.whitelist diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 0ef7955a4..cfc7bc903 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -236,12 +236,12 @@ def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to: from freqtrade.data.history.idatahandler import get_datahandler src = get_datahandler(config['datadir'], convert_from) trg = get_datahandler(config['datadir'], convert_to) - timeframes = config.get('timeframes', [config.get('ticker_interval')]) + timeframes = config.get('timeframes', [config.get('timeframe')]) logger.info(f"Converting candle (OHLCV) for timeframe {timeframes}") if 'pairs' not in config: config['pairs'] = [] - # Check timeframes or fall back to ticker_interval. + # Check timeframes or fall back to timeframe. for timeframe in timeframes: config['pairs'].extend(src.ohlcv_get_pairs(config['datadir'], timeframe)) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index a01344364..058ca42da 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -55,7 +55,7 @@ class DataProvider: Use False only for read-only operations (where the dataframe is not modified) """ if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE): - return self._exchange.klines((pair, timeframe or self._config['ticker_interval']), + return self._exchange.klines((pair, timeframe or self._config['timeframe']), copy=copy) else: return DataFrame() @@ -67,7 +67,7 @@ class DataProvider: :param timeframe: timeframe to get data for """ return load_pair_history(pair=pair, - timeframe=timeframe or self._config['ticker_interval'], + timeframe=timeframe or self._config['timeframe'], datadir=self._config['datadir'] ) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d4afa1d60..627971c31 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -547,7 +547,7 @@ class FreqtradeBot: exchange=self.exchange.id, open_order_id=order_id, strategy=self.strategy.get_strategy_name(), - ticker_interval=timeframe_to_minutes(self.config['ticker_interval']) + ticker_interval=timeframe_to_minutes(self.config['timeframe']) ) # Update fees if order is closed @@ -780,7 +780,7 @@ class FreqtradeBot: self.update_trade_state(trade, stoploss_order, sl_order=True) # Lock pair for one candle to prevent immediate rebuys self.strategy.lock_pair(trade.pair, - timeframe_to_next_date(self.config['ticker_interval'])) + timeframe_to_next_date(self.config['timeframe'])) self._notify_sell(trade, "stoploss") return True @@ -1090,7 +1090,7 @@ class FreqtradeBot: Trade.session.flush() # Lock pair for one candle to prevent immediate rebuys - self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['ticker_interval'])) + self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe'])) self._notify_sell(trade, order_type) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3bf211d99..9a48338f1 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -94,10 +94,10 @@ class Backtesting: self.strategylist.append(StrategyResolver.load_strategy(self.config)) validate_config_consistency(self.config) - if "ticker_interval" not in self.config: + if "timeframe" not in self.config: raise OperationalException("Timeframe (ticker interval) needs to be set in either " - "configuration or as cli argument `--ticker-interval 5m`") - self.timeframe = str(self.config.get('ticker_interval')) + "configuration or as cli argument `--timeframe 5m`") + self.timeframe = str(self.config.get('timeframe')) self.timeframe_min = timeframe_to_minutes(self.timeframe) # Get maximum required startup period diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index b3cedef2c..20209d8a9 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -37,7 +37,8 @@ class IHyperOpt(ABC): self.config = config # Assign ticker_interval to be used in hyperopt - IHyperOpt.ticker_interval = str(config['ticker_interval']) + IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECTED + IHyperOpt.timeframe = str(config['timeframe']) @staticmethod def buy_strategy_generator(params: Dict[str, Any]) -> Callable: @@ -218,9 +219,10 @@ class IHyperOpt(ABC): # Why do I still need such shamanic mantras in modern python? def __getstate__(self): state = self.__dict__.copy() - state['ticker_interval'] = self.ticker_interval + state['timeframe'] = self.timeframe return state def __setstate__(self, state): self.__dict__.update(state) - IHyperOpt.ticker_interval = state['ticker_interval'] + IHyperOpt.ticker_interval = state['timeframe'] + IHyperOpt.timeframe = state['timeframe'] diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/pairlist/pairlistmanager.py index f532f2cd0..81e52768e 100644 --- a/freqtrade/pairlist/pairlistmanager.py +++ b/freqtrade/pairlist/pairlistmanager.py @@ -131,6 +131,6 @@ class PairListManager(): def create_pair_list(self, pairs: List[str], timeframe: str = None) -> ListPairsWithTimeframes: """ - Create list of pair tuples with (pair, ticker_interval) + Create list of pair tuples with (pair, timeframe) """ - return [(pair, timeframe or self._config['ticker_interval']) for pair in pairs] + return [(pair, timeframe or self._config['timeframe']) for pair in pairs] diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index f1d114e2b..f9e526967 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -45,7 +45,7 @@ def init_plotscript(config): data = load_data( datadir=config.get("datadir"), pairs=pairs, - timeframe=config.get('ticker_interval', '5m'), + timeframe=config.get('timeframe', '5m'), timerange=timerange, data_format=config.get('dataformat_ohlcv', 'json'), ) @@ -487,7 +487,7 @@ def load_and_plot_trades(config: Dict[str, Any]): plot_config=strategy.plot_config if hasattr(strategy, 'plot_config') else {} ) - store_plot_file(fig, filename=generate_plot_filename(pair, config['ticker_interval']), + store_plot_file(fig, filename=generate_plot_filename(pair, config['timeframe']), directory=config['user_data_dir'] / "plot") logger.info('End of plotting process. %s plots generated', pair_counter) @@ -515,6 +515,6 @@ def plot_profit(config: Dict[str, Any]) -> None: # Create an average close price of all the pairs that were involved. # this could be useful to gauge the overall market trend fig = generate_profit_graph(plot_elements["pairs"], plot_elements["ohlcv"], - trades, config.get('ticker_interval', '5m')) + trades, config.get('timeframe', '5m')) store_plot_file(fig, filename='freqtrade-profit-plot.html', directory=config['user_data_dir'] / "plot", auto_open=True) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index ddf461252..633363134 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -77,8 +77,9 @@ class HyperOptLossResolver(IResolver): config, kwargs={}, extra_dir=config.get('hyperopt_path')) - # Assign ticker_interval to be used in hyperopt - hyperoptloss.__class__.ticker_interval = str(config['ticker_interval']) + # Assign timeframe to be used in hyperopt + hyperoptloss.__class__.ticker_interval = str(config['timeframe']) + hyperoptloss.__class__.timeframe = str(config['timeframe']) if not hasattr(hyperoptloss, 'hyperopt_loss_function'): raise OperationalException( diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index abd6a4195..99cf2d785 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -54,7 +54,7 @@ class StrategyResolver(IResolver): # Check if we need to override configuration # (Attribute name, default, subkey) attributes = [("minimal_roi", {"0": 10.0}, None), - ("ticker_interval", None, None), + ("timeframe", None, None), ("stoploss", None, None), ("trailing_stop", None, None), ("trailing_stop_positive", None, None), diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index ed2344a53..17c4aa5ca 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -62,7 +62,7 @@ class IStrategy(ABC): Attributes you can use: minimal_roi -> Dict: Minimal ROI designed for the strategy stoploss -> float: optimal stoploss designed for the strategy - ticker_interval -> str: value of the timeframe (ticker interval) to use with the strategy + timeframe -> str: value of the timeframe (ticker interval) to use with the strategy """ # Strategy interface version # Default to version 2 diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index c37164568..ce2c6d5c0 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -51,8 +51,8 @@ class {{ strategy }}(IStrategy): # trailing_stop_positive = 0.01 # trailing_stop_positive_offset = 0.0 # Disabled / not configured - # Optimal ticker interval for the strategy. - ticker_interval = '5m' + # Optimal timeframe for the strategy. + timeframe = '5m' # Run "populate_indicators()" only for new candle. process_only_new_candles = False diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py index 5cb59cad0..59ce8c5b8 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -186,7 +186,7 @@ def test_strategy_override_timeframe(caplog, default_conf): }) strategy = StrategyResolver.load_strategy(default_conf) - assert strategy.ticker_interval == 60 + assert strategy.timeframe == 60 assert strategy.stake_currency == 'ETH' assert log_has("Override strategy 'timeframe' with value in config file: 60.", caplog) From 388573800c673e1fe383fd83938c85c06fa49db2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Jun 2020 20:52:33 +0200 Subject: [PATCH 0037/1197] Update configuration messages --- freqtrade/configuration/configuration.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 7edd9bca1..139e42084 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -204,9 +204,9 @@ class Configuration: def _process_optimize_options(self, config: Dict[str, Any]) -> None: # This will override the strategy configuration - self._args_to_config(config, argname='ticker_interval', - logstring='Parameter -i/--ticker-interval detected ... ' - 'Using ticker_interval: {} ...') + self._args_to_config(config, argname='timeframe', + logstring='Parameter -i/--timeframe detected ... ' + 'Using timeframe: {} ...') self._args_to_config(config, argname='position_stacking', logstring='Parameter --enable-position-stacking detected ...') @@ -242,8 +242,8 @@ class Configuration: self._args_to_config(config, argname='strategy_list', logstring='Using strategy list of {} strategies', logfun=len) - self._args_to_config(config, argname='ticker_interval', - logstring='Overriding ticker interval with Command line argument') + self._args_to_config(config, argname='timeframe', + logstring='Overriding timeframe with Command line argument') self._args_to_config(config, argname='export', logstring='Parameter --export detected: {} ...') From 947903a4acac05fbda301e0dc90040488a772b28 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jun 2020 09:36:04 +0200 Subject: [PATCH 0038/1197] Use timeframe from within strategy --- freqtrade/edge/edge_positioning.py | 4 ++-- freqtrade/freqtradebot.py | 8 ++++---- tests/edge/test_edge.py | 10 +++++----- tests/strategy/strats/default_strategy.py | 2 +- tests/strategy/test_default_strategy.py | 1 + tests/strategy/test_strategy.py | 1 + tests/test_configuration.py | 12 ++++++------ 7 files changed, 20 insertions(+), 18 deletions(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index c19d4552a..dd4ea35bb 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -100,14 +100,14 @@ class Edge: datadir=self.config['datadir'], pairs=pairs, exchange=self.exchange, - timeframe=self.strategy.ticker_interval, + timeframe=self.strategy.timeframe, timerange=self._timerange, ) data = load_data( datadir=self.config['datadir'], pairs=pairs, - timeframe=self.strategy.ticker_interval, + timeframe=self.strategy.timeframe, timerange=self._timerange, startup_candles=self.strategy.startup_candle_count, data_format=self.config.get('dataformat_ohlcv', 'json'), diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 627971c31..c52fe18d1 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -421,8 +421,8 @@ class FreqtradeBot: # running get_signal on historical data fetched (buy, sell) = self.strategy.get_signal( - pair, self.strategy.ticker_interval, - self.dataprovider.ohlcv(pair, self.strategy.ticker_interval)) + pair, self.strategy.timeframe, + self.dataprovider.ohlcv(pair, self.strategy.timeframe)) if buy and not sell: stake_amount = self.get_trade_stake_amount(pair) @@ -696,8 +696,8 @@ class FreqtradeBot: if (config_ask_strategy.get('use_sell_signal', True) or config_ask_strategy.get('ignore_roi_if_buy_signal', False)): (buy, sell) = self.strategy.get_signal( - trade.pair, self.strategy.ticker_interval, - self.dataprovider.ohlcv(trade.pair, self.strategy.ticker_interval)) + trade.pair, self.strategy.timeframe, + self.dataprovider.ohlcv(trade.pair, self.strategy.timeframe)) if config_ask_strategy.get('use_order_book', False): # logger.debug('Order book %s',orderBook) diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index 163ceff4b..cf9cb6fe1 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -27,7 +27,7 @@ from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, #################################################################### tests_start_time = arrow.get(2018, 10, 3) -ticker_interval_in_minute = 60 +timeframe_in_minute = 60 _ohlc = {'date': 0, 'buy': 1, 'open': 2, 'high': 3, 'low': 4, 'close': 5, 'sell': 6, 'volume': 7} # Helpers for this test file @@ -49,7 +49,7 @@ def _build_dataframe(buy_ohlc_sell_matrice): 'date': tests_start_time.shift( minutes=( ohlc[0] * - ticker_interval_in_minute)).timestamp * + timeframe_in_minute)).timestamp * 1000, 'buy': ohlc[1], 'open': ohlc[2], @@ -70,7 +70,7 @@ def _build_dataframe(buy_ohlc_sell_matrice): def _time_on_candle(number): return np.datetime64(tests_start_time.shift( - minutes=(number * ticker_interval_in_minute)).timestamp * 1000, 'ms') + minutes=(number * timeframe_in_minute)).timestamp * 1000, 'ms') # End helper functions @@ -262,7 +262,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m', NEOBTC = [ [ - tests_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000, + tests_start_time.shift(minutes=(x * timeframe_in_minute)).timestamp * 1000, math.sin(x * hz) / 1000 + base, math.sin(x * hz) / 1000 + base + 0.0001, math.sin(x * hz) / 1000 + base - 0.0001, @@ -274,7 +274,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m', base = 0.002 LTCBTC = [ [ - tests_start_time.shift(minutes=(x * ticker_interval_in_minute)).timestamp * 1000, + tests_start_time.shift(minutes=(x * timeframe_in_minute)).timestamp * 1000, math.sin(x * hz) / 1000 + base, math.sin(x * hz) / 1000 + base + 0.0001, math.sin(x * hz) / 1000 + base - 0.0001, diff --git a/tests/strategy/strats/default_strategy.py b/tests/strategy/strats/default_strategy.py index 7ea55d3f9..98842ff7c 100644 --- a/tests/strategy/strats/default_strategy.py +++ b/tests/strategy/strats/default_strategy.py @@ -29,7 +29,7 @@ class DefaultStrategy(IStrategy): stoploss = -0.10 # Optimal ticker interval for the strategy - ticker_interval = '5m' + timeframe = '5m' # Optional order type mapping order_types = { diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index 0b8ea9f85..315f80440 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -19,6 +19,7 @@ def test_default_strategy(result): assert type(strategy.minimal_roi) is dict assert type(strategy.stoploss) is float assert type(strategy.ticker_interval) is str + assert type(strategy.timeframe) is str indicators = strategy.populate_indicators(result, metadata) assert type(indicators) is DataFrame assert type(strategy.populate_buy_trend(indicators, metadata)) is DataFrame diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py index 59ce8c5b8..1bb45f28c 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -105,6 +105,7 @@ def test_strategy(result, default_conf): assert strategy.stoploss == -0.10 assert default_conf['stoploss'] == -0.10 + assert strategy.timeframe == '5m' assert strategy.ticker_interval == '5m' assert default_conf['timeframe'] == '5m' diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 05074c258..8e79f4297 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -401,8 +401,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert 'datadir' in config assert 'user_data_dir' in config assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog) - assert 'ticker_interval' in config - assert not log_has('Parameter -i/--ticker-interval detected ...', caplog) + assert 'timeframe' in config + assert not log_has('Parameter -i/--timeframe detected ...', caplog) assert 'position_stacking' not in config assert not log_has('Parameter --enable-position-stacking detected ...', caplog) @@ -448,8 +448,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non assert log_has('Using user-data directory: {} ...'.format(Path("/tmp/freqtrade")), caplog) assert 'user_data_dir' in config - assert 'ticker_interval' in config - assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', + assert 'timeframe' in config + assert log_has('Parameter -i/--timeframe detected ... Using timeframe: 1m ...', caplog) assert 'position_stacking' in config @@ -494,8 +494,8 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non assert 'pair_whitelist' in config['exchange'] assert 'datadir' in config assert log_has('Using data directory: {} ...'.format(config['datadir']), caplog) - assert 'ticker_interval' in config - assert log_has('Parameter -i/--ticker-interval detected ... Using ticker_interval: 1m ...', + assert 'timeframe' in config + assert log_has('Parameter -i/--timeframe detected ... Using timeframe: 1m ...', caplog) assert 'strategy_list' in config From 3e895ae74aa3d754a0d8a2bc477c1c6da50b7baa Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jun 2020 09:41:42 +0200 Subject: [PATCH 0039/1197] Some more replacements of ticker_interval --- freqtrade/optimize/hyperopt_loss_interface.py | 1 + freqtrade/persistence.py | 1 + tests/data/test_dataprovider.py | 2 +- tests/test_configuration.py | 2 +- 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt_loss_interface.py b/freqtrade/optimize/hyperopt_loss_interface.py index 879a9f0e9..c2607a9a8 100644 --- a/freqtrade/optimize/hyperopt_loss_interface.py +++ b/freqtrade/optimize/hyperopt_loss_interface.py @@ -15,6 +15,7 @@ class IHyperOptLoss(ABC): Defines the custom loss function (`hyperopt_loss_function()` which is evaluated every epoch.) """ ticker_interval: str + timeframe: str @staticmethod @abstractmethod diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 111ccfe2a..363ce35ad 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -288,6 +288,7 @@ class Trade(_DECL_BASE): 'max_rate': self.max_rate, 'strategy': self.strategy, 'ticker_interval': self.ticker_interval, + 'timeframe': self.ticker_interval, 'open_order_id': self.open_order_id, 'exchange': self.exchange, } diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index 718060c5e..def3ad535 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -104,7 +104,7 @@ def test_refresh(mocker, default_conf, ohlcv_history): timeframe = default_conf["timeframe"] pairs = [("XRP/BTC", timeframe), ("UNITTEST/BTC", timeframe)] - pairs_non_trad = [("ETH/USDT", ticker_interval), ("BTC/TUSD", "1h")] + pairs_non_trad = [("ETH/USDT", timeframe), ("BTC/TUSD", "1h")] dp = DataProvider(default_conf, exchange) dp.refresh(pairs) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 8e79f4297..1ed97f728 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -87,7 +87,7 @@ def test_load_config_file_error_range(default_conf, mocker, caplog) -> None: assert isinstance(x, str) assert (x == '{"max_open_trades": 1, "stake_currency": "BTC", ' '"stake_amount": .001, "fiat_display_currency": "USD", ' - '"timeframe": "5m", "dry_run": true, ') + '"timeframe": "5m", "dry_run": true, "cance') def test__args_to_config(caplog): From 09fe3c6f5e1f9f0a42882782e3caac6607e34600 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jun 2020 09:50:56 +0200 Subject: [PATCH 0040/1197] create compatibility code --- freqtrade/configuration/deprecated_settings.py | 6 ++++++ freqtrade/constants.py | 1 - freqtrade/resolvers/strategy_resolver.py | 11 +++++++++++ freqtrade/strategy/interface.py | 4 ++-- tests/strategy/test_default_strategy.py | 1 - 5 files changed, 19 insertions(+), 4 deletions(-) diff --git a/freqtrade/configuration/deprecated_settings.py b/freqtrade/configuration/deprecated_settings.py index 3999ea422..2d5dba9ca 100644 --- a/freqtrade/configuration/deprecated_settings.py +++ b/freqtrade/configuration/deprecated_settings.py @@ -67,3 +67,9 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None: "'tradable_balance_ratio' and remove 'capital_available_percentage' " "from the edge configuration." ) + if 'ticker_interval' in config: + logger.warning( + "DEPRECATED: " + "Please use 'timeframe' instead of 'ticker_interval." + ) + config['timeframe'] = config['ticker_interval'] diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 511c2993d..3afb4b0f1 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -71,7 +71,6 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'max_open_trades': {'type': ['integer', 'number'], 'minimum': -1}, - 'ticker_interval': {'type': 'string'}, 'timeframe': {'type': 'string'}, 'stake_currency': {'type': 'string'}, 'stake_amount': { diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 99cf2d785..26bce01ca 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -50,6 +50,14 @@ class StrategyResolver(IResolver): if 'ask_strategy' not in config: config['ask_strategy'] = {} + if hasattr(strategy, 'ticker_interval') and not hasattr(strategy, 'timeframe'): + # Assign ticker_interval to timeframe to keep compatibility + if 'timeframe' not in config: + logger.warning( + "DEPRECATED: Please migrate to using timeframe instead of ticker_interval." + ) + strategy.timeframe = strategy.ticker_interval + # Set attributes # Check if we need to override configuration # (Attribute name, default, subkey) @@ -80,6 +88,9 @@ class StrategyResolver(IResolver): StrategyResolver._override_attribute_helper(strategy, config, attribute, default) + # Assign deprecated variable - to not break users code relying on this. + strategy.ticker_interval = strategy.timeframe + # Loop this list again to have output combined for attribute, _, subkey in attributes: if subkey and attribute in config[subkey]: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 17c4aa5ca..7bf750249 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -85,8 +85,8 @@ class IStrategy(ABC): trailing_stop_positive_offset: float = 0.0 trailing_only_offset_is_reached = False - # associated ticker interval - ticker_interval: str + # associated timeframe + timeframe: str # Optional order types order_types: Dict = { diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index 315f80440..df7e35197 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -18,7 +18,6 @@ def test_default_strategy(result): metadata = {'pair': 'ETH/BTC'} assert type(strategy.minimal_roi) is dict assert type(strategy.stoploss) is float - assert type(strategy.ticker_interval) is str assert type(strategy.timeframe) is str indicators = strategy.populate_indicators(result, metadata) assert type(indicators) is DataFrame From af0f29e6b7b6647ce8e651d35b4a558fee09b578 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jun 2020 10:02:24 +0200 Subject: [PATCH 0041/1197] Update persistence to use timeframe --- docs/sql_cheatsheet.md | 2 +- freqtrade/freqtradebot.py | 2 +- freqtrade/persistence.py | 19 ++++++++++++------- tests/test_persistence.py | 7 ++++--- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index 895a0536a..b261904d7 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -70,7 +70,7 @@ CREATE TABLE trades min_rate FLOAT, sell_reason VARCHAR, strategy VARCHAR, - ticker_interval INTEGER, + timeframe INTEGER, PRIMARY KEY (id), CHECK (is_open IN (0, 1)) ); diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c52fe18d1..a8483051e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -547,7 +547,7 @@ class FreqtradeBot: exchange=self.exchange.id, open_order_id=order_id, strategy=self.strategy.get_strategy_name(), - ticker_interval=timeframe_to_minutes(self.config['timeframe']) + timeframe=timeframe_to_minutes(self.config['timeframe']) ) # Update fees if order is closed diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 363ce35ad..f0b97be1e 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, 'sell_order_status'): + if not has_column(cols, 'timeframe'): logger.info(f'Running database migration - backup available as {table_back_name}') fee_open = get_column_def(cols, 'fee_open', 'fee') @@ -107,7 +107,12 @@ def check_migrate(engine) -> None: min_rate = get_column_def(cols, 'min_rate', 'null') 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') + # If ticker-interval existed use that, else null. + if has_column(cols, 'ticker_interval'): + timeframe = get_column_def(cols, 'timeframe', 'ticker_interval') + else: + timeframe = get_column_def(cols, 'timeframe', 'null') + open_trade_price = get_column_def(cols, 'open_trade_price', f'amount * open_rate * (1 + {fee_open})') close_profit_abs = get_column_def( @@ -133,7 +138,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, sell_order_status, strategy, - ticker_interval, open_trade_price, close_profit_abs + timeframe, open_trade_price, close_profit_abs ) select id, lower(exchange), case @@ -155,7 +160,7 @@ def check_migrate(engine) -> None: {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, {sell_order_status} sell_order_status, - {strategy} strategy, {ticker_interval} ticker_interval, + {strategy} strategy, {timeframe} timeframe, {open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs from {table_back_name} """) @@ -232,7 +237,7 @@ class Trade(_DECL_BASE): sell_reason = Column(String, nullable=True) sell_order_status = Column(String, nullable=True) strategy = Column(String, nullable=True) - ticker_interval = Column(Integer, nullable=True) + timeframe = Column(Integer, nullable=True) def __init__(self, **kwargs): super().__init__(**kwargs) @@ -287,8 +292,8 @@ class Trade(_DECL_BASE): 'min_rate': self.min_rate, 'max_rate': self.max_rate, 'strategy': self.strategy, - 'ticker_interval': self.ticker_interval, - 'timeframe': self.ticker_interval, + 'ticker_interval': self.timeframe, + 'timeframe': self.timeframe, 'open_order_id': self.open_order_id, 'exchange': self.exchange, } diff --git a/tests/test_persistence.py b/tests/test_persistence.py index c15936cf6..bc5b315a1 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -469,6 +469,7 @@ def test_migrate_old(mocker, default_conf, fee): assert trade.fee_open_currency is None assert trade.fee_close_cost is None assert trade.fee_close_currency is None + assert trade.timeframe is None trade = Trade.query.filter(Trade.id == 2).first() assert trade.close_rate is not None @@ -512,11 +513,11 @@ def test_migrate_new(mocker, default_conf, fee, caplog): );""" insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee, open_rate, stake_amount, amount, open_date, - stop_loss, initial_stop_loss, max_rate) + stop_loss, initial_stop_loss, max_rate, ticker_interval) VALUES ('binance', 'ETC/BTC', 1, {fee}, 0.00258580, {stake}, {amount}, '2019-11-28 12:44:24.000000', - 0.0, 0.0, 0.0) + 0.0, 0.0, 0.0, '5m') """.format(fee=fee.return_value, stake=default_conf.get("stake_amount"), amount=amount @@ -554,7 +555,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert trade.initial_stop_loss == 0.0 assert trade.sell_reason is None assert trade.strategy is None - assert trade.ticker_interval is None + assert trade.timeframe == '5m' assert trade.stoploss_order_id is None assert trade.stoploss_last_update is None assert log_has("trying trades_bak1", caplog) From f9bb1a7f22ee086042e37d563ee67d90a12577a1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jun 2020 10:02:55 +0200 Subject: [PATCH 0042/1197] Update more occurances of ticker_interval --- freqtrade/data/btanalysis.py | 4 ++-- freqtrade/optimize/hyperopt_loss_interface.py | 1 - freqtrade/templates/sample_strategy.py | 2 +- tests/optimize/test_backtesting.py | 4 ++-- tests/optimize/test_hyperopt.py | 3 ++- tests/strategy/test_default_strategy.py | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index f98135c27..5948d933c 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -103,7 +103,7 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: "open_rate", "close_rate", "amount", "duration", "sell_reason", "fee_open", "fee_close", "open_rate_requested", "close_rate_requested", "stake_amount", "max_rate", "min_rate", "id", "exchange", - "stop_loss", "initial_stop_loss", "strategy", "ticker_interval"] + "stop_loss", "initial_stop_loss", "strategy", "timeframe"] trades = pd.DataFrame([(t.pair, t.open_date.replace(tzinfo=timezone.utc), @@ -121,7 +121,7 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: t.min_rate, t.id, t.exchange, t.stop_loss, t.initial_stop_loss, - t.strategy, t.ticker_interval + t.strategy, t.timeframe ) for t in Trade.get_trades().all()], columns=columns) diff --git a/freqtrade/optimize/hyperopt_loss_interface.py b/freqtrade/optimize/hyperopt_loss_interface.py index c2607a9a8..48407a8a8 100644 --- a/freqtrade/optimize/hyperopt_loss_interface.py +++ b/freqtrade/optimize/hyperopt_loss_interface.py @@ -14,7 +14,6 @@ class IHyperOptLoss(ABC): Interface for freqtrade hyperopt Loss functions. Defines the custom loss function (`hyperopt_loss_function()` which is evaluated every epoch.) """ - ticker_interval: str timeframe: str @staticmethod diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index f78489173..e269848d2 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -53,7 +53,7 @@ class SampleStrategy(IStrategy): # trailing_stop_positive_offset = 0.0 # Disabled / not configured # Optimal ticker interval for the strategy. - ticker_interval = '5m' + timeframe = '5m' # Run "populate_indicators()" only for new candle. process_only_new_candles = False diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 407604d9c..342d7689f 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -286,7 +286,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None: assert not backtesting.strategy.order_types["stoploss_on_exchange"] -def test_backtesting_init_no_ticker_interval(mocker, default_conf, caplog) -> None: +def test_backtesting_init_no_timeframe(mocker, default_conf, caplog) -> None: patch_exchange(mocker) del default_conf['timeframe'] default_conf['strategy_list'] = ['DefaultStrategy', @@ -453,7 +453,7 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None: t["close_rate"], 6) < round(ln.iloc[0]["high"], 6)) -def test_backtest_1min_ticker_interval(default_conf, fee, mocker, testdatadir) -> None: +def test_backtest_1min_timeframe(default_conf, fee, mocker, testdatadir) -> None: default_conf['ask_strategy']['use_sell_signal'] = False mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 4f5b3983a..564725709 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -197,7 +197,8 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None: "Using populate_sell_trend from the strategy.", caplog) assert log_has("Hyperopt class does not provide populate_buy_trend() method. " "Using populate_buy_trend from the strategy.", caplog) - assert hasattr(x, "ticker_interval") + assert hasattr(x, "ticker_interval") # DEPRECATED + assert hasattr(x, "timeframe") def test_hyperoptresolver_wrongname(mocker, default_conf, caplog) -> None: diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index df7e35197..1b1648db9 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -6,7 +6,7 @@ from .strats.default_strategy import DefaultStrategy def test_default_strategy_structure(): assert hasattr(DefaultStrategy, 'minimal_roi') assert hasattr(DefaultStrategy, 'stoploss') - assert hasattr(DefaultStrategy, 'ticker_interval') + assert hasattr(DefaultStrategy, 'timeframe') assert hasattr(DefaultStrategy, 'populate_indicators') assert hasattr(DefaultStrategy, 'populate_buy_trend') assert hasattr(DefaultStrategy, 'populate_sell_trend') From febc95dcdf95d9ee6a080d25684491320f24f236 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jun 2020 10:03:23 +0200 Subject: [PATCH 0043/1197] Update documentation to remove ticker_interval --- docs/bot-usage.md | 25 ++++++++++++------------- docs/configuration.md | 4 ++-- docs/hyperopt.md | 8 ++++---- docs/plotting.md | 15 ++++++++------- docs/strategy-customization.md | 14 +++++++------- docs/strategy_analysis_example.md | 4 ++-- 6 files changed, 35 insertions(+), 35 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index b1649374a..b5af60f2b 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -72,7 +72,6 @@ Strategy arguments: Specify strategy class name which will be used by the bot. --strategy-path PATH Specify additional strategy lookup path. -. ``` @@ -197,7 +196,7 @@ Backtesting also uses the config specified via `-c/--config`. ``` usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] - [--strategy-path PATH] [-i TICKER_INTERVAL] + [--strategy-path PATH] [-i TIMEFRAME] [--timerange TIMERANGE] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] [--eps] [--dmmp] @@ -206,7 +205,7 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH] optional arguments: -h, --help show this help message and exit - -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL + -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`). --timerange TIMERANGE @@ -280,7 +279,7 @@ to find optimal parameter values for your strategy. ``` usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] - [-i TICKER_INTERVAL] [--timerange TIMERANGE] + [-i TIMEFRAME] [--timerange TIMERANGE] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] [--hyperopt NAME] [--hyperopt-path PATH] [--eps] @@ -292,7 +291,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] optional arguments: -h, --help show this help message and exit - -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL + -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`). --timerange TIMERANGE @@ -323,7 +322,7 @@ optional arguments: --print-all Print all results, not only the best ones. --no-color Disable colorization of hyperopt results. May be useful if you are redirecting output to a file. - --print-json Print best results in JSON format. + --print-json Print output in JSON format. -j JOBS, --job-workers JOBS The number of concurrently running jobs for hyperoptimization (hyperopt worker processes). If -1 @@ -341,11 +340,11 @@ optional arguments: class (IHyperOptLoss). Different functions can generate completely different results, since the target for optimization is different. Built-in - Hyperopt-loss-functions are: - DefaultHyperOptLoss, OnlyProfitHyperOptLoss, - SharpeHyperOptLoss, SharpeHyperOptLossDaily, - SortinoHyperOptLoss, SortinoHyperOptLossDaily. - (default: `DefaultHyperOptLoss`). + Hyperopt-loss-functions are: DefaultHyperOptLoss, + OnlyProfitHyperOptLoss, SharpeHyperOptLoss, + SharpeHyperOptLossDaily, SortinoHyperOptLoss, + SortinoHyperOptLossDaily.(default: + `DefaultHyperOptLoss`). Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). @@ -378,13 +377,13 @@ To know your trade expectancy and winrate against historical data, you can use E ``` usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] - [-i TICKER_INTERVAL] [--timerange TIMERANGE] + [-i TIMEFRAME] [--timerange TIMERANGE] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] [--stoplosses STOPLOSS_RANGE] optional arguments: -h, --help show this help message and exit - -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL + -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`). --timerange TIMERANGE diff --git a/docs/configuration.md b/docs/configuration.md index a1cb45e0f..4dbcc6d2a 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 | `amend_last_stake_amount` | Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade).
*Defaults to `false`.*
**Datatype:** Boolean | `last_stake_amount_min_ratio` | Defines minimum stake amount that has to be left and executed. Applies only to the last stake amount when it's amended to a reduced value (i.e. if `amend_last_stake_amount` is set to `true`). [More information below](#configuring-amount-per-trade).
*Defaults to `0.5`.*
**Datatype:** Float (as ratio) | `amount_reserve_percent` | Reserve some amount in min pair stake amount. The bot will reserve `amount_reserve_percent` + stoploss value when calculating min pair stake amount in order to avoid possible trade refusals.
*Defaults to `0.05` (5%).*
**Datatype:** Positive Float as ratio. -| `ticker_interval` | The timeframe (ticker interval) to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy).
**Datatype:** String +| `timeframe` | The timeframe (former 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` | 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 @@ -125,7 +125,7 @@ The following parameters can be set in either configuration file or strategy. Values set in the configuration file always overwrite values set in the strategy. * `minimal_roi` -* `ticker_interval` +* `timeframe` * `stoploss` * `trailing_stop` * `trailing_stop_positive` diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 8efc51a39..a4d36530c 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -124,9 +124,9 @@ To avoid naming collisions in the search-space, please prefix all sell-spaces wi #### Using timeframe as a part of the Strategy -The Strategy class exposes the timeframe (ticker interval) value as the `self.ticker_interval` attribute. -The same value is available as class-attribute `HyperoptName.ticker_interval`. -In the case of the linked sample-value this would be `SampleHyperOpt.ticker_interval`. +The Strategy class exposes the timeframe value as the `self.timeframe` attribute. +The same value is available as class-attribute `HyperoptName.timeframe`. +In the case of the linked sample-value this would be `SampleHyperOpt.timeframe`. ## Solving a Mystery @@ -403,7 +403,7 @@ As stated in the comment, you can also use it as the value of the `minimal_roi` #### Default ROI Search Space -If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the ticker_interval used. By default the values vary in the following ranges (for some of the most used timeframes, values are rounded to 5 digits after the decimal point): +If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the timeframe used. By default the values vary in the following ranges (for some of the most used timeframes, values are rounded to 5 digits after the decimal point): | # step | 1m | | 5m | | 1h | | 1d | | | ------ | ------ | ----------------- | -------- | ----------- | ---------- | ----------------- | ------------ | ----------------- | diff --git a/docs/plotting.md b/docs/plotting.md index be83065a6..d3a2df1c1 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -31,7 +31,7 @@ usage: freqtrade plot-dataframe [-h] [-v] [--logfile FILE] [-V] [-c PATH] [--plot-limit INT] [--db-url PATH] [--trade-source {DB,file}] [--export EXPORT] [--export-filename PATH] - [--timerange TIMERANGE] [-i TICKER_INTERVAL] + [--timerange TIMERANGE] [-i TIMEFRAME] [--no-trades] optional arguments: @@ -65,7 +65,7 @@ optional arguments: _today.json` --timerange TIMERANGE Specify what timerange of data to use. - -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL + -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`). --no-trades Skip using trades from backtesting file and DB. @@ -227,7 +227,7 @@ usage: freqtrade plot-profit [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-p PAIRS [PAIRS ...]] [--timerange TIMERANGE] [--export EXPORT] [--export-filename PATH] [--db-url PATH] - [--trade-source {DB,file}] [-i TICKER_INTERVAL] + [--trade-source {DB,file}] [-i TIMEFRAME] optional arguments: -h, --help show this help message and exit @@ -250,7 +250,7 @@ optional arguments: --trade-source {DB,file} Specify the source for trades (Can be DB or file (backtest file)) Default: file - -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL + -i TIMEFRAME, --timeframe TIMEFRAME, --ticker-interval TIMEFRAME Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`). @@ -261,9 +261,10 @@ Common arguments: details. -V, --version show program's version number and exit -c PATH, --config PATH - Specify configuration file (default: `config.json`). - Multiple --config options may be used. Can be set to - `-` to read config from stdin. + Specify configuration file (default: + `userdir/config.json` or `config.json` whichever + exists). Multiple --config options may be used. Can be + set to `-` to read config from stdin. -d PATH, --datadir PATH Path to directory with historical backtesting data. --userdir PATH, --user-data-dir PATH diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 7197b0fba..ca0d9a9a3 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -248,7 +248,7 @@ minimal_roi = { While technically not completely disabled, this would sell once the trade reaches 10000% Profit. -To use times based on candle duration (ticker_interval or timeframe), the following snippet can be handy. +To use times based on candle duration (timeframe), the following snippet can be handy. This will allow you to change the ticket_interval for the strategy, and ROI times will still be set as candles (e.g. after 3 candles ...) ``` python @@ -256,12 +256,12 @@ from freqtrade.exchange import timeframe_to_minutes class AwesomeStrategy(IStrategy): - ticker_interval = "1d" - ticker_interval_mins = timeframe_to_minutes(ticker_interval) + timeframe = "1d" + timeframe_mins = timeframe_to_minutes(timeframe) minimal_roi = { "0": 0.05, # 5% for the first 3 candles - str(ticker_interval_mins * 3)): 0.02, # 2% after 3 candles - str(ticker_interval_mins * 6)): 0.01, # 1% After 6 candles + str(timeframe_mins * 3)): 0.02, # 2% after 3 candles + str(timeframe_mins * 6)): 0.01, # 1% After 6 candles } ``` @@ -290,7 +290,7 @@ Common values are `"1m"`, `"5m"`, `"15m"`, `"1h"`, however all values supported Please note that the same buy/sell signals may work well with one timeframe, but not with the others. -This setting is accessible within the strategy methods as the `self.ticker_interval` attribute. +This setting is accessible within the strategy methods as the `self.timeframe` attribute. ### Metadata dict @@ -400,7 +400,7 @@ This is where calling `self.dp.current_whitelist()` comes in handy. class SampleStrategy(IStrategy): # strategy init stuff... - ticker_interval = '5m' + timeframe = '5m' # more strategy init stuff.. diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index d26d684ce..6b4ad567f 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -18,7 +18,7 @@ config = Configuration.from_files([]) # config = Configuration.from_files(["config.json"]) # Define some constants -config["ticker_interval"] = "5m" +config["timeframe"] = "5m" # Name of the strategy class config["strategy"] = "SampleStrategy" # Location of the data @@ -33,7 +33,7 @@ pair = "BTC_USDT" from freqtrade.data.history import load_pair_history candles = load_pair_history(datadir=data_location, - timeframe=config["ticker_interval"], + timeframe=config["timeframe"], pair=pair) # Confirm success From 33b7046260fc500fe5ae2c60ab999f67ac7f0ff4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jun 2020 10:06:26 +0200 Subject: [PATCH 0044/1197] Update more documentation --- README.md | 5 +++-- docs/backtesting.md | 10 +++++----- docs/bot-usage.md | 25 +++++++++++++++++++------ docs/edge.md | 2 +- docs/hyperopt.md | 2 +- docs/strategy-customization.md | 2 +- docs/utils.md | 2 +- freqtrade/commands/arguments.py | 2 +- 8 files changed, 32 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index cfb384702..7e0acde46 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,8 @@ positional arguments: new-hyperopt Create new hyperopt new-strategy Create new strategy download-data Download backtesting data. - convert-data Convert candle (OHLCV) data from one format to another. + convert-data Convert candle (OHLCV) data from one format to + another. convert-trade-data Convert trade data from one format to another. backtesting Backtesting module. edge Edge module. @@ -94,7 +95,7 @@ positional arguments: list-markets Print markets on exchange. list-pairs Print pairs on exchange. list-strategies Print available strategies. - list-timeframes Print available ticker intervals (timeframes) for the exchange. + list-timeframes Print available timeframes for the exchange. show-trades Show trades. test-pairlist Test your pairlist configuration. plot-dataframe Plot candles with indicators. diff --git a/docs/backtesting.md b/docs/backtesting.md index 9b2997510..51b2e953b 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -12,7 +12,7 @@ real data. This is what we call [backtesting](https://en.wikipedia.org/wiki/Backtesting). Backtesting will use the crypto-currencies (pairs) from your config file and load historical candle (OHCLV) data from `user_data/data/` by default. -If no data is available for the exchange / pair / timeframe (ticker interval) combination, backtesting will ask you to download them first using `freqtrade download-data`. +If no data is available for the exchange / pair / timeframe combination, backtesting will ask you to download them first using `freqtrade download-data`. For details on downloading, please refer to the [Data Downloading](data-download.md) section in the documentation. The result of backtesting will confirm if your bot has better odds of making a profit than a loss. @@ -35,7 +35,7 @@ freqtrade backtesting #### With 1 min candle (OHLCV) data ```bash -freqtrade backtesting --ticker-interval 1m +freqtrade backtesting --timeframe 1m ``` #### Using a different on-disk historical candle (OHLCV) data source @@ -58,7 +58,7 @@ Where `-s SampleStrategy` refers to the class name within the strategy file `sam #### Comparing multiple Strategies ```bash -freqtrade backtesting --strategy-list SampleStrategy1 AwesomeStrategy --ticker-interval 5m +freqtrade backtesting --strategy-list SampleStrategy1 AwesomeStrategy --timeframe 5m ``` Where `SampleStrategy1` and `AwesomeStrategy` refer to class names of strategies. @@ -228,13 +228,13 @@ You can then load the trades to perform further analysis as shown in our [data a To compare multiple strategies, a list of Strategies can be provided to backtesting. -This is limited to 1 timeframe (ticker interval) value per run. However, data is only loaded once from disk so if you have multiple +This is limited to 1 timeframe value per run. However, data is only loaded once from disk so if you have multiple strategies you'd like to compare, this will give a nice runtime boost. All listed Strategies need to be in the same directory. ``` bash -freqtrade backtesting --timerange 20180401-20180410 --ticker-interval 5m --strategy-list Strategy001 Strategy002 --export trades +freqtrade backtesting --timerange 20180401-20180410 --timeframe 5m --strategy-list Strategy001 Strategy002 --export trades ``` This will save the results to `user_data/backtest_results/backtest-result-.json`, injecting the strategy-name into the target filename. diff --git a/docs/bot-usage.md b/docs/bot-usage.md index b5af60f2b..40ff3d82b 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -9,22 +9,35 @@ This page explains the different parameters of the bot and how to run it. ``` usage: freqtrade [-h] [-V] - {trade,backtesting,edge,hyperopt,create-userdir,list-exchanges,list-timeframes,download-data,plot-dataframe,plot-profit} + {trade,create-userdir,new-config,new-hyperopt,new-strategy,download-data,convert-data,convert-trade-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,plot-dataframe,plot-profit} ... Free, open source crypto trading bot positional arguments: - {trade,backtesting,edge,hyperopt,create-userdir,list-exchanges,list-timeframes,download-data,plot-dataframe,plot-profit} + {trade,create-userdir,new-config,new-hyperopt,new-strategy,download-data,convert-data,convert-trade-data,backtesting,edge,hyperopt,hyperopt-list,hyperopt-show,list-exchanges,list-hyperopts,list-markets,list-pairs,list-strategies,list-timeframes,show-trades,test-pairlist,plot-dataframe,plot-profit} trade Trade module. + create-userdir Create user-data directory. + new-config Create new config + new-hyperopt Create new hyperopt + new-strategy Create new strategy + download-data Download backtesting data. + convert-data Convert candle (OHLCV) data from one format to + another. + convert-trade-data Convert trade data from one format to another. backtesting Backtesting module. edge Edge module. hyperopt Hyperopt module. - create-userdir Create user-data directory. + hyperopt-list List Hyperopt results + hyperopt-show Show details of Hyperopt results list-exchanges Print available exchanges. - list-timeframes Print available ticker intervals (timeframes) for the - exchange. - download-data Download backtesting data. + list-hyperopts Print available hyperopt classes. + list-markets Print markets on exchange. + list-pairs Print pairs on exchange. + list-strategies Print available strategies. + list-timeframes Print available timeframes for the exchange. + show-trades Show trades. + test-pairlist Test your pairlist configuration. plot-dataframe Plot candles with indicators. plot-profit Generate plot showing profits. diff --git a/docs/edge.md b/docs/edge.md index 029844c0b..28a7f14cb 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -156,7 +156,7 @@ Edge module has following configuration options: | `minimum_winrate` | It filters out pairs which don't have at least minimum_winrate.
This comes handy if you want to be conservative and don't comprise win rate in favour of risk reward ratio.
*Defaults to `0.60`.*
**Datatype:** Float | `minimum_expectancy` | It filters out pairs which have the expectancy lower than this number.
Having an expectancy of 0.20 means if you put 10$ on a trade you expect a 12$ return.
*Defaults to `0.20`.*
**Datatype:** Float | `min_trade_number` | When calculating *W*, *R* and *E* (expectancy) against historical data, you always want to have a minimum number of trades. The more this number is the more Edge is reliable.
Having a win rate of 100% on a single trade doesn't mean anything at all. But having a win rate of 70% over past 100 trades means clearly something.
*Defaults to `10` (it is highly recommended not to decrease this number).*
**Datatype:** Integer -| `max_trade_duration_minute` | Edge will filter out trades with long duration. If a trade is profitable after 1 month, it is hard to evaluate the strategy based on it. But if most of trades are profitable and they have maximum duration of 30 minutes, then it is clearly a good sign.
**NOTICE:** While configuring this value, you should take into consideration your timeframe (ticker interval). As an example filtering out trades having duration less than one day for a strategy which has 4h interval does not make sense. Default value is set assuming your strategy interval is relatively small (1m or 5m, etc.).
*Defaults to `1440` (one day).*
**Datatype:** Integer +| `max_trade_duration_minute` | Edge will filter out trades with long duration. If a trade is profitable after 1 month, it is hard to evaluate the strategy based on it. But if most of trades are profitable and they have maximum duration of 30 minutes, then it is clearly a good sign.
**NOTICE:** While configuring this value, you should take into consideration your timeframe. As an example filtering out trades having duration less than one day for a strategy which has 4h interval does not make sense. Default value is set assuming your strategy interval is relatively small (1m or 5m, etc.).
*Defaults to `1440` (one day).*
**Datatype:** Integer | `remove_pumps` | Edge will remove sudden pumps in a given market while going through historical data. However, given that pumps happen very often in crypto markets, we recommend you keep this off.
*Defaults to `false`.*
**Datatype:** Boolean ## Running Edge independently diff --git a/docs/hyperopt.md b/docs/hyperopt.md index a4d36530c..c9c87ead3 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -412,7 +412,7 @@ If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace f | 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 | | 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 | -These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe (ticker interval) used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used. +These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used. If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index ca0d9a9a3..70013c821 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -142,7 +142,7 @@ By letting the bot know how much history is needed, backtest trades can start at Let's try to backtest 1 month (January 2019) of 5m candles using the an example strategy with EMA100, as above. ``` bash -freqtrade backtesting --timerange 20190101-20190201 --ticker-interval 5m +freqtrade backtesting --timerange 20190101-20190201 --timeframe 5m ``` Assuming `startup_candle_count` is set to 100, backtesting knows it needs 100 candles to generate valid buy signals. It will load data from `20190101 - (100 * 5m)` - which is ~2019-12-31 15:30:00. diff --git a/docs/utils.md b/docs/utils.md index 7ed31376f..793c84a93 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -62,7 +62,7 @@ $ freqtrade new-config --config config_binance.json ? Please insert your stake currency: BTC ? Please insert your stake amount: 0.05 ? Please insert max_open_trades (Integer or 'unlimited'): 3 -? Please insert your timeframe (ticker interval): 5m +? Please insert your desired timeframe (e.g. 5m): 5m ? Please insert your display Currency (for reporting): USD ? Select exchange binance ? Do you want to enable Telegram? No diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 36e3dedf0..72f2a02f0 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -318,7 +318,7 @@ class Arguments: # Add list-timeframes subcommand list_timeframes_cmd = subparsers.add_parser( 'list-timeframes', - help='Print available ticker intervals (timeframes) for the exchange.', + help='Print available timeframes for the exchange.', parents=[_common_parser], ) list_timeframes_cmd.set_defaults(func=start_list_timeframes) From 8e1a664a48f7b315907a9bbdd3b7f8fd790a8211 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jun 2020 10:11:50 +0200 Subject: [PATCH 0045/1197] Add test for deprecation updating --- tests/strategy/strats/legacy_strategy.py | 1 + tests/strategy/test_strategy.py | 2 ++ tests/test_configuration.py | 12 ++++++++++++ 3 files changed, 15 insertions(+) diff --git a/tests/strategy/strats/legacy_strategy.py b/tests/strategy/strats/legacy_strategy.py index 89ce3f8cb..9cbce0ad5 100644 --- a/tests/strategy/strats/legacy_strategy.py +++ b/tests/strategy/strats/legacy_strategy.py @@ -31,6 +31,7 @@ class TestStrategyLegacy(IStrategy): stoploss = -0.10 # Optimal ticker interval for the strategy + # Keep the legacy value here to test compatibility ticker_interval = '5m' def populate_indicators(self, dataframe: DataFrame) -> DataFrame: diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py index 1bb45f28c..f2cf11712 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -370,6 +370,8 @@ def test_call_deprecated_function(result, monkeypatch, default_conf): assert strategy._buy_fun_len == 2 assert strategy._sell_fun_len == 2 assert strategy.INTERFACE_VERSION == 1 + assert strategy.timeframe == '5m' + assert strategy.ticker_interval == '5m' indicator_df = strategy.advise_indicators(result, metadata=metadata) assert isinstance(indicator_df, DataFrame) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 1ed97f728..9602f6389 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1137,3 +1137,15 @@ def test_process_deprecated_setting(mocker, default_conf, caplog): 'sectionB', 'deprecated_setting') assert not log_has_re('DEPRECATED', caplog) assert default_conf['sectionA']['new_setting'] == 'valA' + + +def test_process_deprecated_ticker_interval(mocker, default_conf, caplog): + message = "DEPRECATED: Please use 'timeframe' instead of 'ticker_interval." + process_temporary_deprecated_settings(default_conf) + assert not log_has(message, caplog) + + del default_conf['timeframe'] + default_conf['ticker_interval'] = '15m' + process_temporary_deprecated_settings(default_conf) + assert log_has(message, caplog) + assert default_conf['ticker_interval'] == '15m' From a8005819c97461055bf29a0bae4a1e662a313c28 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jun 2020 10:19:27 +0200 Subject: [PATCH 0046/1197] Add class-level attributes to hyperopt and strategy --- freqtrade/optimize/hyperopt_interface.py | 3 ++- freqtrade/strategy/interface.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 20209d8a9..00353cbf4 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -31,7 +31,8 @@ class IHyperOpt(ABC): Class attributes you can use: ticker_interval -> int: value of the ticker interval to use for the strategy """ - ticker_interval: str + ticker_interval: str # deprecated + timeframe: str def __init__(self, config: dict) -> None: self.config = config diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 7bf750249..f9f3a3678 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -86,6 +86,7 @@ class IStrategy(ABC): trailing_only_offset_is_reached = False # associated timeframe + ticker_interval: str # DEPRECATED timeframe: str # Optional order types From b106c886309f446861bd01b4062e3e285c23a3ff Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jun 2020 13:08:21 +0200 Subject: [PATCH 0047/1197] Add test case for strategy overwriting --- tests/strategy/test_strategy.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py index f2cf11712..85cb8c132 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -358,8 +358,9 @@ def test_deprecate_populate_indicators(result, default_conf): @pytest.mark.filterwarnings("ignore:deprecated") -def test_call_deprecated_function(result, monkeypatch, default_conf): +def test_call_deprecated_function(result, monkeypatch, default_conf, caplog): default_location = Path(__file__).parent / "strats" + del default_conf['timeframe'] default_conf.update({'strategy': 'TestStrategyLegacy', 'strategy_path': default_location}) strategy = StrategyResolver.load_strategy(default_conf) @@ -385,6 +386,9 @@ def test_call_deprecated_function(result, monkeypatch, default_conf): assert isinstance(selldf, DataFrame) assert 'sell' in selldf + assert log_has('DEPRECATED: Please migrate to using timeframe instead of ticker_interval.', + caplog) + def test_strategy_interface_versioning(result, monkeypatch, default_conf): default_conf.update({'strategy': 'DefaultStrategy'}) From 9995a5899fb16dcfe1e13a7525077142a4152076 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Tue, 2 Jun 2020 16:25:22 +0300 Subject: [PATCH 0048/1197] Fix merge --- freqtrade/persistence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index a3b394769..bb9db703d 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -108,7 +108,7 @@ def check_migrate(engine) -> None: sell_reason = get_column_def(cols, 'sell_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') # If ticker-interval existed use that, else null. - if has_column(cols, '): + if has_column(cols, 'ticker_interval'): timeframe = get_column_def(cols, 'timeframe', 'ticker_interval') else: timeframe = get_column_def(cols, 'timeframe', 'null') From edf8e39bc11c6c37b06459ac9d9bdfc38f86eb9a Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 2 Jun 2020 17:57:45 +0300 Subject: [PATCH 0049/1197] Fix tests after merge --- tests/rpc/test_rpc.py | 2 -- tests/rpc/test_rpc_apiserver.py | 4 +--- tests/test_persistence.py | 2 -- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 9a55c7639..44d54bad6 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -65,7 +65,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'min_rate': ANY, 'max_rate': ANY, 'strategy': ANY, - 'ticker_interval': ANY, 'timeframe': ANY, 'open_order_id': ANY, 'close_date': None, @@ -124,7 +123,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'min_rate': ANY, 'max_rate': ANY, 'strategy': ANY, - 'ticker_interval': ANY, 'timeframe': ANY, 'open_order_id': ANY, 'close_date': None, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index b3859c4e6..5547ee004 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -322,7 +322,7 @@ def test_api_show_config(botclient, mocker): assert_response(rc) assert 'dry_run' in rc.json assert rc.json['exchange'] == 'bittrex' - assert rc.json['ticker_interval'] == '5m' + assert rc.json['timeframe'] == '5m' assert rc.json['state'] == 'running' assert not rc.json['trailing_stop'] @@ -547,7 +547,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'sell_reason': None, 'sell_order_status': None, 'strategy': 'DefaultStrategy', - 'ticker_interval': 5, 'timeframe': 5, 'exchange': 'bittrex', }] @@ -671,7 +670,6 @@ def test_api_forcebuy(botclient, mocker, fee): 'sell_reason': None, 'sell_order_status': None, 'strategy': None, - 'ticker_interval': None, 'timeframe': None, 'exchange': 'bittrex', } diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 69ee014c5..d55f24719 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -776,7 +776,6 @@ def test_to_json(default_conf, fee): 'min_rate': None, 'max_rate': None, 'strategy': None, - 'ticker_interval': None, 'timeframe': None, 'exchange': 'bittrex', } @@ -838,7 +837,6 @@ def test_to_json(default_conf, fee): 'sell_reason': None, 'sell_order_status': None, 'strategy': None, - 'ticker_interval': None, 'timeframe': None, 'exchange': 'bittrex', } From 1a5dba9a79a50e567bbc16da3da63a82294c7802 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jun 2020 19:39:17 +0200 Subject: [PATCH 0050/1197] Revert "Fix tests after merge" This reverts commit edf8e39bc11c6c37b06459ac9d9bdfc38f86eb9a. --- tests/rpc/test_rpc.py | 2 ++ tests/rpc/test_rpc_apiserver.py | 4 +++- tests/test_persistence.py | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 44d54bad6..9a55c7639 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -65,6 +65,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'min_rate': ANY, 'max_rate': ANY, 'strategy': ANY, + 'ticker_interval': ANY, 'timeframe': ANY, 'open_order_id': ANY, 'close_date': None, @@ -123,6 +124,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'min_rate': ANY, 'max_rate': ANY, 'strategy': ANY, + 'ticker_interval': ANY, 'timeframe': ANY, 'open_order_id': ANY, 'close_date': None, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 5547ee004..b3859c4e6 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -322,7 +322,7 @@ def test_api_show_config(botclient, mocker): assert_response(rc) assert 'dry_run' in rc.json assert rc.json['exchange'] == 'bittrex' - assert rc.json['timeframe'] == '5m' + assert rc.json['ticker_interval'] == '5m' assert rc.json['state'] == 'running' assert not rc.json['trailing_stop'] @@ -547,6 +547,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'sell_reason': None, 'sell_order_status': None, 'strategy': 'DefaultStrategy', + 'ticker_interval': 5, 'timeframe': 5, 'exchange': 'bittrex', }] @@ -670,6 +671,7 @@ def test_api_forcebuy(botclient, mocker, fee): 'sell_reason': None, 'sell_order_status': None, 'strategy': None, + 'ticker_interval': None, 'timeframe': None, 'exchange': 'bittrex', } diff --git a/tests/test_persistence.py b/tests/test_persistence.py index d55f24719..69ee014c5 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -776,6 +776,7 @@ def test_to_json(default_conf, fee): 'min_rate': None, 'max_rate': None, 'strategy': None, + 'ticker_interval': None, 'timeframe': None, 'exchange': 'bittrex', } @@ -837,6 +838,7 @@ def test_to_json(default_conf, fee): 'sell_reason': None, 'sell_order_status': None, 'strategy': None, + 'ticker_interval': None, 'timeframe': None, 'exchange': 'bittrex', } From 02fca141a03fccf9fd49dffc3ba8b30e84c6dfbc Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jun 2020 19:43:15 +0200 Subject: [PATCH 0051/1197] Readd ticker_interval to trade api response --- freqtrade/persistence.py | 1 + tests/rpc/test_rpc_apiserver.py | 1 + 2 files changed, 2 insertions(+) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index bb9db703d..628c9cf22 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -258,6 +258,7 @@ class Trade(_DECL_BASE): 'amount': round(self.amount, 8), 'stake_amount': round(self.stake_amount, 8), 'strategy': self.strategy, + 'ticker_interval': self.timeframe, # DEPRECATED 'timeframe': self.timeframe, 'fee_open': self.fee_open, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index b3859c4e6..daee0186a 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -323,6 +323,7 @@ def test_api_show_config(botclient, mocker): assert 'dry_run' in rc.json assert rc.json['exchange'] == 'bittrex' assert rc.json['ticker_interval'] == '5m' + assert rc.json['timeframe'] == '5m' assert rc.json['state'] == 'running' assert not rc.json['trailing_stop'] From b22e3a67d86b27d06ab2c1b4bee0d8f0b2f0f382 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jun 2020 20:29:48 +0200 Subject: [PATCH 0052/1197] rename symbol_is_pair to market_is_tradable Make it part of the exchange class, so subclasses can override this --- freqtrade/commands/list_commands.py | 4 ++-- freqtrade/exchange/__init__.py | 3 +-- freqtrade/exchange/exchange.py | 31 +++++++++++++---------------- freqtrade/exchange/ftx.py | 12 ++++++++++- freqtrade/exchange/kraken.py | 12 ++++++++++- 5 files changed, 39 insertions(+), 23 deletions(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index bc4bd694f..503f8a4ee 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -14,7 +14,7 @@ from freqtrade.configuration import setup_utils_configuration from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES from freqtrade.exceptions import OperationalException from freqtrade.exchange import (available_exchanges, ccxt_exchanges, - market_is_active, symbol_is_pair) + market_is_active) from freqtrade.misc import plural from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode @@ -163,7 +163,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: tabular_data.append({'Id': v['id'], 'Symbol': v['symbol'], 'Base': v['base'], 'Quote': v['quote'], 'Active': market_is_active(v), - **({'Is pair': symbol_is_pair(v)} + **({'Is pair': exchange.market_is_tradable(v)} if not pairs_only else {})}) if (args.get('print_one_column', False) or diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index a39f8f5df..bdf1f91ec 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -12,8 +12,7 @@ from freqtrade.exchange.exchange import (timeframe_to_seconds, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_prev_date) -from freqtrade.exchange.exchange import (market_is_active, - symbol_is_pair) +from freqtrade.exchange.exchange import (market_is_active) from freqtrade.exchange.kraken import Kraken from freqtrade.exchange.binance import Binance from freqtrade.exchange.bibox import Bibox diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 09f700bbb..bec8b9686 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -214,7 +214,7 @@ class Exchange: if quote_currencies: markets = {k: v for k, v in markets.items() if v['quote'] in quote_currencies} if pairs_only: - markets = {k: v for k, v in markets.items() if symbol_is_pair(v)} + markets = {k: v for k, v in markets.items() if self.symbol_is_pair(v)} if active_only: markets = {k: v for k, v in markets.items() if market_is_active(v)} return markets @@ -238,6 +238,19 @@ class Exchange: """ return self.markets.get(pair, {}).get('base', '') + def market_is_tradable(self, market: Dict[str, Any]) -> bool: + """ + Check if the market symbol is tradable by Freqtrade. + By default, checks if it's splittable by `/` and both sides correspond to base / quote + """ + symbol_parts = market['symbol'].split('/') + return (len(symbol_parts) == 2 and + len(symbol_parts[0]) > 0 and + len(symbol_parts[1]) > 0 and + symbol_parts[0] == market.get('base') and + symbol_parts[1] == market.get('quote') + ) + def klines(self, pair_interval: Tuple[str, str], copy: bool = True) -> DataFrame: if pair_interval in self._klines: return self._klines[pair_interval].copy() if copy else self._klines[pair_interval] @@ -1210,22 +1223,6 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime: return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) -def symbol_is_pair(market_symbol: Dict[str, Any], base_currency: str = None, - quote_currency: str = None) -> bool: - """ - Check if the market symbol is a pair, i.e. that its symbol consists of the base currency and the - quote currency separated by '/' character. If base_currency and/or quote_currency is passed, - it also checks that the symbol contains appropriate base and/or quote currency part before - and after the separating character correspondingly. - """ - symbol_parts = market_symbol['symbol'].split('/') - return (len(symbol_parts) == 2 and - (market_symbol.get('base') == base_currency - if base_currency else len(symbol_parts[0]) > 0) and - (market_symbol.get('quote') == quote_currency - if quote_currency else len(symbol_parts[1]) > 0)) - - def market_is_active(market: Dict) -> bool: """ Return True if the market is active. diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 75915122b..cad11bbfa 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -1,6 +1,6 @@ """ FTX exchange subclass """ import logging -from typing import Dict +from typing import Any, Dict from freqtrade.exchange import Exchange @@ -12,3 +12,13 @@ class Ftx(Exchange): _ft_has: Dict = { "ohlcv_candle_limit": 1500, } + + def market_is_tradable(self, market: Dict[str, Any]) -> bool: + """ + Check if the market symbol is tradable by Freqtrade. + Default checks + check if pair is darkpool pair. + """ + parent_check = super().market_is_tradable(market) + + return (parent_check and + market.get('spot', False) is True) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 932d82a27..af75ef9b2 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -1,6 +1,6 @@ """ Kraken exchange subclass """ import logging -from typing import Dict +from typing import Any, Dict import ccxt @@ -21,6 +21,16 @@ class Kraken(Exchange): "trades_pagination_arg": "since", } + def market_is_tradable(self, market: Dict[str, Any]) -> bool: + """ + Check if the market symbol is tradable by Freqtrade. + Default checks + check if pair is darkpool pair. + """ + parent_check = super().market_is_tradable(market) + + return (parent_check and + market.get('darkpool', False) is False) + @retrier def get_balances(self) -> dict: if self._config['dry_run']: From b74a3addc65514aa8f7494ca995caa4bee74c4b5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jun 2020 20:30:31 +0200 Subject: [PATCH 0053/1197] Update tests --- freqtrade/exchange/ftx.py | 2 +- tests/exchange/test_exchange.py | 55 +++++++++++++++++++++------------ 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index cad11bbfa..e5f083fb6 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -16,7 +16,7 @@ class Ftx(Exchange): def market_is_tradable(self, market: Dict[str, Any]) -> bool: """ Check if the market symbol is tradable by Freqtrade. - Default checks + check if pair is darkpool pair. + Default checks + check if pair is spot pair (no futures trading yet). """ parent_check = super().market_is_tradable(market) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index e40f691a8..b87acc27c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -15,7 +15,7 @@ from freqtrade.exceptions import (DependencyException, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken from freqtrade.exchange.common import API_RETRY_COUNT -from freqtrade.exchange.exchange import (market_is_active, symbol_is_pair, +from freqtrade.exchange.exchange import (market_is_active, timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, @@ -2117,25 +2117,42 @@ def test_timeframe_to_next_date(): assert timeframe_to_next_date("5m") > date -@pytest.mark.parametrize("market_symbol,base_currency,quote_currency,expected_result", [ - ("BTC/USDT", None, None, True), - ("USDT/BTC", None, None, True), - ("BTCUSDT", None, None, False), - ("BTC/USDT", None, "USDT", True), - ("USDT/BTC", None, "USDT", False), - ("BTCUSDT", None, "USDT", False), - ("BTC/USDT", "BTC", None, True), - ("USDT/BTC", "BTC", None, False), - ("BTCUSDT", "BTC", None, False), - ("BTC/USDT", "BTC", "USDT", True), - ("BTC/USDT", "USDT", "BTC", False), - ("BTC/USDT", "BTC", "USD", False), - ("BTCUSDT", "BTC", "USDT", False), - ("BTC/", None, None, False), - ("/USDT", None, None, False), +@pytest.mark.parametrize("market_symbol,base,quote,exchange,add_dict,expected_result", [ + ("BTC/USDT", 'BTC', 'USDT', "binance", {}, True), + ("USDT/BTC", 'USDT', 'BTC', "binance", {}, True), + ("USDT/BTC", 'BTC', 'USDT', "binance", {}, False), # Reversed currencies + ("BTCUSDT", 'BTC', 'USDT', "binance", {}, False), # No seperating / + ("BTCUSDT", None, "USDT", "binance", {}, False), # + ("USDT/BTC", "BTC", None, "binance", {}, False), + ("BTCUSDT", "BTC", None, "binance", {}, False), + ("BTC/USDT", "BTC", "USDT", "binance", {}, True), + ("BTC/USDT", "USDT", "BTC", "binance", {}, False), # reversed currencies + ("BTC/USDT", "BTC", "USD", "binance", {}, False), # Wrong quote currency + ("BTC/", "BTC", 'UNK', "binance", {}, False), + ("/USDT", 'UNK', 'USDT', "binance", {}, False), + ("BTC/EUR", 'BTC', 'EUR', "kraken", {"darkpool": False}, True), + ("EUR/BTC", 'EUR', 'BTC', "kraken", {"darkpool": False}, True), + ("EUR/BTC", 'BTC', 'EUR', "kraken", {"darkpool": False}, False), # Reversed currencies + ("BTC/EUR", 'BTC', 'USD', "kraken", {"darkpool": False}, False), # wrong quote currency + ("BTC/EUR", 'BTC', 'EUR', "kraken", {"darkpool": True}, False), # no darkpools + ("BTC/EUR.d", 'BTC', 'EUR', "kraken", {"darkpool": True}, False), # no darkpools + ("BTC/USD", 'BTC', 'USD', "ftx", {'spot': True}, True), + ("USD/BTC", 'USD', 'BTC', "ftx", {'spot': True}, True), + ("BTC/USD", 'BTC', 'USDT', "ftx", {'spot': True}, False), # Wrong quote currency + ("BTC/USD", 'USD', 'BTC', "ftx", {'spot': True}, False), # Reversed currencies + ("BTC/USD", 'BTC', 'USD', "ftx", {'spot': False}, False), # Can only trade spot markets + ("BTC-PERP", 'BTC', 'USD', "ftx", {'spot': False}, False), # Can only trade spot markets ]) -def test_symbol_is_pair(market_symbol, base_currency, quote_currency, expected_result) -> None: - assert symbol_is_pair(market_symbol, base_currency, quote_currency) == expected_result +def test_market_is_tradable(mocker, default_conf, market_symbol, base, + quote, add_dict, exchange, expected_result) -> None: + ex = get_patched_exchange(mocker, default_conf, id=exchange) + market = { + 'symbol': market_symbol, + 'base': base, + 'quote': quote, + **(add_dict), + } + assert ex.market_is_tradable(market) == expected_result @pytest.mark.parametrize("market,expected_result", [ From 08049d23b48242c5c468102b4ae8cb182a9236cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 2 Jun 2020 20:41:29 +0200 Subject: [PATCH 0054/1197] Use "market_is_tradable" for whitelist validation --- freqtrade/exchange/exchange.py | 2 +- freqtrade/pairlist/IPairList.py | 5 +++++ tests/pairlist/test_pairlist.py | 4 +++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index bec8b9686..a2bb8627a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -214,7 +214,7 @@ class Exchange: if quote_currencies: markets = {k: v for k, v in markets.items() if v['quote'] in quote_currencies} if pairs_only: - markets = {k: v for k, v in markets.items() if self.symbol_is_pair(v)} + markets = {k: v for k, v in markets.items() if self.market_is_tradable(v)} if active_only: markets = {k: v for k, v in markets.items() if market_is_active(v)} return markets diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index f48a7dcfd..61fdc73ad 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -159,6 +159,11 @@ class IPairList(ABC): f"{self._exchange.name}. Removing it from whitelist..") continue + if not self._exchange.market_is_tradable(markets[pair]): + logger.warning(f"Pair {pair} is not tradable with Freqtrade." + "Removing it from whitelist..") + continue + if self._exchange.get_pair_quote_currency(pair) != self._config['stake_currency']: logger.warning(f"Pair {pair} is not compatible with your stake currency " f"{self._config['stake_currency']}. Removing it from whitelist..") diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 421f06911..c6d1eeb38 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -400,7 +400,9 @@ def test_pairlist_class(mocker, whitelist_conf, markets, pairlist): # BCH/BTC not available (['ETH/BTC', 'TKN/BTC', 'BCH/BTC'], "is not compatible with exchange"), # BTT/BTC is inactive - (['ETH/BTC', 'TKN/BTC', 'BTT/BTC'], "Market is not active") + (['ETH/BTC', 'TKN/BTC', 'BTT/BTC'], "Market is not active"), + # XLTCUSDT is not a valid pair + (['ETH/BTC', 'TKN/BTC', 'XLTCUSDT'], "is not tradable with Freqtrade"), ]) def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist, whitelist, caplog, log_message, tickers): From 78dea19ffb4e0efa9fa7594fadfddd49010a06e7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Mar 2020 20:57:12 +0100 Subject: [PATCH 0055/1197] Implement first version of FTX stop --- freqtrade/exchange/ftx.py | 53 +++++++++++++++++ tests/exchange/test_ftx.py | 106 ++++++++++++++++++++++++++++++++++ tests/exchange/test_kraken.py | 10 ++-- 3 files changed, 164 insertions(+), 5 deletions(-) create mode 100644 tests/exchange/test_ftx.py diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 75915122b..d06d49e5a 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -2,6 +2,10 @@ import logging from typing import Dict +import ccxt + +from freqtrade.exceptions import (DependencyException, InvalidOrderException, + OperationalException, TemporaryError) from freqtrade.exchange import Exchange logger = logging.getLogger(__name__) @@ -10,5 +14,54 @@ logger = logging.getLogger(__name__) class Ftx(Exchange): _ft_has: Dict = { + "stoploss_on_exchange": True, "ohlcv_candle_limit": 1500, } + + def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + """ + Verify stop_loss against stoploss-order value (limit or price) + Returns True if adjustment is necessary. + """ + return order['type'] == 'stop' and stop_loss > float(order['price']) + + def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: + """ + Creates a stoploss market order. + Stoploss market orders is the only stoploss type supported by kraken. + """ + + ordertype = "stop" + + stop_price = self.price_to_precision(pair, stop_price) + + if self._config['dry_run']: + dry_order = self.dry_run_order( + pair, ordertype, "sell", amount, stop_price) + return dry_order + + try: + params = self._params.copy() + + amount = self.amount_to_precision(pair, amount) + + order = self._api.create_order(symbol=pair, type=ordertype, side='sell', + amount=amount, price=stop_price, params=params) + logger.info('stoploss order added for %s. ' + 'stop price: %s.', pair, stop_price) + return order + except ccxt.InsufficientFunds as e: + raise DependencyException( + f'Insufficient funds to create {ordertype} sell order on market {pair}.' + f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' + f'Message: {e}') from e + except ccxt.InvalidOrder as e: + raise InvalidOrderException( + f'Could not create {ordertype} sell order on market {pair}. ' + f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' + f'Message: {e}') from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py new file mode 100644 index 000000000..f17d5b42e --- /dev/null +++ b/tests/exchange/test_ftx.py @@ -0,0 +1,106 @@ +# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement +# pragma pylint: disable=protected-access +from random import randint +from unittest.mock import MagicMock + +import ccxt +import pytest + +from freqtrade.exceptions import (DependencyException, InvalidOrderException, + OperationalException, TemporaryError) +from tests.conftest import get_patched_exchange + + +STOPLOSS_ORDERTYPE = 'stop' + + +def test_stoploss_order_ftx(default_conf, mocker): + api_mock = MagicMock() + order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + + api_mock.create_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + } + }) + + default_conf['dry_run'] = False + mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') + + # stoploss_on_exchange_limit_ratio is irrelevant for ftx market orders + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, + order_types={'stoploss_on_exchange_limit_ratio': 1.05}) + assert api_mock.create_order.call_count == 1 + + api_mock.create_order.reset_mock() + + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' + assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE + assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 + assert api_mock.create_order.call_args_list[0][1]['price'] == 220 + + # test exception handling + with pytest.raises(DependencyException): + api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + with pytest.raises(InvalidOrderException): + api_mock.create_order = MagicMock( + side_effect=ccxt.InvalidOrder("ftx Order would trigger immediately.")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + with pytest.raises(TemporaryError): + api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + with pytest.raises(OperationalException, match=r".*DeadBeef.*"): + api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + +def test_stoploss_order_dry_run_ftx(default_conf, mocker): + api_mock = MagicMock() + default_conf['dry_run'] = True + mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') + + api_mock.create_order.reset_mock() + + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + assert 'id' in order + assert 'info' in order + assert 'type' in order + + assert order['type'] == STOPLOSS_ORDERTYPE + assert order['price'] == 220 + assert order['amount'] == 1 + + +def test_stoploss_adjust_ftx(mocker, default_conf): + exchange = get_patched_exchange(mocker, default_conf, id='ftx') + order = { + 'type': STOPLOSS_ORDERTYPE, + 'price': 1500, + } + assert exchange.stoploss_adjust(1501, order) + assert not exchange.stoploss_adjust(1499, order) + # Test with invalid order case ... + order['type'] = 'stop_loss_limit' + assert not exchange.stoploss_adjust(1501, order) diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index d63dd66cc..0950979cf 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -11,6 +11,8 @@ from freqtrade.exceptions import (DependencyException, InvalidOrderException, from tests.conftest import get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers +STOPLOSS_ORDERTYPE = 'stop-loss' + def test_buy_kraken_trading_agreement(default_conf, mocker): api_mock = MagicMock() @@ -159,7 +161,6 @@ def test_get_balances_prod(default_conf, mocker): def test_stoploss_order_kraken(default_conf, mocker): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) - order_type = 'stop-loss' api_mock.create_order = MagicMock(return_value={ 'id': order_id, @@ -187,7 +188,7 @@ def test_stoploss_order_kraken(default_conf, mocker): assert 'info' in order assert order['id'] == order_id assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' - assert api_mock.create_order.call_args_list[0][1]['type'] == order_type + assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 assert api_mock.create_order.call_args_list[0][1]['price'] == 220 @@ -218,7 +219,6 @@ def test_stoploss_order_kraken(default_conf, mocker): def test_stoploss_order_dry_run_kraken(default_conf, mocker): api_mock = MagicMock() - order_type = 'stop-loss' default_conf['dry_run'] = True mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) @@ -233,7 +233,7 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker): assert 'info' in order assert 'type' in order - assert order['type'] == order_type + assert order['type'] == STOPLOSS_ORDERTYPE assert order['price'] == 220 assert order['amount'] == 1 @@ -241,7 +241,7 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker): def test_stoploss_adjust_kraken(mocker, default_conf): exchange = get_patched_exchange(mocker, default_conf, id='kraken') order = { - 'type': 'stop-loss', + 'type': STOPLOSS_ORDERTYPE, 'price': 1500, } assert exchange.stoploss_adjust(1501, order) From 68a59fd26d720efa3d22f9f67bbafac8ce2d280b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Mar 2020 20:58:05 +0100 Subject: [PATCH 0056/1197] Add Hint to suggest this is still broken --- freqtrade/exchange/ftx.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index d06d49e5a..97257530e 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -28,7 +28,8 @@ class Ftx(Exchange): def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ Creates a stoploss market order. - Stoploss market orders is the only stoploss type supported by kraken. + Stoploss market orders is the only stoploss type supported by ftx. + TODO: This doesnot work yet as the order cannot be aquired via fetch_orders - so Freqtrade assumes the order as always missing. """ ordertype = "stop" From a808fb3b1068baa7a1c522ac4840ac93d8438b07 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Mar 2020 15:32:52 +0100 Subject: [PATCH 0057/1197] versionbump ccxt to first version that has FTX implemented correctly --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 20963a15f..6d832e3f5 100644 --- a/setup.py +++ b/setup.py @@ -63,7 +63,7 @@ setup(name='freqtrade', tests_require=['pytest', 'pytest-asyncio', 'pytest-cov', 'pytest-mock', ], install_requires=[ # from requirements-common.txt - 'ccxt>=1.18.1080', + 'ccxt>=1.24.96', 'SQLAlchemy', 'python-telegram-bot', 'arrow', From d90d6ed5d01283040b61416925a19dd1541fe037 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Mar 2020 15:50:33 +0100 Subject: [PATCH 0058/1197] Add ftx to tested exchanges --- tests/exchange/test_exchange.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 32163f696..d8950dd09 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -25,7 +25,7 @@ from freqtrade.resolvers.exchange_resolver import ExchangeResolver from tests.conftest import get_patched_exchange, log_has, log_has_re # Make sure to always keep one exchange here which is NOT subclassed!! -EXCHANGES = ['bittrex', 'binance', 'kraken', ] +EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx'] # Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines @@ -1258,7 +1258,8 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) # one_call calculation * 1.8 should do 2 calls - since = 5 * 60 * 500 * 1.8 + + since = 5 * 60 * exchange._ft_has['ohlcv_candle_limit'] * 1.8 ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000)) assert exchange._async_get_candle_history.call_count == 2 From f83c1c5abf5d2b20ec29adfc84c85d3bcca6911d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Mar 2020 17:01:11 +0100 Subject: [PATCH 0059/1197] Use get_stoploss_order and cancel_stoploss_order This allows exchanges to use stoploss which don't have the same endpoints --- freqtrade/exchange/exchange.py | 8 ++++++- freqtrade/exchange/ftx.py | 44 ++++++++++++++++++++++++++++++++++ freqtrade/freqtradebot.py | 10 ++++---- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 038fc22bc..e666b07ec 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -79,7 +79,7 @@ class Exchange: if config['dry_run']: logger.info('Instance is running with dry_run enabled') - + logger.info(f"Using CCXT {ccxt.__version__}") exchange_config = config['exchange'] # Deep merge ft_has with default ft_has options @@ -952,6 +952,9 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + # Assign method to get_stoploss_order to allow easy overriding in other classes + cancel_stoploss_order = cancel_order + def is_cancel_order_result_suitable(self, corder) -> bool: if not isinstance(corder, dict): return False @@ -1004,6 +1007,9 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + # Assign method to get_stoploss_order to allow easy overriding in other classes + get_stoploss_order = get_order + @retrier def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: """ diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 97257530e..70f140ac5 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -7,6 +7,7 @@ import ccxt from freqtrade.exceptions import (DependencyException, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange +from freqtrade.exchange.common import retrier logger = logging.getLogger(__name__) @@ -66,3 +67,46 @@ class Ftx(Exchange): f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e + + @retrier + def get_stoploss_order(self, order_id: str, pair: str) -> Dict: + if self._config['dry_run']: + try: + order = self._dry_run_open_orders[order_id] + return order + except KeyError as e: + # Gracefully handle errors with dry-run orders. + raise InvalidOrderException( + f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e + try: + orders = self._api.fetch_orders('BNB/USD', None, params={'type': 'stop'}) + + order = [order for order in orders if order['id'] == order_id] + if len(order) == 1: + return order[0] + else: + raise InvalidOrderException(f"Could not get Stoploss Order for id {order_id}") + + except ccxt.InvalidOrder as e: + raise InvalidOrderException( + f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e + + @retrier + def cancel_stoploss_order(self, order_id: str, pair: str) -> None: + if self._config['dry_run']: + return + try: + return self._api.cancel_order(order_id, pair, params={'type': 'stop'}) + except ccxt.InvalidOrder as e: + raise InvalidOrderException( + f'Could not cancel order. Message: {e}') from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a74b0a5a1..4e4fe6e11 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -774,13 +774,13 @@ class FreqtradeBot: try: # First we check if there is already a stoploss on exchange - stoploss_order = self.exchange.get_order(trade.stoploss_order_id, trade.pair) \ + stoploss_order = self.exchange.get_stoploss_order(trade.stoploss_order_id, trade.pair) \ if trade.stoploss_order_id else None except InvalidOrderException as exception: logger.warning('Unable to fetch stoploss order: %s', exception) # We check if stoploss order is fulfilled - if stoploss_order and stoploss_order['status'] == 'closed': + if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value self.update_trade_state(trade, stoploss_order, sl_order=True) # Lock pair for one candle to prevent immediate rebuys @@ -807,7 +807,7 @@ class FreqtradeBot: return False # If stoploss order is canceled for some reason we add it - if stoploss_order and stoploss_order['status'] == 'canceled': + if stoploss_order and stoploss_order['status'] in ('canceled', 'cancelled'): if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss, rate=trade.stop_loss): return False @@ -840,7 +840,7 @@ class FreqtradeBot: logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s}) ' 'in order to add another one ...', order['id']) try: - self.exchange.cancel_order(order['id'], trade.pair) + self.exchange.cancel_stoploss_order(order['id'], trade.pair) except InvalidOrderException: logger.exception(f"Could not cancel stoploss order {order['id']} " f"for pair {trade.pair}") @@ -1068,7 +1068,7 @@ class FreqtradeBot: # First cancelling stoploss on exchange ... if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: try: - self.exchange.cancel_order(trade.stoploss_order_id, trade.pair) + self.exchange.cancel_stoploss_order(trade.stoploss_order_id, trade.pair) except InvalidOrderException: logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") From cf50c1cb7be618f48ab9268be506a5b18765d871 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Mar 2020 17:01:45 +0100 Subject: [PATCH 0060/1197] Add tests for new exchange methods --- tests/exchange/test_exchange.py | 51 +++++++++++++++++++++++++++++++++ tests/exchange/test_ftx.py | 35 +++++++++++++++++++++- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index d8950dd09..c06b934ba 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1734,6 +1734,7 @@ def test_cancel_order_dry_run(default_conf, mocker, exchange_name): default_conf['dry_run'] = True exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) assert exchange.cancel_order(order_id='123', pair='TKN/BTC') == {} + assert exchange.cancel_stoploss_order(order_id='123', pair='TKN/BTC') == {} @pytest.mark.parametrize("exchange_name", EXCHANGES) @@ -1818,6 +1819,25 @@ def test_cancel_order(default_conf, mocker, exchange_name): order_id='_', pair='TKN/BTC') +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_cancel_stoploss_order(default_conf, mocker, exchange_name): + default_conf['dry_run'] = False + api_mock = MagicMock() + api_mock.cancel_order = MagicMock(return_value=123) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + assert exchange.cancel_stoploss_order(order_id='_', pair='TKN/BTC') == 123 + + with pytest.raises(InvalidOrderException): + api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder("Did not find order")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.cancel_stoploss_order(order_id='_', pair='TKN/BTC') + assert api_mock.cancel_order.call_count == 1 + + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, + "cancel_stoploss_order", "cancel_order", + order_id='_', pair='TKN/BTC') + + @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_get_order(default_conf, mocker, exchange_name): default_conf['dry_run'] = True @@ -1847,6 +1867,37 @@ def test_get_order(default_conf, mocker, exchange_name): order_id='_', pair='TKN/BTC') +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_stoploss_order(default_conf, mocker, exchange_name): + # Don't test FTX here - that needs a seperate test + if exchange_name == 'ftx': + return + default_conf['dry_run'] = True + order = MagicMock() + order.myid = 123 + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange._dry_run_open_orders['X'] = order + assert exchange.get_stoploss_order('X', 'TKN/BTC').myid == 123 + + with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'): + exchange.get_stoploss_order('Y', 'TKN/BTC') + + default_conf['dry_run'] = False + api_mock = MagicMock() + api_mock.fetch_order = MagicMock(return_value=456) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + assert exchange.get_stoploss_order('X', 'TKN/BTC') == 456 + + with pytest.raises(InvalidOrderException): + api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange.get_stoploss_order(order_id='_', pair='TKN/BTC') + assert api_mock.fetch_order.call_count == 1 + + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, + 'get_stoploss_order', 'fetch_order', + order_id='_', pair='TKN/BTC') + @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_name(default_conf, mocker, exchange_name): exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index f17d5b42e..2b75e5324 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -9,7 +9,7 @@ import pytest from freqtrade.exceptions import (DependencyException, InvalidOrderException, OperationalException, TemporaryError) from tests.conftest import get_patched_exchange - +from .test_exchange import ccxt_exceptionhandlers STOPLOSS_ORDERTYPE = 'stop' @@ -104,3 +104,36 @@ def test_stoploss_adjust_ftx(mocker, default_conf): # Test with invalid order case ... order['type'] = 'stop_loss_limit' assert not exchange.stoploss_adjust(1501, order) + + +def test_get_stoploss_order(default_conf, mocker): + default_conf['dry_run'] = True + order = MagicMock() + order.myid = 123 + exchange = get_patched_exchange(mocker, default_conf, id='ftx') + exchange._dry_run_open_orders['X'] = order + assert exchange.get_stoploss_order('X', 'TKN/BTC').myid == 123 + + with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'): + exchange.get_stoploss_order('Y', 'TKN/BTC') + + default_conf['dry_run'] = False + api_mock = MagicMock() + api_mock.fetch_orders = MagicMock(return_value=[{'id': 'X', 'status': '456'}]) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') + assert exchange.get_stoploss_order('X', 'TKN/BTC')['status'] == '456' + + api_mock.fetch_orders = MagicMock(return_value=[{'id': 'Y', 'status': '456'}]) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') + with pytest.raises(InvalidOrderException, match=r"Could not get Stoploss Order for id X"): + exchange.get_stoploss_order('X', 'TKN/BTC')['status'] + + with pytest.raises(InvalidOrderException): + api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') + exchange.get_stoploss_order(order_id='_', pair='TKN/BTC') + assert api_mock.fetch_orders.call_count == 1 + + ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'ftx', + 'get_stoploss_order', 'fetch_orders', + order_id='_', pair='TKN/BTC') From 3174f37b41b515a886673501055ce4fe4d394947 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Mar 2020 17:02:47 +0100 Subject: [PATCH 0061/1197] adapt tests to use stoploss_* methods --- tests/test_freqtradebot.py | 33 ++++++++++++++++++--------------- tests/test_integration.py | 4 ++-- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 487e3a60e..dc5a5c7d3 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1126,7 +1126,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = 100 hanging_stoploss_order = MagicMock(return_value={'status': 'open'}) - mocker.patch('freqtrade.exchange.Exchange.get_order', hanging_stoploss_order) + mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', hanging_stoploss_order) assert freqtrade.handle_stoploss_on_exchange(trade) is False assert trade.stoploss_order_id == 100 @@ -1139,7 +1139,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = 100 canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'}) - mocker.patch('freqtrade.exchange.Exchange.get_order', canceled_stoploss_order) + mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', canceled_stoploss_order) stoploss.reset_mock() assert freqtrade.handle_stoploss_on_exchange(trade) is False @@ -1164,7 +1164,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, 'average': 2, 'amount': limit_buy_order['amount'], }) - mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hit) + mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hit) assert freqtrade.handle_stoploss_on_exchange(trade) is True assert log_has('STOP_LOSS_LIMIT is hit for {}.'.format(trade), caplog) assert trade.stoploss_order_id is None @@ -1183,7 +1183,8 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, # It should try to add stoploss order trade.stoploss_order_id = 100 stoploss.reset_mock() - mocker.patch('freqtrade.exchange.Exchange.get_order', side_effect=InvalidOrderException()) + mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', + side_effect=InvalidOrderException()) mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) freqtrade.handle_stoploss_on_exchange(trade) assert stoploss.call_count == 1 @@ -1214,7 +1215,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, - get_order=MagicMock(return_value={'status': 'canceled'}), + get_stoploss_order=MagicMock(return_value={'status': 'canceled'}), stoploss=MagicMock(side_effect=DependencyException()), ) freqtrade = FreqtradeBot(default_conf) @@ -1331,7 +1332,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, } }) - mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging) # stoploss initially at 5% assert freqtrade.handle_trade(trade) is False @@ -1346,7 +1347,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, cancel_order_mock = MagicMock() stoploss_order_mock = MagicMock() - mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock) + mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock) mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock) # stoploss should not be updated as the interval is 60 seconds @@ -1429,8 +1430,9 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c 'stopPrice': '0.1' } } - mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException()) - mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', + side_effect=InvalidOrderException()) + mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog) @@ -1439,7 +1441,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c # Fail creating stoploss order caplog.clear() - cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_order", MagicMock()) + cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_stoploss_order", MagicMock()) mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=DependencyException()) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert cancel_mock.call_count == 1 @@ -1510,7 +1512,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, } }) - mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging) # stoploss initially at 20% as edge dictated it. assert freqtrade.handle_trade(trade) is False @@ -1519,7 +1521,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, cancel_order_mock = MagicMock() stoploss_order_mock = MagicMock() - mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock) + mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock) mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock) # price goes down 5% @@ -2632,7 +2634,8 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException()) + mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', + side_effect=InvalidOrderException()) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=300)) sellmock = MagicMock() patch_exchange(mocker) @@ -2680,7 +2683,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke amount_to_precision=lambda s, x, y: y, price_to_precision=lambda s, x, y: y, stoploss=stoploss, - cancel_order=cancel_order, + cancel_stoploss_order=cancel_order, ) freqtrade = FreqtradeBot(default_conf) @@ -2771,7 +2774,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f "fee": None, "trades": None }) - mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_executed) + mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_executed) freqtrade.exit_positions(trades) assert trade.stoploss_order_id is None diff --git a/tests/test_integration.py b/tests/test_integration.py index 1396e86f5..57960503e 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -62,8 +62,8 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, get_fee=fee, amount_to_precision=lambda s, x, y: y, price_to_precision=lambda s, x, y: y, - get_order=stoploss_order_mock, - cancel_order=cancel_order_mock, + get_stoploss_order=stoploss_order_mock, + cancel_stoploss_order=cancel_order_mock, ) mocker.patch.multiple( From 11ebdefd09ef1d587e326b0d7d23020f32d71ebb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Apr 2020 19:52:21 +0200 Subject: [PATCH 0062/1197] Fix bug after rebase --- freqtrade/exchange/ftx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 70f140ac5..0bd32b581 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -97,9 +97,9 @@ class Ftx(Exchange): raise OperationalException(e) from e @retrier - def cancel_stoploss_order(self, order_id: str, pair: str) -> None: + def cancel_stoploss_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: - return + return {} try: return self._api.cancel_order(order_id, pair, params={'type': 'stop'}) except ccxt.InvalidOrder as e: From b58fd179f231ed304d8d47d85c8d43169218b0a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Jun 2020 10:05:14 +0200 Subject: [PATCH 0063/1197] Don't hardcode pair ... --- freqtrade/exchange/ftx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 0bd32b581..1bc97c4d3 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -79,7 +79,7 @@ class Ftx(Exchange): raise InvalidOrderException( f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e try: - orders = self._api.fetch_orders('BNB/USD', None, params={'type': 'stop'}) + orders = self._api.fetch_orders(pair, None, params={'type': 'stop'}) order = [order for order in orders if order['id'] == order_id] if len(order) == 1: From 1d9aeef792a4eaa100646f04aaf6e5d47a2169b9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Jun 2020 10:36:23 +0200 Subject: [PATCH 0064/1197] Support stop order in persistence --- freqtrade/persistence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 823bf6dc0..2e02c3999 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -374,7 +374,7 @@ class Trade(_DECL_BASE): elif order_type in ('market', 'limit') and order['side'] == 'sell': self.close(order['price']) logger.info('%s_SELL has been fulfilled for %s.', order_type.upper(), self) - elif order_type in ('stop_loss_limit', 'stop-loss'): + elif order_type in ('stop_loss_limit', 'stop-loss', 'stop'): self.stoploss_order_id = None self.close_rate_requested = self.stop_loss logger.info('%s is hit for %s.', order_type.upper(), self) From 77a62b845a1d5c13146966e3609f40e99f39fbf2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Jun 2020 11:24:23 +0200 Subject: [PATCH 0065/1197] Fix some comments --- freqtrade/exchange/ftx.py | 2 +- tests/exchange/test_exchange.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 1bc97c4d3..20d643a83 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -30,7 +30,6 @@ class Ftx(Exchange): """ Creates a stoploss market order. Stoploss market orders is the only stoploss type supported by ftx. - TODO: This doesnot work yet as the order cannot be aquired via fetch_orders - so Freqtrade assumes the order as always missing. """ ordertype = "stop" @@ -46,6 +45,7 @@ class Ftx(Exchange): params = self._params.copy() amount = self.amount_to_precision(pair, amount) + # set orderPrice to place limit order (?) order = self._api.create_order(symbol=pair, type=ordertype, side='sell', amount=amount, price=stop_price, params=params) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index c06b934ba..dc272de36 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1898,6 +1898,7 @@ def test_get_stoploss_order(default_conf, mocker, exchange_name): 'get_stoploss_order', 'fetch_order', order_id='_', pair='TKN/BTC') + @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_name(default_conf, mocker, exchange_name): exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) From f0eb0bc350a876c514deb63e331cedac6707a884 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 1 Jun 2020 11:33:40 +0200 Subject: [PATCH 0066/1197] Support limit orders --- freqtrade/exchange/ftx.py | 12 +++++++++--- tests/exchange/test_ftx.py | 24 ++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 20d643a83..73347f1eb 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -28,9 +28,13 @@ class Ftx(Exchange): def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ - Creates a stoploss market order. - Stoploss market orders is the only stoploss type supported by ftx. + Creates a stoploss order. + depending on order_types.stoploss configuration, uses 'market' or limit order. + + Limit orders are defined by having orderPrice set, otherwise a market order is used. """ + limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) + limit_rate = stop_price * limit_price_pct ordertype = "stop" @@ -43,9 +47,11 @@ class Ftx(Exchange): try: params = self._params.copy() + if order_types.get('stoploss', 'market') == 'limit': + # set orderPrice to place limit order, otherwise it's a market order + params['orderPrice'] = limit_rate amount = self.amount_to_precision(pair, amount) - # set orderPrice to place limit order (?) order = self._api.create_order(symbol=pair, type=ordertype, side='sell', amount=amount, price=stop_price, params=params) diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 2b75e5324..bead63096 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -34,6 +34,14 @@ def test_stoploss_order_ftx(default_conf, mocker): # stoploss_on_exchange_limit_ratio is irrelevant for ftx market orders order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, order_types={'stoploss_on_exchange_limit_ratio': 1.05}) + + assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' + assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE + assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 + assert api_mock.create_order.call_args_list[0][1]['price'] == 190 + assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params'] + assert api_mock.create_order.call_count == 1 api_mock.create_order.reset_mock() @@ -48,6 +56,22 @@ def test_stoploss_order_ftx(default_conf, mocker): assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 assert api_mock.create_order.call_args_list[0][1]['price'] == 220 + assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params'] + + api_mock.create_order.reset_mock() + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, + order_types={'stoploss': 'limit'}) + + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' + assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE + assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 + assert api_mock.create_order.call_args_list[0][1]['price'] == 220 + assert 'orderPrice' in api_mock.create_order.call_args_list[0][1]['params'] + assert api_mock.create_order.call_args_list[0][1]['params']['orderPrice'] == 217.8 # test exception handling with pytest.raises(DependencyException): From 2f07d21629b8aade68a5916303652133bf57d3cd Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 3 Jun 2020 20:20:39 +0200 Subject: [PATCH 0067/1197] Update documentation with FTX Stoploss on exchange --- docs/configuration.md | 12 ++++++++---- docs/exchanges.md | 5 +++++ docs/stoploss.md | 2 +- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index da0f015e8..467190463 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -272,7 +272,7 @@ the static list of pairs) if we should buy. ### Understand order_types -The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds. +The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds. This allows to buy using limit orders, sell using limit-orders, and create stoplosses using using market orders. It also allows to set the @@ -288,8 +288,12 @@ If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and `emergencysell` is an optional value, which defaults to `market` and is used when creating stoploss on exchange orders fails. The below is the default which is used if this is not configured in either strategy or configuration file. -Since `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price. -`stoploss` defines the stop-price - and limit should be slightly below this. This defaults to 0.99 / 1% (configurable via `stoploss_on_exchange_limit_ratio`). +Not all Exchanges support `stoploss_on_exchange`. If an exchange supports both limit and market stoploss orders, then the value of `stoploss` will be used to determine the stoploss type. + +If `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price. +`stoploss` defines the stop-price - and limit should be slightly below this. + +This defaults to 0.99 / 1% (configurable via `stoploss_on_exchange_limit_ratio`). Calculation example: we bought the asset at 100$. Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the stoploss will happen between 95$ and 94.05$. @@ -331,7 +335,7 @@ Configuration: refer to [the stoploss documentation](stoploss.md). !!! Note - If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new order. + If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new stoploss order. !!! Warning "Warning: stoploss_on_exchange failures" If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however this is not advised. diff --git a/docs/exchanges.md b/docs/exchanges.md index 81f017023..cd210eb69 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -64,6 +64,11 @@ print(res) ## FTX +!!! Tip "Stoploss on Exchange" + FTX supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it. + You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide. + + ### Using subaccounts To use subaccounts with FTX, you need to edit the configuration and add the following: diff --git a/docs/stoploss.md b/docs/stoploss.md index 0e43817ec..cd90a71b4 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -27,7 +27,7 @@ So this parameter will tell the bot how often it should update the stoploss orde This same logic will reapply a stoploss order on the exchange should you cancel it accidentally. !!! Note - Stoploss on exchange is only supported for Binance (stop-loss-limit) and Kraken (stop-loss-market) as of now. + Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market) and FTX (stop limit and stop-market) as of now. ## Static Stop Loss From 54226b45b1913383da8cc2922421ce01f83ffeac Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jun 2020 16:02:08 +0200 Subject: [PATCH 0068/1197] Add test verifying failure --- tests/optimize/test_backtesting.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 40c106975..b1e9dec56 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -401,6 +401,33 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> Backtesting(default_conf) + +def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, tickers) -> None: + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) + mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + mocker.patch('freqtrade.data.history.get_timerange', get_timerange) + patch_exchange(mocker) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') + mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', + PropertyMock(return_value=['XRP/BTC'])) + mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.refresh_pairlist') + + default_conf['ticker_interval'] = "1m" + default_conf['datadir'] = testdatadir + default_conf['export'] = None + # Use stoploss from strategy + del default_conf['stoploss'] + default_conf['timerange'] = '20180101-20180102' + + default_conf['pairlists'] = [{"method": "VolumePairList", "number_assets": 5}] + with pytest.raises(OperationalException, match='VolumePairList not allowed for backtesting.'): + Backtesting(default_conf) + + default_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}, ] + Backtesting(default_conf) + + def test_backtest(default_conf, fee, mocker, testdatadir) -> None: default_conf['ask_strategy']['use_sell_signal'] = False mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) From 72ae4b15002801a47ad4b3ae0576b414d37020f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 7 Jun 2020 16:06:20 +0200 Subject: [PATCH 0069/1197] Load pairlist after strategy to use strategy-config fail in certain conditions when using strategy-list Fix #3363 --- freqtrade/optimize/backtesting.py | 33 +++++++++++++++++------------- tests/optimize/test_backtesting.py | 7 ++++++- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b47b38ea4..0c5bb1a0c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -65,20 +65,6 @@ class Backtesting: self.strategylist: List[IStrategy] = [] self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) - self.pairlists = PairListManager(self.exchange, self.config) - if 'VolumePairList' in self.pairlists.name_list: - raise OperationalException("VolumePairList not allowed for backtesting.") - - self.pairlists.refresh_pairlist() - - if len(self.pairlists.whitelist) == 0: - raise OperationalException("No pair in whitelist.") - - if config.get('fee'): - self.fee = config['fee'] - else: - self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0]) - if self.config.get('runmode') != RunMode.HYPEROPT: self.dataprovider = DataProvider(self.config, self.exchange) IStrategy.dp = self.dataprovider @@ -101,6 +87,25 @@ class Backtesting: self.timeframe = str(self.config.get('ticker_interval')) self.timeframe_min = timeframe_to_minutes(self.timeframe) + self.pairlists = PairListManager(self.exchange, self.config) + if 'VolumePairList' in self.pairlists.name_list: + raise OperationalException("VolumePairList not allowed for backtesting.") + + if len(self.strategylist) > 1 and 'PrecisionFilter' in self.pairlists.name_list: + raise OperationalException( + "PrecisionFilter not allowed for backtesting multiple strategies." + ) + + self.pairlists.refresh_pairlist() + + if len(self.pairlists.whitelist) == 0: + raise OperationalException("No pair in whitelist.") + + if config.get('fee'): + self.fee = config['fee'] + else: + self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0]) + # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) # Load one (first) strategy diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index b1e9dec56..fc03223d2 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -401,7 +401,6 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> Backtesting(default_conf) - def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, tickers) -> None: mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) @@ -427,6 +426,12 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti default_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}, ] Backtesting(default_conf) + # Multiple strategies + default_conf['strategy_list'] = ['DefaultStrategy', 'TestStrategyLegacy'] + with pytest.raises(OperationalException, + match='PrecisionFilter not allowed for backtesting multiple strategies.'): + Backtesting(default_conf) + def test_backtest(default_conf, fee, mocker, testdatadir) -> None: default_conf['ask_strategy']['use_sell_signal'] = False From ab0003f56517a51e358e24245f53ea054032d151 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Jun 2020 14:33:57 +0200 Subject: [PATCH 0070/1197] fix #3463 by explicitly failing if no stoploss is defined --- freqtrade/pairlist/PrecisionFilter.py | 6 +++++- tests/pairlist/test_pairlist.py | 11 +++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py index 0331347be..45baf656c 100644 --- a/freqtrade/pairlist/PrecisionFilter.py +++ b/freqtrade/pairlist/PrecisionFilter.py @@ -5,7 +5,7 @@ import logging from typing import Any, Dict from freqtrade.pairlist.IPairList import IPairList - +from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) @@ -17,6 +17,10 @@ class PrecisionFilter(IPairList): pairlist_pos: int) -> None: super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + if 'stoploss' not in self._config: + raise OperationalException( + 'PrecisionFilter can only work with stoploss defined. Please add the ' + 'stoploss key to your configuration (overwrites eventual strategy settings).') self._stoploss = self._config['stoploss'] self._enabled = self._stoploss != 0 diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 421f06911..07f853342 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -362,6 +362,17 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t assert not log_has(logmsg, caplog) +def test_PrecisionFilter_error(mocker, whitelist_conf, tickers) -> None: + whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}] + del whitelist_conf['stoploss'] + + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + + with pytest.raises(OperationalException, + match=r"PrecisionFilter can only work with stoploss defined\..*"): + PairListManager(MagicMock, whitelist_conf) + + def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}] From 05deb5ba05361df908e22c18ac02c222f9298a29 Mon Sep 17 00:00:00 2001 From: Mister Render Date: Tue, 9 Jun 2020 16:08:20 +0000 Subject: [PATCH 0071/1197] Fixed typo and missing { This should help with copy pasting the pairlists code block. Also fixed minor typo on line 594 (was: "I selects" and is now: "It selects") --- docs/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index da0f015e8..c31af1cc9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -591,7 +591,7 @@ It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklis #### Volume Pair List -`VolumePairList` employs sorting/filtering of pairs by their trading volume. I selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`). +`VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`). When used in the chain of Pairlist Handlers in a non-leading position (after StaticPairList and other Pairlist Filters), `VolumePairList` considers outputs of previous Pairlist Handlers, adding its sorting/selection of the pairs by the trading volume. @@ -609,7 +609,7 @@ The `refresh_period` setting allows to define the period (in seconds), at which "number_assets": 20, "sort_key": "quoteVolume", "refresh_period": 1800, -], +}], ``` #### PrecisionFilter From bd942992ef80bff0c7768ffe7ee57c8965e40265 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Jun 2020 20:47:52 +0200 Subject: [PATCH 0072/1197] Add documentation about pricing related to market orders --- docs/configuration.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index c31af1cc9..035a65abf 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -333,6 +333,9 @@ Configuration: !!! Note If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new order. +!!! Warning "Using market orders" + Please read the section [Market order pricing](#market-order-pricing) section when using market orders. + !!! Warning "Warning: stoploss_on_exchange failures" If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however this is not advised. @@ -459,6 +462,9 @@ Prices are always retrieved right before an order is placed, either by querying !!! Note Orderbook data used by Freqtrade are the data retrieved from exchange by the ccxt's function `fetch_order_book()`, i.e. are usually data from the L2-aggregated orderbook, while the ticker data are the structures returned by the ccxt's `fetch_ticker()`/`fetch_tickers()` functions. Refer to the ccxt library [documentation](https://github.com/ccxt/ccxt/wiki/Manual#market-data) for more details. +!!! Warning "Using market orders" + Please read the section [Market order pricing](#market-order-pricing) section when using market orders. + ### Buy price #### Check depth of market @@ -553,6 +559,29 @@ A fixed slot (mirroring `bid_strategy.order_book_top`) can be defined by setting When not using orderbook (`ask_strategy.use_order_book=False`), the price at the `ask_strategy.price_side` side (defaults to `"ask"`) from the ticker will be used as the sell price. +### Market order pricing + +When using market orders, prices should be configured to use the "correct" side of the orderbook to allow realistic pricing detection. +Assuming both buy and sell are using market orders, a configuration similar to the following might be used + +``` jsonc + "order_types": { + "buy": "market", + "sell": "market" + // ... + }, + "bid_strategy": { + "price_side": "ask", + //... + }, + "ask_strategy":{ + "price_side": "bid", + //... + }, +``` + +Obviously, if only one side is using limit orders, different pricing combinations can be used. + ## Pairlists and Pairlist Handlers Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings. From 6744f8f052d66e1f7a49eff81c6ff57e65fec0cf Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 10 Jun 2020 01:22:55 +0300 Subject: [PATCH 0073/1197] Remove _load_async_markets --- freqtrade/exchange/exchange.py | 17 +++-------------- tests/exchange/test_exchange.py | 28 ---------------------------- 2 files changed, 3 insertions(+), 42 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 038fc22bc..773f7a0a4 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -256,32 +256,21 @@ class Exchange: "Please check your config.json") raise OperationalException(f'Exchange {name} does not provide a sandbox api') - def _load_async_markets(self, reload: bool = False) -> None: - try: - if self._api_async: - asyncio.get_event_loop().run_until_complete( - self._api_async.load_markets(reload=reload)) - - except ccxt.BaseError as e: - logger.warning('Could not load async markets. Reason: %s', e) - return - def _load_markets(self) -> None: - """ Initialize markets both sync and async """ + """ Initialize markets """ try: self._api.load_markets() - self._load_async_markets() self._last_markets_refresh = arrow.utcnow().timestamp except ccxt.BaseError as e: logger.warning('Unable to initialize markets. Reason: %s', e) def _reload_markets(self) -> None: - """Reload markets both sync and async, if refresh interval has passed""" + """Reload markets if refresh interval has passed""" # Check whether markets have to be reloaded if (self._last_markets_refresh > 0) and ( self._last_markets_refresh + self.markets_refresh_interval > arrow.utcnow().timestamp): - return None + return logger.debug("Performing scheduled market reload..") try: self._api.load_markets(reload=True) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 32163f696..31efc65ff 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -130,7 +130,6 @@ def test_init_exception(default_conf, mocker): def test_exchange_resolver(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=MagicMock())) - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') @@ -318,19 +317,6 @@ def test_set_sandbox_exception(default_conf, mocker): exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname') -def test__load_async_markets(default_conf, mocker, caplog): - exchange = get_patched_exchange(mocker, default_conf) - exchange._api_async.load_markets = get_mock_coro(None) - exchange._load_async_markets() - assert exchange._api_async.load_markets.call_count == 1 - caplog.set_level(logging.DEBUG) - - exchange._api_async.load_markets = Mock(side_effect=ccxt.BaseError("deadbeef")) - exchange._load_async_markets() - - assert log_has('Could not load async markets. Reason: deadbeef', caplog) - - def test__load_markets(default_conf, mocker, caplog): caplog.set_level(logging.INFO) api_mock = MagicMock() @@ -338,7 +324,6 @@ def test__load_markets(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) assert log_has('Unable to initialize markets. Reason: SomeError', caplog) @@ -406,7 +391,6 @@ def test_validate_stake_currency(default_conf, stake_currency, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') Exchange(default_conf) @@ -420,7 +404,6 @@ def test_validate_stake_currency_error(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') with pytest.raises(OperationalException, match=r'XRP is not available as stake on .*' 'Available currencies are: BTC, ETH, USDT'): @@ -470,7 +453,6 @@ def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs d mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) @@ -483,7 +465,6 @@ def test_validate_pairs_not_available(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') with pytest.raises(OperationalException, match=r'not available'): Exchange(default_conf) @@ -498,7 +479,6 @@ def test_validate_pairs_exception(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available on Binance'): Exchange(default_conf) @@ -517,7 +497,6 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog): }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) @@ -535,7 +514,6 @@ def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog): }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) @@ -551,7 +529,6 @@ def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, ca }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) @@ -567,7 +544,6 @@ def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog): }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') with pytest.raises(OperationalException, match=r"Stake-currency 'BTC' not compatible with.*"): @@ -742,7 +718,6 @@ def test_validate_required_startup_candles(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') - mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') @@ -1934,7 +1909,6 @@ def test_stoploss_order_unsupported_exchange(default_conf, mocker): def test_merge_ft_has_dict(default_conf, mocker): mocker.patch.multiple('freqtrade.exchange.Exchange', _init_ccxt=MagicMock(return_value=MagicMock()), - _load_async_markets=MagicMock(), validate_pairs=MagicMock(), validate_timeframes=MagicMock(), validate_stakecurrency=MagicMock() @@ -1968,7 +1942,6 @@ def test_merge_ft_has_dict(default_conf, mocker): def test_get_valid_pair_combination(default_conf, mocker, markets): mocker.patch.multiple('freqtrade.exchange.Exchange', _init_ccxt=MagicMock(return_value=MagicMock()), - _load_async_markets=MagicMock(), validate_pairs=MagicMock(), validate_timeframes=MagicMock(), markets=PropertyMock(return_value=markets)) @@ -2041,7 +2014,6 @@ def test_get_markets(default_conf, mocker, markets, expected_keys): mocker.patch.multiple('freqtrade.exchange.Exchange', _init_ccxt=MagicMock(return_value=MagicMock()), - _load_async_markets=MagicMock(), validate_pairs=MagicMock(), validate_timeframes=MagicMock(), markets=PropertyMock(return_value=markets)) From 7d451638a84e9d709291a4038cb06f760bfa9c80 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 10 Jun 2020 01:39:23 +0300 Subject: [PATCH 0074/1197] Make _reload_markets() public --- freqtrade/exchange/exchange.py | 4 ++-- freqtrade/freqtradebot.py | 4 ++-- tests/exchange/test_exchange.py | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 773f7a0a4..6802a6b9e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -264,8 +264,8 @@ class Exchange: except ccxt.BaseError as e: logger.warning('Unable to initialize markets. Reason: %s', e) - def _reload_markets(self) -> None: - """Reload markets if refresh interval has passed""" + def reload_markets(self) -> None: + """Reload markets if refresh interval has passed """ # Check whether markets have to be reloaded if (self._last_markets_refresh > 0) and ( self._last_markets_refresh + self.markets_refresh_interval diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5104e4f95..8a66957c3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -139,8 +139,8 @@ class FreqtradeBot: :return: True if one or more trades has been created or closed, False otherwise """ - # Check whether markets have to be reloaded - self.exchange._reload_markets() + # Check whether markets have to be reloaded and reload them when it's needed + self.exchange.reload_markets() # Query trades from persistence layer trades = Trade.get_open_trades() diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 31efc65ff..9914188b8 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -337,7 +337,7 @@ def test__load_markets(default_conf, mocker, caplog): assert ex.markets == expected_return -def test__reload_markets(default_conf, mocker, caplog): +def test_reload_markets(default_conf, mocker, caplog): caplog.set_level(logging.DEBUG) initial_markets = {'ETH/BTC': {}} @@ -356,17 +356,17 @@ def test__reload_markets(default_conf, mocker, caplog): assert exchange.markets == initial_markets # less than 10 minutes have passed, no reload - exchange._reload_markets() + exchange.reload_markets() assert exchange.markets == initial_markets # more than 10 minutes have passed, reload is executed exchange._last_markets_refresh = arrow.utcnow().timestamp - 15 * 60 - exchange._reload_markets() + exchange.reload_markets() assert exchange.markets == updated_markets assert log_has('Performing scheduled market reload..', caplog) -def test__reload_markets_exception(default_conf, mocker, caplog): +def test_reload_markets_exception(default_conf, mocker, caplog): caplog.set_level(logging.DEBUG) api_mock = MagicMock() @@ -375,7 +375,7 @@ def test__reload_markets_exception(default_conf, mocker, caplog): exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") # less than 10 minutes have passed, no reload - exchange._reload_markets() + exchange.reload_markets() assert exchange._last_markets_refresh == 0 assert log_has_re(r"Could not reload markets.*", caplog) From 0067a3ab7ceb929b2ded3a7916a064f759f45c69 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 10 Jun 2020 06:30:29 +0300 Subject: [PATCH 0075/1197] Change logging level --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 6802a6b9e..e974655cb 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -190,7 +190,7 @@ class Exchange: def markets(self) -> Dict: """exchange ccxt markets""" if not self._api.markets: - logger.warning("Markets were not loaded. Loading them now..") + logger.info("Markets were not loaded. Loading them now..") self._load_markets() return self._api.markets From a198b91b873ca428ccb19dc5244a47104c228a2b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Jun 2020 06:36:35 +0200 Subject: [PATCH 0076/1197] align Spaces in commented config section Co-authored-by: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> --- docs/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 035a65abf..cb5b6c3ea 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -572,11 +572,11 @@ Assuming both buy and sell are using market orders, a configuration similar to t }, "bid_strategy": { "price_side": "ask", - //... + // ... }, "ask_strategy":{ "price_side": "bid", - //... + // ... }, ``` From ac92834693fe5e9a3c165c3529c7dbfc4f1a3798 Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Tue, 9 Jun 2020 23:03:55 +0200 Subject: [PATCH 0077/1197] reload_conf & reload_config now both accepted, code is more consistent now --- docs/rest-api.md | 6 +++--- docs/stoploss.md | 2 +- docs/strategy-customization.md | 2 +- docs/telegram-usage.md | 8 ++++---- freqtrade/rpc/api_server.py | 10 +++++----- freqtrade/rpc/rpc.py | 8 ++++---- freqtrade/rpc/telegram.py | 12 +++++++----- freqtrade/state.py | 2 +- freqtrade/worker.py | 2 +- tests/rpc/test_rpc.py | 2 +- tests/rpc/test_rpc_apiserver.py | 6 +++--- tests/rpc/test_rpc_telegram.py | 13 +++++++------ tests/test_main.py | 4 ++-- 13 files changed, 40 insertions(+), 37 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index ed5f355b4..33f62f884 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -110,7 +110,7 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `start` | | Starts the trader | `stop` | | Stops the trader | `stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. -| `reload_conf` | | Reloads the configuration file +| `reload_config` | | Reloads the configuration file | `show_config` | | Shows part of the current configuration with relevant settings to operation | `status` | | Lists all open trades | `count` | | Displays number of trades used and available @@ -174,7 +174,7 @@ profit Returns the profit summary :returns: json object -reload_conf +reload_config Reload configuration :returns: json object @@ -196,7 +196,7 @@ stop stopbuy Stop buying (but handle sells gracefully). - use reload_conf to reset + use reload_config to reset :returns: json object version diff --git a/docs/stoploss.md b/docs/stoploss.md index 0e43817ec..7ebe98ee6 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -101,7 +101,7 @@ Simplified example: ## Changing stoploss on open trades -A stoploss on an open trade can be changed by changing the value in the configuration or strategy and use the `/reload_conf` command (alternatively, completely stopping and restarting the bot also works). +A stoploss on an open trade can be changed by changing the value in the configuration or strategy and use the `/reload_config` command (alternatively, completely stopping and restarting the bot also works). The new stoploss value will be applied to open trades (and corresponding log-messages will be generated). diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 7197b0fba..92e4453d2 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -557,7 +557,7 @@ Locks can also be lifted manually, by calling `self.unlock_pair(pair)`. To verify if a pair is currently locked, use `self.is_pair_locked(pair)`. !!! Note - Locked pairs are not persisted, so a restart of the bot, or calling `/reload_conf` will reset locked pairs. + Locked pairs are not persisted, so a restart of the bot, or calling `/reload_config` will reset locked pairs. !!! Warning Locking pairs is not functioning during backtesting. diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index f683ae8da..f423a9376 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -52,7 +52,7 @@ official commands. You can ask at any moment for help with `/help`. | `/start` | | Starts the trader | `/stop` | | Stops the trader | `/stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. -| `/reload_conf` | | Reloads the configuration file +| `/reload_config` | | Reloads the configuration file | `/show_config` | | Shows part of the current configuration with relevant settings to operation | `/status` | | Lists all open trades | `/status table` | | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**) @@ -85,14 +85,14 @@ Below, example of Telegram message you will receive for each command. ### /stopbuy -> **status:** `Setting max_open_trades to 0. Run /reload_conf to reset.` +> **status:** `Setting max_open_trades to 0. Run /reload_config to reset.` Prevents the bot from opening new trades by temporarily setting "max_open_trades" to 0. Open trades will be handled via their regular rules (ROI / Sell-signal, stoploss, ...). After this, give the bot time to close off open trades (can be checked via `/status table`). Once all positions are sold, run `/stop` to completely stop the bot. -`/reload_conf` resets "max_open_trades" to the value set in the configuration and resets this command. +`/reload_config` resets "max_open_trades" to the value set in the configuration and resets this command. !!! Warning The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset. @@ -209,7 +209,7 @@ Shows the current whitelist Shows the current blacklist. If Pair is set, then this pair will be added to the pairlist. Also supports multiple pairs, seperated by a space. -Use `/reload_conf` to reset the blacklist. +Use `/reload_config` to reset the blacklist. > Using blacklist `StaticPairList` with 2 pairs >`DODGE/BTC`, `HOT/BTC`. diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 9d0899ccd..f424bea92 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -172,8 +172,8 @@ class ApiServer(RPC): self.app.add_url_rule(f'{BASE_URI}/stop', 'stop', view_func=self._stop, methods=['POST']) self.app.add_url_rule(f'{BASE_URI}/stopbuy', 'stopbuy', view_func=self._stopbuy, methods=['POST']) - self.app.add_url_rule(f'{BASE_URI}/reload_conf', 'reload_conf', - view_func=self._reload_conf, methods=['POST']) + self.app.add_url_rule(f'{BASE_URI}/reload_config', 'reload_config', + view_func=self._reload_config, methods=['POST']) # Info commands self.app.add_url_rule(f'{BASE_URI}/balance', 'balance', view_func=self._balance, methods=['GET']) @@ -304,12 +304,12 @@ class ApiServer(RPC): @require_login @rpc_catch_errors - def _reload_conf(self): + def _reload_config(self): """ - Handler for /reload_conf. + Handler for /reload_config. Triggers a config file reload """ - msg = self._rpc_reload_conf() + msg = self._rpc_reload_config() return self.rest_dump(msg) @require_login diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 00a170ee3..e4c96cf3b 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -418,9 +418,9 @@ class RPC: return {'status': 'already stopped'} - def _rpc_reload_conf(self) -> Dict[str, str]: - """ Handler for reload_conf. """ - self._freqtrade.state = State.RELOAD_CONF + def _rpc_reload_config(self) -> Dict[str, str]: + """ Handler for reload_config. """ + self._freqtrade.state = State.RELOAD_CONFIG return {'status': 'reloading config ...'} def _rpc_stopbuy(self) -> Dict[str, str]: @@ -431,7 +431,7 @@ class RPC: # Set 'max_open_trades' to 0 self._freqtrade.config['max_open_trades'] = 0 - return {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} + return {'status': 'No more buy will occur from now. Run /reload_config to reset.'} def _rpc_forcesell(self, trade_id: str) -> Dict[str, str]: """ diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index eb53fc68f..e86f35687 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -95,7 +95,9 @@ class Telegram(RPC): CommandHandler('performance', self._performance), CommandHandler('daily', self._daily), CommandHandler('count', self._count), - CommandHandler('reload_conf', self._reload_conf), + CommandHandler('reload_conf', self._reload_config), + CommandHandler('reload_config', self._reload_config), + CommandHandler('show_conf', self._reload_config), CommandHandler('show_config', self._show_config), CommandHandler('stopbuy', self._stopbuy), CommandHandler('whitelist', self._whitelist), @@ -436,15 +438,15 @@ class Telegram(RPC): self._send_msg('Status: `{status}`'.format(**msg)) @authorized_only - def _reload_conf(self, update: Update, context: CallbackContext) -> None: + def _reload_config(self, update: Update, context: CallbackContext) -> None: """ - Handler for /reload_conf. + Handler for /reload_config. Triggers a config file reload :param bot: telegram bot :param update: message update :return: None """ - msg = self._rpc_reload_conf() + msg = self._rpc_reload_config() self._send_msg('Status: `{status}`'.format(**msg)) @authorized_only @@ -617,7 +619,7 @@ class Telegram(RPC): "\n" "*/balance:* `Show account balance per currency`\n" "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" - "*/reload_conf:* `Reload configuration file` \n" + "*/reload_config:* `Reload configuration file` \n" "*/show_config:* `Show running configuration` \n" "*/whitelist:* `Show current whitelist` \n" "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " diff --git a/freqtrade/state.py b/freqtrade/state.py index 38784c6a4..8ddff71d9 100644 --- a/freqtrade/state.py +++ b/freqtrade/state.py @@ -12,7 +12,7 @@ class State(Enum): """ RUNNING = 1 STOPPED = 2 - RELOAD_CONF = 3 + RELOAD_CONFIG = 3 def __str__(self): return f"{self.name.lower()}" diff --git a/freqtrade/worker.py b/freqtrade/worker.py index 3f5ab734e..5bdb166c2 100755 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -71,7 +71,7 @@ class Worker: state = None while True: state = self._worker(old_state=state) - if state == State.RELOAD_CONF: + if state == State.RELOAD_CONFIG: self._reconfigure() def _worker(self, old_state: Optional[State]) -> State: diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index ef1c1bc16..88c82c5ea 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -592,7 +592,7 @@ def test_rpc_stopbuy(mocker, default_conf) -> None: assert freqtradebot.config['max_open_trades'] != 0 result = rpc._rpc_stopbuy() - assert {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} == result + assert {'status': 'No more buy will occur from now. Run /reload_config to reset.'} == result assert freqtradebot.config['max_open_trades'] == 0 diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 9b247fefc..a6d38cc9f 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -251,10 +251,10 @@ def test_api_cleanup(default_conf, mocker, caplog): def test_api_reloadconf(botclient): ftbot, client = botclient - rc = client_post(client, f"{BASE_URI}/reload_conf") + rc = client_post(client, f"{BASE_URI}/reload_config") assert_response(rc) assert rc.json == {'status': 'reloading config ...'} - assert ftbot.state == State.RELOAD_CONF + assert ftbot.state == State.RELOAD_CONFIG def test_api_stopbuy(botclient): @@ -263,7 +263,7 @@ def test_api_stopbuy(botclient): rc = client_post(client, f"{BASE_URI}/stopbuy") assert_response(rc) - assert rc.json == {'status': 'No more buy will occur from now. Run /reload_conf to reset.'} + assert rc.json == {'status': 'No more buy will occur from now. Run /reload_config to reset.'} assert ftbot.config['max_open_trades'] == 0 diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index b18106ee5..afe007347 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -73,8 +73,9 @@ def test_init(default_conf, mocker, caplog) -> None: message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " - "['performance'], ['daily'], ['count'], ['reload_conf'], ['show_config'], " - "['stopbuy'], ['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]") + "['performance'], ['daily'], ['count'], ['reload_conf'], ['reload_config'], " + "['show_conf'], ['show_config'], ['stopbuy'], ['whitelist'], " + "['blacklist'], ['edge'], ['help'], ['version']]") assert log_has(message_str, caplog) @@ -666,11 +667,11 @@ def test_stopbuy_handle(default_conf, update, mocker) -> None: telegram._stopbuy(update=update, context=MagicMock()) assert freqtradebot.config['max_open_trades'] == 0 assert msg_mock.call_count == 1 - assert 'No more buy will occur from now. Run /reload_conf to reset.' \ + assert 'No more buy will occur from now. Run /reload_config to reset.' \ in msg_mock.call_args_list[0][0][0] -def test_reload_conf_handle(default_conf, update, mocker) -> None: +def test_reload_config_handle(default_conf, update, mocker) -> None: msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -683,8 +684,8 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None: freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING - telegram._reload_conf(update=update, context=MagicMock()) - assert freqtradebot.state == State.RELOAD_CONF + telegram._reload_config(update=update, context=MagicMock()) + assert freqtradebot.state == State.RELOAD_CONFIG assert msg_mock.call_count == 1 assert 'reloading config' in msg_mock.call_args_list[0][0][0] diff --git a/tests/test_main.py b/tests/test_main.py index 11d0ede3a..48cc47104 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -141,12 +141,12 @@ def test_main_operational_exception1(mocker, default_conf, caplog) -> None: assert log_has_re(r'SIGINT.*', caplog) -def test_main_reload_conf(mocker, default_conf, caplog) -> None: +def test_main_reload_config(mocker, default_conf, caplog) -> None: patch_exchange(mocker) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock()) # Simulate Running, reload, running workflow worker_mock = MagicMock(side_effect=[State.RUNNING, - State.RELOAD_CONF, + State.RELOAD_CONFIG, State.RUNNING, OperationalException("Oh snap!")]) mocker.patch('freqtrade.worker.Worker._worker', worker_mock) From 04fa59769596506c4ff958b0554aaa0ca42c4a1d Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Wed, 10 Jun 2020 16:28:37 +0200 Subject: [PATCH 0078/1197] Test with multiple commands in one line --- freqtrade/rpc/telegram.py | 8 ++++---- tests/rpc/test_rpc_telegram.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e86f35687..51130e653 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -95,10 +95,10 @@ class Telegram(RPC): CommandHandler('performance', self._performance), CommandHandler('daily', self._daily), CommandHandler('count', self._count), - CommandHandler('reload_conf', self._reload_config), - CommandHandler('reload_config', self._reload_config), - CommandHandler('show_conf', self._reload_config), - CommandHandler('show_config', self._show_config), + #CommandHandler('reload_conf', self._reload_config), + CommandHandler(('reload_config' or 'reload_con'), self._reload_config), + #CommandHandler('show_conf', self._reload_config), + CommandHandler('show_config' or 'show_conf', self._show_config), CommandHandler('stopbuy', self._stopbuy), CommandHandler('whitelist', self._whitelist), CommandHandler('blacklist', self._blacklist), diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index afe007347..ce0c9d1f0 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -73,8 +73,8 @@ def test_init(default_conf, mocker, caplog) -> None: message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " - "['performance'], ['daily'], ['count'], ['reload_conf'], ['reload_config'], " - "['show_conf'], ['show_config'], ['stopbuy'], ['whitelist'], " + "['performance'], ['daily'], ['count'], ['reload_config'], " + "['show_config'], ['stopbuy'], ['whitelist'], " "['blacklist'], ['edge'], ['help'], ['version']]") assert log_has(message_str, caplog) From 043397c5d7d70d2e7c3821b892486413a8a68a32 Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Wed, 10 Jun 2020 16:34:46 +0200 Subject: [PATCH 0079/1197] reload_conf & reload_config now both accepted, code is more consistent now --- 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 51130e653..e7a8971ec 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -96,7 +96,7 @@ class Telegram(RPC): CommandHandler('daily', self._daily), CommandHandler('count', self._count), #CommandHandler('reload_conf', self._reload_config), - CommandHandler(('reload_config' or 'reload_con'), self._reload_config), + CommandHandler(('reload_config' or 'reload_conf'), self._reload_config), #CommandHandler('show_conf', self._reload_config), CommandHandler('show_config' or 'show_conf', self._show_config), CommandHandler('stopbuy', self._stopbuy), From 8c9dea988cf0665744dba54c7b9c0b4c2dfd3cfe Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Wed, 10 Jun 2020 16:55:47 +0200 Subject: [PATCH 0080/1197] Now supports both commands & fixed test --- freqtrade/rpc/telegram.py | 6 ++---- tests/rpc/test_rpc_telegram.py | 7 +++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e7a8971ec..006baee60 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -95,10 +95,8 @@ class Telegram(RPC): CommandHandler('performance', self._performance), CommandHandler('daily', self._daily), CommandHandler('count', self._count), - #CommandHandler('reload_conf', self._reload_config), - CommandHandler(('reload_config' or 'reload_conf'), self._reload_config), - #CommandHandler('show_conf', self._reload_config), - CommandHandler('show_config' or 'show_conf', self._show_config), + CommandHandler(['reload_config', 'reload_conf'], self._reload_config), + CommandHandler(['show_config', 'show_conf'], self._show_config), CommandHandler('stopbuy', self._stopbuy), CommandHandler('whitelist', self._whitelist), CommandHandler('blacklist', self._blacklist), diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index ce0c9d1f0..b19c6d9ee 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -72,10 +72,9 @@ def test_init(default_conf, mocker, caplog) -> None: assert start_polling.start_polling.call_count == 1 message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " - "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " - "['performance'], ['daily'], ['count'], ['reload_config'], " - "['show_config'], ['stopbuy'], ['whitelist'], " - "['blacklist'], ['edge'], ['help'], ['version']]") + "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['performance'], " + "['daily'], ['count'], ['reload_config', 'reload_conf'], ['show_config', 'show_conf'], " + "['stopbuy'], ['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]") assert log_has(message_str, caplog) From 4f643f8481af5ed983615c9a88b9296cc9a21b92 Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Wed, 10 Jun 2020 17:12:21 +0200 Subject: [PATCH 0081/1197] Fix Flake8 error: line too long --- tests/rpc/test_rpc_telegram.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index b19c6d9ee..0a4352f5b 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -72,9 +72,10 @@ def test_init(default_conf, mocker, caplog) -> None: assert start_polling.start_polling.call_count == 1 message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " - "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['performance'], " - "['daily'], ['count'], ['reload_config', 'reload_conf'], ['show_config', 'show_conf'], " - "['stopbuy'], ['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]") + "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " + "['performance'], ['daily'], ['count'], ['reload_config', 'reload_conf'], " + "['show_config', 'show_conf'], ['stopbuy'], ['whitelist'], ['blacklist'], " + "['edge'], ['help'], ['version']]") assert log_has(message_str, caplog) From 69ac5c1ac7a6178113b8f6df8650661f6f3fa284 Mon Sep 17 00:00:00 2001 From: Felipe Lambert Date: Mon, 8 Jun 2020 14:55:28 -0300 Subject: [PATCH 0082/1197] change hyperopt return to better copy to strategy file --- freqtrade/optimize/hyperopt.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 3a28de785..58bcbd208 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -12,7 +12,7 @@ from math import ceil from collections import OrderedDict from operator import itemgetter from pathlib import Path -from pprint import pprint +from pprint import pformat from typing import Any, Dict, List, Optional import rapidjson @@ -244,11 +244,21 @@ class Hyperopt: def _params_pretty_print(params, space: str, header: str) -> None: if space in params: space_params = Hyperopt._space_params(params, space, 5) + print(f"\n # {header}") if space == 'stoploss': - print(header, space_params.get('stoploss')) + print(" stoploss =", space_params.get('stoploss')) + elif space == 'roi': + minimal_roi_result = rapidjson.dumps( + OrderedDict( + (str(k), v) for k, v in space_params.items() + ), + default=str, indent=4, number_mode=rapidjson.NM_NATIVE) + minimal_roi_result = minimal_roi_result.replace("\n", "\n ") + print(f" minimal_roi = {minimal_roi_result}") else: - print(header) - pprint(space_params, indent=4) + params_result = pformat(space_params, indent=4).replace("}", "\n}") + params_result = params_result.replace("{", "{\n ").replace("\n", "\n ") + print(f" {space}_params = {params_result}") @staticmethod def _space_params(params, space: str, r: int = None) -> Dict: From a7cd68121beb33cb40520b94439344879c1b7114 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Jun 2020 19:44:34 +0200 Subject: [PATCH 0083/1197] Have rest-client use new reload_config endpoint --- scripts/rest_client.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index b26c32479..1f96bcb69 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -80,18 +80,18 @@ class FtRestClient(): return self._post("stop") def stopbuy(self): - """Stop buying (but handle sells gracefully). Use `reload_conf` to reset. + """Stop buying (but handle sells gracefully). Use `reload_config` to reset. :return: json object """ return self._post("stopbuy") - def reload_conf(self): + def reload_config(self): """Reload configuration. :return: json object """ - return self._post("reload_conf") + return self._post("reload_config") def balance(self): """Get the account balance. From c66ca957d9a6c5adc2560042d802c29c966988c7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Jun 2020 19:57:47 +0200 Subject: [PATCH 0084/1197] Add test verifying this behaviour --- tests/pairlist/test_pairlist.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 421f06911..c67f7ae1c 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -421,6 +421,23 @@ def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist assert log_message in caplog.text +@pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS) +def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, markets, pairlist, tickers): + whitelist_conf['pairlists'][0]['method'] = pairlist + + mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + + freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=None), + get_tickers=tickers + ) + # Assign starting whitelist + pairlist_handler = freqtrade.pairlists._pairlist_handlers[0] + with pytest.raises(OperationalException, match=r'Markets not loaded.*'): + pairlist_handler._whitelist_for_active_markets(['ETH/BTC']) + + def test_volumepairlist_invalid_sortvalue(mocker, markets, whitelist_conf): whitelist_conf['pairlists'][0].update({"sort_key": "asdf"}) From 1e7826f3922748449554d01cfb81c7c366c5babb Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 10 Jun 2020 19:57:59 +0200 Subject: [PATCH 0085/1197] Explicitly raise OperationalException if markets are not loaded correctly --- freqtrade/pairlist/IPairList.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index f48a7dcfd..fd25e0766 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -150,6 +150,9 @@ class IPairList(ABC): black_listed """ markets = self._exchange.markets + if not markets: + raise OperationalException( + 'Markets not loaded. Make sure that exchange is initialized correctly.') sanitized_whitelist: List[str] = [] for pair in pairlist: From ab2f5579d8ad638d10a222ff1f84fdff41306c4e Mon Sep 17 00:00:00 2001 From: CoDaXe Date: Thu, 11 Jun 2020 20:34:14 -0400 Subject: [PATCH 0086/1197] Update strategy-customization.md Fix typo "the an" --- docs/strategy-customization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 92e4453d2..be41b196e 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -139,7 +139,7 @@ By letting the bot know how much history is needed, backtest trades can start at #### Example -Let's try to backtest 1 month (January 2019) of 5m candles using the an example strategy with EMA100, as above. +Let's try to backtest 1 month (January 2019) of 5m candles using an example strategy with EMA100, as above. ``` bash freqtrade backtesting --timerange 20190101-20190201 --ticker-interval 5m From 9615614e489d194155a896c6cb8c4ca86522ce8f Mon Sep 17 00:00:00 2001 From: CoDaXe Date: Fri, 12 Jun 2020 13:13:10 -0400 Subject: [PATCH 0087/1197] Update hyperopt.md Wrong flag name --- docs/hyperopt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 8efc51a39..f66534726 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -265,7 +265,7 @@ freqtrade hyperopt --timerange 20180401-20180501 Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided. ```bash -freqtrade hyperopt --strategy SampleStrategy --customhyperopt SampleHyperopt +freqtrade hyperopt --strategy SampleStrategy --hyperopt SampleHyperopt ``` ### Running Hyperopt with Smaller Search Space From 9890e26aeb7d1664088b1dc3cad3db1eba69a633 Mon Sep 17 00:00:00 2001 From: John Duong Date: Fri, 12 Jun 2020 22:10:18 -0700 Subject: [PATCH 0088/1197] fix SQL cheatsheet query (#3475) --- docs/sql_cheatsheet.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index 895a0536a..88eb0d3d4 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -101,7 +101,7 @@ SET is_open=0, close_date=, close_rate=, close_profit=close_rate/open_rate-1, - close_profit_abs = (amount * * (1 - fee_close) - (amount * open_rate * 1 - fee_open), + close_profit_abs = (amount * * (1 - fee_close) - (amount * open_rate * 1 - fee_open)), sell_reason= WHERE id=; ``` @@ -114,7 +114,7 @@ SET is_open=0, close_date='2017-12-20 03:08:45.103418', close_rate=0.19638016, close_profit=0.0496, - close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * open_rate * 1 - fee_open) + close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * open_rate * 1 - fee_open)) sell_reason='force_sell' WHERE id=31; ``` From 37bc2d28ad4572234940689fa938a48732d11207 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 13 Jun 2020 13:34:29 +0300 Subject: [PATCH 0089/1197] Revert "Remove _load_async_markets" This reverts commit 6744f8f052d66e1f7a49eff81c6ff57e65fec0cf. --- freqtrade/exchange/exchange.py | 17 ++++++++++++++--- tests/exchange/test_exchange.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e974655cb..bd44f56f2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -256,21 +256,32 @@ class Exchange: "Please check your config.json") raise OperationalException(f'Exchange {name} does not provide a sandbox api') + def _load_async_markets(self, reload: bool = False) -> None: + try: + if self._api_async: + asyncio.get_event_loop().run_until_complete( + self._api_async.load_markets(reload=reload)) + + except ccxt.BaseError as e: + logger.warning('Could not load async markets. Reason: %s', e) + return + def _load_markets(self) -> None: - """ Initialize markets """ + """ Initialize markets both sync and async """ try: self._api.load_markets() + self._load_async_markets() self._last_markets_refresh = arrow.utcnow().timestamp except ccxt.BaseError as e: logger.warning('Unable to initialize markets. Reason: %s', e) def reload_markets(self) -> None: - """Reload markets if refresh interval has passed """ + """Reload markets both sync and async if refresh interval has passed """ # Check whether markets have to be reloaded if (self._last_markets_refresh > 0) and ( self._last_markets_refresh + self.markets_refresh_interval > arrow.utcnow().timestamp): - return + return None logger.debug("Performing scheduled market reload..") try: self._api.load_markets(reload=True) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 9914188b8..2b63eee23 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -130,6 +130,7 @@ def test_init_exception(default_conf, mocker): def test_exchange_resolver(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=MagicMock())) + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') @@ -317,6 +318,19 @@ def test_set_sandbox_exception(default_conf, mocker): exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname') +def test__load_async_markets(default_conf, mocker, caplog): + exchange = get_patched_exchange(mocker, default_conf) + exchange._api_async.load_markets = get_mock_coro(None) + exchange._load_async_markets() + assert exchange._api_async.load_markets.call_count == 1 + caplog.set_level(logging.DEBUG) + + exchange._api_async.load_markets = Mock(side_effect=ccxt.BaseError("deadbeef")) + exchange._load_async_markets() + + assert log_has('Could not load async markets. Reason: deadbeef', caplog) + + def test__load_markets(default_conf, mocker, caplog): caplog.set_level(logging.INFO) api_mock = MagicMock() @@ -324,6 +338,7 @@ def test__load_markets(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) assert log_has('Unable to initialize markets. Reason: SomeError', caplog) @@ -391,6 +406,7 @@ def test_validate_stake_currency(default_conf, stake_currency, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') Exchange(default_conf) @@ -404,6 +420,7 @@ def test_validate_stake_currency_error(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') with pytest.raises(OperationalException, match=r'XRP is not available as stake on .*' 'Available currencies are: BTC, ETH, USDT'): @@ -453,6 +470,7 @@ def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs d mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) @@ -465,6 +483,7 @@ def test_validate_pairs_not_available(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') with pytest.raises(OperationalException, match=r'not available'): Exchange(default_conf) @@ -479,6 +498,7 @@ def test_validate_pairs_exception(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available on Binance'): Exchange(default_conf) @@ -497,6 +517,7 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog): }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) @@ -514,6 +535,7 @@ def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog): }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) @@ -529,6 +551,7 @@ def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, ca }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) @@ -544,6 +567,7 @@ def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog): }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') with pytest.raises(OperationalException, match=r"Stake-currency 'BTC' not compatible with.*"): @@ -718,6 +742,7 @@ def test_validate_required_startup_candles(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange._load_async_markets') mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') @@ -1909,6 +1934,7 @@ def test_stoploss_order_unsupported_exchange(default_conf, mocker): def test_merge_ft_has_dict(default_conf, mocker): mocker.patch.multiple('freqtrade.exchange.Exchange', _init_ccxt=MagicMock(return_value=MagicMock()), + _load_async_markets=MagicMock(), validate_pairs=MagicMock(), validate_timeframes=MagicMock(), validate_stakecurrency=MagicMock() @@ -1942,6 +1968,7 @@ def test_merge_ft_has_dict(default_conf, mocker): def test_get_valid_pair_combination(default_conf, mocker, markets): mocker.patch.multiple('freqtrade.exchange.Exchange', _init_ccxt=MagicMock(return_value=MagicMock()), + _load_async_markets=MagicMock(), validate_pairs=MagicMock(), validate_timeframes=MagicMock(), markets=PropertyMock(return_value=markets)) @@ -2014,6 +2041,7 @@ def test_get_markets(default_conf, mocker, markets, expected_keys): mocker.patch.multiple('freqtrade.exchange.Exchange', _init_ccxt=MagicMock(return_value=MagicMock()), + _load_async_markets=MagicMock(), validate_pairs=MagicMock(), validate_timeframes=MagicMock(), markets=PropertyMock(return_value=markets)) From 3d9b1077612c272267363c72a1653ec39ffdaa83 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Sat, 13 Jun 2020 17:12:37 +0300 Subject: [PATCH 0090/1197] Changes after review --- freqtrade/optimize/hyperopt.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 58bcbd208..1136fc4a7 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -230,6 +230,9 @@ class Hyperopt: if space in ['buy', 'sell']: result_dict.setdefault('params', {}).update(space_params) elif space == 'roi': + # TODO: get rid of OrderedDict when support for python 3.6 will be + # dropped (dicts keep the order as the language feature) + # Convert keys in min_roi dict to strings because # rapidjson cannot dump dicts with integer keys... # OrderedDict is used to keep the numeric order of the items @@ -244,21 +247,24 @@ class Hyperopt: def _params_pretty_print(params, space: str, header: str) -> None: if space in params: space_params = Hyperopt._space_params(params, space, 5) - print(f"\n # {header}") + params_result = f"\n# {header}\n" if space == 'stoploss': - print(" stoploss =", space_params.get('stoploss')) + params_result += f"stoploss = {space_params.get('stoploss')}" elif space == 'roi': minimal_roi_result = rapidjson.dumps( + # TODO: get rid of OrderedDict when support for python 3.6 will be + # dropped (dicts keep the order as the language feature) OrderedDict( (str(k), v) for k, v in space_params.items() ), default=str, indent=4, number_mode=rapidjson.NM_NATIVE) - minimal_roi_result = minimal_roi_result.replace("\n", "\n ") - print(f" minimal_roi = {minimal_roi_result}") + params_result += f"minimal_roi = {minimal_roi_result}" else: - params_result = pformat(space_params, indent=4).replace("}", "\n}") - params_result = params_result.replace("{", "{\n ").replace("\n", "\n ") - print(f" {space}_params = {params_result}") + params_result += f"{space}_params = {pformat(space_params, indent=4)}" + params_result = params_result.replace("}", "\n}").replace("{", "{\n ") + + params_result = params_result.replace("\n", "\n ") + print(params_result) @staticmethod def _space_params(params, space: str, r: int = None) -> Dict: From ea77edce0519bdd7664fd20fd65416f85bdbaada Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Sat, 13 Jun 2020 18:54:54 +0300 Subject: [PATCH 0091/1197] Make flake happy --- 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 1136fc4a7..153ae3861 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -251,9 +251,9 @@ class Hyperopt: if space == 'stoploss': params_result += f"stoploss = {space_params.get('stoploss')}" elif space == 'roi': - minimal_roi_result = rapidjson.dumps( # TODO: get rid of OrderedDict when support for python 3.6 will be # dropped (dicts keep the order as the language feature) + minimal_roi_result = rapidjson.dumps( OrderedDict( (str(k), v) for k, v in space_params.items() ), From be03c22dba4037d371cb26ff43431e10c928f5a0 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 14 Jun 2020 00:35:58 +0300 Subject: [PATCH 0092/1197] Minor: Fix exception message --- freqtrade/exchange/binance.py | 2 +- freqtrade/exchange/kraken.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 4279f392c..4d76c7966 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -78,7 +78,7 @@ class Binance(Exchange): return order except ccxt.InsufficientFunds as e: raise DependencyException( - f'Insufficient funds to create {ordertype} sell order on market {pair}.' + f'Insufficient funds to create {ordertype} sell order on market {pair}. ' f'Tried to sell amount {amount} at rate {rate}. ' f'Message: {e}') from e except ccxt.InvalidOrder as e: diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 932d82a27..cac9a945c 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -85,7 +85,7 @@ class Kraken(Exchange): return order except ccxt.InsufficientFunds as e: raise DependencyException( - f'Insufficient funds to create {ordertype} sell order on market {pair}.' + f'Insufficient funds to create {ordertype} sell order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e except ccxt.InvalidOrder as e: From 1bf333d3200ba931215303b6886be1be89065902 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 14 Jun 2020 00:57:13 +0300 Subject: [PATCH 0093/1197] Minor: fix test --- tests/exchange/test_exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 2b63eee23..48c4956cf 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -705,7 +705,7 @@ def test_validate_order_types(default_conf, mocker): 'buy': 'limit', 'sell': 'limit', 'stoploss': 'market', - 'stoploss_on_exchange': 'false' + 'stoploss_on_exchange': False } with pytest.raises(OperationalException, From 4660909e958eb8da2b79e989435c4e7426f5fbba Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 14 Jun 2020 01:07:00 +0300 Subject: [PATCH 0094/1197] Validate stoploss_on_exchange_limit_ratio at startup time --- freqtrade/exchange/exchange.py | 7 +++++++ tests/exchange/test_exchange.py | 26 +++++++++++++++++++++++--- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index bd44f56f2..820526b49 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -386,6 +386,13 @@ class Exchange: f'On exchange stoploss is not supported for {self.name}.' ) + # Limit price threshold: As limit price should always be below stop-price + # Used for limit stoplosses on exchange + limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) + if limit_price_pct >= 1.0 or limit_price_pct <= 0.0: + raise OperationalException( + "stoploss_on_exchange_limit_ratio should be < 1.0 and > 0.0") + def validate_order_time_in_force(self, order_time_in_force: Dict) -> None: """ Checks if order time in force configured in strategy/config are supported diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 48c4956cf..1aaf95379 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -689,13 +689,13 @@ def test_validate_order_types(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') mocker.patch('freqtrade.exchange.Exchange.name', 'Bittrex') + default_conf['order_types'] = { 'buy': 'limit', 'sell': 'limit', 'stoploss': 'market', 'stoploss_on_exchange': False } - Exchange(default_conf) type(api_mock).has = PropertyMock(return_value={'createMarketOrder': False}) @@ -707,7 +707,6 @@ def test_validate_order_types(default_conf, mocker): 'stoploss': 'market', 'stoploss_on_exchange': False } - with pytest.raises(OperationalException, match=r'Exchange .* does not support market orders.'): Exchange(default_conf) @@ -718,11 +717,32 @@ def test_validate_order_types(default_conf, mocker): 'stoploss': 'limit', 'stoploss_on_exchange': True } - with pytest.raises(OperationalException, match=r'On exchange stoploss is not supported for .*'): Exchange(default_conf) + default_conf['order_types'] = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'limit', + 'stoploss_on_exchange': False, + 'stoploss_on_exchange_limit_ratio': 1.05 + } + with pytest.raises(OperationalException, + match=r'stoploss_on_exchange_limit_ratio should be < 1.0 and > 0.0'): + Exchange(default_conf) + + default_conf['order_types'] = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'limit', + 'stoploss_on_exchange': False, + 'stoploss_on_exchange_limit_ratio': -0.1 + } + with pytest.raises(OperationalException, + match=r'stoploss_on_exchange_limit_ratio should be < 1.0 and > 0.0'): + Exchange(default_conf) + def test_validate_order_types_not_in_config(default_conf, mocker): api_mock = MagicMock() From de36f3d850b5f17f027ef2ac0fd1ad147d2c4a47 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 14 Jun 2020 01:42:45 +0300 Subject: [PATCH 0095/1197] Cosmetics in freqtradebot --- freqtrade/freqtradebot.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 8a66957c3..341cd5416 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -795,10 +795,8 @@ class FreqtradeBot: return False # If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange - if (not stoploss_order): - + if not stoploss_order: stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss - stop_price = trade.open_rate * (1 + stoploss) if self.create_stoploss_order(trade=trade, stop_price=stop_price, rate=stop_price): From f6f7c99b9c558a6410b98b4b884a5c952cce074a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 06:31:05 +0200 Subject: [PATCH 0096/1197] Adjust typography and add missing space Co-authored-by: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> --- freqtrade/exchange/ftx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 73347f1eb..f16db96f5 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -60,7 +60,7 @@ class Ftx(Exchange): return order except ccxt.InsufficientFunds as e: raise DependencyException( - f'Insufficient funds to create {ordertype} sell order on market {pair}.' + f'Insufficient funds to create {ordertype} sell order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e except ccxt.InvalidOrder as e: @@ -91,7 +91,7 @@ class Ftx(Exchange): if len(order) == 1: return order[0] else: - raise InvalidOrderException(f"Could not get Stoploss Order for id {order_id}") + raise InvalidOrderException(f"Could not get stoploss order for id {order_id}") except ccxt.InvalidOrder as e: raise InvalidOrderException( From 534c242d1b44f0cda20202da85632936fc1e6ab3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 06:33:08 +0200 Subject: [PATCH 0097/1197] Apply typography to test too --- tests/exchange/test_ftx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index bead63096..75e98740c 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -149,7 +149,7 @@ def test_get_stoploss_order(default_conf, mocker): api_mock.fetch_orders = MagicMock(return_value=[{'id': 'Y', 'status': '456'}]) exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') - with pytest.raises(InvalidOrderException, match=r"Could not get Stoploss Order for id X"): + with pytest.raises(InvalidOrderException, match=r"Could not get stoploss order for id X"): exchange.get_stoploss_order('X', 'TKN/BTC')['status'] with pytest.raises(InvalidOrderException): From 837aedb0c7622826a2e5b7f2fc2bf105e43d472c Mon Sep 17 00:00:00 2001 From: muletman <66912721+muletman@users.noreply.github.com> Date: Sun, 14 Jun 2020 17:03:18 +0100 Subject: [PATCH 0098/1197] Docs: Run multiple instances for new users This is my proposition of contribution for how new users could get up and running with multiple instances of the bot, based on the conversation I had on Slack with @hroff-1902 It appeared to me that the transparent creation and usage of the sqlite databases, and the necessity to create other databases to run multiple bots at the same time was not so straightforward to me in the first place, despite browsing through the docs. It is evident now ;) but that will maybe save time for devs if any other new user come on slack with the same issue. Thanks --- HowToRunMultipleInstances.md | 51 ++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 HowToRunMultipleInstances.md diff --git a/HowToRunMultipleInstances.md b/HowToRunMultipleInstances.md new file mode 100644 index 000000000..a5ec4c10c --- /dev/null +++ b/HowToRunMultipleInstances.md @@ -0,0 +1,51 @@ +# How to run multiple instances of freqtrade simultaneously + +This page is meant to be a quick tip for new users on how to run multiple bots a the same time, on the same computer (or other devices). + +In order to keep track of your trades, profits, etc., freqtrade is using a SQLite database where it stores various types of information such as the trades you performed in the past and the current position(s) you are holding at any time. This allow you to keep track of your profits, but most importantly, keep track of ongoing activity if the bot process would finish unexpectedly for one or another reason. + +As for various other things, upon docker or manual install, [freqtrade will create by default two different databases, one for dry-run, and the other for live trades.](https://www.freqtrade.io/en/latest/docker/#create-your-database-file) + +By default, executing the trade command in the command line interface, without specifying any database (`--db-url`) argument, freqtrade will store your trades and performance history in one of these two default databases, depending if dry-run mode is enabled or not. These databases are actual .sqlite files which are stored in your main freqtrade folder, under the name `tradesv3.dryrun.sqlite` for the dry-run mode, and `tradesv3.sqlite` for the live mode. + +The optional argument to the trade command used to specify the path of these files is `--db-url`. So when you are starting a bot with only the config and strategy arguments in dry-run mode for instance : + +``` +freqtrade trade -c MyConfig.json -s MyStrategy +``` + +is equivalent to : + +``` +freqtrade trade -c MyConfig.json -s MyStrategy --db-url sqlite:///tradesv3.dryrun.sqlite +``` + +That means that if you are running the trade command in two different terminals, for example to test your strategy both for trades in USDT and in another instance for trades in BTC, you will have to run them with different databases. Even if you are using the same configuration file and strategy. + + If you specify the URL of a database which does not exist, freqtrade will create one with the name you specified. So for example if you want to test your custom strategy in BTC vs USDT, you could type in one terminal : + +``` +freqtrade trade -c MyConfigBTC.json -s MyCustomStrategy --db-url sqlite://path/tradesBTC.dryrun.sqlite +``` + +and in the other + +``` +freqtrade trade -c MyConfigUSDT.json -s MyCustomStrategy --db-url sqlite://path/path/tradesUSDT.dryrun.sqlite +``` + +Conversely, if you wish to do the same thing in production mode, you will also have to create at least one new database (in addition to the default one) and specify the path to the "live" databases, for example : + +``` +freqtrade trade -c MyConfigBTC.json -s MyCustomStrategy --db-url sqlite://path/tradesBTC.live.sqlite +``` + +and in the other + +``` +freqtrade trade -c MyConfigUSDT.json -s MyCustomStrategy --db-url sqlite://path/path/tradesUSDT.live.sqlite +``` + +For more information regarding usage of the sqlite databases, for example to manually enter or remove trades, please refer to the following page : + +https://www.freqtrade.io/en/latest/sql_cheatsheet/ From d337fb6c6a4a3ca3e3f7bfb542b6bf7db0a788a8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jun 2020 06:35:31 +0200 Subject: [PATCH 0099/1197] Update some comments --- freqtrade/commands/pairlist_commands.py | 1 - freqtrade/optimize/hyperopt_interface.py | 4 ++-- freqtrade/resolvers/strategy_resolver.py | 2 +- tests/strategy/test_strategy.py | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/commands/pairlist_commands.py b/freqtrade/commands/pairlist_commands.py index dffe0c82e..77bcb04b4 100644 --- a/freqtrade/commands/pairlist_commands.py +++ b/freqtrade/commands/pairlist_commands.py @@ -25,7 +25,6 @@ def start_test_pairlist(args: Dict[str, Any]) -> None: results = {} for curr in quote_currencies: config['stake_currency'] = curr - # Do not use timeframe set in the config pairlists = PairListManager(exchange, config) pairlists.refresh_pairlist() results[curr] = pairlists.whitelist diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 00353cbf4..65069b984 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -31,14 +31,14 @@ class IHyperOpt(ABC): Class attributes you can use: ticker_interval -> int: value of the ticker interval to use for the strategy """ - ticker_interval: str # deprecated + ticker_interval: str # DEPRECATED timeframe: str def __init__(self, config: dict) -> None: self.config = config # Assign ticker_interval to be used in hyperopt - IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECTED + IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED IHyperOpt.timeframe = str(config['timeframe']) @staticmethod diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 26bce01ca..121a04877 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -54,7 +54,7 @@ class StrategyResolver(IResolver): # Assign ticker_interval to timeframe to keep compatibility if 'timeframe' not in config: logger.warning( - "DEPRECATED: Please migrate to using timeframe instead of ticker_interval." + "DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'." ) strategy.timeframe = strategy.ticker_interval diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py index 85cb8c132..240f3d8ec 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -386,7 +386,7 @@ def test_call_deprecated_function(result, monkeypatch, default_conf, caplog): assert isinstance(selldf, DataFrame) assert 'sell' in selldf - assert log_has('DEPRECATED: Please migrate to using timeframe instead of ticker_interval.', + assert log_has("DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'.", caplog) From 1853350c7d344a0860d2f2d371ca211dd5f3bb4b Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2020 09:01:05 +0000 Subject: [PATCH 0100/1197] Bump mkdocs-material from 5.2.3 to 5.3.0 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 5.2.3 to 5.3.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/5.2.3...5.3.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 b8ea338de..666cf5ac4 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==5.2.3 +mkdocs-material==5.3.0 mdx_truly_sane_lists==1.2 From d66050522c30da1d7b39fee9ff1ac5b906f39587 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2020 09:02:14 +0000 Subject: [PATCH 0101/1197] Bump ccxt from 1.29.52 to 1.30.2 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.29.52 to 1.30.2. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.29.52...1.30.2) 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 dab3a5da4..a6420d76a 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.29.52 +ccxt==1.30.2 SQLAlchemy==1.3.17 python-telegram-bot==12.7 arrow==0.15.6 From f38f3643f55157e52523df7651df79002b319433 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2020 09:02:34 +0000 Subject: [PATCH 0102/1197] Bump pytest-cov from 2.9.0 to 2.10.0 Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.9.0 to 2.10.0. - [Release notes](https://github.com/pytest-dev/pytest-cov/releases) - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.9.0...v2.10.0) 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 d62b768c1..d8d09a0c5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,7 +10,7 @@ flake8-tidy-imports==4.1.0 mypy==0.780 pytest==5.4.3 pytest-asyncio==0.12.0 -pytest-cov==2.9.0 +pytest-cov==2.10.0 pytest-mock==3.1.1 pytest-random-order==1.0.4 From df90d631fb3b32327dbae36d0e4d0b25205e7d98 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 15 Jun 2020 10:05:20 +0000 Subject: [PATCH 0103/1197] Bump flake8 from 3.8.2 to 3.8.3 Bumps [flake8](https://gitlab.com/pycqa/flake8) from 3.8.2 to 3.8.3. - [Release notes](https://gitlab.com/pycqa/flake8/tags) - [Commits](https://gitlab.com/pycqa/flake8/compare/3.8.2...3.8.3) 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 d8d09a0c5..840eff15f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,7 @@ -r requirements-hyperopt.txt coveralls==2.0.0 -flake8==3.8.2 +flake8==3.8.3 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 mypy==0.780 From e24ffebe691b3e90ac4c660edbb702aca1ca53c5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jun 2020 19:24:33 +0200 Subject: [PATCH 0104/1197] Move Multiple instances section to advanced-setup.md --- HowToRunMultipleInstances.md | 51 ----------------------------------- docs/advanced-setup.md | 52 ++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 51 deletions(-) delete mode 100644 HowToRunMultipleInstances.md diff --git a/HowToRunMultipleInstances.md b/HowToRunMultipleInstances.md deleted file mode 100644 index a5ec4c10c..000000000 --- a/HowToRunMultipleInstances.md +++ /dev/null @@ -1,51 +0,0 @@ -# How to run multiple instances of freqtrade simultaneously - -This page is meant to be a quick tip for new users on how to run multiple bots a the same time, on the same computer (or other devices). - -In order to keep track of your trades, profits, etc., freqtrade is using a SQLite database where it stores various types of information such as the trades you performed in the past and the current position(s) you are holding at any time. This allow you to keep track of your profits, but most importantly, keep track of ongoing activity if the bot process would finish unexpectedly for one or another reason. - -As for various other things, upon docker or manual install, [freqtrade will create by default two different databases, one for dry-run, and the other for live trades.](https://www.freqtrade.io/en/latest/docker/#create-your-database-file) - -By default, executing the trade command in the command line interface, without specifying any database (`--db-url`) argument, freqtrade will store your trades and performance history in one of these two default databases, depending if dry-run mode is enabled or not. These databases are actual .sqlite files which are stored in your main freqtrade folder, under the name `tradesv3.dryrun.sqlite` for the dry-run mode, and `tradesv3.sqlite` for the live mode. - -The optional argument to the trade command used to specify the path of these files is `--db-url`. So when you are starting a bot with only the config and strategy arguments in dry-run mode for instance : - -``` -freqtrade trade -c MyConfig.json -s MyStrategy -``` - -is equivalent to : - -``` -freqtrade trade -c MyConfig.json -s MyStrategy --db-url sqlite:///tradesv3.dryrun.sqlite -``` - -That means that if you are running the trade command in two different terminals, for example to test your strategy both for trades in USDT and in another instance for trades in BTC, you will have to run them with different databases. Even if you are using the same configuration file and strategy. - - If you specify the URL of a database which does not exist, freqtrade will create one with the name you specified. So for example if you want to test your custom strategy in BTC vs USDT, you could type in one terminal : - -``` -freqtrade trade -c MyConfigBTC.json -s MyCustomStrategy --db-url sqlite://path/tradesBTC.dryrun.sqlite -``` - -and in the other - -``` -freqtrade trade -c MyConfigUSDT.json -s MyCustomStrategy --db-url sqlite://path/path/tradesUSDT.dryrun.sqlite -``` - -Conversely, if you wish to do the same thing in production mode, you will also have to create at least one new database (in addition to the default one) and specify the path to the "live" databases, for example : - -``` -freqtrade trade -c MyConfigBTC.json -s MyCustomStrategy --db-url sqlite://path/tradesBTC.live.sqlite -``` - -and in the other - -``` -freqtrade trade -c MyConfigUSDT.json -s MyCustomStrategy --db-url sqlite://path/path/tradesUSDT.live.sqlite -``` - -For more information regarding usage of the sqlite databases, for example to manually enter or remove trades, please refer to the following page : - -https://www.freqtrade.io/en/latest/sql_cheatsheet/ diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md index 95480a2c6..9848d04c8 100644 --- a/docs/advanced-setup.md +++ b/docs/advanced-setup.md @@ -4,6 +4,58 @@ This page explains some advanced tasks and configuration options that can be per If you do not know what things mentioned here mean, you probably do not need it. +## Running multiple instances of Freqtrade + +This page is meant to be a quick tip for new users on how to run multiple bots a the same time, on the same computer (or other devices). + +In order to keep track of your trades, profits, etc., freqtrade is using a SQLite database where it stores various types of information such as the trades you performed in the past and the current position(s) you are holding at any time. This allow you to keep track of your profits, but most importantly, keep track of ongoing activity if the bot process would finish unexpectedly for one or another reason. + +As for various other things, upon docker or manual install, [freqtrade will create by default two different databases, one for dry-run, and the other for live trades.](https://www.freqtrade.io/en/latest/docker/#create-your-database-file) + +By default, executing the trade command in the command line interface, without specifying any database (`--db-url`) argument, freqtrade will store your trades and performance history in one of these two default databases, depending if dry-run mode is enabled or not. These databases are actual .sqlite files which are stored in your main freqtrade folder, under the name `tradesv3.dryrun.sqlite` for the dry-run mode, and `tradesv3.sqlite` for the live mode. + +The optional argument to the trade command used to specify the path of these files is `--db-url`. So when you are starting a bot with only the config and strategy arguments in dry-run mode for instance : + +``` bash +freqtrade trade -c MyConfig.json -s MyStrategy +``` + +is equivalent to: + +``` bash +freqtrade trade -c MyConfig.json -s MyStrategy --db-url sqlite:///tradesv3.dryrun.sqlite +``` + +That means that if you are running the trade command in two different terminals, for example to test your strategy both for trades in USDT and in another instance for trades in BTC, you will have to run them with different databases. Even if you are using the same configuration file and strategy. + + If you specify the URL of a database which does not exist, freqtrade will create one with the name you specified. So for example if you want to test your custom strategy in BTC vs USDT, you could type in one terminal : + +``` bash +freqtrade trade -c MyConfigBTC.json -s MyCustomStrategy --db-url sqlite://path/tradesBTC.dryrun.sqlite +``` + +and in the other + +``` bash +freqtrade trade -c MyConfigUSDT.json -s MyCustomStrategy --db-url sqlite://path/path/tradesUSDT.dryrun.sqlite +``` + +Conversely, if you wish to do the same thing in production mode, you will also have to create at least one new database (in addition to the default one) and specify the path to the "live" databases, for example : + +``` bash +freqtrade trade -c MyConfigBTC.json -s MyCustomStrategy --db-url sqlite://path/tradesBTC.live.sqlite +``` + +and in the other + +``` bash +freqtrade trade -c MyConfigUSDT.json -s MyCustomStrategy --db-url sqlite://path/path/tradesUSDT.live.sqlite +``` + +For more information regarding usage of the sqlite databases, for example to manually enter or remove trades, please refer to the following page: + +https://www.freqtrade.io/en/latest/sql_cheatsheet/ + ## Configure the bot running as a systemd service Copy the `freqtrade.service` file to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup. From 61ce18a4abb17b3c5c7d46701b62a4b4504410ce Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jun 2020 19:35:57 +0200 Subject: [PATCH 0105/1197] Slightly reword certain things - add link in FAQ --- docs/advanced-setup.md | 56 ++++++++++++++++++++---------------------- docs/faq.md | 4 +++ 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md index 9848d04c8..21e428fa7 100644 --- a/docs/advanced-setup.md +++ b/docs/advanced-setup.md @@ -6,55 +6,51 @@ If you do not know what things mentioned here mean, you probably do not need it. ## Running multiple instances of Freqtrade -This page is meant to be a quick tip for new users on how to run multiple bots a the same time, on the same computer (or other devices). +This section will show you how to run multiple bots a the same time, on the same computer (or other devices). -In order to keep track of your trades, profits, etc., freqtrade is using a SQLite database where it stores various types of information such as the trades you performed in the past and the current position(s) you are holding at any time. This allow you to keep track of your profits, but most importantly, keep track of ongoing activity if the bot process would finish unexpectedly for one or another reason. +### Things to consider -As for various other things, upon docker or manual install, [freqtrade will create by default two different databases, one for dry-run, and the other for live trades.](https://www.freqtrade.io/en/latest/docker/#create-your-database-file) +* use different database files. +* use different telegram Bots (requires 2 different configuration files). +* use different ports (*applies only when webserver is enabled). -By default, executing the trade command in the command line interface, without specifying any database (`--db-url`) argument, freqtrade will store your trades and performance history in one of these two default databases, depending if dry-run mode is enabled or not. These databases are actual .sqlite files which are stored in your main freqtrade folder, under the name `tradesv3.dryrun.sqlite` for the dry-run mode, and `tradesv3.sqlite` for the live mode. +#### Different database files -The optional argument to the trade command used to specify the path of these files is `--db-url`. So when you are starting a bot with only the config and strategy arguments in dry-run mode for instance : +In order to keep track of your trades, profits, etc., freqtrade is using a SQLite database where it stores various types of information such as the trades you performed in the past and the current position(s) you are holding at any time. This allows you to keep track of your profits, but most importantly, keep track of ongoing activity if the bot process would be restarted or would be terminated unexpectently. + +Freqtrade will, by default, use seperate database files for dry-run and live bots (this assumes no database-url is given in either configuration nor via command line argument). +For live trading mode, the default database will be `tradesv3.sqlite`, and for dry-run, it will be `tradesv3.dryrun.sqlite`. + +The optional argument to the trade command used to specify the path of these files is `--db-url`, which requires a valid SQLAlchemy url. +So when you are starting a bot with only the config and strategy arguments in dry-run mode, the following 2 commands would have the same outcome. ``` bash freqtrade trade -c MyConfig.json -s MyStrategy -``` - -is equivalent to: - -``` bash +# is equivalent to freqtrade trade -c MyConfig.json -s MyStrategy --db-url sqlite:///tradesv3.dryrun.sqlite ``` -That means that if you are running the trade command in two different terminals, for example to test your strategy both for trades in USDT and in another instance for trades in BTC, you will have to run them with different databases. Even if you are using the same configuration file and strategy. +That means that if you are running the trade command in two different terminals, for example to test your strategy both for trades in USDT and in another instance for trades in BTC, you will have to run them with different databases. - If you specify the URL of a database which does not exist, freqtrade will create one with the name you specified. So for example if you want to test your custom strategy in BTC vs USDT, you could type in one terminal : +If you specify the URL of a database which does not exist, freqtrade will create one with the name you specified. So to test your custom strategy in BTC and USDT, you could use the following commands (in 2 seperate terminals): ``` bash -freqtrade trade -c MyConfigBTC.json -s MyCustomStrategy --db-url sqlite://path/tradesBTC.dryrun.sqlite +# Terminal 1: +freqtrade trade -c MyConfigBTC.json -s MyCustomStrategy --db-url sqlite:///user_data/tradesBTC.dryrun.sqlite +# Terminal 2: +freqtrade trade -c MyConfigUSDT.json -s MyCustomStrategy --db-url sqlite:///user_data/tradesUSDT.dryrun.sqlite ``` -and in the other +Conversely, if you wish to do the same thing in production mode, you will also have to create at least one new database (in addition to the default one) and specify the path to the "live" databases, for example: ``` bash -freqtrade trade -c MyConfigUSDT.json -s MyCustomStrategy --db-url sqlite://path/path/tradesUSDT.dryrun.sqlite +# Terminal 1: +freqtrade trade -c MyConfigBTC.json -s MyCustomStrategy --db-url sqlite:///user_data/tradesBTC.live.sqlite +# Terminal 2: +freqtrade trade -c MyConfigUSDT.json -s MyCustomStrategy --db-url sqlite:///user_data/tradesUSDT.live.sqlite ``` -Conversely, if you wish to do the same thing in production mode, you will also have to create at least one new database (in addition to the default one) and specify the path to the "live" databases, for example : - -``` bash -freqtrade trade -c MyConfigBTC.json -s MyCustomStrategy --db-url sqlite://path/tradesBTC.live.sqlite -``` - -and in the other - -``` bash -freqtrade trade -c MyConfigUSDT.json -s MyCustomStrategy --db-url sqlite://path/path/tradesUSDT.live.sqlite -``` - -For more information regarding usage of the sqlite databases, for example to manually enter or remove trades, please refer to the following page: - -https://www.freqtrade.io/en/latest/sql_cheatsheet/ +For more information regarding usage of the sqlite databases, for example to manually enter or remove trades, please refer to the [SQL Cheatsheet](sql_cheatsheet.md) ## Configure the bot running as a systemd service diff --git a/docs/faq.md b/docs/faq.md index 8e8a1bf35..31c49171d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -45,6 +45,10 @@ the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-c You can use the `/forcesell all` command from Telegram. +### I want to run multiple bots on the same machine + +Please look at the [advanced setup documentation Page](advanced-setup.md#running-multiple-instances-of-freqtrade). + ### I'm getting the "RESTRICTED_MARKET" message in the log Currently known to happen for US Bittrex users. From 5e4dd44155958487d5ae141953951f9471a3843f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jun 2020 20:55:06 +0200 Subject: [PATCH 0106/1197] Fix formatting --- docs/advanced-setup.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md index 21e428fa7..d3f692f33 100644 --- a/docs/advanced-setup.md +++ b/docs/advanced-setup.md @@ -6,13 +6,13 @@ If you do not know what things mentioned here mean, you probably do not need it. ## Running multiple instances of Freqtrade -This section will show you how to run multiple bots a the same time, on the same computer (or other devices). +This section will show you how to run multiple bots a the same time, on the same machine. ### Things to consider * use different database files. * use different telegram Bots (requires 2 different configuration files). -* use different ports (*applies only when webserver is enabled). +* use different ports (applies only when webserver is enabled). #### Different database files From 9cc04c929ff13595d0e29ab72c75673233eaa648 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Jun 2020 07:17:15 +0200 Subject: [PATCH 0107/1197] Fix documentation typo --- docs/data-download.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index 903d62854..3fb775e69 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -109,7 +109,7 @@ The following command will convert all candle (OHLCV) data available in `~/.freq It'll also remove original json data files (`--erase` parameter). ``` bash -freqtrade convert-data --format-from json --format-to jsongz --data-dir ~/.freqtrade/data/binance -t 5m 15m --erase +freqtrade convert-data --format-from json --format-to jsongz --datadir ~/.freqtrade/data/binance -t 5m 15m --erase ``` #### Subcommand convert-trade data @@ -155,7 +155,7 @@ The following command will convert all available trade-data in `~/.freqtrade/dat It'll also remove original jsongz data files (`--erase` parameter). ``` bash -freqtrade convert-trade-data --format-from jsongz --format-to json --data-dir ~/.freqtrade/data/kraken --erase +freqtrade convert-trade-data --format-from jsongz --format-to json --datadir ~/.freqtrade/data/kraken --erase ``` ### Pairs file From 9dba2a34f9edc91a9e0e3ab0f970fb3286aa9b75 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Jun 2020 10:16:23 +0200 Subject: [PATCH 0108/1197] Add note for hyperopt color support on windows --- docs/hyperopt.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 8efc51a39..9f7f97476 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -370,6 +370,9 @@ By default, hyperopt prints colorized results -- epochs with positive profit are You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. When `--print-all` is used, current best results are also colorized by default -- they are printed in bold (bright) style. This can also be switched off with the `--no-color` command line option. +!!! Note "Windows and color output" + Windows does not support color-output nativly, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL. + ### Understand Hyperopt ROI results If you are optimizing ROI (i.e. if optimization search-space contains 'all', 'default' or 'roi'), your result will look as follows and include a ROI table: From 3517c86fa28786630bcb49c3c58bcddd41f0cf74 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 16 Jun 2020 16:02:38 +0200 Subject: [PATCH 0109/1197] Fail if both ticker_interval and timeframe are present in a configuration Otherwise the wrong might be used, as it's unclear which one the intend of the user is --- .../configuration/deprecated_settings.py | 5 +++++ tests/test_configuration.py | 20 ++++++++++++++----- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/freqtrade/configuration/deprecated_settings.py b/freqtrade/configuration/deprecated_settings.py index cefc6ac14..03ed41ab8 100644 --- a/freqtrade/configuration/deprecated_settings.py +++ b/freqtrade/configuration/deprecated_settings.py @@ -72,4 +72,9 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None: "DEPRECATED: " "Please use 'timeframe' instead of 'ticker_interval." ) + if 'timeframe' in config: + raise OperationalException( + "Both 'timeframe' and 'ticker_interval' detected." + "Please remove 'ticker_interval' from your configuration to continue operating." + ) config['timeframe'] = config['ticker_interval'] diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 689e62ab9..cccc87670 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1144,11 +1144,21 @@ def test_process_deprecated_setting(mocker, default_conf, caplog): def test_process_deprecated_ticker_interval(mocker, default_conf, caplog): message = "DEPRECATED: Please use 'timeframe' instead of 'ticker_interval." - process_temporary_deprecated_settings(default_conf) + config = deepcopy(default_conf) + process_temporary_deprecated_settings(config) assert not log_has(message, caplog) - del default_conf['timeframe'] - default_conf['ticker_interval'] = '15m' - process_temporary_deprecated_settings(default_conf) + del config['timeframe'] + config['ticker_interval'] = '15m' + process_temporary_deprecated_settings(config) assert log_has(message, caplog) - assert default_conf['ticker_interval'] == '15m' + assert config['ticker_interval'] == '15m' + + config = deepcopy(default_conf) + # Have both timeframe and ticker interval in config + # Can also happen when using ticker_interval in configuration, and --timeframe as cli argument + config['timeframe'] = '5m' + config['ticker_interval'] = '4h' + with pytest.raises(OperationalException, + match=r"Both 'timeframe' and 'ticker_interval' detected."): + process_temporary_deprecated_settings(config) From d4fb5af456771083cf3facfd296fc59001d59fdb Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Jun 2020 07:23:20 +0200 Subject: [PATCH 0110/1197] Also reload async markets fixes #2876 - Logs and Empty ticker history for new pair --- freqtrade/exchange/exchange.py | 2 ++ tests/exchange/test_exchange.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 35c62db27..b62410c34 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -285,6 +285,8 @@ class Exchange: logger.debug("Performing scheduled market reload..") try: self._api.load_markets(reload=True) + # Also reload async markets to avoid issues with newly listed pairs + self._load_async_markets(reload=True) self._last_markets_refresh = arrow.utcnow().timestamp except ccxt.BaseError: logger.exception("Could not reload markets.") diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 762ee295e..c9a600d50 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -365,6 +365,7 @@ def test_reload_markets(default_conf, mocker, caplog): default_conf['exchange']['markets_refresh_interval'] = 10 exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance", mock_markets=False) + exchange._load_async_markets = MagicMock() exchange._last_markets_refresh = arrow.utcnow().timestamp updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}} @@ -373,11 +374,13 @@ def test_reload_markets(default_conf, mocker, caplog): # less than 10 minutes have passed, no reload exchange.reload_markets() assert exchange.markets == initial_markets + assert exchange._load_async_markets.call_count == 0 # more than 10 minutes have passed, reload is executed exchange._last_markets_refresh = arrow.utcnow().timestamp - 15 * 60 exchange.reload_markets() assert exchange.markets == updated_markets + assert exchange._load_async_markets.call_count == 1 assert log_has('Performing scheduled market reload..', caplog) From e2465f979bd6641f0413f58719622e471afef807 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 17 Jun 2020 08:33:53 +0200 Subject: [PATCH 0111/1197] Correctly mock out async_reload --- tests/conftest.py | 1 + tests/exchange/test_exchange.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 3ca431f40..a4106c767 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -56,6 +56,7 @@ def patched_configuration_load_config_file(mocker, config) -> None: def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> None: + mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index c9a600d50..700aff969 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -319,7 +319,12 @@ def test_set_sandbox_exception(default_conf, mocker): def test__load_async_markets(default_conf, mocker, caplog): - exchange = get_patched_exchange(mocker, default_conf) + mocker.patch('freqtrade.exchange.Exchange._init_ccxt') + mocker.patch('freqtrade.exchange.Exchange.validate_pairs') + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange._load_markets') + mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') + exchange = Exchange(default_conf) exchange._api_async.load_markets = get_mock_coro(None) exchange._load_async_markets() assert exchange._api_async.load_markets.call_count == 1 From 0b8cac68befa83b2e705da8d350442b7a06cfe8c Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Wed, 17 Jun 2020 22:49:01 +0300 Subject: [PATCH 0112/1197] Improve advanced-setup.md --- docs/advanced-setup.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md index d3f692f33..f03bc10c0 100644 --- a/docs/advanced-setup.md +++ b/docs/advanced-setup.md @@ -6,20 +6,20 @@ If you do not know what things mentioned here mean, you probably do not need it. ## Running multiple instances of Freqtrade -This section will show you how to run multiple bots a the same time, on the same machine. +This section will show you how to run multiple bots at the same time, on the same machine. ### Things to consider -* use different database files. -* use different telegram Bots (requires 2 different configuration files). -* use different ports (applies only when webserver is enabled). +* Use different database files. +* Use different Telegram bots (requires multiple different configuration files; applies only when Telegram is enabled). +* Use different ports (applies only when Freqtrade REST API webserver is enabled). -#### Different database files +### Different database files -In order to keep track of your trades, profits, etc., freqtrade is using a SQLite database where it stores various types of information such as the trades you performed in the past and the current position(s) you are holding at any time. This allows you to keep track of your profits, but most importantly, keep track of ongoing activity if the bot process would be restarted or would be terminated unexpectently. +In order to keep track of your trades, profits, etc., freqtrade is using a SQLite database where it stores various types of information such as the trades you performed in the past and the current position(s) you are holding at any time. This allows you to keep track of your profits, but most importantly, keep track of ongoing activity if the bot process would be restarted or would be terminated unexpectedly. -Freqtrade will, by default, use seperate database files for dry-run and live bots (this assumes no database-url is given in either configuration nor via command line argument). -For live trading mode, the default database will be `tradesv3.sqlite`, and for dry-run, it will be `tradesv3.dryrun.sqlite`. +Freqtrade will, by default, use separate database files for dry-run and live bots (this assumes no database-url is given in either configuration nor via command line argument). +For live trading mode, the default database will be `tradesv3.sqlite` and for dry-run it will be `tradesv3.dryrun.sqlite`. The optional argument to the trade command used to specify the path of these files is `--db-url`, which requires a valid SQLAlchemy url. So when you are starting a bot with only the config and strategy arguments in dry-run mode, the following 2 commands would have the same outcome. @@ -30,9 +30,9 @@ freqtrade trade -c MyConfig.json -s MyStrategy freqtrade trade -c MyConfig.json -s MyStrategy --db-url sqlite:///tradesv3.dryrun.sqlite ``` -That means that if you are running the trade command in two different terminals, for example to test your strategy both for trades in USDT and in another instance for trades in BTC, you will have to run them with different databases. +It means that if you are running the trade command in two different terminals, for example to test your strategy both for trades in USDT and in another instance for trades in BTC, you will have to run them with different databases. -If you specify the URL of a database which does not exist, freqtrade will create one with the name you specified. So to test your custom strategy in BTC and USDT, you could use the following commands (in 2 seperate terminals): +If you specify the URL of a database which does not exist, freqtrade will create one with the name you specified. So to test your custom strategy with BTC and USDT stake currencies, you could use the following commands (in 2 separate terminals): ``` bash # Terminal 1: @@ -50,7 +50,7 @@ freqtrade trade -c MyConfigBTC.json -s MyCustomStrategy --db-url sqlite:///user_ freqtrade trade -c MyConfigUSDT.json -s MyCustomStrategy --db-url sqlite:///user_data/tradesUSDT.live.sqlite ``` -For more information regarding usage of the sqlite databases, for example to manually enter or remove trades, please refer to the [SQL Cheatsheet](sql_cheatsheet.md) +For more information regarding usage of the sqlite databases, for example to manually enter or remove trades, please refer to the [SQL Cheatsheet](sql_cheatsheet.md). ## Configure the bot running as a systemd service From fd97ad9b76973f7ec463591be62e577a9b703b1e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 12 Jun 2020 14:02:21 +0200 Subject: [PATCH 0113/1197] Cache analyzed dataframe --- freqtrade/data/dataprovider.py | 34 ++++++++++++++++++++++++++++++--- freqtrade/strategy/interface.py | 10 +++------- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 058ca42da..8dc53b034 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -5,16 +5,16 @@ including ticker and orderbook data, live and historical candle (OHLCV) data Common Interface for bot and strategy to access data. """ import logging -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple +from arrow import Arrow from pandas import DataFrame +from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.history import load_pair_history from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import Exchange from freqtrade.state import RunMode -from freqtrade.constants import ListPairsWithTimeframes - logger = logging.getLogger(__name__) @@ -25,6 +25,21 @@ class DataProvider: self._config = config self._exchange = exchange self._pairlists = pairlists + self.__cached_pairs: Dict[Tuple(str, str), DataFrame] = {} + + def _set_cached_df(self, pair: str, timeframe: str, dataframe: DataFrame) -> None: + """ + Store cached Dataframe. + Using private method as this should never be used by a user + (but the class is exposed via `self.dp` to the strategy) + :param pair: pair to get the data for + :param timeframe: Timeframe to get data for + :param dataframe: analyzed dataframe + """ + self.__cached_pairs[(pair, timeframe)] = { + 'data': dataframe, + 'updated': Arrow.utcnow().datetime, + } def refresh(self, pairlist: ListPairsWithTimeframes, @@ -89,6 +104,19 @@ class DataProvider: logger.warning(f"No data found for ({pair}, {timeframe}).") return data + def get_analyzed_dataframe(self, pair: str, timeframe: str = None) -> DataFrame: + """ + :param pair: pair to get the data for + :param timeframe: timeframe to get data for + :return: Analyzed Dataframe for this pair + """ + # TODO: check updated time and don't return outdated data. + if (pair, timeframe) in self._set_cached_df: + return self._set_cached_df[(pair, timeframe)]['data'] + else: + # TODO: this is most likely wrong... + raise ValueError(f"No analyzed dataframe found for ({pair}, {timeframe})") + def market(self, pair: str) -> Optional[Dict[str, Any]]: """ Return market data for the pair diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index f9f3a3678..843197602 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -273,6 +273,7 @@ class IStrategy(ABC): # Defs that only make change on new candle data. dataframe = self.analyze_ticker(dataframe, metadata) self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date'] + self.dp._set_cached_df(pair, self.timeframe, dataframe) else: logger.debug("Skipping TA Analysis for already analyzed candle") dataframe['buy'] = 0 @@ -348,13 +349,8 @@ class IStrategy(ABC): return False, False (buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1 - logger.debug( - 'trigger: %s (pair=%s) buy=%s sell=%s', - latest['date'], - pair, - str(buy), - str(sell) - ) + logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', + latest['date'], pair, str(buy), str(sell)) return buy, sell def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, From 9794914838e9f90bcdbf021583b75499db8ad7e8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 12 Jun 2020 14:12:33 +0200 Subject: [PATCH 0114/1197] store dataframe updated as tuple --- freqtrade/constants.py | 3 ++- freqtrade/data/dataprovider.py | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 6741e0605..45df778cd 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -336,4 +336,5 @@ CANCEL_REASON = { } # List of pairs with their timeframes -ListPairsWithTimeframes = List[Tuple[str, str]] +PairWithTimeframe = Tuple[str, str] +ListPairsWithTimeframes = List[PairWithTimeframe] diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 8dc53b034..d93b77121 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -5,12 +5,13 @@ including ticker and orderbook data, live and historical candle (OHLCV) data Common Interface for bot and strategy to access data. """ import logging +from datetime import datetime from typing import Any, Dict, List, Optional, Tuple from arrow import Arrow from pandas import DataFrame -from freqtrade.constants import ListPairsWithTimeframes +from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe from freqtrade.data.history import load_pair_history from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.exchange import Exchange @@ -25,7 +26,7 @@ class DataProvider: self._config = config self._exchange = exchange self._pairlists = pairlists - self.__cached_pairs: Dict[Tuple(str, str), DataFrame] = {} + self.__cached_pairs: Dict[PairWithTimeframe, Tuple(DataFrame, datetime)] = {} def _set_cached_df(self, pair: str, timeframe: str, dataframe: DataFrame) -> None: """ @@ -36,10 +37,7 @@ class DataProvider: :param timeframe: Timeframe to get data for :param dataframe: analyzed dataframe """ - self.__cached_pairs[(pair, timeframe)] = { - 'data': dataframe, - 'updated': Arrow.utcnow().datetime, - } + self.__cached_pairs[(pair, timeframe)] = (dataframe, Arrow.utcnow().datetime) def refresh(self, pairlist: ListPairsWithTimeframes, @@ -104,15 +102,17 @@ class DataProvider: logger.warning(f"No data found for ({pair}, {timeframe}).") return data - def get_analyzed_dataframe(self, pair: str, timeframe: str = None) -> DataFrame: + def get_analyzed_dataframe(self, pair: str, + timeframe: str = None) -> Tuple[DataFrame, datetime]: """ :param pair: pair to get the data for :param timeframe: timeframe to get data for - :return: Analyzed Dataframe for this pair + :return: Tuple of (Analyzed Dataframe, lastrefreshed) for the requested pair / timeframe + combination """ # TODO: check updated time and don't return outdated data. - if (pair, timeframe) in self._set_cached_df: - return self._set_cached_df[(pair, timeframe)]['data'] + if (pair, timeframe) in self.__cached_pairs: + return self.__cached_pairs[(pair, timeframe)] else: # TODO: this is most likely wrong... raise ValueError(f"No analyzed dataframe found for ({pair}, {timeframe})") From 95f3ac08d406dd635f74dd68552fd93680c62267 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Jun 2020 07:09:44 +0200 Subject: [PATCH 0115/1197] Update some comments --- freqtrade/strategy/interface.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 843197602..0ef92b315 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -204,6 +204,10 @@ class IStrategy(ABC): """ return [] +### +# END - Intended to be overridden by strategy +### + def get_strategy_name(self) -> str: """ Returns strategy class name @@ -308,6 +312,7 @@ class IStrategy(ABC): def get_signal(self, pair: str, interval: str, dataframe: DataFrame) -> Tuple[bool, bool]: """ Calculates current signal based several technical analysis indicators + Used by Bot to get the latest signal :param pair: pair in format ANT/BTC :param interval: Interval to use (in min) :param dataframe: Dataframe to analyze @@ -496,7 +501,8 @@ class IStrategy(ABC): def ohlcvdata_to_dataframe(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]: """ - Creates a dataframe and populates indicators for given candle (OHLCV) data + Populates indicators for given candle (OHLCV) data (for multiple pairs) + Does not run advice_buy or advise_sell! Used by optimize operations only, not during dry / live runs. Using .copy() to get a fresh copy of the dataframe for every strategy run. Has positive effects on memory usage for whatever reason - also when From 273aaaff12e273c58adf2ba05170b189e5b24125 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Jun 2020 18:06:52 +0200 Subject: [PATCH 0116/1197] Introduce .analyze() function for Strategy Fixing a few tests along the way --- freqtrade/freqtradebot.py | 10 +++--- freqtrade/strategy/interface.py | 59 +++++++++++++++++++++++---------- tests/conftest.py | 2 +- tests/test_freqtradebot.py | 1 + 4 files changed, 47 insertions(+), 25 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 289850709..3ad0d061a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -151,6 +151,8 @@ class FreqtradeBot: self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist), self.strategy.informative_pairs()) + self.strategy.analyze(self.active_pair_whitelist) + with self._sell_lock: # Check and handle any timed out open orders self.check_handle_timedout() @@ -420,9 +422,7 @@ class FreqtradeBot: return False # running get_signal on historical data fetched - (buy, sell) = self.strategy.get_signal( - pair, self.strategy.timeframe, - self.dataprovider.ohlcv(pair, self.strategy.timeframe)) + (buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe) if buy and not sell: stake_amount = self.get_trade_stake_amount(pair) @@ -697,9 +697,7 @@ class FreqtradeBot: if (config_ask_strategy.get('use_sell_signal', True) or config_ask_strategy.get('ignore_roi_if_buy_signal', False)): - (buy, sell) = self.strategy.get_signal( - trade.pair, self.strategy.timeframe, - self.dataprovider.ohlcv(trade.pair, self.strategy.timeframe)) + (buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe) if config_ask_strategy.get('use_order_book', False): order_book_min = config_ask_strategy.get('order_book_min', 1) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 0ef92b315..9dcc2d613 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -7,20 +7,19 @@ import warnings from abc import ABC, abstractmethod from datetime import datetime, timezone from enum import Enum -from typing import Dict, NamedTuple, Optional, Tuple +from typing import Dict, List, NamedTuple, Optional, Tuple import arrow from pandas import DataFrame +from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import StrategyError from freqtrade.exchange import timeframe_to_minutes from freqtrade.persistence import Trade from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper -from freqtrade.constants import ListPairsWithTimeframes from freqtrade.wallets import Wallets - logger = logging.getLogger(__name__) @@ -289,6 +288,38 @@ class IStrategy(ABC): return dataframe + def analyze_pair(self, pair: str): + """ + Fetch data for this pair from dataprovider and analyze. + Stores the dataframe into the dataprovider. + The analyzed dataframe is then accessible via `dp.get_analyzed_dataframe()`. + :param pair: Pair to analyze. + """ + dataframe = self.dp.ohlcv(pair, self.timeframe) + if not isinstance(dataframe, DataFrame) or dataframe.empty: + logger.warning('Empty candle (OHLCV) data for pair %s', pair) + return + + try: + df_len, df_close, df_date = self.preserve_df(dataframe) + + dataframe = strategy_safe_wrapper( + self._analyze_ticker_internal, message="" + )(dataframe, {'pair': pair}) + + self.assert_df(dataframe, df_len, df_close, df_date) + except StrategyError as error: + logger.warning(f"Unable to analyze candle (OHLCV) data for pair {pair}: {error}") + return + + if dataframe.empty: + logger.warning('Empty dataframe for pair %s', pair) + return + + def analyze(self, pairs: List[str]): + for pair in pairs: + self.analyze_pair(pair) + @staticmethod def preserve_df(dataframe: DataFrame) -> Tuple[int, float, datetime]: """ keep some data for dataframes """ @@ -309,30 +340,22 @@ class IStrategy(ABC): else: raise StrategyError(f"Dataframe returned from strategy has mismatching {message}.") - def get_signal(self, pair: str, interval: str, dataframe: DataFrame) -> Tuple[bool, bool]: + def get_signal(self, pair: str, timeframe: str) -> Tuple[bool, bool]: """ Calculates current signal based several technical analysis indicators Used by Bot to get the latest signal :param pair: pair in format ANT/BTC - :param interval: Interval to use (in min) + :param timeframe: timeframe to use :param dataframe: Dataframe to analyze :return: (Buy, Sell) A bool-tuple indicating buy/sell signal """ + + dataframe, _ = self.dp.get_analyzed_dataframe(pair, timeframe) + if not isinstance(dataframe, DataFrame) or dataframe.empty: logger.warning('Empty candle (OHLCV) data for pair %s', pair) return False, False - try: - df_len, df_close, df_date = self.preserve_df(dataframe) - dataframe = strategy_safe_wrapper( - self._analyze_ticker_internal, message="" - )(dataframe, {'pair': pair}) - self.assert_df(dataframe, df_len, df_close, df_date) - except StrategyError as error: - logger.warning(f"Unable to analyze candle (OHLCV) data for pair {pair}: {error}") - - return False, False - if dataframe.empty: logger.warning('Empty dataframe for pair %s', pair) return False, False @@ -343,9 +366,9 @@ class IStrategy(ABC): latest_date = arrow.get(latest_date) # Check if dataframe is out of date - interval_minutes = timeframe_to_minutes(interval) + timeframe_minutes = timeframe_to_minutes(timeframe) offset = self.config.get('exchange', {}).get('outdated_offset', 5) - if latest_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))): + if latest_date < (arrow.utcnow().shift(minutes=-(timeframe_minutes * 2 + offset))): logger.warning( 'Outdated history for pair %s. Last tick is %s minutes old', pair, diff --git a/tests/conftest.py b/tests/conftest.py index a4106c767..3be7bbd22 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -163,7 +163,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: :param value: which value IStrategy.get_signal() must return :return: None """ - freqtrade.strategy.get_signal = lambda e, s, t: value + freqtrade.strategy.get_signal = lambda e, s: value freqtrade.exchange.refresh_latest_ohlcv = lambda p: None diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5d83c893e..e83ac2038 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -912,6 +912,7 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None: refresh_latest_ohlcv=refresh_mock, ) inf_pairs = MagicMock(return_value=[("BTC/ETH", '1m'), ("ETH/USDT", "1h")]) + mocker.patch('freqtrade.strategy.interface.IStrategy.get_signal', return_value=(False, False)) mocker.patch('time.sleep', return_value=None) freqtrade = FreqtradeBot(default_conf) From 55fa514ec93a61994c4ee738a5ee4d443c8f15a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Jun 2020 19:40:58 +0200 Subject: [PATCH 0117/1197] Adapt most tests --- freqtrade/strategy/interface.py | 4 -- tests/strategy/test_interface.py | 72 +++++++++++++++----------------- 2 files changed, 34 insertions(+), 42 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 9dcc2d613..a7d467cb2 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -353,10 +353,6 @@ class IStrategy(ABC): dataframe, _ = self.dp.get_analyzed_dataframe(pair, timeframe) if not isinstance(dataframe, DataFrame) or dataframe.empty: - logger.warning('Empty candle (OHLCV) data for pair %s', pair) - return False, False - - if dataframe.empty: logger.warning('Empty dataframe for pair %s', pair) return False, False diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 59b4d5902..da8de947a 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -13,12 +13,14 @@ from freqtrade.exceptions import StrategyError from freqtrade.persistence import Trade from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper +from freqtrade.data.dataprovider import DataProvider from tests.conftest import get_patched_exchange, log_has, log_has_re from .strats.default_strategy import DefaultStrategy # Avoid to reinit the same object again and again _STRATEGY = DefaultStrategy(config={}) +_STRATEGY.dp = DataProvider({}, None, None) def test_returns_latest_signal(mocker, default_conf, ohlcv_history): @@ -30,61 +32,65 @@ def test_returns_latest_signal(mocker, default_conf, ohlcv_history): mocked_history.loc[1, 'sell'] = 1 mocker.patch.object( - _STRATEGY, '_analyze_ticker_internal', - return_value=mocked_history + _STRATEGY.dp, 'get_analyzed_dataframe', + return_value=(mocked_history, 0) ) - assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (False, True) + assert _STRATEGY.get_signal('ETH/BTC', '5m') == (False, True) mocked_history.loc[1, 'sell'] = 0 mocked_history.loc[1, 'buy'] = 1 mocker.patch.object( - _STRATEGY, '_analyze_ticker_internal', - return_value=mocked_history + _STRATEGY.dp, 'get_analyzed_dataframe', + return_value=(mocked_history, 0) ) - assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (True, False) + assert _STRATEGY.get_signal('ETH/BTC', '5m') == (True, False) mocked_history.loc[1, 'sell'] = 0 mocked_history.loc[1, 'buy'] = 0 mocker.patch.object( - _STRATEGY, '_analyze_ticker_internal', - return_value=mocked_history + _STRATEGY.dp, 'get_analyzed_dataframe', + return_value=(mocked_history, 0) ) - assert _STRATEGY.get_signal('ETH/BTC', '5m', ohlcv_history) == (False, False) + assert _STRATEGY.get_signal('ETH/BTC', '5m') == (False, False) def test_get_signal_empty(default_conf, mocker, caplog): - assert (False, False) == _STRATEGY.get_signal('foo', default_conf['timeframe'], - DataFrame()) + mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(DataFrame(), 0)) + assert (False, False) == _STRATEGY.get_signal('foo', default_conf['timeframe']) assert log_has('Empty candle (OHLCV) data for pair foo', caplog) caplog.clear() - assert (False, False) == _STRATEGY.get_signal('bar', default_conf['timeframe'], - []) + mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(None, 0)) + assert (False, False) == _STRATEGY.get_signal('bar', default_conf['timeframe']) assert log_has('Empty candle (OHLCV) data for pair bar', caplog) def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ohlcv_history): caplog.set_level(logging.INFO) + mocker.patch.object(_STRATEGY.dp, 'ohlcv', return_value=ohlcv_history) mocker.patch.object( _STRATEGY, '_analyze_ticker_internal', side_effect=ValueError('xyz') ) - assert (False, False) == _STRATEGY.get_signal('foo', default_conf['timeframe'], - ohlcv_history) + _STRATEGY.analyze_pair('foo') + assert log_has_re(r'Strategy caused the following exception: xyz.*', caplog) + caplog.clear() + + mocker.patch.object( + _STRATEGY, 'analyze_ticker', + side_effect=Exception('invalid ticker history ') + ) + _STRATEGY.analyze_pair('foo') assert log_has_re(r'Strategy caused the following exception: xyz.*', caplog) def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ohlcv_history): caplog.set_level(logging.INFO) - mocker.patch.object( - _STRATEGY, '_analyze_ticker_internal', - return_value=DataFrame([]) - ) + mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(DataFrame([]), 0)) mocker.patch.object(_STRATEGY, 'assert_df') - assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe'], - ohlcv_history) + assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe']) assert log_has('Empty dataframe for pair xyz', caplog) @@ -99,13 +105,10 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): mocked_history.loc[1, 'buy'] = 1 caplog.set_level(logging.INFO) - mocker.patch.object( - _STRATEGY, '_analyze_ticker_internal', - return_value=mocked_history - ) + mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(mocked_history, 0)) mocker.patch.object(_STRATEGY, 'assert_df') - assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe'], - ohlcv_history) + + assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe']) assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog) @@ -120,12 +123,13 @@ def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history): mocked_history.loc[1, 'buy'] = 1 caplog.set_level(logging.INFO) + mocker.patch.object(_STRATEGY.dp, 'ohlcv', return_value=ohlcv_history) + mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(mocked_history, 0)) mocker.patch.object( _STRATEGY, 'assert_df', side_effect=StrategyError('Dataframe returned...') ) - assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe'], - ohlcv_history) + _STRATEGY.analyze_pair('xyz') assert log_has('Unable to analyze candle (OHLCV) data for pair xyz: Dataframe returned...', caplog) @@ -157,15 +161,6 @@ def test_assert_df(default_conf, mocker, ohlcv_history, caplog): _STRATEGY.disable_dataframe_checks = False -def test_get_signal_handles_exceptions(mocker, default_conf): - exchange = get_patched_exchange(mocker, default_conf) - mocker.patch.object( - _STRATEGY, 'analyze_ticker', - side_effect=Exception('invalid ticker history ') - ) - assert _STRATEGY.get_signal(exchange, 'ETH/BTC', '5m') == (False, False) - - def test_ohlcvdata_to_dataframe(default_conf, testdatadir) -> None: default_conf.update({'strategy': 'DefaultStrategy'}) strategy = StrategyResolver.load_strategy(default_conf) @@ -342,6 +337,7 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> ) strategy = DefaultStrategy({}) + strategy.dp = DataProvider({}, None, None) strategy.process_only_new_candles = True ret = strategy._analyze_ticker_internal(ohlcv_history, {'pair': 'ETH/BTC'}) From 8166b37253e06b1b600e2545fd69fab4cdcb1978 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jun 2020 14:08:57 +0200 Subject: [PATCH 0118/1197] Explicitly check if dp is available --- freqtrade/data/dataprovider.py | 5 ++--- freqtrade/strategy/interface.py | 9 +++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index d93b77121..adc9ea334 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -26,7 +26,7 @@ class DataProvider: self._config = config self._exchange = exchange self._pairlists = pairlists - self.__cached_pairs: Dict[PairWithTimeframe, Tuple(DataFrame, datetime)] = {} + self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {} def _set_cached_df(self, pair: str, timeframe: str, dataframe: DataFrame) -> None: """ @@ -102,8 +102,7 @@ class DataProvider: logger.warning(f"No data found for ({pair}, {timeframe}).") return data - def get_analyzed_dataframe(self, pair: str, - timeframe: str = None) -> Tuple[DataFrame, datetime]: + def get_analyzed_dataframe(self, pair: str, timeframe: str) -> Tuple[DataFrame, datetime]: """ :param pair: pair to get the data for :param timeframe: timeframe to get data for diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index a7d467cb2..f7a918624 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -14,7 +14,7 @@ from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.exceptions import StrategyError +from freqtrade.exceptions import StrategyError, OperationalException from freqtrade.exchange import timeframe_to_minutes from freqtrade.persistence import Trade from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper @@ -276,7 +276,8 @@ class IStrategy(ABC): # Defs that only make change on new candle data. dataframe = self.analyze_ticker(dataframe, metadata) self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date'] - self.dp._set_cached_df(pair, self.timeframe, dataframe) + if self.dp: + self.dp._set_cached_df(pair, self.timeframe, dataframe) else: logger.debug("Skipping TA Analysis for already analyzed candle") dataframe['buy'] = 0 @@ -295,6 +296,8 @@ class IStrategy(ABC): The analyzed dataframe is then accessible via `dp.get_analyzed_dataframe()`. :param pair: Pair to analyze. """ + if not self.dp: + raise OperationalException("DataProvider not found.") dataframe = self.dp.ohlcv(pair, self.timeframe) if not isinstance(dataframe, DataFrame) or dataframe.empty: logger.warning('Empty candle (OHLCV) data for pair %s', pair) @@ -349,6 +352,8 @@ class IStrategy(ABC): :param dataframe: Dataframe to analyze :return: (Buy, Sell) A bool-tuple indicating buy/sell signal """ + if not self.dp: + raise OperationalException("DataProvider not found.") dataframe, _ = self.dp.get_analyzed_dataframe(pair, timeframe) From 7da955556d18e0cd8d9cebcb8dc2b64436d49e26 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 13 Jun 2020 20:04:15 +0200 Subject: [PATCH 0119/1197] Add test for empty pair case --- tests/strategy/test_interface.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index da8de947a..94437d373 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -9,12 +9,12 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.data.history import load_data -from freqtrade.exceptions import StrategyError +from freqtrade.exceptions import StrategyError, OperationalException from freqtrade.persistence import Trade from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.data.dataprovider import DataProvider -from tests.conftest import get_patched_exchange, log_has, log_has_re +from tests.conftest import log_has, log_has_re from .strats.default_strategy import DefaultStrategy @@ -55,6 +55,28 @@ def test_returns_latest_signal(mocker, default_conf, ohlcv_history): assert _STRATEGY.get_signal('ETH/BTC', '5m') == (False, False) +def test_trade_no_dataprovider(default_conf, mocker, caplog): + strategy = DefaultStrategy({}) + with pytest.raises(OperationalException, match="DataProvider not found."): + strategy.get_signal('ETH/BTC', '5m') + + with pytest.raises(OperationalException, match="DataProvider not found."): + strategy.analyze_pair('ETH/BTC') + + +def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history): + mocker.patch.object(_STRATEGY.dp, 'ohlcv', return_value=ohlcv_history) + mocker.patch.object( + _STRATEGY, '_analyze_ticker_internal', + return_value=DataFrame([]) + ) + mocker.patch.object(_STRATEGY, 'assert_df') + + _STRATEGY.analyze_pair('ETH/BTC') + + assert log_has('Empty dataframe for pair ETH/BTC', caplog) + + def test_get_signal_empty(default_conf, mocker, caplog): mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(DataFrame(), 0)) assert (False, False) == _STRATEGY.get_signal('foo', default_conf['timeframe']) From 77056a3119e5d7e7f9ab82104c09aa5013f6fd0b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 06:52:11 +0200 Subject: [PATCH 0120/1197] Add bot_loop_start callback --- freqtrade/strategy/interface.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index f7a918624..4483e1e8f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -190,6 +190,15 @@ class IStrategy(ABC): """ return False + def bot_loop_start(self, **kwargs) -> None: + """ + Called at the start of the bot iteration (one loop). + Might be used to perform pair-independent tasks + (e.g. like lock pairs with negative profit in the last hour) + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + """ + pass + def informative_pairs(self) -> ListPairsWithTimeframes: """ Define additional, informative pair/interval combinations to be cached from the exchange. From bc821c7c208f9ebede5b02b2677416f1806fed31 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 07:00:55 +0200 Subject: [PATCH 0121/1197] Add documentation for bot_loop_start --- docs/strategy-advanced.md | 27 +++++++++++++++++++ freqtrade/freqtradebot.py | 2 ++ freqtrade/strategy/interface.py | 2 +- .../subtemplates/strategy_methods_advanced.j2 | 9 +++++++ tests/strategy/test_interface.py | 11 ++++++++ 5 files changed, 50 insertions(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 69e2256a1..aba834c55 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -89,3 +89,30 @@ class Awesomestrategy(IStrategy): return True return False ``` + +## Bot loop start callback + +A simple callback which is called at the start of every bot iteration. +This can be used to perform calculations which are pair independent. + + +``` python +import requests + +class Awesomestrategy(IStrategy): + + # ... populate_* methods + + def bot_loop_start(self, **kwargs) -> None: + """ + Called at the start of the bot iteration (one loop). + Might be used to perform pair-independent tasks + (e.g. gather some remote ressource for comparison) + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + """ + if self.config['runmode'].value in ('live', 'dry_run'): + # Assign this to the class by using self.* + # can then be used by populate_* methods + self.remote_data = requests.get('https://some_remote_source.example.com') + +``` diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 3ad0d061a..a69178691 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -151,6 +151,8 @@ class FreqtradeBot: self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist), self.strategy.informative_pairs()) + strategy_safe_wrapper(self.strategy.bot_loop_start)() + self.strategy.analyze(self.active_pair_whitelist) with self._sell_lock: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 4483e1e8f..2ee567c20 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -194,7 +194,7 @@ class IStrategy(ABC): """ Called at the start of the bot iteration (one loop). Might be used to perform pair-independent tasks - (e.g. like lock pairs with negative profit in the last hour) + (e.g. gather some remote ressource for comparison) :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. """ pass diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 0ca35e117..9af086c77 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -1,4 +1,13 @@ +def bot_loop_start(self, **kwargs) -> None: + """ + Called at the start of the bot iteration (one loop). + Might be used to perform pair-independent tasks + (e.g. gather some remote ressource for comparison) + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + """ + pass + def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: """ Check buy timeout function callback. diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 94437d373..a50b4e1db 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -57,6 +57,9 @@ def test_returns_latest_signal(mocker, default_conf, ohlcv_history): def test_trade_no_dataprovider(default_conf, mocker, caplog): strategy = DefaultStrategy({}) + # Delete DP for sure (suffers from test leakage, as we update this in the base class) + if strategy.dp: + del strategy.dp with pytest.raises(OperationalException, match="DataProvider not found."): strategy.get_signal('ETH/BTC', '5m') @@ -418,6 +421,14 @@ def test_is_pair_locked(default_conf): assert not strategy.is_pair_locked(pair) +def test_is_informative_pairs_callback(default_conf): + default_conf.update({'strategy': 'TestStrategyLegacy'}) + strategy = StrategyResolver.load_strategy(default_conf) + # Should return empty + # Uses fallback to base implementation + assert [] == strategy.informative_pairs() + + @pytest.mark.parametrize('error', [ ValueError, KeyError, Exception, ]) From c047e48a47a7b06bba7525e8750fa006c58f9930 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 07:15:24 +0200 Subject: [PATCH 0122/1197] Add errorsupression to safe wrapper --- freqtrade/strategy/strategy_wrapper.py | 6 +++--- tests/strategy/test_interface.py | 5 +++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/freqtrade/strategy/strategy_wrapper.py b/freqtrade/strategy/strategy_wrapper.py index 7b9da9140..8fc548074 100644 --- a/freqtrade/strategy/strategy_wrapper.py +++ b/freqtrade/strategy/strategy_wrapper.py @@ -5,7 +5,7 @@ from freqtrade.exceptions import StrategyError logger = logging.getLogger(__name__) -def strategy_safe_wrapper(f, message: str = "", default_retval=None): +def strategy_safe_wrapper(f, message: str = "", default_retval=None, supress_error=False): """ Wrapper around user-provided methods and functions. Caches all exceptions and returns either the default_retval (if it's not None) or raises @@ -20,7 +20,7 @@ def strategy_safe_wrapper(f, message: str = "", default_retval=None): f"Strategy caused the following exception: {error}" f"{f}" ) - if default_retval is None: + if default_retval is None and not supress_error: raise StrategyError(str(error)) from error return default_retval except Exception as error: @@ -28,7 +28,7 @@ def strategy_safe_wrapper(f, message: str = "", default_retval=None): f"{message}" f"Unexpected error {error} calling {f}" ) - if default_retval is None: + if default_retval is None and not supress_error: raise StrategyError(str(error)) from error return default_retval diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index a50b4e1db..63d3b85a1 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -448,6 +448,11 @@ def test_strategy_safe_wrapper_error(caplog, error): assert isinstance(ret, bool) assert ret + caplog.clear() + # Test supressing error + ret = strategy_safe_wrapper(failing_method, message='DeadBeef', supress_error=True)() + assert log_has_re(r'DeadBeef.*', caplog) + @pytest.mark.parametrize('value', [ 1, 22, 55, True, False, {'a': 1, 'b': '112'}, From dea7e3db014f2d8b1888035185b33ac98e37152c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 07:16:56 +0200 Subject: [PATCH 0123/1197] Use supress_errors in strategy wrapper - ensure it's called once --- freqtrade/freqtradebot.py | 2 +- tests/test_freqtradebot.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a69178691..77ded8355 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -151,7 +151,7 @@ class FreqtradeBot: self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist), self.strategy.informative_pairs()) - strategy_safe_wrapper(self.strategy.bot_loop_start)() + strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)() self.strategy.analyze(self.active_pair_whitelist) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index e83ac2038..381531eba 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1964,6 +1964,18 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, freqtrade.handle_trade(trade) +def test_bot_loop_start_called_once(mocker, default_conf, caplog): + ftbot = get_patched_freqtradebot(mocker, default_conf) + patch_get_signal(ftbot) + ftbot.strategy.bot_loop_start = MagicMock(side_effect=ValueError) + ftbot.strategy.analyze = MagicMock() + + ftbot.process() + assert log_has_re(r'Strategy caused the following exception.*', caplog) + assert ftbot.strategy.bot_loop_start.call_count == 1 + assert ftbot.strategy.analyze.call_count == 1 + + def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_order_old, open_trade, fee, mocker) -> None: default_conf["unfilledtimeout"] = {"buy": 1400, "sell": 30} From 910100f1c88efa16622acc425a8ea41018482c8c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 07:27:13 +0200 Subject: [PATCH 0124/1197] Improve docstring comment --- freqtrade/strategy/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 2ee567c20..abe5f7dfb 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -354,7 +354,7 @@ class IStrategy(ABC): def get_signal(self, pair: str, timeframe: str) -> Tuple[bool, bool]: """ - Calculates current signal based several technical analysis indicators + Calculates current signal based based on the buy / sell columns of the dataframe. Used by Bot to get the latest signal :param pair: pair in format ANT/BTC :param timeframe: timeframe to use From de676bcabac2ca49d5ceea9be5c85dd794b0dbfb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 10:08:06 +0200 Subject: [PATCH 0125/1197] Document get_analyzed_dataframe for dataprovider --- docs/strategy-customization.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 08e79d307..1c4c80c47 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -366,6 +366,7 @@ Please always check the mode of operation to select the correct method to get da - [`available_pairs`](#available_pairs) - Property with tuples listing cached pairs with their intervals (pair, interval). - [`current_whitelist()`](#current_whitelist) - Returns a current list of whitelisted pairs. Useful for accessing dynamic whitelists (ie. VolumePairlist) - [`get_pair_dataframe(pair, timeframe)`](#get_pair_dataframepair-timeframe) - This is a universal method, which returns either historical data (for backtesting) or cached live data (for the Dry-Run and Live-Run modes). +- [`get_analyzed_dataframe(pair, timeframe)`](#get_analyzed_dataframepair-timeframe) - Returns the analyzed dataframe (after calling `populate_indicators()`, `populate_buy()`, `populate_sell()`) and the time of the latest analysis. - `historic_ohlcv(pair, timeframe)` - Returns historical data stored on disk. - `market(pair)` - Returns market data for the pair: fees, limits, precisions, activity flag, etc. See [ccxt documentation](https://github.com/ccxt/ccxt/wiki/Manual#markets) for more details on the Market data structure. - `ohlcv(pair, timeframe)` - Currently cached candle (OHLCV) data for the pair, returns DataFrame or empty DataFrame. @@ -431,13 +432,25 @@ if self.dp: ``` !!! Warning "Warning about backtesting" - Be carefull when using dataprovider in backtesting. `historic_ohlcv()` (and `get_pair_dataframe()` + Be careful when using dataprovider in backtesting. `historic_ohlcv()` (and `get_pair_dataframe()` for the backtesting runmode) provides the full time-range in one go, so please be aware of it and make sure to not "look into the future" to avoid surprises when running in dry/live mode). !!! Warning "Warning in hyperopt" This option cannot currently be used during hyperopt. +#### *get_analyzed_dataframe(pair, timeframe)* + +This method is used by freqtrade internally to determine the last signal. +It can also be used in specific callbacks to get the signal that caused the action (see [Advanced Strategy Documentation](strategy-advanced.md) for more details on available callbacks). + +``` python +# fetch current dataframe +if self.dp: + dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=metadata['pair'], + timeframe=self.ticker_interval) +``` + #### *orderbook(pair, maximum)* ``` python From 84329ad2ca7297491a11a0fa542fcc5ac729b0a9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 10:08:19 +0200 Subject: [PATCH 0126/1197] Add confirm_trade* methods to abort buying or selling --- docs/strategy-advanced.md | 83 ++++++++++++++++++- freqtrade/freqtradebot.py | 16 +++- freqtrade/strategy/interface.py | 48 +++++++++++ .../subtemplates/strategy_methods_advanced.j2 | 52 ++++++++++++ 4 files changed, 197 insertions(+), 2 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index aba834c55..7edb0d05d 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -3,6 +3,9 @@ This page explains some advanced concepts available for strategies. If you're just getting started, please be familiar with the methods described in the [Strategy Customization](strategy-customization.md) documentation first. +!!! Note + All callback methods described below should only be implemented in a strategy if they are also actively used. + ## Custom order timeout rules Simple, timebased order-timeouts can be configured either via strategy or in the configuration in the `unfilledtimeout` section. @@ -95,7 +98,6 @@ class Awesomestrategy(IStrategy): A simple callback which is called at the start of every bot iteration. This can be used to perform calculations which are pair independent. - ``` python import requests @@ -116,3 +118,82 @@ class Awesomestrategy(IStrategy): self.remote_data = requests.get('https://some_remote_source.example.com') ``` + +## Bot order confirmation + +### Trade entry (buy order) confirmation + +`confirm_trade_entry()` an be used to abort a trade entry at the latest second (maybe because the price is not what we expect). + +``` python +class Awesomestrategy(IStrategy): + + # ... populate_* methods + + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, + time_in_force: str, **kwargs) -> bool: + """ + Called right before placing a buy order. + Timing for this function is critical, so avoid doing heavy computations or + network reqeusts in this method. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns True (always confirming). + + :param pair: Pair that's about to be bought. + :param order_type: Order type (as configured in order_types). usually limit or market. + :param amount: Amount in target (quote) currency that's going to be traded. + :param rate: Rate that's going to be used when using limit orders + :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return bool: When True is returned, then the buy-order is placed on the exchange. + False aborts the process + """ + return True + +``` + +### Trade exit (sell order) confirmation + +`confirm_trade_exit()` an be used to abort a trade exit (sell) at the latest second (maybe because the price is not what we expect). + +``` python +from freqtrade.persistence import Trade + + +class Awesomestrategy(IStrategy): + + # ... populate_* methods + + def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, rate: float, + time_in_force: str, sell_reason: str, ** kwargs) -> bool: + """ + Called right before placing a regular sell order. + Timing for this function is critical, so avoid doing heavy computations or + network reqeusts in this method. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns True (always confirming). + + :param pair: Pair that's about to be sold. + :param order_type: Order type (as configured in order_types). usually limit or market. + :param amount: Amount in quote currency. + :param rate: Rate that's going to be used when using limit orders + :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). + :param sell_reason: Sell reason. + Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', + 'sell_signal', 'force_sell', 'emergency_sell'] + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return bool: When True is returned, then the sell-order is placed on the exchange. + False aborts the process + """ + if sell_reason == 'force_sell' and trade.calc_profit_ratio(rate) < 0: + # Reject force-sells with negative profit + # This is just a sample, please adjust to your needs + # (this does not necessarily make sense, assuming you know when you're force-selling) + return False + return True + +``` diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 77ded8355..09b794a99 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -497,6 +497,12 @@ class FreqtradeBot: amount = stake_amount / buy_limit_requested order_type = self.strategy.order_types['buy'] + if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( + pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested, + time_in_force=time_in_force): + logger.info(f"User requested abortion of buying {pair}") + return False + order = self.exchange.buy(pair=pair, ordertype=order_type, amount=amount, rate=buy_limit_requested, time_in_force=time_in_force) @@ -1077,12 +1083,20 @@ class FreqtradeBot: order_type = self.strategy.order_types.get("emergencysell", "market") amount = self._safe_sell_amount(trade.pair, trade.amount) + time_in_force = self.strategy.order_time_in_force['sell'] + + if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( + pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, + time_in_force=time_in_force, + sell_reason=sell_reason.value): + logger.info(f"User requested abortion of selling {trade.pair}") + return False # Execute sell and update trade record order = self.exchange.sell(pair=str(trade.pair), ordertype=order_type, amount=amount, rate=limit, - time_in_force=self.strategy.order_time_in_force['sell'] + time_in_force=time_in_force ) trade.open_order_id = order['id'] diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index abe5f7dfb..afa8c192e 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -199,6 +199,54 @@ class IStrategy(ABC): """ pass + def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, + time_in_force: str, **kwargs) -> bool: + """ + Called right before placing a buy order. + Timing for this function is critical, so avoid doing heavy computations or + network reqeusts in this method. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns True (always confirming). + + :param pair: Pair that's about to be bought. + :param order_type: Order type (as configured in order_types). usually limit or market. + :param amount: Amount in target (quote) currency that's going to be traded. + :param rate: Rate that's going to be used when using limit orders + :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return bool: When True is returned, then the buy-order is placed on the exchange. + False aborts the process + """ + return True + + def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, rate: float, + time_in_force: str, sell_reason: str, ** kwargs) -> bool: + """ + Called right before placing a regular sell order. + Timing for this function is critical, so avoid doing heavy computations or + network reqeusts in this method. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns True (always confirming). + + :param pair: Pair that's about to be sold. + :param trade: trade object. + :param order_type: Order type (as configured in order_types). usually limit or market. + :param amount: Amount in quote currency. + :param rate: Rate that's going to be used when using limit orders + :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). + :param sell_reason: Sell reason. + Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', + 'sell_signal', 'force_sell', 'emergency_sell'] + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return bool: When True is returned, then the sell-order is placed on the exchange. + False aborts the process + """ + return True + def informative_pairs(self) -> ListPairsWithTimeframes: """ Define additional, informative pair/interval combinations to be cached from the exchange. diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 9af086c77..782c5a475 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -4,10 +4,62 @@ def bot_loop_start(self, **kwargs) -> None: Called at the start of the bot iteration (one loop). Might be used to perform pair-independent tasks (e.g. gather some remote ressource for comparison) + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, this simply does nothing. :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. """ pass +def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, + time_in_force: str, **kwargs) -> bool: + """ + Called right before placing a buy order. + Timing for this function is critical, so avoid doing heavy computations or + network reqeusts in this method. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns True (always confirming). + + :param pair: Pair that's about to be bought. + :param order_type: Order type (as configured in order_types). usually limit or market. + :param amount: Amount in target (quote) currency that's going to be traded. + :param rate: Rate that's going to be used when using limit orders + :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return bool: When True is returned, then the buy-order is placed on the exchange. + False aborts the process + """ + return True + +def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float, rate: float, + time_in_force: str, sell_reason: str, ** kwargs) -> bool: + """ + Called right before placing a regular sell order. + Timing for this function is critical, so avoid doing heavy computations or + network reqeusts in this method. + + For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ + + When not implemented by a strategy, returns True (always confirming). + + :param pair: Pair that's about to be sold. + :param trade: trade object. + :param order_type: Order type (as configured in order_types). usually limit or market. + :param amount: Amount in quote currency. + :param rate: Rate that's going to be used when using limit orders + :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). + :param sell_reason: Sell reason. + Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss', + 'sell_signal', 'force_sell', 'emergency_sell'] + :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. + :return bool: When True is returned, then the sell-order is placed on the exchange. + False aborts the process + """ + return True + def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict, **kwargs) -> bool: """ Check buy timeout function callback. From 6d6e7196f43e97c16208c7685f51d3a39cae3206 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 10:20:23 +0200 Subject: [PATCH 0127/1197] Test trade entry / exit is called correctly --- tests/test_integration.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_integration.py b/tests/test_integration.py index 57960503e..fdc3ab1d0 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -79,10 +79,15 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, freqtrade.strategy.order_types['stoploss_on_exchange'] = True # Switch ordertype to market to close trade immediately freqtrade.strategy.order_types['sell'] = 'market' + freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) + freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True) patch_get_signal(freqtrade) # Create some test data freqtrade.enter_positions() + assert freqtrade.strategy.confirm_trade_entry.call_count == 3 + freqtrade.strategy.confirm_trade_entry.reset_mock() + assert freqtrade.strategy.confirm_trade_exit.call_count == 0 wallets_mock.reset_mock() Trade.session = MagicMock() @@ -95,6 +100,9 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, n = freqtrade.exit_positions(trades) assert n == 2 assert should_sell_mock.call_count == 2 + assert freqtrade.strategy.confirm_trade_entry.call_count == 0 + assert freqtrade.strategy.confirm_trade_exit.call_count == 1 + freqtrade.strategy.confirm_trade_exit.reset_mock() # Only order for 3rd trade needs to be cancelled assert cancel_order_mock.call_count == 1 From 7c3fb111f283286a3c7297ab6aef604a9ab4b66d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 10:23:49 +0200 Subject: [PATCH 0128/1197] Confirm execute_sell calls confirm_trade_exit --- tests/test_freqtradebot.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 381531eba..fd903fe43 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2502,22 +2502,33 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N patch_whitelist(mocker, default_conf) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) + freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=False) # Create some test data freqtrade.enter_positions() + rpc_mock.reset_mock() trade = Trade.query.first() assert trade + assert freqtrade.strategy.confirm_trade_exit.call_count == 0 # Increase the price and sell it mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker_sell_up ) + # Prevented sell ... + freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) + assert rpc_mock.call_count == 0 + assert freqtrade.strategy.confirm_trade_exit.call_count == 1 + + # Repatch with true + freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True) freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) + assert freqtrade.strategy.confirm_trade_exit.call_count == 1 - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 1 last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, From 1c1a7150ae9906f9d513c86c611bf51d70d728f8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 10:27:29 +0200 Subject: [PATCH 0129/1197] ensure confirm_trade_entry is called and has the desired effect --- tests/test_freqtradebot.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index fd903fe43..820e69bb0 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -975,6 +975,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: patch_RPCManager(mocker) patch_exchange(mocker) freqtrade = FreqtradeBot(default_conf) + freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False) stake_amount = 2 bid = 0.11 buy_rate_mock = MagicMock(return_value=bid) @@ -996,6 +997,13 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: ) pair = 'ETH/BTC' + assert not freqtrade.execute_buy(pair, stake_amount) + assert buy_rate_mock.call_count == 1 + assert buy_mm.call_count == 0 + assert freqtrade.strategy.confirm_trade_entry.call_count == 1 + buy_rate_mock.reset_mock() + + freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) assert freqtrade.execute_buy(pair, stake_amount) assert buy_rate_mock.call_count == 1 assert buy_mm.call_count == 1 @@ -1003,6 +1011,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: assert call_args['pair'] == pair assert call_args['rate'] == bid assert call_args['amount'] == stake_amount / bid + buy_rate_mock.reset_mock() # Should create an open trade with an open order id # As the order is not fulfilled yet @@ -1015,7 +1024,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: fix_price = 0.06 assert freqtrade.execute_buy(pair, stake_amount, fix_price) # Make sure get_buy_rate wasn't called again - assert buy_rate_mock.call_count == 1 + assert buy_rate_mock.call_count == 0 assert buy_mm.call_count == 2 call_args = buy_mm.call_args_list[1][1] From 8b186dbe0eb3872ba685c7a8c156a1b266c4aa92 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 10:49:15 +0200 Subject: [PATCH 0130/1197] Add additional test scenarios --- docs/strategy-advanced.md | 4 +-- freqtrade/strategy/interface.py | 4 +-- .../subtemplates/strategy_methods_advanced.j2 | 4 +-- tests/conftest.py | 1 + tests/strategy/test_interface.py | 6 ++-- tests/test_freqtradebot.py | 33 +++++++++++++++++++ 6 files changed, 43 insertions(+), 9 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 7edb0d05d..f1d8c21dc 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -135,7 +135,7 @@ class Awesomestrategy(IStrategy): """ Called right before placing a buy order. Timing for this function is critical, so avoid doing heavy computations or - network reqeusts in this method. + network requests in this method. For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ @@ -171,7 +171,7 @@ class Awesomestrategy(IStrategy): """ Called right before placing a regular sell order. Timing for this function is critical, so avoid doing heavy computations or - network reqeusts in this method. + network requests in this method. For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index afa8c192e..fab438ae6 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -204,7 +204,7 @@ class IStrategy(ABC): """ Called right before placing a buy order. Timing for this function is critical, so avoid doing heavy computations or - network reqeusts in this method. + network requests in this method. For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ @@ -226,7 +226,7 @@ class IStrategy(ABC): """ Called right before placing a regular sell order. Timing for this function is critical, so avoid doing heavy computations or - network reqeusts in this method. + network requests in this method. For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 782c5a475..28d2d1c1b 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -17,7 +17,7 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f """ Called right before placing a buy order. Timing for this function is critical, so avoid doing heavy computations or - network reqeusts in this method. + network requests in this method. For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ @@ -39,7 +39,7 @@ def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: """ Called right before placing a regular sell order. Timing for this function is critical, so avoid doing heavy computations or - network reqeusts in this method. + network requests in this method. For full documentation please go to https://www.freqtrade.io/en/latest/strategy-advanced/ diff --git a/tests/conftest.py b/tests/conftest.py index 3be7bbd22..c64f443bc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -787,6 +787,7 @@ def limit_buy_order(): 'price': 0.00001099, 'amount': 90.99181073, 'filled': 90.99181073, + 'cost': 0.0009999, 'remaining': 0.0, 'status': 'closed' } diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 63d3b85a1..176fa43ca 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -57,9 +57,9 @@ def test_returns_latest_signal(mocker, default_conf, ohlcv_history): def test_trade_no_dataprovider(default_conf, mocker, caplog): strategy = DefaultStrategy({}) - # Delete DP for sure (suffers from test leakage, as we update this in the base class) - if strategy.dp: - del strategy.dp + # Delete DP for sure (suffers from test leakage, as this is updated in the base class) + if strategy.dp is not None: + strategy.dp = None with pytest.raises(OperationalException, match="DataProvider not found."): strategy.get_signal('ETH/BTC', '5m') diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 820e69bb0..047885942 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1070,6 +1070,39 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: assert not freqtrade.execute_buy(pair, stake_amount) +def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) -> None: + freqtrade = get_patched_freqtradebot(mocker, default_conf) + mocker.patch.multiple( + 'freqtrade.freqtradebot.FreqtradeBot', + get_buy_rate=MagicMock(return_value=0.11), + _get_min_pair_stake_amount=MagicMock(return_value=1) + ) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=MagicMock(return_value={ + 'bid': 0.00001172, + 'ask': 0.00001173, + 'last': 0.00001172 + }), + buy=MagicMock(return_value=limit_buy_order), + get_fee=fee, + ) + stake_amount = 2 + pair = 'ETH/BTC' + + freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=ValueError) + assert freqtrade.execute_buy(pair, stake_amount) + + freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=Exception) + assert freqtrade.execute_buy(pair, stake_amount) + + freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) + assert freqtrade.execute_buy(pair, stake_amount) + + freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False) + assert not freqtrade.execute_buy(pair, stake_amount) + + def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None: patch_RPCManager(mocker) patch_exchange(mocker) From e5f7610b5d7b1552f6809171812b3937133ccb12 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 11:38:56 +0200 Subject: [PATCH 0131/1197] Add bot basics documentation --- docs/bot-basics.md | 58 ++++++++++++++++++++++++++++++++++ docs/hyperopt.md | 5 --- docs/strategy-advanced.md | 4 ++- docs/strategy-customization.md | 5 ++- mkdocs.yml | 1 + 5 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 docs/bot-basics.md diff --git a/docs/bot-basics.md b/docs/bot-basics.md new file mode 100644 index 000000000..728dcf46e --- /dev/null +++ b/docs/bot-basics.md @@ -0,0 +1,58 @@ +# Freqtrade basics + +This page will try to teach you some basic concepts on how freqtrade works and operates. + +## Freqtrade terminology + +* Trade: Open position +* Open Order: Order which is currently placed on the exchange, and is not yet complete. +* Pair: Tradable pair, usually in the format of Quote/Base (e.g. XRP/USDT). +* Timeframe: Candle length to use (e.g. `"5m"`, `"1h"`, ...). +* Indicators: Technical indicators (SMA, EMA, RSI, ...). +* Limit order: Limit orders which execute at the defined limit price or better. +* Market order: Guaranteed to fill, may move price depending on the order size. + +## Fee handling + +All profit calculations of Freqtrade include fees. For Backtesting / Hyperopt / Dry-run modes, the exchange default fee is used (lowest tier on the exchange). For live operations, fees are used as applied by the exchange (this includes BNB rebates etc.). + +## Bot execution logic + +Starting freqtrade in dry-run or live mode (using `freqtrade trade`) will start the bot and start the bot iteration loop. +By default, loop runs every few seconds (`internals.process_throttle_secs`) and does roughly the following in the following sequence: + +* Fetch open trades from persistence. +* Calculate current list of tradable pairs. +* Download ohlcv data for the pairlist including all [informative pairs](strategy-customization.md#get-data-for-non-tradeable-pairs) + This step is only executed once per Candle to avoid unnecessary network traffic. +* Call `bot_loop_start()` strategy callback. +* Analyze strategy per pair. + * Call `populate_indicators()` + * Call `populate_buy_trend()` + * Call `populate_sell_trend()` +* Check timeouts for open orders. + * Calls `check_buy_timeout()` strategy callback for open buy orders. + * Calls `check_sell_timeout()` strategy callback for open sell orders. +* Verifies existing positions and eventually places sell orders. + * Considers stoploss, ROI and sell-signal. + * Determine sell-price based on `ask_strategy` configuration setting. + * Before a sell order is placed, `confirm_trade_exit()` strategy callback is called. +* Check if trade-slots are still available (if `max_open_trades` is reached). +* Verifies buy signal trying to enter new positions. + * Determine buy-price based on `bid_strategy` configuration setting. + * Before a buy order is placed, `confirm_trade_entry()` strategy callback is called. + +This loop will be repeated again and again until the bot is stopped. + +## Backtesting / Hyperopt execution logic + +[backtesting](backtesting.md) or [hyperopt](hyperopt.md) do only part of the above logic, since most of the trading operations are fully simulated. + +* Load historic data for configured pairlist. +* Calculate indicators (calls `populate_indicators()`). +* Calls `populate_buy_trend()` and `populate_sell_trend()` +* Loops per candle simulating entry and exit points. +* Generate backtest report output + +!!! Note + Both Backtesting and Hyperopt include exchange default Fees in the calculation. Custom fees can be passed to backtesting / hyperopt by specifying the `--fee` argument. diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 9acb606c3..efb11e188 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -498,8 +498,3 @@ After you run Hyperopt for the desired amount of epochs, you can later list all Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same set of arguments `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. - -## Next Step - -Now you have a perfect bot and want to control it from Telegram. Your -next step is to learn the [Telegram usage](telegram-usage.md). diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index f1d8c21dc..100e96b81 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -1,7 +1,9 @@ # Advanced Strategies This page explains some advanced concepts available for strategies. -If you're just getting started, please be familiar with the methods described in the [Strategy Customization](strategy-customization.md) documentation first. +If you're just getting started, please be familiar with the methods described in the [Strategy Customization](strategy-customization.md) documentation and with the [Freqtrade basics](bot-basics.md) first. + +[Freqtrade basics](bot-basics.md) describes in which sequence each method defined below is called, which can be helpful to understand which method to use. !!! Note All callback methods described below should only be implemented in a strategy if they are also actively used. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 1c4c80c47..4a373fb0d 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -1,6 +1,8 @@ # Strategy Customization -This page explains where to customize your strategies, and add new indicators. +This page explains how to customize your strategies, and add new indicators. + +Please familiarize yourself with [Freqtrade basics](bot-basics.md) first. ## Install a custom strategy file @@ -385,6 +387,7 @@ if self.dp: ``` #### *current_whitelist()* + Imagine you've developed a strategy that trades the `5m` timeframe using signals generated from a `1d` timeframe on the top 10 volume pairs by volume. The strategy might look something like this: diff --git a/mkdocs.yml b/mkdocs.yml index ae24e150c..ebd32b3c1 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -3,6 +3,7 @@ nav: - Home: index.md - Installation Docker: docker.md - Installation: installation.md + - Freqtrade Basics: bot-basics.md - Configuration: configuration.md - Strategy Customization: strategy-customization.md - Stoploss: stoploss.md From ab9382434fb7945cd882c6905daddf0664918889 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 11:51:20 +0200 Subject: [PATCH 0132/1197] Add test for get_analyzed_dataframe --- freqtrade/data/dataprovider.py | 10 +++++----- tests/data/test_dataprovider.py | 31 +++++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index adc9ea334..113a092bc 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -5,7 +5,7 @@ including ticker and orderbook data, live and historical candle (OHLCV) data Common Interface for bot and strategy to access data. """ import logging -from datetime import datetime +from datetime import datetime, timezone from typing import Any, Dict, List, Optional, Tuple from arrow import Arrow @@ -107,14 +107,14 @@ class DataProvider: :param pair: pair to get the data for :param timeframe: timeframe to get data for :return: Tuple of (Analyzed Dataframe, lastrefreshed) for the requested pair / timeframe - combination + combination. + Returns empty dataframe and Epoch 0 (1970-01-01) if no dataframe was cached. """ - # TODO: check updated time and don't return outdated data. if (pair, timeframe) in self.__cached_pairs: return self.__cached_pairs[(pair, timeframe)] else: - # TODO: this is most likely wrong... - raise ValueError(f"No analyzed dataframe found for ({pair}, {timeframe})") + + return (DataFrame(), datetime.fromtimestamp(0, tz=timezone.utc)) def market(self, pair: str) -> Optional[Dict[str, Any]]: """ diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index def3ad535..c572cd9f3 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -1,11 +1,12 @@ +from datetime import datetime, timezone from unittest.mock import MagicMock -from pandas import DataFrame import pytest +from pandas import DataFrame from freqtrade.data.dataprovider import DataProvider -from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.state import RunMode from tests.conftest import get_patched_exchange @@ -194,3 +195,29 @@ def test_current_whitelist(mocker, default_conf, tickers): with pytest.raises(OperationalException): dp = DataProvider(default_conf, exchange) dp.current_whitelist() + + +def test_get_analyzed_dataframe(mocker, default_conf, ohlcv_history): + + default_conf["runmode"] = RunMode.DRY_RUN + + timeframe = default_conf["timeframe"] + exchange = get_patched_exchange(mocker, default_conf) + + dp = DataProvider(default_conf, exchange) + dp._set_cached_df("XRP/BTC", timeframe, ohlcv_history) + dp._set_cached_df("UNITTEST/BTC", timeframe, ohlcv_history) + + assert dp.runmode == RunMode.DRY_RUN + dataframe, time = dp.get_analyzed_dataframe("UNITTEST/BTC", timeframe) + assert ohlcv_history.equals(dataframe) + assert isinstance(time, datetime) + + dataframe, time = dp.get_analyzed_dataframe("XRP/BTC", timeframe) + assert ohlcv_history.equals(dataframe) + assert isinstance(time, datetime) + + dataframe, time = dp.get_analyzed_dataframe("NOTHING/BTC", timeframe) + assert dataframe.empty + assert isinstance(time, datetime) + assert time == datetime(1970, 1, 1, tzinfo=timezone.utc) From 8472fcfff9a6ad93e96ac8800bfe8dbb90277a52 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 14 Jun 2020 11:52:42 +0200 Subject: [PATCH 0133/1197] Add empty to documentation --- docs/strategy-customization.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 4a373fb0d..0a1049f3b 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -454,6 +454,13 @@ if self.dp: timeframe=self.ticker_interval) ``` +!!! Note "No data available" + Returns an empty dataframe if the requested pair was not cached. + This should not happen when using whitelisted pairs. + +!!! Warning "Warning in hyperopt" + This option cannot currently be used during hyperopt. + #### *orderbook(pair, maximum)* ``` python From f2a778d294a435c2b775dae1c644eef042dfe1dd Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Jun 2020 07:03:30 +0200 Subject: [PATCH 0134/1197] Combine tests for empty dataframe --- freqtrade/strategy/interface.py | 2 +- tests/strategy/test_interface.py | 14 +++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index fab438ae6..5b5cce268 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -415,7 +415,7 @@ class IStrategy(ABC): dataframe, _ = self.dp.get_analyzed_dataframe(pair, timeframe) if not isinstance(dataframe, DataFrame) or dataframe.empty: - logger.warning('Empty dataframe for pair %s', pair) + logger.warning(f'Empty candle (OHLCV) data for pair {pair}') return False, False latest_date = dataframe['date'].max() diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 176fa43ca..835465f38 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -89,6 +89,11 @@ def test_get_signal_empty(default_conf, mocker, caplog): mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(None, 0)) assert (False, False) == _STRATEGY.get_signal('bar', default_conf['timeframe']) assert log_has('Empty candle (OHLCV) data for pair bar', caplog) + caplog.clear() + + mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(DataFrame([]), 0)) + assert (False, False) == _STRATEGY.get_signal('baz', default_conf['timeframe']) + assert log_has('Empty candle (OHLCV) data for pair baz', caplog) def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ohlcv_history): @@ -110,15 +115,6 @@ def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ohlcv_his assert log_has_re(r'Strategy caused the following exception: xyz.*', caplog) -def test_get_signal_empty_dataframe(default_conf, mocker, caplog, ohlcv_history): - caplog.set_level(logging.INFO) - mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(DataFrame([]), 0)) - mocker.patch.object(_STRATEGY, 'assert_df') - - assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe']) - assert log_has('Empty dataframe for pair xyz', caplog) - - def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): # default_conf defines a 5m interval. we check interval * 2 + 5m # this is necessary as the last candle is removed (partial candles) by default From 48225e0d80669e100cb56518133a95cc8d6adad0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Jun 2020 07:05:06 +0200 Subject: [PATCH 0135/1197] Improve interface docstrings for analyze functions --- freqtrade/strategy/interface.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 5b5cce268..279453920 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -346,7 +346,7 @@ class IStrategy(ABC): return dataframe - def analyze_pair(self, pair: str): + def analyze_pair(self, pair: str) -> None: """ Fetch data for this pair from dataprovider and analyze. Stores the dataframe into the dataprovider. @@ -376,7 +376,11 @@ class IStrategy(ABC): logger.warning('Empty dataframe for pair %s', pair) return - def analyze(self, pairs: List[str]): + def analyze(self, pairs: List[str]) -> None: + """ + Analyze all pairs using analyze_pair(). + :param pairs: List of pairs to analyze + """ for pair in pairs: self.analyze_pair(pair) @@ -386,7 +390,9 @@ class IStrategy(ABC): return len(dataframe), dataframe["close"].iloc[-1], dataframe["date"].iloc[-1] def assert_df(self, dataframe: DataFrame, df_len: int, df_close: float, df_date: datetime): - """ make sure data is unmodified """ + """ + Ensure dataframe (length, last candle) was not modified, and has all elements we need. + """ message = "" if df_len != len(dataframe): message = "length" @@ -403,10 +409,9 @@ class IStrategy(ABC): def get_signal(self, pair: str, timeframe: str) -> Tuple[bool, bool]: """ Calculates current signal based based on the buy / sell columns of the dataframe. - Used by Bot to get the latest signal + Used by Bot to get the signal to buy or sell :param pair: pair in format ANT/BTC :param timeframe: timeframe to use - :param dataframe: Dataframe to analyze :return: (Buy, Sell) A bool-tuple indicating buy/sell signal """ if not self.dp: @@ -429,8 +434,7 @@ class IStrategy(ABC): if latest_date < (arrow.utcnow().shift(minutes=-(timeframe_minutes * 2 + offset))): logger.warning( 'Outdated history for pair %s. Last tick is %s minutes old', - pair, - (arrow.utcnow() - latest_date).seconds // 60 + pair, (arrow.utcnow() - latest_date).seconds // 60 ) return False, False From f1993fb2f48b986caef0372f1d6e9e0fe39f3c29 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Jun 2020 08:01:09 +0200 Subject: [PATCH 0136/1197] Pass analyzed dataframe to get_signal --- freqtrade/freqtradebot.py | 8 ++++-- freqtrade/strategy/interface.py | 8 ++---- tests/conftest.py | 2 +- tests/strategy/test_interface.py | 43 ++++++-------------------------- 4 files changed, 16 insertions(+), 45 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 09b794a99..59f4447d7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -424,7 +424,8 @@ class FreqtradeBot: return False # running get_signal on historical data fetched - (buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe) + analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, self.strategy.timeframe) + (buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df) if buy and not sell: stake_amount = self.get_trade_stake_amount(pair) @@ -705,7 +706,10 @@ class FreqtradeBot: if (config_ask_strategy.get('use_sell_signal', True) or config_ask_strategy.get('ignore_roi_if_buy_signal', False)): - (buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe) + analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, + self.strategy.timeframe) + + (buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe, analyzed_df) if config_ask_strategy.get('use_order_book', False): order_book_min = config_ask_strategy.get('order_book_min', 1) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 279453920..969259446 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -406,19 +406,15 @@ class IStrategy(ABC): else: raise StrategyError(f"Dataframe returned from strategy has mismatching {message}.") - def get_signal(self, pair: str, timeframe: str) -> Tuple[bool, bool]: + def get_signal(self, pair: str, timeframe: str, dataframe: DataFrame) -> Tuple[bool, bool]: """ Calculates current signal based based on the buy / sell columns of the dataframe. Used by Bot to get the signal to buy or sell :param pair: pair in format ANT/BTC :param timeframe: timeframe to use + :param dataframe: Analyzed dataframe to get signal from. :return: (Buy, Sell) A bool-tuple indicating buy/sell signal """ - if not self.dp: - raise OperationalException("DataProvider not found.") - - dataframe, _ = self.dp.get_analyzed_dataframe(pair, timeframe) - if not isinstance(dataframe, DataFrame) or dataframe.empty: logger.warning(f'Empty candle (OHLCV) data for pair {pair}') return False, False diff --git a/tests/conftest.py b/tests/conftest.py index c64f443bc..8e8c1bfaa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -163,7 +163,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: :param value: which value IStrategy.get_signal() must return :return: None """ - freqtrade.strategy.get_signal = lambda e, s: value + freqtrade.strategy.get_signal = lambda e, s, x: value freqtrade.exchange.refresh_latest_ohlcv = lambda p: None diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 835465f38..70ae067d7 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -31,40 +31,15 @@ def test_returns_latest_signal(mocker, default_conf, ohlcv_history): mocked_history['buy'] = 0 mocked_history.loc[1, 'sell'] = 1 - mocker.patch.object( - _STRATEGY.dp, 'get_analyzed_dataframe', - return_value=(mocked_history, 0) - ) - - assert _STRATEGY.get_signal('ETH/BTC', '5m') == (False, True) + assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, True) mocked_history.loc[1, 'sell'] = 0 mocked_history.loc[1, 'buy'] = 1 - mocker.patch.object( - _STRATEGY.dp, 'get_analyzed_dataframe', - return_value=(mocked_history, 0) - ) - assert _STRATEGY.get_signal('ETH/BTC', '5m') == (True, False) + assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False) mocked_history.loc[1, 'sell'] = 0 mocked_history.loc[1, 'buy'] = 0 - mocker.patch.object( - _STRATEGY.dp, 'get_analyzed_dataframe', - return_value=(mocked_history, 0) - ) - assert _STRATEGY.get_signal('ETH/BTC', '5m') == (False, False) - - -def test_trade_no_dataprovider(default_conf, mocker, caplog): - strategy = DefaultStrategy({}) - # Delete DP for sure (suffers from test leakage, as this is updated in the base class) - if strategy.dp is not None: - strategy.dp = None - with pytest.raises(OperationalException, match="DataProvider not found."): - strategy.get_signal('ETH/BTC', '5m') - - with pytest.raises(OperationalException, match="DataProvider not found."): - strategy.analyze_pair('ETH/BTC') + assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, False) def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history): @@ -81,18 +56,15 @@ def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history): def test_get_signal_empty(default_conf, mocker, caplog): - mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(DataFrame(), 0)) - assert (False, False) == _STRATEGY.get_signal('foo', default_conf['timeframe']) + assert (False, False) == _STRATEGY.get_signal('foo', default_conf['timeframe'], DataFrame()) assert log_has('Empty candle (OHLCV) data for pair foo', caplog) caplog.clear() - mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(None, 0)) - assert (False, False) == _STRATEGY.get_signal('bar', default_conf['timeframe']) + assert (False, False) == _STRATEGY.get_signal('bar', default_conf['timeframe'], None) assert log_has('Empty candle (OHLCV) data for pair bar', caplog) caplog.clear() - mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(DataFrame([]), 0)) - assert (False, False) == _STRATEGY.get_signal('baz', default_conf['timeframe']) + assert (False, False) == _STRATEGY.get_signal('baz', default_conf['timeframe'], DataFrame([])) assert log_has('Empty candle (OHLCV) data for pair baz', caplog) @@ -126,10 +98,9 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): mocked_history.loc[1, 'buy'] = 1 caplog.set_level(logging.INFO) - mocker.patch.object(_STRATEGY.dp, 'get_analyzed_dataframe', return_value=(mocked_history, 0)) mocker.patch.object(_STRATEGY, 'assert_df') - assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe']) + assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe'], mocked_history) assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog) From eef3c01da73008c9f9a4bb03ff5c4106cbbf68fa Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Jun 2020 19:46:03 +0200 Subject: [PATCH 0137/1197] Fix function header formatting --- docs/strategy-advanced.md | 4 ++-- freqtrade/strategy/interface.py | 4 ++-- freqtrade/templates/subtemplates/strategy_methods_advanced.j2 | 4 ++-- tests/strategy/test_interface.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 100e96b81..c576cb46e 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -168,8 +168,8 @@ class Awesomestrategy(IStrategy): # ... populate_* methods - def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, rate: float, - time_in_force: str, sell_reason: str, ** kwargs) -> bool: + def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, + rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool: """ Called right before placing a regular sell order. Timing for this function is critical, so avoid doing heavy computations or diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 969259446..f8e59ac7b 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -221,8 +221,8 @@ class IStrategy(ABC): """ return True - def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, rate: float, - time_in_force: str, sell_reason: str, ** kwargs) -> bool: + def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, + rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool: """ Called right before placing a regular sell order. Timing for this function is critical, so avoid doing heavy computations or diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 28d2d1c1b..c7ce41bb7 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -34,8 +34,8 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f """ return True -def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float, rate: float, - time_in_force: str, sell_reason: str, ** kwargs) -> bool: +def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float, + rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool: """ Called right before placing a regular sell order. Timing for this function is critical, so avoid doing heavy computations or diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 70ae067d7..381454622 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -9,7 +9,7 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.data.history import load_data -from freqtrade.exceptions import StrategyError, OperationalException +from freqtrade.exceptions import StrategyError from freqtrade.persistence import Trade from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper From f976905728c45ee21e6daccea25eb25d3ff3ec47 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 18 Jun 2020 20:00:18 +0200 Subject: [PATCH 0138/1197] Fix more exchange message typos --- freqtrade/exchange/exchange.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 48219096d..4564e671f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -526,13 +526,13 @@ class Exchange: except ccxt.InsufficientFunds as e: raise DependencyException( - f'Insufficient funds to create {ordertype} {side} order on market {pair}.' + f'Insufficient funds to create {ordertype} {side} order on market {pair}. ' f'Tried to {side} amount {amount} at rate {rate}.' f'Message: {e}') from e except ccxt.InvalidOrder as e: raise DependencyException( - f'Could not create {ordertype} {side} order on market {pair}.' - f'Tried to {side} amount {amount} at rate {rate}.' + f'Could not create {ordertype} {side} order on market {pair}. ' + f'Tried to {side} amount {amount} at rate {rate}. ' f'Message: {e}') from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( From dbf14ccf13d00be1687d4d3b45af252384bbec7f Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 09:13:36 +0000 Subject: [PATCH 0139/1197] Bump mypy from 0.780 to 0.781 Bumps [mypy](https://github.com/python/mypy) from 0.780 to 0.781. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.780...v0.781) 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 840eff15f..91b33c573 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ coveralls==2.0.0 flake8==3.8.3 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 -mypy==0.780 +mypy==0.781 pytest==5.4.3 pytest-asyncio==0.12.0 pytest-cov==2.10.0 From 993333a61cdc99afbc04aa7e96ffcb582e181776 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 09:14:25 +0000 Subject: [PATCH 0140/1197] Bump pandas from 1.0.4 to 1.0.5 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.0.4 to 1.0.5. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.0.4...v1.0.5) Signed-off-by: dependabot-preview[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2c68b8f2c..8376e41aa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ -r requirements-common.txt numpy==1.18.5 -pandas==1.0.4 +pandas==1.0.5 From 432c1b54bfc8332aab70f1a08ba491d08972b7a8 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 09:14:57 +0000 Subject: [PATCH 0141/1197] Bump arrow from 0.15.6 to 0.15.7 Bumps [arrow](https://github.com/crsmithdev/arrow) from 0.15.6 to 0.15.7. - [Release notes](https://github.com/crsmithdev/arrow/releases) - [Changelog](https://github.com/crsmithdev/arrow/blob/master/CHANGELOG.rst) - [Commits](https://github.com/crsmithdev/arrow/compare/0.15.6...0.15.7) 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 a6420d76a..767e993de 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -3,7 +3,7 @@ ccxt==1.30.2 SQLAlchemy==1.3.17 python-telegram-bot==12.7 -arrow==0.15.6 +arrow==0.15.7 cachetools==4.1.0 requests==2.23.0 urllib3==1.25.9 From dcc95d09339a104977a347499ada458f1855fba7 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 09:15:33 +0000 Subject: [PATCH 0142/1197] Bump mkdocs-material from 5.3.0 to 5.3.2 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 5.3.0 to 5.3.2. - [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/5.3.0...5.3.2) 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 666cf5ac4..7ddfc1dfb 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==5.3.0 +mkdocs-material==5.3.2 mdx_truly_sane_lists==1.2 From b29f12bfad2e526c4dd3ae2b1bc0fcebe5bc4a9f Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 09:16:00 +0000 Subject: [PATCH 0143/1197] Bump scipy from 1.4.1 to 1.5.0 Bumps [scipy](https://github.com/scipy/scipy) from 1.4.1 to 1.5.0. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.4.1...v1.5.0) 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 e1b3fef4f..aedfc0eaa 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.4.1 +scipy==1.5.0 scikit-learn==0.23.1 scikit-optimize==0.7.4 filelock==3.0.12 From 6d82e41dd1bcf966d89d4ae6d8d7104b8cb14809 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 09:17:07 +0000 Subject: [PATCH 0144/1197] Bump ccxt from 1.30.2 to 1.30.31 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.30.2 to 1.30.31. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.30.2...1.30.31) 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 a6420d76a..2e90c6cc1 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.30.2 +ccxt==1.30.31 SQLAlchemy==1.3.17 python-telegram-bot==12.7 arrow==0.15.6 From 9af1dae53e40f35beb4fc41069a655c95449f52c Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 09:48:54 +0000 Subject: [PATCH 0145/1197] Bump numpy from 1.18.5 to 1.19.0 Bumps [numpy](https://github.com/numpy/numpy) from 1.18.5 to 1.19.0. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/master/doc/HOWTO_RELEASE.rst.txt) - [Commits](https://github.com/numpy/numpy/compare/v1.18.5...v1.19.0) Signed-off-by: dependabot-preview[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8376e41aa..1e61d165f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Load common requirements -r requirements-common.txt -numpy==1.18.5 +numpy==1.19.0 pandas==1.0.5 From 1854e3053890552e3892757568c359219c6a3106 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 11:56:51 +0000 Subject: [PATCH 0146/1197] Bump requests from 2.23.0 to 2.24.0 Bumps [requests](https://github.com/psf/requests) from 2.23.0 to 2.24.0. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/master/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.23.0...v2.24.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 767e993de..3519eda69 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -5,7 +5,7 @@ SQLAlchemy==1.3.17 python-telegram-bot==12.7 arrow==0.15.7 cachetools==4.1.0 -requests==2.23.0 +requests==2.24.0 urllib3==1.25.9 wrapt==1.12.1 jsonschema==3.2.0 From f2807143c65c7bb3e9f46566cb1e3d2f43952a22 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 22 Jun 2020 12:10:52 +0000 Subject: [PATCH 0147/1197] Bump ccxt from 1.30.2 to 1.30.34 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.30.2 to 1.30.34. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.30.2...1.30.34) 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 767e993de..1c981aee3 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.30.2 +ccxt==1.30.34 SQLAlchemy==1.3.17 python-telegram-bot==12.7 arrow==0.15.7 From 0509b9a8fce4eacfa55e01a876b909958ffcf4c3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Jun 2020 06:43:19 +0200 Subject: [PATCH 0148/1197] Show winning vs. losing trades --- freqtrade/rpc/rpc.py | 8 ++++++++ freqtrade/rpc/telegram.py | 4 +++- tests/rpc/test_rpc_apiserver.py | 2 ++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index aeaf82662..4cb432aea 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -269,6 +269,8 @@ class RPC: profit_closed_coin = [] profit_closed_ratio = [] durations = [] + winning_trades = 0 + losing_trades = 0 for trade in trades: current_rate: float = 0.0 @@ -282,6 +284,10 @@ class RPC: profit_ratio = trade.close_profit profit_closed_coin.append(trade.close_profit_abs) profit_closed_ratio.append(profit_ratio) + if trade.close_profit > 0: + winning_trades += 1 + else: + losing_trades += 1 else: # Get current rate try: @@ -344,6 +350,8 @@ class RPC: 'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0], 'best_pair': best_pair[0] if best_pair else '', 'best_rate': round(best_pair[1] * 100, 2) if best_pair else 0, + 'winning_trades': winning_trades, + 'losing_trades': losing_trades, } def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 9b40ee2f6..13cc1afaf 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -366,7 +366,9 @@ class Telegram(RPC): f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n" f"*Total Trade Count:* `{trade_count}`\n" f"*First Trade opened:* `{first_trade_date}`\n" - f"*Latest Trade opened:* `{latest_trade_date}`") + f"*Latest Trade opened:* `{latest_trade_date}\n`" + f"*Win / Loss:* `{stats['winning_trades']} / {stats['losing_trades']}`" + ) if stats['closed_trade_count'] > 0: markdown_msg += (f"\n*Avg. Duration:* `{avg_duration}`\n" f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`") diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 8e73eacf8..77a3e5c30 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -444,6 +444,8 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li 'profit_closed_percent_sum': 6.2, 'trade_count': 1, 'closed_trade_count': 1, + 'winning_trades': 1, + 'losing_trades': 0, } From 3624aec059f0bfc5f8aa587c18fcc97cf7a5cd8b Mon Sep 17 00:00:00 2001 From: gambcl Date: Wed, 24 Jun 2020 15:21:28 +0100 Subject: [PATCH 0149/1197] Typos --- freqtrade/pairlist/IPairList.py | 2 +- freqtrade/pairlist/PrecisionFilter.py | 2 +- freqtrade/pairlist/PriceFilter.py | 2 +- freqtrade/pairlist/ShuffleFilter.py | 2 +- freqtrade/pairlist/SpreadFilter.py | 2 +- freqtrade/pairlist/StaticPairList.py | 2 +- freqtrade/pairlist/VolumePairList.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index fd25e0766..1cca00eba 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -68,7 +68,7 @@ class IPairList(ABC): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requries tickers, an empty List is passed + If no Pairlist requires tickers, an empty List is passed as tickers argument to filter_pairlist """ diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py index 0331347be..120f7c4e0 100644 --- a/freqtrade/pairlist/PrecisionFilter.py +++ b/freqtrade/pairlist/PrecisionFilter.py @@ -27,7 +27,7 @@ class PrecisionFilter(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requries tickers, an empty List is passed + If no Pairlist requires tickers, an empty List is passed as tickers argument to filter_pairlist """ return True diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/pairlist/PriceFilter.py index b85d68269..29dd88a76 100644 --- a/freqtrade/pairlist/PriceFilter.py +++ b/freqtrade/pairlist/PriceFilter.py @@ -24,7 +24,7 @@ class PriceFilter(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requries tickers, an empty List is passed + If no Pairlist requires tickers, an empty List is passed as tickers argument to filter_pairlist """ return True diff --git a/freqtrade/pairlist/ShuffleFilter.py b/freqtrade/pairlist/ShuffleFilter.py index ba3792213..eb4f6dcc3 100644 --- a/freqtrade/pairlist/ShuffleFilter.py +++ b/freqtrade/pairlist/ShuffleFilter.py @@ -25,7 +25,7 @@ class ShuffleFilter(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requries tickers, an empty List is passed + If no Pairlist requires tickers, an empty List is passed as tickers argument to filter_pairlist """ return False diff --git a/freqtrade/pairlist/SpreadFilter.py b/freqtrade/pairlist/SpreadFilter.py index 0147c0068..2527a3131 100644 --- a/freqtrade/pairlist/SpreadFilter.py +++ b/freqtrade/pairlist/SpreadFilter.py @@ -24,7 +24,7 @@ class SpreadFilter(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requries tickers, an empty List is passed + If no Pairlist requires tickers, an empty List is passed as tickers argument to filter_pairlist """ return True diff --git a/freqtrade/pairlist/StaticPairList.py b/freqtrade/pairlist/StaticPairList.py index b5c1bc767..aa6268ba3 100644 --- a/freqtrade/pairlist/StaticPairList.py +++ b/freqtrade/pairlist/StaticPairList.py @@ -28,7 +28,7 @@ class StaticPairList(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requries tickers, an empty List is passed + If no Pairlist requires tickers, an empty List is passed as tickers argument to filter_pairlist """ return False diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index d32be3dc9..35dce93eb 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -54,7 +54,7 @@ class VolumePairList(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requries tickers, an empty List is passed + If no Pairlist requires tickers, an empty List is passed as tickers argument to filter_pairlist """ return True From 676006b99c0610fcbb975325b4544597bc0fcaa7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Jun 2020 17:40:23 +0200 Subject: [PATCH 0150/1197] --dl-trades should also support increasing download span (by downloading the whole dataset again to avoid missing data in the middle). --- freqtrade/data/history/history_utils.py | 5 +++++ tests/conftest.py | 2 +- tests/data/test_history.py | 22 ++++++++++++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 4f3f75a87..58bd752ea 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -270,6 +270,11 @@ def _download_trades_history(exchange: Exchange, # DEFAULT_TRADES_COLUMNS: 0 -> timestamp # DEFAULT_TRADES_COLUMNS: 1 -> id + if trades and since < trades[0][0]: + # since is before the first trade + logger.info(f"Start earlier than available data. Redownloading trades for {pair}...") + trades = [] + from_id = trades[-1][1] if trades else None if trades and since < trades[-1][0]: # Reset since to the last available point diff --git a/tests/conftest.py b/tests/conftest.py index a4106c767..f2143e60e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1424,7 +1424,7 @@ def trades_for_order(): @pytest.fixture(scope="function") def trades_history(): - return [[1565798399463, '126181329', None, 'buy', 0.019627, 0.04, 0.00078508], + return [[1565798389463, '126181329', None, 'buy', 0.019627, 0.04, 0.00078508], [1565798399629, '126181330', None, 'buy', 0.019627, 0.244, 0.004788987999999999], [1565798399752, '126181331', None, 'sell', 0.019626, 0.011, 0.00021588599999999999], [1565798399862, '126181332', None, 'sell', 0.019626, 0.011, 0.00021588599999999999], diff --git a/tests/data/test_history.py b/tests/data/test_history.py index c52163bbc..c2eb2d715 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -557,6 +557,7 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad assert ght_mock.call_count == 1 # Check this in seconds - since we had to convert to seconds above too. assert int(ght_mock.call_args_list[0][1]['since'] // 1000) == since_time2 - 5 + assert ght_mock.call_args_list[0][1]['from_id'] is not None # clean files freshly downloaded _clean_test_file(file1) @@ -568,6 +569,27 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad pair='ETH/BTC') assert log_has_re('Failed to download historic trades for pair: "ETH/BTC".*', caplog) + file2 = testdatadir / 'XRP_ETH-trades.json.gz' + + _backup_file(file2, True) + + ght_mock.reset_mock() + mocker.patch('freqtrade.exchange.Exchange.get_historic_trades', + ght_mock) + # Since before first start date + since_time = int(trades_history[0][0] // 1000) - 500 + timerange = TimeRange('date', None, since_time, 0) + + assert _download_trades_history(data_handler=data_handler, exchange=exchange, + pair='XRP/ETH', timerange=timerange) + + assert ght_mock.call_count == 1 + + assert int(ght_mock.call_args_list[0][1]['since'] // 1000) == since_time + assert ght_mock.call_args_list[0][1]['from_id'] is None + assert log_has_re(r'Start earlier than available data. Redownloading trades for.*', caplog) + _clean_test_file(file2) + def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog): From b77a105778842ee012a122ac5f9a8f218fbc3716 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Jun 2020 20:32:19 +0200 Subject: [PATCH 0151/1197] Add CORS_origins key to configuration --- config.json.example | 1 + config_binance.json.example | 1 + config_full.json.example | 1 + config_kraken.json.example | 1 + docs/rest-api.md | 24 ++++++++++++++++++++++++ freqtrade/constants.py | 2 ++ freqtrade/rpc/api_server.py | 4 +++- freqtrade/templates/base_config.json.j2 | 1 + 8 files changed, 34 insertions(+), 1 deletion(-) diff --git a/config.json.example b/config.json.example index 9e3daa2b5..77a147d0c 100644 --- a/config.json.example +++ b/config.json.example @@ -82,6 +82,7 @@ "listen_port": 8080, "verbosity": "info", "jwt_secret_key": "somethingrandom", + "CORS_origins": [], "username": "", "password": "" }, diff --git a/config_binance.json.example b/config_binance.json.example index b45e69bba..82943749d 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -87,6 +87,7 @@ "listen_port": 8080, "verbosity": "info", "jwt_secret_key": "somethingrandom", + "CORS_origins": [], "username": "", "password": "" }, diff --git a/config_full.json.example b/config_full.json.example index 1fd1b44a5..3a8667da4 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -123,6 +123,7 @@ "listen_port": 8080, "verbosity": "info", "jwt_secret_key": "somethingrandom", + "CORS_origins": [], "username": "freqtrader", "password": "SuperSecurePassword" }, diff --git a/config_kraken.json.example b/config_kraken.json.example index 7e4001ff3..fb983a4a3 100644 --- a/config_kraken.json.example +++ b/config_kraken.json.example @@ -93,6 +93,7 @@ "listen_port": 8080, "verbosity": "info", "jwt_secret_key": "somethingrandom", + "CORS_origins": [], "username": "", "password": "" }, diff --git a/docs/rest-api.md b/docs/rest-api.md index 33f62f884..630c952b4 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -13,6 +13,7 @@ Sample configuration: "listen_port": 8080, "verbosity": "info", "jwt_secret_key": "somethingrandom", + "CORS_origins": [], "username": "Freqtrader", "password": "SuperSecret1!" }, @@ -232,3 +233,26 @@ Since the access token has a short timeout (15 min) - the `token/refresh` reques > curl -X POST --header "Authorization: Bearer ${refresh_token}"http://localhost:8080/api/v1/token/refresh {"access_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1ODkxMTk5NzQsIm5iZiI6MTU4OTExOTk3NCwianRpIjoiMDBjNTlhMWUtMjBmYS00ZTk0LTliZjAtNWQwNTg2MTdiZDIyIiwiZXhwIjoxNTg5MTIwODc0LCJpZGVudGl0eSI6eyJ1IjoiRnJlcXRyYWRlciJ9LCJmcmVzaCI6ZmFsc2UsInR5cGUiOiJhY2Nlc3MifQ.1seHlII3WprjjclY6DpRhen0rqdF4j6jbvxIhUFaSbs"} ``` + +## CORS + +All web-based frontends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing. +Since most request to the Freqtrade API must be authenticated, a proper CORS policy is key to avoid security problems. +Also, the Standard disallows `*` CORS policies for requests with credentials, so this setting must be done appropriately. + +Users can configure this themselfs via the `CORS_origins` configuration setting. +It consists of a list of allowed sites that are allowed to consume resources from the bot's API. + +Assuming your Application would be deployed as `https://frequi.freqtrade.io/home/` - this would mean that the following configuration becomes necessary: + +```jsonc +{ + //... + "jwt_secret_key": "somethingrandom", + "CORS_origins": ["https://frequi.freqtrade.io"], + //... +} +``` + +!!! Note + We strongly recommend to also set `jwt_secret_key` to something random and known only to yourself to avoid unauthorized access to your bot. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 6741e0605..1f8944ed9 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -221,6 +221,8 @@ CONF_SCHEMA = { }, 'username': {'type': 'string'}, 'password': {'type': 'string'}, + 'jwt_secret_key': {'type': 'string'}, + 'CORS_origins': {'type': 'array', 'items': {'type': 'string'}}, 'verbosity': {'type': 'string', 'enum': ['error', 'info']}, }, 'required': ['enabled', 'listen_ip_address', 'listen_port', 'username', 'password'] diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index f424bea92..a2cef9a98 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -90,7 +90,9 @@ class ApiServer(RPC): self._config = freqtrade.config self.app = Flask(__name__) self._cors = CORS(self.app, - resources={r"/api/*": {"supports_credentials": True, }} + resources={r"/api/*": { + "supports_credentials": True, + "origins": self._config['api_server'].get('CORS_origins', [])}} ) # Setup the Flask-JWT-Extended extension diff --git a/freqtrade/templates/base_config.json.j2 b/freqtrade/templates/base_config.json.j2 index 118ae348b..b362690f9 100644 --- a/freqtrade/templates/base_config.json.j2 +++ b/freqtrade/templates/base_config.json.j2 @@ -59,6 +59,7 @@ "listen_port": 8080, "verbosity": "info", "jwt_secret_key": "somethingrandom", + "CORS_origins": [], "username": "", "password": "" }, From 5423d8588ecb6bf1c9094f9ca89b19585fe3ddaf Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 24 Jun 2020 20:32:35 +0200 Subject: [PATCH 0152/1197] Test for cors settings --- tests/rpc/test_rpc_apiserver.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 8e73eacf8..0acb31282 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -24,6 +24,7 @@ def botclient(default_conf, mocker): default_conf.update({"api_server": {"enabled": True, "listen_ip_address": "127.0.0.1", "listen_port": 8080, + "CORS_origins": ['http://example.com'], "username": _TEST_USER, "password": _TEST_PASS, }}) @@ -40,13 +41,13 @@ def client_post(client, url, data={}): content_type="application/json", data=data, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS), - 'Origin': 'example.com'}) + 'Origin': 'http://example.com'}) def client_get(client, url): # Add fake Origin to ensure CORS kicks in return client.get(url, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS), - 'Origin': 'example.com'}) + 'Origin': 'http://example.com'}) def assert_response(response, expected_code=200, needs_cors=True): @@ -54,6 +55,7 @@ def assert_response(response, expected_code=200, needs_cors=True): assert response.content_type == "application/json" if needs_cors: assert ('Access-Control-Allow-Credentials', 'true') in response.headers._list + assert ('Access-Control-Allow-Origin', 'http://example.com') in response.headers._list def test_api_not_found(botclient): @@ -110,7 +112,7 @@ def test_api_token_login(botclient): rc = client.get(f"{BASE_URI}/count", content_type="application/json", headers={'Authorization': f'Bearer {rc.json["access_token"]}', - 'Origin': 'example.com'}) + 'Origin': 'http://example.com'}) assert_response(rc) @@ -122,7 +124,7 @@ def test_api_token_refresh(botclient): content_type="application/json", data=None, headers={'Authorization': f'Bearer {rc.json["refresh_token"]}', - 'Origin': 'example.com'}) + 'Origin': 'http://example.com'}) assert_response(rc) assert 'access_token' in rc.json assert 'refresh_token' not in rc.json From ab7f5a2bcf41e3e0c988b5e66d8f02d6ed39a4b4 Mon Sep 17 00:00:00 2001 From: gambcl Date: Wed, 24 Jun 2020 23:58:12 +0100 Subject: [PATCH 0153/1197] Added pairslist AgeFilter --- config_full.json.example | 1 + docs/configuration.md | 14 +++++- freqtrade/constants.py | 3 +- freqtrade/pairlist/AgeFilter.py | 76 +++++++++++++++++++++++++++++++++ tests/pairlist/test_pairlist.py | 76 +++++++++++++++++++++++++++++++-- 5 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 freqtrade/pairlist/AgeFilter.py diff --git a/config_full.json.example b/config_full.json.example index 1fd1b44a5..5b8fa256b 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -64,6 +64,7 @@ "sort_key": "quoteVolume", "refresh_period": 1800 }, + {"method": "AgeFilter", "min_days_listed": 10}, {"method": "PrecisionFilter"}, {"method": "PriceFilter", "low_price_ratio": 0.01}, {"method": "SpreadFilter", "max_spread_ratio": 0.005} diff --git a/docs/configuration.md b/docs/configuration.md index 8438d55da..e7a79361a 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -592,7 +592,7 @@ Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) Pairlist Handler). -Additionaly, [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter) and [`SpreadFilter`](#spreadfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist. +Additionaly, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter) and [`SpreadFilter`](#spreadfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist. If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You should always configure either `StaticPairList` or `VolumePairList` as the starting Pairlist Handler. @@ -602,6 +602,7 @@ Inactive markets are always removed from the resulting pairlist. Explicitly blac * [`StaticPairList`](#static-pair-list) (default, if not configured differently) * [`VolumePairList`](#volume-pair-list) +* [`AgeFilter`](#agefilter) * [`PrecisionFilter`](#precisionfilter) * [`PriceFilter`](#pricefilter) * [`ShuffleFilter`](#shufflefilter) @@ -645,6 +646,16 @@ The `refresh_period` setting allows to define the period (in seconds), at which }], ``` +#### AgeFilter + +Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`). + +When pairs are first listed on an exchange they can suffer huge price drops and volatility +in the first few days while the pair goes through its price-discovery period. Bots can often +be caught out buying before the pair has finished dropping in price. + +This filter allows freqtrade to ignore pairs until they have been listed for at least `min_days_listed` days. + #### PrecisionFilter Filters low-value coins which would not allow setting stoplosses. @@ -692,6 +703,7 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, "number_assets": 20, "sort_key": "quoteVolume", }, + {"method": "AgeFilter", "min_days_listed": 10}, {"method": "PrecisionFilter"}, {"method": "PriceFilter", "low_price_ratio": 0.01}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}, diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 6741e0605..92824f4c4 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -22,7 +22,8 @@ ORDERBOOK_SIDES = ['ask', 'bid'] ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', - 'PrecisionFilter', 'PriceFilter', 'ShuffleFilter', 'SpreadFilter'] + 'AgeFilter', 'PrecisionFilter', 'PriceFilter', + 'ShuffleFilter', 'SpreadFilter'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz'] DRY_RUN_WALLET = 1000 MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py new file mode 100644 index 000000000..a23682599 --- /dev/null +++ b/freqtrade/pairlist/AgeFilter.py @@ -0,0 +1,76 @@ +""" +Minimum age (days listed) pair list filter +""" +import logging +import arrow +from typing import Any, Dict + +from freqtrade.misc import plural +from freqtrade.pairlist.IPairList import IPairList + + +logger = logging.getLogger(__name__) + + +class AgeFilter(IPairList): + + # Checked symbols cache (dictionary of ticker symbol => timestamp) + _symbolsChecked: Dict[str, int] = {} + + def __init__(self, exchange, pairlistmanager, + config: Dict[str, Any], pairlistconfig: Dict[str, Any], + pairlist_pos: int) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + + self._min_days_listed = pairlistconfig.get('min_days_listed', 10) + self._enabled = self._min_days_listed >= 1 + + @property + def needstickers(self) -> bool: + """ + Boolean property defining if tickers are necessary. + If no Pairlist requires tickers, an empty List is passed + as tickers argument to filter_pairlist + """ + return True + + def short_desc(self) -> str: + """ + Short whitelist method description - used for startup-messages + """ + return (f"{self.name} - Filtering pairs with age less than " + f"{self._min_days_listed} {plural(self._min_days_listed, 'day')}.") + + def _validate_pair(self, ticker: dict) -> bool: + """ + Validate age for the ticker + :param ticker: ticker dict as returned from ccxt.load_markets() + :return: True if the pair can stay, False if it should be removed + """ + + # Check symbol in cache + if ticker['symbol'] in self._symbolsChecked: + return True + + since_ms = int(arrow.utcnow() + .floor('day') + .shift(days=-self._min_days_listed) + .float_timestamp) * 1000 + + daily_candles = self._exchange.get_historic_ohlcv(pair=ticker['symbol'], + timeframe='1d', + since_ms=since_ms) + + if daily_candles is not None: + if len(daily_candles) > self._min_days_listed: + # We have fetched at least the minimum required number of daily candles + # Add to cache, store the time we last checked this symbol + self._symbolsChecked[ticker['symbol']] = int(arrow.utcnow().float_timestamp) * 1000 + return True + else: + self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " + f"because age is less than " + f"{self._min_days_listed} " + f"{plural(self._min_days_listed, 'day')}") + return False + return False diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index c67f7ae1c..87ecced21 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -57,6 +57,31 @@ def whitelist_conf_2(default_conf): return default_conf +@pytest.fixture(scope="function") +def whitelist_conf_3(default_conf): + default_conf['stake_currency'] = 'BTC' + default_conf['exchange']['pair_whitelist'] = [ + 'ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC', + 'BTT/BTC', 'HOT/BTC', 'FUEL/BTC', 'XRP/BTC' + ] + default_conf['exchange']['pair_blacklist'] = [ + 'BLK/BTC' + ] + default_conf['pairlists'] = [ + { + "method": "VolumePairList", + "number_assets": 5, + "sort_key": "quoteVolume", + "refresh_period": 0, + }, + { + "method": "AgeFilter", + "min_days_listed": 2 + } + ] + return default_conf + + @pytest.fixture(scope="function") def static_pl_conf(whitelist_conf): whitelist_conf['pairlists'] = [ @@ -220,11 +245,20 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # No pair for ETH, all handlers ([{"method": "StaticPairList"}, {"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "AgeFilter", "min_days_listed": 2}, {"method": "PrecisionFilter"}, {"method": "PriceFilter", "low_price_ratio": 0.03}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}, {"method": "ShuffleFilter"}], "ETH", []), + # AgeFilter and VolumePairList (require 2 days only, all should pass age test) + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "AgeFilter", "min_days_listed": 2}], + "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC']), + # AgeFilter and VolumePairList (require 10 days, all should fail age test) + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "AgeFilter", "min_days_listed": 10}], + "BTC", []), # Precisionfilter and quote volume ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "PrecisionFilter"}], @@ -272,7 +306,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # ShuffleFilter, no seed ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "ShuffleFilter"}], - "USDT", 3), # whitelist_result is integer -- check only lenght of randomized pairlist + "USDT", 3), # whitelist_result is integer -- check only length of randomized pairlist + # AgeFilter only + ([{"method": "AgeFilter", "min_days_listed": 2}], + "BTC", 'filter_at_the_beginning'), # OperationalException expected # PrecisionFilter after StaticPairList ([{"method": "StaticPairList"}, {"method": "PrecisionFilter"}], @@ -307,8 +344,8 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): "BTC", 'static_in_the_middle'), ]) def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, - pairlists, base_currency, whitelist_result, - caplog) -> None: + ohlcv_history_list, pairlists, base_currency, + whitelist_result, caplog) -> None: whitelist_conf['pairlists'] = pairlists whitelist_conf['stake_currency'] = base_currency @@ -324,8 +361,12 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) mocker.patch.multiple('freqtrade.exchange.Exchange', get_tickers=tickers, - markets=PropertyMock(return_value=shitcoinmarkets), + markets=PropertyMock(return_value=shitcoinmarkets) ) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), + ) # Set whitelist_result to None if pairlist is invalid and should produce exception if whitelist_result == 'filter_at_the_beginning': @@ -346,6 +387,10 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t len(whitelist) == whitelist_result for pairlist in pairlists: + if pairlist['method'] == 'AgeFilter' and pairlist['min_days_listed'] and \ + len(ohlcv_history_list) <= pairlist['min_days_listed']: + assert log_has_re(r'^Removed .* from whitelist, because age is less than ' + r'.* day.*', caplog) if pairlist['method'] == 'PrecisionFilter' and whitelist_result: assert log_has_re(r'^Removed .* from whitelist, because stop price .* ' r'would be <= stop limit.*', caplog) @@ -468,6 +513,29 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers): assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == lrf +def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_history_list): + + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers + ) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), + ) + + freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_3) + assert freqtrade.exchange.get_historic_ohlcv.call_count == 0 + freqtrade.pairlists.refresh_pairlist() + assert freqtrade.exchange.get_historic_ohlcv.call_count > 0 + + previous_call_count = freqtrade.exchange.get_historic_ohlcv.call_count + freqtrade.pairlists.refresh_pairlist() + # Should not have increased since first call. + assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count + + def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog): mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) From 6734269bfcb253171a84a51efeb212b18b7cbff1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Jun 2020 19:22:50 +0200 Subject: [PATCH 0154/1197] Use >= to compare for winning trades --- freqtrade/rpc/rpc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 4cb432aea..d6f840aa9 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -284,7 +284,7 @@ class RPC: profit_ratio = trade.close_profit profit_closed_coin.append(trade.close_profit_abs) profit_closed_ratio.append(profit_ratio) - if trade.close_profit > 0: + if trade.close_profit >= 0: winning_trades += 1 else: losing_trades += 1 From da8e87660e35e50f9d955dce703cd4cdfe9d50ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Jun 2020 06:39:47 +0200 Subject: [PATCH 0155/1197] Add missing data fillup to FAQ --- docs/faq.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/faq.md b/docs/faq.md index 31c49171d..7e9551051 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -49,6 +49,16 @@ You can use the `/forcesell all` command from Telegram. Please look at the [advanced setup documentation Page](advanced-setup.md#running-multiple-instances-of-freqtrade). +### I'm getting "Missing data fillup" messages in the log + +This message is just a warning that the latest candles had missing candles in them. +Depending on the exchange, this can indicate that the pair didn't have a trade for `` - and the exchange does only return candles with volume. +On low volume pairs, this is a rather common occurance. + +If this happens for all pairs in the pairlist, this might indicate a recent exchange downtime. Please check your exchange's public channels for details. + +Independently of the reason, Freqtrade will fill up these candles with "empty" candles, where open, high, low and close are set to the previous candle close - and volume is empty. In a chart, this will look like a `_` - and is aligned with how exchanges usually represent 0 volume candles. + ### I'm getting the "RESTRICTED_MARKET" message in the log Currently known to happen for US Bittrex users. From 185fab7b57fc7e47e05e06022a2df1ef32f688aa Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Jun 2020 15:26:55 +0200 Subject: [PATCH 0156/1197] Change some wordings in documentation Co-authored-by: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> --- docs/rest-api.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 630c952b4..a8d902b53 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -237,13 +237,13 @@ Since the access token has a short timeout (15 min) - the `token/refresh` reques ## CORS All web-based frontends are subject to [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) - Cross-Origin Resource Sharing. -Since most request to the Freqtrade API must be authenticated, a proper CORS policy is key to avoid security problems. -Also, the Standard disallows `*` CORS policies for requests with credentials, so this setting must be done appropriately. +Since most of the requests to the Freqtrade API must be authenticated, a proper CORS policy is key to avoid security problems. +Also, the standard disallows `*` CORS policies for requests with credentials, so this setting must be set appropriately. -Users can configure this themselfs via the `CORS_origins` configuration setting. +Users can configure this themselves via the `CORS_origins` configuration setting. It consists of a list of allowed sites that are allowed to consume resources from the bot's API. -Assuming your Application would be deployed as `https://frequi.freqtrade.io/home/` - this would mean that the following configuration becomes necessary: +Assuming your application is deployed as `https://frequi.freqtrade.io/home/` - this would mean that the following configuration becomes necessary: ```jsonc { From e11d22a6a2d997da487a298d87b6de7927f33a10 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Jun 2020 15:31:37 +0200 Subject: [PATCH 0157/1197] Apply suggestions from code review Co-authored-by: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> --- docs/faq.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 7e9551051..151b2c054 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -52,12 +52,12 @@ Please look at the [advanced setup documentation Page](advanced-setup.md#running ### I'm getting "Missing data fillup" messages in the log This message is just a warning that the latest candles had missing candles in them. -Depending on the exchange, this can indicate that the pair didn't have a trade for `` - and the exchange does only return candles with volume. +Depending on the exchange, this can indicate that the pair didn't have a trade for the timeframe you are using - and the exchange does only return candles with volume. On low volume pairs, this is a rather common occurance. If this happens for all pairs in the pairlist, this might indicate a recent exchange downtime. Please check your exchange's public channels for details. -Independently of the reason, Freqtrade will fill up these candles with "empty" candles, where open, high, low and close are set to the previous candle close - and volume is empty. In a chart, this will look like a `_` - and is aligned with how exchanges usually represent 0 volume candles. +Irrespectively of the reason, Freqtrade will fill up these candles with "empty" candles, where open, high, low and close are set to the previous candle close - and volume is empty. In a chart, this will look like a `_` - and is aligned with how exchanges usually represent 0 volume candles. ### I'm getting the "RESTRICTED_MARKET" message in the log From e813573f270eb8e5579b063d8426954cd87c1451 Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Sat, 27 Jun 2020 18:35:46 +0200 Subject: [PATCH 0158/1197] Warning message for open trades when stopping bot --- freqtrade/freqtradebot.py | 13 +++++++++++++ freqtrade/worker.py | 3 +++ 2 files changed, 16 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 289850709..2a1a1492c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -175,6 +175,19 @@ class FreqtradeBot: if self.config['cancel_open_orders_on_exit']: self.cancel_all_open_orders() + def check_for_open_trades(self): + open_trades = Trade.get_trades([Trade.is_open == True, + ]).all() + + if len(open_trades) is not 0: + msg = { + f'type': RPCMessageType.WARNING_NOTIFICATION, + f'status': f'{len(open_trades)} OPEN TRADES ACTIVE\n\n' + f'Handle these trades manually or \'/start\' the bot again ' + f'and use \'/stopbuy\' to handle open trades gracefully.' + } + self.rpc.send_msg(msg) + def _refresh_active_whitelist(self, trades: List[Trade] = []) -> List[str]: """ Refresh active whitelist from pairlist or edge and extend it with diff --git a/freqtrade/worker.py b/freqtrade/worker.py index 5bdb166c2..2fc206bd5 100755 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -90,6 +90,9 @@ class Worker: if state == State.RUNNING: self.freqtrade.startup() + if state == State.STOPPED: + self.freqtrade.check_for_open_trades() + # Reset heartbeat timestamp to log the heartbeat message at # first throttling iteration when the state changes self._heartbeat_msg = 0 From 0642ab76bf0f69180a498a3b48e7b5ca25a4b26e Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Sat, 27 Jun 2020 18:40:44 +0200 Subject: [PATCH 0159/1197] Added information to the new function --- freqtrade/freqtradebot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2a1a1492c..060250b49 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -176,6 +176,10 @@ class FreqtradeBot: self.cancel_all_open_orders() def check_for_open_trades(self): + """ + Notify the user when he stops the bot + and there are still open trades active. + """ open_trades = Trade.get_trades([Trade.is_open == True, ]).all() From 48289e8ca78236f6e2b9dc7eeb67409117f3d0eb Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Sat, 27 Jun 2020 20:24:50 +0200 Subject: [PATCH 0160/1197] Added exchange name, removed capital letters --- freqtrade/freqtradebot.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 060250b49..e3bb5b70b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -177,7 +177,7 @@ class FreqtradeBot: def check_for_open_trades(self): """ - Notify the user when he stops the bot + Notify the user when the bot is stopped and there are still open trades active. """ open_trades = Trade.get_trades([Trade.is_open == True, @@ -186,9 +186,10 @@ class FreqtradeBot: if len(open_trades) is not 0: msg = { f'type': RPCMessageType.WARNING_NOTIFICATION, - f'status': f'{len(open_trades)} OPEN TRADES ACTIVE\n\n' - f'Handle these trades manually or \'/start\' the bot again ' - f'and use \'/stopbuy\' to handle open trades gracefully.' + f'status': f'{len(open_trades)} open trades active.\n\n' + f'Handle these trades manually on {self.exchange.name}, ' + f'or \'/start\' the bot again and use \'/stopbuy\' ' + f'to handle open trades gracefully.' } self.rpc.send_msg(msg) From b938c536fa5cd61f86311e6e83d3901228cc91b9 Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Sat, 27 Jun 2020 21:46:53 +0200 Subject: [PATCH 0161/1197] Trying to fix flake8 errors --- freqtrade/freqtradebot.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e3bb5b70b..6d9002c77 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -180,16 +180,16 @@ class FreqtradeBot: Notify the user when the bot is stopped and there are still open trades active. """ - open_trades = Trade.get_trades([Trade.is_open == True, - ]).all() + open_trades = Trade.get_trades([Trade.is_open == 1, + ]).all() - if len(open_trades) is not 0: + if len(open_trades) != 0: msg = { - f'type': RPCMessageType.WARNING_NOTIFICATION, - f'status': f'{len(open_trades)} open trades active.\n\n' + 'type': RPCMessageType.WARNING_NOTIFICATION, + 'status': f'{len(open_trades)} open trades active.\n\n' f'Handle these trades manually on {self.exchange.name}, ' f'or \'/start\' the bot again and use \'/stopbuy\' ' - f'to handle open trades gracefully.' + f'to handle open trades gracefully.', } self.rpc.send_msg(msg) From e5676867a871771dbc887f332464a1b0509e233f Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Sat, 27 Jun 2020 21:53:12 +0200 Subject: [PATCH 0162/1197] Trying to fix flake8 errors --- freqtrade/freqtradebot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6d9002c77..a0bfe5bc4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -180,8 +180,7 @@ class FreqtradeBot: Notify the user when the bot is stopped and there are still open trades active. """ - open_trades = Trade.get_trades([Trade.is_open == 1, - ]).all() + open_trades = Trade.get_trades([Trade.is_open == 1]).all() if len(open_trades) != 0: msg = { From 118f0511719a9479b83f5010367debc66c5b695d Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Sun, 28 Jun 2020 11:02:50 +0200 Subject: [PATCH 0163/1197] Added message in cleanup and fixes --- freqtrade/freqtradebot.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a0bfe5bc4..e9e65ccac 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -119,6 +119,8 @@ class FreqtradeBot: if self.config['cancel_open_orders_on_exit']: self.cancel_all_open_orders() + self.check_for_open_trades() + self.rpc.cleanup() persistence.cleanup() @@ -185,10 +187,11 @@ class FreqtradeBot: if len(open_trades) != 0: msg = { 'type': RPCMessageType.WARNING_NOTIFICATION, - 'status': f'{len(open_trades)} open trades active.\n\n' - f'Handle these trades manually on {self.exchange.name}, ' - f'or \'/start\' the bot again and use \'/stopbuy\' ' - f'to handle open trades gracefully.', + 'status': f"{len(open_trades)} open trades active.\n\n" + f"Handle these trades manually on {self.exchange.name}, " + f"or '/start' the bot again and use '/stopbuy' " + f"to handle open trades gracefully. \n" + f"{'Trades are simulated.' if self.config['dry_run'] else ''}", } self.rpc.send_msg(msg) From 2c45114a64c31ff1abaed6e6d22bf0fe0561e2f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 11:17:06 +0200 Subject: [PATCH 0164/1197] Implement DDos backoff (1s) --- freqtrade/exceptions.py | 7 +++++++ freqtrade/exchange/binance.py | 7 +++++-- freqtrade/exchange/common.py | 8 +++++++- freqtrade/exchange/exchange.py | 30 ++++++++++++++++++++++++++---- freqtrade/exchange/ftx.py | 11 +++++++++-- freqtrade/exchange/kraken.py | 6 +++++- tests/exchange/test_exchange.py | 19 +++++++++++++++++-- 7 files changed, 76 insertions(+), 12 deletions(-) diff --git a/freqtrade/exceptions.py b/freqtrade/exceptions.py index 7cfed87e8..bc84f30b8 100644 --- a/freqtrade/exceptions.py +++ b/freqtrade/exceptions.py @@ -45,6 +45,13 @@ class TemporaryError(FreqtradeException): """ +class DDosProtection(TemporaryError): + """ + Temporary error caused by DDOS protection. + Bot will wait for a second and then retry. + """ + + class StrategyError(FreqtradeException): """ Errors with custom user-code deteced. diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 4279f392c..3a98f161b 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -4,8 +4,9 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +from freqtrade.exceptions import (DDosProtection, DependencyException, + InvalidOrderException, OperationalException, + TemporaryError) from freqtrade.exchange import Exchange logger = logging.getLogger(__name__) @@ -88,6 +89,8 @@ class Binance(Exchange): f'Could not create {ordertype} sell order on market {pair}. ' f'Tried to sell amount {amount} at rate {rate}. ' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index a10d41247..1f74412c6 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -1,6 +1,8 @@ +import asyncio import logging +import time -from freqtrade.exceptions import TemporaryError +from freqtrade.exceptions import DDosProtection, TemporaryError logger = logging.getLogger(__name__) @@ -99,6 +101,8 @@ def retrier_async(f): count -= 1 kwargs.update({'count': count}) logger.warning('retrying %s() still for %s times', f.__name__, count) + if isinstance(ex, DDosProtection): + await asyncio.sleep(1) return await wrapper(*args, **kwargs) else: logger.warning('Giving up retrying: %s()', f.__name__) @@ -117,6 +121,8 @@ def retrier(f): count -= 1 kwargs.update({'count': count}) logger.warning('retrying %s() still for %s times', f.__name__, count) + if isinstance(ex, DDosProtection): + time.sleep(1) return wrapper(*args, **kwargs) else: logger.warning('Giving up retrying: %s()', f.__name__) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b62410c34..09f27e638 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -18,12 +18,13 @@ from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision) from pandas import DataFrame +from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +from freqtrade.exceptions import (DDosProtection, DependencyException, + InvalidOrderException, OperationalException, + TemporaryError) from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async from freqtrade.misc import deep_merge_dicts, safe_value_fallback -from freqtrade.constants import ListPairsWithTimeframes CcxtModuleType = Any @@ -527,6 +528,8 @@ class Exchange: f'Could not create {ordertype} {side} order on market {pair}.' f'Tried to {side} amount {amount} at rate {rate}.' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e @@ -606,6 +609,8 @@ class Exchange: balances.pop("used", None) return balances + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e @@ -620,6 +625,8 @@ class Exchange: raise OperationalException( f'Exchange {self._api.name} does not support fetching tickers in batch. ' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not load tickers due to {e.__class__.__name__}. Message: {e}') from e @@ -633,6 +640,8 @@ class Exchange: raise DependencyException(f"Pair {pair} not available") data = self._api.fetch_ticker(pair) return data + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not load ticker due to {e.__class__.__name__}. Message: {e}') from e @@ -766,6 +775,8 @@ class Exchange: raise OperationalException( f'Exchange {self._api.name} does not support fetching historical ' f'candle (OHLCV) data. Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError(f'Could not fetch historical candle (OHLCV) data ' f'for pair {pair} due to {e.__class__.__name__}. ' @@ -802,6 +813,8 @@ class Exchange: raise OperationalException( f'Exchange {self._api.name} does not support fetching historical trade data.' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError(f'Could not load trade history due to {e.__class__.__name__}. ' f'Message: {e}') from e @@ -948,6 +961,8 @@ class Exchange: except ccxt.InvalidOrder as e: raise InvalidOrderException( f'Could not cancel order. Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e @@ -1003,6 +1018,8 @@ class Exchange: except ccxt.InvalidOrder as e: raise InvalidOrderException( f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e @@ -1027,6 +1044,8 @@ class Exchange: raise OperationalException( f'Exchange {self._api.name} does not support fetching order book.' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get order book due to {e.__class__.__name__}. Message: {e}') from e @@ -1063,7 +1082,8 @@ class Exchange: matched_trades = [trade for trade in my_trades if trade['order'] == order_id] return matched_trades - + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get trades due to {e.__class__.__name__}. Message: {e}') from e @@ -1080,6 +1100,8 @@ class Exchange: return self._api.calculate_fee(symbol=symbol, type=type, side=side, amount=amount, price=price, takerOrMaker=taker_or_maker)['rate'] + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get fee info due to {e.__class__.__name__}. Message: {e}') from e diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index f16db96f5..7d0778ae5 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -4,8 +4,9 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +from freqtrade.exceptions import (DDosProtection, DependencyException, + InvalidOrderException, OperationalException, + TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier @@ -68,6 +69,8 @@ class Ftx(Exchange): f'Could not create {ordertype} sell order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e @@ -96,6 +99,8 @@ class Ftx(Exchange): except ccxt.InvalidOrder as e: raise InvalidOrderException( f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get order due to {e.__class__.__name__}. Message: {e}') from e @@ -111,6 +116,8 @@ class Ftx(Exchange): except ccxt.InvalidOrder as e: raise InvalidOrderException( f'Could not cancel order. Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not cancel order due to {e.__class__.__name__}. Message: {e}') from e diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 932d82a27..7710b8e9b 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -4,7 +4,7 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DependencyException, InvalidOrderException, +from freqtrade.exceptions import (DependencyException, InvalidOrderException, DDosProtection, OperationalException, TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier @@ -45,6 +45,8 @@ class Kraken(Exchange): balances[bal]['free'] = balances[bal]['total'] - balances[bal]['used'] return balances + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e @@ -93,6 +95,8 @@ class Kraken(Exchange): f'Could not create {ordertype} sell order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 700aff969..f3a3a3789 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -4,14 +4,14 @@ import copy import logging from datetime import datetime, timezone from random import randint -from unittest.mock import MagicMock, Mock, PropertyMock +from unittest.mock import MagicMock, Mock, PropertyMock, patch import arrow import ccxt import pytest from pandas import DataFrame -from freqtrade.exceptions import (DependencyException, InvalidOrderException, +from freqtrade.exceptions import (DependencyException, InvalidOrderException, DDosProtection, OperationalException, TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken from freqtrade.exchange.common import API_RETRY_COUNT @@ -38,6 +38,14 @@ def get_mock_coro(return_value): def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, fun, mock_ccxt_fun, **kwargs): + + with patch('freqtrade.exchange.common.time.sleep'): + with pytest.raises(DDosProtection): + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("DDos")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + getattr(exchange, fun)(**kwargs) + assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 + with pytest.raises(TemporaryError): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeaDBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) @@ -52,6 +60,13 @@ def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs): + + with patch('freqtrade.exchange.common.asyncio.sleep'): + with pytest.raises(DDosProtection): + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("DeadBeef")) + exchange = get_patched_exchange(mocker, default_conf, api_mock) + await getattr(exchange, fun)(**kwargs) + with pytest.raises(TemporaryError): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) From 5bd4798ed0953ab1b94fd5a2cc388cdd82eccf50 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 11:56:29 +0200 Subject: [PATCH 0165/1197] Add retrier to stoploss calls (but without retrying) --- freqtrade/exchange/binance.py | 2 ++ freqtrade/exchange/common.py | 44 +++++++++++++++++++-------------- freqtrade/exchange/ftx.py | 1 + freqtrade/exchange/kraken.py | 1 + tests/exchange/test_binance.py | 15 ++++------- tests/exchange/test_exchange.py | 24 ++++++++++++------ tests/exchange/test_ftx.py | 16 ++++-------- tests/exchange/test_kraken.py | 15 +++-------- 8 files changed, 61 insertions(+), 57 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 3a98f161b..ee9566282 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -8,6 +8,7 @@ from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange +from freqtrade.exchange.common import retrier logger = logging.getLogger(__name__) @@ -40,6 +41,7 @@ class Binance(Exchange): """ return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice']) + @retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ creates a stoploss limit order. diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 1f74412c6..d931e1789 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -1,6 +1,7 @@ import asyncio import logging import time +from functools import wraps from freqtrade.exceptions import DDosProtection, TemporaryError @@ -110,21 +111,28 @@ def retrier_async(f): return wrapper -def retrier(f): - def wrapper(*args, **kwargs): - count = kwargs.pop('count', API_RETRY_COUNT) - try: - return f(*args, **kwargs) - except TemporaryError as ex: - logger.warning('%s() returned exception: "%s"', f.__name__, ex) - if count > 0: - count -= 1 - kwargs.update({'count': count}) - logger.warning('retrying %s() still for %s times', f.__name__, count) - if isinstance(ex, DDosProtection): - time.sleep(1) - return wrapper(*args, **kwargs) - else: - logger.warning('Giving up retrying: %s()', f.__name__) - raise ex - return wrapper +def retrier(_func=None, retries=API_RETRY_COUNT): + def decorator(f): + @wraps(f) + def wrapper(*args, **kwargs): + count = kwargs.pop('count', retries) + try: + return f(*args, **kwargs) + except TemporaryError as ex: + logger.warning('%s() returned exception: "%s"', f.__name__, ex) + if count > 0: + count -= 1 + kwargs.update({'count': count}) + logger.warning('retrying %s() still for %s times', f.__name__, count) + if isinstance(ex, DDosProtection): + time.sleep(1) + return wrapper(*args, **kwargs) + else: + logger.warning('Giving up retrying: %s()', f.__name__) + raise ex + return wrapper + # Support both @retrier and @retrier() syntax + if _func is None: + return decorator + else: + return decorator(_func) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 7d0778ae5..f1bd23b52 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -27,6 +27,7 @@ class Ftx(Exchange): """ return order['type'] == 'stop' and stop_loss > float(order['price']) + @retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ Creates a stoploss order. diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 7710b8e9b..01aa647b4 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -60,6 +60,7 @@ class Kraken(Exchange): """ return order['type'] == 'stop-loss' and stop_loss > float(order['price']) + @retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ Creates a stoploss market order. diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 52faa284b..72da708b4 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -5,8 +5,9 @@ import ccxt import pytest from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) + OperationalException) from tests.conftest import get_patched_exchange +from tests.exchange.test_exchange import ccxt_exceptionhandlers @pytest.mark.parametrize('limitratio,expected', [ @@ -62,15 +63,9 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - with pytest.raises(TemporaryError): - api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - - with pytest.raises(OperationalException, match=r".*DeadBeef.*"): - api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance", + "stoploss", "create_order", retries=1, + pair='ETH/BTC', amount=1, stop_price=220, order_types={}) def test_stoploss_order_dry_run_binance(default_conf, mocker): diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index f3a3a3789..15ab0d3d9 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -37,20 +37,20 @@ def get_mock_coro(return_value): def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, - fun, mock_ccxt_fun, **kwargs): + fun, mock_ccxt_fun, retries=API_RETRY_COUNT + 1, **kwargs): with patch('freqtrade.exchange.common.time.sleep'): with pytest.raises(DDosProtection): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("DDos")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) getattr(exchange, fun)(**kwargs) - assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 + assert api_mock.__dict__[mock_ccxt_fun].call_count == retries with pytest.raises(TemporaryError): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeaDBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) getattr(exchange, fun)(**kwargs) - assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 + assert api_mock.__dict__[mock_ccxt_fun].call_count == retries with pytest.raises(OperationalException): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) @@ -59,19 +59,21 @@ def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, assert api_mock.__dict__[mock_ccxt_fun].call_count == 1 -async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs): +async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, + retries=API_RETRY_COUNT + 1, **kwargs): with patch('freqtrade.exchange.common.asyncio.sleep'): with pytest.raises(DDosProtection): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) + assert api_mock.__dict__[mock_ccxt_fun].call_count == retries with pytest.raises(TemporaryError): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) - assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1 + assert api_mock.__dict__[mock_ccxt_fun].call_count == retries with pytest.raises(OperationalException): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) @@ -1142,9 +1144,10 @@ def test_get_balance_prod(default_conf, mocker, exchange_name): exchange.get_balance(currency='BTC') -def test_get_balances_dry_run(default_conf, mocker): +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_balances_dry_run(default_conf, mocker, exchange_name): default_conf['dry_run'] = True - exchange = get_patched_exchange(mocker, default_conf) + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) assert exchange.get_balances() == {} @@ -2126,6 +2129,13 @@ def test_get_markets(default_conf, mocker, markets, assert sorted(pairs.keys()) == sorted(expected_keys) +def test_get_markets_error(default_conf, mocker): + ex = get_patched_exchange(mocker, default_conf) + mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=None)) + with pytest.raises(OperationalException, match="Markets were not loaded."): + ex.get_markets('LTC', 'USDT', True, False) + + def test_timeframe_to_minutes(): assert timeframe_to_minutes("5m") == 5 assert timeframe_to_minutes("10m") == 10 diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 75e98740c..1b7d68770 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -6,9 +6,9 @@ from unittest.mock import MagicMock import ccxt import pytest -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +from freqtrade.exceptions import DependencyException, InvalidOrderException from tests.conftest import get_patched_exchange + from .test_exchange import ccxt_exceptionhandlers STOPLOSS_ORDERTYPE = 'stop' @@ -85,15 +85,9 @@ def test_stoploss_order_ftx(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - with pytest.raises(TemporaryError): - api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - - with pytest.raises(OperationalException, match=r".*DeadBeef.*"): - api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + ccxt_exceptionhandlers(mocker, default_conf, api_mock, "ftx", + "stoploss", "create_order", retries=1, + pair='ETH/BTC', amount=1, stop_price=220, order_types={}) def test_stoploss_order_dry_run_ftx(default_conf, mocker): diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 0950979cf..9451c0b9e 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -6,8 +6,7 @@ from unittest.mock import MagicMock import ccxt import pytest -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +from freqtrade.exceptions import DependencyException, InvalidOrderException from tests.conftest import get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -206,15 +205,9 @@ def test_stoploss_order_kraken(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - with pytest.raises(TemporaryError): - api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) - - with pytest.raises(OperationalException, match=r".*DeadBeef.*"): - api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) - exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken", + "stoploss", "create_order", retries=1, + pair='ETH/BTC', amount=1, stop_price=220, order_types={}) def test_stoploss_order_dry_run_kraken(default_conf, mocker): From e74d2af85788b2e52a4025105e5f6063f29b50e9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 15:44:58 +0200 Subject: [PATCH 0166/1197] Have TemporaryError a subCategory of DependencyException so it's safe to raise out of the exchange --- freqtrade/exceptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exceptions.py b/freqtrade/exceptions.py index bc84f30b8..1ddb2a396 100644 --- a/freqtrade/exceptions.py +++ b/freqtrade/exceptions.py @@ -37,7 +37,7 @@ class InvalidOrderException(FreqtradeException): """ -class TemporaryError(FreqtradeException): +class TemporaryError(DependencyException): """ Temporary network or exchange related error. This could happen when an exchange is congested, unavailable, or the user From bf61bc9d8329aed5ecc7fa034ccb458c1c442434 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 16:01:40 +0200 Subject: [PATCH 0167/1197] Introduce ExchangeError --- freqtrade/data/dataprovider.py | 4 ++-- freqtrade/exceptions.py | 9 ++++++++- freqtrade/exchange/binance.py | 4 ++-- freqtrade/exchange/exchange.py | 12 ++++++------ freqtrade/exchange/ftx.py | 4 ++-- freqtrade/exchange/kraken.py | 7 ++++--- freqtrade/freqtradebot.py | 8 ++++---- freqtrade/rpc/rpc.py | 12 ++++++------ 8 files changed, 34 insertions(+), 26 deletions(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 058ca42da..b677f374b 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -10,7 +10,7 @@ from typing import Any, Dict, List, Optional from pandas import DataFrame from freqtrade.data.history import load_pair_history -from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.exceptions import ExchangeError, OperationalException from freqtrade.exchange import Exchange from freqtrade.state import RunMode from freqtrade.constants import ListPairsWithTimeframes @@ -105,7 +105,7 @@ class DataProvider: """ try: return self._exchange.fetch_ticker(pair) - except DependencyException: + except ExchangeError: return {} def orderbook(self, pair: str, maximum: int) -> Dict[str, List]: diff --git a/freqtrade/exceptions.py b/freqtrade/exceptions.py index 1ddb2a396..995a2cdb7 100644 --- a/freqtrade/exceptions.py +++ b/freqtrade/exceptions.py @@ -37,7 +37,14 @@ class InvalidOrderException(FreqtradeException): """ -class TemporaryError(DependencyException): +class ExchangeError(DependencyException): + """ + Error raised out of the exchange. + Has multiple Errors to determine the appropriate error. + """ + + +class TemporaryError(ExchangeError): """ Temporary network or exchange related error. This could happen when an exchange is congested, unavailable, or the user diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index ee9566282..08e84ee34 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -4,7 +4,7 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DDosProtection, DependencyException, +from freqtrade.exceptions import (DDosProtection, ExchangeError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -80,7 +80,7 @@ class Binance(Exchange): 'stop price: %s. limit: %s', pair, stop_price, rate) return order except ccxt.InsufficientFunds as e: - raise DependencyException( + raise ExchangeError( f'Insufficient funds to create {ordertype} sell order on market {pair}.' f'Tried to sell amount {amount} at rate {rate}. ' f'Message: {e}') from e diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 09f27e638..d48e47909 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -20,7 +20,7 @@ from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list -from freqtrade.exceptions import (DDosProtection, DependencyException, +from freqtrade.exceptions import (DDosProtection, ExchangeError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async @@ -352,7 +352,7 @@ class Exchange: for pair in [f"{curr_1}/{curr_2}", f"{curr_2}/{curr_1}"]: if pair in self.markets and self.markets[pair].get('active'): return pair - raise DependencyException(f"Could not combine {curr_1} and {curr_2} to get a valid pair.") + raise ExchangeError(f"Could not combine {curr_1} and {curr_2} to get a valid pair.") def validate_timeframes(self, timeframe: Optional[str]) -> None: """ @@ -519,12 +519,12 @@ class Exchange: amount, rate_for_order, params) except ccxt.InsufficientFunds as e: - raise DependencyException( + raise ExchangeError( f'Insufficient funds to create {ordertype} {side} order on market {pair}.' f'Tried to {side} amount {amount} at rate {rate}.' f'Message: {e}') from e except ccxt.InvalidOrder as e: - raise DependencyException( + raise ExchangeError( f'Could not create {ordertype} {side} order on market {pair}.' f'Tried to {side} amount {amount} at rate {rate}.' f'Message: {e}') from e @@ -637,7 +637,7 @@ class Exchange: def fetch_ticker(self, pair: str) -> dict: try: if pair not in self._api.markets or not self._api.markets[pair].get('active'): - raise DependencyException(f"Pair {pair} not available") + raise ExchangeError(f"Pair {pair} not available") data = self._api.fetch_ticker(pair) return data except ccxt.DDoSProtection as e: @@ -1151,7 +1151,7 @@ class Exchange: fee_to_quote_rate = safe_value_fallback(tick, tick, 'last', 'ask') return round((order['fee']['cost'] * fee_to_quote_rate) / order['cost'], 8) - except DependencyException: + except ExchangeError: return None def extract_cost_curr_rate(self, order: Dict) -> Tuple[float, str, Optional[float]]: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index f1bd23b52..be815d336 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -4,7 +4,7 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DDosProtection, DependencyException, +from freqtrade.exceptions import (DDosProtection, ExchangeError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -61,7 +61,7 @@ class Ftx(Exchange): 'stop price: %s.', pair, stop_price) return order except ccxt.InsufficientFunds as e: - raise DependencyException( + raise ExchangeError( f'Insufficient funds to create {ordertype} sell order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 01aa647b4..2ca4ba167 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -4,8 +4,9 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DependencyException, InvalidOrderException, DDosProtection, - OperationalException, TemporaryError) +from freqtrade.exceptions import (DDosProtection, ExchangeError, + InvalidOrderException, OperationalException, + TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier @@ -87,7 +88,7 @@ class Kraken(Exchange): 'stop price: %s.', pair, stop_price) return order except ccxt.InsufficientFunds as e: - raise DependencyException( + raise ExchangeError( f'Insufficient funds to create {ordertype} sell order on market {pair}.' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 289850709..2e59d915d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -11,14 +11,14 @@ from typing import Any, Dict, List, Optional import arrow from cachetools import TTLCache -from requests.exceptions import RequestException from freqtrade import __version__, constants, persistence from freqtrade.configuration import validate_config_consistency from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge -from freqtrade.exceptions import DependencyException, InvalidOrderException, PricingError +from freqtrade.exceptions import (DependencyException, ExchangeError, + InvalidOrderException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.misc import safe_value_fallback from freqtrade.pairlist.pairlistmanager import PairListManager @@ -755,7 +755,7 @@ class FreqtradeBot: logger.warning('Selling the trade forcefully') self.execute_sell(trade, trade.stop_loss, sell_reason=SellType.EMERGENCY_SELL) - except DependencyException: + except ExchangeError: trade.stoploss_order_id = None logger.exception('Unable to place a stoploss order on exchange.') return False @@ -891,7 +891,7 @@ class FreqtradeBot: if not trade.open_order_id: continue order = self.exchange.get_order(trade.open_order_id, trade.pair) - except (RequestException, DependencyException, InvalidOrderException): + except (ExchangeError, InvalidOrderException): logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) continue diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index aeaf82662..aab3da258 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Tuple import arrow from numpy import NAN, mean -from freqtrade.exceptions import DependencyException, TemporaryError +from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.misc import shorten_date from freqtrade.persistence import Trade from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -130,7 +130,7 @@ class RPC: # calculate profit and send message to user try: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - except DependencyException: + except (ExchangeError, PricingError): current_rate = NAN current_profit = trade.calc_profit_ratio(current_rate) current_profit_abs = trade.calc_profit(current_rate) @@ -174,7 +174,7 @@ class RPC: # calculate profit and send message to user try: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - except DependencyException: + except (PricingError, ExchangeError): current_rate = NAN trade_percent = (100 * trade.calc_profit_ratio(current_rate)) trade_profit = trade.calc_profit(current_rate) @@ -286,7 +286,7 @@ class RPC: # Get current rate try: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - except DependencyException: + except (PricingError, ExchangeError): current_rate = NAN profit_ratio = trade.calc_profit_ratio(rate=current_rate) @@ -352,7 +352,7 @@ class RPC: total = 0.0 try: tickers = self._freqtrade.exchange.get_tickers() - except (TemporaryError, DependencyException): + except (ExchangeError): raise RPCException('Error getting current tickers.') self._freqtrade.wallets.update(require_update=False) @@ -373,7 +373,7 @@ class RPC: if pair.startswith(stake_currency): rate = 1.0 / rate est_stake = rate * balance.total - except (TemporaryError, DependencyException): + except (ExchangeError): logger.warning(f" Could not get rate for pair {coin}.") continue total = total + (est_stake or 0) From 29d3ff1bc922690b2aae5865cc574613ec454631 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 16:04:04 +0200 Subject: [PATCH 0168/1197] Adjust tests to work with ExchangeError --- tests/data/test_dataprovider.py | 6 +++--- tests/exchange/test_exchange.py | 2 +- tests/rpc/test_rpc.py | 11 ++++++----- tests/test_freqtradebot.py | 15 +++++++-------- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index def3ad535..2f91dcd38 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -1,11 +1,11 @@ from unittest.mock import MagicMock -from pandas import DataFrame import pytest +from pandas import DataFrame from freqtrade.data.dataprovider import DataProvider +from freqtrade.exceptions import ExchangeError, OperationalException from freqtrade.pairlist.pairlistmanager import PairListManager -from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.state import RunMode from tests.conftest import get_patched_exchange @@ -164,7 +164,7 @@ def test_ticker(mocker, default_conf, tickers): assert 'symbol' in res assert res['symbol'] == 'ETH/BTC' - ticker_mock = MagicMock(side_effect=DependencyException('Pair not found')) + ticker_mock = MagicMock(side_effect=ExchangeError('Pair not found')) mocker.patch("freqtrade.exchange.Exchange.fetch_ticker", ticker_mock) exchange = get_patched_exchange(mocker, default_conf) dp = DataProvider(default_conf, exchange) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 15ab0d3d9..64ea317aa 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -64,7 +64,7 @@ async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fu with patch('freqtrade.exchange.common.asyncio.sleep'): with pytest.raises(DDosProtection): - api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("DeadBeef")) + api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("Dooh")) exchange = get_patched_exchange(mocker, default_conf, api_mock) await getattr(exchange, fun)(**kwargs) assert api_mock.__dict__[mock_ccxt_fun].call_count == retries diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 0ffbaa72a..45243e5e6 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -8,12 +8,13 @@ import pytest from numpy import isnan from freqtrade.edge import PairInfo -from freqtrade.exceptions import DependencyException, TemporaryError +from freqtrade.exceptions import ExchangeError, TemporaryError from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State -from tests.conftest import get_patched_freqtradebot, patch_get_signal, create_mock_trades +from tests.conftest import (create_mock_trades, get_patched_freqtradebot, + patch_get_signal) # Functions for recurrent object patching @@ -106,7 +107,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: } mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', - MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available"))) + MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) results = rpc._rpc_trade_status() assert isnan(results[0]['current_profit']) assert isnan(results[0]['current_rate']) @@ -209,7 +210,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert '-0.41% (-0.06)' == result[0][3] mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', - MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available"))) + MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert 'instantly' == result[0][2] assert 'ETH/BTC' in result[0][1] @@ -365,7 +366,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, # Test non-available pair mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', - MagicMock(side_effect=DependencyException("Pair 'ETH/BTC' not available"))) + MagicMock(side_effect=ExchangeError("Pair 'ETH/BTC' not available"))) stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) assert stats['trade_count'] == 2 assert stats['first_trade_date'] == 'just now' diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5d83c893e..f2967411b 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -9,13 +9,12 @@ from unittest.mock import ANY, MagicMock, PropertyMock import arrow import pytest -import requests from freqtrade.constants import (CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT) -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException, PricingError, - TemporaryError) +from freqtrade.exceptions import (DependencyException, ExchangeError, + InvalidOrderException, OperationalException, + PricingError, TemporaryError) from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.rpc import RPCMessageType @@ -1172,7 +1171,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, mocker.patch( 'freqtrade.exchange.Exchange.stoploss', - side_effect=DependencyException() + side_effect=ExchangeError() ) trade.is_open = True freqtrade.handle_stoploss_on_exchange(trade) @@ -1216,7 +1215,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, get_stoploss_order=MagicMock(return_value={'status': 'canceled'}), - stoploss=MagicMock(side_effect=DependencyException()), + stoploss=MagicMock(side_effect=ExchangeError()), ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -1442,7 +1441,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c # Fail creating stoploss order caplog.clear() cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_stoploss_order", MagicMock()) - mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=DependencyException()) + mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=ExchangeError()) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert cancel_mock.call_count == 1 assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog) @@ -2320,7 +2319,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(side_effect=requests.exceptions.RequestException('Oh snap')), + get_order=MagicMock(side_effect=ExchangeError('Oh snap')), cancel_order=cancel_order_mock ) freqtrade = FreqtradeBot(default_conf) From e040c518cac73e6ee1ad551757eef93407fc6fdc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 16:18:39 +0200 Subject: [PATCH 0169/1197] Dynamic backoff on DDos errors --- freqtrade/exchange/common.py | 11 +++++++++-- tests/exchange/test_exchange.py | 14 +++++++++++++- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index d931e1789..b0f6b14e3 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -91,6 +91,13 @@ MAP_EXCHANGE_CHILDCLASS = { } +def calculate_backoff(retry, max_retries): + """ + Calculate backoff + """ + return retry ** 2 + 1 + + def retrier_async(f): async def wrapper(*args, **kwargs): count = kwargs.pop('count', API_RETRY_COUNT) @@ -125,13 +132,13 @@ def retrier(_func=None, retries=API_RETRY_COUNT): kwargs.update({'count': count}) logger.warning('retrying %s() still for %s times', f.__name__, count) if isinstance(ex, DDosProtection): - time.sleep(1) + time.sleep(calculate_backoff(count, retries)) return wrapper(*args, **kwargs) else: logger.warning('Giving up retrying: %s()', f.__name__) raise ex return wrapper - # Support both @retrier and @retrier() syntax + # Support both @retrier and @retrier(retries=2) syntax if _func is None: return decorator else: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 64ea317aa..358452caf 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -14,7 +14,7 @@ from pandas import DataFrame from freqtrade.exceptions import (DependencyException, InvalidOrderException, DDosProtection, OperationalException, TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken -from freqtrade.exchange.common import API_RETRY_COUNT +from freqtrade.exchange.common import API_RETRY_COUNT, calculate_backoff from freqtrade.exchange.exchange import (market_is_active, symbol_is_pair, timeframe_to_minutes, timeframe_to_msecs, @@ -2296,3 +2296,15 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None: ex = get_patched_exchange(mocker, default_conf) assert ex.calculate_fee_rate(order) == expected + + +@pytest.mark.parametrize('retry,max_retries,expected', [ + (0, 3, 1), + (1, 3, 2), + (2, 3, 5), + (3, 3, 10), + (0, 1, 1), + (1, 1, 2), +]) +def test_calculate_backoff(retry, max_retries, expected): + assert calculate_backoff(retry, max_retries) == expected From 92c70fb903f59d9651f50efb3061f36a32d13cf3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 16:27:35 +0200 Subject: [PATCH 0170/1197] Rename get_order to fetch_order (to align to ccxt naming) --- freqtrade/exchange/exchange.py | 10 +++--- freqtrade/freqtradebot.py | 6 ++-- freqtrade/persistence.py | 2 +- freqtrade/rpc/rpc.py | 4 +-- tests/exchange/test_exchange.py | 12 +++---- tests/rpc/test_rpc.py | 8 ++--- tests/test_freqtradebot.py | 60 ++++++++++++++++----------------- 7 files changed, 51 insertions(+), 51 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index d48e47909..5a19b34af 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -946,7 +946,7 @@ class Exchange: def check_order_canceled_empty(self, order: Dict) -> bool: """ Verify if an order has been cancelled without being partially filled - :param order: Order dict as returned from get_order() + :param order: Order dict as returned from fetch_order() :return: True if order has been cancelled without being filled, False otherwise. """ return order.get('status') in ('closed', 'canceled') and order.get('filled') == 0.0 @@ -983,7 +983,7 @@ class Exchange: """ Cancel order returning a result. Creates a fake result if cancel order returns a non-usable result - and get_order does not work (certain exchanges don't return cancelled orders) + and fetch_order does not work (certain exchanges don't return cancelled orders) :param order_id: Orderid to cancel :param pair: Pair corresponding to order_id :param amount: Amount to use for fake response @@ -996,7 +996,7 @@ class Exchange: except InvalidOrderException: logger.warning(f"Could not cancel order {order_id}.") try: - order = self.get_order(order_id, pair) + order = self.fetch_order(order_id, pair) except InvalidOrderException: logger.warning(f"Could not fetch cancelled order {order_id}.") order = {'fee': {}, 'status': 'canceled', 'amount': amount, 'info': {}} @@ -1004,7 +1004,7 @@ class Exchange: return order @retrier - def get_order(self, order_id: str, pair: str) -> Dict: + def fetch_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: try: order = self._dry_run_open_orders[order_id] @@ -1027,7 +1027,7 @@ class Exchange: raise OperationalException(e) from e # Assign method to get_stoploss_order to allow easy overriding in other classes - get_stoploss_order = get_order + get_stoploss_order = fetch_order @retrier def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2e59d915d..0d9c5e27c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -890,7 +890,7 @@ class FreqtradeBot: try: if not trade.open_order_id: continue - order = self.exchange.get_order(trade.open_order_id, trade.pair) + order = self.exchange.fetch_order(trade.open_order_id, trade.pair) except (ExchangeError, InvalidOrderException): logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) continue @@ -923,7 +923,7 @@ class FreqtradeBot: for trade in Trade.get_open_order_trades(): try: - order = self.exchange.get_order(trade.open_order_id, trade.pair) + order = self.exchange.fetch_order(trade.open_order_id, trade.pair) except (DependencyException, InvalidOrderException): logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) continue @@ -1202,7 +1202,7 @@ class FreqtradeBot: # Update trade with order values logger.info('Found open order for %s', trade) try: - order = action_order or self.exchange.get_order(order_id, trade.pair) + order = action_order or self.exchange.fetch_order(order_id, trade.pair) except InvalidOrderException as exception: logger.warning('Unable to fetch order %s: %s', order_id, exception) return False diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 097a2f984..a6c1de402 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -360,7 +360,7 @@ class Trade(_DECL_BASE): def update(self, order: Dict) -> None: """ Updates this entity with amount and actual open/close rates. - :param order: order retrieved by exchange.get_order() + :param order: order retrieved by exchange.fetch_order() :return: None """ order_type = order['type'] diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index aab3da258..cc5f35f0d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -126,7 +126,7 @@ class RPC: for trade in trades: order = None if trade.open_order_id: - order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair) + order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) # calculate profit and send message to user try: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) @@ -442,7 +442,7 @@ class RPC: def _exec_forcesell(trade: Trade) -> None: # Check if there is there is an open order if trade.open_order_id: - order = self._freqtrade.exchange.get_order(trade.open_order_id, trade.pair) + order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) # Cancel open LIMIT_BUY orders and close trade if order and order['status'] == 'open' \ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 358452caf..cf38b3cd5 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1865,31 +1865,31 @@ def test_cancel_stoploss_order(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_get_order(default_conf, mocker, exchange_name): +def test_fetch_order(default_conf, mocker, exchange_name): default_conf['dry_run'] = True order = MagicMock() order.myid = 123 exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange._dry_run_open_orders['X'] = order - assert exchange.get_order('X', 'TKN/BTC').myid == 123 + assert exchange.fetch_order('X', 'TKN/BTC').myid == 123 with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'): - exchange.get_order('Y', 'TKN/BTC') + exchange.fetch_order('Y', 'TKN/BTC') default_conf['dry_run'] = False api_mock = MagicMock() api_mock.fetch_order = MagicMock(return_value=456) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - assert exchange.get_order('X', 'TKN/BTC') == 456 + assert exchange.fetch_order('X', 'TKN/BTC') == 456 with pytest.raises(InvalidOrderException): api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.get_order(order_id='_', pair='TKN/BTC') + exchange.fetch_order(order_id='_', pair='TKN/BTC') assert api_mock.fetch_order.call_count == 1 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, - 'get_order', 'fetch_order', + 'fetch_order', 'fetch_order', order_id='_', pair='TKN/BTC') diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 45243e5e6..de9327ba9 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -607,7 +607,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: 'freqtrade.exchange.Exchange', fetch_ticker=ticker, cancel_order=cancel_order_mock, - get_order=MagicMock( + fetch_order=MagicMock( return_value={ 'status': 'closed', 'type': 'limit', @@ -653,7 +653,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: trade = Trade.query.filter(Trade.id == '1').first() filled_amount = trade.amount / 2 mocker.patch( - 'freqtrade.exchange.Exchange.get_order', + 'freqtrade.exchange.Exchange.fetch_order', return_value={ 'status': 'open', 'type': 'limit', @@ -672,7 +672,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: amount = trade.amount # make an limit-buy open trade, if there is no 'filled', don't sell it mocker.patch( - 'freqtrade.exchange.Exchange.get_order', + 'freqtrade.exchange.Exchange.fetch_order', return_value={ 'status': 'open', 'type': 'limit', @@ -689,7 +689,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: freqtradebot.enter_positions() # make an limit-sell open trade mocker.patch( - 'freqtrade.exchange.Exchange.get_order', + 'freqtrade.exchange.Exchange.fetch_order', return_value={ 'status': 'open', 'type': 'limit', diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index f2967411b..3b00f3371 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -762,7 +762,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, 'freqtrade.exchange.Exchange', fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), - get_order=MagicMock(return_value=limit_buy_order), + fetch_order=MagicMock(return_value=limit_buy_order), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -831,7 +831,7 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order, fee, mock 'freqtrade.exchange.Exchange', fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), - get_order=MagicMock(return_value=limit_buy_order), + fetch_order=MagicMock(return_value=limit_buy_order), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -858,7 +858,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, 'freqtrade.exchange.Exchange', fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), - get_order=MagicMock(return_value=limit_buy_order), + fetch_order=MagicMock(return_value=limit_buy_order), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -1063,7 +1063,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=limit_buy_order['amount']) @@ -1178,7 +1178,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, assert log_has('Unable to place a stoploss order on exchange.', caplog) assert trade.stoploss_order_id is None - # Fifth case: get_order returns InvalidOrder + # Fifth case: fetch_order returns InvalidOrder # It should try to add stoploss order trade.stoploss_order_id = 100 stoploss.reset_mock() @@ -1193,7 +1193,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = None trade.is_open = False stoploss.reset_mock() - mocker.patch('freqtrade.exchange.Exchange.get_order') + mocker.patch('freqtrade.exchange.Exchange.fetch_order') mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) assert freqtrade.handle_stoploss_on_exchange(trade) is False assert stoploss.call_count == 0 @@ -1248,7 +1248,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=sell_mock, get_fee=fee, - get_order=MagicMock(return_value={'status': 'canceled'}), + fetch_order=MagicMock(return_value={'status': 'canceled'}), stoploss=MagicMock(side_effect=InvalidOrderException()), ) freqtrade = FreqtradeBot(default_conf) @@ -1588,7 +1588,7 @@ def test_exit_positions(mocker, default_conf, limit_buy_order, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=limit_buy_order['amount']) @@ -1612,7 +1612,7 @@ def test_exit_positions(mocker, default_conf, limit_buy_order, caplog) -> None: def test_exit_positions_exception(mocker, default_conf, limit_buy_order, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) trade = MagicMock() trade.open_order_id = None @@ -1633,7 +1633,7 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=limit_buy_order['amount']) @@ -1672,8 +1672,8 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No 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)) + # fetch_order should not be called!! + mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) patch_exchange(mocker) Trade.session = MagicMock() amount = sum(x['amount'] for x in trades_for_order) @@ -1697,8 +1697,8 @@ def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_ 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) - # get_order should not be called!! - mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError)) + # fetch_order should not be called!! + mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) patch_exchange(mocker) Trade.session = MagicMock() amount = sum(x['amount'] for x in trades_for_order) @@ -1723,7 +1723,7 @@ def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_ def test_update_trade_state_exception(mocker, default_conf, limit_buy_order, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) trade = MagicMock() trade.open_order_id = '123' @@ -1740,7 +1740,7 @@ def test_update_trade_state_exception(mocker, default_conf, def test_update_trade_state_orderexception(mocker, default_conf, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.get_order', + mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=InvalidOrderException)) trade = MagicMock() @@ -1756,8 +1756,8 @@ def test_update_trade_state_orderexception(mocker, default_conf, caplog) -> None def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_order, 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)) + # fetch_order should not be called!! + mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) wallet_mock = MagicMock() mocker.patch('freqtrade.wallets.Wallets.update', wallet_mock) @@ -1972,7 +1972,7 @@ def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_or mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old), + fetch_order=MagicMock(return_value=limit_buy_order_old), cancel_order=cancel_order_mock, get_fee=fee ) @@ -2021,7 +2021,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, op mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old), + fetch_order=MagicMock(return_value=limit_buy_order_old), cancel_order_with_result=cancel_order_mock, get_fee=fee ) @@ -2051,7 +2051,7 @@ def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, o mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old), + fetch_order=MagicMock(return_value=limit_buy_order_old), cancel_order=cancel_order_mock, get_fee=fee ) @@ -2078,7 +2078,7 @@ def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_ord 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), fetch_ticker=ticker, - get_order=MagicMock(side_effect=DependencyException), + fetch_order=MagicMock(side_effect=DependencyException), cancel_order=cancel_order_mock, get_fee=fee ) @@ -2104,7 +2104,7 @@ def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_ mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_sell_order_old), + fetch_order=MagicMock(return_value=limit_sell_order_old), cancel_order=cancel_order_mock ) freqtrade = FreqtradeBot(default_conf) @@ -2151,7 +2151,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_sell_order_old), + fetch_order=MagicMock(return_value=limit_sell_order_old), cancel_order=cancel_order_mock ) freqtrade = FreqtradeBot(default_conf) @@ -2182,7 +2182,7 @@ def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_sell_order_old), + fetch_order=MagicMock(return_value=limit_sell_order_old), cancel_order_with_result=cancel_order_mock ) freqtrade = FreqtradeBot(default_conf) @@ -2209,7 +2209,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old_partial), + fetch_order=MagicMock(return_value=limit_buy_order_old_partial), cancel_order_with_result=cancel_order_mock ) freqtrade = FreqtradeBot(default_conf) @@ -2237,7 +2237,7 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old_partial), + fetch_order=MagicMock(return_value=limit_buy_order_old_partial), cancel_order_with_result=cancel_order_mock, get_trades_for_order=MagicMock(return_value=trades_for_order), ) @@ -2275,7 +2275,7 @@ def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(return_value=limit_buy_order_old_partial), + fetch_order=MagicMock(return_value=limit_buy_order_old_partial), cancel_order_with_result=cancel_order_mock, get_trades_for_order=MagicMock(return_value=trades_for_order), ) @@ -2319,7 +2319,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - get_order=MagicMock(side_effect=ExchangeError('Oh snap')), + fetch_order=MagicMock(side_effect=ExchangeError('Oh snap')), cancel_order=cancel_order_mock ) freqtrade = FreqtradeBot(default_conf) @@ -4016,7 +4016,7 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order, @pytest.mark.usefixtures("init_persistence") def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limit_sell_order): default_conf['cancel_open_orders_on_exit'] = True - mocker.patch('freqtrade.exchange.Exchange.get_order', + mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=[DependencyException(), limit_sell_order, limit_buy_order]) buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_buy') sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_sell') From cbcbb4bdb533247076b338035b640dc0f29f0495 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 16:30:24 +0200 Subject: [PATCH 0171/1197] Rename get_stoploss_order to fetch_stoploss_order (align with fetch_order) --- freqtrade/exchange/exchange.py | 6 +++--- freqtrade/exchange/ftx.py | 2 +- freqtrade/freqtradebot.py | 4 ++-- tests/exchange/test_exchange.py | 12 ++++++------ tests/exchange/test_ftx.py | 14 +++++++------- tests/test_freqtradebot.py | 18 +++++++++--------- tests/test_integration.py | 2 +- 7 files changed, 29 insertions(+), 29 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 5a19b34af..daa73ca35 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -969,7 +969,7 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - # Assign method to get_stoploss_order to allow easy overriding in other classes + # Assign method to fetch_stoploss_order to allow easy overriding in other classes cancel_stoploss_order = cancel_order def is_cancel_order_result_suitable(self, corder) -> bool: @@ -1026,8 +1026,8 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - # Assign method to get_stoploss_order to allow easy overriding in other classes - get_stoploss_order = fetch_order + # Assign method to fetch_stoploss_order to allow easy overriding in other classes + fetch_stoploss_order = fetch_order @retrier def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index be815d336..b75f77ca4 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -79,7 +79,7 @@ class Ftx(Exchange): raise OperationalException(e) from e @retrier - def get_stoploss_order(self, order_id: str, pair: str) -> Dict: + def fetch_stoploss_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: try: order = self._dry_run_open_orders[order_id] diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0d9c5e27c..bd6bba344 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -773,8 +773,8 @@ class FreqtradeBot: try: # First we check if there is already a stoploss on exchange - stoploss_order = self.exchange.get_stoploss_order(trade.stoploss_order_id, trade.pair) \ - if trade.stoploss_order_id else None + stoploss_order = self.exchange.fetch_stoploss_order( + trade.stoploss_order_id, trade.pair) if trade.stoploss_order_id else None except InvalidOrderException as exception: logger.warning('Unable to fetch stoploss order: %s', exception) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index cf38b3cd5..fc4bf490c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1894,7 +1894,7 @@ def test_fetch_order(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_get_stoploss_order(default_conf, mocker, exchange_name): +def test_fetch_stoploss_order(default_conf, mocker, exchange_name): # Don't test FTX here - that needs a seperate test if exchange_name == 'ftx': return @@ -1903,25 +1903,25 @@ def test_get_stoploss_order(default_conf, mocker, exchange_name): order.myid = 123 exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) exchange._dry_run_open_orders['X'] = order - assert exchange.get_stoploss_order('X', 'TKN/BTC').myid == 123 + assert exchange.fetch_stoploss_order('X', 'TKN/BTC').myid == 123 with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'): - exchange.get_stoploss_order('Y', 'TKN/BTC') + exchange.fetch_stoploss_order('Y', 'TKN/BTC') default_conf['dry_run'] = False api_mock = MagicMock() api_mock.fetch_order = MagicMock(return_value=456) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - assert exchange.get_stoploss_order('X', 'TKN/BTC') == 456 + assert exchange.fetch_stoploss_order('X', 'TKN/BTC') == 456 with pytest.raises(InvalidOrderException): api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.get_stoploss_order(order_id='_', pair='TKN/BTC') + exchange.fetch_stoploss_order(order_id='_', pair='TKN/BTC') assert api_mock.fetch_order.call_count == 1 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, - 'get_stoploss_order', 'fetch_order', + 'fetch_stoploss_order', 'fetch_order', order_id='_', pair='TKN/BTC') diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 1b7d68770..eb7d83be3 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -124,34 +124,34 @@ def test_stoploss_adjust_ftx(mocker, default_conf): assert not exchange.stoploss_adjust(1501, order) -def test_get_stoploss_order(default_conf, mocker): +def test_fetch_stoploss_order(default_conf, mocker): default_conf['dry_run'] = True order = MagicMock() order.myid = 123 exchange = get_patched_exchange(mocker, default_conf, id='ftx') exchange._dry_run_open_orders['X'] = order - assert exchange.get_stoploss_order('X', 'TKN/BTC').myid == 123 + assert exchange.fetch_stoploss_order('X', 'TKN/BTC').myid == 123 with pytest.raises(InvalidOrderException, match=r'Tried to get an invalid dry-run-order.*'): - exchange.get_stoploss_order('Y', 'TKN/BTC') + exchange.fetch_stoploss_order('Y', 'TKN/BTC') default_conf['dry_run'] = False api_mock = MagicMock() api_mock.fetch_orders = MagicMock(return_value=[{'id': 'X', 'status': '456'}]) exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') - assert exchange.get_stoploss_order('X', 'TKN/BTC')['status'] == '456' + assert exchange.fetch_stoploss_order('X', 'TKN/BTC')['status'] == '456' api_mock.fetch_orders = MagicMock(return_value=[{'id': 'Y', 'status': '456'}]) exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') with pytest.raises(InvalidOrderException, match=r"Could not get stoploss order for id X"): - exchange.get_stoploss_order('X', 'TKN/BTC')['status'] + exchange.fetch_stoploss_order('X', 'TKN/BTC')['status'] with pytest.raises(InvalidOrderException): api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') - exchange.get_stoploss_order(order_id='_', pair='TKN/BTC') + exchange.fetch_stoploss_order(order_id='_', pair='TKN/BTC') assert api_mock.fetch_orders.call_count == 1 ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'ftx', - 'get_stoploss_order', 'fetch_orders', + 'fetch_stoploss_order', 'fetch_orders', order_id='_', pair='TKN/BTC') diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 3b00f3371..ef48bdc34 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1125,7 +1125,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = 100 hanging_stoploss_order = MagicMock(return_value={'status': 'open'}) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', hanging_stoploss_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', hanging_stoploss_order) assert freqtrade.handle_stoploss_on_exchange(trade) is False assert trade.stoploss_order_id == 100 @@ -1138,7 +1138,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = 100 canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'}) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', canceled_stoploss_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', canceled_stoploss_order) stoploss.reset_mock() assert freqtrade.handle_stoploss_on_exchange(trade) is False @@ -1163,7 +1163,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, 'average': 2, 'amount': limit_buy_order['amount'], }) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hit) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hit) assert freqtrade.handle_stoploss_on_exchange(trade) is True assert log_has('STOP_LOSS_LIMIT is hit for {}.'.format(trade), caplog) assert trade.stoploss_order_id is None @@ -1182,7 +1182,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, # It should try to add stoploss order trade.stoploss_order_id = 100 stoploss.reset_mock() - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', side_effect=InvalidOrderException()) mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) freqtrade.handle_stoploss_on_exchange(trade) @@ -1214,7 +1214,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, - get_stoploss_order=MagicMock(return_value={'status': 'canceled'}), + fetch_stoploss_order=MagicMock(return_value={'status': 'canceled'}), stoploss=MagicMock(side_effect=ExchangeError()), ) freqtrade = FreqtradeBot(default_conf) @@ -1331,7 +1331,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, } }) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) # stoploss initially at 5% assert freqtrade.handle_trade(trade) is False @@ -1431,7 +1431,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c } mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', side_effect=InvalidOrderException()) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog) @@ -1511,7 +1511,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, } }) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) # stoploss initially at 20% as edge dictated it. assert freqtrade.handle_trade(trade) is False @@ -2773,7 +2773,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f "fee": None, "trades": None }) - mocker.patch('freqtrade.exchange.Exchange.get_stoploss_order', stoploss_executed) + mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_executed) freqtrade.exit_positions(trades) assert trade.stoploss_order_id is None diff --git a/tests/test_integration.py b/tests/test_integration.py index 57960503e..168286e6d 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -62,7 +62,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, get_fee=fee, amount_to_precision=lambda s, x, y: y, price_to_precision=lambda s, x, y: y, - get_stoploss_order=stoploss_order_mock, + fetch_stoploss_order=stoploss_order_mock, cancel_stoploss_order=cancel_order_mock, ) From 6362bfc36ef843c60046a94de8184b51a09f64e7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 19:40:33 +0200 Subject: [PATCH 0172/1197] Fix calculate_backoff implementation --- freqtrade/exchange/common.py | 4 ++-- tests/exchange/test_exchange.py | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index b0f6b14e3..d3ba8cef6 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -91,11 +91,11 @@ MAP_EXCHANGE_CHILDCLASS = { } -def calculate_backoff(retry, max_retries): +def calculate_backoff(retrycount, max_retries): """ Calculate backoff """ - return retry ** 2 + 1 + return (max_retries - retrycount) ** 2 + 1 def retrier_async(f): diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index fc4bf490c..8c397fe68 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2298,13 +2298,13 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None: assert ex.calculate_fee_rate(order) == expected -@pytest.mark.parametrize('retry,max_retries,expected', [ - (0, 3, 1), - (1, 3, 2), - (2, 3, 5), - (3, 3, 10), - (0, 1, 1), - (1, 1, 2), +@pytest.mark.parametrize('retrycount,max_retries,expected', [ + (0, 3, 10), + (1, 3, 5), + (2, 3, 2), + (3, 3, 1), + (0, 1, 2), + (1, 1, 1), ]) -def test_calculate_backoff(retry, max_retries, expected): - assert calculate_backoff(retry, max_retries) == expected +def test_calculate_backoff(retrycount, max_retries, expected): + assert calculate_backoff(retrycount, max_retries) == expected From c6124180fe14bcdf9f78676d021c2e8e6880d1e7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 19:45:42 +0200 Subject: [PATCH 0173/1197] Fix bug when fetching orders fails --- freqtrade/exceptions.py | 7 +++++++ freqtrade/exchange/common.py | 14 ++++++++------ freqtrade/exchange/exchange.py | 5 ++++- tests/exchange/test_exchange.py | 12 ++++++++++++ tests/test_freqtradebot.py | 2 +- 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/freqtrade/exceptions.py b/freqtrade/exceptions.py index 995a2cdb7..c85fccc4b 100644 --- a/freqtrade/exceptions.py +++ b/freqtrade/exceptions.py @@ -37,6 +37,13 @@ class InvalidOrderException(FreqtradeException): """ +class RetryableOrderError(InvalidOrderException): + """ + This is returned when the order is not found. + This Error will be repeated with increasing backof (in line with DDosError). + """ + + class ExchangeError(DependencyException): """ Error raised out of the exchange. diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index d3ba8cef6..9b7d8dfea 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -3,7 +3,8 @@ import logging import time from functools import wraps -from freqtrade.exceptions import DDosProtection, TemporaryError +from freqtrade.exceptions import (DDosProtection, RetryableOrderError, + TemporaryError) logger = logging.getLogger(__name__) @@ -109,8 +110,8 @@ def retrier_async(f): count -= 1 kwargs.update({'count': count}) logger.warning('retrying %s() still for %s times', f.__name__, count) - if isinstance(ex, DDosProtection): - await asyncio.sleep(1) + if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError): + await asyncio.sleep(calculate_backoff(count + 1, API_RETRY_COUNT)) return await wrapper(*args, **kwargs) else: logger.warning('Giving up retrying: %s()', f.__name__) @@ -125,14 +126,15 @@ def retrier(_func=None, retries=API_RETRY_COUNT): count = kwargs.pop('count', retries) try: return f(*args, **kwargs) - except TemporaryError as ex: + except (TemporaryError, RetryableOrderError) as ex: logger.warning('%s() returned exception: "%s"', f.__name__, ex) if count > 0: count -= 1 kwargs.update({'count': count}) logger.warning('retrying %s() still for %s times', f.__name__, count) - if isinstance(ex, DDosProtection): - time.sleep(calculate_backoff(count, retries)) + if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError): + # increasing backoff + time.sleep(calculate_backoff(count + 1, retries)) return wrapper(*args, **kwargs) else: logger.warning('Giving up retrying: %s()', f.__name__) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index daa73ca35..a3a548176 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -22,7 +22,7 @@ from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list from freqtrade.exceptions import (DDosProtection, ExchangeError, InvalidOrderException, OperationalException, - TemporaryError) + RetryableOrderError, TemporaryError) from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async from freqtrade.misc import deep_merge_dicts, safe_value_fallback @@ -1015,6 +1015,9 @@ class Exchange: f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e try: return self._api.fetch_order(order_id, pair) + except ccxt.OrderNotFound as e: + raise RetryableOrderError( + f'Order not found (id: {order_id}). Message: {e}') from e except ccxt.InvalidOrder as e: raise InvalidOrderException( f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 8c397fe68..66f88d82f 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1888,6 +1888,18 @@ def test_fetch_order(default_conf, mocker, exchange_name): exchange.fetch_order(order_id='_', pair='TKN/BTC') assert api_mock.fetch_order.call_count == 1 + api_mock.fetch_order = MagicMock(side_effect=ccxt.OrderNotFound("Order not found")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + with patch('freqtrade.exchange.common.time.sleep') as tm: + with pytest.raises(InvalidOrderException): + exchange.fetch_order(order_id='_', pair='TKN/BTC') + # Ensure backoff is called + assert tm.call_args_list[0][0][0] == 1 + assert tm.call_args_list[1][0][0] == 2 + assert tm.call_args_list[2][0][0] == 5 + assert tm.call_args_list[3][0][0] == 10 + assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1 + ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, 'fetch_order', 'fetch_order', order_id='_', pair='TKN/BTC') diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index ef48bdc34..654e6ca4f 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2078,7 +2078,7 @@ def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_ord 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), fetch_ticker=ticker, - fetch_order=MagicMock(side_effect=DependencyException), + fetch_order=MagicMock(side_effect=ExchangeError), cancel_order=cancel_order_mock, get_fee=fee ) From 4d9ecf137b5d2d037e888a56eda4a0bd78925f7c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 20:17:03 +0200 Subject: [PATCH 0174/1197] Fix failing test in python 3.7 can't use Magicmock in 3.7 (works in 3.8 though). --- freqtrade/exchange/common.py | 2 +- tests/exchange/test_exchange.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 9b7d8dfea..cc70bb875 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -110,7 +110,7 @@ def retrier_async(f): count -= 1 kwargs.update({'count': count}) logger.warning('retrying %s() still for %s times', f.__name__, count) - if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError): + if isinstance(ex, DDosProtection): await asyncio.sleep(calculate_backoff(count + 1, API_RETRY_COUNT)) return await wrapper(*args, **kwargs) else: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 66f88d82f..251f257f7 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -62,7 +62,7 @@ def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, retries=API_RETRY_COUNT + 1, **kwargs): - with patch('freqtrade.exchange.common.asyncio.sleep'): + with patch('freqtrade.exchange.common.asyncio.sleep', get_mock_coro(None)): with pytest.raises(DDosProtection): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.DDoSProtection("Dooh")) exchange = get_patched_exchange(mocker, default_conf, api_mock) From fe0b17c70c82e429afbdf5a17e4242275548ad2f Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2020 09:01:20 +0000 Subject: [PATCH 0175/1197] Bump progressbar2 from 3.51.3 to 3.51.4 Bumps [progressbar2](https://github.com/WoLpH/python-progressbar) from 3.51.3 to 3.51.4. - [Release notes](https://github.com/WoLpH/python-progressbar/releases) - [Changelog](https://github.com/WoLpH/python-progressbar/blob/develop/CHANGES.rst) - [Commits](https://github.com/WoLpH/python-progressbar/compare/v3.51.3...v3.51.4) 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 aedfc0eaa..2784bc156 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -7,4 +7,4 @@ scikit-learn==0.23.1 scikit-optimize==0.7.4 filelock==3.0.12 joblib==0.15.1 -progressbar2==3.51.3 +progressbar2==3.51.4 From e06b00921416acdd8f05ab9a770ebe0a21689254 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2020 09:02:57 +0000 Subject: [PATCH 0176/1197] Bump plotly from 4.8.1 to 4.8.2 Bumps [plotly](https://github.com/plotly/plotly.py) from 4.8.1 to 4.8.2. - [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.8.1...v4.8.2) 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 cb13a59bf..ec5af3dbf 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.8.1 +plotly==4.8.2 From 4e5910afba3a72efe0735a0f257b6328ff3c8f24 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2020 09:03:19 +0000 Subject: [PATCH 0177/1197] Bump mkdocs-material from 5.3.2 to 5.3.3 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 5.3.2 to 5.3.3. - [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/5.3.2...5.3.3) 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 7ddfc1dfb..a0505c84b 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==5.3.2 +mkdocs-material==5.3.3 mdx_truly_sane_lists==1.2 From be2b326a6e208d7f0835c7c899c6649764cdd924 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2020 09:03:58 +0000 Subject: [PATCH 0178/1197] Bump pytest-asyncio from 0.12.0 to 0.14.0 Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.12.0 to 0.14.0. - [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) - [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.12.0...v0.14.0) 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 91b33c573..7d68a0c3b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,7 +9,7 @@ flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 mypy==0.781 pytest==5.4.3 -pytest-asyncio==0.12.0 +pytest-asyncio==0.14.0 pytest-cov==2.10.0 pytest-mock==3.1.1 pytest-random-order==1.0.4 From 9e1ce0c67ab1dbb505a35d1c3e1e24e5b946e9c7 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2020 09:04:12 +0000 Subject: [PATCH 0179/1197] Bump sqlalchemy from 1.3.17 to 1.3.18 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.17 to 1.3.18. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) 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 916b0e24b..06549d2bf 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,7 +1,7 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs ccxt==1.30.34 -SQLAlchemy==1.3.17 +SQLAlchemy==1.3.18 python-telegram-bot==12.7 arrow==0.15.7 cachetools==4.1.0 From 449d4625336096658e1cbd39f8c9ed6ef9677af4 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2020 12:12:58 +0000 Subject: [PATCH 0180/1197] Bump python-telegram-bot from 12.7 to 12.8 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 12.7 to 12.8. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v12.7...v12.8) 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 06549d2bf..06eddb536 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -2,7 +2,7 @@ # mainly used for Raspberry pi installs ccxt==1.30.34 SQLAlchemy==1.3.18 -python-telegram-bot==12.7 +python-telegram-bot==12.8 arrow==0.15.7 cachetools==4.1.0 requests==2.24.0 From c06b2802882ec70b7f725ca39818212d75e0e1d5 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2020 12:14:27 +0000 Subject: [PATCH 0181/1197] Bump mypy from 0.781 to 0.782 Bumps [mypy](https://github.com/python/mypy) from 0.781 to 0.782. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.781...v0.782) 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 7d68a0c3b..ed4f8f713 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ coveralls==2.0.0 flake8==3.8.3 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 -mypy==0.781 +mypy==0.782 pytest==5.4.3 pytest-asyncio==0.14.0 pytest-cov==2.10.0 From 8fb1683bdc1237ec55f2f5a47379b424ab6a5ea2 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2020 12:32:42 +0000 Subject: [PATCH 0182/1197] Bump cachetools from 4.1.0 to 4.1.1 Bumps [cachetools](https://github.com/tkem/cachetools) from 4.1.0 to 4.1.1. - [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/v4.1.0...v4.1.1) 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 06eddb536..4c0f93ef7 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -4,7 +4,7 @@ ccxt==1.30.34 SQLAlchemy==1.3.18 python-telegram-bot==12.8 arrow==0.15.7 -cachetools==4.1.0 +cachetools==4.1.1 requests==2.24.0 urllib3==1.25.9 wrapt==1.12.1 From a9064117a5b35c7d6a2f63a0edce3c9188fa27de Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 29 Jun 2020 12:53:58 +0000 Subject: [PATCH 0183/1197] Bump ccxt from 1.30.34 to 1.30.48 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.30.34 to 1.30.48. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.30.34...1.30.48) 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 4c0f93ef7..2948b8f35 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.30.34 +ccxt==1.30.48 SQLAlchemy==1.3.18 python-telegram-bot==12.8 arrow==0.15.7 From b95065d70179318f1841708c64c7e333e763afb2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 29 Jun 2020 20:00:42 +0200 Subject: [PATCH 0184/1197] Log backoff --- freqtrade/exchange/common.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index cc70bb875..0610e8447 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -111,7 +111,9 @@ def retrier_async(f): kwargs.update({'count': count}) logger.warning('retrying %s() still for %s times', f.__name__, count) if isinstance(ex, DDosProtection): - await asyncio.sleep(calculate_backoff(count + 1, API_RETRY_COUNT)) + backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT) + logger.debug(f"Applying DDosProtection backoff delay: {backoff_delay}") + await asyncio.sleep(backoff_delay) return await wrapper(*args, **kwargs) else: logger.warning('Giving up retrying: %s()', f.__name__) @@ -134,7 +136,9 @@ def retrier(_func=None, retries=API_RETRY_COUNT): logger.warning('retrying %s() still for %s times', f.__name__, count) if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError): # increasing backoff - time.sleep(calculate_backoff(count + 1, retries)) + backoff_delay = calculate_backoff(count + 1, retries) + logger.debug(f"Applying DDosProtection backoff delay: {backoff_delay}") + time.sleep(backoff_delay) return wrapper(*args, **kwargs) else: logger.warning('Giving up retrying: %s()', f.__name__) From efd6e4a87535adb95fa4eb7710259dd130a7395e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 30 Jun 2020 07:16:08 +0200 Subject: [PATCH 0185/1197] Add test for check_for_open_trades --- tests/test_freqtradebot.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5d83c893e..d3014d7a8 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4029,3 +4029,19 @@ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limi freqtrade.cancel_all_open_orders() assert buy_mock.call_count == 1 assert sell_mock.call_count == 1 + + +@pytest.mark.usefixtures("init_persistence") +def test_check_for_open_trades(mocker, default_conf, fee, limit_buy_order, limit_sell_order): + freqtrade = get_patched_freqtradebot(mocker, default_conf) + + freqtrade.check_for_open_trades() + assert freqtrade.rpc.send_msg.call_count == 0 + + create_mock_trades(fee) + trade = Trade.query.first() + trade.is_open = True + + freqtrade.check_for_open_trades() + assert freqtrade.rpc.send_msg.call_count == 1 + assert 'Handle these trades manually' in freqtrade.rpc.send_msg.call_args[0][0]['status'] From 2f759825e4c670af51e4e17b074f96f48ff5ce16 Mon Sep 17 00:00:00 2001 From: Confucius-The-Great <40965179+qkum@users.noreply.github.com> Date: Tue, 30 Jun 2020 11:01:00 +0200 Subject: [PATCH 0186/1197] Update faq.md Major changes :) --- docs/faq.md | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 151b2c054..cc43e326d 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,5 +1,9 @@ # Freqtrade FAQ +## Beginner Tips & Tricks + +#1 When you work with your strategy & hyperopt file you should use a real programmer software like Pycharm. If you by accident moved some code and freqtrade says error and you cant find the place where you moved something, or you cant find line 180 where you messed something up. Then a program like Pycharm shows you where line 180 is in your strategy file so you can fix the problem, or Pycharm shows you with some color marking that "here is a line of code that does not belong here" and you found your error in no time! This will save you many hours of problemsolving when working with the bot. Pycharm also got a usefull "Debug" feature that can tell you exactly what command on that line is making the error :) + ## Freqtrade common issues ### The bot does not start @@ -15,10 +19,12 @@ This could have the following reasons: ### I have waited 5 minutes, why hasn't the bot made any trades yet?! -Depending on the buy strategy, the amount of whitelisted coins, the +#1 Depending on the buy strategy, the amount of whitelisted coins, the situation of the market etc, it can take up to hours to find good entry position for a trade. Be patient! +#2 Or it may because you made an human error? Like writing --dry-run when you wanted to trade live?. Maybe an error with the exchange API? Or something else. You will have to do the hard work of finding out the root cause of the problem :) + ### I have made 12 trades already, why is my total profit negative?! I understand your disappointment but unfortunately 12 trades is just @@ -129,25 +135,25 @@ to find a great result (unless if you are very lucky), so you probably have to run it for 10.000 or more. But it will take an eternity to compute. -We recommend you to run it at least 10.000 epochs: +We recommend you to run between 500-1000 epochs over and over untill you hit at least 10.000 epocs in total. You can best judge by looking at the results - if the bot keep discovering more profitable strategies or not. ```bash -freqtrade hyperopt -e 10000 +freqtrade hyperopt -e 1000 ``` or if you want intermediate result to see ```bash -for i in {1..100}; do freqtrade hyperopt -e 100; done +for i in {1..100}; do freqtrade hyperopt -e 1000; done ``` -### Why it is so long to run hyperopt? +### Why does it take so long time to run hyperopt? -Finding a great Hyperopt results takes time. +#1 Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Github page, join the Freqtrade Discord - or something totally else. While you patiently wait for the most advanced, public known, crypto bot, in the world, to hand you a possible golden strategy specially designed just for you =) -If you wonder why it takes a while to find great hyperopt results +#2 If you wonder why it can take from 20 minutes to days to do 1000 epocs here are some answers: -This answer was written during the under the release 0.15.1, when we had: +This answer was written during the release 0.15.1, when we had: - 8 triggers - 9 guards: let's say we evaluate even 10 values from each @@ -157,7 +163,10 @@ The following calculation is still very rough and not very precise but it will give the idea. With only these triggers and guards there is already 8\*10^9\*10 evaluations. A roughly total of 80 billion evals. Did you run 100 000 evals? Congrats, you've done roughly 1 / 100 000 th -of the search space. +of the search space. If we assume that the bot never test the same strategy more than once. + +#3 The time it takes to run 1000 hyperopt epocs depends on things like: The cpu, harddisk, ram, motherboard, indicator settings, indicator count, amount of coins that hyperopt test strategies on, trade count - can be 650 trades in a year or 10.0000 trades depending on if the strategy aims for a high profit rarely or a low profit many many many times. Example: 4% profit 650 times vs 0,3% profit a trade 10.000 times in a year. If we assume you set the --timerange to 365 days. +Example: freqtrade --config config_mcd_1.json --strategy mcd_1 --hyperopt mcd_hyperopt_1 -e 1000 --timerange 20190601-20200601 ## Edge module From 61ae471eef74bd18619ce26710d5650e06d05ccb Mon Sep 17 00:00:00 2001 From: HumanBot Date: Tue, 30 Jun 2020 10:13:27 -0400 Subject: [PATCH 0187/1197] fixed --export trades command refers to issue 3413 @ https://github.com/freqtrade/freqtrade/issues/3413 --- docs/backtesting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 51b2e953b..ecd48bdc9 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -66,7 +66,7 @@ Where `SampleStrategy1` and `AwesomeStrategy` refer to class names of strategies #### Exporting trades to file ```bash -freqtrade backtesting --export trades +freqtrade backtesting --export trades --config config.json --strategy SampleStrategy ``` The exported trades can be used for [further analysis](#further-backtest-result-analysis), or can be used by the plotting script `plot_dataframe.py` in the scripts directory. From 81850b5fdf5b860db1802fbda0998a2d1662b4dd Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Thu, 2 Jul 2020 11:26:52 +0200 Subject: [PATCH 0188/1197] AgeFilter add actual amount of days in log message (debug info) --- freqtrade/pairlist/AgeFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index a23682599..b489a59bc 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -69,7 +69,7 @@ class AgeFilter(IPairList): return True else: self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " - f"because age is less than " + f"because age {len(daily_candles)} is less than " f"{self._min_days_listed} " f"{plural(self._min_days_listed, 'day')}") return False From 99ac2659f3566526ebbecf95af76a5030871f5fc Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Thu, 2 Jul 2020 11:27:33 +0200 Subject: [PATCH 0189/1197] Init FIAT converter in api_server.py --- freqtrade/rpc/api_server.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index a2cef9a98..351842e10 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -17,6 +17,7 @@ from werkzeug.serving import make_server from freqtrade.__init__ import __version__ from freqtrade.rpc.rpc import RPC, RPCException +from freqtrade.rpc.fiat_convert import CryptoToFiatConverter logger = logging.getLogger(__name__) @@ -105,6 +106,9 @@ class ApiServer(RPC): # Register application handling self.register_rest_rpc_urls() + if self._config.get('fiat_display_currency', None): + self._fiat_converter = CryptoToFiatConverter() + thread = threading.Thread(target=self.run, daemon=True) thread.start() From db965332b99622fa93ae1638a94cf49688bd1e81 Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Thu, 2 Jul 2020 11:38:38 +0200 Subject: [PATCH 0190/1197] Update tests for AgeFilter message --- tests/pairlist/test_pairlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 072e497f3..a2644fe8c 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -389,7 +389,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t for pairlist in pairlists: if pairlist['method'] == 'AgeFilter' and pairlist['min_days_listed'] and \ len(ohlcv_history_list) <= pairlist['min_days_listed']: - assert log_has_re(r'^Removed .* from whitelist, because age is less than ' + assert log_has_re(r'^Removed .* from whitelist, because age .* is less than ' r'.* day.*', caplog) if pairlist['method'] == 'PrecisionFilter' and whitelist_result: assert log_has_re(r'^Removed .* from whitelist, because stop price .* ' From 39fa58973527431b5c66040d2b44f8f184abe788 Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Thu, 2 Jul 2020 13:39:02 +0200 Subject: [PATCH 0191/1197] Update API test, currently just with 'ANY' --- tests/rpc/test_rpc_apiserver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index cd2b0d311..2935094a5 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -431,14 +431,14 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li 'latest_trade_date': 'just now', 'latest_trade_timestamp': ANY, 'profit_all_coin': 6.217e-05, - 'profit_all_fiat': 0, + 'profit_all_fiat': ANY, 'profit_all_percent': 6.2, 'profit_all_percent_mean': 6.2, 'profit_all_ratio_mean': 0.06201058, 'profit_all_percent_sum': 6.2, 'profit_all_ratio_sum': 0.06201058, 'profit_closed_coin': 6.217e-05, - 'profit_closed_fiat': 0, + 'profit_closed_fiat': ANY, 'profit_closed_percent': 6.2, 'profit_closed_ratio_mean': 0.06201058, 'profit_closed_percent_mean': 6.2, From f32e522bd73579d532541a7b444ced723a65e159 Mon Sep 17 00:00:00 2001 From: Theagainmen <24569139+Theagainmen@users.noreply.github.com> Date: Thu, 2 Jul 2020 20:03:15 +0200 Subject: [PATCH 0192/1197] Update API test, removed 'ANY' --- tests/rpc/test_rpc_apiserver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 2935094a5..45aa57588 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -431,14 +431,14 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li 'latest_trade_date': 'just now', 'latest_trade_timestamp': ANY, 'profit_all_coin': 6.217e-05, - 'profit_all_fiat': ANY, + 'profit_all_fiat': 0.76748865, 'profit_all_percent': 6.2, 'profit_all_percent_mean': 6.2, 'profit_all_ratio_mean': 0.06201058, 'profit_all_percent_sum': 6.2, 'profit_all_ratio_sum': 0.06201058, 'profit_closed_coin': 6.217e-05, - 'profit_closed_fiat': ANY, + 'profit_closed_fiat': 0.76748865, 'profit_closed_percent': 6.2, 'profit_closed_ratio_mean': 0.06201058, 'profit_closed_percent_mean': 6.2, From 23c0db925e22652b7e53b758bb21f0e6e1896600 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste LE STANG Date: Thu, 2 Jul 2020 20:55:16 +0200 Subject: [PATCH 0193/1197] Adding a dataprovider to the strategy before plotting --- freqtrade/plot/plotting.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index e8b0b4938..ae9f9c409 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -15,6 +15,7 @@ from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_prev_date from freqtrade.misc import pair_to_filename from freqtrade.resolvers import StrategyResolver +from freqtrade.data.dataprovider import DataProvider logger = logging.getLogger(__name__) @@ -474,6 +475,7 @@ def load_and_plot_trades(config: Dict[str, Any]): pair_counter += 1 logger.info("analyse pair %s", pair) + strategy.dp = DataProvider(config,config["exchange"]) df_analyzed = strategy.analyze_ticker(data, {'pair': pair}) trades_pair = trades.loc[trades['pair'] == pair] trades_pair = extract_trades_of_period(df_analyzed, trades_pair) From 20e8a29262e190f5be2d5f792a7bbaf03d6a2c73 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste LE STANG Date: Thu, 2 Jul 2020 20:55:16 +0200 Subject: [PATCH 0194/1197] Adding a dataprovider to the strategy before plotting Fix flake8 --- freqtrade/plot/plotting.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index e8b0b4938..db83448c0 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -15,6 +15,7 @@ from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_prev_date from freqtrade.misc import pair_to_filename from freqtrade.resolvers import StrategyResolver +from freqtrade.data.dataprovider import DataProvider logger = logging.getLogger(__name__) @@ -474,6 +475,7 @@ def load_and_plot_trades(config: Dict[str, Any]): pair_counter += 1 logger.info("analyse pair %s", pair) + strategy.dp = DataProvider(config, config["exchange"]) df_analyzed = strategy.analyze_ticker(data, {'pair': pair}) trades_pair = trades.loc[trades['pair'] == pair] trades_pair = extract_trades_of_period(df_analyzed, trades_pair) From 455b26ea48975178664d8da76c8b6d9fbbdf60b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Jun 2020 06:37:30 +0200 Subject: [PATCH 0195/1197] Add max drawdown to backtesting --- freqtrade/optimize/optimize_reports.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index d89860a73..5be1a47a9 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -6,6 +6,7 @@ from typing import Any, Dict, List from pandas import DataFrame from tabulate import tabulate +from freqtrade.data.btanalysis import calculate_max_drawdown from freqtrade.misc import file_dump_json logger = logging.getLogger(__name__) @@ -212,11 +213,20 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], max_open_trades=max_open_trades, results=results.loc[results['open_at_end']], skip_nan=True) + + max_drawdown, drawdown_start, drawdown_end = calculate_max_drawdown( + results, value_col='profit_percent') + strat_stats = { 'trades': backtest_result_to_list(results), 'results_per_pair': pair_results, 'sell_reason_summary': sell_reason_stats, 'left_open_trades': left_open_results, + 'max_drawdown': max_drawdown, + 'drawdown_start': drawdown_start, + 'drawdown_start_ts': drawdown_start.timestamp(), + 'drawdown_end': drawdown_end, + 'drawdown_end_ts': drawdown_end.timestamp(), } result['strategy'][strategy] = strat_stats @@ -298,6 +308,16 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") +def text_table_add_metrics(strategy_results: Dict) -> str: + xxx = [ + ('Max Drawdown', f"{round(strategy_results['max_drawdown'] * 100, 2)}%"), + ('Drawdown Start', strategy_results['drawdown_start'].strftime('%Y-%m-%d %H:%M:%S')), + ('Drawdown End', strategy_results['drawdown_end'].strftime('%Y-%m-%d %H:%M:%S')), + ] + + return tabulate(xxx, headers=["Metric", "Value"], tablefmt="orgtbl") + + def show_backtest_results(config: Dict, backtest_stats: Dict): stake_currency = config['stake_currency'] @@ -320,6 +340,12 @@ def show_backtest_results(config: Dict, backtest_stats: Dict): if isinstance(table, str): print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) print(table) + + table = text_table_add_metrics(results) + if isinstance(table, str): + print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '=')) + print(table) + if isinstance(table, str): print('=' * len(table.splitlines()[0])) print() From 6922fbc3aae545e4ef3020610e819199770171f7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 8 Jun 2020 06:38:29 +0200 Subject: [PATCH 0196/1197] Add max_drawdown error handler --- freqtrade/optimize/optimize_reports.py | 44 +++++++++++++++++--------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 5be1a47a9..0188761d4 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -1,5 +1,5 @@ import logging -from datetime import timedelta +from datetime import timedelta, datetime from pathlib import Path from typing import Any, Dict, List @@ -214,22 +214,33 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], results=results.loc[results['open_at_end']], skip_nan=True) - max_drawdown, drawdown_start, drawdown_end = calculate_max_drawdown( - results, value_col='profit_percent') - strat_stats = { 'trades': backtest_result_to_list(results), 'results_per_pair': pair_results, 'sell_reason_summary': sell_reason_stats, 'left_open_trades': left_open_results, - 'max_drawdown': max_drawdown, - 'drawdown_start': drawdown_start, - 'drawdown_start_ts': drawdown_start.timestamp(), - 'drawdown_end': drawdown_end, - 'drawdown_end_ts': drawdown_end.timestamp(), } result['strategy'][strategy] = strat_stats + try: + max_drawdown, drawdown_start, drawdown_end = calculate_max_drawdown( + results, value_col='profit_percent') + strat_stats.update({ + 'max_drawdown': max_drawdown, + 'drawdown_start': drawdown_start, + 'drawdown_start_ts': drawdown_start.timestamp(), + 'drawdown_end': drawdown_end, + 'drawdown_end_ts': drawdown_end.timestamp(), + }) + except ValueError: + strat_stats.update({ + 'max_drawdown': 0.0, + 'drawdown_start': datetime.min, + 'drawdown_start_ts': datetime.min.timestamp(), + 'drawdown_end': datetime.min, + 'drawdown_end_ts': datetime.min.timestamp(), + }) + strategy_results = generate_strategy_metrics(stake_currency=stake_currency, max_open_trades=max_open_trades, all_results=all_results) @@ -309,13 +320,16 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: def text_table_add_metrics(strategy_results: Dict) -> str: - xxx = [ - ('Max Drawdown', f"{round(strategy_results['max_drawdown'] * 100, 2)}%"), - ('Drawdown Start', strategy_results['drawdown_start'].strftime('%Y-%m-%d %H:%M:%S')), - ('Drawdown End', strategy_results['drawdown_end'].strftime('%Y-%m-%d %H:%M:%S')), - ] + if len(strategy_results['trades']) > 0: + metrics = [ + ('Max Drawdown', f"{round(strategy_results['max_drawdown'] * 100, 2)}%"), + ('Drawdown Start', strategy_results['drawdown_start'].strftime('%Y-%m-%d %H:%M:%S')), + ('Drawdown End', strategy_results['drawdown_end'].strftime('%Y-%m-%d %H:%M:%S')), + ] - return tabulate(xxx, headers=["Metric", "Value"], tablefmt="orgtbl") + return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl") + else: + return def show_backtest_results(config: Dict, backtest_stats: Dict): From cbcf3dbb43222e4863d7dacc27a726a8b261a43f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Jun 2020 08:00:35 +0200 Subject: [PATCH 0197/1197] Add more metrics to summarytable --- freqtrade/optimize/backtesting.py | 3 ++- freqtrade/optimize/optimize_reports.py | 26 ++++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index e5014dd5a..c7a3515b5 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -417,5 +417,6 @@ class Backtesting: if self.config.get('export', False): store_backtest_result(self.config['exportfilename'], all_results) # Show backtest results - stats = generate_backtest_stats(self.config, data, all_results) + stats = generate_backtest_stats(self.config, data, all_results, + min_date=min_date, max_date=max_date) show_backtest_results(self.config, stats) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 0188761d4..dc3b52377 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -1,8 +1,9 @@ import logging -from datetime import timedelta, datetime +from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any, Dict, List +from arrow import Arrow from pandas import DataFrame from tabulate import tabulate @@ -191,11 +192,15 @@ def generate_edge_table(results: dict) -> str: def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], - all_results: Dict[str, DataFrame]) -> Dict[str, Any]: + all_results: Dict[str, DataFrame], + min_date: Arrow, max_date: Arrow + ) -> Dict[str, Any]: """ :param config: Configuration object used for backtest :param btdata: Backtest data :param all_results: backtest result - dictionary with { Strategy: results}. + :param min_date: Backtest start date + :param max_date: Backtest end date :return: Dictionary containing results per strategy and a stratgy summary. """ @@ -214,11 +219,19 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], results=results.loc[results['open_at_end']], skip_nan=True) + backtest_days = (max_date - min_date).days strat_stats = { 'trades': backtest_result_to_list(results), 'results_per_pair': pair_results, 'sell_reason_summary': sell_reason_stats, 'left_open_trades': left_open_results, + 'total_trades': len(results), + 'backtest_start': min_date.datetime, + 'backtest_start_ts': min_date.timestamp, + 'backtest_end': max_date.datetime, + 'backtest_end_ts': max_date.timestamp, + 'backtest_days': backtest_days, + 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else None } result['strategy'][strategy] = strat_stats @@ -321,7 +334,16 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: def text_table_add_metrics(strategy_results: Dict) -> str: if len(strategy_results['trades']) > 0: + min_trade = min(strategy_results['trades'], key=lambda x: x[2]) metrics = [ + ('Total trades', strategy_results['total_trades']), + ('First trade', datetime.fromtimestamp(min_trade[2], + tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S')), + ('First trade Pair', min_trade[0]), + ('Backtesting from', strategy_results['backtest_start'].strftime('%Y-%m-%d %H:%M:%S')), + ('Backtesting to', strategy_results['backtest_end'].strftime('%Y-%m-%d %H:%M:%S')), + ('Trades per day', strategy_results['trades_per_day']), + ('', ''), # Empty line to improve readability ('Max Drawdown', f"{round(strategy_results['max_drawdown'] * 100, 2)}%"), ('Drawdown Start', strategy_results['drawdown_start'].strftime('%Y-%m-%d %H:%M:%S')), ('Drawdown End', strategy_results['drawdown_end'].strftime('%Y-%m-%d %H:%M:%S')), From fbddfaeacf3d861da6c4b2023215fd31820bd992 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Jun 2020 08:07:34 +0200 Subject: [PATCH 0198/1197] Introduce DatetimePrintFormat --- freqtrade/constants.py | 1 + freqtrade/edge/edge_positioning.py | 11 ++++------- freqtrade/optimize/backtesting.py | 16 ++++++++-------- freqtrade/optimize/hyperopt.py | 9 +++++---- freqtrade/optimize/optimize_reports.py | 11 ++++++----- freqtrade/rpc/api_server.py | 3 ++- 6 files changed, 26 insertions(+), 25 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 2cfff07cd..ccb05a60f 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -26,6 +26,7 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'ShuffleFilter', 'SpreadFilter'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz'] DRY_RUN_WALLET = 1000 +DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S' MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume'] # Don't modify sequence of DEFAULT_TRADES_COLUMNS diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 41252ee51..dd2f44f23 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -9,7 +9,7 @@ import utils_find_1st as utf1st from pandas import DataFrame from freqtrade.configuration import TimeRange -from freqtrade.constants import UNLIMITED_STAKE_AMOUNT +from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, DATETIME_PRINT_FORMAT from freqtrade.exceptions import OperationalException from freqtrade.data.history import get_timerange, load_data, refresh_data from freqtrade.strategy.interface import SellType @@ -121,12 +121,9 @@ class Edge: # Print timeframe min_date, max_date = get_timerange(preprocessed) - logger.info( - 'Measuring data from %s up to %s (%s days) ...', - min_date.isoformat(), - max_date.isoformat(), - (max_date - min_date).days - ) + logger.info(f'Measuring data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' + f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' + f'({(max_date - min_date).days} days)..') headers = ['date', 'buy', 'open', 'close', 'sell', 'high', 'low'] trades: list = [] diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index c7a3515b5..847434789 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -11,6 +11,7 @@ from typing import Any, Dict, List, NamedTuple, Optional, Tuple import arrow from pandas import DataFrame +from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.configuration import (TimeRange, remove_credentials, validate_config_consistency) from freqtrade.data import history @@ -137,10 +138,10 @@ class Backtesting: min_date, max_date = history.get_timerange(data) - logger.info( - 'Loading data from %s up to %s (%s days)..', - min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days - ) + logger.info(f'Loading data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' + f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' + f'({(max_date - min_date).days} days)..') + # Adjust startts forward if not enough data is available timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe), self.required_startup, min_date) @@ -400,10 +401,9 @@ class Backtesting: preprocessed[pair] = trim_dataframe(df, timerange) min_date, max_date = history.get_timerange(preprocessed) - logger.info( - 'Backtesting with data from %s up to %s (%s days)..', - min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days - ) + logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' + f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' + f'({(max_date - min_date).days} days)..') # Execute backtest and print results all_results[self.strategy.get_strategy_name()] = self.backtest( processed=preprocessed, diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 153ae3861..69dff463b 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -25,6 +25,7 @@ import tabulate from os import path import io +from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.data.converter import trim_dataframe from freqtrade.data.history import get_timerange from freqtrade.exceptions import OperationalException @@ -625,10 +626,10 @@ class Hyperopt: preprocessed[pair] = trim_dataframe(df, timerange) min_date, max_date = get_timerange(data) - logger.info( - 'Hyperopting with data from %s up to %s (%s days)..', - min_date.isoformat(), max_date.isoformat(), (max_date - min_date).days - ) + logger.info(f'Hyperopting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' + f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' + f'({(max_date - min_date).days} days)..') + dump(preprocessed, self.data_pickle_file) # We don't need exchange instance anymore while running hyperopt diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index dc3b52377..6efda2956 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -7,6 +7,7 @@ from arrow import Arrow from pandas import DataFrame from tabulate import tabulate +from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.data.btanalysis import calculate_max_drawdown from freqtrade.misc import file_dump_json @@ -338,15 +339,15 @@ def text_table_add_metrics(strategy_results: Dict) -> str: metrics = [ ('Total trades', strategy_results['total_trades']), ('First trade', datetime.fromtimestamp(min_trade[2], - tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S')), + tz=timezone.utc).strftime(DATETIME_PRINT_FORMAT)), ('First trade Pair', min_trade[0]), - ('Backtesting from', strategy_results['backtest_start'].strftime('%Y-%m-%d %H:%M:%S')), - ('Backtesting to', strategy_results['backtest_end'].strftime('%Y-%m-%d %H:%M:%S')), + ('Backtesting from', strategy_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)), + ('Backtesting to', strategy_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)), ('Trades per day', strategy_results['trades_per_day']), ('', ''), # Empty line to improve readability ('Max Drawdown', f"{round(strategy_results['max_drawdown'] * 100, 2)}%"), - ('Drawdown Start', strategy_results['drawdown_start'].strftime('%Y-%m-%d %H:%M:%S')), - ('Drawdown End', strategy_results['drawdown_end'].strftime('%Y-%m-%d %H:%M:%S')), + ('Drawdown Start', strategy_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)), + ('Drawdown End', strategy_results['drawdown_end'].strftime(DATETIME_PRINT_FORMAT)), ] return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl") diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index a2cef9a98..e86a4783f 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -16,6 +16,7 @@ from werkzeug.security import safe_str_cmp from werkzeug.serving import make_server from freqtrade.__init__ import __version__ +from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.rpc.rpc import RPC, RPCException logger = logging.getLogger(__name__) @@ -31,7 +32,7 @@ class ArrowJSONEncoder(JSONEncoder): elif isinstance(obj, date): return obj.strftime("%Y-%m-%d") elif isinstance(obj, datetime): - return obj.strftime("%Y-%m-%d %H:%M:%S") + return obj.strftime(DATETIME_PRINT_FORMAT) iterable = iter(obj) except TypeError: pass From cf044d166edc68ba427d8c3b86f13ebb252a490b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 9 Jun 2020 08:14:18 +0200 Subject: [PATCH 0199/1197] Tests should use new Datetime format too --- freqtrade/optimize/optimize_reports.py | 4 ++-- tests/optimize/test_backtesting.py | 28 +++++++++++++------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 6efda2956..1e2d41e32 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -250,9 +250,9 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], strat_stats.update({ 'max_drawdown': 0.0, 'drawdown_start': datetime.min, - 'drawdown_start_ts': datetime.min.timestamp(), + 'drawdown_start_ts': datetime(1970, 1, 1).timestamp(), 'drawdown_end': datetime.min, - 'drawdown_end_ts': datetime.min.timestamp(), + 'drawdown_end_ts': datetime(1970, 1, 1).timestamp(), }) strategy_results = generate_strategy_metrics(stake_currency=stake_currency, diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 67da38648..853780b82 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -349,8 +349,8 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: exists = [ 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', - 'Backtesting with data from 2017-11-14T21:17:00+00:00 ' - 'up to 2017-11-14T22:59:00+00:00 (0 days)..' + 'Backtesting with data from 2017-11-14 21:17:00 ' + 'up to 2017-11-14 22:59:00 (0 days)..' ] for line in exists: assert log_has(line, caplog) @@ -672,10 +672,10 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): f'Using data directory: {testdatadir} ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', - 'Loading data from 2017-11-14T20:57:00+00:00 ' - 'up to 2017-11-14T22:58:00+00:00 (0 days)..', - 'Backtesting with data from 2017-11-14T21:17:00+00:00 ' - 'up to 2017-11-14T22:58:00+00:00 (0 days)..', + 'Loading data from 2017-11-14 20:57:00 ' + 'up to 2017-11-14 22:58:00 (0 days)..', + 'Backtesting with data from 2017-11-14 21:17:00 ' + 'up to 2017-11-14 22:58:00 (0 days)..', 'Parameter --enable-position-stacking detected ...' ] @@ -735,10 +735,10 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): f'Using data directory: {testdatadir} ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', - 'Loading data from 2017-11-14T20:57:00+00:00 ' - 'up to 2017-11-14T22:58:00+00:00 (0 days)..', - 'Backtesting with data from 2017-11-14T21:17:00+00:00 ' - 'up to 2017-11-14T22:58:00+00:00 (0 days)..', + 'Loading data from 2017-11-14 20:57:00 ' + 'up to 2017-11-14 22:58:00 (0 days)..', + 'Backtesting with data from 2017-11-14 21:17:00 ' + 'up to 2017-11-14 22:58:00 (0 days)..', 'Parameter --enable-position-stacking detected ...', 'Running backtesting for Strategy DefaultStrategy', 'Running backtesting for Strategy TestStrategyLegacy', @@ -818,10 +818,10 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat f'Using data directory: {testdatadir} ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', - 'Loading data from 2017-11-14T20:57:00+00:00 ' - 'up to 2017-11-14T22:58:00+00:00 (0 days)..', - 'Backtesting with data from 2017-11-14T21:17:00+00:00 ' - 'up to 2017-11-14T22:58:00+00:00 (0 days)..', + 'Loading data from 2017-11-14 20:57:00 ' + 'up to 2017-11-14 22:58:00 (0 days)..', + 'Backtesting with data from 2017-11-14 21:17:00 ' + 'up to 2017-11-14 22:58:00 (0 days)..', 'Parameter --enable-position-stacking detected ...', 'Running backtesting for Strategy DefaultStrategy', 'Running backtesting for Strategy TestStrategyLegacy', From 5fce7f3b22e50b23944737f60758178a428c133b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 25 Jun 2020 20:39:55 +0200 Subject: [PATCH 0200/1197] Add market Change closes #2524 and #3518 --- freqtrade/data/btanalysis.py | 20 ++++++++++++++++++++ freqtrade/optimize/optimize_reports.py | 12 ++++++++---- tests/data/test_btanalysis.py | 9 +++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index b169850ba..8601f8176 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -168,6 +168,26 @@ def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame, return trades +def calculate_market_change(data: Dict[str, pd.DataFrame], column: str = "close") -> float: + """ + Calculate market change based on "column". + Calculation is done by taking the first non-null and the last non-null element of each column + and calculating the pctchange as "(last - first) / first". + Then the results per pair are combined as mean. + + :param data: Dict of Dataframes, dict key should be pair. + :param column: Column in the original dataframes to use + :return: + """ + tmp_means = [] + for pair, df in data.items(): + start = df[column].dropna().iloc[0] + end = df[column].dropna().iloc[-1] + tmp_means.append((end - start) / start) + + return np.mean(tmp_means) + + def combine_dataframes_with_mean(data: Dict[str, pd.DataFrame], column: str = "close") -> pd.DataFrame: """ diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 1e2d41e32..4ec3dc3ad 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -8,7 +8,7 @@ from pandas import DataFrame from tabulate import tabulate from freqtrade.constants import DATETIME_PRINT_FORMAT -from freqtrade.data.btanalysis import calculate_max_drawdown +from freqtrade.data.btanalysis import calculate_max_drawdown, calculate_market_change from freqtrade.misc import file_dump_json logger = logging.getLogger(__name__) @@ -208,6 +208,8 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], stake_currency = config['stake_currency'] max_open_trades = config['max_open_trades'] result: Dict[str, Any] = {'strategy': {}} + market_change = calculate_market_change(btdata, 'close') + for strategy, results in all_results.items(): pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, @@ -232,8 +234,9 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], 'backtest_end': max_date.datetime, 'backtest_end_ts': max_date.timestamp, 'backtest_days': backtest_days, - 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else None - } + 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else None, + 'market_change': market_change, + } result['strategy'][strategy] = strat_stats try: @@ -348,11 +351,12 @@ def text_table_add_metrics(strategy_results: Dict) -> str: ('Max Drawdown', f"{round(strategy_results['max_drawdown'] * 100, 2)}%"), ('Drawdown Start', strategy_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)), ('Drawdown End', strategy_results['drawdown_end'].strftime(DATETIME_PRINT_FORMAT)), + ('Market change', f"{round(strategy_results['market_change'] * 100, 2)}%"), ] return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl") else: - return + return '' def show_backtest_results(config: Dict, backtest_stats: Dict): diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index b65db7fd8..d571569b9 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -8,6 +8,7 @@ from pandas import DataFrame, DateOffset, Timestamp, to_datetime from freqtrade.configuration import TimeRange from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, analyze_trade_parallelism, + calculate_market_change, calculate_max_drawdown, combine_dataframes_with_mean, create_cum_profit, @@ -135,6 +136,14 @@ def test_load_trades(default_conf, mocker): assert bt_mock.call_count == 0 +def test_calculate_market_change(testdatadir): + pairs = ["ETH/BTC", "ADA/BTC"] + data = load_data(datadir=testdatadir, pairs=pairs, timeframe='5m') + result = calculate_market_change(data) + assert isinstance(result, float) + assert pytest.approx(result) == 0.00955514 + + def test_combine_dataframes_with_mean(testdatadir): pairs = ["ETH/BTC", "ADA/BTC"] data = load_data(datadir=testdatadir, pairs=pairs, timeframe='5m') From 480c5117f1d41315eb9742bd64b028363b68be13 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Jun 2020 06:47:04 +0200 Subject: [PATCH 0201/1197] Handle empty return strings --- freqtrade/optimize/optimize_reports.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 4ec3dc3ad..9feb7f20d 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -373,21 +373,21 @@ def show_backtest_results(config: Dict, backtest_stats: Dict): table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'], stake_currency=stake_currency) - if isinstance(table, str): + if isinstance(table, str) and len(table) > 0: print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '=')) print(table) table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency) - if isinstance(table, str): + if isinstance(table, str) and len(table) > 0: print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '=')) print(table) table = text_table_add_metrics(results) - if isinstance(table, str): + if isinstance(table, str) and len(table) > 0: print(' SUMMARY METRICS '.center(len(table.splitlines()[0]), '=')) print(table) - if isinstance(table, str): + if isinstance(table, str) and len(table) > 0: print('=' * len(table.splitlines()[0])) print() From 81c8e8677dfa98f18661259389972bd844c111ef Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Jun 2020 06:56:59 +0200 Subject: [PATCH 0202/1197] use 0 as profit mean, not nan --- freqtrade/optimize/optimize_reports.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 9feb7f20d..b334917ba 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -69,8 +69,8 @@ def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: return { 'key': first_column, 'trades': len(result), - 'profit_mean': result['profit_percent'].mean(), - 'profit_mean_pct': result['profit_percent'].mean() * 100.0, + 'profit_mean': result['profit_percent'].mean() if len(result) > 0 else 0.0, + 'profit_mean_pct': result['profit_percent'].mean() * 100.0 if len(result) > 0 else 0.0, 'profit_sum': result['profit_percent'].sum(), 'profit_sum_pct': result['profit_percent'].sum() * 100.0, 'profit_total_abs': result['profit_abs'].sum(), From 415853583bf39b3a62a84e24f60bc74b5addae3a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Jun 2020 07:46:59 +0200 Subject: [PATCH 0203/1197] Save backtest-stats --- freqtrade/data/btanalysis.py | 19 ++++++++++++++++++- freqtrade/optimize/backtesting.py | 6 ++++-- freqtrade/optimize/optimize_reports.py | 21 ++++++++++++++++----- 3 files changed, 38 insertions(+), 8 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 8601f8176..ecedc55db 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -3,7 +3,7 @@ Helpers when analyzing backtest data """ import logging from pathlib import Path -from typing import Dict, Union, Tuple +from typing import Dict, Union, Tuple, Any import numpy as np import pandas as pd @@ -20,6 +20,23 @@ BT_DATA_COLUMNS = ["pair", "profit_percent", "open_time", "close_time", "index", "open_rate", "close_rate", "open_at_end", "sell_reason"] +def load_backtest_stats(filename: Union[Path, str]) -> Dict[str, Any]: + """ + Load backtest statistics file. + :param filename: pathlib.Path object, or string pointing to the file. + :return: a dictionary containing the resulting file. + """ + if isinstance(filename, str): + filename = Path(filename) + if not filename.is_file(): + raise ValueError(f"File {filename} does not exist.") + + with filename.open() as file: + data = json_load(file) + + return data + + def load_backtest_data(filename: Union[Path, str]) -> pd.DataFrame: """ Load backtest data file. diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 847434789..e4df80a82 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -11,9 +11,9 @@ from typing import Any, Dict, List, NamedTuple, Optional, Tuple import arrow from pandas import DataFrame -from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.configuration import (TimeRange, remove_credentials, validate_config_consistency) +from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.data import history from freqtrade.data.converter import trim_dataframe from freqtrade.data.dataprovider import DataProvider @@ -21,7 +21,8 @@ from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, - store_backtest_result) + store_backtest_result, + store_backtest_stats) from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -420,3 +421,4 @@ class Backtesting: stats = generate_backtest_stats(self.config, data, all_results, min_date=min_date, max_date=max_date) show_backtest_results(self.config, stats) + store_backtest_stats(self.config['exportfilename'], stats) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index b334917ba..3c0bfcb96 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -14,6 +14,18 @@ from freqtrade.misc import file_dump_json logger = logging.getLogger(__name__) +def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> None: + + filename = Path.joinpath(recordfilename.parent, + f'{recordfilename.stem}-{datetime.now().isoformat()}' + ).with_suffix(recordfilename.suffix) + file_dump_json(filename, stats) + + latest_filename = Path.joinpath(recordfilename.parent, + '.last_result.json') + file_dump_json(latest_filename, {'latest_backtest': str(filename.name)}) + + def store_backtest_result(recordfilename: Path, all_results: Dict[str, DataFrame]) -> None: """ Stores backtest results to file (one file per strategy) @@ -224,7 +236,7 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], backtest_days = (max_date - min_date).days strat_stats = { - 'trades': backtest_result_to_list(results), + 'trades': results.to_dict(orient='records'), 'results_per_pair': pair_results, 'sell_reason_summary': sell_reason_stats, 'left_open_trades': left_open_results, @@ -338,12 +350,11 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: def text_table_add_metrics(strategy_results: Dict) -> str: if len(strategy_results['trades']) > 0: - min_trade = min(strategy_results['trades'], key=lambda x: x[2]) + min_trade = min(strategy_results['trades'], key=lambda x: x['open_time']) metrics = [ ('Total trades', strategy_results['total_trades']), - ('First trade', datetime.fromtimestamp(min_trade[2], - tz=timezone.utc).strftime(DATETIME_PRINT_FORMAT)), - ('First trade Pair', min_trade[0]), + ('First trade', min_trade['open_time'].strftime(DATETIME_PRINT_FORMAT)), + ('First trade Pair', min_trade['pair']), ('Backtesting from', strategy_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)), ('Backtesting to', strategy_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)), ('Trades per day', strategy_results['trades_per_day']), From b068e7c564021ba178c969602461e9d3f57d1790 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Jun 2020 09:19:44 +0200 Subject: [PATCH 0204/1197] Rename open_time and close_time to *date --- freqtrade/data/btanalysis.py | 28 +++++++++---------- freqtrade/edge/edge_positioning.py | 6 ++-- freqtrade/optimize/backtesting.py | 19 +++++++------ .../optimize/hyperopt_loss_sharpe_daily.py | 2 +- .../optimize/hyperopt_loss_sortino_daily.py | 2 +- freqtrade/optimize/optimize_reports.py | 8 +++--- freqtrade/plot/plotting.py | 10 +++---- 7 files changed, 38 insertions(+), 37 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index ecedc55db..f174b8ea9 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -16,7 +16,7 @@ from freqtrade.persistence import Trade logger = logging.getLogger(__name__) # must align with columns in backtest.py -BT_DATA_COLUMNS = ["pair", "profit_percent", "open_time", "close_time", "index", "duration", +BT_DATA_COLUMNS = ["pair", "profit_percent", "open_date", "close_date", "index", "duration", "open_rate", "close_rate", "open_at_end", "sell_reason"] @@ -54,18 +54,18 @@ def load_backtest_data(filename: Union[Path, str]) -> pd.DataFrame: df = pd.DataFrame(data, columns=BT_DATA_COLUMNS) - df['open_time'] = pd.to_datetime(df['open_time'], + df['open_date'] = pd.to_datetime(df['open_date'], unit='s', utc=True, infer_datetime_format=True ) - df['close_time'] = pd.to_datetime(df['close_time'], + df['close_date'] = pd.to_datetime(df['close_date'], unit='s', utc=True, infer_datetime_format=True ) df['profit'] = df['close_rate'] - df['open_rate'] - df = df.sort_values("open_time").reset_index(drop=True) + df = df.sort_values("open_date").reset_index(drop=True) return df @@ -79,9 +79,9 @@ def analyze_trade_parallelism(results: pd.DataFrame, timeframe: str) -> pd.DataF """ from freqtrade.exchange import timeframe_to_minutes timeframe_min = timeframe_to_minutes(timeframe) - dates = [pd.Series(pd.date_range(row[1].open_time, row[1].close_time, + dates = [pd.Series(pd.date_range(row[1]['open_date'], row[1]['close_date'], freq=f"{timeframe_min}min")) - for row in results[['open_time', 'close_time']].iterrows()] + for row in results[['open_date', 'close_date']].iterrows()] deltas = [len(x) for x in dates] dates = pd.Series(pd.concat(dates).values, name='date') df2 = pd.DataFrame(np.repeat(results.values, deltas, axis=0), columns=results.columns) @@ -116,7 +116,7 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS) persistence.init(db_url, clean_open_orders=False) - columns = ["pair", "open_time", "close_time", "profit", "profit_percent", + columns = ["pair", "open_date", "close_date", "profit", "profit_percent", "open_rate", "close_rate", "amount", "duration", "sell_reason", "fee_open", "fee_close", "open_rate_requested", "close_rate_requested", "stake_amount", "max_rate", "min_rate", "id", "exchange", @@ -180,8 +180,8 @@ def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame, else: trades_start = dataframe.iloc[0]['date'] trades_stop = dataframe.iloc[-1]['date'] - trades = trades.loc[(trades['open_time'] >= trades_start) & - (trades['close_time'] <= trades_stop)] + trades = trades.loc[(trades['open_date'] >= trades_start) & + (trades['close_date'] <= trades_stop)] return trades @@ -227,7 +227,7 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, """ Adds a column `col_name` with the cumulative profit for the given trades array. :param df: DataFrame with date index - :param trades: DataFrame containing trades (requires columns close_time and profit_percent) + :param trades: DataFrame containing trades (requires columns close_date and profit_percent) :param col_name: Column name that will be assigned the results :param timeframe: Timeframe used during the operations :return: Returns df with one additional column, col_name, containing the cumulative profit. @@ -238,7 +238,7 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, from freqtrade.exchange import timeframe_to_minutes timeframe_minutes = timeframe_to_minutes(timeframe) # Resample to timeframe to make sure trades match candles - _trades_sum = trades.resample(f'{timeframe_minutes}min', on='close_time' + _trades_sum = trades.resample(f'{timeframe_minutes}min', on='close_date' )[['profit_percent']].sum() df.loc[:, col_name] = _trades_sum.cumsum() # Set first value to 0 @@ -248,13 +248,13 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str, return df -def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_time', +def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_date', value_col: str = 'profit_percent' ) -> Tuple[float, pd.Timestamp, pd.Timestamp]: """ Calculate max drawdown and the corresponding close dates - :param trades: DataFrame containing trades (requires columns close_time and profit_percent) - :param date_col: Column in DataFrame to use for dates (defaults to 'close_time') + :param trades: DataFrame containing trades (requires columns close_date and profit_percent) + :param date_col: Column in DataFrame to use for dates (defaults to 'close_date') :param value_col: Column in DataFrame to use for values (defaults to 'profit_percent') :return: Tuple (float, highdate, lowdate) with absolute max drawdown, high and low time :raise: ValueError if trade-dataframe was found empty. diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index dd2f44f23..169732314 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -237,7 +237,7 @@ class Edge: # All returned values are relative, they are defined as ratios. stake = 0.015 - result['trade_duration'] = result['close_time'] - result['open_time'] + result['trade_duration'] = result['close_date'] - result['open_date'] result['trade_duration'] = result['trade_duration'].map( lambda x: int(x.total_seconds() / 60)) @@ -427,8 +427,8 @@ class Edge: 'stoploss': stoploss, 'profit_ratio': '', 'profit_abs': '', - 'open_time': date_column[open_trade_index], - 'close_time': date_column[exit_index], + 'open_date': date_column[open_trade_index], + 'close_date': date_column[exit_index], 'open_index': start_point + open_trade_index, 'close_index': start_point + exit_index, 'trade_duration': '', diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index e4df80a82..4197ec087 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -39,8 +39,8 @@ class BacktestResult(NamedTuple): pair: str profit_percent: float profit_abs: float - open_time: datetime - close_time: datetime + open_date: datetime + close_date: datetime open_index: int close_index: int trade_duration: float @@ -248,8 +248,8 @@ class Backtesting: return BacktestResult(pair=pair, 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, + open_date=buy_row.date, + close_date=sell_row.date, trade_duration=trade_dur, open_index=buy_row.Index, close_index=sell_row.Index, @@ -264,8 +264,8 @@ class Backtesting: bt_res = BacktestResult(pair=pair, 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, + open_date=buy_row.date, + close_date=sell_row.date, trade_duration=int(( sell_row.date - buy_row.date).total_seconds() // 60), open_index=buy_row.Index, @@ -358,8 +358,8 @@ class Backtesting: if trade_entry: logger.debug(f"{pair} - Locking pair till " - f"close_time={trade_entry.close_time}") - lock_pair_until[pair] = trade_entry.close_time + f"close_date={trade_entry.close_date}") + lock_pair_until[pair] = trade_entry.close_date trades.append(trade_entry) else: # Set lock_pair_until to end of testing period if trade could not be closed @@ -421,4 +421,5 @@ class Backtesting: stats = generate_backtest_stats(self.config, data, all_results, min_date=min_date, max_date=max_date) show_backtest_results(self.config, stats) - store_backtest_stats(self.config['exportfilename'], stats) + if self.config.get('export', False): + store_backtest_stats(self.config['exportfilename'], stats) diff --git a/freqtrade/optimize/hyperopt_loss_sharpe_daily.py b/freqtrade/optimize/hyperopt_loss_sharpe_daily.py index e4cd1d749..bcba73a7f 100644 --- a/freqtrade/optimize/hyperopt_loss_sharpe_daily.py +++ b/freqtrade/optimize/hyperopt_loss_sharpe_daily.py @@ -43,7 +43,7 @@ class SharpeHyperOptLossDaily(IHyperOptLoss): normalize=True) sum_daily = ( - results.resample(resample_freq, on='close_time').agg( + results.resample(resample_freq, on='close_date').agg( {"profit_percent_after_slippage": sum}).reindex(t_index).fillna(0) ) diff --git a/freqtrade/optimize/hyperopt_loss_sortino_daily.py b/freqtrade/optimize/hyperopt_loss_sortino_daily.py index cd6a8bcc2..3b099a253 100644 --- a/freqtrade/optimize/hyperopt_loss_sortino_daily.py +++ b/freqtrade/optimize/hyperopt_loss_sortino_daily.py @@ -45,7 +45,7 @@ class SortinoHyperOptLossDaily(IHyperOptLoss): normalize=True) sum_daily = ( - results.resample(resample_freq, on='close_time').agg( + results.resample(resample_freq, on='close_date').agg( {"profit_percent_after_slippage": sum}).reindex(t_index).fillna(0) ) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 3c0bfcb96..63c2d2022 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -52,8 +52,8 @@ def backtest_result_to_list(results: DataFrame) -> List[List]: :param results: Dataframe containing results for one strategy :return: List of Lists containing the trades """ - return [[t.pair, t.profit_percent, t.open_time.timestamp(), - t.close_time.timestamp(), t.open_index - 1, t.trade_duration, + return [[t.pair, t.profit_percent, t.open_date.timestamp(), + t.open_date.timestamp(), t.open_index - 1, t.trade_duration, t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value] for index, t in results.iterrows()] @@ -350,10 +350,10 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: def text_table_add_metrics(strategy_results: Dict) -> str: if len(strategy_results['trades']) > 0: - min_trade = min(strategy_results['trades'], key=lambda x: x['open_time']) + min_trade = min(strategy_results['trades'], key=lambda x: x['open_date']) metrics = [ ('Total trades', strategy_results['total_trades']), - ('First trade', min_trade['open_time'].strftime(DATETIME_PRINT_FORMAT)), + ('First trade', min_trade['open_date'].strftime(DATETIME_PRINT_FORMAT)), ('First trade Pair', min_trade['pair']), ('Backtesting from', strategy_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)), ('Backtesting to', strategy_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)), diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index e8b0b4938..6d50defaf 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -63,7 +63,7 @@ def init_plotscript(config): exportfilename=config.get('exportfilename'), no_trades=no_trades ) - trades = trim_dataframe(trades, timerange, 'open_time') + trades = trim_dataframe(trades, timerange, 'open_date') return {"ohlcv": data, "trades": trades, @@ -166,7 +166,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: f"{row['sell_reason']}, {row['duration']} min", axis=1) trade_buys = go.Scatter( - x=trades["open_time"], + x=trades["open_date"], y=trades["open_rate"], mode='markers', name='Trade buy', @@ -181,7 +181,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: ) trade_sells = go.Scatter( - x=trades.loc[trades['profit_percent'] > 0, "close_time"], + x=trades.loc[trades['profit_percent'] > 0, "close_date"], y=trades.loc[trades['profit_percent'] > 0, "close_rate"], text=trades.loc[trades['profit_percent'] > 0, "desc"], mode='markers', @@ -194,7 +194,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: ) ) trade_sells_loss = go.Scatter( - x=trades.loc[trades['profit_percent'] <= 0, "close_time"], + x=trades.loc[trades['profit_percent'] <= 0, "close_date"], y=trades.loc[trades['profit_percent'] <= 0, "close_rate"], text=trades.loc[trades['profit_percent'] <= 0, "desc"], mode='markers', @@ -506,7 +506,7 @@ def plot_profit(config: Dict[str, Any]) -> None: # Remove open pairs - we don't know the profit yet so can't calculate profit for these. # Also, If only one open pair is left, then the profit-generation would fail. trades = trades[(trades['pair'].isin(plot_elements["pairs"])) - & (~trades['close_time'].isnull()) + & (~trades['close_date'].isnull()) ] if len(trades) == 0: raise OperationalException("No trades found, cannot generate Profit-plot without " From 28817187331fd7d84a55aa69cc38504263f43c0d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Jun 2020 09:21:28 +0200 Subject: [PATCH 0205/1197] Adapt tests for new column names --- tests/data/test_btanalysis.py | 24 ++++++++++++------------ tests/edge/test_edge.py | 16 ++++++++-------- tests/optimize/test_backtest_detail.py | 4 ++-- tests/optimize/test_backtesting.py | 16 ++++++++-------- tests/optimize/test_hyperopt.py | 2 +- tests/optimize/test_optimize_reports.py | 4 ++-- tests/test_plotting.py | 2 +- 7 files changed, 34 insertions(+), 34 deletions(-) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index d571569b9..077db19f1 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -47,7 +47,7 @@ def test_load_trades_from_db(default_conf, fee, mocker): assert len(trades) == 3 assert isinstance(trades, DataFrame) assert "pair" in trades.columns - assert "open_time" in trades.columns + assert "open_date" in trades.columns assert "profit_percent" in trades.columns for col in BT_DATA_COLUMNS: @@ -67,13 +67,13 @@ def test_extract_trades_of_period(testdatadir): {'pair': [pair, pair, pair, pair], 'profit_percent': [0.0, 0.1, -0.2, -0.5], 'profit_abs': [0.0, 1, -2, -5], - 'open_time': to_datetime([Arrow(2017, 11, 13, 15, 40, 0).datetime, + 'open_date': to_datetime([Arrow(2017, 11, 13, 15, 40, 0).datetime, Arrow(2017, 11, 14, 9, 41, 0).datetime, Arrow(2017, 11, 14, 14, 20, 0).datetime, Arrow(2017, 11, 15, 3, 40, 0).datetime, ], utc=True ), - 'close_time': to_datetime([Arrow(2017, 11, 13, 16, 40, 0).datetime, + 'close_date': to_datetime([Arrow(2017, 11, 13, 16, 40, 0).datetime, Arrow(2017, 11, 14, 10, 41, 0).datetime, Arrow(2017, 11, 14, 15, 25, 0).datetime, Arrow(2017, 11, 15, 3, 55, 0).datetime, @@ -82,10 +82,10 @@ def test_extract_trades_of_period(testdatadir): trades1 = extract_trades_of_period(data, trades) # First and last trade are dropped as they are out of range assert len(trades1) == 2 - assert trades1.iloc[0].open_time == Arrow(2017, 11, 14, 9, 41, 0).datetime - assert trades1.iloc[0].close_time == Arrow(2017, 11, 14, 10, 41, 0).datetime - assert trades1.iloc[-1].open_time == Arrow(2017, 11, 14, 14, 20, 0).datetime - assert trades1.iloc[-1].close_time == Arrow(2017, 11, 14, 15, 25, 0).datetime + assert trades1.iloc[0].open_date == Arrow(2017, 11, 14, 9, 41, 0).datetime + assert trades1.iloc[0].close_date == Arrow(2017, 11, 14, 10, 41, 0).datetime + assert trades1.iloc[-1].open_date == Arrow(2017, 11, 14, 14, 20, 0).datetime + assert trades1.iloc[-1].close_date == Arrow(2017, 11, 14, 15, 25, 0).datetime def test_analyze_trade_parallelism(default_conf, mocker, testdatadir): @@ -174,7 +174,7 @@ def test_create_cum_profit1(testdatadir): filename = testdatadir / "backtest-result_test.json" bt_data = load_backtest_data(filename) # Move close-time to "off" the candle, to make sure the logic still works - bt_data.loc[:, 'close_time'] = bt_data.loc[:, 'close_time'] + DateOffset(seconds=20) + bt_data.loc[:, 'close_date'] = bt_data.loc[:, 'close_date'] + DateOffset(seconds=20) timerange = TimeRange.parse_timerange("20180110-20180112") df = load_pair_history(pair="TRX/BTC", timeframe='5m', @@ -213,11 +213,11 @@ def test_calculate_max_drawdown2(): -0.033961, 0.010680, 0.010886, -0.029274, 0.011178, 0.010693, 0.010711] dates = [Arrow(2020, 1, 1).shift(days=i) for i in range(len(values))] - df = DataFrame(zip(values, dates), columns=['profit', 'open_time']) + df = DataFrame(zip(values, dates), columns=['profit', 'open_date']) # sort by profit and reset index df = df.sort_values('profit').reset_index(drop=True) df1 = df.copy() - drawdown, h, low = calculate_max_drawdown(df, date_col='open_time', value_col='profit') + drawdown, h, low = calculate_max_drawdown(df, date_col='open_date', value_col='profit') # Ensure df has not been altered. assert df.equals(df1) @@ -226,6 +226,6 @@ def test_calculate_max_drawdown2(): assert h < low assert drawdown == 0.091755 - df = DataFrame(zip(values[:5], dates[:5]), columns=['profit', 'open_time']) + df = DataFrame(zip(values[:5], dates[:5]), columns=['profit', 'open_date']) with pytest.raises(ValueError, match='No losing trade, therefore no drawdown.'): - calculate_max_drawdown(df, date_col='open_time', value_col='profit') + calculate_max_drawdown(df, date_col='open_date', value_col='profit') diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index cf9cb6fe1..ea1e709a2 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -163,8 +163,8 @@ def test_edge_results(edge_conf, mocker, caplog, data) -> None: for c, trade in enumerate(data.trades): res = results.iloc[c] assert res.exit_type == trade.sell_reason - assert res.open_time == _get_frame_time_from_offset(trade.open_tick).replace(tzinfo=None) - assert res.close_time == _get_frame_time_from_offset(trade.close_tick).replace(tzinfo=None) + assert res.open_date == _get_frame_time_from_offset(trade.open_tick).replace(tzinfo=None) + assert res.close_date == _get_frame_time_from_offset(trade.close_tick).replace(tzinfo=None) def test_adjust(mocker, edge_conf): @@ -354,8 +354,8 @@ def test_process_expectancy(mocker, edge_conf, fee, risk_reward_ratio, expectanc 'stoploss': -0.9, 'profit_percent': '', 'profit_abs': '', - 'open_time': np.datetime64('2018-10-03T00:05:00.000000000'), - 'close_time': np.datetime64('2018-10-03T00:10:00.000000000'), + 'open_date': np.datetime64('2018-10-03T00:05:00.000000000'), + 'close_date': np.datetime64('2018-10-03T00:10:00.000000000'), 'open_index': 1, 'close_index': 1, 'trade_duration': '', @@ -367,8 +367,8 @@ def test_process_expectancy(mocker, edge_conf, fee, risk_reward_ratio, expectanc 'stoploss': -0.9, 'profit_percent': '', 'profit_abs': '', - 'open_time': np.datetime64('2018-10-03T00:20:00.000000000'), - 'close_time': np.datetime64('2018-10-03T00:25:00.000000000'), + 'open_date': np.datetime64('2018-10-03T00:20:00.000000000'), + 'close_date': np.datetime64('2018-10-03T00:25:00.000000000'), 'open_index': 4, 'close_index': 4, 'trade_duration': '', @@ -380,8 +380,8 @@ def test_process_expectancy(mocker, edge_conf, fee, risk_reward_ratio, expectanc 'stoploss': -0.9, 'profit_percent': '', 'profit_abs': '', - 'open_time': np.datetime64('2018-10-03T00:30:00.000000000'), - 'close_time': np.datetime64('2018-10-03T00:40:00.000000000'), + 'open_date': np.datetime64('2018-10-03T00:30:00.000000000'), + 'close_date': np.datetime64('2018-10-03T00:40:00.000000000'), 'open_index': 6, 'close_index': 7, 'trade_duration': '', diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 9b3043086..f6ac95aeb 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -395,5 +395,5 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: for c, trade in enumerate(data.trades): res = results.iloc[c] assert res.sell_reason == trade.sell_reason - assert res.open_time == _get_frame_time_from_offset(trade.open_tick) - assert res.close_time == _get_frame_time_from_offset(trade.close_tick) + assert res.open_date == _get_frame_time_from_offset(trade.open_tick) + assert res.close_date == _get_frame_time_from_offset(trade.close_tick) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 853780b82..e7a13f251 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -459,10 +459,10 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None: {'pair': [pair, pair], 'profit_percent': [0.0, 0.0], 'profit_abs': [0.0, 0.0], - 'open_time': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime, + 'open_date': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime, Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True ), - 'close_time': pd.to_datetime([Arrow(2018, 1, 29, 22, 35, 0).datetime, + 'close_date': pd.to_datetime([Arrow(2018, 1, 29, 22, 35, 0).datetime, Arrow(2018, 1, 30, 4, 10, 0).datetime], utc=True), 'open_index': [78, 184], 'close_index': [125, 192], @@ -475,12 +475,12 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None: pd.testing.assert_frame_equal(results, expected) data_pair = processed[pair] for _, t in results.iterrows(): - ln = data_pair.loc[data_pair["date"] == t["open_time"]] + ln = data_pair.loc[data_pair["date"] == t["open_date"]] # Check open trade rate alignes to open rate assert ln is not None assert round(ln.iloc[0]["open"], 6) == round(t["open_rate"], 6) # check close trade rate alignes to close rate or is between high and low - ln = data_pair.loc[data_pair["date"] == t["close_time"]] + ln = data_pair.loc[data_pair["date"] == t["close_date"]] assert (round(ln.iloc[0]["open"], 6) == round(t["close_rate"], 6) or round(ln.iloc[0]["low"], 6) < round( t["close_rate"], 6) < round(ln.iloc[0]["high"], 6)) @@ -756,10 +756,10 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC'], 'profit_percent': [0.0, 0.0], 'profit_abs': [0.0, 0.0], - 'open_time': pd.to_datetime(['2018-01-29 18:40:00', + 'open_date': pd.to_datetime(['2018-01-29 18:40:00', '2018-01-30 03:30:00', ], utc=True ), - 'close_time': pd.to_datetime(['2018-01-29 20:45:00', + 'close_date': pd.to_datetime(['2018-01-29 20:45:00', '2018-01-30 05:35:00', ], utc=True), 'open_index': [78, 184], 'close_index': [125, 192], @@ -772,11 +772,11 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat pd.DataFrame({'pair': ['XRP/BTC', 'LTC/BTC', 'ETH/BTC'], 'profit_percent': [0.03, 0.01, 0.1], 'profit_abs': [0.01, 0.02, 0.2], - 'open_time': pd.to_datetime(['2018-01-29 18:40:00', + 'open_date': pd.to_datetime(['2018-01-29 18:40:00', '2018-01-30 03:30:00', '2018-01-30 05:30:00'], utc=True ), - 'close_time': pd.to_datetime(['2018-01-29 20:45:00', + 'close_date': pd.to_datetime(['2018-01-29 20:45:00', '2018-01-30 05:35:00', '2018-01-30 08:30:00'], utc=True), 'open_index': [78, 184, 185], diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 564725709..00edd8ad2 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -46,7 +46,7 @@ def hyperopt_results(): 'profit_abs': [-0.2, 0.4, 0.6], 'trade_duration': [10, 30, 10], 'sell_reason': [SellType.STOP_LOSS, SellType.ROI, SellType.ROI], - 'close_time': + 'close_date': [ datetime(2019, 1, 1, 9, 26, 3, 478039), datetime(2019, 2, 1, 9, 26, 3, 478039), diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 175405e4c..7efd87787 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -204,11 +204,11 @@ def test_backtest_record(default_conf, fee, mocker): "UNITTEST/BTC", "UNITTEST/BTC"], "profit_percent": [0.003312, 0.010801, 0.013803, 0.002780], "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], - "open_time": [Arrow(2017, 11, 14, 19, 32, 00).datetime, + "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, Arrow(2017, 11, 14, 21, 36, 00).datetime, Arrow(2017, 11, 14, 22, 12, 00).datetime, Arrow(2017, 11, 14, 22, 44, 00).datetime], - "close_time": [Arrow(2017, 11, 14, 21, 35, 00).datetime, + "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, Arrow(2017, 11, 14, 22, 10, 00).datetime, Arrow(2017, 11, 14, 22, 43, 00).datetime, Arrow(2017, 11, 14, 22, 58, 00).datetime], diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 05805eb24..83a41deeb 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -267,7 +267,7 @@ def test_generate_profit_graph(testdatadir): trades = load_backtest_data(filename) timerange = TimeRange.parse_timerange("20180110-20180112") pairs = ["TRX/BTC", "XLM/BTC"] - trades = trades[trades['close_time'] < pd.Timestamp('2018-01-12', tz='UTC')] + trades = trades[trades['close_date'] < pd.Timestamp('2018-01-12', tz='UTC')] data = history.load_data(datadir=testdatadir, pairs=pairs, From 04cbc2cde5a832119db5c95b75e720974794fa5d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Jun 2020 09:22:50 +0200 Subject: [PATCH 0206/1197] Shorten variable --- freqtrade/optimize/optimize_reports.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 63c2d2022..de609589a 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -1,5 +1,5 @@ import logging -from datetime import datetime, timedelta, timezone +from datetime import datetime, timedelta from pathlib import Path from typing import Any, Dict, List @@ -348,21 +348,21 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") -def text_table_add_metrics(strategy_results: Dict) -> str: - if len(strategy_results['trades']) > 0: - min_trade = min(strategy_results['trades'], key=lambda x: x['open_date']) +def text_table_add_metrics(strat_results: Dict) -> str: + if len(strat_results['trades']) > 0: + min_trade = min(strat_results['trades'], key=lambda x: x['open_date']) metrics = [ - ('Total trades', strategy_results['total_trades']), + ('Total trades', strat_results['total_trades']), ('First trade', min_trade['open_date'].strftime(DATETIME_PRINT_FORMAT)), ('First trade Pair', min_trade['pair']), - ('Backtesting from', strategy_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)), - ('Backtesting to', strategy_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)), - ('Trades per day', strategy_results['trades_per_day']), + ('Backtesting from', strat_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)), + ('Backtesting to', strat_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)), + ('Trades per day', strat_results['trades_per_day']), ('', ''), # Empty line to improve readability - ('Max Drawdown', f"{round(strategy_results['max_drawdown'] * 100, 2)}%"), - ('Drawdown Start', strategy_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)), - ('Drawdown End', strategy_results['drawdown_end'].strftime(DATETIME_PRINT_FORMAT)), - ('Market change', f"{round(strategy_results['market_change'] * 100, 2)}%"), + ('Max Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"), + ('Drawdown Start', strat_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)), + ('Drawdown End', strat_results['drawdown_end'].strftime(DATETIME_PRINT_FORMAT)), + ('Market change', f"{round(strat_results['market_change'] * 100, 2)}%"), ] return tabulate(metrics, headers=["Metric", "Value"], tablefmt="orgtbl") From 0fa56be9d2a06fcc11b4f9bcdfa5b3564315539d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Jun 2020 09:27:07 +0200 Subject: [PATCH 0207/1197] remove openIndex and closeIndex from backtest-report --- freqtrade/edge/edge_positioning.py | 2 -- freqtrade/optimize/backtesting.py | 6 ------ freqtrade/optimize/optimize_reports.py | 4 +++- tests/edge/test_edge.py | 6 ------ tests/optimize/test_backtesting.py | 6 ------ tests/optimize/test_optimize_reports.py | 2 -- 6 files changed, 3 insertions(+), 23 deletions(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 169732314..9843d4e83 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -429,8 +429,6 @@ class Edge: 'profit_abs': '', 'open_date': date_column[open_trade_index], 'close_date': date_column[exit_index], - 'open_index': start_point + open_trade_index, - 'close_index': start_point + exit_index, 'trade_duration': '', 'open_rate': round(open_price, 15), 'close_rate': round(exit_price, 15), diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 4197ec087..1eef4f4b9 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -41,8 +41,6 @@ class BacktestResult(NamedTuple): profit_abs: float open_date: datetime close_date: datetime - open_index: int - close_index: int trade_duration: float open_at_end: bool open_rate: float @@ -251,8 +249,6 @@ class Backtesting: open_date=buy_row.date, close_date=sell_row.date, trade_duration=trade_dur, - open_index=buy_row.Index, - close_index=sell_row.Index, open_at_end=False, open_rate=buy_row.open, close_rate=closerate, @@ -268,8 +264,6 @@ class Backtesting: close_date=sell_row.date, trade_duration=int(( sell_row.date - buy_row.date).total_seconds() // 60), - open_index=buy_row.Index, - close_index=sell_row.Index, open_at_end=True, open_rate=buy_row.open, close_rate=sell_row.open, diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index de609589a..8f9104640 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -52,8 +52,10 @@ def backtest_result_to_list(results: DataFrame) -> List[List]: :param results: Dataframe containing results for one strategy :return: List of Lists containing the trades """ + # Return 0 as "index" for compatibility reasons (for now) + # TODO: Evaluate if we can remove this return [[t.pair, t.profit_percent, t.open_date.timestamp(), - t.open_date.timestamp(), t.open_index - 1, t.trade_duration, + t.open_date.timestamp(), 0, t.trade_duration, t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value] for index, t in results.iterrows()] diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index ea1e709a2..cd5f623e3 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -356,8 +356,6 @@ def test_process_expectancy(mocker, edge_conf, fee, risk_reward_ratio, expectanc 'profit_abs': '', 'open_date': np.datetime64('2018-10-03T00:05:00.000000000'), 'close_date': np.datetime64('2018-10-03T00:10:00.000000000'), - 'open_index': 1, - 'close_index': 1, 'trade_duration': '', 'open_rate': 17, 'close_rate': 17, @@ -369,8 +367,6 @@ def test_process_expectancy(mocker, edge_conf, fee, risk_reward_ratio, expectanc 'profit_abs': '', 'open_date': np.datetime64('2018-10-03T00:20:00.000000000'), 'close_date': np.datetime64('2018-10-03T00:25:00.000000000'), - 'open_index': 4, - 'close_index': 4, 'trade_duration': '', 'open_rate': 20, 'close_rate': 20, @@ -382,8 +378,6 @@ def test_process_expectancy(mocker, edge_conf, fee, risk_reward_ratio, expectanc 'profit_abs': '', 'open_date': np.datetime64('2018-10-03T00:30:00.000000000'), 'close_date': np.datetime64('2018-10-03T00:40:00.000000000'), - 'open_index': 6, - 'close_index': 7, 'trade_duration': '', 'open_rate': 26, 'close_rate': 34, diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index e7a13f251..c0a9e798a 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -464,8 +464,6 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None: ), 'close_date': pd.to_datetime([Arrow(2018, 1, 29, 22, 35, 0).datetime, Arrow(2018, 1, 30, 4, 10, 0).datetime], utc=True), - 'open_index': [78, 184], - 'close_index': [125, 192], 'trade_duration': [235, 40], 'open_at_end': [False, False], 'open_rate': [0.104445, 0.10302485], @@ -761,8 +759,6 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat ), 'close_date': pd.to_datetime(['2018-01-29 20:45:00', '2018-01-30 05:35:00', ], utc=True), - 'open_index': [78, 184], - 'close_index': [125, 192], 'trade_duration': [235, 40], 'open_at_end': [False, False], 'open_rate': [0.104445, 0.10302485], @@ -779,8 +775,6 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat 'close_date': pd.to_datetime(['2018-01-29 20:45:00', '2018-01-30 05:35:00', '2018-01-30 08:30:00'], utc=True), - 'open_index': [78, 184, 185], - 'close_index': [125, 224, 205], 'trade_duration': [47, 40, 20], 'open_at_end': [False, False, False], 'open_rate': [0.104445, 0.10302485, 0.122541], diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 7efd87787..9f06043cc 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -214,8 +214,6 @@ def test_backtest_record(default_conf, fee, mocker): Arrow(2017, 11, 14, 22, 58, 00).datetime], "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], - "open_index": [1, 119, 153, 185], - "close_index": [118, 151, 184, 199], "trade_duration": [123, 34, 31, 14], "open_at_end": [False, False, False, True], "sell_reason": [SellType.ROI, SellType.STOP_LOSS, From dacb40a9765249c949beb1e07ce57db1915b7eb2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Jun 2020 09:34:18 +0200 Subject: [PATCH 0208/1197] Add get_latest_backtest_filename --- freqtrade/data/btanalysis.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index f174b8ea9..89380059f 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -20,6 +20,34 @@ BT_DATA_COLUMNS = ["pair", "profit_percent", "open_date", "close_date", "index", "open_rate", "close_rate", "open_at_end", "sell_reason"] +def get_latest_backtest_filename(directory: Union[Path, str]) -> str: + """ + Get latest backtest export based on '.last_result.json'. + :param directory: Directory to search for last result + :return: string containing the filename of the latest backtest result + :raises: ValueError in the following cases: + * Directory does not exist + * `directory/.last_result.json` does not exist + * `directory/.last_result.json` has the wrong content + """ + if isinstance(directory, str): + directory = Path(directory) + if not directory.is_dir(): + raise ValueError(f"Directory {directory} does not exist.") + filename = directory / '.last_result.json' + + if not filename.is_file(): + raise ValueError(f"Directory {directory} does not seem to contain backtest statistics yet.") + + with filename.open() as file: + data = json_load(file) + + if 'latest_backtest' not in data: + raise ValueError("Invalid .last_result.json format") + + return data['latest_backtest'] + + def load_backtest_stats(filename: Union[Path, str]) -> Dict[str, Any]: """ Load backtest statistics file. From 075eb0a161496a59d067ef4385ffc51997d4e87a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Jun 2020 19:20:51 +0200 Subject: [PATCH 0209/1197] Fix sequence of saving --- freqtrade/optimize/backtesting.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 1eef4f4b9..30418f0d7 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -409,11 +409,11 @@ class Backtesting: position_stacking=position_stacking, ) - if self.config.get('export', False): - store_backtest_result(self.config['exportfilename'], all_results) - # Show backtest results stats = generate_backtest_stats(self.config, data, all_results, min_date=min_date, max_date=max_date) - show_backtest_results(self.config, stats) if self.config.get('export', False): + store_backtest_result(self.config['exportfilename'], all_results) store_backtest_stats(self.config['exportfilename'], stats) + + # Show backtest results + show_backtest_results(self.config, stats) From af9a9592b78330925c4bd640b3280fdd4a96caff Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Jun 2020 19:32:47 +0200 Subject: [PATCH 0210/1197] Remove unnecessary statement --- freqtrade/data/btanalysis.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 89380059f..d6af67a32 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -141,7 +141,6 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: :param db_url: Sqlite url (default format sqlite:///tradesv3.dry-run.sqlite) :return: Dataframe containing Trades """ - trades: pd.DataFrame = pd.DataFrame([], columns=BT_DATA_COLUMNS) persistence.init(db_url, clean_open_orders=False) columns = ["pair", "open_date", "close_date", "profit", "profit_percent", From 03ab61959b6f0a53f74c50ca52190e96677ea45b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Jun 2020 20:08:45 +0200 Subject: [PATCH 0211/1197] Add test for generate_backtest_stats --- freqtrade/optimize/optimize_reports.py | 10 +-- tests/optimize/test_optimize_reports.py | 81 +++++++++++++++++++++++-- 2 files changed, 82 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 8f9104640..d1c58617d 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -1,5 +1,5 @@ import logging -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from pathlib import Path from typing import Any, Dict, List @@ -266,10 +266,10 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], except ValueError: strat_stats.update({ 'max_drawdown': 0.0, - 'drawdown_start': datetime.min, - 'drawdown_start_ts': datetime(1970, 1, 1).timestamp(), - 'drawdown_end': datetime.min, - 'drawdown_end_ts': datetime(1970, 1, 1).timestamp(), + 'drawdown_start': datetime(1970, 1, 1, tzinfo=timezone.utc), + 'drawdown_start_ts': 0, + 'drawdown_end': datetime(1970, 1, 1, tzinfo=timezone.utc), + 'drawdown_end_ts': 0, }) strategy_results = generate_strategy_metrics(stake_currency=stake_currency, diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 9f06043cc..c46b96ab2 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -1,14 +1,22 @@ +from datetime import datetime from pathlib import Path import pandas as pd import pytest from arrow import Arrow +from freqtrade.configuration import TimeRange +from freqtrade.data import history from freqtrade.edge import PairInfo -from freqtrade.optimize.optimize_reports import ( - generate_pair_metrics, generate_edge_table, generate_sell_reason_stats, - text_table_bt_results, text_table_sell_reason, generate_strategy_metrics, - text_table_strategy, store_backtest_result) +from freqtrade.optimize.optimize_reports import (generate_backtest_stats, + generate_edge_table, + generate_pair_metrics, + generate_sell_reason_stats, + generate_strategy_metrics, + store_backtest_result, + text_table_bt_results, + text_table_sell_reason, + text_table_strategy) from freqtrade.strategy.interface import SellType from tests.conftest import patch_exchange @@ -43,6 +51,71 @@ def test_text_table_bt_results(default_conf, mocker): assert text_table_bt_results(pair_results, stake_currency='BTC') == result_str +def test_generate_backtest_stats(default_conf, testdatadir): + results = {'DefStrat': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", + "UNITTEST/BTC", "UNITTEST/BTC"], + "profit_percent": [0.003312, 0.010801, 0.013803, 0.002780], + "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], + "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, + Arrow(2017, 11, 14, 21, 36, 00).datetime, + Arrow(2017, 11, 14, 22, 12, 00).datetime, + Arrow(2017, 11, 14, 22, 44, 00).datetime], + "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, + Arrow(2017, 11, 14, 22, 10, 00).datetime, + Arrow(2017, 11, 14, 22, 43, 00).datetime, + Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], + "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], + "trade_duration": [123, 34, 31, 14], + "open_at_end": [False, False, False, True], + "sell_reason": [SellType.ROI, SellType.STOP_LOSS, + SellType.ROI, SellType.FORCE_SELL] + })} + timerange = TimeRange.parse_timerange('1510688220-1510700340') + min_date = Arrow.fromtimestamp(1510688220) + max_date = Arrow.fromtimestamp(1510700340) + btdata = history.load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange, + fill_up_missing=True) + + stats = generate_backtest_stats(default_conf, btdata, results, min_date, max_date) + assert isinstance(stats, dict) + assert 'strategy' in stats + assert 'DefStrat' in stats['strategy'] + assert 'strategy_comparison' in stats + strat_stats = stats['strategy']['DefStrat'] + assert strat_stats['backtest_start'] == min_date.datetime + assert strat_stats['backtest_end'] == max_date.datetime + assert strat_stats['total_trades'] == len(results['DefStrat']) + # Above sample had no loosing trade + assert strat_stats['max_drawdown'] == 0.0 + + results = {'DefStrat': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", + "UNITTEST/BTC", "UNITTEST/BTC"], + "profit_percent": [0.003312, 0.010801, -0.013803, 0.002780], + "profit_abs": [0.000003, 0.000011, -0.000014, 0.000003], + "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, + Arrow(2017, 11, 14, 21, 36, 00).datetime, + Arrow(2017, 11, 14, 22, 12, 00).datetime, + Arrow(2017, 11, 14, 22, 44, 00).datetime], + "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, + Arrow(2017, 11, 14, 22, 10, 00).datetime, + Arrow(2017, 11, 14, 22, 43, 00).datetime, + Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], + "close_rate": [0.002546, 0.003014, 0.0032903, 0.003217], + "trade_duration": [123, 34, 31, 14], + "open_at_end": [False, False, False, True], + "sell_reason": [SellType.ROI, SellType.STOP_LOSS, + SellType.ROI, SellType.FORCE_SELL] + })} + + assert strat_stats['max_drawdown'] == 0.0 + assert strat_stats['drawdown_start'] == Arrow.fromtimestamp(0).datetime + assert strat_stats['drawdown_end'] == Arrow.fromtimestamp(0).datetime + assert strat_stats['drawdown_end_ts'] == 0 + assert strat_stats['drawdown_start_ts'] == 0 + + def test_generate_pair_metrics(default_conf, mocker): results = pd.DataFrame( From 6e947346787560085dcdb51970ddaa68e9525597 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Jun 2020 20:21:08 +0200 Subject: [PATCH 0212/1197] Add fee to backtestresult --- freqtrade/optimize/backtesting.py | 18 ++++++++++++------ tests/optimize/test_backtesting.py | 6 ++++-- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 30418f0d7..7021e85b6 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -40,11 +40,13 @@ class BacktestResult(NamedTuple): profit_percent: float profit_abs: float open_date: datetime + open_rate: float + open_fee: float close_date: datetime + close_rate: float + close_fee: float trade_duration: float open_at_end: bool - open_rate: float - close_rate: float sell_reason: SellType @@ -247,11 +249,13 @@ class Backtesting: profit_percent=trade.calc_profit_ratio(rate=closerate), profit_abs=trade.calc_profit(rate=closerate), open_date=buy_row.date, + open_rate=buy_row.open, + open_fee=self.fee, close_date=sell_row.date, + close_rate=closerate, + close_fee=self.fee, trade_duration=trade_dur, open_at_end=False, - open_rate=buy_row.open, - close_rate=closerate, sell_reason=sell.sell_type ) if partial_ohlcv: @@ -261,12 +265,14 @@ class Backtesting: profit_percent=trade.calc_profit_ratio(rate=sell_row.open), profit_abs=trade.calc_profit(rate=sell_row.open), open_date=buy_row.date, + open_rate=buy_row.open, + open_fee=self.fee, close_date=sell_row.date, + close_rate=sell_row.open, + close_fee=self.fee, trade_duration=int(( sell_row.date - buy_row.date).total_seconds() // 60), open_at_end=True, - open_rate=buy_row.open, - close_rate=sell_row.open, sell_reason=SellType.FORCE_SELL ) logger.debug(f"{pair} - Force selling still open trade, " diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index c0a9e798a..4ee03f6ba 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -462,12 +462,14 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None: 'open_date': pd.to_datetime([Arrow(2018, 1, 29, 18, 40, 0).datetime, Arrow(2018, 1, 30, 3, 30, 0).datetime], utc=True ), + 'open_rate': [0.104445, 0.10302485], + 'open_fee': [0.0025, 0.0025], 'close_date': pd.to_datetime([Arrow(2018, 1, 29, 22, 35, 0).datetime, Arrow(2018, 1, 30, 4, 10, 0).datetime], utc=True), + 'close_rate': [0.104969, 0.103541], + 'close_fee': [0.0025, 0.0025], 'trade_duration': [235, 40], 'open_at_end': [False, False], - 'open_rate': [0.104445, 0.10302485], - 'close_rate': [0.104969, 0.103541], 'sell_reason': [SellType.ROI, SellType.ROI] }) pd.testing.assert_frame_equal(results, expected) From f368aabcc7bce32752f93f26d020be7222e12678 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Jun 2020 20:51:36 +0200 Subject: [PATCH 0213/1197] Add amount to backtest-result --- freqtrade/optimize/backtesting.py | 3 +++ freqtrade/optimize/optimize_reports.py | 1 + tests/optimize/test_backtesting.py | 1 + 3 files changed, 5 insertions(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7021e85b6..d00f033cd 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -45,6 +45,7 @@ class BacktestResult(NamedTuple): close_date: datetime close_rate: float close_fee: float + amount: float trade_duration: float open_at_end: bool sell_reason: SellType @@ -254,6 +255,7 @@ class Backtesting: close_date=sell_row.date, close_rate=closerate, close_fee=self.fee, + amount=trade.amount, trade_duration=trade_dur, open_at_end=False, sell_reason=sell.sell_type @@ -270,6 +272,7 @@ class Backtesting: close_date=sell_row.date, close_rate=sell_row.open, close_fee=self.fee, + amount=trade.amount, trade_duration=int(( sell_row.date - buy_row.date).total_seconds() // 60), open_at_end=True, diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index d1c58617d..a7cc8a7d6 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -250,6 +250,7 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], 'backtest_days': backtest_days, 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else None, 'market_change': market_change, + 'stake_amount': config['stake_amount'] } result['strategy'][strategy] = strat_stats diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 4ee03f6ba..d5a6f8888 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -468,6 +468,7 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None: Arrow(2018, 1, 30, 4, 10, 0).datetime], utc=True), 'close_rate': [0.104969, 0.103541], 'close_fee': [0.0025, 0.0025], + 'amount': [0.009574, 0.009706], 'trade_duration': [235, 40], 'open_at_end': [False, False], 'sell_reason': [SellType.ROI, SellType.ROI] From 7727292861d661a09926cc1da3a5b9aaf4f501bf Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 26 Jun 2020 21:04:40 +0200 Subject: [PATCH 0214/1197] Rename duration to trade_duration --- freqtrade/data/btanalysis.py | 4 ++-- freqtrade/optimize/backtesting.py | 2 +- freqtrade/plot/plotting.py | 3 ++- tests/optimize/test_backtesting.py | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index d6af67a32..f34dbf313 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -16,7 +16,7 @@ from freqtrade.persistence import Trade logger = logging.getLogger(__name__) # must align with columns in backtest.py -BT_DATA_COLUMNS = ["pair", "profit_percent", "open_date", "close_date", "index", "duration", +BT_DATA_COLUMNS = ["pair", "profit_percent", "open_date", "close_date", "index", "trade_duration", "open_rate", "close_rate", "open_at_end", "sell_reason"] @@ -144,7 +144,7 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: persistence.init(db_url, clean_open_orders=False) columns = ["pair", "open_date", "close_date", "profit", "profit_percent", - "open_rate", "close_rate", "amount", "duration", "sell_reason", + "open_rate", "close_rate", "amount", "trade_duration", "sell_reason", "fee_open", "fee_close", "open_rate_requested", "close_rate_requested", "stake_amount", "max_rate", "min_rate", "id", "exchange", "stop_loss", "initial_stop_loss", "strategy", "timeframe"] diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index d00f033cd..18881f9db 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -228,7 +228,7 @@ class Backtesting: open_rate=buy_row.open, open_date=buy_row.date, stake_amount=stake_amount, - amount=stake_amount / buy_row.open, + amount=round(stake_amount / buy_row.open, 8), fee_open=self.fee, fee_close=self.fee, is_open=True, diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 6d50defaf..eee338a42 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -163,7 +163,8 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: if trades is not None and len(trades) > 0: # Create description for sell summarizing the trade trades['desc'] = trades.apply(lambda row: f"{round(row['profit_percent'] * 100, 1)}%, " - f"{row['sell_reason']}, {row['duration']} min", + f"{row['sell_reason']}, " + f"{row['trade_duration']} min", axis=1) trade_buys = go.Scatter( x=trades["open_date"], diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index d5a6f8888..2c855fbc0 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -468,7 +468,7 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None: Arrow(2018, 1, 30, 4, 10, 0).datetime], utc=True), 'close_rate': [0.104969, 0.103541], 'close_fee': [0.0025, 0.0025], - 'amount': [0.009574, 0.009706], + 'amount': [0.00957442, 0.0097064], 'trade_duration': [235, 40], 'open_at_end': [False, False], 'sell_reason': [SellType.ROI, SellType.ROI] From 04eaf2c39cc41d67e599dc8114f3247931110e9a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Jun 2020 06:46:54 +0200 Subject: [PATCH 0215/1197] Add test for get_last_backtest_Result --- freqtrade/data/btanalysis.py | 6 +++--- tests/data/test_btanalysis.py | 19 +++++++++++++++++++ tests/testdata/.last_result.json | 1 + tests/testdata/backtest-result_new.json | 1 + tests/testdata/backtest-result_test copy.json | 7 ------- 5 files changed, 24 insertions(+), 10 deletions(-) create mode 100644 tests/testdata/.last_result.json create mode 100644 tests/testdata/backtest-result_new.json delete mode 100644 tests/testdata/backtest-result_test copy.json diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index f34dbf313..a556207e5 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -33,17 +33,17 @@ def get_latest_backtest_filename(directory: Union[Path, str]) -> str: if isinstance(directory, str): directory = Path(directory) if not directory.is_dir(): - raise ValueError(f"Directory {directory} does not exist.") + raise ValueError(f"Directory '{directory}' does not exist.") filename = directory / '.last_result.json' if not filename.is_file(): - raise ValueError(f"Directory {directory} does not seem to contain backtest statistics yet.") + raise ValueError(f"Directory '{directory}' does not seem to contain backtest statistics yet.") with filename.open() as file: data = json_load(file) if 'latest_backtest' not in data: - raise ValueError("Invalid .last_result.json format") + raise ValueError("Invalid '.last_result.json' format.") return data['latest_backtest'] diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 077db19f1..fd3783bf2 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -13,12 +13,31 @@ from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, combine_dataframes_with_mean, create_cum_profit, extract_trades_of_period, + get_latest_backtest_filename, load_backtest_data, load_trades, load_trades_from_db) from freqtrade.data.history import load_data, load_pair_history from tests.conftest import create_mock_trades +def test_get_latest_backtest_filename(testdatadir, mocker): + with pytest.raises(ValueError, match=r"Directory .* does not exist\."): + get_latest_backtest_filename(testdatadir / 'does_not_exist') + + with pytest.raises(ValueError, + match=r"Directory .* does not seem to contain .*"): + get_latest_backtest_filename(testdatadir.parent) + + res = get_latest_backtest_filename(testdatadir) + assert res == 'backtest-result_new.json' + + mocker.patch("freqtrade.data.btanalysis.json_load", return_value={}) + + + with pytest.raises(ValueError, match=r"Invalid '.last_result.json' format."): + get_latest_backtest_filename(testdatadir) + + def test_load_backtest_data(testdatadir): filename = testdatadir / "backtest-result_test.json" diff --git a/tests/testdata/.last_result.json b/tests/testdata/.last_result.json new file mode 100644 index 000000000..98448e10f --- /dev/null +++ b/tests/testdata/.last_result.json @@ -0,0 +1 @@ +{"latest_backtest":"backtest-result_new.json"} diff --git a/tests/testdata/backtest-result_new.json b/tests/testdata/backtest-result_new.json new file mode 100644 index 000000000..457ad1bc7 --- /dev/null +++ b/tests/testdata/backtest-result_new.json @@ -0,0 +1 @@ +{"strategy": {"DefaultStrategy": {"trades": [{"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:20:00+00:00", "trade_duration": 5, "open_rate": 9.64e-05, "close_rate": 0.00010074887218045112, "open_at_end": false, "sell_reason": "roi", "profit": 4.348872180451118e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1037.344398340249, "profit_abs": 0.00399999999999999}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:30:00+00:00", "trade_duration": 15, "open_rate": 4.756e-05, "close_rate": 4.9705563909774425e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.1455639097744267e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2102.6072329688814, "profit_abs": 0.00399999999999999}, {"pair": "XLM/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:35:00+00:00", "trade_duration": 10, "open_rate": 3.339e-05, "close_rate": 3.489631578947368e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.506315789473681e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2994.908655286014, "profit_abs": 0.0040000000000000036}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:40:00+00:00", "trade_duration": 15, "open_rate": 9.696e-05, "close_rate": 0.00010133413533834584, "open_at_end": false, "sell_reason": "roi", "profit": 4.3741353383458455e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1031.3531353135315, "profit_abs": 0.00399999999999999}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 07:35:00+00:00", "close_date": "2018-01-10 08:35:00+00:00", "trade_duration": 60, "open_rate": 0.0943, "close_rate": 0.09477268170426063, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004726817042606385, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0604453870625663, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 07:40:00+00:00", "close_date": "2018-01-10 08:10:00+00:00", "trade_duration": 30, "open_rate": 0.02719607, "close_rate": 0.02760503345864661, "open_at_end": false, "sell_reason": "roi", "profit": 0.00040896345864661204, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.677001860930642, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 08:15:00+00:00", "close_date": "2018-01-10 09:55:00+00:00", "trade_duration": 100, "open_rate": 0.04634952, "close_rate": 0.046581848421052625, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002323284210526272, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1575196463739, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 14:45:00+00:00", "close_date": "2018-01-10 15:50:00+00:00", "trade_duration": 65, "open_rate": 3.066e-05, "close_rate": 3.081368421052631e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5368421052630647e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3261.5786040443577, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 16:35:00+00:00", "close_date": "2018-01-10 17:15:00+00:00", "trade_duration": 40, "open_rate": 0.0168999, "close_rate": 0.016984611278195488, "open_at_end": false, "sell_reason": "roi", "profit": 8.471127819548868e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.917194776300452, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 16:40:00+00:00", "close_date": "2018-01-10 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.09132568, "close_rate": 0.0917834528320802, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004577728320801916, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0949822656672252, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 18:50:00+00:00", "close_date": "2018-01-10 19:45:00+00:00", "trade_duration": 55, "open_rate": 0.08898003, "close_rate": 0.08942604518796991, "open_at_end": false, "sell_reason": "roi", "profit": 0.00044601518796991146, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1238476768326557, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 22:15:00+00:00", "close_date": "2018-01-10 23:00:00+00:00", "trade_duration": 45, "open_rate": 0.08560008, "close_rate": 0.08602915308270676, "open_at_end": false, "sell_reason": "roi", "profit": 0.00042907308270676014, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1682232072680307, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 22:50:00+00:00", "close_date": "2018-01-10 23:20:00+00:00", "trade_duration": 30, "open_rate": 0.00249083, "close_rate": 0.0025282860902255634, "open_at_end": false, "sell_reason": "roi", "profit": 3.745609022556351e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 40.147260150231055, "profit_abs": 0.000999999999999987}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 23:15:00+00:00", "close_date": "2018-01-11 00:15:00+00:00", "trade_duration": 60, "open_rate": 3.022e-05, "close_rate": 3.037147869674185e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5147869674185174e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3309.0668431502318, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-10 23:40:00+00:00", "close_date": "2018-01-11 00:05:00+00:00", "trade_duration": 25, "open_rate": 0.002437, "close_rate": 0.0024980776942355883, "open_at_end": false, "sell_reason": "roi", "profit": 6.107769423558838e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.03405826836274, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 00:00:00+00:00", "close_date": "2018-01-11 00:35:00+00:00", "trade_duration": 35, "open_rate": 0.04771803, "close_rate": 0.04843559436090225, "open_at_end": false, "sell_reason": "roi", "profit": 0.0007175643609022495, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0956439316543456, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-11 03:40:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 45, "open_rate": 3.651e-05, "close_rate": 3.2859000000000005e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -3.650999999999996e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2738.9756231169545, "profit_abs": -0.01047499999999997}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 03:55:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 30, "open_rate": 0.08824105, "close_rate": 0.08956798308270676, "open_at_end": false, "sell_reason": "roi", "profit": 0.0013269330827067605, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1332594070446804, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 04:00:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 50, "open_rate": 0.00243, "close_rate": 0.002442180451127819, "open_at_end": false, "sell_reason": "roi", "profit": 1.2180451127819219e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.1522633744856, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:55:00+00:00", "trade_duration": 25, "open_rate": 0.04545064, "close_rate": 0.046589753784461146, "open_at_end": false, "sell_reason": "roi", "profit": 0.001139113784461146, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.200189040242338, "profit_abs": 0.001999999999999988}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 20, "open_rate": 3.372e-05, "close_rate": 3.456511278195488e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.4511278195488e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2965.599051008304, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:55:00+00:00", "close_date": "2018-01-11 05:15:00+00:00", "trade_duration": 20, "open_rate": 0.02644, "close_rate": 0.02710265664160401, "open_at_end": false, "sell_reason": "roi", "profit": 0.0006626566416040071, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7821482602118004, "profit_abs": 0.001999999999999988}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:20:00+00:00", "close_date": "2018-01-11 12:00:00+00:00", "trade_duration": 40, "open_rate": 0.08812, "close_rate": 0.08856170426065162, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004417042606516125, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1348161597821154, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:35:00+00:00", "close_date": "2018-01-11 12:15:00+00:00", "trade_duration": 40, "open_rate": 0.02683577, "close_rate": 0.026970285137844607, "open_at_end": false, "sell_reason": "roi", "profit": 0.00013451513784460897, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7263696923919087, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 14:00:00+00:00", "close_date": "2018-01-11 14:25:00+00:00", "trade_duration": 25, "open_rate": 4.919e-05, "close_rate": 5.04228320802005e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.232832080200495e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.9335230737956, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 19:25:00+00:00", "close_date": "2018-01-11 20:35:00+00:00", "trade_duration": 70, "open_rate": 0.08784896, "close_rate": 0.08828930566416039, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004403456641603881, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1383174029607181, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:35:00+00:00", "close_date": "2018-01-11 23:30:00+00:00", "trade_duration": 55, "open_rate": 5.105e-05, "close_rate": 5.130588972431077e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.558897243107704e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1958.8638589618022, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:25:00+00:00", "trade_duration": 30, "open_rate": 3.96e-05, "close_rate": 4.019548872180451e-05, "open_at_end": false, "sell_reason": "roi", "profit": 5.954887218045116e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2525.252525252525, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:35:00+00:00", "trade_duration": 40, "open_rate": 2.885e-05, "close_rate": 2.899461152882205e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.4461152882205115e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3466.204506065858, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 23:30:00+00:00", "close_date": "2018-01-12 00:05:00+00:00", "trade_duration": 35, "open_rate": 0.02645, "close_rate": 0.026847744360902256, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003977443609022545, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.780718336483932, "profit_abs": 0.0010000000000000148}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 23:55:00+00:00", "close_date": "2018-01-12 01:15:00+00:00", "trade_duration": 80, "open_rate": 0.048, "close_rate": 0.04824060150375939, "open_at_end": false, "sell_reason": "roi", "profit": 0.00024060150375938838, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0833333333333335, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-12 21:15:00+00:00", "close_date": "2018-01-12 21:40:00+00:00", "trade_duration": 25, "open_rate": 4.692e-05, "close_rate": 4.809593984962405e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.1759398496240516e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2131.287297527707, "profit_abs": 0.001999999999999974}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 00:55:00+00:00", "close_date": "2018-01-13 06:20:00+00:00", "trade_duration": 325, "open_rate": 0.00256966, "close_rate": 0.0025825405012531327, "open_at_end": false, "sell_reason": "roi", "profit": 1.2880501253132587e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.91565421106294, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 10:55:00+00:00", "close_date": "2018-01-13 11:35:00+00:00", "trade_duration": 40, "open_rate": 6.262e-05, "close_rate": 6.293388471177944e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.138847117794446e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1596.933886937081, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 13:05:00+00:00", "close_date": "2018-01-15 14:10:00+00:00", "trade_duration": 2945, "open_rate": 4.73e-05, "close_rate": 4.753709273182957e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.3709273182957117e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2114.1649048625795, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:30:00+00:00", "close_date": "2018-01-13 14:45:00+00:00", "trade_duration": 75, "open_rate": 6.063e-05, "close_rate": 6.0933909774436085e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.039097744360846e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1649.348507339601, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:40:00+00:00", "close_date": "2018-01-13 23:30:00+00:00", "trade_duration": 590, "open_rate": 0.00011082, "close_rate": 0.00011137548872180448, "open_at_end": false, "sell_reason": "roi", "profit": 5.554887218044781e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 902.3641941887746, "profit_abs": -2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 15:15:00+00:00", "close_date": "2018-01-13 15:55:00+00:00", "trade_duration": 40, "open_rate": 5.93e-05, "close_rate": 5.9597243107769415e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.9724310776941686e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1686.3406408094436, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 16:30:00+00:00", "close_date": "2018-01-13 17:10:00+00:00", "trade_duration": 40, "open_rate": 0.04850003, "close_rate": 0.04874313791979949, "open_at_end": false, "sell_reason": "roi", "profit": 0.00024310791979949287, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0618543947292407, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 22:05:00+00:00", "close_date": "2018-01-14 06:25:00+00:00", "trade_duration": 500, "open_rate": 0.09825019, "close_rate": 0.09874267215538848, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004924821553884823, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0178097365511456, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 00:20:00+00:00", "close_date": "2018-01-14 22:55:00+00:00", "trade_duration": 1355, "open_rate": 6.018e-05, "close_rate": 6.048165413533834e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.0165413533833987e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1661.681621801263, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 12:45:00+00:00", "close_date": "2018-01-14 13:25:00+00:00", "trade_duration": 40, "open_rate": 0.09758999, "close_rate": 0.0980791628822055, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004891728822054991, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.024695258191952, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-14 15:30:00+00:00", "close_date": "2018-01-14 16:00:00+00:00", "trade_duration": 30, "open_rate": 0.00311, "close_rate": 0.0031567669172932328, "open_at_end": false, "sell_reason": "roi", "profit": 4.676691729323286e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.154340836012864, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 20:45:00+00:00", "close_date": "2018-01-14 22:15:00+00:00", "trade_duration": 90, "open_rate": 0.00312401, "close_rate": 0.003139669197994987, "open_at_end": false, "sell_reason": "roi", "profit": 1.5659197994987058e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.010140812609436, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 23:35:00+00:00", "close_date": "2018-01-15 00:30:00+00:00", "trade_duration": 55, "open_rate": 0.0174679, "close_rate": 0.017555458395989976, "open_at_end": false, "sell_reason": "roi", "profit": 8.755839598997492e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.724786608579165, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 23:45:00+00:00", "close_date": "2018-01-15 00:25:00+00:00", "trade_duration": 40, "open_rate": 0.07346846, "close_rate": 0.07383672295739348, "open_at_end": false, "sell_reason": "roi", "profit": 0.00036826295739347814, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.3611282991367997, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 02:25:00+00:00", "close_date": "2018-01-15 03:05:00+00:00", "trade_duration": 40, "open_rate": 0.097994, "close_rate": 0.09848519799498744, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004911979949874384, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.020470641059657, "profit_abs": -2.7755575615628914e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 07:20:00+00:00", "close_date": "2018-01-15 08:00:00+00:00", "trade_duration": 40, "open_rate": 0.09659, "close_rate": 0.09707416040100247, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004841604010024786, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0353038616834043, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-15 08:20:00+00:00", "close_date": "2018-01-15 08:55:00+00:00", "trade_duration": 35, "open_rate": 9.987e-05, "close_rate": 0.00010137180451127818, "open_at_end": false, "sell_reason": "roi", "profit": 1.501804511278178e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1001.3016921998599, "profit_abs": 0.0010000000000000009}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-15 12:10:00+00:00", "close_date": "2018-01-16 02:50:00+00:00", "trade_duration": 880, "open_rate": 0.0948969, "close_rate": 0.09537257368421052, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004756736842105175, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0537752023511833, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:10:00+00:00", "close_date": "2018-01-15 17:40:00+00:00", "trade_duration": 210, "open_rate": 0.071, "close_rate": 0.07135588972431077, "open_at_end": false, "sell_reason": "roi", "profit": 0.00035588972431077615, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4084507042253522, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:30:00+00:00", "close_date": "2018-01-15 15:10:00+00:00", "trade_duration": 40, "open_rate": 0.04600501, "close_rate": 0.046235611553884705, "open_at_end": false, "sell_reason": "roi", "profit": 0.00023060155388470588, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.173676301776698, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:10:00+00:00", "close_date": "2018-01-15 19:25:00+00:00", "trade_duration": 75, "open_rate": 9.438e-05, "close_rate": 9.485308270676693e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.7308270676692514e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1059.5465140919687, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:35:00+00:00", "close_date": "2018-01-15 19:15:00+00:00", "trade_duration": 40, "open_rate": 0.03040001, "close_rate": 0.030552391002506264, "open_at_end": false, "sell_reason": "roi", "profit": 0.0001523810025062626, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.2894726021471703, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-15 20:25:00+00:00", "close_date": "2018-01-16 08:25:00+00:00", "trade_duration": 720, "open_rate": 5.837e-05, "close_rate": 5.2533e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -5.8369999999999985e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1713.2088401576154, "profit_abs": -0.010474999999999984}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 20:40:00+00:00", "close_date": "2018-01-15 22:00:00+00:00", "trade_duration": 80, "open_rate": 0.046036, "close_rate": 0.04626675689223057, "open_at_end": false, "sell_reason": "roi", "profit": 0.00023075689223057277, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1722130506560084, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 00:30:00+00:00", "close_date": "2018-01-16 01:10:00+00:00", "trade_duration": 40, "open_rate": 0.0028685, "close_rate": 0.0028828784461152877, "open_at_end": false, "sell_reason": "roi", "profit": 1.4378446115287727e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 34.86142583231654, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 01:15:00+00:00", "close_date": "2018-01-16 02:35:00+00:00", "trade_duration": 80, "open_rate": 0.06731755, "close_rate": 0.0676549813283208, "open_at_end": false, "sell_reason": "roi", "profit": 0.00033743132832080025, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4854967241083492, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 07:45:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 55, "open_rate": 0.09217614, "close_rate": 0.09263817578947368, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004620357894736804, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0848794492804754, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:55:00+00:00", "trade_duration": 20, "open_rate": 0.0165, "close_rate": 0.016913533834586467, "open_at_end": false, "sell_reason": "roi", "profit": 0.00041353383458646656, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.0606060606060606, "profit_abs": 0.0020000000000000018}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 5, "open_rate": 7.953e-05, "close_rate": 8.311781954887218e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.587819548872171e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1257.387149503332, "profit_abs": 0.00399999999999999}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 08:45:00+00:00", "close_date": "2018-01-16 09:50:00+00:00", "trade_duration": 65, "open_rate": 0.045202, "close_rate": 0.04542857644110275, "open_at_end": false, "sell_reason": "roi", "profit": 0.00022657644110275071, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2122914915269236, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:45:00+00:00", "trade_duration": 30, "open_rate": 5.248e-05, "close_rate": 5.326917293233082e-05, "open_at_end": false, "sell_reason": "roi", "profit": 7.891729323308177e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1905.487804878049, "profit_abs": 0.0010000000000000009}, {"pair": "XMR/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:55:00+00:00", "trade_duration": 40, "open_rate": 0.02892318, "close_rate": 0.02906815834586466, "open_at_end": false, "sell_reason": "roi", "profit": 0.0001449783458646603, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.457434486802627, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 09:50:00+00:00", "close_date": "2018-01-16 10:10:00+00:00", "trade_duration": 20, "open_rate": 5.158e-05, "close_rate": 5.287273182957392e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.2927318295739246e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1938.735944164405, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:35:00+00:00", "trade_duration": 30, "open_rate": 0.02828232, "close_rate": 0.02870761804511278, "open_at_end": false, "sell_reason": "roi", "profit": 0.00042529804511277913, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5357778286929786, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:40:00+00:00", "trade_duration": 35, "open_rate": 0.04357584, "close_rate": 0.044231115789473675, "open_at_end": false, "sell_reason": "roi", "profit": 0.0006552757894736777, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.294849623093898, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 13:45:00+00:00", "close_date": "2018-01-16 14:20:00+00:00", "trade_duration": 35, "open_rate": 5.362e-05, "close_rate": 5.442631578947368e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.063157894736843e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1864.975755315181, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 17:30:00+00:00", "close_date": "2018-01-16 18:25:00+00:00", "trade_duration": 55, "open_rate": 5.302e-05, "close_rate": 5.328576441102756e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.6576441102756397e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.0807242549984, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:45:00+00:00", "trade_duration": 30, "open_rate": 0.09129999, "close_rate": 0.09267292218045112, "open_at_end": false, "sell_reason": "roi", "profit": 0.0013729321804511196, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0952903718828448, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:35:00+00:00", "trade_duration": 20, "open_rate": 3.808e-05, "close_rate": 3.903438596491228e-05, "open_at_end": false, "sell_reason": "roi", "profit": 9.543859649122774e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2626.0504201680674, "profit_abs": 0.0020000000000000018}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 19:00:00+00:00", "close_date": "2018-01-16 19:30:00+00:00", "trade_duration": 30, "open_rate": 0.02811012, "close_rate": 0.028532828571428567, "open_at_end": false, "sell_reason": "roi", "profit": 0.00042270857142856846, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.557437677249333, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 60, "open_rate": 0.00258379, "close_rate": 0.002325411, "open_at_end": false, "sell_reason": "stop_loss", "profit": -0.000258379, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.702835756775904, "profit_abs": -0.010474999999999984}, {"pair": "NXT/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 80, "open_rate": 2.559e-05, "close_rate": 2.3031e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -2.5590000000000004e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3907.7764751856193, "profit_abs": -0.010474999999999998}, {"pair": "TRX/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:35:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 50, "open_rate": 7.62e-05, "close_rate": 6.858e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -7.619999999999998e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1312.3359580052495, "profit_abs": -0.010474999999999984}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:35:00+00:00", "trade_duration": 5, "open_rate": 0.00229844, "close_rate": 0.002402129022556391, "open_at_end": false, "sell_reason": "roi", "profit": 0.00010368902255639091, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 43.507770487809125, "profit_abs": 0.004000000000000017}, {"pair": "LTC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:40:00+00:00", "trade_duration": 10, "open_rate": 0.0151, "close_rate": 0.015781203007518795, "open_at_end": false, "sell_reason": "roi", "profit": 0.0006812030075187946, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.622516556291391, "profit_abs": 0.00399999999999999}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:40:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 5, "open_rate": 0.00235676, "close_rate": 0.00246308, "open_at_end": false, "sell_reason": "roi", "profit": 0.00010632000000000003, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 42.431134269081284, "profit_abs": 0.0040000000000000036}, {"pair": "DASH/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 22:45:00+00:00", "close_date": "2018-01-16 23:05:00+00:00", "trade_duration": 20, "open_rate": 0.0630692, "close_rate": 0.06464988170426066, "open_at_end": false, "sell_reason": "roi", "profit": 0.0015806817042606502, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.585559988076589, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:50:00+00:00", "close_date": "2018-01-16 22:55:00+00:00", "trade_duration": 5, "open_rate": 2.2e-05, "close_rate": 2.299248120300751e-05, "open_at_end": false, "sell_reason": "roi", "profit": 9.924812030075114e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 4545.454545454546, "profit_abs": 0.003999999999999976}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-17 03:30:00+00:00", "close_date": "2018-01-17 04:00:00+00:00", "trade_duration": 30, "open_rate": 4.974e-05, "close_rate": 5.048796992481203e-05, "open_at_end": false, "sell_reason": "roi", "profit": 7.479699248120277e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2010.454362685967, "profit_abs": 0.0010000000000000009}, {"pair": "TRX/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-17 03:55:00+00:00", "close_date": "2018-01-17 04:15:00+00:00", "trade_duration": 20, "open_rate": 7.108e-05, "close_rate": 7.28614536340852e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.7814536340851996e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1406.8655036578502, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 09:35:00+00:00", "close_date": "2018-01-17 10:15:00+00:00", "trade_duration": 40, "open_rate": 0.04327, "close_rate": 0.04348689223057644, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002168922305764362, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.3110700254217704, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:20:00+00:00", "close_date": "2018-01-17 17:00:00+00:00", "trade_duration": 400, "open_rate": 4.997e-05, "close_rate": 5.022047619047618e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.504761904761831e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2001.2007204322595, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:25:00+00:00", "trade_duration": 55, "open_rate": 0.06836818, "close_rate": 0.06871087764411027, "open_at_end": false, "sell_reason": "roi", "profit": 0.00034269764411026804, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4626687444363737, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:10:00+00:00", "trade_duration": 40, "open_rate": 3.63e-05, "close_rate": 3.648195488721804e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.8195488721804031e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2754.8209366391184, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:30:00+00:00", "close_date": "2018-01-17 22:05:00+00:00", "trade_duration": 575, "open_rate": 0.0281, "close_rate": 0.02824085213032581, "open_at_end": false, "sell_reason": "roi", "profit": 0.0001408521303258095, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5587188612099645, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:35:00+00:00", "close_date": "2018-01-17 16:55:00+00:00", "trade_duration": 260, "open_rate": 0.08651001, "close_rate": 0.08694364413533832, "open_at_end": false, "sell_reason": "roi", "profit": 0.00043363413533832607, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1559355963546878, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 05:00:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 55, "open_rate": 5.633e-05, "close_rate": 5.6612355889724306e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.8235588972430847e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1775.2529735487308, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 05:20:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 35, "open_rate": 0.06988494, "close_rate": 0.07093584135338346, "open_at_end": false, "sell_reason": "roi", "profit": 0.0010509013533834544, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.430923457900944, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 07:35:00+00:00", "close_date": "2018-01-18 08:15:00+00:00", "trade_duration": 40, "open_rate": 5.545e-05, "close_rate": 5.572794486215538e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.779448621553787e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1803.4265103697026, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 09:00:00+00:00", "close_date": "2018-01-18 09:40:00+00:00", "trade_duration": 40, "open_rate": 0.01633527, "close_rate": 0.016417151052631574, "open_at_end": false, "sell_reason": "roi", "profit": 8.188105263157511e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.121723118136401, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 16:40:00+00:00", "close_date": "2018-01-18 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.00269734, "close_rate": 0.002710860501253133, "open_at_end": false, "sell_reason": "roi", "profit": 1.3520501253133123e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.073561360451414, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-18 18:05:00+00:00", "close_date": "2018-01-18 18:30:00+00:00", "trade_duration": 25, "open_rate": 4.475e-05, "close_rate": 4.587155388471177e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.1215538847117757e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2234.63687150838, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 18:25:00+00:00", "close_date": "2018-01-18 18:55:00+00:00", "trade_duration": 30, "open_rate": 2.79e-05, "close_rate": 2.8319548872180444e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.1954887218044365e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3584.2293906810037, "profit_abs": 0.000999999999999987}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 20:10:00+00:00", "close_date": "2018-01-18 20:50:00+00:00", "trade_duration": 40, "open_rate": 0.04439326, "close_rate": 0.04461578260651629, "open_at_end": false, "sell_reason": "roi", "profit": 0.00022252260651629135, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2525942001105577, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 21:30:00+00:00", "close_date": "2018-01-19 00:35:00+00:00", "trade_duration": 185, "open_rate": 4.49e-05, "close_rate": 4.51250626566416e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.2506265664159932e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2227.1714922049, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 21:55:00+00:00", "close_date": "2018-01-19 05:05:00+00:00", "trade_duration": 430, "open_rate": 0.02855, "close_rate": 0.028693107769423555, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014310776942355607, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.502626970227671, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 22:10:00+00:00", "close_date": "2018-01-18 22:50:00+00:00", "trade_duration": 40, "open_rate": 5.796e-05, "close_rate": 5.8250526315789473e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.905263157894727e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1725.3278122843342, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 23:50:00+00:00", "close_date": "2018-01-19 00:30:00+00:00", "trade_duration": 40, "open_rate": 0.04340323, "close_rate": 0.04362079005012531, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002175600501253122, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.303975994413319, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-19 16:45:00+00:00", "close_date": "2018-01-19 17:35:00+00:00", "trade_duration": 50, "open_rate": 0.04454455, "close_rate": 0.04476783095238095, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002232809523809512, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.244943545282195, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:15:00+00:00", "close_date": "2018-01-19 19:55:00+00:00", "trade_duration": 160, "open_rate": 5.62e-05, "close_rate": 5.648170426065162e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.817042606516199e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1779.3594306049824, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:20:00+00:00", "close_date": "2018-01-19 20:15:00+00:00", "trade_duration": 175, "open_rate": 4.339e-05, "close_rate": 4.360749373433584e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.174937343358337e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2304.6784973496196, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-20 04:45:00+00:00", "close_date": "2018-01-20 17:35:00+00:00", "trade_duration": 770, "open_rate": 0.0001009, "close_rate": 0.00010140576441102755, "open_at_end": false, "sell_reason": "roi", "profit": 5.057644110275549e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 991.0802775024778, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 15:15:00+00:00", "trade_duration": 625, "open_rate": 0.00270505, "close_rate": 0.002718609147869674, "open_at_end": false, "sell_reason": "roi", "profit": 1.3559147869673764e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.96789338459548, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 07:00:00+00:00", "trade_duration": 130, "open_rate": 0.03000002, "close_rate": 0.030150396040100245, "open_at_end": false, "sell_reason": "roi", "profit": 0.00015037604010024672, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.3333311111125927, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 09:00:00+00:00", "close_date": "2018-01-20 09:40:00+00:00", "trade_duration": 40, "open_rate": 5.46e-05, "close_rate": 5.4873684210526304e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.736842105263053e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1831.5018315018317, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-20 18:25:00+00:00", "close_date": "2018-01-25 03:50:00+00:00", "trade_duration": 6325, "open_rate": 0.03082222, "close_rate": 0.027739998, "open_at_end": false, "sell_reason": "stop_loss", "profit": -0.0030822220000000025, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.244412634781012, "profit_abs": -0.010474999999999998}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 22:25:00+00:00", "close_date": "2018-01-20 23:15:00+00:00", "trade_duration": 50, "open_rate": 0.08969999, "close_rate": 0.09014961401002504, "open_at_end": false, "sell_reason": "roi", "profit": 0.00044962401002504593, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1148273260677064, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 02:50:00+00:00", "close_date": "2018-01-21 14:30:00+00:00", "trade_duration": 700, "open_rate": 0.01632501, "close_rate": 0.01640683962406015, "open_at_end": false, "sell_reason": "roi", "profit": 8.182962406014932e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.125570520324337, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 10:20:00+00:00", "close_date": "2018-01-21 11:00:00+00:00", "trade_duration": 40, "open_rate": 0.070538, "close_rate": 0.07089157393483708, "open_at_end": false, "sell_reason": "roi", "profit": 0.00035357393483707866, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.417675579120474, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 15:50:00+00:00", "close_date": "2018-01-21 18:45:00+00:00", "trade_duration": 175, "open_rate": 5.301e-05, "close_rate": 5.327571428571427e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.657142857142672e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.4365214110546, "profit_abs": -2.7755575615628914e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 16:20:00+00:00", "close_date": "2018-01-21 17:00:00+00:00", "trade_duration": 40, "open_rate": 3.955e-05, "close_rate": 3.9748245614035085e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.9824561403508552e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2528.4450063211125, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:45:00+00:00", "trade_duration": 30, "open_rate": 0.00258505, "close_rate": 0.002623922932330827, "open_at_end": false, "sell_reason": "roi", "profit": 3.8872932330826816e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.6839712964933, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:55:00+00:00", "trade_duration": 40, "open_rate": 3.903e-05, "close_rate": 3.922563909774435e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.9563909774435151e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2562.1316935690497, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 00:35:00+00:00", "close_date": "2018-01-22 10:35:00+00:00", "trade_duration": 600, "open_rate": 5.236e-05, "close_rate": 5.262245614035087e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.624561403508717e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1909.8548510313217, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 01:30:00+00:00", "close_date": "2018-01-22 02:10:00+00:00", "trade_duration": 40, "open_rate": 9.028e-05, "close_rate": 9.07325313283208e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.5253132832080657e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1107.6650420912717, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 12:25:00+00:00", "close_date": "2018-01-22 14:35:00+00:00", "trade_duration": 130, "open_rate": 0.002687, "close_rate": 0.002700468671679198, "open_at_end": false, "sell_reason": "roi", "profit": 1.3468671679197925e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.21622627465575, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 13:15:00+00:00", "close_date": "2018-01-22 13:55:00+00:00", "trade_duration": 40, "open_rate": 4.168e-05, "close_rate": 4.188892230576441e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.0892230576441054e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2399.232245681382, "profit_abs": 1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-22 14:00:00+00:00", "close_date": "2018-01-22 14:30:00+00:00", "trade_duration": 30, "open_rate": 8.821e-05, "close_rate": 8.953646616541353e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.326466165413529e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1133.6583153837435, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 15:55:00+00:00", "close_date": "2018-01-22 16:40:00+00:00", "trade_duration": 45, "open_rate": 5.172e-05, "close_rate": 5.1979248120300745e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.592481203007459e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1933.4880123743235, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-22 16:05:00+00:00", "close_date": "2018-01-22 16:25:00+00:00", "trade_duration": 20, "open_rate": 3.026e-05, "close_rate": 3.101839598997494e-05, "open_at_end": false, "sell_reason": "roi", "profit": 7.5839598997494e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3304.692663582287, "profit_abs": 0.0020000000000000157}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 19:50:00+00:00", "close_date": "2018-01-23 00:10:00+00:00", "trade_duration": 260, "open_rate": 0.07064, "close_rate": 0.07099408521303258, "open_at_end": false, "sell_reason": "roi", "profit": 0.00035408521303258167, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.415628539071348, "profit_abs": 1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 21:25:00+00:00", "close_date": "2018-01-22 22:05:00+00:00", "trade_duration": 40, "open_rate": 0.01644483, "close_rate": 0.01652726022556391, "open_at_end": false, "sell_reason": "roi", "profit": 8.243022556390922e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.080938507725528, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-23 00:05:00+00:00", "close_date": "2018-01-23 00:35:00+00:00", "trade_duration": 30, "open_rate": 4.331e-05, "close_rate": 4.3961278195488714e-05, "open_at_end": false, "sell_reason": "roi", "profit": 6.512781954887175e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2308.935580697299, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-23 01:50:00+00:00", "close_date": "2018-01-23 02:15:00+00:00", "trade_duration": 25, "open_rate": 3.2e-05, "close_rate": 3.2802005012531326e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.020050125313278e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3125.0000000000005, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 04:25:00+00:00", "close_date": "2018-01-23 05:15:00+00:00", "trade_duration": 50, "open_rate": 0.09167706, "close_rate": 0.09213659413533835, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004595341353383492, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0907854156754153, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 07:35:00+00:00", "close_date": "2018-01-23 09:00:00+00:00", "trade_duration": 85, "open_rate": 0.0692498, "close_rate": 0.06959691679197995, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003471167919799484, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4440474918339115, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 10:50:00+00:00", "close_date": "2018-01-23 13:05:00+00:00", "trade_duration": 135, "open_rate": 3.182e-05, "close_rate": 3.197949874686716e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.594987468671663e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3142.677561282213, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 11:05:00+00:00", "close_date": "2018-01-23 16:05:00+00:00", "trade_duration": 300, "open_rate": 0.04088, "close_rate": 0.04108491228070175, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002049122807017481, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4461839530332683, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 14:55:00+00:00", "close_date": "2018-01-23 15:35:00+00:00", "trade_duration": 40, "open_rate": 5.15e-05, "close_rate": 5.175814536340851e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.5814536340851513e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1941.747572815534, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 16:35:00+00:00", "close_date": "2018-01-24 00:05:00+00:00", "trade_duration": 450, "open_rate": 0.09071698, "close_rate": 0.09117170170426064, "open_at_end": false, "sell_reason": "roi", "profit": 0.00045472170426064107, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1023294646713329, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 17:25:00+00:00", "close_date": "2018-01-23 18:45:00+00:00", "trade_duration": 80, "open_rate": 3.128e-05, "close_rate": 3.1436791979949865e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5679197994986587e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3196.9309462915603, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 20:15:00+00:00", "close_date": "2018-01-23 22:00:00+00:00", "trade_duration": 105, "open_rate": 9.555e-05, "close_rate": 9.602894736842104e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.789473684210343e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1046.5724751439038, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 22:30:00+00:00", "close_date": "2018-01-23 23:10:00+00:00", "trade_duration": 40, "open_rate": 0.04080001, "close_rate": 0.0410045213283208, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020451132832080554, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.450979791426522, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 23:50:00+00:00", "close_date": "2018-01-24 03:35:00+00:00", "trade_duration": 225, "open_rate": 5.163e-05, "close_rate": 5.18887969924812e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.587969924812037e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1936.8584156498162, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 00:20:00+00:00", "close_date": "2018-01-24 01:50:00+00:00", "trade_duration": 90, "open_rate": 0.04040781, "close_rate": 0.04061035541353383, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002025454135338306, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.474769110228938, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 06:45:00+00:00", "close_date": "2018-01-24 07:25:00+00:00", "trade_duration": 40, "open_rate": 5.132e-05, "close_rate": 5.157724310776942e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.5724310776941724e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1948.5580670303975, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-24 14:15:00+00:00", "close_date": "2018-01-24 14:25:00+00:00", "trade_duration": 10, "open_rate": 5.198e-05, "close_rate": 5.432496240601503e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.344962406015033e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1923.8168526356292, "profit_abs": 0.0040000000000000036}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 14:50:00+00:00", "close_date": "2018-01-24 16:35:00+00:00", "trade_duration": 105, "open_rate": 3.054e-05, "close_rate": 3.069308270676692e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5308270676691466e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3274.3942370661425, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 15:10:00+00:00", "close_date": "2018-01-24 16:15:00+00:00", "trade_duration": 65, "open_rate": 9.263e-05, "close_rate": 9.309431077694236e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.6431077694236234e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1079.5638562020945, "profit_abs": 2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 22:40:00+00:00", "close_date": "2018-01-24 23:25:00+00:00", "trade_duration": 45, "open_rate": 5.514e-05, "close_rate": 5.54163909774436e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.7639097744360576e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1813.5654697134569, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 00:50:00+00:00", "close_date": "2018-01-25 01:30:00+00:00", "trade_duration": 40, "open_rate": 4.921e-05, "close_rate": 4.9456666666666664e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.4666666666666543e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.1072952651903, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 08:15:00+00:00", "close_date": "2018-01-25 12:15:00+00:00", "trade_duration": 240, "open_rate": 0.0026, "close_rate": 0.002613032581453634, "open_at_end": false, "sell_reason": "roi", "profit": 1.3032581453634e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.46153846153847, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 10:25:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 350, "open_rate": 0.02799871, "close_rate": 0.028139054411027563, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014034441102756326, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.571593119825878, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 11:00:00+00:00", "close_date": "2018-01-25 11:45:00+00:00", "trade_duration": 45, "open_rate": 0.04078902, "close_rate": 0.0409934762406015, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020445624060149575, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4516401717913303, "profit_abs": -1.3877787807814457e-17}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:05:00+00:00", "close_date": "2018-01-25 13:45:00+00:00", "trade_duration": 40, "open_rate": 2.89e-05, "close_rate": 2.904486215538847e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.4486215538846723e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3460.2076124567475, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:20:00+00:00", "close_date": "2018-01-25 14:05:00+00:00", "trade_duration": 45, "open_rate": 0.041103, "close_rate": 0.04130903007518797, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020603007518796984, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4329124394813033, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-25 15:45:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 30, "open_rate": 5.428e-05, "close_rate": 5.509624060150376e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.162406015037611e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1842.2991893883568, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 17:45:00+00:00", "close_date": "2018-01-25 23:15:00+00:00", "trade_duration": 330, "open_rate": 5.414e-05, "close_rate": 5.441137844611528e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.713784461152774e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1847.063169560399, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 21:15:00+00:00", "close_date": "2018-01-25 21:55:00+00:00", "trade_duration": 40, "open_rate": 0.04140777, "close_rate": 0.0416153277443609, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002075577443608964, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.415005686130888, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 02:05:00+00:00", "close_date": "2018-01-26 02:45:00+00:00", "trade_duration": 40, "open_rate": 0.00254309, "close_rate": 0.002555837318295739, "open_at_end": false, "sell_reason": "roi", "profit": 1.2747318295739177e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.32224183965177, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 02:55:00+00:00", "close_date": "2018-01-26 15:10:00+00:00", "trade_duration": 735, "open_rate": 5.607e-05, "close_rate": 5.6351052631578935e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.810526315789381e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1783.4849295523454, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 06:10:00+00:00", "close_date": "2018-01-26 09:25:00+00:00", "trade_duration": 195, "open_rate": 0.00253806, "close_rate": 0.0025507821052631577, "open_at_end": false, "sell_reason": "roi", "profit": 1.2722105263157733e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.400171784748984, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 07:25:00+00:00", "close_date": "2018-01-26 09:55:00+00:00", "trade_duration": 150, "open_rate": 0.0415, "close_rate": 0.04170802005012531, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020802005012530989, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4096385542168677, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-26 09:55:00+00:00", "close_date": "2018-01-26 10:25:00+00:00", "trade_duration": 30, "open_rate": 5.321e-05, "close_rate": 5.401015037593984e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.00150375939842e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1879.3459875963165, "profit_abs": 0.000999999999999987}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 16:05:00+00:00", "close_date": "2018-01-26 16:45:00+00:00", "trade_duration": 40, "open_rate": 0.02772046, "close_rate": 0.02785940967418546, "open_at_end": false, "sell_reason": "roi", "profit": 0.00013894967418546025, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.6074437437185387, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 23:35:00+00:00", "close_date": "2018-01-27 00:15:00+00:00", "trade_duration": 40, "open_rate": 0.09461341, "close_rate": 0.09508766268170424, "open_at_end": false, "sell_reason": "roi", "profit": 0.00047425268170424306, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0569326272036914, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 00:35:00+00:00", "close_date": "2018-01-27 01:30:00+00:00", "trade_duration": 55, "open_rate": 5.615e-05, "close_rate": 5.643145363408521e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.814536340852038e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1780.9439002671415, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.07877175, "open_date": "2018-01-27 00:45:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 4560, "open_rate": 5.556e-05, "close_rate": 5.144e-05, "open_at_end": true, "sell_reason": "force_sell", "profit": -4.120000000000001e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1799.8560115190785, "profit_abs": -0.007896868250539965}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 02:30:00+00:00", "close_date": "2018-01-27 11:25:00+00:00", "trade_duration": 535, "open_rate": 0.06900001, "close_rate": 0.06934587471177944, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003458647117794422, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492751522789635, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 06:25:00+00:00", "close_date": "2018-01-27 07:05:00+00:00", "trade_duration": 40, "open_rate": 0.09449985, "close_rate": 0.0949735334586466, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004736834586466093, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.058202737887944, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.04815133, "open_date": "2018-01-27 09:40:00+00:00", "close_date": "2018-01-30 04:40:00+00:00", "trade_duration": 4020, "open_rate": 0.0410697, "close_rate": 0.03928809, "open_at_end": true, "sell_reason": "force_sell", "profit": -0.001781610000000003, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4348850855983852, "profit_abs": -0.004827170578309559}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 11:45:00+00:00", "close_date": "2018-01-27 12:30:00+00:00", "trade_duration": 45, "open_rate": 0.0285, "close_rate": 0.02864285714285714, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014285714285713902, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5087719298245617, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 12:35:00+00:00", "close_date": "2018-01-27 15:25:00+00:00", "trade_duration": 170, "open_rate": 0.02866372, "close_rate": 0.02880739779448621, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014367779448621124, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.4887307020861216, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 15:50:00+00:00", "close_date": "2018-01-27 16:50:00+00:00", "trade_duration": 60, "open_rate": 0.095381, "close_rate": 0.09585910025062656, "open_at_end": false, "sell_reason": "roi", "profit": 0.00047810025062657024, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0484268355332824, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 17:05:00+00:00", "close_date": "2018-01-27 17:45:00+00:00", "trade_duration": 40, "open_rate": 0.06759092, "close_rate": 0.06792972160401002, "open_at_end": false, "sell_reason": "roi", "profit": 0.00033880160401002224, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4794886650455417, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 23:40:00+00:00", "close_date": "2018-01-28 01:05:00+00:00", "trade_duration": 85, "open_rate": 0.00258501, "close_rate": 0.002597967443609022, "open_at_end": false, "sell_reason": "roi", "profit": 1.2957443609021985e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.684569885609726, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 02:25:00+00:00", "close_date": "2018-01-28 08:10:00+00:00", "trade_duration": 345, "open_rate": 0.06698502, "close_rate": 0.0673207845112782, "open_at_end": false, "sell_reason": "roi", "profit": 0.00033576451127818874, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4928710926711672, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 10:25:00+00:00", "close_date": "2018-01-28 16:30:00+00:00", "trade_duration": 365, "open_rate": 0.0677177, "close_rate": 0.06805713709273183, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003394370927318202, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4767187899175547, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-28 20:35:00+00:00", "close_date": "2018-01-28 21:35:00+00:00", "trade_duration": 60, "open_rate": 5.215e-05, "close_rate": 5.2411403508771925e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.6140350877192417e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1917.5455417066157, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-28 22:00:00+00:00", "close_date": "2018-01-28 22:30:00+00:00", "trade_duration": 30, "open_rate": 0.00273809, "close_rate": 0.002779264285714285, "open_at_end": false, "sell_reason": "roi", "profit": 4.117428571428529e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.5218089982433, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-29 00:00:00+00:00", "close_date": "2018-01-29 00:30:00+00:00", "trade_duration": 30, "open_rate": 0.00274632, "close_rate": 0.002787618045112782, "open_at_end": false, "sell_reason": "roi", "profit": 4.129804511278194e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.412362725392526, "profit_abs": 0.0010000000000000148}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-29 02:15:00+00:00", "close_date": "2018-01-29 03:00:00+00:00", "trade_duration": 45, "open_rate": 0.01622478, "close_rate": 0.016306107218045113, "open_at_end": false, "sell_reason": "roi", "profit": 8.132721804511231e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.163411768911504, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 03:05:00+00:00", "close_date": "2018-01-29 03:45:00+00:00", "trade_duration": 40, "open_rate": 0.069, "close_rate": 0.06934586466165413, "open_at_end": false, "sell_reason": "roi", "profit": 0.00034586466165412166, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492753623188406, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 05:20:00+00:00", "close_date": "2018-01-29 06:55:00+00:00", "trade_duration": 95, "open_rate": 8.755e-05, "close_rate": 8.798884711779448e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.3884711779447504e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1142.204454597373, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 07:00:00+00:00", "close_date": "2018-01-29 19:25:00+00:00", "trade_duration": 745, "open_rate": 0.06825763, "close_rate": 0.06859977350877192, "open_at_end": false, "sell_reason": "roi", "profit": 0.00034214350877191657, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4650376815016872, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 19:45:00+00:00", "close_date": "2018-01-29 20:25:00+00:00", "trade_duration": 40, "open_rate": 0.06713892, "close_rate": 0.06747545593984962, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003365359398496137, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4894490408841845, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0199116, "open_date": "2018-01-29 23:30:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 315, "open_rate": 8.934e-05, "close_rate": 8.8e-05, "open_at_end": true, "sell_reason": "force_sell", "profit": -1.3399999999999973e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1119.3194537721067, "profit_abs": -0.0019961383478844796}], "results_per_pair": [{"key": "TRX/BTC", "trades": 15, "profit_mean": 0.0023467073333333323, "profit_mean_pct": 0.23467073333333321, "profit_sum": 0.035200609999999986, "profit_sum_pct": 3.5200609999999988, "profit_total_abs": 0.0035288616521155086, "profit_total_pct": 1.1733536666666662, "duration_avg": "2:28:00", "wins": 9, "draws": 2, "losses": 4}, {"key": "ADA/BTC", "trades": 29, "profit_mean": -0.0011598141379310352, "profit_mean_pct": -0.11598141379310352, "profit_sum": -0.03363461000000002, "profit_sum_pct": -3.3634610000000023, "profit_total_abs": -0.0033718682505400333, "profit_total_pct": -1.1211536666666675, "duration_avg": "5:35:00", "wins": 9, "draws": 11, "losses": 9}, {"key": "XLM/BTC", "trades": 21, "profit_mean": 0.0026243899999999994, "profit_mean_pct": 0.2624389999999999, "profit_sum": 0.05511218999999999, "profit_sum_pct": 5.511218999999999, "profit_total_abs": 0.005525000000000002, "profit_total_pct": 1.8370729999999995, "duration_avg": "3:21:00", "wins": 12, "draws": 3, "losses": 6}, {"key": "ETH/BTC", "trades": 21, "profit_mean": 0.0009500057142857142, "profit_mean_pct": 0.09500057142857142, "profit_sum": 0.01995012, "profit_sum_pct": 1.9950119999999998, "profit_total_abs": 0.0019999999999999463, "profit_total_pct": 0.6650039999999999, "duration_avg": "2:17:00", "wins": 5, "draws": 10, "losses": 6}, {"key": "XMR/BTC", "trades": 16, "profit_mean": -0.0027899012500000007, "profit_mean_pct": -0.2789901250000001, "profit_sum": -0.04463842000000001, "profit_sum_pct": -4.463842000000001, "profit_total_abs": -0.0044750000000000345, "profit_total_pct": -1.4879473333333337, "duration_avg": "8:41:00", "wins": 6, "draws": 5, "losses": 5}, {"key": "ZEC/BTC", "trades": 21, "profit_mean": -0.00039290904761904774, "profit_mean_pct": -0.03929090476190478, "profit_sum": -0.008251090000000003, "profit_sum_pct": -0.8251090000000003, "profit_total_abs": -0.000827170578309569, "profit_total_pct": -0.27503633333333344, "duration_avg": "4:17:00", "wins": 8, "draws": 7, "losses": 6}, {"key": "NXT/BTC", "trades": 12, "profit_mean": -0.0012261025000000006, "profit_mean_pct": -0.12261025000000006, "profit_sum": -0.014713230000000008, "profit_sum_pct": -1.4713230000000008, "profit_total_abs": -0.0014750000000000874, "profit_total_pct": -0.4904410000000003, "duration_avg": "0:57:00", "wins": 4, "draws": 3, "losses": 5}, {"key": "LTC/BTC", "trades": 8, "profit_mean": 0.00748129625, "profit_mean_pct": 0.748129625, "profit_sum": 0.05985037, "profit_sum_pct": 5.985037, "profit_total_abs": 0.006000000000000019, "profit_total_pct": 1.9950123333333334, "duration_avg": "1:59:00", "wins": 5, "draws": 2, "losses": 1}, {"key": "ETC/BTC", "trades": 20, "profit_mean": 0.0022568569999999997, "profit_mean_pct": 0.22568569999999996, "profit_sum": 0.04513713999999999, "profit_sum_pct": 4.513713999999999, "profit_total_abs": 0.004525000000000001, "profit_total_pct": 1.504571333333333, "duration_avg": "1:45:00", "wins": 11, "draws": 4, "losses": 5}, {"key": "DASH/BTC", "trades": 16, "profit_mean": 0.0018703237499999997, "profit_mean_pct": 0.18703237499999997, "profit_sum": 0.029925179999999996, "profit_sum_pct": 2.9925179999999996, "profit_total_abs": 0.002999999999999961, "profit_total_pct": 0.9975059999999999, "duration_avg": "3:03:00", "wins": 4, "draws": 7, "losses": 5}, {"key": "TOTAL", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}], "sell_reason_summary": [{"sell_reason": "roi", "trades": 170, "wins": 73, "draws": 54, "losses": 43, "profit_mean": 0.005398268352941177, "profit_mean_pct": 0.54, "profit_sum": 0.91770562, "profit_sum_pct": 91.77, "profit_total_abs": 0.09199999999999964, "profit_pct_total": 30.59}, {"sell_reason": "stop_loss", "trades": 6, "wins": 0, "draws": 0, "losses": 6, "profit_mean": -0.10448878000000002, "profit_mean_pct": -10.45, "profit_sum": -0.6269326800000001, "profit_sum_pct": -62.69, "profit_total_abs": -0.06284999999999992, "profit_pct_total": -20.9}, {"sell_reason": "force_sell", "trades": 3, "wins": 0, "draws": 0, "losses": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.89, "profit_sum": -0.14683468, "profit_sum_pct": -14.68, "profit_total_abs": -0.014720177176734003, "profit_pct_total": -4.89}], "left_open_trades": [{"key": "TRX/BTC", "trades": 1, "profit_mean": -0.0199116, "profit_mean_pct": -1.9911600000000003, "profit_sum": -0.0199116, "profit_sum_pct": -1.9911600000000003, "profit_total_abs": -0.0019961383478844796, "profit_total_pct": -0.6637200000000001, "duration_avg": "5:15:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ADA/BTC", "trades": 1, "profit_mean": -0.07877175, "profit_mean_pct": -7.877175, "profit_sum": -0.07877175, "profit_sum_pct": -7.877175, "profit_total_abs": -0.007896868250539965, "profit_total_pct": -2.625725, "duration_avg": "3 days, 4:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ZEC/BTC", "trades": 1, "profit_mean": -0.04815133, "profit_mean_pct": -4.815133, "profit_sum": -0.04815133, "profit_sum_pct": -4.815133, "profit_total_abs": -0.004827170578309559, "profit_total_pct": -1.6050443333333335, "duration_avg": "2 days, 19:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "TOTAL", "trades": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.894489333333333, "profit_sum": -0.14683468, "profit_sum_pct": -14.683468, "profit_total_abs": -0.014720177176734003, "profit_total_pct": -4.8944893333333335, "duration_avg": "2 days, 1:25:00", "wins": 0, "draws": 0, "losses": 3}], "total_trades": 179, "backtest_start": "2018-01-30 04:45:00+00:00", "backtest_start_ts": 1517287500, "backtest_end": "2018-01-30 04:45:00+00:00", "backtest_end_ts": 1517287500, "backtest_days": 0, "trades_per_day": null, "market_change": 0.25, "stake_amount": 0.1, "max_drawdown": 0.21142322000000008, "drawdown_start": "2018-01-24 14:25:00+00:00", "drawdown_start_ts": 1516803900.0, "drawdown_end": "2018-01-30 04:45:00+00:00", "drawdown_end_ts": 1517287500.0}}, "strategy_comparison": [{"key": "DefaultStrategy", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}]} diff --git a/tests/testdata/backtest-result_test copy.json b/tests/testdata/backtest-result_test copy.json deleted file mode 100644 index 0395830d4..000000000 --- a/tests/testdata/backtest-result_test copy.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "ASDF": {, - "trades": [], - "metrics":[], - } -} -[["TRX/BTC",0.03990025,1515568500.0,1515568800.0,27,5,9.64e-05,0.00010074887218045112,false,"roi"],["ADA/BTC",0.03990025,1515568500.0,1515569400.0,27,15,4.756e-05,4.9705563909774425e-05,false,"roi"],["XLM/BTC",0.03990025,1515569100.0,1515569700.0,29,10,3.339e-05,3.489631578947368e-05,false,"roi"],["TRX/BTC",0.03990025,1515569100.0,1515570000.0,29,15,9.696e-05,0.00010133413533834584,false,"roi"],["ETH/BTC",-0.0,1515569700.0,1515573300.0,31,60,0.0943,0.09477268170426063,false,"roi"],["XMR/BTC",0.00997506,1515570000.0,1515571800.0,32,30,0.02719607,0.02760503345864661,false,"roi"],["ZEC/BTC",0.0,1515572100.0,1515578100.0,39,100,0.04634952,0.046581848421052625,false,"roi"],["NXT/BTC",-0.0,1515595500.0,1515599400.0,117,65,3.066e-05,3.081368421052631e-05,false,"roi"],["LTC/BTC",0.0,1515602100.0,1515604500.0,139,40,0.0168999,0.016984611278195488,false,"roi"],["ETH/BTC",-0.0,1515602400.0,1515604800.0,140,40,0.09132568,0.0917834528320802,false,"roi"],["ETH/BTC",-0.0,1515610200.0,1515613500.0,166,55,0.08898003,0.08942604518796991,false,"roi"],["ETH/BTC",0.0,1515622500.0,1515625200.0,207,45,0.08560008,0.08602915308270676,false,"roi"],["ETC/BTC",0.00997506,1515624600.0,1515626400.0,214,30,0.00249083,0.0025282860902255634,false,"roi"],["NXT/BTC",-0.0,1515626100.0,1515629700.0,219,60,3.022e-05,3.037147869674185e-05,false,"roi"],["ETC/BTC",0.01995012,1515627600.0,1515629100.0,224,25,0.002437,0.0024980776942355883,false,"roi"],["ZEC/BTC",0.00997506,1515628800.0,1515630900.0,228,35,0.04771803,0.04843559436090225,false,"roi"],["XLM/BTC",-0.10448878,1515642000.0,1515644700.0,272,45,3.651e-05,3.2859000000000005e-05,false,"stop_loss"],["ETH/BTC",0.00997506,1515642900.0,1515644700.0,275,30,0.08824105,0.08956798308270676,false,"roi"],["ETC/BTC",-0.0,1515643200.0,1515646200.0,276,50,0.00243,0.002442180451127819,false,"roi"],["ZEC/BTC",0.01995012,1515645000.0,1515646500.0,282,25,0.04545064,0.046589753784461146,false,"roi"],["XLM/BTC",0.01995012,1515645000.0,1515646200.0,282,20,3.372e-05,3.456511278195488e-05,false,"roi"],["XMR/BTC",0.01995012,1515646500.0,1515647700.0,287,20,0.02644,0.02710265664160401,false,"roi"],["ETH/BTC",-0.0,1515669600.0,1515672000.0,364,40,0.08812,0.08856170426065162,false,"roi"],["XMR/BTC",-0.0,1515670500.0,1515672900.0,367,40,0.02683577,0.026970285137844607,false,"roi"],["ADA/BTC",0.01995012,1515679200.0,1515680700.0,396,25,4.919e-05,5.04228320802005e-05,false,"roi"],["ETH/BTC",-0.0,1515698700.0,1515702900.0,461,70,0.08784896,0.08828930566416039,false,"roi"],["ADA/BTC",-0.0,1515710100.0,1515713400.0,499,55,5.105e-05,5.130588972431077e-05,false,"roi"],["XLM/BTC",0.00997506,1515711300.0,1515713100.0,503,30,3.96e-05,4.019548872180451e-05,false,"roi"],["NXT/BTC",-0.0,1515711300.0,1515713700.0,503,40,2.885e-05,2.899461152882205e-05,false,"roi"],["XMR/BTC",0.00997506,1515713400.0,1515715500.0,510,35,0.02645,0.026847744360902256,false,"roi"],["ZEC/BTC",-0.0,1515714900.0,1515719700.0,515,80,0.048,0.04824060150375939,false,"roi"],["XLM/BTC",0.01995012,1515791700.0,1515793200.0,771,25,4.692e-05,4.809593984962405e-05,false,"roi"],["ETC/BTC",-0.0,1515804900.0,1515824400.0,815,325,0.00256966,0.0025825405012531327,false,"roi"],["ADA/BTC",0.0,1515840900.0,1515843300.0,935,40,6.262e-05,6.293388471177944e-05,false,"roi"],["XLM/BTC",0.0,1515848700.0,1516025400.0,961,2945,4.73e-05,4.753709273182957e-05,false,"roi"],["ADA/BTC",-0.0,1515850200.0,1515854700.0,966,75,6.063e-05,6.0933909774436085e-05,false,"roi"],["TRX/BTC",-0.0,1515850800.0,1515886200.0,968,590,0.00011082,0.00011137548872180449,false,"roi"],["ADA/BTC",-0.0,1515856500.0,1515858900.0,987,40,5.93e-05,5.9597243107769415e-05,false,"roi"],["ZEC/BTC",-0.0,1515861000.0,1515863400.0,1002,40,0.04850003,0.04874313791979949,false,"roi"],["ETH/BTC",-0.0,1515881100.0,1515911100.0,1069,500,0.09825019,0.09874267215538847,false,"roi"],["ADA/BTC",0.0,1515889200.0,1515970500.0,1096,1355,6.018e-05,6.048165413533834e-05,false,"roi"],["ETH/BTC",-0.0,1515933900.0,1515936300.0,1245,40,0.09758999,0.0980791628822055,false,"roi"],["ETC/BTC",0.00997506,1515943800.0,1515945600.0,1278,30,0.00311,0.0031567669172932328,false,"roi"],["ETC/BTC",-0.0,1515962700.0,1515968100.0,1341,90,0.00312401,0.003139669197994987,false,"roi"],["LTC/BTC",0.0,1515972900.0,1515976200.0,1375,55,0.0174679,0.017555458395989976,false,"roi"],["DASH/BTC",-0.0,1515973500.0,1515975900.0,1377,40,0.07346846,0.07383672295739348,false,"roi"],["ETH/BTC",-0.0,1515983100.0,1515985500.0,1409,40,0.097994,0.09848519799498745,false,"roi"],["ETH/BTC",-0.0,1516000800.0,1516003200.0,1468,40,0.09659,0.09707416040100249,false,"roi"],["TRX/BTC",0.00997506,1516004400.0,1516006500.0,1480,35,9.987e-05,0.00010137180451127818,false,"roi"],["ETH/BTC",0.0,1516018200.0,1516071000.0,1526,880,0.0948969,0.09537257368421052,false,"roi"],["DASH/BTC",-0.0,1516025400.0,1516038000.0,1550,210,0.071,0.07135588972431077,false,"roi"],["ZEC/BTC",-0.0,1516026600.0,1516029000.0,1554,40,0.04600501,0.046235611553884705,false,"roi"],["TRX/BTC",-0.0,1516039800.0,1516044300.0,1598,75,9.438e-05,9.485308270676691e-05,false,"roi"],["XMR/BTC",-0.0,1516041300.0,1516043700.0,1603,40,0.03040001,0.030552391002506264,false,"roi"],["ADA/BTC",-0.10448878,1516047900.0,1516091100.0,1625,720,5.837e-05,5.2533e-05,false,"stop_loss"],["ZEC/BTC",-0.0,1516048800.0,1516053600.0,1628,80,0.046036,0.04626675689223057,false,"roi"],["ETC/BTC",-0.0,1516062600.0,1516065000.0,1674,40,0.0028685,0.0028828784461152877,false,"roi"],["DASH/BTC",0.0,1516065300.0,1516070100.0,1683,80,0.06731755,0.0676549813283208,false,"roi"],["ETH/BTC",0.0,1516088700.0,1516092000.0,1761,55,0.09217614,0.09263817578947368,false,"roi"],["LTC/BTC",0.01995012,1516091700.0,1516092900.0,1771,20,0.0165,0.016913533834586467,false,"roi"],["TRX/BTC",0.03990025,1516091700.0,1516092000.0,1771,5,7.953e-05,8.311781954887218e-05,false,"roi"],["ZEC/BTC",-0.0,1516092300.0,1516096200.0,1773,65,0.045202,0.04542857644110275,false,"roi"],["ADA/BTC",0.00997506,1516094100.0,1516095900.0,1779,30,5.248e-05,5.326917293233082e-05,false,"roi"],["XMR/BTC",0.0,1516094100.0,1516096500.0,1779,40,0.02892318,0.02906815834586466,false,"roi"],["ADA/BTC",0.01995012,1516096200.0,1516097400.0,1786,20,5.158e-05,5.287273182957392e-05,false,"roi"],["ZEC/BTC",0.00997506,1516097100.0,1516099200.0,1789,35,0.04357584,0.044231115789473675,false,"roi"],["XMR/BTC",0.00997506,1516097100.0,1516098900.0,1789,30,0.02828232,0.02870761804511278,false,"roi"],["ADA/BTC",0.00997506,1516110300.0,1516112400.0,1833,35,5.362e-05,5.4426315789473676e-05,false,"roi"],["ADA/BTC",-0.0,1516123800.0,1516127100.0,1878,55,5.302e-05,5.328576441102756e-05,false,"roi"],["ETH/BTC",0.00997506,1516126500.0,1516128300.0,1887,30,0.09129999,0.09267292218045112,false,"roi"],["XLM/BTC",0.01995012,1516126500.0,1516127700.0,1887,20,3.808e-05,3.903438596491228e-05,false,"roi"],["XMR/BTC",0.00997506,1516129200.0,1516131000.0,1896,30,0.02811012,0.028532828571428567,false,"roi"],["ETC/BTC",-0.10448878,1516137900.0,1516141500.0,1925,60,0.00258379,0.002325411,false,"stop_loss"],["NXT/BTC",-0.10448878,1516137900.0,1516142700.0,1925,80,2.559e-05,2.3031e-05,false,"stop_loss"],["TRX/BTC",-0.10448878,1516138500.0,1516141500.0,1927,50,7.62e-05,6.858e-05,false,"stop_loss"],["LTC/BTC",0.03990025,1516141800.0,1516142400.0,1938,10,0.0151,0.015781203007518795,false,"roi"],["ETC/BTC",0.03990025,1516141800.0,1516142100.0,1938,5,0.00229844,0.002402129022556391,false,"roi"],["ETC/BTC",0.03990025,1516142400.0,1516142700.0,1940,5,0.00235676,0.00246308,false,"roi"],["DASH/BTC",0.01995012,1516142700.0,1516143900.0,1941,20,0.0630692,0.06464988170426066,false,"roi"],["NXT/BTC",0.03990025,1516143000.0,1516143300.0,1942,5,2.2e-05,2.2992481203007514e-05,false,"roi"],["ADA/BTC",0.00997506,1516159800.0,1516161600.0,1998,30,4.974e-05,5.048796992481203e-05,false,"roi"],["TRX/BTC",0.01995012,1516161300.0,1516162500.0,2003,20,7.108e-05,7.28614536340852e-05,false,"roi"],["ZEC/BTC",-0.0,1516181700.0,1516184100.0,2071,40,0.04327,0.04348689223057644,false,"roi"],["ADA/BTC",-0.0,1516184400.0,1516208400.0,2080,400,4.997e-05,5.022047619047618e-05,false,"roi"],["DASH/BTC",-0.0,1516185000.0,1516188300.0,2082,55,0.06836818,0.06871087764411027,false,"roi"],["XLM/BTC",-0.0,1516185000.0,1516187400.0,2082,40,3.63e-05,3.648195488721804e-05,false,"roi"],["XMR/BTC",-0.0,1516192200.0,1516226700.0,2106,575,0.0281,0.02824085213032581,false,"roi"],["ETH/BTC",-0.0,1516192500.0,1516208100.0,2107,260,0.08651001,0.08694364413533832,false,"roi"],["ADA/BTC",-0.0,1516251600.0,1516254900.0,2304,55,5.633e-05,5.6612355889724306e-05,false,"roi"],["DASH/BTC",0.00997506,1516252800.0,1516254900.0,2308,35,0.06988494,0.07093584135338346,false,"roi"],["ADA/BTC",-0.0,1516260900.0,1516263300.0,2335,40,5.545e-05,5.572794486215538e-05,false,"roi"],["LTC/BTC",-0.0,1516266000.0,1516268400.0,2352,40,0.01633527,0.016417151052631574,false,"roi"],["ETC/BTC",-0.0,1516293600.0,1516296000.0,2444,40,0.00269734,0.0027108605012531326,false,"roi"],["XLM/BTC",0.01995012,1516298700.0,1516300200.0,2461,25,4.475e-05,4.587155388471177e-05,false,"roi"],["NXT/BTC",0.00997506,1516299900.0,1516301700.0,2465,30,2.79e-05,2.8319548872180444e-05,false,"roi"],["ZEC/BTC",0.0,1516306200.0,1516308600.0,2486,40,0.04439326,0.04461578260651629,false,"roi"],["XLM/BTC",0.0,1516311000.0,1516322100.0,2502,185,4.49e-05,4.51250626566416e-05,false,"roi"],["XMR/BTC",-0.0,1516312500.0,1516338300.0,2507,430,0.02855,0.028693107769423555,false,"roi"],["ADA/BTC",0.0,1516313400.0,1516315800.0,2510,40,5.796e-05,5.8250526315789473e-05,false,"roi"],["ZEC/BTC",0.0,1516319400.0,1516321800.0,2530,40,0.04340323,0.04362079005012531,false,"roi"],["ZEC/BTC",0.0,1516380300.0,1516383300.0,2733,50,0.04454455,0.04476783095238095,false,"roi"],["ADA/BTC",-0.0,1516382100.0,1516391700.0,2739,160,5.62e-05,5.648170426065162e-05,false,"roi"],["XLM/BTC",-0.0,1516382400.0,1516392900.0,2740,175,4.339e-05,4.360749373433584e-05,false,"roi"],["TRX/BTC",0.0,1516423500.0,1516469700.0,2877,770,0.0001009,0.00010140576441102757,false,"roi"],["ETC/BTC",-0.0,1516423800.0,1516461300.0,2878,625,0.00270505,0.002718609147869674,false,"roi"],["XMR/BTC",-0.0,1516423800.0,1516431600.0,2878,130,0.03000002,0.030150396040100245,false,"roi"],["ADA/BTC",-0.0,1516438800.0,1516441200.0,2928,40,5.46e-05,5.4873684210526304e-05,false,"roi"],["XMR/BTC",-0.10448878,1516472700.0,1516852200.0,3041,6325,0.03082222,0.027739998000000002,false,"stop_loss"],["ETH/BTC",-0.0,1516487100.0,1516490100.0,3089,50,0.08969999,0.09014961401002504,false,"roi"],["LTC/BTC",0.0,1516503000.0,1516545000.0,3142,700,0.01632501,0.01640683962406015,false,"roi"],["DASH/BTC",-0.0,1516530000.0,1516532400.0,3232,40,0.070538,0.07089157393483708,false,"roi"],["ADA/BTC",-0.0,1516549800.0,1516560300.0,3298,175,5.301e-05,5.3275714285714276e-05,false,"roi"],["XLM/BTC",0.0,1516551600.0,1516554000.0,3304,40,3.955e-05,3.9748245614035085e-05,false,"roi"],["ETC/BTC",0.00997506,1516569300.0,1516571100.0,3363,30,0.00258505,0.002623922932330827,false,"roi"],["XLM/BTC",-0.0,1516569300.0,1516571700.0,3363,40,3.903e-05,3.922563909774435e-05,false,"roi"],["ADA/BTC",-0.0,1516581300.0,1516617300.0,3403,600,5.236e-05,5.262245614035087e-05,false,"roi"],["TRX/BTC",0.0,1516584600.0,1516587000.0,3414,40,9.028e-05,9.073253132832079e-05,false,"roi"],["ETC/BTC",-0.0,1516623900.0,1516631700.0,3545,130,0.002687,0.002700468671679198,false,"roi"],["XLM/BTC",-0.0,1516626900.0,1516629300.0,3555,40,4.168e-05,4.1888922305764405e-05,false,"roi"],["TRX/BTC",0.00997506,1516629600.0,1516631400.0,3564,30,8.821e-05,8.953646616541353e-05,false,"roi"],["ADA/BTC",-0.0,1516636500.0,1516639200.0,3587,45,5.172e-05,5.1979248120300745e-05,false,"roi"],["NXT/BTC",0.01995012,1516637100.0,1516638300.0,3589,20,3.026e-05,3.101839598997494e-05,false,"roi"],["DASH/BTC",0.0,1516650600.0,1516666200.0,3634,260,0.07064,0.07099408521303258,false,"roi"],["LTC/BTC",0.0,1516656300.0,1516658700.0,3653,40,0.01644483,0.01652726022556391,false,"roi"],["XLM/BTC",0.00997506,1516665900.0,1516667700.0,3685,30,4.331e-05,4.3961278195488714e-05,false,"roi"],["NXT/BTC",0.01995012,1516672200.0,1516673700.0,3706,25,3.2e-05,3.2802005012531326e-05,false,"roi"],["ETH/BTC",0.0,1516681500.0,1516684500.0,3737,50,0.09167706,0.09213659413533835,false,"roi"],["DASH/BTC",0.0,1516692900.0,1516698000.0,3775,85,0.0692498,0.06959691679197995,false,"roi"],["NXT/BTC",0.0,1516704600.0,1516712700.0,3814,135,3.182e-05,3.197949874686716e-05,false,"roi"],["ZEC/BTC",-0.0,1516705500.0,1516723500.0,3817,300,0.04088,0.04108491228070175,false,"roi"],["ADA/BTC",-0.0,1516719300.0,1516721700.0,3863,40,5.15e-05,5.175814536340851e-05,false,"roi"],["ETH/BTC",0.0,1516725300.0,1516752300.0,3883,450,0.09071698,0.09117170170426064,false,"roi"],["NXT/BTC",-0.0,1516728300.0,1516733100.0,3893,80,3.128e-05,3.1436791979949865e-05,false,"roi"],["TRX/BTC",-0.0,1516738500.0,1516744800.0,3927,105,9.555e-05,9.602894736842104e-05,false,"roi"],["ZEC/BTC",-0.0,1516746600.0,1516749000.0,3954,40,0.04080001,0.041004521328320796,false,"roi"],["ADA/BTC",-0.0,1516751400.0,1516764900.0,3970,225,5.163e-05,5.1888796992481196e-05,false,"roi"],["ZEC/BTC",0.0,1516753200.0,1516758600.0,3976,90,0.04040781,0.04061035541353383,false,"roi"],["ADA/BTC",-0.0,1516776300.0,1516778700.0,4053,40,5.132e-05,5.157724310776942e-05,false,"roi"],["ADA/BTC",0.03990025,1516803300.0,1516803900.0,4143,10,5.198e-05,5.432496240601503e-05,false,"roi"],["NXT/BTC",-0.0,1516805400.0,1516811700.0,4150,105,3.054e-05,3.069308270676692e-05,false,"roi"],["TRX/BTC",0.0,1516806600.0,1516810500.0,4154,65,9.263e-05,9.309431077694235e-05,false,"roi"],["ADA/BTC",-0.0,1516833600.0,1516836300.0,4244,45,5.514e-05,5.5416390977443596e-05,false,"roi"],["XLM/BTC",0.0,1516841400.0,1516843800.0,4270,40,4.921e-05,4.9456666666666664e-05,false,"roi"],["ETC/BTC",0.0,1516868100.0,1516882500.0,4359,240,0.0026,0.002613032581453634,false,"roi"],["XMR/BTC",-0.0,1516875900.0,1516896900.0,4385,350,0.02799871,0.028139054411027563,false,"roi"],["ZEC/BTC",-0.0,1516878000.0,1516880700.0,4392,45,0.04078902,0.0409934762406015,false,"roi"],["NXT/BTC",-0.0,1516885500.0,1516887900.0,4417,40,2.89e-05,2.904486215538847e-05,false,"roi"],["ZEC/BTC",-0.0,1516886400.0,1516889100.0,4420,45,0.041103,0.041309030075187964,false,"roi"],["XLM/BTC",0.00997506,1516895100.0,1516896900.0,4449,30,5.428e-05,5.5096240601503756e-05,false,"roi"],["XLM/BTC",-0.0,1516902300.0,1516922100.0,4473,330,5.414e-05,5.441137844611528e-05,false,"roi"],["ZEC/BTC",-0.0,1516914900.0,1516917300.0,4515,40,0.04140777,0.0416153277443609,false,"roi"],["ETC/BTC",0.0,1516932300.0,1516934700.0,4573,40,0.00254309,0.002555837318295739,false,"roi"],["ADA/BTC",-0.0,1516935300.0,1516979400.0,4583,735,5.607e-05,5.6351052631578935e-05,false,"roi"],["ETC/BTC",0.0,1516947000.0,1516958700.0,4622,195,0.00253806,0.0025507821052631577,false,"roi"],["ZEC/BTC",-0.0,1516951500.0,1516960500.0,4637,150,0.0415,0.04170802005012531,false,"roi"],["XLM/BTC",0.00997506,1516960500.0,1516962300.0,4667,30,5.321e-05,5.401015037593984e-05,false,"roi"],["XMR/BTC",-0.0,1516982700.0,1516985100.0,4741,40,0.02772046,0.02785940967418546,false,"roi"],["ETH/BTC",0.0,1517009700.0,1517012100.0,4831,40,0.09461341,0.09508766268170425,false,"roi"],["XLM/BTC",-0.0,1517013300.0,1517016600.0,4843,55,5.615e-05,5.643145363408521e-05,false,"roi"],["ADA/BTC",-0.07877175,1517013900.0,1517287500.0,4845,4560,5.556e-05,5.144e-05,true,"force_sell"],["DASH/BTC",-0.0,1517020200.0,1517052300.0,4866,535,0.06900001,0.06934587471177944,false,"roi"],["ETH/BTC",-0.0,1517034300.0,1517036700.0,4913,40,0.09449985,0.09497353345864659,false,"roi"],["ZEC/BTC",-0.04815133,1517046000.0,1517287200.0,4952,4020,0.0410697,0.03928809,true,"force_sell"],["XMR/BTC",-0.0,1517053500.0,1517056200.0,4977,45,0.0285,0.02864285714285714,false,"roi"],["XMR/BTC",-0.0,1517056500.0,1517066700.0,4987,170,0.02866372,0.02880739779448621,false,"roi"],["ETH/BTC",-0.0,1517068200.0,1517071800.0,5026,60,0.095381,0.09585910025062655,false,"roi"],["DASH/BTC",-0.0,1517072700.0,1517075100.0,5041,40,0.06759092,0.06792972160401002,false,"roi"],["ETC/BTC",-0.0,1517096400.0,1517101500.0,5120,85,0.00258501,0.002597967443609022,false,"roi"],["DASH/BTC",-0.0,1517106300.0,1517127000.0,5153,345,0.06698502,0.0673207845112782,false,"roi"],["DASH/BTC",-0.0,1517135100.0,1517157000.0,5249,365,0.0677177,0.06805713709273183,false,"roi"],["XLM/BTC",0.0,1517171700.0,1517175300.0,5371,60,5.215e-05,5.2411403508771925e-05,false,"roi"],["ETC/BTC",0.00997506,1517176800.0,1517178600.0,5388,30,0.00273809,0.002779264285714285,false,"roi"],["ETC/BTC",0.00997506,1517184000.0,1517185800.0,5412,30,0.00274632,0.002787618045112782,false,"roi"],["LTC/BTC",0.0,1517192100.0,1517194800.0,5439,45,0.01622478,0.016306107218045113,false,"roi"],["DASH/BTC",-0.0,1517195100.0,1517197500.0,5449,40,0.069,0.06934586466165413,false,"roi"],["TRX/BTC",-0.0,1517203200.0,1517208900.0,5476,95,8.755e-05,8.798884711779448e-05,false,"roi"],["DASH/BTC",-0.0,1517209200.0,1517253900.0,5496,745,0.06825763,0.06859977350877192,false,"roi"],["DASH/BTC",-0.0,1517255100.0,1517257500.0,5649,40,0.06713892,0.06747545593984962,false,"roi"],["TRX/BTC",-0.0199116,1517268600.0,1517287500.0,5694,315,8.934e-05,8.8e-05,true,"force_sell"]] From 133947988238905752513992b4ad2caafb5eead3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Jun 2020 07:08:16 +0200 Subject: [PATCH 0216/1197] Have sell_type stringify correctly --- freqtrade/strategy/interface.py | 4 ++++ tests/test_freqtradebot.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index f9f3a3678..cee217ed5 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -45,6 +45,10 @@ class SellType(Enum): EMERGENCY_SELL = "emergency_sell" NONE = "" + def __str__(self): + # explicitly convert to String to help with exporting data. + return self.value + class SellCheckTuple(NamedTuple): """ diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 6496043f9..89991fde8 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -320,7 +320,7 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf # stoploss shoud be hit assert freqtrade.handle_trade(trade) is True - assert log_has('Executing Sell for NEO/BTC. Reason: SellType.STOP_LOSS', caplog) + assert log_has('Executing Sell for NEO/BTC. Reason: stop_loss', caplog) assert trade.sell_reason == SellType.STOP_LOSS.value From c13ec4a1d485e372c905a75a38ecc1bc42a60c6a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Jun 2020 07:15:33 +0200 Subject: [PATCH 0217/1197] implement fallback loading for load_backtest_data --- freqtrade/data/btanalysis.py | 45 ++++++++++++++------------ freqtrade/optimize/optimize_reports.py | 2 +- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index a556207e5..adf01e33d 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -70,29 +70,34 @@ def load_backtest_data(filename: Union[Path, str]) -> pd.DataFrame: Load backtest data file. :param filename: pathlib.Path object, or string pointing to the file. :return: a dataframe with the analysis results + :raise: ValueError if loading goes wrong. """ - if isinstance(filename, str): - filename = Path(filename) + data = load_backtest_stats(filename) + if not isinstance(data, list): + # new format + if 'strategy' not in data: + raise ValueError("Unknown dataformat") + if len(data['strategy']) != 1: + raise ValueError("Detected new Format with more than one strategy") + strategy = list(data['strategy'].keys())[0] + data = data['strategy'][strategy]['trades'] + df = pd.DataFrame(data) - if not filename.is_file(): - raise ValueError(f"File {filename} does not exist.") + else: + # old format - only with lists. + df = pd.DataFrame(data, columns=BT_DATA_COLUMNS) - with filename.open() as file: - data = json_load(file) - - df = pd.DataFrame(data, columns=BT_DATA_COLUMNS) - - df['open_date'] = pd.to_datetime(df['open_date'], - unit='s', - utc=True, - infer_datetime_format=True - ) - df['close_date'] = pd.to_datetime(df['close_date'], - unit='s', - utc=True, - infer_datetime_format=True - ) - df['profit'] = df['close_rate'] - df['open_rate'] + df['open_date'] = pd.to_datetime(df['open_date'], + unit='s', + utc=True, + infer_datetime_format=True + ) + df['close_date'] = pd.to_datetime(df['close_date'], + unit='s', + utc=True, + infer_datetime_format=True + ) + df['profit_abs'] = df['close_rate'] - df['open_rate'] df = df.sort_values("open_date").reset_index(drop=True) return df diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index a7cc8a7d6..6f9d3f34e 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -55,7 +55,7 @@ def backtest_result_to_list(results: DataFrame) -> List[List]: # Return 0 as "index" for compatibility reasons (for now) # TODO: Evaluate if we can remove this return [[t.pair, t.profit_percent, t.open_date.timestamp(), - t.open_date.timestamp(), 0, t.trade_duration, + t.close_date.timestamp(), 0, t.trade_duration, t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value] for index, t in results.iterrows()] From afefe92523421c87b0e968aa79e25fde175687ba Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Jun 2020 09:56:37 +0200 Subject: [PATCH 0218/1197] Add multi-strategy loading logic --- freqtrade/data/btanalysis.py | 47 ++++++++++++++----- .../testdata/backtest-result_multistrat.json | 1 + 2 files changed, 36 insertions(+), 12 deletions(-) create mode 100644 tests/testdata/backtest-result_multistrat.json diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index adf01e33d..17d3fed14 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -3,7 +3,7 @@ Helpers when analyzing backtest data """ import logging from pathlib import Path -from typing import Dict, Union, Tuple, Any +from typing import Dict, Union, Tuple, Any, Optional import numpy as np import pandas as pd @@ -65,24 +65,41 @@ def load_backtest_stats(filename: Union[Path, str]) -> Dict[str, Any]: return data -def load_backtest_data(filename: Union[Path, str]) -> pd.DataFrame: +def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = None) -> pd.DataFrame: """ Load backtest data file. :param filename: pathlib.Path object, or string pointing to the file. + :param strategy: Strategy to load - mainly relevant for multi-strategy backtests + Can also serve as protection to load the correct result. :return: a dataframe with the analysis results :raise: ValueError if loading goes wrong. """ data = load_backtest_stats(filename) if not isinstance(data, list): - # new format + # new, nested format if 'strategy' not in data: - raise ValueError("Unknown dataformat") - if len(data['strategy']) != 1: - raise ValueError("Detected new Format with more than one strategy") - strategy = list(data['strategy'].keys())[0] + raise ValueError("Unknown dataformat.") + + if not strategy: + if len(data['strategy']) == 1: + strategy = list(data['strategy'].keys())[0] + else: + raise ValueError("Detected backtest result with more than one strategy. " + "Please specify a strategy.") + + if strategy not in data['strategy']: + raise ValueError(f"Strategy {strategy} not available in the backtest result.") + data = data['strategy'][strategy]['trades'] df = pd.DataFrame(data) - + df['open_date'] = pd.to_datetime(df['open_date'], + utc=True, + infer_datetime_format=True + ) + df['close_date'] = pd.to_datetime(df['close_date'], + utc=True, + infer_datetime_format=True + ) else: # old format - only with lists. df = pd.DataFrame(data, columns=BT_DATA_COLUMNS) @@ -140,10 +157,12 @@ def evaluate_result_multi(results: pd.DataFrame, timeframe: str, return df_final[df_final['open_trades'] > max_open_trades] -def load_trades_from_db(db_url: str) -> pd.DataFrame: +def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataFrame: """ Load trades from a DB (using dburl) :param db_url: Sqlite url (default format sqlite:///tradesv3.dry-run.sqlite) + :param strategy: Strategy to load - mainly relevant for multi-strategy backtests + Can also serve as protection to load the correct result. :return: Dataframe containing Trades """ persistence.init(db_url, clean_open_orders=False) @@ -154,6 +173,10 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: "stake_amount", "max_rate", "min_rate", "id", "exchange", "stop_loss", "initial_stop_loss", "strategy", "timeframe"] + filters = [] + if strategy: + filters = Trade.strategy == strategy + 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, @@ -172,14 +195,14 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: t.stop_loss, t.initial_stop_loss, t.strategy, t.timeframe ) - for t in Trade.get_trades().all()], + for t in Trade.get_trades(filters).all()], columns=columns) return trades def load_trades(source: str, db_url: str, exportfilename: Path, - no_trades: bool = False) -> pd.DataFrame: + no_trades: bool = False, strategy: Optional[str] = None) -> pd.DataFrame: """ Based on configuration option "trade_source": * loads data from DB (using `db_url`) @@ -197,7 +220,7 @@ def load_trades(source: str, db_url: str, exportfilename: Path, if source == "DB": return load_trades_from_db(db_url) elif source == "file": - return load_backtest_data(exportfilename) + return load_backtest_data(exportfilename, strategy) def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame, diff --git a/tests/testdata/backtest-result_multistrat.json b/tests/testdata/backtest-result_multistrat.json new file mode 100644 index 000000000..a58ab28cb --- /dev/null +++ b/tests/testdata/backtest-result_multistrat.json @@ -0,0 +1 @@ +{"strategy": {"DefaultStrategy": {"trades": [{"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:20:00+00:00", "trade_duration": 5, "open_rate": 9.64e-05, "close_rate": 0.00010074887218045112, "open_at_end": false, "sell_reason": "roi", "profit": 4.348872180451118e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1037.344398340249, "profit_abs": 0.00399999999999999}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:30:00+00:00", "trade_duration": 15, "open_rate": 4.756e-05, "close_rate": 4.9705563909774425e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.1455639097744267e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2102.6072329688814, "profit_abs": 0.00399999999999999}, {"pair": "XLM/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:35:00+00:00", "trade_duration": 10, "open_rate": 3.339e-05, "close_rate": 3.489631578947368e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.506315789473681e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2994.908655286014, "profit_abs": 0.0040000000000000036}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:40:00+00:00", "trade_duration": 15, "open_rate": 9.696e-05, "close_rate": 0.00010133413533834584, "open_at_end": false, "sell_reason": "roi", "profit": 4.3741353383458455e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1031.3531353135315, "profit_abs": 0.00399999999999999}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 07:35:00+00:00", "close_date": "2018-01-10 08:35:00+00:00", "trade_duration": 60, "open_rate": 0.0943, "close_rate": 0.09477268170426063, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004726817042606385, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0604453870625663, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 07:40:00+00:00", "close_date": "2018-01-10 08:10:00+00:00", "trade_duration": 30, "open_rate": 0.02719607, "close_rate": 0.02760503345864661, "open_at_end": false, "sell_reason": "roi", "profit": 0.00040896345864661204, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.677001860930642, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 08:15:00+00:00", "close_date": "2018-01-10 09:55:00+00:00", "trade_duration": 100, "open_rate": 0.04634952, "close_rate": 0.046581848421052625, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002323284210526272, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1575196463739, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 14:45:00+00:00", "close_date": "2018-01-10 15:50:00+00:00", "trade_duration": 65, "open_rate": 3.066e-05, "close_rate": 3.081368421052631e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5368421052630647e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3261.5786040443577, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 16:35:00+00:00", "close_date": "2018-01-10 17:15:00+00:00", "trade_duration": 40, "open_rate": 0.0168999, "close_rate": 0.016984611278195488, "open_at_end": false, "sell_reason": "roi", "profit": 8.471127819548868e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.917194776300452, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 16:40:00+00:00", "close_date": "2018-01-10 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.09132568, "close_rate": 0.0917834528320802, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004577728320801916, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0949822656672252, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 18:50:00+00:00", "close_date": "2018-01-10 19:45:00+00:00", "trade_duration": 55, "open_rate": 0.08898003, "close_rate": 0.08942604518796991, "open_at_end": false, "sell_reason": "roi", "profit": 0.00044601518796991146, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1238476768326557, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 22:15:00+00:00", "close_date": "2018-01-10 23:00:00+00:00", "trade_duration": 45, "open_rate": 0.08560008, "close_rate": 0.08602915308270676, "open_at_end": false, "sell_reason": "roi", "profit": 0.00042907308270676014, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1682232072680307, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 22:50:00+00:00", "close_date": "2018-01-10 23:20:00+00:00", "trade_duration": 30, "open_rate": 0.00249083, "close_rate": 0.0025282860902255634, "open_at_end": false, "sell_reason": "roi", "profit": 3.745609022556351e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 40.147260150231055, "profit_abs": 0.000999999999999987}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 23:15:00+00:00", "close_date": "2018-01-11 00:15:00+00:00", "trade_duration": 60, "open_rate": 3.022e-05, "close_rate": 3.037147869674185e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5147869674185174e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3309.0668431502318, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-10 23:40:00+00:00", "close_date": "2018-01-11 00:05:00+00:00", "trade_duration": 25, "open_rate": 0.002437, "close_rate": 0.0024980776942355883, "open_at_end": false, "sell_reason": "roi", "profit": 6.107769423558838e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.03405826836274, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 00:00:00+00:00", "close_date": "2018-01-11 00:35:00+00:00", "trade_duration": 35, "open_rate": 0.04771803, "close_rate": 0.04843559436090225, "open_at_end": false, "sell_reason": "roi", "profit": 0.0007175643609022495, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0956439316543456, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-11 03:40:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 45, "open_rate": 3.651e-05, "close_rate": 3.2859000000000005e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -3.650999999999996e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2738.9756231169545, "profit_abs": -0.01047499999999997}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 03:55:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 30, "open_rate": 0.08824105, "close_rate": 0.08956798308270676, "open_at_end": false, "sell_reason": "roi", "profit": 0.0013269330827067605, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1332594070446804, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 04:00:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 50, "open_rate": 0.00243, "close_rate": 0.002442180451127819, "open_at_end": false, "sell_reason": "roi", "profit": 1.2180451127819219e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.1522633744856, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:55:00+00:00", "trade_duration": 25, "open_rate": 0.04545064, "close_rate": 0.046589753784461146, "open_at_end": false, "sell_reason": "roi", "profit": 0.001139113784461146, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.200189040242338, "profit_abs": 0.001999999999999988}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 20, "open_rate": 3.372e-05, "close_rate": 3.456511278195488e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.4511278195488e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2965.599051008304, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:55:00+00:00", "close_date": "2018-01-11 05:15:00+00:00", "trade_duration": 20, "open_rate": 0.02644, "close_rate": 0.02710265664160401, "open_at_end": false, "sell_reason": "roi", "profit": 0.0006626566416040071, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7821482602118004, "profit_abs": 0.001999999999999988}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:20:00+00:00", "close_date": "2018-01-11 12:00:00+00:00", "trade_duration": 40, "open_rate": 0.08812, "close_rate": 0.08856170426065162, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004417042606516125, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1348161597821154, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:35:00+00:00", "close_date": "2018-01-11 12:15:00+00:00", "trade_duration": 40, "open_rate": 0.02683577, "close_rate": 0.026970285137844607, "open_at_end": false, "sell_reason": "roi", "profit": 0.00013451513784460897, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7263696923919087, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 14:00:00+00:00", "close_date": "2018-01-11 14:25:00+00:00", "trade_duration": 25, "open_rate": 4.919e-05, "close_rate": 5.04228320802005e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.232832080200495e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.9335230737956, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 19:25:00+00:00", "close_date": "2018-01-11 20:35:00+00:00", "trade_duration": 70, "open_rate": 0.08784896, "close_rate": 0.08828930566416039, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004403456641603881, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1383174029607181, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:35:00+00:00", "close_date": "2018-01-11 23:30:00+00:00", "trade_duration": 55, "open_rate": 5.105e-05, "close_rate": 5.130588972431077e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.558897243107704e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1958.8638589618022, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:25:00+00:00", "trade_duration": 30, "open_rate": 3.96e-05, "close_rate": 4.019548872180451e-05, "open_at_end": false, "sell_reason": "roi", "profit": 5.954887218045116e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2525.252525252525, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:35:00+00:00", "trade_duration": 40, "open_rate": 2.885e-05, "close_rate": 2.899461152882205e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.4461152882205115e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3466.204506065858, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 23:30:00+00:00", "close_date": "2018-01-12 00:05:00+00:00", "trade_duration": 35, "open_rate": 0.02645, "close_rate": 0.026847744360902256, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003977443609022545, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.780718336483932, "profit_abs": 0.0010000000000000148}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 23:55:00+00:00", "close_date": "2018-01-12 01:15:00+00:00", "trade_duration": 80, "open_rate": 0.048, "close_rate": 0.04824060150375939, "open_at_end": false, "sell_reason": "roi", "profit": 0.00024060150375938838, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0833333333333335, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-12 21:15:00+00:00", "close_date": "2018-01-12 21:40:00+00:00", "trade_duration": 25, "open_rate": 4.692e-05, "close_rate": 4.809593984962405e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.1759398496240516e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2131.287297527707, "profit_abs": 0.001999999999999974}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 00:55:00+00:00", "close_date": "2018-01-13 06:20:00+00:00", "trade_duration": 325, "open_rate": 0.00256966, "close_rate": 0.0025825405012531327, "open_at_end": false, "sell_reason": "roi", "profit": 1.2880501253132587e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.91565421106294, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 10:55:00+00:00", "close_date": "2018-01-13 11:35:00+00:00", "trade_duration": 40, "open_rate": 6.262e-05, "close_rate": 6.293388471177944e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.138847117794446e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1596.933886937081, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 13:05:00+00:00", "close_date": "2018-01-15 14:10:00+00:00", "trade_duration": 2945, "open_rate": 4.73e-05, "close_rate": 4.753709273182957e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.3709273182957117e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2114.1649048625795, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:30:00+00:00", "close_date": "2018-01-13 14:45:00+00:00", "trade_duration": 75, "open_rate": 6.063e-05, "close_rate": 6.0933909774436085e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.039097744360846e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1649.348507339601, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:40:00+00:00", "close_date": "2018-01-13 23:30:00+00:00", "trade_duration": 590, "open_rate": 0.00011082, "close_rate": 0.00011137548872180448, "open_at_end": false, "sell_reason": "roi", "profit": 5.554887218044781e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 902.3641941887746, "profit_abs": -2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 15:15:00+00:00", "close_date": "2018-01-13 15:55:00+00:00", "trade_duration": 40, "open_rate": 5.93e-05, "close_rate": 5.9597243107769415e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.9724310776941686e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1686.3406408094436, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 16:30:00+00:00", "close_date": "2018-01-13 17:10:00+00:00", "trade_duration": 40, "open_rate": 0.04850003, "close_rate": 0.04874313791979949, "open_at_end": false, "sell_reason": "roi", "profit": 0.00024310791979949287, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0618543947292407, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 22:05:00+00:00", "close_date": "2018-01-14 06:25:00+00:00", "trade_duration": 500, "open_rate": 0.09825019, "close_rate": 0.09874267215538848, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004924821553884823, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0178097365511456, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 00:20:00+00:00", "close_date": "2018-01-14 22:55:00+00:00", "trade_duration": 1355, "open_rate": 6.018e-05, "close_rate": 6.048165413533834e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.0165413533833987e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1661.681621801263, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 12:45:00+00:00", "close_date": "2018-01-14 13:25:00+00:00", "trade_duration": 40, "open_rate": 0.09758999, "close_rate": 0.0980791628822055, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004891728822054991, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.024695258191952, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-14 15:30:00+00:00", "close_date": "2018-01-14 16:00:00+00:00", "trade_duration": 30, "open_rate": 0.00311, "close_rate": 0.0031567669172932328, "open_at_end": false, "sell_reason": "roi", "profit": 4.676691729323286e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.154340836012864, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 20:45:00+00:00", "close_date": "2018-01-14 22:15:00+00:00", "trade_duration": 90, "open_rate": 0.00312401, "close_rate": 0.003139669197994987, "open_at_end": false, "sell_reason": "roi", "profit": 1.5659197994987058e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.010140812609436, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 23:35:00+00:00", "close_date": "2018-01-15 00:30:00+00:00", "trade_duration": 55, "open_rate": 0.0174679, "close_rate": 0.017555458395989976, "open_at_end": false, "sell_reason": "roi", "profit": 8.755839598997492e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.724786608579165, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 23:45:00+00:00", "close_date": "2018-01-15 00:25:00+00:00", "trade_duration": 40, "open_rate": 0.07346846, "close_rate": 0.07383672295739348, "open_at_end": false, "sell_reason": "roi", "profit": 0.00036826295739347814, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.3611282991367997, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 02:25:00+00:00", "close_date": "2018-01-15 03:05:00+00:00", "trade_duration": 40, "open_rate": 0.097994, "close_rate": 0.09848519799498744, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004911979949874384, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.020470641059657, "profit_abs": -2.7755575615628914e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 07:20:00+00:00", "close_date": "2018-01-15 08:00:00+00:00", "trade_duration": 40, "open_rate": 0.09659, "close_rate": 0.09707416040100247, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004841604010024786, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0353038616834043, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-15 08:20:00+00:00", "close_date": "2018-01-15 08:55:00+00:00", "trade_duration": 35, "open_rate": 9.987e-05, "close_rate": 0.00010137180451127818, "open_at_end": false, "sell_reason": "roi", "profit": 1.501804511278178e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1001.3016921998599, "profit_abs": 0.0010000000000000009}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-15 12:10:00+00:00", "close_date": "2018-01-16 02:50:00+00:00", "trade_duration": 880, "open_rate": 0.0948969, "close_rate": 0.09537257368421052, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004756736842105175, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0537752023511833, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:10:00+00:00", "close_date": "2018-01-15 17:40:00+00:00", "trade_duration": 210, "open_rate": 0.071, "close_rate": 0.07135588972431077, "open_at_end": false, "sell_reason": "roi", "profit": 0.00035588972431077615, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4084507042253522, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:30:00+00:00", "close_date": "2018-01-15 15:10:00+00:00", "trade_duration": 40, "open_rate": 0.04600501, "close_rate": 0.046235611553884705, "open_at_end": false, "sell_reason": "roi", "profit": 0.00023060155388470588, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.173676301776698, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:10:00+00:00", "close_date": "2018-01-15 19:25:00+00:00", "trade_duration": 75, "open_rate": 9.438e-05, "close_rate": 9.485308270676693e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.7308270676692514e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1059.5465140919687, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:35:00+00:00", "close_date": "2018-01-15 19:15:00+00:00", "trade_duration": 40, "open_rate": 0.03040001, "close_rate": 0.030552391002506264, "open_at_end": false, "sell_reason": "roi", "profit": 0.0001523810025062626, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.2894726021471703, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-15 20:25:00+00:00", "close_date": "2018-01-16 08:25:00+00:00", "trade_duration": 720, "open_rate": 5.837e-05, "close_rate": 5.2533e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -5.8369999999999985e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1713.2088401576154, "profit_abs": -0.010474999999999984}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 20:40:00+00:00", "close_date": "2018-01-15 22:00:00+00:00", "trade_duration": 80, "open_rate": 0.046036, "close_rate": 0.04626675689223057, "open_at_end": false, "sell_reason": "roi", "profit": 0.00023075689223057277, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1722130506560084, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 00:30:00+00:00", "close_date": "2018-01-16 01:10:00+00:00", "trade_duration": 40, "open_rate": 0.0028685, "close_rate": 0.0028828784461152877, "open_at_end": false, "sell_reason": "roi", "profit": 1.4378446115287727e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 34.86142583231654, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 01:15:00+00:00", "close_date": "2018-01-16 02:35:00+00:00", "trade_duration": 80, "open_rate": 0.06731755, "close_rate": 0.0676549813283208, "open_at_end": false, "sell_reason": "roi", "profit": 0.00033743132832080025, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4854967241083492, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 07:45:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 55, "open_rate": 0.09217614, "close_rate": 0.09263817578947368, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004620357894736804, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0848794492804754, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:55:00+00:00", "trade_duration": 20, "open_rate": 0.0165, "close_rate": 0.016913533834586467, "open_at_end": false, "sell_reason": "roi", "profit": 0.00041353383458646656, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.0606060606060606, "profit_abs": 0.0020000000000000018}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 5, "open_rate": 7.953e-05, "close_rate": 8.311781954887218e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.587819548872171e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1257.387149503332, "profit_abs": 0.00399999999999999}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 08:45:00+00:00", "close_date": "2018-01-16 09:50:00+00:00", "trade_duration": 65, "open_rate": 0.045202, "close_rate": 0.04542857644110275, "open_at_end": false, "sell_reason": "roi", "profit": 0.00022657644110275071, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2122914915269236, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:45:00+00:00", "trade_duration": 30, "open_rate": 5.248e-05, "close_rate": 5.326917293233082e-05, "open_at_end": false, "sell_reason": "roi", "profit": 7.891729323308177e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1905.487804878049, "profit_abs": 0.0010000000000000009}, {"pair": "XMR/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:55:00+00:00", "trade_duration": 40, "open_rate": 0.02892318, "close_rate": 0.02906815834586466, "open_at_end": false, "sell_reason": "roi", "profit": 0.0001449783458646603, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.457434486802627, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 09:50:00+00:00", "close_date": "2018-01-16 10:10:00+00:00", "trade_duration": 20, "open_rate": 5.158e-05, "close_rate": 5.287273182957392e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.2927318295739246e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1938.735944164405, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:35:00+00:00", "trade_duration": 30, "open_rate": 0.02828232, "close_rate": 0.02870761804511278, "open_at_end": false, "sell_reason": "roi", "profit": 0.00042529804511277913, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5357778286929786, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:40:00+00:00", "trade_duration": 35, "open_rate": 0.04357584, "close_rate": 0.044231115789473675, "open_at_end": false, "sell_reason": "roi", "profit": 0.0006552757894736777, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.294849623093898, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 13:45:00+00:00", "close_date": "2018-01-16 14:20:00+00:00", "trade_duration": 35, "open_rate": 5.362e-05, "close_rate": 5.442631578947368e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.063157894736843e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1864.975755315181, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 17:30:00+00:00", "close_date": "2018-01-16 18:25:00+00:00", "trade_duration": 55, "open_rate": 5.302e-05, "close_rate": 5.328576441102756e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.6576441102756397e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.0807242549984, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:45:00+00:00", "trade_duration": 30, "open_rate": 0.09129999, "close_rate": 0.09267292218045112, "open_at_end": false, "sell_reason": "roi", "profit": 0.0013729321804511196, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0952903718828448, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:35:00+00:00", "trade_duration": 20, "open_rate": 3.808e-05, "close_rate": 3.903438596491228e-05, "open_at_end": false, "sell_reason": "roi", "profit": 9.543859649122774e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2626.0504201680674, "profit_abs": 0.0020000000000000018}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 19:00:00+00:00", "close_date": "2018-01-16 19:30:00+00:00", "trade_duration": 30, "open_rate": 0.02811012, "close_rate": 0.028532828571428567, "open_at_end": false, "sell_reason": "roi", "profit": 0.00042270857142856846, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.557437677249333, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 60, "open_rate": 0.00258379, "close_rate": 0.002325411, "open_at_end": false, "sell_reason": "stop_loss", "profit": -0.000258379, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.702835756775904, "profit_abs": -0.010474999999999984}, {"pair": "NXT/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 80, "open_rate": 2.559e-05, "close_rate": 2.3031e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -2.5590000000000004e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3907.7764751856193, "profit_abs": -0.010474999999999998}, {"pair": "TRX/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:35:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 50, "open_rate": 7.62e-05, "close_rate": 6.858e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -7.619999999999998e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1312.3359580052495, "profit_abs": -0.010474999999999984}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:35:00+00:00", "trade_duration": 5, "open_rate": 0.00229844, "close_rate": 0.002402129022556391, "open_at_end": false, "sell_reason": "roi", "profit": 0.00010368902255639091, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 43.507770487809125, "profit_abs": 0.004000000000000017}, {"pair": "LTC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:40:00+00:00", "trade_duration": 10, "open_rate": 0.0151, "close_rate": 0.015781203007518795, "open_at_end": false, "sell_reason": "roi", "profit": 0.0006812030075187946, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.622516556291391, "profit_abs": 0.00399999999999999}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:40:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 5, "open_rate": 0.00235676, "close_rate": 0.00246308, "open_at_end": false, "sell_reason": "roi", "profit": 0.00010632000000000003, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 42.431134269081284, "profit_abs": 0.0040000000000000036}, {"pair": "DASH/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 22:45:00+00:00", "close_date": "2018-01-16 23:05:00+00:00", "trade_duration": 20, "open_rate": 0.0630692, "close_rate": 0.06464988170426066, "open_at_end": false, "sell_reason": "roi", "profit": 0.0015806817042606502, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.585559988076589, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:50:00+00:00", "close_date": "2018-01-16 22:55:00+00:00", "trade_duration": 5, "open_rate": 2.2e-05, "close_rate": 2.299248120300751e-05, "open_at_end": false, "sell_reason": "roi", "profit": 9.924812030075114e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 4545.454545454546, "profit_abs": 0.003999999999999976}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-17 03:30:00+00:00", "close_date": "2018-01-17 04:00:00+00:00", "trade_duration": 30, "open_rate": 4.974e-05, "close_rate": 5.048796992481203e-05, "open_at_end": false, "sell_reason": "roi", "profit": 7.479699248120277e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2010.454362685967, "profit_abs": 0.0010000000000000009}, {"pair": "TRX/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-17 03:55:00+00:00", "close_date": "2018-01-17 04:15:00+00:00", "trade_duration": 20, "open_rate": 7.108e-05, "close_rate": 7.28614536340852e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.7814536340851996e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1406.8655036578502, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 09:35:00+00:00", "close_date": "2018-01-17 10:15:00+00:00", "trade_duration": 40, "open_rate": 0.04327, "close_rate": 0.04348689223057644, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002168922305764362, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.3110700254217704, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:20:00+00:00", "close_date": "2018-01-17 17:00:00+00:00", "trade_duration": 400, "open_rate": 4.997e-05, "close_rate": 5.022047619047618e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.504761904761831e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2001.2007204322595, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:25:00+00:00", "trade_duration": 55, "open_rate": 0.06836818, "close_rate": 0.06871087764411027, "open_at_end": false, "sell_reason": "roi", "profit": 0.00034269764411026804, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4626687444363737, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:10:00+00:00", "trade_duration": 40, "open_rate": 3.63e-05, "close_rate": 3.648195488721804e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.8195488721804031e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2754.8209366391184, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:30:00+00:00", "close_date": "2018-01-17 22:05:00+00:00", "trade_duration": 575, "open_rate": 0.0281, "close_rate": 0.02824085213032581, "open_at_end": false, "sell_reason": "roi", "profit": 0.0001408521303258095, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5587188612099645, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:35:00+00:00", "close_date": "2018-01-17 16:55:00+00:00", "trade_duration": 260, "open_rate": 0.08651001, "close_rate": 0.08694364413533832, "open_at_end": false, "sell_reason": "roi", "profit": 0.00043363413533832607, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1559355963546878, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 05:00:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 55, "open_rate": 5.633e-05, "close_rate": 5.6612355889724306e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.8235588972430847e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1775.2529735487308, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 05:20:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 35, "open_rate": 0.06988494, "close_rate": 0.07093584135338346, "open_at_end": false, "sell_reason": "roi", "profit": 0.0010509013533834544, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.430923457900944, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 07:35:00+00:00", "close_date": "2018-01-18 08:15:00+00:00", "trade_duration": 40, "open_rate": 5.545e-05, "close_rate": 5.572794486215538e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.779448621553787e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1803.4265103697026, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 09:00:00+00:00", "close_date": "2018-01-18 09:40:00+00:00", "trade_duration": 40, "open_rate": 0.01633527, "close_rate": 0.016417151052631574, "open_at_end": false, "sell_reason": "roi", "profit": 8.188105263157511e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.121723118136401, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 16:40:00+00:00", "close_date": "2018-01-18 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.00269734, "close_rate": 0.002710860501253133, "open_at_end": false, "sell_reason": "roi", "profit": 1.3520501253133123e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.073561360451414, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-18 18:05:00+00:00", "close_date": "2018-01-18 18:30:00+00:00", "trade_duration": 25, "open_rate": 4.475e-05, "close_rate": 4.587155388471177e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.1215538847117757e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2234.63687150838, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 18:25:00+00:00", "close_date": "2018-01-18 18:55:00+00:00", "trade_duration": 30, "open_rate": 2.79e-05, "close_rate": 2.8319548872180444e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.1954887218044365e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3584.2293906810037, "profit_abs": 0.000999999999999987}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 20:10:00+00:00", "close_date": "2018-01-18 20:50:00+00:00", "trade_duration": 40, "open_rate": 0.04439326, "close_rate": 0.04461578260651629, "open_at_end": false, "sell_reason": "roi", "profit": 0.00022252260651629135, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2525942001105577, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 21:30:00+00:00", "close_date": "2018-01-19 00:35:00+00:00", "trade_duration": 185, "open_rate": 4.49e-05, "close_rate": 4.51250626566416e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.2506265664159932e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2227.1714922049, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 21:55:00+00:00", "close_date": "2018-01-19 05:05:00+00:00", "trade_duration": 430, "open_rate": 0.02855, "close_rate": 0.028693107769423555, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014310776942355607, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.502626970227671, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 22:10:00+00:00", "close_date": "2018-01-18 22:50:00+00:00", "trade_duration": 40, "open_rate": 5.796e-05, "close_rate": 5.8250526315789473e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.905263157894727e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1725.3278122843342, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 23:50:00+00:00", "close_date": "2018-01-19 00:30:00+00:00", "trade_duration": 40, "open_rate": 0.04340323, "close_rate": 0.04362079005012531, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002175600501253122, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.303975994413319, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-19 16:45:00+00:00", "close_date": "2018-01-19 17:35:00+00:00", "trade_duration": 50, "open_rate": 0.04454455, "close_rate": 0.04476783095238095, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002232809523809512, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.244943545282195, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:15:00+00:00", "close_date": "2018-01-19 19:55:00+00:00", "trade_duration": 160, "open_rate": 5.62e-05, "close_rate": 5.648170426065162e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.817042606516199e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1779.3594306049824, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:20:00+00:00", "close_date": "2018-01-19 20:15:00+00:00", "trade_duration": 175, "open_rate": 4.339e-05, "close_rate": 4.360749373433584e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.174937343358337e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2304.6784973496196, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-20 04:45:00+00:00", "close_date": "2018-01-20 17:35:00+00:00", "trade_duration": 770, "open_rate": 0.0001009, "close_rate": 0.00010140576441102755, "open_at_end": false, "sell_reason": "roi", "profit": 5.057644110275549e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 991.0802775024778, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 15:15:00+00:00", "trade_duration": 625, "open_rate": 0.00270505, "close_rate": 0.002718609147869674, "open_at_end": false, "sell_reason": "roi", "profit": 1.3559147869673764e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.96789338459548, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 07:00:00+00:00", "trade_duration": 130, "open_rate": 0.03000002, "close_rate": 0.030150396040100245, "open_at_end": false, "sell_reason": "roi", "profit": 0.00015037604010024672, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.3333311111125927, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 09:00:00+00:00", "close_date": "2018-01-20 09:40:00+00:00", "trade_duration": 40, "open_rate": 5.46e-05, "close_rate": 5.4873684210526304e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.736842105263053e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1831.5018315018317, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-20 18:25:00+00:00", "close_date": "2018-01-25 03:50:00+00:00", "trade_duration": 6325, "open_rate": 0.03082222, "close_rate": 0.027739998, "open_at_end": false, "sell_reason": "stop_loss", "profit": -0.0030822220000000025, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.244412634781012, "profit_abs": -0.010474999999999998}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 22:25:00+00:00", "close_date": "2018-01-20 23:15:00+00:00", "trade_duration": 50, "open_rate": 0.08969999, "close_rate": 0.09014961401002504, "open_at_end": false, "sell_reason": "roi", "profit": 0.00044962401002504593, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1148273260677064, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 02:50:00+00:00", "close_date": "2018-01-21 14:30:00+00:00", "trade_duration": 700, "open_rate": 0.01632501, "close_rate": 0.01640683962406015, "open_at_end": false, "sell_reason": "roi", "profit": 8.182962406014932e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.125570520324337, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 10:20:00+00:00", "close_date": "2018-01-21 11:00:00+00:00", "trade_duration": 40, "open_rate": 0.070538, "close_rate": 0.07089157393483708, "open_at_end": false, "sell_reason": "roi", "profit": 0.00035357393483707866, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.417675579120474, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 15:50:00+00:00", "close_date": "2018-01-21 18:45:00+00:00", "trade_duration": 175, "open_rate": 5.301e-05, "close_rate": 5.327571428571427e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.657142857142672e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.4365214110546, "profit_abs": -2.7755575615628914e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 16:20:00+00:00", "close_date": "2018-01-21 17:00:00+00:00", "trade_duration": 40, "open_rate": 3.955e-05, "close_rate": 3.9748245614035085e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.9824561403508552e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2528.4450063211125, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:45:00+00:00", "trade_duration": 30, "open_rate": 0.00258505, "close_rate": 0.002623922932330827, "open_at_end": false, "sell_reason": "roi", "profit": 3.8872932330826816e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.6839712964933, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:55:00+00:00", "trade_duration": 40, "open_rate": 3.903e-05, "close_rate": 3.922563909774435e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.9563909774435151e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2562.1316935690497, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 00:35:00+00:00", "close_date": "2018-01-22 10:35:00+00:00", "trade_duration": 600, "open_rate": 5.236e-05, "close_rate": 5.262245614035087e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.624561403508717e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1909.8548510313217, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 01:30:00+00:00", "close_date": "2018-01-22 02:10:00+00:00", "trade_duration": 40, "open_rate": 9.028e-05, "close_rate": 9.07325313283208e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.5253132832080657e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1107.6650420912717, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 12:25:00+00:00", "close_date": "2018-01-22 14:35:00+00:00", "trade_duration": 130, "open_rate": 0.002687, "close_rate": 0.002700468671679198, "open_at_end": false, "sell_reason": "roi", "profit": 1.3468671679197925e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.21622627465575, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 13:15:00+00:00", "close_date": "2018-01-22 13:55:00+00:00", "trade_duration": 40, "open_rate": 4.168e-05, "close_rate": 4.188892230576441e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.0892230576441054e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2399.232245681382, "profit_abs": 1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-22 14:00:00+00:00", "close_date": "2018-01-22 14:30:00+00:00", "trade_duration": 30, "open_rate": 8.821e-05, "close_rate": 8.953646616541353e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.326466165413529e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1133.6583153837435, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 15:55:00+00:00", "close_date": "2018-01-22 16:40:00+00:00", "trade_duration": 45, "open_rate": 5.172e-05, "close_rate": 5.1979248120300745e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.592481203007459e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1933.4880123743235, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-22 16:05:00+00:00", "close_date": "2018-01-22 16:25:00+00:00", "trade_duration": 20, "open_rate": 3.026e-05, "close_rate": 3.101839598997494e-05, "open_at_end": false, "sell_reason": "roi", "profit": 7.5839598997494e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3304.692663582287, "profit_abs": 0.0020000000000000157}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 19:50:00+00:00", "close_date": "2018-01-23 00:10:00+00:00", "trade_duration": 260, "open_rate": 0.07064, "close_rate": 0.07099408521303258, "open_at_end": false, "sell_reason": "roi", "profit": 0.00035408521303258167, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.415628539071348, "profit_abs": 1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 21:25:00+00:00", "close_date": "2018-01-22 22:05:00+00:00", "trade_duration": 40, "open_rate": 0.01644483, "close_rate": 0.01652726022556391, "open_at_end": false, "sell_reason": "roi", "profit": 8.243022556390922e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.080938507725528, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-23 00:05:00+00:00", "close_date": "2018-01-23 00:35:00+00:00", "trade_duration": 30, "open_rate": 4.331e-05, "close_rate": 4.3961278195488714e-05, "open_at_end": false, "sell_reason": "roi", "profit": 6.512781954887175e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2308.935580697299, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-23 01:50:00+00:00", "close_date": "2018-01-23 02:15:00+00:00", "trade_duration": 25, "open_rate": 3.2e-05, "close_rate": 3.2802005012531326e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.020050125313278e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3125.0000000000005, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 04:25:00+00:00", "close_date": "2018-01-23 05:15:00+00:00", "trade_duration": 50, "open_rate": 0.09167706, "close_rate": 0.09213659413533835, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004595341353383492, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0907854156754153, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 07:35:00+00:00", "close_date": "2018-01-23 09:00:00+00:00", "trade_duration": 85, "open_rate": 0.0692498, "close_rate": 0.06959691679197995, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003471167919799484, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4440474918339115, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 10:50:00+00:00", "close_date": "2018-01-23 13:05:00+00:00", "trade_duration": 135, "open_rate": 3.182e-05, "close_rate": 3.197949874686716e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.594987468671663e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3142.677561282213, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 11:05:00+00:00", "close_date": "2018-01-23 16:05:00+00:00", "trade_duration": 300, "open_rate": 0.04088, "close_rate": 0.04108491228070175, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002049122807017481, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4461839530332683, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 14:55:00+00:00", "close_date": "2018-01-23 15:35:00+00:00", "trade_duration": 40, "open_rate": 5.15e-05, "close_rate": 5.175814536340851e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.5814536340851513e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1941.747572815534, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 16:35:00+00:00", "close_date": "2018-01-24 00:05:00+00:00", "trade_duration": 450, "open_rate": 0.09071698, "close_rate": 0.09117170170426064, "open_at_end": false, "sell_reason": "roi", "profit": 0.00045472170426064107, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1023294646713329, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 17:25:00+00:00", "close_date": "2018-01-23 18:45:00+00:00", "trade_duration": 80, "open_rate": 3.128e-05, "close_rate": 3.1436791979949865e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5679197994986587e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3196.9309462915603, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 20:15:00+00:00", "close_date": "2018-01-23 22:00:00+00:00", "trade_duration": 105, "open_rate": 9.555e-05, "close_rate": 9.602894736842104e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.789473684210343e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1046.5724751439038, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 22:30:00+00:00", "close_date": "2018-01-23 23:10:00+00:00", "trade_duration": 40, "open_rate": 0.04080001, "close_rate": 0.0410045213283208, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020451132832080554, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.450979791426522, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 23:50:00+00:00", "close_date": "2018-01-24 03:35:00+00:00", "trade_duration": 225, "open_rate": 5.163e-05, "close_rate": 5.18887969924812e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.587969924812037e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1936.8584156498162, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 00:20:00+00:00", "close_date": "2018-01-24 01:50:00+00:00", "trade_duration": 90, "open_rate": 0.04040781, "close_rate": 0.04061035541353383, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002025454135338306, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.474769110228938, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 06:45:00+00:00", "close_date": "2018-01-24 07:25:00+00:00", "trade_duration": 40, "open_rate": 5.132e-05, "close_rate": 5.157724310776942e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.5724310776941724e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1948.5580670303975, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-24 14:15:00+00:00", "close_date": "2018-01-24 14:25:00+00:00", "trade_duration": 10, "open_rate": 5.198e-05, "close_rate": 5.432496240601503e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.344962406015033e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1923.8168526356292, "profit_abs": 0.0040000000000000036}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 14:50:00+00:00", "close_date": "2018-01-24 16:35:00+00:00", "trade_duration": 105, "open_rate": 3.054e-05, "close_rate": 3.069308270676692e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5308270676691466e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3274.3942370661425, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 15:10:00+00:00", "close_date": "2018-01-24 16:15:00+00:00", "trade_duration": 65, "open_rate": 9.263e-05, "close_rate": 9.309431077694236e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.6431077694236234e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1079.5638562020945, "profit_abs": 2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 22:40:00+00:00", "close_date": "2018-01-24 23:25:00+00:00", "trade_duration": 45, "open_rate": 5.514e-05, "close_rate": 5.54163909774436e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.7639097744360576e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1813.5654697134569, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 00:50:00+00:00", "close_date": "2018-01-25 01:30:00+00:00", "trade_duration": 40, "open_rate": 4.921e-05, "close_rate": 4.9456666666666664e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.4666666666666543e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.1072952651903, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 08:15:00+00:00", "close_date": "2018-01-25 12:15:00+00:00", "trade_duration": 240, "open_rate": 0.0026, "close_rate": 0.002613032581453634, "open_at_end": false, "sell_reason": "roi", "profit": 1.3032581453634e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.46153846153847, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 10:25:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 350, "open_rate": 0.02799871, "close_rate": 0.028139054411027563, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014034441102756326, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.571593119825878, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 11:00:00+00:00", "close_date": "2018-01-25 11:45:00+00:00", "trade_duration": 45, "open_rate": 0.04078902, "close_rate": 0.0409934762406015, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020445624060149575, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4516401717913303, "profit_abs": -1.3877787807814457e-17}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:05:00+00:00", "close_date": "2018-01-25 13:45:00+00:00", "trade_duration": 40, "open_rate": 2.89e-05, "close_rate": 2.904486215538847e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.4486215538846723e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3460.2076124567475, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:20:00+00:00", "close_date": "2018-01-25 14:05:00+00:00", "trade_duration": 45, "open_rate": 0.041103, "close_rate": 0.04130903007518797, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020603007518796984, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4329124394813033, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-25 15:45:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 30, "open_rate": 5.428e-05, "close_rate": 5.509624060150376e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.162406015037611e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1842.2991893883568, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 17:45:00+00:00", "close_date": "2018-01-25 23:15:00+00:00", "trade_duration": 330, "open_rate": 5.414e-05, "close_rate": 5.441137844611528e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.713784461152774e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1847.063169560399, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 21:15:00+00:00", "close_date": "2018-01-25 21:55:00+00:00", "trade_duration": 40, "open_rate": 0.04140777, "close_rate": 0.0416153277443609, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002075577443608964, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.415005686130888, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 02:05:00+00:00", "close_date": "2018-01-26 02:45:00+00:00", "trade_duration": 40, "open_rate": 0.00254309, "close_rate": 0.002555837318295739, "open_at_end": false, "sell_reason": "roi", "profit": 1.2747318295739177e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.32224183965177, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 02:55:00+00:00", "close_date": "2018-01-26 15:10:00+00:00", "trade_duration": 735, "open_rate": 5.607e-05, "close_rate": 5.6351052631578935e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.810526315789381e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1783.4849295523454, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 06:10:00+00:00", "close_date": "2018-01-26 09:25:00+00:00", "trade_duration": 195, "open_rate": 0.00253806, "close_rate": 0.0025507821052631577, "open_at_end": false, "sell_reason": "roi", "profit": 1.2722105263157733e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.400171784748984, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 07:25:00+00:00", "close_date": "2018-01-26 09:55:00+00:00", "trade_duration": 150, "open_rate": 0.0415, "close_rate": 0.04170802005012531, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020802005012530989, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4096385542168677, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-26 09:55:00+00:00", "close_date": "2018-01-26 10:25:00+00:00", "trade_duration": 30, "open_rate": 5.321e-05, "close_rate": 5.401015037593984e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.00150375939842e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1879.3459875963165, "profit_abs": 0.000999999999999987}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 16:05:00+00:00", "close_date": "2018-01-26 16:45:00+00:00", "trade_duration": 40, "open_rate": 0.02772046, "close_rate": 0.02785940967418546, "open_at_end": false, "sell_reason": "roi", "profit": 0.00013894967418546025, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.6074437437185387, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 23:35:00+00:00", "close_date": "2018-01-27 00:15:00+00:00", "trade_duration": 40, "open_rate": 0.09461341, "close_rate": 0.09508766268170424, "open_at_end": false, "sell_reason": "roi", "profit": 0.00047425268170424306, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0569326272036914, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 00:35:00+00:00", "close_date": "2018-01-27 01:30:00+00:00", "trade_duration": 55, "open_rate": 5.615e-05, "close_rate": 5.643145363408521e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.814536340852038e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1780.9439002671415, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.07877175, "open_date": "2018-01-27 00:45:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 4560, "open_rate": 5.556e-05, "close_rate": 5.144e-05, "open_at_end": true, "sell_reason": "force_sell", "profit": -4.120000000000001e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1799.8560115190785, "profit_abs": -0.007896868250539965}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 02:30:00+00:00", "close_date": "2018-01-27 11:25:00+00:00", "trade_duration": 535, "open_rate": 0.06900001, "close_rate": 0.06934587471177944, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003458647117794422, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492751522789635, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 06:25:00+00:00", "close_date": "2018-01-27 07:05:00+00:00", "trade_duration": 40, "open_rate": 0.09449985, "close_rate": 0.0949735334586466, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004736834586466093, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.058202737887944, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.04815133, "open_date": "2018-01-27 09:40:00+00:00", "close_date": "2018-01-30 04:40:00+00:00", "trade_duration": 4020, "open_rate": 0.0410697, "close_rate": 0.03928809, "open_at_end": true, "sell_reason": "force_sell", "profit": -0.001781610000000003, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4348850855983852, "profit_abs": -0.004827170578309559}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 11:45:00+00:00", "close_date": "2018-01-27 12:30:00+00:00", "trade_duration": 45, "open_rate": 0.0285, "close_rate": 0.02864285714285714, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014285714285713902, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5087719298245617, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 12:35:00+00:00", "close_date": "2018-01-27 15:25:00+00:00", "trade_duration": 170, "open_rate": 0.02866372, "close_rate": 0.02880739779448621, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014367779448621124, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.4887307020861216, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 15:50:00+00:00", "close_date": "2018-01-27 16:50:00+00:00", "trade_duration": 60, "open_rate": 0.095381, "close_rate": 0.09585910025062656, "open_at_end": false, "sell_reason": "roi", "profit": 0.00047810025062657024, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0484268355332824, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 17:05:00+00:00", "close_date": "2018-01-27 17:45:00+00:00", "trade_duration": 40, "open_rate": 0.06759092, "close_rate": 0.06792972160401002, "open_at_end": false, "sell_reason": "roi", "profit": 0.00033880160401002224, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4794886650455417, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 23:40:00+00:00", "close_date": "2018-01-28 01:05:00+00:00", "trade_duration": 85, "open_rate": 0.00258501, "close_rate": 0.002597967443609022, "open_at_end": false, "sell_reason": "roi", "profit": 1.2957443609021985e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.684569885609726, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 02:25:00+00:00", "close_date": "2018-01-28 08:10:00+00:00", "trade_duration": 345, "open_rate": 0.06698502, "close_rate": 0.0673207845112782, "open_at_end": false, "sell_reason": "roi", "profit": 0.00033576451127818874, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4928710926711672, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 10:25:00+00:00", "close_date": "2018-01-28 16:30:00+00:00", "trade_duration": 365, "open_rate": 0.0677177, "close_rate": 0.06805713709273183, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003394370927318202, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4767187899175547, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-28 20:35:00+00:00", "close_date": "2018-01-28 21:35:00+00:00", "trade_duration": 60, "open_rate": 5.215e-05, "close_rate": 5.2411403508771925e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.6140350877192417e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1917.5455417066157, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-28 22:00:00+00:00", "close_date": "2018-01-28 22:30:00+00:00", "trade_duration": 30, "open_rate": 0.00273809, "close_rate": 0.002779264285714285, "open_at_end": false, "sell_reason": "roi", "profit": 4.117428571428529e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.5218089982433, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-29 00:00:00+00:00", "close_date": "2018-01-29 00:30:00+00:00", "trade_duration": 30, "open_rate": 0.00274632, "close_rate": 0.002787618045112782, "open_at_end": false, "sell_reason": "roi", "profit": 4.129804511278194e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.412362725392526, "profit_abs": 0.0010000000000000148}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-29 02:15:00+00:00", "close_date": "2018-01-29 03:00:00+00:00", "trade_duration": 45, "open_rate": 0.01622478, "close_rate": 0.016306107218045113, "open_at_end": false, "sell_reason": "roi", "profit": 8.132721804511231e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.163411768911504, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 03:05:00+00:00", "close_date": "2018-01-29 03:45:00+00:00", "trade_duration": 40, "open_rate": 0.069, "close_rate": 0.06934586466165413, "open_at_end": false, "sell_reason": "roi", "profit": 0.00034586466165412166, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492753623188406, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 05:20:00+00:00", "close_date": "2018-01-29 06:55:00+00:00", "trade_duration": 95, "open_rate": 8.755e-05, "close_rate": 8.798884711779448e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.3884711779447504e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1142.204454597373, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 07:00:00+00:00", "close_date": "2018-01-29 19:25:00+00:00", "trade_duration": 745, "open_rate": 0.06825763, "close_rate": 0.06859977350877192, "open_at_end": false, "sell_reason": "roi", "profit": 0.00034214350877191657, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4650376815016872, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 19:45:00+00:00", "close_date": "2018-01-29 20:25:00+00:00", "trade_duration": 40, "open_rate": 0.06713892, "close_rate": 0.06747545593984962, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003365359398496137, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4894490408841845, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0199116, "open_date": "2018-01-29 23:30:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 315, "open_rate": 8.934e-05, "close_rate": 8.8e-05, "open_at_end": true, "sell_reason": "force_sell", "profit": -1.3399999999999973e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1119.3194537721067, "profit_abs": -0.0019961383478844796}], "results_per_pair": [{"key": "TRX/BTC", "trades": 15, "profit_mean": 0.0023467073333333323, "profit_mean_pct": 0.23467073333333321, "profit_sum": 0.035200609999999986, "profit_sum_pct": 3.5200609999999988, "profit_total_abs": 0.0035288616521155086, "profit_total_pct": 1.1733536666666662, "duration_avg": "2:28:00", "wins": 9, "draws": 2, "losses": 4}, {"key": "ADA/BTC", "trades": 29, "profit_mean": -0.0011598141379310352, "profit_mean_pct": -0.11598141379310352, "profit_sum": -0.03363461000000002, "profit_sum_pct": -3.3634610000000023, "profit_total_abs": -0.0033718682505400333, "profit_total_pct": -1.1211536666666675, "duration_avg": "5:35:00", "wins": 9, "draws": 11, "losses": 9}, {"key": "XLM/BTC", "trades": 21, "profit_mean": 0.0026243899999999994, "profit_mean_pct": 0.2624389999999999, "profit_sum": 0.05511218999999999, "profit_sum_pct": 5.511218999999999, "profit_total_abs": 0.005525000000000002, "profit_total_pct": 1.8370729999999995, "duration_avg": "3:21:00", "wins": 12, "draws": 3, "losses": 6}, {"key": "ETH/BTC", "trades": 21, "profit_mean": 0.0009500057142857142, "profit_mean_pct": 0.09500057142857142, "profit_sum": 0.01995012, "profit_sum_pct": 1.9950119999999998, "profit_total_abs": 0.0019999999999999463, "profit_total_pct": 0.6650039999999999, "duration_avg": "2:17:00", "wins": 5, "draws": 10, "losses": 6}, {"key": "XMR/BTC", "trades": 16, "profit_mean": -0.0027899012500000007, "profit_mean_pct": -0.2789901250000001, "profit_sum": -0.04463842000000001, "profit_sum_pct": -4.463842000000001, "profit_total_abs": -0.0044750000000000345, "profit_total_pct": -1.4879473333333337, "duration_avg": "8:41:00", "wins": 6, "draws": 5, "losses": 5}, {"key": "ZEC/BTC", "trades": 21, "profit_mean": -0.00039290904761904774, "profit_mean_pct": -0.03929090476190478, "profit_sum": -0.008251090000000003, "profit_sum_pct": -0.8251090000000003, "profit_total_abs": -0.000827170578309569, "profit_total_pct": -0.27503633333333344, "duration_avg": "4:17:00", "wins": 8, "draws": 7, "losses": 6}, {"key": "NXT/BTC", "trades": 12, "profit_mean": -0.0012261025000000006, "profit_mean_pct": -0.12261025000000006, "profit_sum": -0.014713230000000008, "profit_sum_pct": -1.4713230000000008, "profit_total_abs": -0.0014750000000000874, "profit_total_pct": -0.4904410000000003, "duration_avg": "0:57:00", "wins": 4, "draws": 3, "losses": 5}, {"key": "LTC/BTC", "trades": 8, "profit_mean": 0.00748129625, "profit_mean_pct": 0.748129625, "profit_sum": 0.05985037, "profit_sum_pct": 5.985037, "profit_total_abs": 0.006000000000000019, "profit_total_pct": 1.9950123333333334, "duration_avg": "1:59:00", "wins": 5, "draws": 2, "losses": 1}, {"key": "ETC/BTC", "trades": 20, "profit_mean": 0.0022568569999999997, "profit_mean_pct": 0.22568569999999996, "profit_sum": 0.04513713999999999, "profit_sum_pct": 4.513713999999999, "profit_total_abs": 0.004525000000000001, "profit_total_pct": 1.504571333333333, "duration_avg": "1:45:00", "wins": 11, "draws": 4, "losses": 5}, {"key": "DASH/BTC", "trades": 16, "profit_mean": 0.0018703237499999997, "profit_mean_pct": 0.18703237499999997, "profit_sum": 0.029925179999999996, "profit_sum_pct": 2.9925179999999996, "profit_total_abs": 0.002999999999999961, "profit_total_pct": 0.9975059999999999, "duration_avg": "3:03:00", "wins": 4, "draws": 7, "losses": 5}, {"key": "TOTAL", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}], "sell_reason_summary": [{"sell_reason": "roi", "trades": 170, "wins": 73, "draws": 54, "losses": 43, "profit_mean": 0.005398268352941177, "profit_mean_pct": 0.54, "profit_sum": 0.91770562, "profit_sum_pct": 91.77, "profit_total_abs": 0.09199999999999964, "profit_pct_total": 30.59}, {"sell_reason": "stop_loss", "trades": 6, "wins": 0, "draws": 0, "losses": 6, "profit_mean": -0.10448878000000002, "profit_mean_pct": -10.45, "profit_sum": -0.6269326800000001, "profit_sum_pct": -62.69, "profit_total_abs": -0.06284999999999992, "profit_pct_total": -20.9}, {"sell_reason": "force_sell", "trades": 3, "wins": 0, "draws": 0, "losses": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.89, "profit_sum": -0.14683468, "profit_sum_pct": -14.68, "profit_total_abs": -0.014720177176734003, "profit_pct_total": -4.89}], "left_open_trades": [{"key": "TRX/BTC", "trades": 1, "profit_mean": -0.0199116, "profit_mean_pct": -1.9911600000000003, "profit_sum": -0.0199116, "profit_sum_pct": -1.9911600000000003, "profit_total_abs": -0.0019961383478844796, "profit_total_pct": -0.6637200000000001, "duration_avg": "5:15:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ADA/BTC", "trades": 1, "profit_mean": -0.07877175, "profit_mean_pct": -7.877175, "profit_sum": -0.07877175, "profit_sum_pct": -7.877175, "profit_total_abs": -0.007896868250539965, "profit_total_pct": -2.625725, "duration_avg": "3 days, 4:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ZEC/BTC", "trades": 1, "profit_mean": -0.04815133, "profit_mean_pct": -4.815133, "profit_sum": -0.04815133, "profit_sum_pct": -4.815133, "profit_total_abs": -0.004827170578309559, "profit_total_pct": -1.6050443333333335, "duration_avg": "2 days, 19:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "TOTAL", "trades": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.894489333333333, "profit_sum": -0.14683468, "profit_sum_pct": -14.683468, "profit_total_abs": -0.014720177176734003, "profit_total_pct": -4.8944893333333335, "duration_avg": "2 days, 1:25:00", "wins": 0, "draws": 0, "losses": 3}], "total_trades": 179, "backtest_start": "2018-01-30 04:45:00+00:00", "backtest_start_ts": 1517287500, "backtest_end": "2018-01-30 04:45:00+00:00", "backtest_end_ts": 1517287500, "backtest_days": 0, "trades_per_day": null, "market_change": 0.25, "stake_amount": 0.1, "max_drawdown": 0.21142322000000008, "drawdown_start": "2018-01-24 14:25:00+00:00", "drawdown_start_ts": 1516803900.0, "drawdown_end": "2018-01-30 04:45:00+00:00", "drawdown_end_ts": 1517287500.0}, "TestStrategy": {"trades": [{"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:20:00+00:00", "trade_duration": 5, "open_rate": 9.64e-05, "close_rate": 0.00010074887218045112, "open_at_end": false, "sell_reason": "roi", "profit": 4.348872180451118e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1037.344398340249, "profit_abs": 0.00399999999999999}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:30:00+00:00", "trade_duration": 15, "open_rate": 4.756e-05, "close_rate": 4.9705563909774425e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.1455639097744267e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2102.6072329688814, "profit_abs": 0.00399999999999999}, {"pair": "XLM/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:35:00+00:00", "trade_duration": 10, "open_rate": 3.339e-05, "close_rate": 3.489631578947368e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.506315789473681e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2994.908655286014, "profit_abs": 0.0040000000000000036}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:40:00+00:00", "trade_duration": 15, "open_rate": 9.696e-05, "close_rate": 0.00010133413533834584, "open_at_end": false, "sell_reason": "roi", "profit": 4.3741353383458455e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1031.3531353135315, "profit_abs": 0.00399999999999999}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 07:35:00+00:00", "close_date": "2018-01-10 08:35:00+00:00", "trade_duration": 60, "open_rate": 0.0943, "close_rate": 0.09477268170426063, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004726817042606385, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0604453870625663, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 07:40:00+00:00", "close_date": "2018-01-10 08:10:00+00:00", "trade_duration": 30, "open_rate": 0.02719607, "close_rate": 0.02760503345864661, "open_at_end": false, "sell_reason": "roi", "profit": 0.00040896345864661204, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.677001860930642, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 08:15:00+00:00", "close_date": "2018-01-10 09:55:00+00:00", "trade_duration": 100, "open_rate": 0.04634952, "close_rate": 0.046581848421052625, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002323284210526272, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1575196463739, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 14:45:00+00:00", "close_date": "2018-01-10 15:50:00+00:00", "trade_duration": 65, "open_rate": 3.066e-05, "close_rate": 3.081368421052631e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5368421052630647e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3261.5786040443577, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 16:35:00+00:00", "close_date": "2018-01-10 17:15:00+00:00", "trade_duration": 40, "open_rate": 0.0168999, "close_rate": 0.016984611278195488, "open_at_end": false, "sell_reason": "roi", "profit": 8.471127819548868e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.917194776300452, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 16:40:00+00:00", "close_date": "2018-01-10 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.09132568, "close_rate": 0.0917834528320802, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004577728320801916, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0949822656672252, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 18:50:00+00:00", "close_date": "2018-01-10 19:45:00+00:00", "trade_duration": 55, "open_rate": 0.08898003, "close_rate": 0.08942604518796991, "open_at_end": false, "sell_reason": "roi", "profit": 0.00044601518796991146, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1238476768326557, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 22:15:00+00:00", "close_date": "2018-01-10 23:00:00+00:00", "trade_duration": 45, "open_rate": 0.08560008, "close_rate": 0.08602915308270676, "open_at_end": false, "sell_reason": "roi", "profit": 0.00042907308270676014, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1682232072680307, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 22:50:00+00:00", "close_date": "2018-01-10 23:20:00+00:00", "trade_duration": 30, "open_rate": 0.00249083, "close_rate": 0.0025282860902255634, "open_at_end": false, "sell_reason": "roi", "profit": 3.745609022556351e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 40.147260150231055, "profit_abs": 0.000999999999999987}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 23:15:00+00:00", "close_date": "2018-01-11 00:15:00+00:00", "trade_duration": 60, "open_rate": 3.022e-05, "close_rate": 3.037147869674185e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5147869674185174e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3309.0668431502318, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-10 23:40:00+00:00", "close_date": "2018-01-11 00:05:00+00:00", "trade_duration": 25, "open_rate": 0.002437, "close_rate": 0.0024980776942355883, "open_at_end": false, "sell_reason": "roi", "profit": 6.107769423558838e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.03405826836274, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 00:00:00+00:00", "close_date": "2018-01-11 00:35:00+00:00", "trade_duration": 35, "open_rate": 0.04771803, "close_rate": 0.04843559436090225, "open_at_end": false, "sell_reason": "roi", "profit": 0.0007175643609022495, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0956439316543456, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-11 03:40:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 45, "open_rate": 3.651e-05, "close_rate": 3.2859000000000005e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -3.650999999999996e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2738.9756231169545, "profit_abs": -0.01047499999999997}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 03:55:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 30, "open_rate": 0.08824105, "close_rate": 0.08956798308270676, "open_at_end": false, "sell_reason": "roi", "profit": 0.0013269330827067605, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1332594070446804, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 04:00:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 50, "open_rate": 0.00243, "close_rate": 0.002442180451127819, "open_at_end": false, "sell_reason": "roi", "profit": 1.2180451127819219e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.1522633744856, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:55:00+00:00", "trade_duration": 25, "open_rate": 0.04545064, "close_rate": 0.046589753784461146, "open_at_end": false, "sell_reason": "roi", "profit": 0.001139113784461146, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.200189040242338, "profit_abs": 0.001999999999999988}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 20, "open_rate": 3.372e-05, "close_rate": 3.456511278195488e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.4511278195488e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2965.599051008304, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:55:00+00:00", "close_date": "2018-01-11 05:15:00+00:00", "trade_duration": 20, "open_rate": 0.02644, "close_rate": 0.02710265664160401, "open_at_end": false, "sell_reason": "roi", "profit": 0.0006626566416040071, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7821482602118004, "profit_abs": 0.001999999999999988}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:20:00+00:00", "close_date": "2018-01-11 12:00:00+00:00", "trade_duration": 40, "open_rate": 0.08812, "close_rate": 0.08856170426065162, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004417042606516125, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1348161597821154, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:35:00+00:00", "close_date": "2018-01-11 12:15:00+00:00", "trade_duration": 40, "open_rate": 0.02683577, "close_rate": 0.026970285137844607, "open_at_end": false, "sell_reason": "roi", "profit": 0.00013451513784460897, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7263696923919087, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 14:00:00+00:00", "close_date": "2018-01-11 14:25:00+00:00", "trade_duration": 25, "open_rate": 4.919e-05, "close_rate": 5.04228320802005e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.232832080200495e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.9335230737956, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 19:25:00+00:00", "close_date": "2018-01-11 20:35:00+00:00", "trade_duration": 70, "open_rate": 0.08784896, "close_rate": 0.08828930566416039, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004403456641603881, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1383174029607181, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:35:00+00:00", "close_date": "2018-01-11 23:30:00+00:00", "trade_duration": 55, "open_rate": 5.105e-05, "close_rate": 5.130588972431077e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.558897243107704e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1958.8638589618022, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:25:00+00:00", "trade_duration": 30, "open_rate": 3.96e-05, "close_rate": 4.019548872180451e-05, "open_at_end": false, "sell_reason": "roi", "profit": 5.954887218045116e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2525.252525252525, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:35:00+00:00", "trade_duration": 40, "open_rate": 2.885e-05, "close_rate": 2.899461152882205e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.4461152882205115e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3466.204506065858, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 23:30:00+00:00", "close_date": "2018-01-12 00:05:00+00:00", "trade_duration": 35, "open_rate": 0.02645, "close_rate": 0.026847744360902256, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003977443609022545, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.780718336483932, "profit_abs": 0.0010000000000000148}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 23:55:00+00:00", "close_date": "2018-01-12 01:15:00+00:00", "trade_duration": 80, "open_rate": 0.048, "close_rate": 0.04824060150375939, "open_at_end": false, "sell_reason": "roi", "profit": 0.00024060150375938838, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0833333333333335, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-12 21:15:00+00:00", "close_date": "2018-01-12 21:40:00+00:00", "trade_duration": 25, "open_rate": 4.692e-05, "close_rate": 4.809593984962405e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.1759398496240516e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2131.287297527707, "profit_abs": 0.001999999999999974}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 00:55:00+00:00", "close_date": "2018-01-13 06:20:00+00:00", "trade_duration": 325, "open_rate": 0.00256966, "close_rate": 0.0025825405012531327, "open_at_end": false, "sell_reason": "roi", "profit": 1.2880501253132587e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.91565421106294, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 10:55:00+00:00", "close_date": "2018-01-13 11:35:00+00:00", "trade_duration": 40, "open_rate": 6.262e-05, "close_rate": 6.293388471177944e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.138847117794446e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1596.933886937081, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 13:05:00+00:00", "close_date": "2018-01-15 14:10:00+00:00", "trade_duration": 2945, "open_rate": 4.73e-05, "close_rate": 4.753709273182957e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.3709273182957117e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2114.1649048625795, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:30:00+00:00", "close_date": "2018-01-13 14:45:00+00:00", "trade_duration": 75, "open_rate": 6.063e-05, "close_rate": 6.0933909774436085e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.039097744360846e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1649.348507339601, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:40:00+00:00", "close_date": "2018-01-13 23:30:00+00:00", "trade_duration": 590, "open_rate": 0.00011082, "close_rate": 0.00011137548872180448, "open_at_end": false, "sell_reason": "roi", "profit": 5.554887218044781e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 902.3641941887746, "profit_abs": -2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 15:15:00+00:00", "close_date": "2018-01-13 15:55:00+00:00", "trade_duration": 40, "open_rate": 5.93e-05, "close_rate": 5.9597243107769415e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.9724310776941686e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1686.3406408094436, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 16:30:00+00:00", "close_date": "2018-01-13 17:10:00+00:00", "trade_duration": 40, "open_rate": 0.04850003, "close_rate": 0.04874313791979949, "open_at_end": false, "sell_reason": "roi", "profit": 0.00024310791979949287, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0618543947292407, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 22:05:00+00:00", "close_date": "2018-01-14 06:25:00+00:00", "trade_duration": 500, "open_rate": 0.09825019, "close_rate": 0.09874267215538848, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004924821553884823, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0178097365511456, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 00:20:00+00:00", "close_date": "2018-01-14 22:55:00+00:00", "trade_duration": 1355, "open_rate": 6.018e-05, "close_rate": 6.048165413533834e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.0165413533833987e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1661.681621801263, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 12:45:00+00:00", "close_date": "2018-01-14 13:25:00+00:00", "trade_duration": 40, "open_rate": 0.09758999, "close_rate": 0.0980791628822055, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004891728822054991, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.024695258191952, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-14 15:30:00+00:00", "close_date": "2018-01-14 16:00:00+00:00", "trade_duration": 30, "open_rate": 0.00311, "close_rate": 0.0031567669172932328, "open_at_end": false, "sell_reason": "roi", "profit": 4.676691729323286e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.154340836012864, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 20:45:00+00:00", "close_date": "2018-01-14 22:15:00+00:00", "trade_duration": 90, "open_rate": 0.00312401, "close_rate": 0.003139669197994987, "open_at_end": false, "sell_reason": "roi", "profit": 1.5659197994987058e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.010140812609436, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 23:35:00+00:00", "close_date": "2018-01-15 00:30:00+00:00", "trade_duration": 55, "open_rate": 0.0174679, "close_rate": 0.017555458395989976, "open_at_end": false, "sell_reason": "roi", "profit": 8.755839598997492e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.724786608579165, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 23:45:00+00:00", "close_date": "2018-01-15 00:25:00+00:00", "trade_duration": 40, "open_rate": 0.07346846, "close_rate": 0.07383672295739348, "open_at_end": false, "sell_reason": "roi", "profit": 0.00036826295739347814, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.3611282991367997, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 02:25:00+00:00", "close_date": "2018-01-15 03:05:00+00:00", "trade_duration": 40, "open_rate": 0.097994, "close_rate": 0.09848519799498744, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004911979949874384, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.020470641059657, "profit_abs": -2.7755575615628914e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 07:20:00+00:00", "close_date": "2018-01-15 08:00:00+00:00", "trade_duration": 40, "open_rate": 0.09659, "close_rate": 0.09707416040100247, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004841604010024786, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0353038616834043, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-15 08:20:00+00:00", "close_date": "2018-01-15 08:55:00+00:00", "trade_duration": 35, "open_rate": 9.987e-05, "close_rate": 0.00010137180451127818, "open_at_end": false, "sell_reason": "roi", "profit": 1.501804511278178e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1001.3016921998599, "profit_abs": 0.0010000000000000009}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-15 12:10:00+00:00", "close_date": "2018-01-16 02:50:00+00:00", "trade_duration": 880, "open_rate": 0.0948969, "close_rate": 0.09537257368421052, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004756736842105175, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0537752023511833, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:10:00+00:00", "close_date": "2018-01-15 17:40:00+00:00", "trade_duration": 210, "open_rate": 0.071, "close_rate": 0.07135588972431077, "open_at_end": false, "sell_reason": "roi", "profit": 0.00035588972431077615, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4084507042253522, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:30:00+00:00", "close_date": "2018-01-15 15:10:00+00:00", "trade_duration": 40, "open_rate": 0.04600501, "close_rate": 0.046235611553884705, "open_at_end": false, "sell_reason": "roi", "profit": 0.00023060155388470588, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.173676301776698, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:10:00+00:00", "close_date": "2018-01-15 19:25:00+00:00", "trade_duration": 75, "open_rate": 9.438e-05, "close_rate": 9.485308270676693e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.7308270676692514e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1059.5465140919687, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:35:00+00:00", "close_date": "2018-01-15 19:15:00+00:00", "trade_duration": 40, "open_rate": 0.03040001, "close_rate": 0.030552391002506264, "open_at_end": false, "sell_reason": "roi", "profit": 0.0001523810025062626, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.2894726021471703, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-15 20:25:00+00:00", "close_date": "2018-01-16 08:25:00+00:00", "trade_duration": 720, "open_rate": 5.837e-05, "close_rate": 5.2533e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -5.8369999999999985e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1713.2088401576154, "profit_abs": -0.010474999999999984}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 20:40:00+00:00", "close_date": "2018-01-15 22:00:00+00:00", "trade_duration": 80, "open_rate": 0.046036, "close_rate": 0.04626675689223057, "open_at_end": false, "sell_reason": "roi", "profit": 0.00023075689223057277, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1722130506560084, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 00:30:00+00:00", "close_date": "2018-01-16 01:10:00+00:00", "trade_duration": 40, "open_rate": 0.0028685, "close_rate": 0.0028828784461152877, "open_at_end": false, "sell_reason": "roi", "profit": 1.4378446115287727e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 34.86142583231654, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 01:15:00+00:00", "close_date": "2018-01-16 02:35:00+00:00", "trade_duration": 80, "open_rate": 0.06731755, "close_rate": 0.0676549813283208, "open_at_end": false, "sell_reason": "roi", "profit": 0.00033743132832080025, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4854967241083492, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 07:45:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 55, "open_rate": 0.09217614, "close_rate": 0.09263817578947368, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004620357894736804, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0848794492804754, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:55:00+00:00", "trade_duration": 20, "open_rate": 0.0165, "close_rate": 0.016913533834586467, "open_at_end": false, "sell_reason": "roi", "profit": 0.00041353383458646656, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.0606060606060606, "profit_abs": 0.0020000000000000018}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 5, "open_rate": 7.953e-05, "close_rate": 8.311781954887218e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.587819548872171e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1257.387149503332, "profit_abs": 0.00399999999999999}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 08:45:00+00:00", "close_date": "2018-01-16 09:50:00+00:00", "trade_duration": 65, "open_rate": 0.045202, "close_rate": 0.04542857644110275, "open_at_end": false, "sell_reason": "roi", "profit": 0.00022657644110275071, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2122914915269236, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:45:00+00:00", "trade_duration": 30, "open_rate": 5.248e-05, "close_rate": 5.326917293233082e-05, "open_at_end": false, "sell_reason": "roi", "profit": 7.891729323308177e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1905.487804878049, "profit_abs": 0.0010000000000000009}, {"pair": "XMR/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:55:00+00:00", "trade_duration": 40, "open_rate": 0.02892318, "close_rate": 0.02906815834586466, "open_at_end": false, "sell_reason": "roi", "profit": 0.0001449783458646603, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.457434486802627, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 09:50:00+00:00", "close_date": "2018-01-16 10:10:00+00:00", "trade_duration": 20, "open_rate": 5.158e-05, "close_rate": 5.287273182957392e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.2927318295739246e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1938.735944164405, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:35:00+00:00", "trade_duration": 30, "open_rate": 0.02828232, "close_rate": 0.02870761804511278, "open_at_end": false, "sell_reason": "roi", "profit": 0.00042529804511277913, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5357778286929786, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:40:00+00:00", "trade_duration": 35, "open_rate": 0.04357584, "close_rate": 0.044231115789473675, "open_at_end": false, "sell_reason": "roi", "profit": 0.0006552757894736777, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.294849623093898, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 13:45:00+00:00", "close_date": "2018-01-16 14:20:00+00:00", "trade_duration": 35, "open_rate": 5.362e-05, "close_rate": 5.442631578947368e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.063157894736843e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1864.975755315181, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 17:30:00+00:00", "close_date": "2018-01-16 18:25:00+00:00", "trade_duration": 55, "open_rate": 5.302e-05, "close_rate": 5.328576441102756e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.6576441102756397e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.0807242549984, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:45:00+00:00", "trade_duration": 30, "open_rate": 0.09129999, "close_rate": 0.09267292218045112, "open_at_end": false, "sell_reason": "roi", "profit": 0.0013729321804511196, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0952903718828448, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:35:00+00:00", "trade_duration": 20, "open_rate": 3.808e-05, "close_rate": 3.903438596491228e-05, "open_at_end": false, "sell_reason": "roi", "profit": 9.543859649122774e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2626.0504201680674, "profit_abs": 0.0020000000000000018}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 19:00:00+00:00", "close_date": "2018-01-16 19:30:00+00:00", "trade_duration": 30, "open_rate": 0.02811012, "close_rate": 0.028532828571428567, "open_at_end": false, "sell_reason": "roi", "profit": 0.00042270857142856846, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.557437677249333, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 60, "open_rate": 0.00258379, "close_rate": 0.002325411, "open_at_end": false, "sell_reason": "stop_loss", "profit": -0.000258379, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.702835756775904, "profit_abs": -0.010474999999999984}, {"pair": "NXT/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 80, "open_rate": 2.559e-05, "close_rate": 2.3031e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -2.5590000000000004e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3907.7764751856193, "profit_abs": -0.010474999999999998}, {"pair": "TRX/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:35:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 50, "open_rate": 7.62e-05, "close_rate": 6.858e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -7.619999999999998e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1312.3359580052495, "profit_abs": -0.010474999999999984}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:35:00+00:00", "trade_duration": 5, "open_rate": 0.00229844, "close_rate": 0.002402129022556391, "open_at_end": false, "sell_reason": "roi", "profit": 0.00010368902255639091, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 43.507770487809125, "profit_abs": 0.004000000000000017}, {"pair": "LTC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:40:00+00:00", "trade_duration": 10, "open_rate": 0.0151, "close_rate": 0.015781203007518795, "open_at_end": false, "sell_reason": "roi", "profit": 0.0006812030075187946, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.622516556291391, "profit_abs": 0.00399999999999999}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:40:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 5, "open_rate": 0.00235676, "close_rate": 0.00246308, "open_at_end": false, "sell_reason": "roi", "profit": 0.00010632000000000003, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 42.431134269081284, "profit_abs": 0.0040000000000000036}, {"pair": "DASH/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 22:45:00+00:00", "close_date": "2018-01-16 23:05:00+00:00", "trade_duration": 20, "open_rate": 0.0630692, "close_rate": 0.06464988170426066, "open_at_end": false, "sell_reason": "roi", "profit": 0.0015806817042606502, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.585559988076589, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:50:00+00:00", "close_date": "2018-01-16 22:55:00+00:00", "trade_duration": 5, "open_rate": 2.2e-05, "close_rate": 2.299248120300751e-05, "open_at_end": false, "sell_reason": "roi", "profit": 9.924812030075114e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 4545.454545454546, "profit_abs": 0.003999999999999976}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-17 03:30:00+00:00", "close_date": "2018-01-17 04:00:00+00:00", "trade_duration": 30, "open_rate": 4.974e-05, "close_rate": 5.048796992481203e-05, "open_at_end": false, "sell_reason": "roi", "profit": 7.479699248120277e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2010.454362685967, "profit_abs": 0.0010000000000000009}, {"pair": "TRX/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-17 03:55:00+00:00", "close_date": "2018-01-17 04:15:00+00:00", "trade_duration": 20, "open_rate": 7.108e-05, "close_rate": 7.28614536340852e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.7814536340851996e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1406.8655036578502, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 09:35:00+00:00", "close_date": "2018-01-17 10:15:00+00:00", "trade_duration": 40, "open_rate": 0.04327, "close_rate": 0.04348689223057644, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002168922305764362, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.3110700254217704, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:20:00+00:00", "close_date": "2018-01-17 17:00:00+00:00", "trade_duration": 400, "open_rate": 4.997e-05, "close_rate": 5.022047619047618e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.504761904761831e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2001.2007204322595, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:25:00+00:00", "trade_duration": 55, "open_rate": 0.06836818, "close_rate": 0.06871087764411027, "open_at_end": false, "sell_reason": "roi", "profit": 0.00034269764411026804, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4626687444363737, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:10:00+00:00", "trade_duration": 40, "open_rate": 3.63e-05, "close_rate": 3.648195488721804e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.8195488721804031e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2754.8209366391184, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:30:00+00:00", "close_date": "2018-01-17 22:05:00+00:00", "trade_duration": 575, "open_rate": 0.0281, "close_rate": 0.02824085213032581, "open_at_end": false, "sell_reason": "roi", "profit": 0.0001408521303258095, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5587188612099645, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:35:00+00:00", "close_date": "2018-01-17 16:55:00+00:00", "trade_duration": 260, "open_rate": 0.08651001, "close_rate": 0.08694364413533832, "open_at_end": false, "sell_reason": "roi", "profit": 0.00043363413533832607, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1559355963546878, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 05:00:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 55, "open_rate": 5.633e-05, "close_rate": 5.6612355889724306e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.8235588972430847e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1775.2529735487308, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 05:20:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 35, "open_rate": 0.06988494, "close_rate": 0.07093584135338346, "open_at_end": false, "sell_reason": "roi", "profit": 0.0010509013533834544, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.430923457900944, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 07:35:00+00:00", "close_date": "2018-01-18 08:15:00+00:00", "trade_duration": 40, "open_rate": 5.545e-05, "close_rate": 5.572794486215538e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.779448621553787e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1803.4265103697026, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 09:00:00+00:00", "close_date": "2018-01-18 09:40:00+00:00", "trade_duration": 40, "open_rate": 0.01633527, "close_rate": 0.016417151052631574, "open_at_end": false, "sell_reason": "roi", "profit": 8.188105263157511e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.121723118136401, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 16:40:00+00:00", "close_date": "2018-01-18 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.00269734, "close_rate": 0.002710860501253133, "open_at_end": false, "sell_reason": "roi", "profit": 1.3520501253133123e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.073561360451414, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-18 18:05:00+00:00", "close_date": "2018-01-18 18:30:00+00:00", "trade_duration": 25, "open_rate": 4.475e-05, "close_rate": 4.587155388471177e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.1215538847117757e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2234.63687150838, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 18:25:00+00:00", "close_date": "2018-01-18 18:55:00+00:00", "trade_duration": 30, "open_rate": 2.79e-05, "close_rate": 2.8319548872180444e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.1954887218044365e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3584.2293906810037, "profit_abs": 0.000999999999999987}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 20:10:00+00:00", "close_date": "2018-01-18 20:50:00+00:00", "trade_duration": 40, "open_rate": 0.04439326, "close_rate": 0.04461578260651629, "open_at_end": false, "sell_reason": "roi", "profit": 0.00022252260651629135, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2525942001105577, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 21:30:00+00:00", "close_date": "2018-01-19 00:35:00+00:00", "trade_duration": 185, "open_rate": 4.49e-05, "close_rate": 4.51250626566416e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.2506265664159932e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2227.1714922049, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 21:55:00+00:00", "close_date": "2018-01-19 05:05:00+00:00", "trade_duration": 430, "open_rate": 0.02855, "close_rate": 0.028693107769423555, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014310776942355607, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.502626970227671, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 22:10:00+00:00", "close_date": "2018-01-18 22:50:00+00:00", "trade_duration": 40, "open_rate": 5.796e-05, "close_rate": 5.8250526315789473e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.905263157894727e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1725.3278122843342, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 23:50:00+00:00", "close_date": "2018-01-19 00:30:00+00:00", "trade_duration": 40, "open_rate": 0.04340323, "close_rate": 0.04362079005012531, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002175600501253122, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.303975994413319, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-19 16:45:00+00:00", "close_date": "2018-01-19 17:35:00+00:00", "trade_duration": 50, "open_rate": 0.04454455, "close_rate": 0.04476783095238095, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002232809523809512, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.244943545282195, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:15:00+00:00", "close_date": "2018-01-19 19:55:00+00:00", "trade_duration": 160, "open_rate": 5.62e-05, "close_rate": 5.648170426065162e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.817042606516199e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1779.3594306049824, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:20:00+00:00", "close_date": "2018-01-19 20:15:00+00:00", "trade_duration": 175, "open_rate": 4.339e-05, "close_rate": 4.360749373433584e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.174937343358337e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2304.6784973496196, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-20 04:45:00+00:00", "close_date": "2018-01-20 17:35:00+00:00", "trade_duration": 770, "open_rate": 0.0001009, "close_rate": 0.00010140576441102755, "open_at_end": false, "sell_reason": "roi", "profit": 5.057644110275549e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 991.0802775024778, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 15:15:00+00:00", "trade_duration": 625, "open_rate": 0.00270505, "close_rate": 0.002718609147869674, "open_at_end": false, "sell_reason": "roi", "profit": 1.3559147869673764e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.96789338459548, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 07:00:00+00:00", "trade_duration": 130, "open_rate": 0.03000002, "close_rate": 0.030150396040100245, "open_at_end": false, "sell_reason": "roi", "profit": 0.00015037604010024672, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.3333311111125927, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 09:00:00+00:00", "close_date": "2018-01-20 09:40:00+00:00", "trade_duration": 40, "open_rate": 5.46e-05, "close_rate": 5.4873684210526304e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.736842105263053e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1831.5018315018317, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-20 18:25:00+00:00", "close_date": "2018-01-25 03:50:00+00:00", "trade_duration": 6325, "open_rate": 0.03082222, "close_rate": 0.027739998, "open_at_end": false, "sell_reason": "stop_loss", "profit": -0.0030822220000000025, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.244412634781012, "profit_abs": -0.010474999999999998}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 22:25:00+00:00", "close_date": "2018-01-20 23:15:00+00:00", "trade_duration": 50, "open_rate": 0.08969999, "close_rate": 0.09014961401002504, "open_at_end": false, "sell_reason": "roi", "profit": 0.00044962401002504593, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1148273260677064, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 02:50:00+00:00", "close_date": "2018-01-21 14:30:00+00:00", "trade_duration": 700, "open_rate": 0.01632501, "close_rate": 0.01640683962406015, "open_at_end": false, "sell_reason": "roi", "profit": 8.182962406014932e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.125570520324337, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 10:20:00+00:00", "close_date": "2018-01-21 11:00:00+00:00", "trade_duration": 40, "open_rate": 0.070538, "close_rate": 0.07089157393483708, "open_at_end": false, "sell_reason": "roi", "profit": 0.00035357393483707866, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.417675579120474, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 15:50:00+00:00", "close_date": "2018-01-21 18:45:00+00:00", "trade_duration": 175, "open_rate": 5.301e-05, "close_rate": 5.327571428571427e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.657142857142672e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.4365214110546, "profit_abs": -2.7755575615628914e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 16:20:00+00:00", "close_date": "2018-01-21 17:00:00+00:00", "trade_duration": 40, "open_rate": 3.955e-05, "close_rate": 3.9748245614035085e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.9824561403508552e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2528.4450063211125, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:45:00+00:00", "trade_duration": 30, "open_rate": 0.00258505, "close_rate": 0.002623922932330827, "open_at_end": false, "sell_reason": "roi", "profit": 3.8872932330826816e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.6839712964933, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:55:00+00:00", "trade_duration": 40, "open_rate": 3.903e-05, "close_rate": 3.922563909774435e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.9563909774435151e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2562.1316935690497, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 00:35:00+00:00", "close_date": "2018-01-22 10:35:00+00:00", "trade_duration": 600, "open_rate": 5.236e-05, "close_rate": 5.262245614035087e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.624561403508717e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1909.8548510313217, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 01:30:00+00:00", "close_date": "2018-01-22 02:10:00+00:00", "trade_duration": 40, "open_rate": 9.028e-05, "close_rate": 9.07325313283208e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.5253132832080657e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1107.6650420912717, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 12:25:00+00:00", "close_date": "2018-01-22 14:35:00+00:00", "trade_duration": 130, "open_rate": 0.002687, "close_rate": 0.002700468671679198, "open_at_end": false, "sell_reason": "roi", "profit": 1.3468671679197925e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.21622627465575, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 13:15:00+00:00", "close_date": "2018-01-22 13:55:00+00:00", "trade_duration": 40, "open_rate": 4.168e-05, "close_rate": 4.188892230576441e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.0892230576441054e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2399.232245681382, "profit_abs": 1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-22 14:00:00+00:00", "close_date": "2018-01-22 14:30:00+00:00", "trade_duration": 30, "open_rate": 8.821e-05, "close_rate": 8.953646616541353e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.326466165413529e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1133.6583153837435, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 15:55:00+00:00", "close_date": "2018-01-22 16:40:00+00:00", "trade_duration": 45, "open_rate": 5.172e-05, "close_rate": 5.1979248120300745e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.592481203007459e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1933.4880123743235, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-22 16:05:00+00:00", "close_date": "2018-01-22 16:25:00+00:00", "trade_duration": 20, "open_rate": 3.026e-05, "close_rate": 3.101839598997494e-05, "open_at_end": false, "sell_reason": "roi", "profit": 7.5839598997494e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3304.692663582287, "profit_abs": 0.0020000000000000157}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 19:50:00+00:00", "close_date": "2018-01-23 00:10:00+00:00", "trade_duration": 260, "open_rate": 0.07064, "close_rate": 0.07099408521303258, "open_at_end": false, "sell_reason": "roi", "profit": 0.00035408521303258167, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.415628539071348, "profit_abs": 1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 21:25:00+00:00", "close_date": "2018-01-22 22:05:00+00:00", "trade_duration": 40, "open_rate": 0.01644483, "close_rate": 0.01652726022556391, "open_at_end": false, "sell_reason": "roi", "profit": 8.243022556390922e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.080938507725528, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-23 00:05:00+00:00", "close_date": "2018-01-23 00:35:00+00:00", "trade_duration": 30, "open_rate": 4.331e-05, "close_rate": 4.3961278195488714e-05, "open_at_end": false, "sell_reason": "roi", "profit": 6.512781954887175e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2308.935580697299, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-23 01:50:00+00:00", "close_date": "2018-01-23 02:15:00+00:00", "trade_duration": 25, "open_rate": 3.2e-05, "close_rate": 3.2802005012531326e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.020050125313278e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3125.0000000000005, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 04:25:00+00:00", "close_date": "2018-01-23 05:15:00+00:00", "trade_duration": 50, "open_rate": 0.09167706, "close_rate": 0.09213659413533835, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004595341353383492, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0907854156754153, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 07:35:00+00:00", "close_date": "2018-01-23 09:00:00+00:00", "trade_duration": 85, "open_rate": 0.0692498, "close_rate": 0.06959691679197995, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003471167919799484, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4440474918339115, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 10:50:00+00:00", "close_date": "2018-01-23 13:05:00+00:00", "trade_duration": 135, "open_rate": 3.182e-05, "close_rate": 3.197949874686716e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.594987468671663e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3142.677561282213, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 11:05:00+00:00", "close_date": "2018-01-23 16:05:00+00:00", "trade_duration": 300, "open_rate": 0.04088, "close_rate": 0.04108491228070175, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002049122807017481, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4461839530332683, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 14:55:00+00:00", "close_date": "2018-01-23 15:35:00+00:00", "trade_duration": 40, "open_rate": 5.15e-05, "close_rate": 5.175814536340851e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.5814536340851513e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1941.747572815534, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 16:35:00+00:00", "close_date": "2018-01-24 00:05:00+00:00", "trade_duration": 450, "open_rate": 0.09071698, "close_rate": 0.09117170170426064, "open_at_end": false, "sell_reason": "roi", "profit": 0.00045472170426064107, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1023294646713329, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 17:25:00+00:00", "close_date": "2018-01-23 18:45:00+00:00", "trade_duration": 80, "open_rate": 3.128e-05, "close_rate": 3.1436791979949865e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5679197994986587e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3196.9309462915603, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 20:15:00+00:00", "close_date": "2018-01-23 22:00:00+00:00", "trade_duration": 105, "open_rate": 9.555e-05, "close_rate": 9.602894736842104e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.789473684210343e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1046.5724751439038, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 22:30:00+00:00", "close_date": "2018-01-23 23:10:00+00:00", "trade_duration": 40, "open_rate": 0.04080001, "close_rate": 0.0410045213283208, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020451132832080554, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.450979791426522, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 23:50:00+00:00", "close_date": "2018-01-24 03:35:00+00:00", "trade_duration": 225, "open_rate": 5.163e-05, "close_rate": 5.18887969924812e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.587969924812037e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1936.8584156498162, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 00:20:00+00:00", "close_date": "2018-01-24 01:50:00+00:00", "trade_duration": 90, "open_rate": 0.04040781, "close_rate": 0.04061035541353383, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002025454135338306, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.474769110228938, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 06:45:00+00:00", "close_date": "2018-01-24 07:25:00+00:00", "trade_duration": 40, "open_rate": 5.132e-05, "close_rate": 5.157724310776942e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.5724310776941724e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1948.5580670303975, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-24 14:15:00+00:00", "close_date": "2018-01-24 14:25:00+00:00", "trade_duration": 10, "open_rate": 5.198e-05, "close_rate": 5.432496240601503e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.344962406015033e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1923.8168526356292, "profit_abs": 0.0040000000000000036}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 14:50:00+00:00", "close_date": "2018-01-24 16:35:00+00:00", "trade_duration": 105, "open_rate": 3.054e-05, "close_rate": 3.069308270676692e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5308270676691466e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3274.3942370661425, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 15:10:00+00:00", "close_date": "2018-01-24 16:15:00+00:00", "trade_duration": 65, "open_rate": 9.263e-05, "close_rate": 9.309431077694236e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.6431077694236234e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1079.5638562020945, "profit_abs": 2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 22:40:00+00:00", "close_date": "2018-01-24 23:25:00+00:00", "trade_duration": 45, "open_rate": 5.514e-05, "close_rate": 5.54163909774436e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.7639097744360576e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1813.5654697134569, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 00:50:00+00:00", "close_date": "2018-01-25 01:30:00+00:00", "trade_duration": 40, "open_rate": 4.921e-05, "close_rate": 4.9456666666666664e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.4666666666666543e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.1072952651903, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 08:15:00+00:00", "close_date": "2018-01-25 12:15:00+00:00", "trade_duration": 240, "open_rate": 0.0026, "close_rate": 0.002613032581453634, "open_at_end": false, "sell_reason": "roi", "profit": 1.3032581453634e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.46153846153847, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 10:25:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 350, "open_rate": 0.02799871, "close_rate": 0.028139054411027563, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014034441102756326, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.571593119825878, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 11:00:00+00:00", "close_date": "2018-01-25 11:45:00+00:00", "trade_duration": 45, "open_rate": 0.04078902, "close_rate": 0.0409934762406015, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020445624060149575, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4516401717913303, "profit_abs": -1.3877787807814457e-17}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:05:00+00:00", "close_date": "2018-01-25 13:45:00+00:00", "trade_duration": 40, "open_rate": 2.89e-05, "close_rate": 2.904486215538847e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.4486215538846723e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3460.2076124567475, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:20:00+00:00", "close_date": "2018-01-25 14:05:00+00:00", "trade_duration": 45, "open_rate": 0.041103, "close_rate": 0.04130903007518797, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020603007518796984, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4329124394813033, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-25 15:45:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 30, "open_rate": 5.428e-05, "close_rate": 5.509624060150376e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.162406015037611e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1842.2991893883568, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 17:45:00+00:00", "close_date": "2018-01-25 23:15:00+00:00", "trade_duration": 330, "open_rate": 5.414e-05, "close_rate": 5.441137844611528e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.713784461152774e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1847.063169560399, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 21:15:00+00:00", "close_date": "2018-01-25 21:55:00+00:00", "trade_duration": 40, "open_rate": 0.04140777, "close_rate": 0.0416153277443609, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002075577443608964, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.415005686130888, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 02:05:00+00:00", "close_date": "2018-01-26 02:45:00+00:00", "trade_duration": 40, "open_rate": 0.00254309, "close_rate": 0.002555837318295739, "open_at_end": false, "sell_reason": "roi", "profit": 1.2747318295739177e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.32224183965177, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 02:55:00+00:00", "close_date": "2018-01-26 15:10:00+00:00", "trade_duration": 735, "open_rate": 5.607e-05, "close_rate": 5.6351052631578935e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.810526315789381e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1783.4849295523454, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 06:10:00+00:00", "close_date": "2018-01-26 09:25:00+00:00", "trade_duration": 195, "open_rate": 0.00253806, "close_rate": 0.0025507821052631577, "open_at_end": false, "sell_reason": "roi", "profit": 1.2722105263157733e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.400171784748984, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 07:25:00+00:00", "close_date": "2018-01-26 09:55:00+00:00", "trade_duration": 150, "open_rate": 0.0415, "close_rate": 0.04170802005012531, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020802005012530989, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4096385542168677, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-26 09:55:00+00:00", "close_date": "2018-01-26 10:25:00+00:00", "trade_duration": 30, "open_rate": 5.321e-05, "close_rate": 5.401015037593984e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.00150375939842e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1879.3459875963165, "profit_abs": 0.000999999999999987}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 16:05:00+00:00", "close_date": "2018-01-26 16:45:00+00:00", "trade_duration": 40, "open_rate": 0.02772046, "close_rate": 0.02785940967418546, "open_at_end": false, "sell_reason": "roi", "profit": 0.00013894967418546025, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.6074437437185387, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 23:35:00+00:00", "close_date": "2018-01-27 00:15:00+00:00", "trade_duration": 40, "open_rate": 0.09461341, "close_rate": 0.09508766268170424, "open_at_end": false, "sell_reason": "roi", "profit": 0.00047425268170424306, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0569326272036914, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 00:35:00+00:00", "close_date": "2018-01-27 01:30:00+00:00", "trade_duration": 55, "open_rate": 5.615e-05, "close_rate": 5.643145363408521e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.814536340852038e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1780.9439002671415, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.07877175, "open_date": "2018-01-27 00:45:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 4560, "open_rate": 5.556e-05, "close_rate": 5.144e-05, "open_at_end": true, "sell_reason": "force_sell", "profit": -4.120000000000001e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1799.8560115190785, "profit_abs": -0.007896868250539965}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 02:30:00+00:00", "close_date": "2018-01-27 11:25:00+00:00", "trade_duration": 535, "open_rate": 0.06900001, "close_rate": 0.06934587471177944, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003458647117794422, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492751522789635, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 06:25:00+00:00", "close_date": "2018-01-27 07:05:00+00:00", "trade_duration": 40, "open_rate": 0.09449985, "close_rate": 0.0949735334586466, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004736834586466093, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.058202737887944, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.04815133, "open_date": "2018-01-27 09:40:00+00:00", "close_date": "2018-01-30 04:40:00+00:00", "trade_duration": 4020, "open_rate": 0.0410697, "close_rate": 0.03928809, "open_at_end": true, "sell_reason": "force_sell", "profit": -0.001781610000000003, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4348850855983852, "profit_abs": -0.004827170578309559}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 11:45:00+00:00", "close_date": "2018-01-27 12:30:00+00:00", "trade_duration": 45, "open_rate": 0.0285, "close_rate": 0.02864285714285714, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014285714285713902, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5087719298245617, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 12:35:00+00:00", "close_date": "2018-01-27 15:25:00+00:00", "trade_duration": 170, "open_rate": 0.02866372, "close_rate": 0.02880739779448621, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014367779448621124, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.4887307020861216, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 15:50:00+00:00", "close_date": "2018-01-27 16:50:00+00:00", "trade_duration": 60, "open_rate": 0.095381, "close_rate": 0.09585910025062656, "open_at_end": false, "sell_reason": "roi", "profit": 0.00047810025062657024, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0484268355332824, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 17:05:00+00:00", "close_date": "2018-01-27 17:45:00+00:00", "trade_duration": 40, "open_rate": 0.06759092, "close_rate": 0.06792972160401002, "open_at_end": false, "sell_reason": "roi", "profit": 0.00033880160401002224, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4794886650455417, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 23:40:00+00:00", "close_date": "2018-01-28 01:05:00+00:00", "trade_duration": 85, "open_rate": 0.00258501, "close_rate": 0.002597967443609022, "open_at_end": false, "sell_reason": "roi", "profit": 1.2957443609021985e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.684569885609726, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 02:25:00+00:00", "close_date": "2018-01-28 08:10:00+00:00", "trade_duration": 345, "open_rate": 0.06698502, "close_rate": 0.0673207845112782, "open_at_end": false, "sell_reason": "roi", "profit": 0.00033576451127818874, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4928710926711672, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 10:25:00+00:00", "close_date": "2018-01-28 16:30:00+00:00", "trade_duration": 365, "open_rate": 0.0677177, "close_rate": 0.06805713709273183, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003394370927318202, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4767187899175547, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-28 20:35:00+00:00", "close_date": "2018-01-28 21:35:00+00:00", "trade_duration": 60, "open_rate": 5.215e-05, "close_rate": 5.2411403508771925e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.6140350877192417e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1917.5455417066157, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-28 22:00:00+00:00", "close_date": "2018-01-28 22:30:00+00:00", "trade_duration": 30, "open_rate": 0.00273809, "close_rate": 0.002779264285714285, "open_at_end": false, "sell_reason": "roi", "profit": 4.117428571428529e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.5218089982433, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-29 00:00:00+00:00", "close_date": "2018-01-29 00:30:00+00:00", "trade_duration": 30, "open_rate": 0.00274632, "close_rate": 0.002787618045112782, "open_at_end": false, "sell_reason": "roi", "profit": 4.129804511278194e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.412362725392526, "profit_abs": 0.0010000000000000148}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-29 02:15:00+00:00", "close_date": "2018-01-29 03:00:00+00:00", "trade_duration": 45, "open_rate": 0.01622478, "close_rate": 0.016306107218045113, "open_at_end": false, "sell_reason": "roi", "profit": 8.132721804511231e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.163411768911504, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 03:05:00+00:00", "close_date": "2018-01-29 03:45:00+00:00", "trade_duration": 40, "open_rate": 0.069, "close_rate": 0.06934586466165413, "open_at_end": false, "sell_reason": "roi", "profit": 0.00034586466165412166, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492753623188406, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 05:20:00+00:00", "close_date": "2018-01-29 06:55:00+00:00", "trade_duration": 95, "open_rate": 8.755e-05, "close_rate": 8.798884711779448e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.3884711779447504e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1142.204454597373, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 07:00:00+00:00", "close_date": "2018-01-29 19:25:00+00:00", "trade_duration": 745, "open_rate": 0.06825763, "close_rate": 0.06859977350877192, "open_at_end": false, "sell_reason": "roi", "profit": 0.00034214350877191657, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4650376815016872, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 19:45:00+00:00", "close_date": "2018-01-29 20:25:00+00:00", "trade_duration": 40, "open_rate": 0.06713892, "close_rate": 0.06747545593984962, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003365359398496137, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4894490408841845, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0199116, "open_date": "2018-01-29 23:30:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 315, "open_rate": 8.934e-05, "close_rate": 8.8e-05, "open_at_end": true, "sell_reason": "force_sell", "profit": -1.3399999999999973e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1119.3194537721067, "profit_abs": -0.0019961383478844796}], "results_per_pair": [{"key": "TRX/BTC", "trades": 15, "profit_mean": 0.0023467073333333323, "profit_mean_pct": 0.23467073333333321, "profit_sum": 0.035200609999999986, "profit_sum_pct": 3.5200609999999988, "profit_total_abs": 0.0035288616521155086, "profit_total_pct": 1.1733536666666662, "duration_avg": "2:28:00", "wins": 9, "draws": 2, "losses": 4}, {"key": "ADA/BTC", "trades": 29, "profit_mean": -0.0011598141379310352, "profit_mean_pct": -0.11598141379310352, "profit_sum": -0.03363461000000002, "profit_sum_pct": -3.3634610000000023, "profit_total_abs": -0.0033718682505400333, "profit_total_pct": -1.1211536666666675, "duration_avg": "5:35:00", "wins": 9, "draws": 11, "losses": 9}, {"key": "XLM/BTC", "trades": 21, "profit_mean": 0.0026243899999999994, "profit_mean_pct": 0.2624389999999999, "profit_sum": 0.05511218999999999, "profit_sum_pct": 5.511218999999999, "profit_total_abs": 0.005525000000000002, "profit_total_pct": 1.8370729999999995, "duration_avg": "3:21:00", "wins": 12, "draws": 3, "losses": 6}, {"key": "ETH/BTC", "trades": 21, "profit_mean": 0.0009500057142857142, "profit_mean_pct": 0.09500057142857142, "profit_sum": 0.01995012, "profit_sum_pct": 1.9950119999999998, "profit_total_abs": 0.0019999999999999463, "profit_total_pct": 0.6650039999999999, "duration_avg": "2:17:00", "wins": 5, "draws": 10, "losses": 6}, {"key": "XMR/BTC", "trades": 16, "profit_mean": -0.0027899012500000007, "profit_mean_pct": -0.2789901250000001, "profit_sum": -0.04463842000000001, "profit_sum_pct": -4.463842000000001, "profit_total_abs": -0.0044750000000000345, "profit_total_pct": -1.4879473333333337, "duration_avg": "8:41:00", "wins": 6, "draws": 5, "losses": 5}, {"key": "ZEC/BTC", "trades": 21, "profit_mean": -0.00039290904761904774, "profit_mean_pct": -0.03929090476190478, "profit_sum": -0.008251090000000003, "profit_sum_pct": -0.8251090000000003, "profit_total_abs": -0.000827170578309569, "profit_total_pct": -0.27503633333333344, "duration_avg": "4:17:00", "wins": 8, "draws": 7, "losses": 6}, {"key": "NXT/BTC", "trades": 12, "profit_mean": -0.0012261025000000006, "profit_mean_pct": -0.12261025000000006, "profit_sum": -0.014713230000000008, "profit_sum_pct": -1.4713230000000008, "profit_total_abs": -0.0014750000000000874, "profit_total_pct": -0.4904410000000003, "duration_avg": "0:57:00", "wins": 4, "draws": 3, "losses": 5}, {"key": "LTC/BTC", "trades": 8, "profit_mean": 0.00748129625, "profit_mean_pct": 0.748129625, "profit_sum": 0.05985037, "profit_sum_pct": 5.985037, "profit_total_abs": 0.006000000000000019, "profit_total_pct": 1.9950123333333334, "duration_avg": "1:59:00", "wins": 5, "draws": 2, "losses": 1}, {"key": "ETC/BTC", "trades": 20, "profit_mean": 0.0022568569999999997, "profit_mean_pct": 0.22568569999999996, "profit_sum": 0.04513713999999999, "profit_sum_pct": 4.513713999999999, "profit_total_abs": 0.004525000000000001, "profit_total_pct": 1.504571333333333, "duration_avg": "1:45:00", "wins": 11, "draws": 4, "losses": 5}, {"key": "DASH/BTC", "trades": 16, "profit_mean": 0.0018703237499999997, "profit_mean_pct": 0.18703237499999997, "profit_sum": 0.029925179999999996, "profit_sum_pct": 2.9925179999999996, "profit_total_abs": 0.002999999999999961, "profit_total_pct": 0.9975059999999999, "duration_avg": "3:03:00", "wins": 4, "draws": 7, "losses": 5}, {"key": "TOTAL", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}], "sell_reason_summary": [{"sell_reason": "roi", "trades": 170, "wins": 73, "draws": 54, "losses": 43, "profit_mean": 0.005398268352941177, "profit_mean_pct": 0.54, "profit_sum": 0.91770562, "profit_sum_pct": 91.77, "profit_total_abs": 0.09199999999999964, "profit_pct_total": 30.59}, {"sell_reason": "stop_loss", "trades": 6, "wins": 0, "draws": 0, "losses": 6, "profit_mean": -0.10448878000000002, "profit_mean_pct": -10.45, "profit_sum": -0.6269326800000001, "profit_sum_pct": -62.69, "profit_total_abs": -0.06284999999999992, "profit_pct_total": -20.9}, {"sell_reason": "force_sell", "trades": 3, "wins": 0, "draws": 0, "losses": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.89, "profit_sum": -0.14683468, "profit_sum_pct": -14.68, "profit_total_abs": -0.014720177176734003, "profit_pct_total": -4.89}], "left_open_trades": [{"key": "TRX/BTC", "trades": 1, "profit_mean": -0.0199116, "profit_mean_pct": -1.9911600000000003, "profit_sum": -0.0199116, "profit_sum_pct": -1.9911600000000003, "profit_total_abs": -0.0019961383478844796, "profit_total_pct": -0.6637200000000001, "duration_avg": "5:15:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ADA/BTC", "trades": 1, "profit_mean": -0.07877175, "profit_mean_pct": -7.877175, "profit_sum": -0.07877175, "profit_sum_pct": -7.877175, "profit_total_abs": -0.007896868250539965, "profit_total_pct": -2.625725, "duration_avg": "3 days, 4:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ZEC/BTC", "trades": 1, "profit_mean": -0.04815133, "profit_mean_pct": -4.815133, "profit_sum": -0.04815133, "profit_sum_pct": -4.815133, "profit_total_abs": -0.004827170578309559, "profit_total_pct": -1.6050443333333335, "duration_avg": "2 days, 19:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "TOTAL", "trades": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.894489333333333, "profit_sum": -0.14683468, "profit_sum_pct": -14.683468, "profit_total_abs": -0.014720177176734003, "profit_total_pct": -4.8944893333333335, "duration_avg": "2 days, 1:25:00", "wins": 0, "draws": 0, "losses": 3}], "total_trades": 179, "backtest_start": "2018-01-30 04:45:00+00:00", "backtest_start_ts": 1517287500, "backtest_end": "2018-01-30 04:45:00+00:00", "backtest_end_ts": 1517287500, "backtest_days": 0, "trades_per_day": null, "market_change": 0.25, "stake_amount": 0.1, "max_drawdown": 0.21142322000000008, "drawdown_start": "2018-01-24 14:25:00+00:00", "drawdown_start_ts": 1516803900.0, "drawdown_end": "2018-01-30 04:45:00+00:00", "drawdown_end_ts": 1517287500.0}}, "strategy_comparison": [{"key": "DefaultStrategy", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}, {"key": "TestStrategy", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}]} \ No newline at end of file From 573502d97226bdaea5ebb1d8eb92cdb078ad748d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Jun 2020 09:59:23 +0200 Subject: [PATCH 0219/1197] Update test for load_trades_from_db --- tests/conftest.py | 9 ++++++--- tests/data/test_btanalysis.py | 9 +++++++-- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index f2143e60e..62810bd6e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -180,7 +180,8 @@ def create_mock_trades(fee): fee_close=fee.return_value, open_rate=0.123, exchange='bittrex', - open_order_id='dry_run_buy_12345' + open_order_id='dry_run_buy_12345', + strategy='DefaultStrategy', ) Trade.session.add(trade) @@ -195,7 +196,8 @@ def create_mock_trades(fee): close_profit=0.005, exchange='bittrex', is_open=False, - open_order_id='dry_run_sell_12345' + open_order_id='dry_run_sell_12345', + strategy='DefaultStrategy', ) Trade.session.add(trade) @@ -208,7 +210,8 @@ def create_mock_trades(fee): fee_close=fee.return_value, open_rate=0.123, exchange='bittrex', - open_order_id='prod_buy_12345' + open_order_id='prod_buy_12345', + strategy='DefaultStrategy', ) Trade.session.add(trade) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index fd3783bf2..32e476b6b 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -43,7 +43,7 @@ def test_load_backtest_data(testdatadir): filename = testdatadir / "backtest-result_test.json" bt_data = load_backtest_data(filename) assert isinstance(bt_data, DataFrame) - assert list(bt_data.columns) == BT_DATA_COLUMNS + ["profit"] + assert list(bt_data.columns) == BT_DATA_COLUMNS + ["profit_abs"] assert len(bt_data) == 179 # Test loading from string (must yield same result) @@ -72,6 +72,10 @@ def test_load_trades_from_db(default_conf, fee, mocker): for col in BT_DATA_COLUMNS: if col not in ['index', 'open_at_end']: assert col in trades.columns + trades = load_trades_from_db(db_url=default_conf['db_url'], strategy='DefaultStrategy') + assert len(trades) == 3 + trades = load_trades_from_db(db_url=default_conf['db_url'], strategy='NoneStrategy') + assert len(trades) == 0 def test_extract_trades_of_period(testdatadir): @@ -125,7 +129,8 @@ def test_load_trades(default_conf, mocker): load_trades("DB", db_url=default_conf.get('db_url'), exportfilename=default_conf.get('exportfilename'), - no_trades=False + no_trades=False, + strategy="DefaultStrategy", ) assert db_mock.call_count == 1 From f952f74bf18c71de95eb5db9e92218f3e166c567 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Jun 2020 10:06:59 +0200 Subject: [PATCH 0220/1197] Add test for new format --- tests/data/test_btanalysis.py | 19 ++++++++++++++++++- tests/testdata/backtest-result_new.json | 2 +- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 32e476b6b..9ae67ed58 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -17,6 +17,7 @@ from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, load_backtest_data, load_trades, load_trades_from_db) from freqtrade.data.history import load_data, load_pair_history +from freqtrade.optimize.backtesting import BacktestResult from tests.conftest import create_mock_trades @@ -38,7 +39,7 @@ def test_get_latest_backtest_filename(testdatadir, mocker): get_latest_backtest_filename(testdatadir) -def test_load_backtest_data(testdatadir): +def test_load_backtest_data_old_format(testdatadir): filename = testdatadir / "backtest-result_test.json" bt_data = load_backtest_data(filename) @@ -54,6 +55,22 @@ def test_load_backtest_data(testdatadir): load_backtest_data(str("filename") + "nofile") +def test_load_backtest_data_new_format(testdatadir): + + filename = testdatadir / "backtest-result_new.json" + bt_data = load_backtest_data(filename) + assert isinstance(bt_data, DataFrame) + assert set(bt_data.columns) == set(list(BacktestResult._fields) + ["profit_abs"]) + assert len(bt_data) == 179 + + # Test loading from string (must yield same result) + bt_data2 = load_backtest_data(str(filename)) + assert bt_data.equals(bt_data2) + + with pytest.raises(ValueError, match=r"File .* does not exist\."): + load_backtest_data(str("filename") + "nofile") + + @pytest.mark.usefixtures("init_persistence") def test_load_trades_from_db(default_conf, fee, mocker): diff --git a/tests/testdata/backtest-result_new.json b/tests/testdata/backtest-result_new.json index 457ad1bc7..a44597e82 100644 --- a/tests/testdata/backtest-result_new.json +++ b/tests/testdata/backtest-result_new.json @@ -1 +1 @@ -{"strategy": {"DefaultStrategy": {"trades": [{"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:20:00+00:00", "trade_duration": 5, "open_rate": 9.64e-05, "close_rate": 0.00010074887218045112, "open_at_end": false, "sell_reason": "roi", "profit": 4.348872180451118e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1037.344398340249, "profit_abs": 0.00399999999999999}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:30:00+00:00", "trade_duration": 15, "open_rate": 4.756e-05, "close_rate": 4.9705563909774425e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.1455639097744267e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2102.6072329688814, "profit_abs": 0.00399999999999999}, {"pair": "XLM/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:35:00+00:00", "trade_duration": 10, "open_rate": 3.339e-05, "close_rate": 3.489631578947368e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.506315789473681e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2994.908655286014, "profit_abs": 0.0040000000000000036}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:40:00+00:00", "trade_duration": 15, "open_rate": 9.696e-05, "close_rate": 0.00010133413533834584, "open_at_end": false, "sell_reason": "roi", "profit": 4.3741353383458455e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1031.3531353135315, "profit_abs": 0.00399999999999999}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 07:35:00+00:00", "close_date": "2018-01-10 08:35:00+00:00", "trade_duration": 60, "open_rate": 0.0943, "close_rate": 0.09477268170426063, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004726817042606385, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0604453870625663, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 07:40:00+00:00", "close_date": "2018-01-10 08:10:00+00:00", "trade_duration": 30, "open_rate": 0.02719607, "close_rate": 0.02760503345864661, "open_at_end": false, "sell_reason": "roi", "profit": 0.00040896345864661204, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.677001860930642, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 08:15:00+00:00", "close_date": "2018-01-10 09:55:00+00:00", "trade_duration": 100, "open_rate": 0.04634952, "close_rate": 0.046581848421052625, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002323284210526272, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1575196463739, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 14:45:00+00:00", "close_date": "2018-01-10 15:50:00+00:00", "trade_duration": 65, "open_rate": 3.066e-05, "close_rate": 3.081368421052631e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5368421052630647e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3261.5786040443577, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 16:35:00+00:00", "close_date": "2018-01-10 17:15:00+00:00", "trade_duration": 40, "open_rate": 0.0168999, "close_rate": 0.016984611278195488, "open_at_end": false, "sell_reason": "roi", "profit": 8.471127819548868e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.917194776300452, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 16:40:00+00:00", "close_date": "2018-01-10 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.09132568, "close_rate": 0.0917834528320802, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004577728320801916, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0949822656672252, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 18:50:00+00:00", "close_date": "2018-01-10 19:45:00+00:00", "trade_duration": 55, "open_rate": 0.08898003, "close_rate": 0.08942604518796991, "open_at_end": false, "sell_reason": "roi", "profit": 0.00044601518796991146, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1238476768326557, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 22:15:00+00:00", "close_date": "2018-01-10 23:00:00+00:00", "trade_duration": 45, "open_rate": 0.08560008, "close_rate": 0.08602915308270676, "open_at_end": false, "sell_reason": "roi", "profit": 0.00042907308270676014, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1682232072680307, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 22:50:00+00:00", "close_date": "2018-01-10 23:20:00+00:00", "trade_duration": 30, "open_rate": 0.00249083, "close_rate": 0.0025282860902255634, "open_at_end": false, "sell_reason": "roi", "profit": 3.745609022556351e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 40.147260150231055, "profit_abs": 0.000999999999999987}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 23:15:00+00:00", "close_date": "2018-01-11 00:15:00+00:00", "trade_duration": 60, "open_rate": 3.022e-05, "close_rate": 3.037147869674185e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5147869674185174e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3309.0668431502318, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-10 23:40:00+00:00", "close_date": "2018-01-11 00:05:00+00:00", "trade_duration": 25, "open_rate": 0.002437, "close_rate": 0.0024980776942355883, "open_at_end": false, "sell_reason": "roi", "profit": 6.107769423558838e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.03405826836274, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 00:00:00+00:00", "close_date": "2018-01-11 00:35:00+00:00", "trade_duration": 35, "open_rate": 0.04771803, "close_rate": 0.04843559436090225, "open_at_end": false, "sell_reason": "roi", "profit": 0.0007175643609022495, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0956439316543456, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-11 03:40:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 45, "open_rate": 3.651e-05, "close_rate": 3.2859000000000005e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -3.650999999999996e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2738.9756231169545, "profit_abs": -0.01047499999999997}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 03:55:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 30, "open_rate": 0.08824105, "close_rate": 0.08956798308270676, "open_at_end": false, "sell_reason": "roi", "profit": 0.0013269330827067605, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1332594070446804, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 04:00:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 50, "open_rate": 0.00243, "close_rate": 0.002442180451127819, "open_at_end": false, "sell_reason": "roi", "profit": 1.2180451127819219e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.1522633744856, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:55:00+00:00", "trade_duration": 25, "open_rate": 0.04545064, "close_rate": 0.046589753784461146, "open_at_end": false, "sell_reason": "roi", "profit": 0.001139113784461146, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.200189040242338, "profit_abs": 0.001999999999999988}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 20, "open_rate": 3.372e-05, "close_rate": 3.456511278195488e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.4511278195488e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2965.599051008304, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:55:00+00:00", "close_date": "2018-01-11 05:15:00+00:00", "trade_duration": 20, "open_rate": 0.02644, "close_rate": 0.02710265664160401, "open_at_end": false, "sell_reason": "roi", "profit": 0.0006626566416040071, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7821482602118004, "profit_abs": 0.001999999999999988}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:20:00+00:00", "close_date": "2018-01-11 12:00:00+00:00", "trade_duration": 40, "open_rate": 0.08812, "close_rate": 0.08856170426065162, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004417042606516125, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1348161597821154, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:35:00+00:00", "close_date": "2018-01-11 12:15:00+00:00", "trade_duration": 40, "open_rate": 0.02683577, "close_rate": 0.026970285137844607, "open_at_end": false, "sell_reason": "roi", "profit": 0.00013451513784460897, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7263696923919087, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 14:00:00+00:00", "close_date": "2018-01-11 14:25:00+00:00", "trade_duration": 25, "open_rate": 4.919e-05, "close_rate": 5.04228320802005e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.232832080200495e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.9335230737956, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 19:25:00+00:00", "close_date": "2018-01-11 20:35:00+00:00", "trade_duration": 70, "open_rate": 0.08784896, "close_rate": 0.08828930566416039, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004403456641603881, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1383174029607181, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:35:00+00:00", "close_date": "2018-01-11 23:30:00+00:00", "trade_duration": 55, "open_rate": 5.105e-05, "close_rate": 5.130588972431077e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.558897243107704e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1958.8638589618022, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:25:00+00:00", "trade_duration": 30, "open_rate": 3.96e-05, "close_rate": 4.019548872180451e-05, "open_at_end": false, "sell_reason": "roi", "profit": 5.954887218045116e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2525.252525252525, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:35:00+00:00", "trade_duration": 40, "open_rate": 2.885e-05, "close_rate": 2.899461152882205e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.4461152882205115e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3466.204506065858, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 23:30:00+00:00", "close_date": "2018-01-12 00:05:00+00:00", "trade_duration": 35, "open_rate": 0.02645, "close_rate": 0.026847744360902256, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003977443609022545, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.780718336483932, "profit_abs": 0.0010000000000000148}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 23:55:00+00:00", "close_date": "2018-01-12 01:15:00+00:00", "trade_duration": 80, "open_rate": 0.048, "close_rate": 0.04824060150375939, "open_at_end": false, "sell_reason": "roi", "profit": 0.00024060150375938838, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0833333333333335, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-12 21:15:00+00:00", "close_date": "2018-01-12 21:40:00+00:00", "trade_duration": 25, "open_rate": 4.692e-05, "close_rate": 4.809593984962405e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.1759398496240516e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2131.287297527707, "profit_abs": 0.001999999999999974}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 00:55:00+00:00", "close_date": "2018-01-13 06:20:00+00:00", "trade_duration": 325, "open_rate": 0.00256966, "close_rate": 0.0025825405012531327, "open_at_end": false, "sell_reason": "roi", "profit": 1.2880501253132587e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.91565421106294, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 10:55:00+00:00", "close_date": "2018-01-13 11:35:00+00:00", "trade_duration": 40, "open_rate": 6.262e-05, "close_rate": 6.293388471177944e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.138847117794446e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1596.933886937081, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 13:05:00+00:00", "close_date": "2018-01-15 14:10:00+00:00", "trade_duration": 2945, "open_rate": 4.73e-05, "close_rate": 4.753709273182957e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.3709273182957117e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2114.1649048625795, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:30:00+00:00", "close_date": "2018-01-13 14:45:00+00:00", "trade_duration": 75, "open_rate": 6.063e-05, "close_rate": 6.0933909774436085e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.039097744360846e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1649.348507339601, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:40:00+00:00", "close_date": "2018-01-13 23:30:00+00:00", "trade_duration": 590, "open_rate": 0.00011082, "close_rate": 0.00011137548872180448, "open_at_end": false, "sell_reason": "roi", "profit": 5.554887218044781e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 902.3641941887746, "profit_abs": -2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 15:15:00+00:00", "close_date": "2018-01-13 15:55:00+00:00", "trade_duration": 40, "open_rate": 5.93e-05, "close_rate": 5.9597243107769415e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.9724310776941686e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1686.3406408094436, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 16:30:00+00:00", "close_date": "2018-01-13 17:10:00+00:00", "trade_duration": 40, "open_rate": 0.04850003, "close_rate": 0.04874313791979949, "open_at_end": false, "sell_reason": "roi", "profit": 0.00024310791979949287, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0618543947292407, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 22:05:00+00:00", "close_date": "2018-01-14 06:25:00+00:00", "trade_duration": 500, "open_rate": 0.09825019, "close_rate": 0.09874267215538848, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004924821553884823, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0178097365511456, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 00:20:00+00:00", "close_date": "2018-01-14 22:55:00+00:00", "trade_duration": 1355, "open_rate": 6.018e-05, "close_rate": 6.048165413533834e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.0165413533833987e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1661.681621801263, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 12:45:00+00:00", "close_date": "2018-01-14 13:25:00+00:00", "trade_duration": 40, "open_rate": 0.09758999, "close_rate": 0.0980791628822055, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004891728822054991, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.024695258191952, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-14 15:30:00+00:00", "close_date": "2018-01-14 16:00:00+00:00", "trade_duration": 30, "open_rate": 0.00311, "close_rate": 0.0031567669172932328, "open_at_end": false, "sell_reason": "roi", "profit": 4.676691729323286e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.154340836012864, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 20:45:00+00:00", "close_date": "2018-01-14 22:15:00+00:00", "trade_duration": 90, "open_rate": 0.00312401, "close_rate": 0.003139669197994987, "open_at_end": false, "sell_reason": "roi", "profit": 1.5659197994987058e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.010140812609436, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 23:35:00+00:00", "close_date": "2018-01-15 00:30:00+00:00", "trade_duration": 55, "open_rate": 0.0174679, "close_rate": 0.017555458395989976, "open_at_end": false, "sell_reason": "roi", "profit": 8.755839598997492e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.724786608579165, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 23:45:00+00:00", "close_date": "2018-01-15 00:25:00+00:00", "trade_duration": 40, "open_rate": 0.07346846, "close_rate": 0.07383672295739348, "open_at_end": false, "sell_reason": "roi", "profit": 0.00036826295739347814, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.3611282991367997, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 02:25:00+00:00", "close_date": "2018-01-15 03:05:00+00:00", "trade_duration": 40, "open_rate": 0.097994, "close_rate": 0.09848519799498744, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004911979949874384, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.020470641059657, "profit_abs": -2.7755575615628914e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 07:20:00+00:00", "close_date": "2018-01-15 08:00:00+00:00", "trade_duration": 40, "open_rate": 0.09659, "close_rate": 0.09707416040100247, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004841604010024786, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0353038616834043, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-15 08:20:00+00:00", "close_date": "2018-01-15 08:55:00+00:00", "trade_duration": 35, "open_rate": 9.987e-05, "close_rate": 0.00010137180451127818, "open_at_end": false, "sell_reason": "roi", "profit": 1.501804511278178e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1001.3016921998599, "profit_abs": 0.0010000000000000009}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-15 12:10:00+00:00", "close_date": "2018-01-16 02:50:00+00:00", "trade_duration": 880, "open_rate": 0.0948969, "close_rate": 0.09537257368421052, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004756736842105175, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0537752023511833, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:10:00+00:00", "close_date": "2018-01-15 17:40:00+00:00", "trade_duration": 210, "open_rate": 0.071, "close_rate": 0.07135588972431077, "open_at_end": false, "sell_reason": "roi", "profit": 0.00035588972431077615, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4084507042253522, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:30:00+00:00", "close_date": "2018-01-15 15:10:00+00:00", "trade_duration": 40, "open_rate": 0.04600501, "close_rate": 0.046235611553884705, "open_at_end": false, "sell_reason": "roi", "profit": 0.00023060155388470588, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.173676301776698, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:10:00+00:00", "close_date": "2018-01-15 19:25:00+00:00", "trade_duration": 75, "open_rate": 9.438e-05, "close_rate": 9.485308270676693e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.7308270676692514e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1059.5465140919687, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:35:00+00:00", "close_date": "2018-01-15 19:15:00+00:00", "trade_duration": 40, "open_rate": 0.03040001, "close_rate": 0.030552391002506264, "open_at_end": false, "sell_reason": "roi", "profit": 0.0001523810025062626, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.2894726021471703, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-15 20:25:00+00:00", "close_date": "2018-01-16 08:25:00+00:00", "trade_duration": 720, "open_rate": 5.837e-05, "close_rate": 5.2533e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -5.8369999999999985e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1713.2088401576154, "profit_abs": -0.010474999999999984}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 20:40:00+00:00", "close_date": "2018-01-15 22:00:00+00:00", "trade_duration": 80, "open_rate": 0.046036, "close_rate": 0.04626675689223057, "open_at_end": false, "sell_reason": "roi", "profit": 0.00023075689223057277, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1722130506560084, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 00:30:00+00:00", "close_date": "2018-01-16 01:10:00+00:00", "trade_duration": 40, "open_rate": 0.0028685, "close_rate": 0.0028828784461152877, "open_at_end": false, "sell_reason": "roi", "profit": 1.4378446115287727e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 34.86142583231654, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 01:15:00+00:00", "close_date": "2018-01-16 02:35:00+00:00", "trade_duration": 80, "open_rate": 0.06731755, "close_rate": 0.0676549813283208, "open_at_end": false, "sell_reason": "roi", "profit": 0.00033743132832080025, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4854967241083492, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 07:45:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 55, "open_rate": 0.09217614, "close_rate": 0.09263817578947368, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004620357894736804, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0848794492804754, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:55:00+00:00", "trade_duration": 20, "open_rate": 0.0165, "close_rate": 0.016913533834586467, "open_at_end": false, "sell_reason": "roi", "profit": 0.00041353383458646656, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.0606060606060606, "profit_abs": 0.0020000000000000018}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 5, "open_rate": 7.953e-05, "close_rate": 8.311781954887218e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.587819548872171e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1257.387149503332, "profit_abs": 0.00399999999999999}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 08:45:00+00:00", "close_date": "2018-01-16 09:50:00+00:00", "trade_duration": 65, "open_rate": 0.045202, "close_rate": 0.04542857644110275, "open_at_end": false, "sell_reason": "roi", "profit": 0.00022657644110275071, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2122914915269236, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:45:00+00:00", "trade_duration": 30, "open_rate": 5.248e-05, "close_rate": 5.326917293233082e-05, "open_at_end": false, "sell_reason": "roi", "profit": 7.891729323308177e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1905.487804878049, "profit_abs": 0.0010000000000000009}, {"pair": "XMR/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:55:00+00:00", "trade_duration": 40, "open_rate": 0.02892318, "close_rate": 0.02906815834586466, "open_at_end": false, "sell_reason": "roi", "profit": 0.0001449783458646603, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.457434486802627, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 09:50:00+00:00", "close_date": "2018-01-16 10:10:00+00:00", "trade_duration": 20, "open_rate": 5.158e-05, "close_rate": 5.287273182957392e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.2927318295739246e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1938.735944164405, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:35:00+00:00", "trade_duration": 30, "open_rate": 0.02828232, "close_rate": 0.02870761804511278, "open_at_end": false, "sell_reason": "roi", "profit": 0.00042529804511277913, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5357778286929786, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:40:00+00:00", "trade_duration": 35, "open_rate": 0.04357584, "close_rate": 0.044231115789473675, "open_at_end": false, "sell_reason": "roi", "profit": 0.0006552757894736777, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.294849623093898, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 13:45:00+00:00", "close_date": "2018-01-16 14:20:00+00:00", "trade_duration": 35, "open_rate": 5.362e-05, "close_rate": 5.442631578947368e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.063157894736843e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1864.975755315181, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 17:30:00+00:00", "close_date": "2018-01-16 18:25:00+00:00", "trade_duration": 55, "open_rate": 5.302e-05, "close_rate": 5.328576441102756e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.6576441102756397e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.0807242549984, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:45:00+00:00", "trade_duration": 30, "open_rate": 0.09129999, "close_rate": 0.09267292218045112, "open_at_end": false, "sell_reason": "roi", "profit": 0.0013729321804511196, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0952903718828448, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:35:00+00:00", "trade_duration": 20, "open_rate": 3.808e-05, "close_rate": 3.903438596491228e-05, "open_at_end": false, "sell_reason": "roi", "profit": 9.543859649122774e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2626.0504201680674, "profit_abs": 0.0020000000000000018}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 19:00:00+00:00", "close_date": "2018-01-16 19:30:00+00:00", "trade_duration": 30, "open_rate": 0.02811012, "close_rate": 0.028532828571428567, "open_at_end": false, "sell_reason": "roi", "profit": 0.00042270857142856846, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.557437677249333, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 60, "open_rate": 0.00258379, "close_rate": 0.002325411, "open_at_end": false, "sell_reason": "stop_loss", "profit": -0.000258379, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.702835756775904, "profit_abs": -0.010474999999999984}, {"pair": "NXT/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 80, "open_rate": 2.559e-05, "close_rate": 2.3031e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -2.5590000000000004e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3907.7764751856193, "profit_abs": -0.010474999999999998}, {"pair": "TRX/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:35:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 50, "open_rate": 7.62e-05, "close_rate": 6.858e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -7.619999999999998e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1312.3359580052495, "profit_abs": -0.010474999999999984}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:35:00+00:00", "trade_duration": 5, "open_rate": 0.00229844, "close_rate": 0.002402129022556391, "open_at_end": false, "sell_reason": "roi", "profit": 0.00010368902255639091, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 43.507770487809125, "profit_abs": 0.004000000000000017}, {"pair": "LTC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:40:00+00:00", "trade_duration": 10, "open_rate": 0.0151, "close_rate": 0.015781203007518795, "open_at_end": false, "sell_reason": "roi", "profit": 0.0006812030075187946, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.622516556291391, "profit_abs": 0.00399999999999999}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:40:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 5, "open_rate": 0.00235676, "close_rate": 0.00246308, "open_at_end": false, "sell_reason": "roi", "profit": 0.00010632000000000003, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 42.431134269081284, "profit_abs": 0.0040000000000000036}, {"pair": "DASH/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 22:45:00+00:00", "close_date": "2018-01-16 23:05:00+00:00", "trade_duration": 20, "open_rate": 0.0630692, "close_rate": 0.06464988170426066, "open_at_end": false, "sell_reason": "roi", "profit": 0.0015806817042606502, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.585559988076589, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:50:00+00:00", "close_date": "2018-01-16 22:55:00+00:00", "trade_duration": 5, "open_rate": 2.2e-05, "close_rate": 2.299248120300751e-05, "open_at_end": false, "sell_reason": "roi", "profit": 9.924812030075114e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 4545.454545454546, "profit_abs": 0.003999999999999976}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-17 03:30:00+00:00", "close_date": "2018-01-17 04:00:00+00:00", "trade_duration": 30, "open_rate": 4.974e-05, "close_rate": 5.048796992481203e-05, "open_at_end": false, "sell_reason": "roi", "profit": 7.479699248120277e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2010.454362685967, "profit_abs": 0.0010000000000000009}, {"pair": "TRX/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-17 03:55:00+00:00", "close_date": "2018-01-17 04:15:00+00:00", "trade_duration": 20, "open_rate": 7.108e-05, "close_rate": 7.28614536340852e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.7814536340851996e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1406.8655036578502, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 09:35:00+00:00", "close_date": "2018-01-17 10:15:00+00:00", "trade_duration": 40, "open_rate": 0.04327, "close_rate": 0.04348689223057644, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002168922305764362, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.3110700254217704, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:20:00+00:00", "close_date": "2018-01-17 17:00:00+00:00", "trade_duration": 400, "open_rate": 4.997e-05, "close_rate": 5.022047619047618e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.504761904761831e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2001.2007204322595, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:25:00+00:00", "trade_duration": 55, "open_rate": 0.06836818, "close_rate": 0.06871087764411027, "open_at_end": false, "sell_reason": "roi", "profit": 0.00034269764411026804, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4626687444363737, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:10:00+00:00", "trade_duration": 40, "open_rate": 3.63e-05, "close_rate": 3.648195488721804e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.8195488721804031e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2754.8209366391184, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:30:00+00:00", "close_date": "2018-01-17 22:05:00+00:00", "trade_duration": 575, "open_rate": 0.0281, "close_rate": 0.02824085213032581, "open_at_end": false, "sell_reason": "roi", "profit": 0.0001408521303258095, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5587188612099645, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:35:00+00:00", "close_date": "2018-01-17 16:55:00+00:00", "trade_duration": 260, "open_rate": 0.08651001, "close_rate": 0.08694364413533832, "open_at_end": false, "sell_reason": "roi", "profit": 0.00043363413533832607, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1559355963546878, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 05:00:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 55, "open_rate": 5.633e-05, "close_rate": 5.6612355889724306e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.8235588972430847e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1775.2529735487308, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 05:20:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 35, "open_rate": 0.06988494, "close_rate": 0.07093584135338346, "open_at_end": false, "sell_reason": "roi", "profit": 0.0010509013533834544, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.430923457900944, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 07:35:00+00:00", "close_date": "2018-01-18 08:15:00+00:00", "trade_duration": 40, "open_rate": 5.545e-05, "close_rate": 5.572794486215538e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.779448621553787e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1803.4265103697026, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 09:00:00+00:00", "close_date": "2018-01-18 09:40:00+00:00", "trade_duration": 40, "open_rate": 0.01633527, "close_rate": 0.016417151052631574, "open_at_end": false, "sell_reason": "roi", "profit": 8.188105263157511e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.121723118136401, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 16:40:00+00:00", "close_date": "2018-01-18 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.00269734, "close_rate": 0.002710860501253133, "open_at_end": false, "sell_reason": "roi", "profit": 1.3520501253133123e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.073561360451414, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-18 18:05:00+00:00", "close_date": "2018-01-18 18:30:00+00:00", "trade_duration": 25, "open_rate": 4.475e-05, "close_rate": 4.587155388471177e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.1215538847117757e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2234.63687150838, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 18:25:00+00:00", "close_date": "2018-01-18 18:55:00+00:00", "trade_duration": 30, "open_rate": 2.79e-05, "close_rate": 2.8319548872180444e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.1954887218044365e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3584.2293906810037, "profit_abs": 0.000999999999999987}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 20:10:00+00:00", "close_date": "2018-01-18 20:50:00+00:00", "trade_duration": 40, "open_rate": 0.04439326, "close_rate": 0.04461578260651629, "open_at_end": false, "sell_reason": "roi", "profit": 0.00022252260651629135, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2525942001105577, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 21:30:00+00:00", "close_date": "2018-01-19 00:35:00+00:00", "trade_duration": 185, "open_rate": 4.49e-05, "close_rate": 4.51250626566416e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.2506265664159932e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2227.1714922049, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 21:55:00+00:00", "close_date": "2018-01-19 05:05:00+00:00", "trade_duration": 430, "open_rate": 0.02855, "close_rate": 0.028693107769423555, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014310776942355607, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.502626970227671, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 22:10:00+00:00", "close_date": "2018-01-18 22:50:00+00:00", "trade_duration": 40, "open_rate": 5.796e-05, "close_rate": 5.8250526315789473e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.905263157894727e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1725.3278122843342, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 23:50:00+00:00", "close_date": "2018-01-19 00:30:00+00:00", "trade_duration": 40, "open_rate": 0.04340323, "close_rate": 0.04362079005012531, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002175600501253122, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.303975994413319, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-19 16:45:00+00:00", "close_date": "2018-01-19 17:35:00+00:00", "trade_duration": 50, "open_rate": 0.04454455, "close_rate": 0.04476783095238095, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002232809523809512, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.244943545282195, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:15:00+00:00", "close_date": "2018-01-19 19:55:00+00:00", "trade_duration": 160, "open_rate": 5.62e-05, "close_rate": 5.648170426065162e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.817042606516199e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1779.3594306049824, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:20:00+00:00", "close_date": "2018-01-19 20:15:00+00:00", "trade_duration": 175, "open_rate": 4.339e-05, "close_rate": 4.360749373433584e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.174937343358337e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2304.6784973496196, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-20 04:45:00+00:00", "close_date": "2018-01-20 17:35:00+00:00", "trade_duration": 770, "open_rate": 0.0001009, "close_rate": 0.00010140576441102755, "open_at_end": false, "sell_reason": "roi", "profit": 5.057644110275549e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 991.0802775024778, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 15:15:00+00:00", "trade_duration": 625, "open_rate": 0.00270505, "close_rate": 0.002718609147869674, "open_at_end": false, "sell_reason": "roi", "profit": 1.3559147869673764e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.96789338459548, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 07:00:00+00:00", "trade_duration": 130, "open_rate": 0.03000002, "close_rate": 0.030150396040100245, "open_at_end": false, "sell_reason": "roi", "profit": 0.00015037604010024672, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.3333311111125927, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 09:00:00+00:00", "close_date": "2018-01-20 09:40:00+00:00", "trade_duration": 40, "open_rate": 5.46e-05, "close_rate": 5.4873684210526304e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.736842105263053e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1831.5018315018317, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-20 18:25:00+00:00", "close_date": "2018-01-25 03:50:00+00:00", "trade_duration": 6325, "open_rate": 0.03082222, "close_rate": 0.027739998, "open_at_end": false, "sell_reason": "stop_loss", "profit": -0.0030822220000000025, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.244412634781012, "profit_abs": -0.010474999999999998}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 22:25:00+00:00", "close_date": "2018-01-20 23:15:00+00:00", "trade_duration": 50, "open_rate": 0.08969999, "close_rate": 0.09014961401002504, "open_at_end": false, "sell_reason": "roi", "profit": 0.00044962401002504593, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1148273260677064, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 02:50:00+00:00", "close_date": "2018-01-21 14:30:00+00:00", "trade_duration": 700, "open_rate": 0.01632501, "close_rate": 0.01640683962406015, "open_at_end": false, "sell_reason": "roi", "profit": 8.182962406014932e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.125570520324337, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 10:20:00+00:00", "close_date": "2018-01-21 11:00:00+00:00", "trade_duration": 40, "open_rate": 0.070538, "close_rate": 0.07089157393483708, "open_at_end": false, "sell_reason": "roi", "profit": 0.00035357393483707866, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.417675579120474, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 15:50:00+00:00", "close_date": "2018-01-21 18:45:00+00:00", "trade_duration": 175, "open_rate": 5.301e-05, "close_rate": 5.327571428571427e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.657142857142672e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.4365214110546, "profit_abs": -2.7755575615628914e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 16:20:00+00:00", "close_date": "2018-01-21 17:00:00+00:00", "trade_duration": 40, "open_rate": 3.955e-05, "close_rate": 3.9748245614035085e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.9824561403508552e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2528.4450063211125, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:45:00+00:00", "trade_duration": 30, "open_rate": 0.00258505, "close_rate": 0.002623922932330827, "open_at_end": false, "sell_reason": "roi", "profit": 3.8872932330826816e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.6839712964933, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:55:00+00:00", "trade_duration": 40, "open_rate": 3.903e-05, "close_rate": 3.922563909774435e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.9563909774435151e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2562.1316935690497, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 00:35:00+00:00", "close_date": "2018-01-22 10:35:00+00:00", "trade_duration": 600, "open_rate": 5.236e-05, "close_rate": 5.262245614035087e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.624561403508717e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1909.8548510313217, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 01:30:00+00:00", "close_date": "2018-01-22 02:10:00+00:00", "trade_duration": 40, "open_rate": 9.028e-05, "close_rate": 9.07325313283208e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.5253132832080657e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1107.6650420912717, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 12:25:00+00:00", "close_date": "2018-01-22 14:35:00+00:00", "trade_duration": 130, "open_rate": 0.002687, "close_rate": 0.002700468671679198, "open_at_end": false, "sell_reason": "roi", "profit": 1.3468671679197925e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.21622627465575, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 13:15:00+00:00", "close_date": "2018-01-22 13:55:00+00:00", "trade_duration": 40, "open_rate": 4.168e-05, "close_rate": 4.188892230576441e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.0892230576441054e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2399.232245681382, "profit_abs": 1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-22 14:00:00+00:00", "close_date": "2018-01-22 14:30:00+00:00", "trade_duration": 30, "open_rate": 8.821e-05, "close_rate": 8.953646616541353e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.326466165413529e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1133.6583153837435, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 15:55:00+00:00", "close_date": "2018-01-22 16:40:00+00:00", "trade_duration": 45, "open_rate": 5.172e-05, "close_rate": 5.1979248120300745e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.592481203007459e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1933.4880123743235, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-22 16:05:00+00:00", "close_date": "2018-01-22 16:25:00+00:00", "trade_duration": 20, "open_rate": 3.026e-05, "close_rate": 3.101839598997494e-05, "open_at_end": false, "sell_reason": "roi", "profit": 7.5839598997494e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3304.692663582287, "profit_abs": 0.0020000000000000157}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 19:50:00+00:00", "close_date": "2018-01-23 00:10:00+00:00", "trade_duration": 260, "open_rate": 0.07064, "close_rate": 0.07099408521303258, "open_at_end": false, "sell_reason": "roi", "profit": 0.00035408521303258167, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.415628539071348, "profit_abs": 1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 21:25:00+00:00", "close_date": "2018-01-22 22:05:00+00:00", "trade_duration": 40, "open_rate": 0.01644483, "close_rate": 0.01652726022556391, "open_at_end": false, "sell_reason": "roi", "profit": 8.243022556390922e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.080938507725528, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-23 00:05:00+00:00", "close_date": "2018-01-23 00:35:00+00:00", "trade_duration": 30, "open_rate": 4.331e-05, "close_rate": 4.3961278195488714e-05, "open_at_end": false, "sell_reason": "roi", "profit": 6.512781954887175e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2308.935580697299, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-23 01:50:00+00:00", "close_date": "2018-01-23 02:15:00+00:00", "trade_duration": 25, "open_rate": 3.2e-05, "close_rate": 3.2802005012531326e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.020050125313278e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3125.0000000000005, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 04:25:00+00:00", "close_date": "2018-01-23 05:15:00+00:00", "trade_duration": 50, "open_rate": 0.09167706, "close_rate": 0.09213659413533835, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004595341353383492, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0907854156754153, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 07:35:00+00:00", "close_date": "2018-01-23 09:00:00+00:00", "trade_duration": 85, "open_rate": 0.0692498, "close_rate": 0.06959691679197995, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003471167919799484, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4440474918339115, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 10:50:00+00:00", "close_date": "2018-01-23 13:05:00+00:00", "trade_duration": 135, "open_rate": 3.182e-05, "close_rate": 3.197949874686716e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.594987468671663e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3142.677561282213, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 11:05:00+00:00", "close_date": "2018-01-23 16:05:00+00:00", "trade_duration": 300, "open_rate": 0.04088, "close_rate": 0.04108491228070175, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002049122807017481, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4461839530332683, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 14:55:00+00:00", "close_date": "2018-01-23 15:35:00+00:00", "trade_duration": 40, "open_rate": 5.15e-05, "close_rate": 5.175814536340851e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.5814536340851513e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1941.747572815534, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 16:35:00+00:00", "close_date": "2018-01-24 00:05:00+00:00", "trade_duration": 450, "open_rate": 0.09071698, "close_rate": 0.09117170170426064, "open_at_end": false, "sell_reason": "roi", "profit": 0.00045472170426064107, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1023294646713329, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 17:25:00+00:00", "close_date": "2018-01-23 18:45:00+00:00", "trade_duration": 80, "open_rate": 3.128e-05, "close_rate": 3.1436791979949865e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5679197994986587e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3196.9309462915603, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 20:15:00+00:00", "close_date": "2018-01-23 22:00:00+00:00", "trade_duration": 105, "open_rate": 9.555e-05, "close_rate": 9.602894736842104e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.789473684210343e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1046.5724751439038, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 22:30:00+00:00", "close_date": "2018-01-23 23:10:00+00:00", "trade_duration": 40, "open_rate": 0.04080001, "close_rate": 0.0410045213283208, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020451132832080554, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.450979791426522, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 23:50:00+00:00", "close_date": "2018-01-24 03:35:00+00:00", "trade_duration": 225, "open_rate": 5.163e-05, "close_rate": 5.18887969924812e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.587969924812037e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1936.8584156498162, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 00:20:00+00:00", "close_date": "2018-01-24 01:50:00+00:00", "trade_duration": 90, "open_rate": 0.04040781, "close_rate": 0.04061035541353383, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002025454135338306, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.474769110228938, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 06:45:00+00:00", "close_date": "2018-01-24 07:25:00+00:00", "trade_duration": 40, "open_rate": 5.132e-05, "close_rate": 5.157724310776942e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.5724310776941724e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1948.5580670303975, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-24 14:15:00+00:00", "close_date": "2018-01-24 14:25:00+00:00", "trade_duration": 10, "open_rate": 5.198e-05, "close_rate": 5.432496240601503e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.344962406015033e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1923.8168526356292, "profit_abs": 0.0040000000000000036}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 14:50:00+00:00", "close_date": "2018-01-24 16:35:00+00:00", "trade_duration": 105, "open_rate": 3.054e-05, "close_rate": 3.069308270676692e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5308270676691466e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3274.3942370661425, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 15:10:00+00:00", "close_date": "2018-01-24 16:15:00+00:00", "trade_duration": 65, "open_rate": 9.263e-05, "close_rate": 9.309431077694236e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.6431077694236234e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1079.5638562020945, "profit_abs": 2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 22:40:00+00:00", "close_date": "2018-01-24 23:25:00+00:00", "trade_duration": 45, "open_rate": 5.514e-05, "close_rate": 5.54163909774436e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.7639097744360576e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1813.5654697134569, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 00:50:00+00:00", "close_date": "2018-01-25 01:30:00+00:00", "trade_duration": 40, "open_rate": 4.921e-05, "close_rate": 4.9456666666666664e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.4666666666666543e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.1072952651903, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 08:15:00+00:00", "close_date": "2018-01-25 12:15:00+00:00", "trade_duration": 240, "open_rate": 0.0026, "close_rate": 0.002613032581453634, "open_at_end": false, "sell_reason": "roi", "profit": 1.3032581453634e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.46153846153847, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 10:25:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 350, "open_rate": 0.02799871, "close_rate": 0.028139054411027563, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014034441102756326, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.571593119825878, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 11:00:00+00:00", "close_date": "2018-01-25 11:45:00+00:00", "trade_duration": 45, "open_rate": 0.04078902, "close_rate": 0.0409934762406015, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020445624060149575, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4516401717913303, "profit_abs": -1.3877787807814457e-17}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:05:00+00:00", "close_date": "2018-01-25 13:45:00+00:00", "trade_duration": 40, "open_rate": 2.89e-05, "close_rate": 2.904486215538847e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.4486215538846723e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3460.2076124567475, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:20:00+00:00", "close_date": "2018-01-25 14:05:00+00:00", "trade_duration": 45, "open_rate": 0.041103, "close_rate": 0.04130903007518797, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020603007518796984, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4329124394813033, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-25 15:45:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 30, "open_rate": 5.428e-05, "close_rate": 5.509624060150376e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.162406015037611e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1842.2991893883568, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 17:45:00+00:00", "close_date": "2018-01-25 23:15:00+00:00", "trade_duration": 330, "open_rate": 5.414e-05, "close_rate": 5.441137844611528e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.713784461152774e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1847.063169560399, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 21:15:00+00:00", "close_date": "2018-01-25 21:55:00+00:00", "trade_duration": 40, "open_rate": 0.04140777, "close_rate": 0.0416153277443609, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002075577443608964, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.415005686130888, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 02:05:00+00:00", "close_date": "2018-01-26 02:45:00+00:00", "trade_duration": 40, "open_rate": 0.00254309, "close_rate": 0.002555837318295739, "open_at_end": false, "sell_reason": "roi", "profit": 1.2747318295739177e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.32224183965177, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 02:55:00+00:00", "close_date": "2018-01-26 15:10:00+00:00", "trade_duration": 735, "open_rate": 5.607e-05, "close_rate": 5.6351052631578935e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.810526315789381e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1783.4849295523454, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 06:10:00+00:00", "close_date": "2018-01-26 09:25:00+00:00", "trade_duration": 195, "open_rate": 0.00253806, "close_rate": 0.0025507821052631577, "open_at_end": false, "sell_reason": "roi", "profit": 1.2722105263157733e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.400171784748984, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 07:25:00+00:00", "close_date": "2018-01-26 09:55:00+00:00", "trade_duration": 150, "open_rate": 0.0415, "close_rate": 0.04170802005012531, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020802005012530989, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4096385542168677, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-26 09:55:00+00:00", "close_date": "2018-01-26 10:25:00+00:00", "trade_duration": 30, "open_rate": 5.321e-05, "close_rate": 5.401015037593984e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.00150375939842e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1879.3459875963165, "profit_abs": 0.000999999999999987}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 16:05:00+00:00", "close_date": "2018-01-26 16:45:00+00:00", "trade_duration": 40, "open_rate": 0.02772046, "close_rate": 0.02785940967418546, "open_at_end": false, "sell_reason": "roi", "profit": 0.00013894967418546025, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.6074437437185387, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 23:35:00+00:00", "close_date": "2018-01-27 00:15:00+00:00", "trade_duration": 40, "open_rate": 0.09461341, "close_rate": 0.09508766268170424, "open_at_end": false, "sell_reason": "roi", "profit": 0.00047425268170424306, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0569326272036914, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 00:35:00+00:00", "close_date": "2018-01-27 01:30:00+00:00", "trade_duration": 55, "open_rate": 5.615e-05, "close_rate": 5.643145363408521e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.814536340852038e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1780.9439002671415, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.07877175, "open_date": "2018-01-27 00:45:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 4560, "open_rate": 5.556e-05, "close_rate": 5.144e-05, "open_at_end": true, "sell_reason": "force_sell", "profit": -4.120000000000001e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1799.8560115190785, "profit_abs": -0.007896868250539965}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 02:30:00+00:00", "close_date": "2018-01-27 11:25:00+00:00", "trade_duration": 535, "open_rate": 0.06900001, "close_rate": 0.06934587471177944, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003458647117794422, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492751522789635, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 06:25:00+00:00", "close_date": "2018-01-27 07:05:00+00:00", "trade_duration": 40, "open_rate": 0.09449985, "close_rate": 0.0949735334586466, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004736834586466093, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.058202737887944, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.04815133, "open_date": "2018-01-27 09:40:00+00:00", "close_date": "2018-01-30 04:40:00+00:00", "trade_duration": 4020, "open_rate": 0.0410697, "close_rate": 0.03928809, "open_at_end": true, "sell_reason": "force_sell", "profit": -0.001781610000000003, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4348850855983852, "profit_abs": -0.004827170578309559}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 11:45:00+00:00", "close_date": "2018-01-27 12:30:00+00:00", "trade_duration": 45, "open_rate": 0.0285, "close_rate": 0.02864285714285714, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014285714285713902, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5087719298245617, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 12:35:00+00:00", "close_date": "2018-01-27 15:25:00+00:00", "trade_duration": 170, "open_rate": 0.02866372, "close_rate": 0.02880739779448621, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014367779448621124, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.4887307020861216, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 15:50:00+00:00", "close_date": "2018-01-27 16:50:00+00:00", "trade_duration": 60, "open_rate": 0.095381, "close_rate": 0.09585910025062656, "open_at_end": false, "sell_reason": "roi", "profit": 0.00047810025062657024, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0484268355332824, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 17:05:00+00:00", "close_date": "2018-01-27 17:45:00+00:00", "trade_duration": 40, "open_rate": 0.06759092, "close_rate": 0.06792972160401002, "open_at_end": false, "sell_reason": "roi", "profit": 0.00033880160401002224, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4794886650455417, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 23:40:00+00:00", "close_date": "2018-01-28 01:05:00+00:00", "trade_duration": 85, "open_rate": 0.00258501, "close_rate": 0.002597967443609022, "open_at_end": false, "sell_reason": "roi", "profit": 1.2957443609021985e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.684569885609726, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 02:25:00+00:00", "close_date": "2018-01-28 08:10:00+00:00", "trade_duration": 345, "open_rate": 0.06698502, "close_rate": 0.0673207845112782, "open_at_end": false, "sell_reason": "roi", "profit": 0.00033576451127818874, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4928710926711672, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 10:25:00+00:00", "close_date": "2018-01-28 16:30:00+00:00", "trade_duration": 365, "open_rate": 0.0677177, "close_rate": 0.06805713709273183, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003394370927318202, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4767187899175547, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-28 20:35:00+00:00", "close_date": "2018-01-28 21:35:00+00:00", "trade_duration": 60, "open_rate": 5.215e-05, "close_rate": 5.2411403508771925e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.6140350877192417e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1917.5455417066157, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-28 22:00:00+00:00", "close_date": "2018-01-28 22:30:00+00:00", "trade_duration": 30, "open_rate": 0.00273809, "close_rate": 0.002779264285714285, "open_at_end": false, "sell_reason": "roi", "profit": 4.117428571428529e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.5218089982433, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-29 00:00:00+00:00", "close_date": "2018-01-29 00:30:00+00:00", "trade_duration": 30, "open_rate": 0.00274632, "close_rate": 0.002787618045112782, "open_at_end": false, "sell_reason": "roi", "profit": 4.129804511278194e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.412362725392526, "profit_abs": 0.0010000000000000148}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-29 02:15:00+00:00", "close_date": "2018-01-29 03:00:00+00:00", "trade_duration": 45, "open_rate": 0.01622478, "close_rate": 0.016306107218045113, "open_at_end": false, "sell_reason": "roi", "profit": 8.132721804511231e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.163411768911504, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 03:05:00+00:00", "close_date": "2018-01-29 03:45:00+00:00", "trade_duration": 40, "open_rate": 0.069, "close_rate": 0.06934586466165413, "open_at_end": false, "sell_reason": "roi", "profit": 0.00034586466165412166, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492753623188406, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 05:20:00+00:00", "close_date": "2018-01-29 06:55:00+00:00", "trade_duration": 95, "open_rate": 8.755e-05, "close_rate": 8.798884711779448e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.3884711779447504e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1142.204454597373, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 07:00:00+00:00", "close_date": "2018-01-29 19:25:00+00:00", "trade_duration": 745, "open_rate": 0.06825763, "close_rate": 0.06859977350877192, "open_at_end": false, "sell_reason": "roi", "profit": 0.00034214350877191657, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4650376815016872, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 19:45:00+00:00", "close_date": "2018-01-29 20:25:00+00:00", "trade_duration": 40, "open_rate": 0.06713892, "close_rate": 0.06747545593984962, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003365359398496137, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4894490408841845, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0199116, "open_date": "2018-01-29 23:30:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 315, "open_rate": 8.934e-05, "close_rate": 8.8e-05, "open_at_end": true, "sell_reason": "force_sell", "profit": -1.3399999999999973e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1119.3194537721067, "profit_abs": -0.0019961383478844796}], "results_per_pair": [{"key": "TRX/BTC", "trades": 15, "profit_mean": 0.0023467073333333323, "profit_mean_pct": 0.23467073333333321, "profit_sum": 0.035200609999999986, "profit_sum_pct": 3.5200609999999988, "profit_total_abs": 0.0035288616521155086, "profit_total_pct": 1.1733536666666662, "duration_avg": "2:28:00", "wins": 9, "draws": 2, "losses": 4}, {"key": "ADA/BTC", "trades": 29, "profit_mean": -0.0011598141379310352, "profit_mean_pct": -0.11598141379310352, "profit_sum": -0.03363461000000002, "profit_sum_pct": -3.3634610000000023, "profit_total_abs": -0.0033718682505400333, "profit_total_pct": -1.1211536666666675, "duration_avg": "5:35:00", "wins": 9, "draws": 11, "losses": 9}, {"key": "XLM/BTC", "trades": 21, "profit_mean": 0.0026243899999999994, "profit_mean_pct": 0.2624389999999999, "profit_sum": 0.05511218999999999, "profit_sum_pct": 5.511218999999999, "profit_total_abs": 0.005525000000000002, "profit_total_pct": 1.8370729999999995, "duration_avg": "3:21:00", "wins": 12, "draws": 3, "losses": 6}, {"key": "ETH/BTC", "trades": 21, "profit_mean": 0.0009500057142857142, "profit_mean_pct": 0.09500057142857142, "profit_sum": 0.01995012, "profit_sum_pct": 1.9950119999999998, "profit_total_abs": 0.0019999999999999463, "profit_total_pct": 0.6650039999999999, "duration_avg": "2:17:00", "wins": 5, "draws": 10, "losses": 6}, {"key": "XMR/BTC", "trades": 16, "profit_mean": -0.0027899012500000007, "profit_mean_pct": -0.2789901250000001, "profit_sum": -0.04463842000000001, "profit_sum_pct": -4.463842000000001, "profit_total_abs": -0.0044750000000000345, "profit_total_pct": -1.4879473333333337, "duration_avg": "8:41:00", "wins": 6, "draws": 5, "losses": 5}, {"key": "ZEC/BTC", "trades": 21, "profit_mean": -0.00039290904761904774, "profit_mean_pct": -0.03929090476190478, "profit_sum": -0.008251090000000003, "profit_sum_pct": -0.8251090000000003, "profit_total_abs": -0.000827170578309569, "profit_total_pct": -0.27503633333333344, "duration_avg": "4:17:00", "wins": 8, "draws": 7, "losses": 6}, {"key": "NXT/BTC", "trades": 12, "profit_mean": -0.0012261025000000006, "profit_mean_pct": -0.12261025000000006, "profit_sum": -0.014713230000000008, "profit_sum_pct": -1.4713230000000008, "profit_total_abs": -0.0014750000000000874, "profit_total_pct": -0.4904410000000003, "duration_avg": "0:57:00", "wins": 4, "draws": 3, "losses": 5}, {"key": "LTC/BTC", "trades": 8, "profit_mean": 0.00748129625, "profit_mean_pct": 0.748129625, "profit_sum": 0.05985037, "profit_sum_pct": 5.985037, "profit_total_abs": 0.006000000000000019, "profit_total_pct": 1.9950123333333334, "duration_avg": "1:59:00", "wins": 5, "draws": 2, "losses": 1}, {"key": "ETC/BTC", "trades": 20, "profit_mean": 0.0022568569999999997, "profit_mean_pct": 0.22568569999999996, "profit_sum": 0.04513713999999999, "profit_sum_pct": 4.513713999999999, "profit_total_abs": 0.004525000000000001, "profit_total_pct": 1.504571333333333, "duration_avg": "1:45:00", "wins": 11, "draws": 4, "losses": 5}, {"key": "DASH/BTC", "trades": 16, "profit_mean": 0.0018703237499999997, "profit_mean_pct": 0.18703237499999997, "profit_sum": 0.029925179999999996, "profit_sum_pct": 2.9925179999999996, "profit_total_abs": 0.002999999999999961, "profit_total_pct": 0.9975059999999999, "duration_avg": "3:03:00", "wins": 4, "draws": 7, "losses": 5}, {"key": "TOTAL", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}], "sell_reason_summary": [{"sell_reason": "roi", "trades": 170, "wins": 73, "draws": 54, "losses": 43, "profit_mean": 0.005398268352941177, "profit_mean_pct": 0.54, "profit_sum": 0.91770562, "profit_sum_pct": 91.77, "profit_total_abs": 0.09199999999999964, "profit_pct_total": 30.59}, {"sell_reason": "stop_loss", "trades": 6, "wins": 0, "draws": 0, "losses": 6, "profit_mean": -0.10448878000000002, "profit_mean_pct": -10.45, "profit_sum": -0.6269326800000001, "profit_sum_pct": -62.69, "profit_total_abs": -0.06284999999999992, "profit_pct_total": -20.9}, {"sell_reason": "force_sell", "trades": 3, "wins": 0, "draws": 0, "losses": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.89, "profit_sum": -0.14683468, "profit_sum_pct": -14.68, "profit_total_abs": -0.014720177176734003, "profit_pct_total": -4.89}], "left_open_trades": [{"key": "TRX/BTC", "trades": 1, "profit_mean": -0.0199116, "profit_mean_pct": -1.9911600000000003, "profit_sum": -0.0199116, "profit_sum_pct": -1.9911600000000003, "profit_total_abs": -0.0019961383478844796, "profit_total_pct": -0.6637200000000001, "duration_avg": "5:15:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ADA/BTC", "trades": 1, "profit_mean": -0.07877175, "profit_mean_pct": -7.877175, "profit_sum": -0.07877175, "profit_sum_pct": -7.877175, "profit_total_abs": -0.007896868250539965, "profit_total_pct": -2.625725, "duration_avg": "3 days, 4:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ZEC/BTC", "trades": 1, "profit_mean": -0.04815133, "profit_mean_pct": -4.815133, "profit_sum": -0.04815133, "profit_sum_pct": -4.815133, "profit_total_abs": -0.004827170578309559, "profit_total_pct": -1.6050443333333335, "duration_avg": "2 days, 19:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "TOTAL", "trades": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.894489333333333, "profit_sum": -0.14683468, "profit_sum_pct": -14.683468, "profit_total_abs": -0.014720177176734003, "profit_total_pct": -4.8944893333333335, "duration_avg": "2 days, 1:25:00", "wins": 0, "draws": 0, "losses": 3}], "total_trades": 179, "backtest_start": "2018-01-30 04:45:00+00:00", "backtest_start_ts": 1517287500, "backtest_end": "2018-01-30 04:45:00+00:00", "backtest_end_ts": 1517287500, "backtest_days": 0, "trades_per_day": null, "market_change": 0.25, "stake_amount": 0.1, "max_drawdown": 0.21142322000000008, "drawdown_start": "2018-01-24 14:25:00+00:00", "drawdown_start_ts": 1516803900.0, "drawdown_end": "2018-01-30 04:45:00+00:00", "drawdown_end_ts": 1517287500.0}}, "strategy_comparison": [{"key": "DefaultStrategy", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}]} +{"strategy": {"DefaultStrategy": {"trades": [{"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:20:00+00:00", "trade_duration": 5, "open_rate": 9.64e-05, "close_rate": 0.00010074887218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1037.344398340249, "profit_abs": 0.00399999999999999}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:30:00+00:00", "trade_duration": 15, "open_rate": 4.756e-05, "close_rate": 4.9705563909774425e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2102.6072329688814, "profit_abs": 0.00399999999999999}, {"pair": "XLM/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:35:00+00:00", "trade_duration": 10, "open_rate": 3.339e-05, "close_rate": 3.489631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2994.908655286014, "profit_abs": 0.0040000000000000036}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:40:00+00:00", "trade_duration": 15, "open_rate": 9.696e-05, "close_rate": 0.00010133413533834584, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1031.3531353135315, "profit_abs": 0.00399999999999999}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 07:35:00+00:00", "close_date": "2018-01-10 08:35:00+00:00", "trade_duration": 60, "open_rate": 0.0943, "close_rate": 0.09477268170426063, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0604453870625663, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 07:40:00+00:00", "close_date": "2018-01-10 08:10:00+00:00", "trade_duration": 30, "open_rate": 0.02719607, "close_rate": 0.02760503345864661, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.677001860930642, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 08:15:00+00:00", "close_date": "2018-01-10 09:55:00+00:00", "trade_duration": 100, "open_rate": 0.04634952, "close_rate": 0.046581848421052625, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1575196463739, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 14:45:00+00:00", "close_date": "2018-01-10 15:50:00+00:00", "trade_duration": 65, "open_rate": 3.066e-05, "close_rate": 3.081368421052631e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3261.5786040443577, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 16:35:00+00:00", "close_date": "2018-01-10 17:15:00+00:00", "trade_duration": 40, "open_rate": 0.0168999, "close_rate": 0.016984611278195488, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.917194776300452, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 16:40:00+00:00", "close_date": "2018-01-10 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.09132568, "close_rate": 0.0917834528320802, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0949822656672252, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 18:50:00+00:00", "close_date": "2018-01-10 19:45:00+00:00", "trade_duration": 55, "open_rate": 0.08898003, "close_rate": 0.08942604518796991, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1238476768326557, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 22:15:00+00:00", "close_date": "2018-01-10 23:00:00+00:00", "trade_duration": 45, "open_rate": 0.08560008, "close_rate": 0.08602915308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1682232072680307, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 22:50:00+00:00", "close_date": "2018-01-10 23:20:00+00:00", "trade_duration": 30, "open_rate": 0.00249083, "close_rate": 0.0025282860902255634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 40.147260150231055, "profit_abs": 0.000999999999999987}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 23:15:00+00:00", "close_date": "2018-01-11 00:15:00+00:00", "trade_duration": 60, "open_rate": 3.022e-05, "close_rate": 3.037147869674185e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3309.0668431502318, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-10 23:40:00+00:00", "close_date": "2018-01-11 00:05:00+00:00", "trade_duration": 25, "open_rate": 0.002437, "close_rate": 0.0024980776942355883, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.03405826836274, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 00:00:00+00:00", "close_date": "2018-01-11 00:35:00+00:00", "trade_duration": 35, "open_rate": 0.04771803, "close_rate": 0.04843559436090225, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0956439316543456, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-11 03:40:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 45, "open_rate": 3.651e-05, "close_rate": 3.2859000000000005e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2738.9756231169545, "profit_abs": -0.01047499999999997}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 03:55:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 30, "open_rate": 0.08824105, "close_rate": 0.08956798308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1332594070446804, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 04:00:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 50, "open_rate": 0.00243, "close_rate": 0.002442180451127819, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.1522633744856, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:55:00+00:00", "trade_duration": 25, "open_rate": 0.04545064, "close_rate": 0.046589753784461146, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.200189040242338, "profit_abs": 0.001999999999999988}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 20, "open_rate": 3.372e-05, "close_rate": 3.456511278195488e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2965.599051008304, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:55:00+00:00", "close_date": "2018-01-11 05:15:00+00:00", "trade_duration": 20, "open_rate": 0.02644, "close_rate": 0.02710265664160401, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7821482602118004, "profit_abs": 0.001999999999999988}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:20:00+00:00", "close_date": "2018-01-11 12:00:00+00:00", "trade_duration": 40, "open_rate": 0.08812, "close_rate": 0.08856170426065162, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1348161597821154, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:35:00+00:00", "close_date": "2018-01-11 12:15:00+00:00", "trade_duration": 40, "open_rate": 0.02683577, "close_rate": 0.026970285137844607, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7263696923919087, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 14:00:00+00:00", "close_date": "2018-01-11 14:25:00+00:00", "trade_duration": 25, "open_rate": 4.919e-05, "close_rate": 5.04228320802005e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.9335230737956, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 19:25:00+00:00", "close_date": "2018-01-11 20:35:00+00:00", "trade_duration": 70, "open_rate": 0.08784896, "close_rate": 0.08828930566416039, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1383174029607181, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:35:00+00:00", "close_date": "2018-01-11 23:30:00+00:00", "trade_duration": 55, "open_rate": 5.105e-05, "close_rate": 5.130588972431077e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1958.8638589618022, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:25:00+00:00", "trade_duration": 30, "open_rate": 3.96e-05, "close_rate": 4.019548872180451e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2525.252525252525, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:35:00+00:00", "trade_duration": 40, "open_rate": 2.885e-05, "close_rate": 2.899461152882205e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3466.204506065858, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 23:30:00+00:00", "close_date": "2018-01-12 00:05:00+00:00", "trade_duration": 35, "open_rate": 0.02645, "close_rate": 0.026847744360902256, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.780718336483932, "profit_abs": 0.0010000000000000148}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 23:55:00+00:00", "close_date": "2018-01-12 01:15:00+00:00", "trade_duration": 80, "open_rate": 0.048, "close_rate": 0.04824060150375939, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0833333333333335, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-12 21:15:00+00:00", "close_date": "2018-01-12 21:40:00+00:00", "trade_duration": 25, "open_rate": 4.692e-05, "close_rate": 4.809593984962405e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2131.287297527707, "profit_abs": 0.001999999999999974}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 00:55:00+00:00", "close_date": "2018-01-13 06:20:00+00:00", "trade_duration": 325, "open_rate": 0.00256966, "close_rate": 0.0025825405012531327, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.91565421106294, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 10:55:00+00:00", "close_date": "2018-01-13 11:35:00+00:00", "trade_duration": 40, "open_rate": 6.262e-05, "close_rate": 6.293388471177944e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1596.933886937081, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 13:05:00+00:00", "close_date": "2018-01-15 14:10:00+00:00", "trade_duration": 2945, "open_rate": 4.73e-05, "close_rate": 4.753709273182957e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2114.1649048625795, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:30:00+00:00", "close_date": "2018-01-13 14:45:00+00:00", "trade_duration": 75, "open_rate": 6.063e-05, "close_rate": 6.0933909774436085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1649.348507339601, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:40:00+00:00", "close_date": "2018-01-13 23:30:00+00:00", "trade_duration": 590, "open_rate": 0.00011082, "close_rate": 0.00011137548872180448, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 902.3641941887746, "profit_abs": -2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 15:15:00+00:00", "close_date": "2018-01-13 15:55:00+00:00", "trade_duration": 40, "open_rate": 5.93e-05, "close_rate": 5.9597243107769415e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1686.3406408094436, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 16:30:00+00:00", "close_date": "2018-01-13 17:10:00+00:00", "trade_duration": 40, "open_rate": 0.04850003, "close_rate": 0.04874313791979949, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0618543947292407, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 22:05:00+00:00", "close_date": "2018-01-14 06:25:00+00:00", "trade_duration": 500, "open_rate": 0.09825019, "close_rate": 0.09874267215538848, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0178097365511456, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 00:20:00+00:00", "close_date": "2018-01-14 22:55:00+00:00", "trade_duration": 1355, "open_rate": 6.018e-05, "close_rate": 6.048165413533834e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1661.681621801263, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 12:45:00+00:00", "close_date": "2018-01-14 13:25:00+00:00", "trade_duration": 40, "open_rate": 0.09758999, "close_rate": 0.0980791628822055, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.024695258191952, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-14 15:30:00+00:00", "close_date": "2018-01-14 16:00:00+00:00", "trade_duration": 30, "open_rate": 0.00311, "close_rate": 0.0031567669172932328, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.154340836012864, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 20:45:00+00:00", "close_date": "2018-01-14 22:15:00+00:00", "trade_duration": 90, "open_rate": 0.00312401, "close_rate": 0.003139669197994987, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.010140812609436, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 23:35:00+00:00", "close_date": "2018-01-15 00:30:00+00:00", "trade_duration": 55, "open_rate": 0.0174679, "close_rate": 0.017555458395989976, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.724786608579165, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 23:45:00+00:00", "close_date": "2018-01-15 00:25:00+00:00", "trade_duration": 40, "open_rate": 0.07346846, "close_rate": 0.07383672295739348, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.3611282991367997, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 02:25:00+00:00", "close_date": "2018-01-15 03:05:00+00:00", "trade_duration": 40, "open_rate": 0.097994, "close_rate": 0.09848519799498744, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.020470641059657, "profit_abs": -2.7755575615628914e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 07:20:00+00:00", "close_date": "2018-01-15 08:00:00+00:00", "trade_duration": 40, "open_rate": 0.09659, "close_rate": 0.09707416040100247, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0353038616834043, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-15 08:20:00+00:00", "close_date": "2018-01-15 08:55:00+00:00", "trade_duration": 35, "open_rate": 9.987e-05, "close_rate": 0.00010137180451127818, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1001.3016921998599, "profit_abs": 0.0010000000000000009}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-15 12:10:00+00:00", "close_date": "2018-01-16 02:50:00+00:00", "trade_duration": 880, "open_rate": 0.0948969, "close_rate": 0.09537257368421052, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0537752023511833, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:10:00+00:00", "close_date": "2018-01-15 17:40:00+00:00", "trade_duration": 210, "open_rate": 0.071, "close_rate": 0.07135588972431077, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4084507042253522, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:30:00+00:00", "close_date": "2018-01-15 15:10:00+00:00", "trade_duration": 40, "open_rate": 0.04600501, "close_rate": 0.046235611553884705, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.173676301776698, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:10:00+00:00", "close_date": "2018-01-15 19:25:00+00:00", "trade_duration": 75, "open_rate": 9.438e-05, "close_rate": 9.485308270676693e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1059.5465140919687, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:35:00+00:00", "close_date": "2018-01-15 19:15:00+00:00", "trade_duration": 40, "open_rate": 0.03040001, "close_rate": 0.030552391002506264, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.2894726021471703, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-15 20:25:00+00:00", "close_date": "2018-01-16 08:25:00+00:00", "trade_duration": 720, "open_rate": 5.837e-05, "close_rate": 5.2533e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1713.2088401576154, "profit_abs": -0.010474999999999984}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 20:40:00+00:00", "close_date": "2018-01-15 22:00:00+00:00", "trade_duration": 80, "open_rate": 0.046036, "close_rate": 0.04626675689223057, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1722130506560084, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 00:30:00+00:00", "close_date": "2018-01-16 01:10:00+00:00", "trade_duration": 40, "open_rate": 0.0028685, "close_rate": 0.0028828784461152877, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 34.86142583231654, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 01:15:00+00:00", "close_date": "2018-01-16 02:35:00+00:00", "trade_duration": 80, "open_rate": 0.06731755, "close_rate": 0.0676549813283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4854967241083492, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 07:45:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 55, "open_rate": 0.09217614, "close_rate": 0.09263817578947368, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0848794492804754, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:55:00+00:00", "trade_duration": 20, "open_rate": 0.0165, "close_rate": 0.016913533834586467, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.0606060606060606, "profit_abs": 0.0020000000000000018}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 5, "open_rate": 7.953e-05, "close_rate": 8.311781954887218e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1257.387149503332, "profit_abs": 0.00399999999999999}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 08:45:00+00:00", "close_date": "2018-01-16 09:50:00+00:00", "trade_duration": 65, "open_rate": 0.045202, "close_rate": 0.04542857644110275, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2122914915269236, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:45:00+00:00", "trade_duration": 30, "open_rate": 5.248e-05, "close_rate": 5.326917293233082e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1905.487804878049, "profit_abs": 0.0010000000000000009}, {"pair": "XMR/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:55:00+00:00", "trade_duration": 40, "open_rate": 0.02892318, "close_rate": 0.02906815834586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.457434486802627, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 09:50:00+00:00", "close_date": "2018-01-16 10:10:00+00:00", "trade_duration": 20, "open_rate": 5.158e-05, "close_rate": 5.287273182957392e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1938.735944164405, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:35:00+00:00", "trade_duration": 30, "open_rate": 0.02828232, "close_rate": 0.02870761804511278, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5357778286929786, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:40:00+00:00", "trade_duration": 35, "open_rate": 0.04357584, "close_rate": 0.044231115789473675, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.294849623093898, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 13:45:00+00:00", "close_date": "2018-01-16 14:20:00+00:00", "trade_duration": 35, "open_rate": 5.362e-05, "close_rate": 5.442631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1864.975755315181, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 17:30:00+00:00", "close_date": "2018-01-16 18:25:00+00:00", "trade_duration": 55, "open_rate": 5.302e-05, "close_rate": 5.328576441102756e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.0807242549984, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:45:00+00:00", "trade_duration": 30, "open_rate": 0.09129999, "close_rate": 0.09267292218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0952903718828448, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:35:00+00:00", "trade_duration": 20, "open_rate": 3.808e-05, "close_rate": 3.903438596491228e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2626.0504201680674, "profit_abs": 0.0020000000000000018}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 19:00:00+00:00", "close_date": "2018-01-16 19:30:00+00:00", "trade_duration": 30, "open_rate": 0.02811012, "close_rate": 0.028532828571428567, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.557437677249333, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 60, "open_rate": 0.00258379, "close_rate": 0.002325411, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.702835756775904, "profit_abs": -0.010474999999999984}, {"pair": "NXT/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 80, "open_rate": 2.559e-05, "close_rate": 2.3031e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3907.7764751856193, "profit_abs": -0.010474999999999998}, {"pair": "TRX/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:35:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 50, "open_rate": 7.62e-05, "close_rate": 6.858e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1312.3359580052495, "profit_abs": -0.010474999999999984}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:35:00+00:00", "trade_duration": 5, "open_rate": 0.00229844, "close_rate": 0.002402129022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 43.507770487809125, "profit_abs": 0.004000000000000017}, {"pair": "LTC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:40:00+00:00", "trade_duration": 10, "open_rate": 0.0151, "close_rate": 0.015781203007518795, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.622516556291391, "profit_abs": 0.00399999999999999}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:40:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 5, "open_rate": 0.00235676, "close_rate": 0.00246308, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 42.431134269081284, "profit_abs": 0.0040000000000000036}, {"pair": "DASH/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 22:45:00+00:00", "close_date": "2018-01-16 23:05:00+00:00", "trade_duration": 20, "open_rate": 0.0630692, "close_rate": 0.06464988170426066, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.585559988076589, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:50:00+00:00", "close_date": "2018-01-16 22:55:00+00:00", "trade_duration": 5, "open_rate": 2.2e-05, "close_rate": 2.299248120300751e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 4545.454545454546, "profit_abs": 0.003999999999999976}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-17 03:30:00+00:00", "close_date": "2018-01-17 04:00:00+00:00", "trade_duration": 30, "open_rate": 4.974e-05, "close_rate": 5.048796992481203e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2010.454362685967, "profit_abs": 0.0010000000000000009}, {"pair": "TRX/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-17 03:55:00+00:00", "close_date": "2018-01-17 04:15:00+00:00", "trade_duration": 20, "open_rate": 7.108e-05, "close_rate": 7.28614536340852e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1406.8655036578502, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 09:35:00+00:00", "close_date": "2018-01-17 10:15:00+00:00", "trade_duration": 40, "open_rate": 0.04327, "close_rate": 0.04348689223057644, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.3110700254217704, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:20:00+00:00", "close_date": "2018-01-17 17:00:00+00:00", "trade_duration": 400, "open_rate": 4.997e-05, "close_rate": 5.022047619047618e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2001.2007204322595, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:25:00+00:00", "trade_duration": 55, "open_rate": 0.06836818, "close_rate": 0.06871087764411027, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4626687444363737, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:10:00+00:00", "trade_duration": 40, "open_rate": 3.63e-05, "close_rate": 3.648195488721804e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2754.8209366391184, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:30:00+00:00", "close_date": "2018-01-17 22:05:00+00:00", "trade_duration": 575, "open_rate": 0.0281, "close_rate": 0.02824085213032581, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5587188612099645, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:35:00+00:00", "close_date": "2018-01-17 16:55:00+00:00", "trade_duration": 260, "open_rate": 0.08651001, "close_rate": 0.08694364413533832, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1559355963546878, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 05:00:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 55, "open_rate": 5.633e-05, "close_rate": 5.6612355889724306e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1775.2529735487308, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 05:20:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 35, "open_rate": 0.06988494, "close_rate": 0.07093584135338346, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.430923457900944, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 07:35:00+00:00", "close_date": "2018-01-18 08:15:00+00:00", "trade_duration": 40, "open_rate": 5.545e-05, "close_rate": 5.572794486215538e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1803.4265103697026, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 09:00:00+00:00", "close_date": "2018-01-18 09:40:00+00:00", "trade_duration": 40, "open_rate": 0.01633527, "close_rate": 0.016417151052631574, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.121723118136401, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 16:40:00+00:00", "close_date": "2018-01-18 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.00269734, "close_rate": 0.002710860501253133, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.073561360451414, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-18 18:05:00+00:00", "close_date": "2018-01-18 18:30:00+00:00", "trade_duration": 25, "open_rate": 4.475e-05, "close_rate": 4.587155388471177e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2234.63687150838, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 18:25:00+00:00", "close_date": "2018-01-18 18:55:00+00:00", "trade_duration": 30, "open_rate": 2.79e-05, "close_rate": 2.8319548872180444e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3584.2293906810037, "profit_abs": 0.000999999999999987}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 20:10:00+00:00", "close_date": "2018-01-18 20:50:00+00:00", "trade_duration": 40, "open_rate": 0.04439326, "close_rate": 0.04461578260651629, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2525942001105577, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 21:30:00+00:00", "close_date": "2018-01-19 00:35:00+00:00", "trade_duration": 185, "open_rate": 4.49e-05, "close_rate": 4.51250626566416e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2227.1714922049, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 21:55:00+00:00", "close_date": "2018-01-19 05:05:00+00:00", "trade_duration": 430, "open_rate": 0.02855, "close_rate": 0.028693107769423555, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.502626970227671, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 22:10:00+00:00", "close_date": "2018-01-18 22:50:00+00:00", "trade_duration": 40, "open_rate": 5.796e-05, "close_rate": 5.8250526315789473e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1725.3278122843342, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 23:50:00+00:00", "close_date": "2018-01-19 00:30:00+00:00", "trade_duration": 40, "open_rate": 0.04340323, "close_rate": 0.04362079005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.303975994413319, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-19 16:45:00+00:00", "close_date": "2018-01-19 17:35:00+00:00", "trade_duration": 50, "open_rate": 0.04454455, "close_rate": 0.04476783095238095, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.244943545282195, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:15:00+00:00", "close_date": "2018-01-19 19:55:00+00:00", "trade_duration": 160, "open_rate": 5.62e-05, "close_rate": 5.648170426065162e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1779.3594306049824, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:20:00+00:00", "close_date": "2018-01-19 20:15:00+00:00", "trade_duration": 175, "open_rate": 4.339e-05, "close_rate": 4.360749373433584e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2304.6784973496196, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-20 04:45:00+00:00", "close_date": "2018-01-20 17:35:00+00:00", "trade_duration": 770, "open_rate": 0.0001009, "close_rate": 0.00010140576441102755, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 991.0802775024778, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 15:15:00+00:00", "trade_duration": 625, "open_rate": 0.00270505, "close_rate": 0.002718609147869674, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.96789338459548, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 07:00:00+00:00", "trade_duration": 130, "open_rate": 0.03000002, "close_rate": 0.030150396040100245, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.3333311111125927, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 09:00:00+00:00", "close_date": "2018-01-20 09:40:00+00:00", "trade_duration": 40, "open_rate": 5.46e-05, "close_rate": 5.4873684210526304e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1831.5018315018317, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-20 18:25:00+00:00", "close_date": "2018-01-25 03:50:00+00:00", "trade_duration": 6325, "open_rate": 0.03082222, "close_rate": 0.027739998, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.244412634781012, "profit_abs": -0.010474999999999998}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 22:25:00+00:00", "close_date": "2018-01-20 23:15:00+00:00", "trade_duration": 50, "open_rate": 0.08969999, "close_rate": 0.09014961401002504, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1148273260677064, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 02:50:00+00:00", "close_date": "2018-01-21 14:30:00+00:00", "trade_duration": 700, "open_rate": 0.01632501, "close_rate": 0.01640683962406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.125570520324337, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 10:20:00+00:00", "close_date": "2018-01-21 11:00:00+00:00", "trade_duration": 40, "open_rate": 0.070538, "close_rate": 0.07089157393483708, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.417675579120474, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 15:50:00+00:00", "close_date": "2018-01-21 18:45:00+00:00", "trade_duration": 175, "open_rate": 5.301e-05, "close_rate": 5.327571428571427e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.4365214110546, "profit_abs": -2.7755575615628914e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 16:20:00+00:00", "close_date": "2018-01-21 17:00:00+00:00", "trade_duration": 40, "open_rate": 3.955e-05, "close_rate": 3.9748245614035085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2528.4450063211125, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:45:00+00:00", "trade_duration": 30, "open_rate": 0.00258505, "close_rate": 0.002623922932330827, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.6839712964933, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:55:00+00:00", "trade_duration": 40, "open_rate": 3.903e-05, "close_rate": 3.922563909774435e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2562.1316935690497, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 00:35:00+00:00", "close_date": "2018-01-22 10:35:00+00:00", "trade_duration": 600, "open_rate": 5.236e-05, "close_rate": 5.262245614035087e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1909.8548510313217, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 01:30:00+00:00", "close_date": "2018-01-22 02:10:00+00:00", "trade_duration": 40, "open_rate": 9.028e-05, "close_rate": 9.07325313283208e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1107.6650420912717, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 12:25:00+00:00", "close_date": "2018-01-22 14:35:00+00:00", "trade_duration": 130, "open_rate": 0.002687, "close_rate": 0.002700468671679198, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.21622627465575, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 13:15:00+00:00", "close_date": "2018-01-22 13:55:00+00:00", "trade_duration": 40, "open_rate": 4.168e-05, "close_rate": 4.188892230576441e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2399.232245681382, "profit_abs": 1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-22 14:00:00+00:00", "close_date": "2018-01-22 14:30:00+00:00", "trade_duration": 30, "open_rate": 8.821e-05, "close_rate": 8.953646616541353e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1133.6583153837435, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 15:55:00+00:00", "close_date": "2018-01-22 16:40:00+00:00", "trade_duration": 45, "open_rate": 5.172e-05, "close_rate": 5.1979248120300745e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1933.4880123743235, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-22 16:05:00+00:00", "close_date": "2018-01-22 16:25:00+00:00", "trade_duration": 20, "open_rate": 3.026e-05, "close_rate": 3.101839598997494e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3304.692663582287, "profit_abs": 0.0020000000000000157}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 19:50:00+00:00", "close_date": "2018-01-23 00:10:00+00:00", "trade_duration": 260, "open_rate": 0.07064, "close_rate": 0.07099408521303258, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.415628539071348, "profit_abs": 1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 21:25:00+00:00", "close_date": "2018-01-22 22:05:00+00:00", "trade_duration": 40, "open_rate": 0.01644483, "close_rate": 0.01652726022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.080938507725528, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-23 00:05:00+00:00", "close_date": "2018-01-23 00:35:00+00:00", "trade_duration": 30, "open_rate": 4.331e-05, "close_rate": 4.3961278195488714e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2308.935580697299, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-23 01:50:00+00:00", "close_date": "2018-01-23 02:15:00+00:00", "trade_duration": 25, "open_rate": 3.2e-05, "close_rate": 3.2802005012531326e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3125.0000000000005, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 04:25:00+00:00", "close_date": "2018-01-23 05:15:00+00:00", "trade_duration": 50, "open_rate": 0.09167706, "close_rate": 0.09213659413533835, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0907854156754153, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 07:35:00+00:00", "close_date": "2018-01-23 09:00:00+00:00", "trade_duration": 85, "open_rate": 0.0692498, "close_rate": 0.06959691679197995, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4440474918339115, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 10:50:00+00:00", "close_date": "2018-01-23 13:05:00+00:00", "trade_duration": 135, "open_rate": 3.182e-05, "close_rate": 3.197949874686716e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3142.677561282213, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 11:05:00+00:00", "close_date": "2018-01-23 16:05:00+00:00", "trade_duration": 300, "open_rate": 0.04088, "close_rate": 0.04108491228070175, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4461839530332683, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 14:55:00+00:00", "close_date": "2018-01-23 15:35:00+00:00", "trade_duration": 40, "open_rate": 5.15e-05, "close_rate": 5.175814536340851e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1941.747572815534, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 16:35:00+00:00", "close_date": "2018-01-24 00:05:00+00:00", "trade_duration": 450, "open_rate": 0.09071698, "close_rate": 0.09117170170426064, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1023294646713329, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 17:25:00+00:00", "close_date": "2018-01-23 18:45:00+00:00", "trade_duration": 80, "open_rate": 3.128e-05, "close_rate": 3.1436791979949865e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3196.9309462915603, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 20:15:00+00:00", "close_date": "2018-01-23 22:00:00+00:00", "trade_duration": 105, "open_rate": 9.555e-05, "close_rate": 9.602894736842104e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1046.5724751439038, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 22:30:00+00:00", "close_date": "2018-01-23 23:10:00+00:00", "trade_duration": 40, "open_rate": 0.04080001, "close_rate": 0.0410045213283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.450979791426522, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 23:50:00+00:00", "close_date": "2018-01-24 03:35:00+00:00", "trade_duration": 225, "open_rate": 5.163e-05, "close_rate": 5.18887969924812e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1936.8584156498162, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 00:20:00+00:00", "close_date": "2018-01-24 01:50:00+00:00", "trade_duration": 90, "open_rate": 0.04040781, "close_rate": 0.04061035541353383, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.474769110228938, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 06:45:00+00:00", "close_date": "2018-01-24 07:25:00+00:00", "trade_duration": 40, "open_rate": 5.132e-05, "close_rate": 5.157724310776942e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1948.5580670303975, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-24 14:15:00+00:00", "close_date": "2018-01-24 14:25:00+00:00", "trade_duration": 10, "open_rate": 5.198e-05, "close_rate": 5.432496240601503e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1923.8168526356292, "profit_abs": 0.0040000000000000036}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 14:50:00+00:00", "close_date": "2018-01-24 16:35:00+00:00", "trade_duration": 105, "open_rate": 3.054e-05, "close_rate": 3.069308270676692e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3274.3942370661425, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 15:10:00+00:00", "close_date": "2018-01-24 16:15:00+00:00", "trade_duration": 65, "open_rate": 9.263e-05, "close_rate": 9.309431077694236e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1079.5638562020945, "profit_abs": 2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 22:40:00+00:00", "close_date": "2018-01-24 23:25:00+00:00", "trade_duration": 45, "open_rate": 5.514e-05, "close_rate": 5.54163909774436e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1813.5654697134569, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 00:50:00+00:00", "close_date": "2018-01-25 01:30:00+00:00", "trade_duration": 40, "open_rate": 4.921e-05, "close_rate": 4.9456666666666664e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.1072952651903, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 08:15:00+00:00", "close_date": "2018-01-25 12:15:00+00:00", "trade_duration": 240, "open_rate": 0.0026, "close_rate": 0.002613032581453634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.46153846153847, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 10:25:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 350, "open_rate": 0.02799871, "close_rate": 0.028139054411027563, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.571593119825878, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 11:00:00+00:00", "close_date": "2018-01-25 11:45:00+00:00", "trade_duration": 45, "open_rate": 0.04078902, "close_rate": 0.0409934762406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4516401717913303, "profit_abs": -1.3877787807814457e-17}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:05:00+00:00", "close_date": "2018-01-25 13:45:00+00:00", "trade_duration": 40, "open_rate": 2.89e-05, "close_rate": 2.904486215538847e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3460.2076124567475, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:20:00+00:00", "close_date": "2018-01-25 14:05:00+00:00", "trade_duration": 45, "open_rate": 0.041103, "close_rate": 0.04130903007518797, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4329124394813033, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-25 15:45:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 30, "open_rate": 5.428e-05, "close_rate": 5.509624060150376e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1842.2991893883568, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 17:45:00+00:00", "close_date": "2018-01-25 23:15:00+00:00", "trade_duration": 330, "open_rate": 5.414e-05, "close_rate": 5.441137844611528e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1847.063169560399, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 21:15:00+00:00", "close_date": "2018-01-25 21:55:00+00:00", "trade_duration": 40, "open_rate": 0.04140777, "close_rate": 0.0416153277443609, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.415005686130888, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 02:05:00+00:00", "close_date": "2018-01-26 02:45:00+00:00", "trade_duration": 40, "open_rate": 0.00254309, "close_rate": 0.002555837318295739, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.32224183965177, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 02:55:00+00:00", "close_date": "2018-01-26 15:10:00+00:00", "trade_duration": 735, "open_rate": 5.607e-05, "close_rate": 5.6351052631578935e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1783.4849295523454, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 06:10:00+00:00", "close_date": "2018-01-26 09:25:00+00:00", "trade_duration": 195, "open_rate": 0.00253806, "close_rate": 0.0025507821052631577, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.400171784748984, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 07:25:00+00:00", "close_date": "2018-01-26 09:55:00+00:00", "trade_duration": 150, "open_rate": 0.0415, "close_rate": 0.04170802005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4096385542168677, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-26 09:55:00+00:00", "close_date": "2018-01-26 10:25:00+00:00", "trade_duration": 30, "open_rate": 5.321e-05, "close_rate": 5.401015037593984e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1879.3459875963165, "profit_abs": 0.000999999999999987}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 16:05:00+00:00", "close_date": "2018-01-26 16:45:00+00:00", "trade_duration": 40, "open_rate": 0.02772046, "close_rate": 0.02785940967418546, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.6074437437185387, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 23:35:00+00:00", "close_date": "2018-01-27 00:15:00+00:00", "trade_duration": 40, "open_rate": 0.09461341, "close_rate": 0.09508766268170424, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0569326272036914, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 00:35:00+00:00", "close_date": "2018-01-27 01:30:00+00:00", "trade_duration": 55, "open_rate": 5.615e-05, "close_rate": 5.643145363408521e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1780.9439002671415, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.07877175, "open_date": "2018-01-27 00:45:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 4560, "open_rate": 5.556e-05, "close_rate": 5.144e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1799.8560115190785, "profit_abs": -0.007896868250539965}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 02:30:00+00:00", "close_date": "2018-01-27 11:25:00+00:00", "trade_duration": 535, "open_rate": 0.06900001, "close_rate": 0.06934587471177944, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492751522789635, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 06:25:00+00:00", "close_date": "2018-01-27 07:05:00+00:00", "trade_duration": 40, "open_rate": 0.09449985, "close_rate": 0.0949735334586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.058202737887944, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.04815133, "open_date": "2018-01-27 09:40:00+00:00", "close_date": "2018-01-30 04:40:00+00:00", "trade_duration": 4020, "open_rate": 0.0410697, "close_rate": 0.03928809, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4348850855983852, "profit_abs": -0.004827170578309559}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 11:45:00+00:00", "close_date": "2018-01-27 12:30:00+00:00", "trade_duration": 45, "open_rate": 0.0285, "close_rate": 0.02864285714285714, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5087719298245617, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 12:35:00+00:00", "close_date": "2018-01-27 15:25:00+00:00", "trade_duration": 170, "open_rate": 0.02866372, "close_rate": 0.02880739779448621, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.4887307020861216, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 15:50:00+00:00", "close_date": "2018-01-27 16:50:00+00:00", "trade_duration": 60, "open_rate": 0.095381, "close_rate": 0.09585910025062656, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0484268355332824, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 17:05:00+00:00", "close_date": "2018-01-27 17:45:00+00:00", "trade_duration": 40, "open_rate": 0.06759092, "close_rate": 0.06792972160401002, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4794886650455417, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 23:40:00+00:00", "close_date": "2018-01-28 01:05:00+00:00", "trade_duration": 85, "open_rate": 0.00258501, "close_rate": 0.002597967443609022, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.684569885609726, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 02:25:00+00:00", "close_date": "2018-01-28 08:10:00+00:00", "trade_duration": 345, "open_rate": 0.06698502, "close_rate": 0.0673207845112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4928710926711672, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 10:25:00+00:00", "close_date": "2018-01-28 16:30:00+00:00", "trade_duration": 365, "open_rate": 0.0677177, "close_rate": 0.06805713709273183, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4767187899175547, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-28 20:35:00+00:00", "close_date": "2018-01-28 21:35:00+00:00", "trade_duration": 60, "open_rate": 5.215e-05, "close_rate": 5.2411403508771925e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1917.5455417066157, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-28 22:00:00+00:00", "close_date": "2018-01-28 22:30:00+00:00", "trade_duration": 30, "open_rate": 0.00273809, "close_rate": 0.002779264285714285, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.5218089982433, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-29 00:00:00+00:00", "close_date": "2018-01-29 00:30:00+00:00", "trade_duration": 30, "open_rate": 0.00274632, "close_rate": 0.002787618045112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.412362725392526, "profit_abs": 0.0010000000000000148}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-29 02:15:00+00:00", "close_date": "2018-01-29 03:00:00+00:00", "trade_duration": 45, "open_rate": 0.01622478, "close_rate": 0.016306107218045113, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.163411768911504, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 03:05:00+00:00", "close_date": "2018-01-29 03:45:00+00:00", "trade_duration": 40, "open_rate": 0.069, "close_rate": 0.06934586466165413, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492753623188406, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 05:20:00+00:00", "close_date": "2018-01-29 06:55:00+00:00", "trade_duration": 95, "open_rate": 8.755e-05, "close_rate": 8.798884711779448e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1142.204454597373, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 07:00:00+00:00", "close_date": "2018-01-29 19:25:00+00:00", "trade_duration": 745, "open_rate": 0.06825763, "close_rate": 0.06859977350877192, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4650376815016872, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 19:45:00+00:00", "close_date": "2018-01-29 20:25:00+00:00", "trade_duration": 40, "open_rate": 0.06713892, "close_rate": 0.06747545593984962, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4894490408841845, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0199116, "open_date": "2018-01-29 23:30:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 315, "open_rate": 8.934e-05, "close_rate": 8.8e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1119.3194537721067, "profit_abs": -0.0019961383478844796}], "results_per_pair": [{"key": "TRX/BTC", "trades": 15, "profit_mean": 0.0023467073333333323, "profit_mean_pct": 0.23467073333333321, "profit_sum": 0.035200609999999986, "profit_sum_pct": 3.5200609999999988, "profit_total_abs": 0.0035288616521155086, "profit_total_pct": 1.1733536666666662, "duration_avg": "2:28:00", "wins": 9, "draws": 2, "losses": 4}, {"key": "ADA/BTC", "trades": 29, "profit_mean": -0.0011598141379310352, "profit_mean_pct": -0.11598141379310352, "profit_sum": -0.03363461000000002, "profit_sum_pct": -3.3634610000000023, "profit_total_abs": -0.0033718682505400333, "profit_total_pct": -1.1211536666666675, "duration_avg": "5:35:00", "wins": 9, "draws": 11, "losses": 9}, {"key": "XLM/BTC", "trades": 21, "profit_mean": 0.0026243899999999994, "profit_mean_pct": 0.2624389999999999, "profit_sum": 0.05511218999999999, "profit_sum_pct": 5.511218999999999, "profit_total_abs": 0.005525000000000002, "profit_total_pct": 1.8370729999999995, "duration_avg": "3:21:00", "wins": 12, "draws": 3, "losses": 6}, {"key": "ETH/BTC", "trades": 21, "profit_mean": 0.0009500057142857142, "profit_mean_pct": 0.09500057142857142, "profit_sum": 0.01995012, "profit_sum_pct": 1.9950119999999998, "profit_total_abs": 0.0019999999999999463, "profit_total_pct": 0.6650039999999999, "duration_avg": "2:17:00", "wins": 5, "draws": 10, "losses": 6}, {"key": "XMR/BTC", "trades": 16, "profit_mean": -0.0027899012500000007, "profit_mean_pct": -0.2789901250000001, "profit_sum": -0.04463842000000001, "profit_sum_pct": -4.463842000000001, "profit_total_abs": -0.0044750000000000345, "profit_total_pct": -1.4879473333333337, "duration_avg": "8:41:00", "wins": 6, "draws": 5, "losses": 5}, {"key": "ZEC/BTC", "trades": 21, "profit_mean": -0.00039290904761904774, "profit_mean_pct": -0.03929090476190478, "profit_sum": -0.008251090000000003, "profit_sum_pct": -0.8251090000000003, "profit_total_abs": -0.000827170578309569, "profit_total_pct": -0.27503633333333344, "duration_avg": "4:17:00", "wins": 8, "draws": 7, "losses": 6}, {"key": "NXT/BTC", "trades": 12, "profit_mean": -0.0012261025000000006, "profit_mean_pct": -0.12261025000000006, "profit_sum": -0.014713230000000008, "profit_sum_pct": -1.4713230000000008, "profit_total_abs": -0.0014750000000000874, "profit_total_pct": -0.4904410000000003, "duration_avg": "0:57:00", "wins": 4, "draws": 3, "losses": 5}, {"key": "LTC/BTC", "trades": 8, "profit_mean": 0.00748129625, "profit_mean_pct": 0.748129625, "profit_sum": 0.05985037, "profit_sum_pct": 5.985037, "profit_total_abs": 0.006000000000000019, "profit_total_pct": 1.9950123333333334, "duration_avg": "1:59:00", "wins": 5, "draws": 2, "losses": 1}, {"key": "ETC/BTC", "trades": 20, "profit_mean": 0.0022568569999999997, "profit_mean_pct": 0.22568569999999996, "profit_sum": 0.04513713999999999, "profit_sum_pct": 4.513713999999999, "profit_total_abs": 0.004525000000000001, "profit_total_pct": 1.504571333333333, "duration_avg": "1:45:00", "wins": 11, "draws": 4, "losses": 5}, {"key": "DASH/BTC", "trades": 16, "profit_mean": 0.0018703237499999997, "profit_mean_pct": 0.18703237499999997, "profit_sum": 0.029925179999999996, "profit_sum_pct": 2.9925179999999996, "profit_total_abs": 0.002999999999999961, "profit_total_pct": 0.9975059999999999, "duration_avg": "3:03:00", "wins": 4, "draws": 7, "losses": 5}, {"key": "TOTAL", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}], "sell_reason_summary": [{"sell_reason": "roi", "trades": 170, "wins": 73, "draws": 54, "losses": 43, "profit_mean": 0.005398268352941177, "profit_mean_pct": 0.54, "profit_sum": 0.91770562, "profit_sum_pct": 91.77, "profit_total_abs": 0.09199999999999964, "profit_pct_total": 30.59}, {"sell_reason": "stop_loss", "trades": 6, "wins": 0, "draws": 0, "losses": 6, "profit_mean": -0.10448878000000002, "profit_mean_pct": -10.45, "profit_sum": -0.6269326800000001, "profit_sum_pct": -62.69, "profit_total_abs": -0.06284999999999992, "profit_pct_total": -20.9}, {"sell_reason": "force_sell", "trades": 3, "wins": 0, "draws": 0, "losses": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.89, "profit_sum": -0.14683468, "profit_sum_pct": -14.68, "profit_total_abs": -0.014720177176734003, "profit_pct_total": -4.89}], "left_open_trades": [{"key": "TRX/BTC", "trades": 1, "profit_mean": -0.0199116, "profit_mean_pct": -1.9911600000000003, "profit_sum": -0.0199116, "profit_sum_pct": -1.9911600000000003, "profit_total_abs": -0.0019961383478844796, "profit_total_pct": -0.6637200000000001, "duration_avg": "5:15:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ADA/BTC", "trades": 1, "profit_mean": -0.07877175, "profit_mean_pct": -7.877175, "profit_sum": -0.07877175, "profit_sum_pct": -7.877175, "profit_total_abs": -0.007896868250539965, "profit_total_pct": -2.625725, "duration_avg": "3 days, 4:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ZEC/BTC", "trades": 1, "profit_mean": -0.04815133, "profit_mean_pct": -4.815133, "profit_sum": -0.04815133, "profit_sum_pct": -4.815133, "profit_total_abs": -0.004827170578309559, "profit_total_pct": -1.6050443333333335, "duration_avg": "2 days, 19:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "TOTAL", "trades": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.894489333333333, "profit_sum": -0.14683468, "profit_sum_pct": -14.683468, "profit_total_abs": -0.014720177176734003, "profit_total_pct": -4.8944893333333335, "duration_avg": "2 days, 1:25:00", "wins": 0, "draws": 0, "losses": 3}], "total_trades": 179, "backtest_start": "2018-01-30 04:45:00+00:00", "backtest_start_ts": 1517287500, "backtest_end": "2018-01-30 04:45:00+00:00", "backtest_end_ts": 1517287500, "backtest_days": 0, "trades_per_day": null, "market_change": 0.25, "stake_amount": 0.1, "max_drawdown": 0.21142322000000008, "drawdown_start": "2018-01-24 14:25:00+00:00", "drawdown_start_ts": 1516803900.0, "drawdown_end": "2018-01-30 04:45:00+00:00", "drawdown_end_ts": 1517287500.0}}, "strategy_comparison": [{"key": "DefaultStrategy", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}]} From 5b1a7ba00f8d9dbde687ba5f26ebfec3c420f38f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Jun 2020 15:59:22 +0200 Subject: [PATCH 0221/1197] Test multistrat loading --- tests/data/test_btanalysis.py | 27 ++++++++++++++++++- .../testdata/backtest-result_multistrat.json | 2 +- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 9ae67ed58..63fe26eaa 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -32,8 +32,10 @@ def test_get_latest_backtest_filename(testdatadir, mocker): res = get_latest_backtest_filename(testdatadir) assert res == 'backtest-result_new.json' - mocker.patch("freqtrade.data.btanalysis.json_load", return_value={}) + res = get_latest_backtest_filename(str(testdatadir)) + assert res == 'backtest-result_new.json' + mocker.patch("freqtrade.data.btanalysis.json_load", return_value={}) with pytest.raises(ValueError, match=r"Invalid '.last_result.json' format."): get_latest_backtest_filename(testdatadir) @@ -70,6 +72,29 @@ def test_load_backtest_data_new_format(testdatadir): with pytest.raises(ValueError, match=r"File .* does not exist\."): load_backtest_data(str("filename") + "nofile") + with pytest.raises(ValueError, match=r"Unknown dataformat."): + load_backtest_data(testdatadir / '.last_result.json') + + +def test_load_backtest_data_multi(testdatadir): + + filename = testdatadir / "backtest-result_multistrat.json" + for strategy in ('DefaultStrategy', 'TestStrategy'): + bt_data = load_backtest_data(filename, strategy=strategy) + assert isinstance(bt_data, DataFrame) + assert set(bt_data.columns) == set(list(BacktestResult._fields) + ["profit_abs"]) + assert len(bt_data) == 179 + + # Test loading from string (must yield same result) + bt_data2 = load_backtest_data(str(filename), strategy=strategy) + assert bt_data.equals(bt_data2) + + with pytest.raises(ValueError, match=r"Strategy XYZ not available in the backtest result\."): + load_backtest_data(filename, strategy='XYZ') + + with pytest.raises(ValueError, match=r"Detected backtest result with more than one strategy.*"): + load_backtest_data(filename) + @pytest.mark.usefixtures("init_persistence") def test_load_trades_from_db(default_conf, fee, mocker): diff --git a/tests/testdata/backtest-result_multistrat.json b/tests/testdata/backtest-result_multistrat.json index a58ab28cb..88f021cb8 100644 --- a/tests/testdata/backtest-result_multistrat.json +++ b/tests/testdata/backtest-result_multistrat.json @@ -1 +1 @@ -{"strategy": {"DefaultStrategy": {"trades": [{"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:20:00+00:00", "trade_duration": 5, "open_rate": 9.64e-05, "close_rate": 0.00010074887218045112, "open_at_end": false, "sell_reason": "roi", "profit": 4.348872180451118e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1037.344398340249, "profit_abs": 0.00399999999999999}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:30:00+00:00", "trade_duration": 15, "open_rate": 4.756e-05, "close_rate": 4.9705563909774425e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.1455639097744267e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2102.6072329688814, "profit_abs": 0.00399999999999999}, {"pair": "XLM/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:35:00+00:00", "trade_duration": 10, "open_rate": 3.339e-05, "close_rate": 3.489631578947368e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.506315789473681e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2994.908655286014, "profit_abs": 0.0040000000000000036}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:40:00+00:00", "trade_duration": 15, "open_rate": 9.696e-05, "close_rate": 0.00010133413533834584, "open_at_end": false, "sell_reason": "roi", "profit": 4.3741353383458455e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1031.3531353135315, "profit_abs": 0.00399999999999999}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 07:35:00+00:00", "close_date": "2018-01-10 08:35:00+00:00", "trade_duration": 60, "open_rate": 0.0943, "close_rate": 0.09477268170426063, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004726817042606385, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0604453870625663, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 07:40:00+00:00", "close_date": "2018-01-10 08:10:00+00:00", "trade_duration": 30, "open_rate": 0.02719607, "close_rate": 0.02760503345864661, "open_at_end": false, "sell_reason": "roi", "profit": 0.00040896345864661204, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.677001860930642, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 08:15:00+00:00", "close_date": "2018-01-10 09:55:00+00:00", "trade_duration": 100, "open_rate": 0.04634952, "close_rate": 0.046581848421052625, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002323284210526272, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1575196463739, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 14:45:00+00:00", "close_date": "2018-01-10 15:50:00+00:00", "trade_duration": 65, "open_rate": 3.066e-05, "close_rate": 3.081368421052631e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5368421052630647e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3261.5786040443577, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 16:35:00+00:00", "close_date": "2018-01-10 17:15:00+00:00", "trade_duration": 40, "open_rate": 0.0168999, "close_rate": 0.016984611278195488, "open_at_end": false, "sell_reason": "roi", "profit": 8.471127819548868e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.917194776300452, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 16:40:00+00:00", "close_date": "2018-01-10 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.09132568, "close_rate": 0.0917834528320802, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004577728320801916, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0949822656672252, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 18:50:00+00:00", "close_date": "2018-01-10 19:45:00+00:00", "trade_duration": 55, "open_rate": 0.08898003, "close_rate": 0.08942604518796991, "open_at_end": false, "sell_reason": "roi", "profit": 0.00044601518796991146, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1238476768326557, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 22:15:00+00:00", "close_date": "2018-01-10 23:00:00+00:00", "trade_duration": 45, "open_rate": 0.08560008, "close_rate": 0.08602915308270676, "open_at_end": false, "sell_reason": "roi", "profit": 0.00042907308270676014, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1682232072680307, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 22:50:00+00:00", "close_date": "2018-01-10 23:20:00+00:00", "trade_duration": 30, "open_rate": 0.00249083, "close_rate": 0.0025282860902255634, "open_at_end": false, "sell_reason": "roi", "profit": 3.745609022556351e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 40.147260150231055, "profit_abs": 0.000999999999999987}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 23:15:00+00:00", "close_date": "2018-01-11 00:15:00+00:00", "trade_duration": 60, "open_rate": 3.022e-05, "close_rate": 3.037147869674185e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5147869674185174e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3309.0668431502318, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-10 23:40:00+00:00", "close_date": "2018-01-11 00:05:00+00:00", "trade_duration": 25, "open_rate": 0.002437, "close_rate": 0.0024980776942355883, "open_at_end": false, "sell_reason": "roi", "profit": 6.107769423558838e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.03405826836274, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 00:00:00+00:00", "close_date": "2018-01-11 00:35:00+00:00", "trade_duration": 35, "open_rate": 0.04771803, "close_rate": 0.04843559436090225, "open_at_end": false, "sell_reason": "roi", "profit": 0.0007175643609022495, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0956439316543456, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-11 03:40:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 45, "open_rate": 3.651e-05, "close_rate": 3.2859000000000005e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -3.650999999999996e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2738.9756231169545, "profit_abs": -0.01047499999999997}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 03:55:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 30, "open_rate": 0.08824105, "close_rate": 0.08956798308270676, "open_at_end": false, "sell_reason": "roi", "profit": 0.0013269330827067605, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1332594070446804, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 04:00:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 50, "open_rate": 0.00243, "close_rate": 0.002442180451127819, "open_at_end": false, "sell_reason": "roi", "profit": 1.2180451127819219e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.1522633744856, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:55:00+00:00", "trade_duration": 25, "open_rate": 0.04545064, "close_rate": 0.046589753784461146, "open_at_end": false, "sell_reason": "roi", "profit": 0.001139113784461146, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.200189040242338, "profit_abs": 0.001999999999999988}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 20, "open_rate": 3.372e-05, "close_rate": 3.456511278195488e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.4511278195488e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2965.599051008304, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:55:00+00:00", "close_date": "2018-01-11 05:15:00+00:00", "trade_duration": 20, "open_rate": 0.02644, "close_rate": 0.02710265664160401, "open_at_end": false, "sell_reason": "roi", "profit": 0.0006626566416040071, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7821482602118004, "profit_abs": 0.001999999999999988}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:20:00+00:00", "close_date": "2018-01-11 12:00:00+00:00", "trade_duration": 40, "open_rate": 0.08812, "close_rate": 0.08856170426065162, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004417042606516125, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1348161597821154, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:35:00+00:00", "close_date": "2018-01-11 12:15:00+00:00", "trade_duration": 40, "open_rate": 0.02683577, "close_rate": 0.026970285137844607, "open_at_end": false, "sell_reason": "roi", "profit": 0.00013451513784460897, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7263696923919087, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 14:00:00+00:00", "close_date": "2018-01-11 14:25:00+00:00", "trade_duration": 25, "open_rate": 4.919e-05, "close_rate": 5.04228320802005e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.232832080200495e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.9335230737956, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 19:25:00+00:00", "close_date": "2018-01-11 20:35:00+00:00", "trade_duration": 70, "open_rate": 0.08784896, "close_rate": 0.08828930566416039, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004403456641603881, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1383174029607181, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:35:00+00:00", "close_date": "2018-01-11 23:30:00+00:00", "trade_duration": 55, "open_rate": 5.105e-05, "close_rate": 5.130588972431077e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.558897243107704e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1958.8638589618022, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:25:00+00:00", "trade_duration": 30, "open_rate": 3.96e-05, "close_rate": 4.019548872180451e-05, "open_at_end": false, "sell_reason": "roi", "profit": 5.954887218045116e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2525.252525252525, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:35:00+00:00", "trade_duration": 40, "open_rate": 2.885e-05, "close_rate": 2.899461152882205e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.4461152882205115e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3466.204506065858, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 23:30:00+00:00", "close_date": "2018-01-12 00:05:00+00:00", "trade_duration": 35, "open_rate": 0.02645, "close_rate": 0.026847744360902256, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003977443609022545, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.780718336483932, "profit_abs": 0.0010000000000000148}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 23:55:00+00:00", "close_date": "2018-01-12 01:15:00+00:00", "trade_duration": 80, "open_rate": 0.048, "close_rate": 0.04824060150375939, "open_at_end": false, "sell_reason": "roi", "profit": 0.00024060150375938838, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0833333333333335, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-12 21:15:00+00:00", "close_date": "2018-01-12 21:40:00+00:00", "trade_duration": 25, "open_rate": 4.692e-05, "close_rate": 4.809593984962405e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.1759398496240516e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2131.287297527707, "profit_abs": 0.001999999999999974}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 00:55:00+00:00", "close_date": "2018-01-13 06:20:00+00:00", "trade_duration": 325, "open_rate": 0.00256966, "close_rate": 0.0025825405012531327, "open_at_end": false, "sell_reason": "roi", "profit": 1.2880501253132587e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.91565421106294, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 10:55:00+00:00", "close_date": "2018-01-13 11:35:00+00:00", "trade_duration": 40, "open_rate": 6.262e-05, "close_rate": 6.293388471177944e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.138847117794446e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1596.933886937081, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 13:05:00+00:00", "close_date": "2018-01-15 14:10:00+00:00", "trade_duration": 2945, "open_rate": 4.73e-05, "close_rate": 4.753709273182957e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.3709273182957117e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2114.1649048625795, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:30:00+00:00", "close_date": "2018-01-13 14:45:00+00:00", "trade_duration": 75, "open_rate": 6.063e-05, "close_rate": 6.0933909774436085e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.039097744360846e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1649.348507339601, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:40:00+00:00", "close_date": "2018-01-13 23:30:00+00:00", "trade_duration": 590, "open_rate": 0.00011082, "close_rate": 0.00011137548872180448, "open_at_end": false, "sell_reason": "roi", "profit": 5.554887218044781e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 902.3641941887746, "profit_abs": -2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 15:15:00+00:00", "close_date": "2018-01-13 15:55:00+00:00", "trade_duration": 40, "open_rate": 5.93e-05, "close_rate": 5.9597243107769415e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.9724310776941686e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1686.3406408094436, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 16:30:00+00:00", "close_date": "2018-01-13 17:10:00+00:00", "trade_duration": 40, "open_rate": 0.04850003, "close_rate": 0.04874313791979949, "open_at_end": false, "sell_reason": "roi", "profit": 0.00024310791979949287, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0618543947292407, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 22:05:00+00:00", "close_date": "2018-01-14 06:25:00+00:00", "trade_duration": 500, "open_rate": 0.09825019, "close_rate": 0.09874267215538848, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004924821553884823, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0178097365511456, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 00:20:00+00:00", "close_date": "2018-01-14 22:55:00+00:00", "trade_duration": 1355, "open_rate": 6.018e-05, "close_rate": 6.048165413533834e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.0165413533833987e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1661.681621801263, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 12:45:00+00:00", "close_date": "2018-01-14 13:25:00+00:00", "trade_duration": 40, "open_rate": 0.09758999, "close_rate": 0.0980791628822055, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004891728822054991, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.024695258191952, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-14 15:30:00+00:00", "close_date": "2018-01-14 16:00:00+00:00", "trade_duration": 30, "open_rate": 0.00311, "close_rate": 0.0031567669172932328, "open_at_end": false, "sell_reason": "roi", "profit": 4.676691729323286e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.154340836012864, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 20:45:00+00:00", "close_date": "2018-01-14 22:15:00+00:00", "trade_duration": 90, "open_rate": 0.00312401, "close_rate": 0.003139669197994987, "open_at_end": false, "sell_reason": "roi", "profit": 1.5659197994987058e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.010140812609436, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 23:35:00+00:00", "close_date": "2018-01-15 00:30:00+00:00", "trade_duration": 55, "open_rate": 0.0174679, "close_rate": 0.017555458395989976, "open_at_end": false, "sell_reason": "roi", "profit": 8.755839598997492e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.724786608579165, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 23:45:00+00:00", "close_date": "2018-01-15 00:25:00+00:00", "trade_duration": 40, "open_rate": 0.07346846, "close_rate": 0.07383672295739348, "open_at_end": false, "sell_reason": "roi", "profit": 0.00036826295739347814, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.3611282991367997, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 02:25:00+00:00", "close_date": "2018-01-15 03:05:00+00:00", "trade_duration": 40, "open_rate": 0.097994, "close_rate": 0.09848519799498744, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004911979949874384, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.020470641059657, "profit_abs": -2.7755575615628914e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 07:20:00+00:00", "close_date": "2018-01-15 08:00:00+00:00", "trade_duration": 40, "open_rate": 0.09659, "close_rate": 0.09707416040100247, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004841604010024786, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0353038616834043, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-15 08:20:00+00:00", "close_date": "2018-01-15 08:55:00+00:00", "trade_duration": 35, "open_rate": 9.987e-05, "close_rate": 0.00010137180451127818, "open_at_end": false, "sell_reason": "roi", "profit": 1.501804511278178e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1001.3016921998599, "profit_abs": 0.0010000000000000009}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-15 12:10:00+00:00", "close_date": "2018-01-16 02:50:00+00:00", "trade_duration": 880, "open_rate": 0.0948969, "close_rate": 0.09537257368421052, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004756736842105175, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0537752023511833, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:10:00+00:00", "close_date": "2018-01-15 17:40:00+00:00", "trade_duration": 210, "open_rate": 0.071, "close_rate": 0.07135588972431077, "open_at_end": false, "sell_reason": "roi", "profit": 0.00035588972431077615, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4084507042253522, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:30:00+00:00", "close_date": "2018-01-15 15:10:00+00:00", "trade_duration": 40, "open_rate": 0.04600501, "close_rate": 0.046235611553884705, "open_at_end": false, "sell_reason": "roi", "profit": 0.00023060155388470588, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.173676301776698, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:10:00+00:00", "close_date": "2018-01-15 19:25:00+00:00", "trade_duration": 75, "open_rate": 9.438e-05, "close_rate": 9.485308270676693e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.7308270676692514e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1059.5465140919687, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:35:00+00:00", "close_date": "2018-01-15 19:15:00+00:00", "trade_duration": 40, "open_rate": 0.03040001, "close_rate": 0.030552391002506264, "open_at_end": false, "sell_reason": "roi", "profit": 0.0001523810025062626, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.2894726021471703, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-15 20:25:00+00:00", "close_date": "2018-01-16 08:25:00+00:00", "trade_duration": 720, "open_rate": 5.837e-05, "close_rate": 5.2533e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -5.8369999999999985e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1713.2088401576154, "profit_abs": -0.010474999999999984}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 20:40:00+00:00", "close_date": "2018-01-15 22:00:00+00:00", "trade_duration": 80, "open_rate": 0.046036, "close_rate": 0.04626675689223057, "open_at_end": false, "sell_reason": "roi", "profit": 0.00023075689223057277, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1722130506560084, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 00:30:00+00:00", "close_date": "2018-01-16 01:10:00+00:00", "trade_duration": 40, "open_rate": 0.0028685, "close_rate": 0.0028828784461152877, "open_at_end": false, "sell_reason": "roi", "profit": 1.4378446115287727e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 34.86142583231654, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 01:15:00+00:00", "close_date": "2018-01-16 02:35:00+00:00", "trade_duration": 80, "open_rate": 0.06731755, "close_rate": 0.0676549813283208, "open_at_end": false, "sell_reason": "roi", "profit": 0.00033743132832080025, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4854967241083492, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 07:45:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 55, "open_rate": 0.09217614, "close_rate": 0.09263817578947368, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004620357894736804, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0848794492804754, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:55:00+00:00", "trade_duration": 20, "open_rate": 0.0165, "close_rate": 0.016913533834586467, "open_at_end": false, "sell_reason": "roi", "profit": 0.00041353383458646656, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.0606060606060606, "profit_abs": 0.0020000000000000018}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 5, "open_rate": 7.953e-05, "close_rate": 8.311781954887218e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.587819548872171e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1257.387149503332, "profit_abs": 0.00399999999999999}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 08:45:00+00:00", "close_date": "2018-01-16 09:50:00+00:00", "trade_duration": 65, "open_rate": 0.045202, "close_rate": 0.04542857644110275, "open_at_end": false, "sell_reason": "roi", "profit": 0.00022657644110275071, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2122914915269236, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:45:00+00:00", "trade_duration": 30, "open_rate": 5.248e-05, "close_rate": 5.326917293233082e-05, "open_at_end": false, "sell_reason": "roi", "profit": 7.891729323308177e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1905.487804878049, "profit_abs": 0.0010000000000000009}, {"pair": "XMR/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:55:00+00:00", "trade_duration": 40, "open_rate": 0.02892318, "close_rate": 0.02906815834586466, "open_at_end": false, "sell_reason": "roi", "profit": 0.0001449783458646603, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.457434486802627, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 09:50:00+00:00", "close_date": "2018-01-16 10:10:00+00:00", "trade_duration": 20, "open_rate": 5.158e-05, "close_rate": 5.287273182957392e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.2927318295739246e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1938.735944164405, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:35:00+00:00", "trade_duration": 30, "open_rate": 0.02828232, "close_rate": 0.02870761804511278, "open_at_end": false, "sell_reason": "roi", "profit": 0.00042529804511277913, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5357778286929786, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:40:00+00:00", "trade_duration": 35, "open_rate": 0.04357584, "close_rate": 0.044231115789473675, "open_at_end": false, "sell_reason": "roi", "profit": 0.0006552757894736777, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.294849623093898, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 13:45:00+00:00", "close_date": "2018-01-16 14:20:00+00:00", "trade_duration": 35, "open_rate": 5.362e-05, "close_rate": 5.442631578947368e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.063157894736843e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1864.975755315181, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 17:30:00+00:00", "close_date": "2018-01-16 18:25:00+00:00", "trade_duration": 55, "open_rate": 5.302e-05, "close_rate": 5.328576441102756e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.6576441102756397e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.0807242549984, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:45:00+00:00", "trade_duration": 30, "open_rate": 0.09129999, "close_rate": 0.09267292218045112, "open_at_end": false, "sell_reason": "roi", "profit": 0.0013729321804511196, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0952903718828448, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:35:00+00:00", "trade_duration": 20, "open_rate": 3.808e-05, "close_rate": 3.903438596491228e-05, "open_at_end": false, "sell_reason": "roi", "profit": 9.543859649122774e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2626.0504201680674, "profit_abs": 0.0020000000000000018}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 19:00:00+00:00", "close_date": "2018-01-16 19:30:00+00:00", "trade_duration": 30, "open_rate": 0.02811012, "close_rate": 0.028532828571428567, "open_at_end": false, "sell_reason": "roi", "profit": 0.00042270857142856846, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.557437677249333, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 60, "open_rate": 0.00258379, "close_rate": 0.002325411, "open_at_end": false, "sell_reason": "stop_loss", "profit": -0.000258379, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.702835756775904, "profit_abs": -0.010474999999999984}, {"pair": "NXT/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 80, "open_rate": 2.559e-05, "close_rate": 2.3031e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -2.5590000000000004e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3907.7764751856193, "profit_abs": -0.010474999999999998}, {"pair": "TRX/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:35:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 50, "open_rate": 7.62e-05, "close_rate": 6.858e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -7.619999999999998e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1312.3359580052495, "profit_abs": -0.010474999999999984}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:35:00+00:00", "trade_duration": 5, "open_rate": 0.00229844, "close_rate": 0.002402129022556391, "open_at_end": false, "sell_reason": "roi", "profit": 0.00010368902255639091, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 43.507770487809125, "profit_abs": 0.004000000000000017}, {"pair": "LTC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:40:00+00:00", "trade_duration": 10, "open_rate": 0.0151, "close_rate": 0.015781203007518795, "open_at_end": false, "sell_reason": "roi", "profit": 0.0006812030075187946, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.622516556291391, "profit_abs": 0.00399999999999999}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:40:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 5, "open_rate": 0.00235676, "close_rate": 0.00246308, "open_at_end": false, "sell_reason": "roi", "profit": 0.00010632000000000003, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 42.431134269081284, "profit_abs": 0.0040000000000000036}, {"pair": "DASH/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 22:45:00+00:00", "close_date": "2018-01-16 23:05:00+00:00", "trade_duration": 20, "open_rate": 0.0630692, "close_rate": 0.06464988170426066, "open_at_end": false, "sell_reason": "roi", "profit": 0.0015806817042606502, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.585559988076589, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:50:00+00:00", "close_date": "2018-01-16 22:55:00+00:00", "trade_duration": 5, "open_rate": 2.2e-05, "close_rate": 2.299248120300751e-05, "open_at_end": false, "sell_reason": "roi", "profit": 9.924812030075114e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 4545.454545454546, "profit_abs": 0.003999999999999976}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-17 03:30:00+00:00", "close_date": "2018-01-17 04:00:00+00:00", "trade_duration": 30, "open_rate": 4.974e-05, "close_rate": 5.048796992481203e-05, "open_at_end": false, "sell_reason": "roi", "profit": 7.479699248120277e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2010.454362685967, "profit_abs": 0.0010000000000000009}, {"pair": "TRX/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-17 03:55:00+00:00", "close_date": "2018-01-17 04:15:00+00:00", "trade_duration": 20, "open_rate": 7.108e-05, "close_rate": 7.28614536340852e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.7814536340851996e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1406.8655036578502, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 09:35:00+00:00", "close_date": "2018-01-17 10:15:00+00:00", "trade_duration": 40, "open_rate": 0.04327, "close_rate": 0.04348689223057644, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002168922305764362, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.3110700254217704, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:20:00+00:00", "close_date": "2018-01-17 17:00:00+00:00", "trade_duration": 400, "open_rate": 4.997e-05, "close_rate": 5.022047619047618e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.504761904761831e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2001.2007204322595, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:25:00+00:00", "trade_duration": 55, "open_rate": 0.06836818, "close_rate": 0.06871087764411027, "open_at_end": false, "sell_reason": "roi", "profit": 0.00034269764411026804, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4626687444363737, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:10:00+00:00", "trade_duration": 40, "open_rate": 3.63e-05, "close_rate": 3.648195488721804e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.8195488721804031e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2754.8209366391184, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:30:00+00:00", "close_date": "2018-01-17 22:05:00+00:00", "trade_duration": 575, "open_rate": 0.0281, "close_rate": 0.02824085213032581, "open_at_end": false, "sell_reason": "roi", "profit": 0.0001408521303258095, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5587188612099645, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:35:00+00:00", "close_date": "2018-01-17 16:55:00+00:00", "trade_duration": 260, "open_rate": 0.08651001, "close_rate": 0.08694364413533832, "open_at_end": false, "sell_reason": "roi", "profit": 0.00043363413533832607, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1559355963546878, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 05:00:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 55, "open_rate": 5.633e-05, "close_rate": 5.6612355889724306e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.8235588972430847e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1775.2529735487308, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 05:20:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 35, "open_rate": 0.06988494, "close_rate": 0.07093584135338346, "open_at_end": false, "sell_reason": "roi", "profit": 0.0010509013533834544, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.430923457900944, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 07:35:00+00:00", "close_date": "2018-01-18 08:15:00+00:00", "trade_duration": 40, "open_rate": 5.545e-05, "close_rate": 5.572794486215538e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.779448621553787e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1803.4265103697026, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 09:00:00+00:00", "close_date": "2018-01-18 09:40:00+00:00", "trade_duration": 40, "open_rate": 0.01633527, "close_rate": 0.016417151052631574, "open_at_end": false, "sell_reason": "roi", "profit": 8.188105263157511e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.121723118136401, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 16:40:00+00:00", "close_date": "2018-01-18 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.00269734, "close_rate": 0.002710860501253133, "open_at_end": false, "sell_reason": "roi", "profit": 1.3520501253133123e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.073561360451414, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-18 18:05:00+00:00", "close_date": "2018-01-18 18:30:00+00:00", "trade_duration": 25, "open_rate": 4.475e-05, "close_rate": 4.587155388471177e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.1215538847117757e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2234.63687150838, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 18:25:00+00:00", "close_date": "2018-01-18 18:55:00+00:00", "trade_duration": 30, "open_rate": 2.79e-05, "close_rate": 2.8319548872180444e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.1954887218044365e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3584.2293906810037, "profit_abs": 0.000999999999999987}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 20:10:00+00:00", "close_date": "2018-01-18 20:50:00+00:00", "trade_duration": 40, "open_rate": 0.04439326, "close_rate": 0.04461578260651629, "open_at_end": false, "sell_reason": "roi", "profit": 0.00022252260651629135, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2525942001105577, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 21:30:00+00:00", "close_date": "2018-01-19 00:35:00+00:00", "trade_duration": 185, "open_rate": 4.49e-05, "close_rate": 4.51250626566416e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.2506265664159932e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2227.1714922049, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 21:55:00+00:00", "close_date": "2018-01-19 05:05:00+00:00", "trade_duration": 430, "open_rate": 0.02855, "close_rate": 0.028693107769423555, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014310776942355607, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.502626970227671, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 22:10:00+00:00", "close_date": "2018-01-18 22:50:00+00:00", "trade_duration": 40, "open_rate": 5.796e-05, "close_rate": 5.8250526315789473e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.905263157894727e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1725.3278122843342, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 23:50:00+00:00", "close_date": "2018-01-19 00:30:00+00:00", "trade_duration": 40, "open_rate": 0.04340323, "close_rate": 0.04362079005012531, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002175600501253122, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.303975994413319, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-19 16:45:00+00:00", "close_date": "2018-01-19 17:35:00+00:00", "trade_duration": 50, "open_rate": 0.04454455, "close_rate": 0.04476783095238095, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002232809523809512, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.244943545282195, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:15:00+00:00", "close_date": "2018-01-19 19:55:00+00:00", "trade_duration": 160, "open_rate": 5.62e-05, "close_rate": 5.648170426065162e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.817042606516199e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1779.3594306049824, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:20:00+00:00", "close_date": "2018-01-19 20:15:00+00:00", "trade_duration": 175, "open_rate": 4.339e-05, "close_rate": 4.360749373433584e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.174937343358337e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2304.6784973496196, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-20 04:45:00+00:00", "close_date": "2018-01-20 17:35:00+00:00", "trade_duration": 770, "open_rate": 0.0001009, "close_rate": 0.00010140576441102755, "open_at_end": false, "sell_reason": "roi", "profit": 5.057644110275549e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 991.0802775024778, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 15:15:00+00:00", "trade_duration": 625, "open_rate": 0.00270505, "close_rate": 0.002718609147869674, "open_at_end": false, "sell_reason": "roi", "profit": 1.3559147869673764e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.96789338459548, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 07:00:00+00:00", "trade_duration": 130, "open_rate": 0.03000002, "close_rate": 0.030150396040100245, "open_at_end": false, "sell_reason": "roi", "profit": 0.00015037604010024672, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.3333311111125927, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 09:00:00+00:00", "close_date": "2018-01-20 09:40:00+00:00", "trade_duration": 40, "open_rate": 5.46e-05, "close_rate": 5.4873684210526304e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.736842105263053e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1831.5018315018317, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-20 18:25:00+00:00", "close_date": "2018-01-25 03:50:00+00:00", "trade_duration": 6325, "open_rate": 0.03082222, "close_rate": 0.027739998, "open_at_end": false, "sell_reason": "stop_loss", "profit": -0.0030822220000000025, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.244412634781012, "profit_abs": -0.010474999999999998}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 22:25:00+00:00", "close_date": "2018-01-20 23:15:00+00:00", "trade_duration": 50, "open_rate": 0.08969999, "close_rate": 0.09014961401002504, "open_at_end": false, "sell_reason": "roi", "profit": 0.00044962401002504593, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1148273260677064, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 02:50:00+00:00", "close_date": "2018-01-21 14:30:00+00:00", "trade_duration": 700, "open_rate": 0.01632501, "close_rate": 0.01640683962406015, "open_at_end": false, "sell_reason": "roi", "profit": 8.182962406014932e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.125570520324337, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 10:20:00+00:00", "close_date": "2018-01-21 11:00:00+00:00", "trade_duration": 40, "open_rate": 0.070538, "close_rate": 0.07089157393483708, "open_at_end": false, "sell_reason": "roi", "profit": 0.00035357393483707866, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.417675579120474, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 15:50:00+00:00", "close_date": "2018-01-21 18:45:00+00:00", "trade_duration": 175, "open_rate": 5.301e-05, "close_rate": 5.327571428571427e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.657142857142672e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.4365214110546, "profit_abs": -2.7755575615628914e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 16:20:00+00:00", "close_date": "2018-01-21 17:00:00+00:00", "trade_duration": 40, "open_rate": 3.955e-05, "close_rate": 3.9748245614035085e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.9824561403508552e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2528.4450063211125, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:45:00+00:00", "trade_duration": 30, "open_rate": 0.00258505, "close_rate": 0.002623922932330827, "open_at_end": false, "sell_reason": "roi", "profit": 3.8872932330826816e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.6839712964933, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:55:00+00:00", "trade_duration": 40, "open_rate": 3.903e-05, "close_rate": 3.922563909774435e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.9563909774435151e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2562.1316935690497, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 00:35:00+00:00", "close_date": "2018-01-22 10:35:00+00:00", "trade_duration": 600, "open_rate": 5.236e-05, "close_rate": 5.262245614035087e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.624561403508717e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1909.8548510313217, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 01:30:00+00:00", "close_date": "2018-01-22 02:10:00+00:00", "trade_duration": 40, "open_rate": 9.028e-05, "close_rate": 9.07325313283208e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.5253132832080657e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1107.6650420912717, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 12:25:00+00:00", "close_date": "2018-01-22 14:35:00+00:00", "trade_duration": 130, "open_rate": 0.002687, "close_rate": 0.002700468671679198, "open_at_end": false, "sell_reason": "roi", "profit": 1.3468671679197925e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.21622627465575, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 13:15:00+00:00", "close_date": "2018-01-22 13:55:00+00:00", "trade_duration": 40, "open_rate": 4.168e-05, "close_rate": 4.188892230576441e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.0892230576441054e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2399.232245681382, "profit_abs": 1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-22 14:00:00+00:00", "close_date": "2018-01-22 14:30:00+00:00", "trade_duration": 30, "open_rate": 8.821e-05, "close_rate": 8.953646616541353e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.326466165413529e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1133.6583153837435, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 15:55:00+00:00", "close_date": "2018-01-22 16:40:00+00:00", "trade_duration": 45, "open_rate": 5.172e-05, "close_rate": 5.1979248120300745e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.592481203007459e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1933.4880123743235, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-22 16:05:00+00:00", "close_date": "2018-01-22 16:25:00+00:00", "trade_duration": 20, "open_rate": 3.026e-05, "close_rate": 3.101839598997494e-05, "open_at_end": false, "sell_reason": "roi", "profit": 7.5839598997494e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3304.692663582287, "profit_abs": 0.0020000000000000157}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 19:50:00+00:00", "close_date": "2018-01-23 00:10:00+00:00", "trade_duration": 260, "open_rate": 0.07064, "close_rate": 0.07099408521303258, "open_at_end": false, "sell_reason": "roi", "profit": 0.00035408521303258167, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.415628539071348, "profit_abs": 1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 21:25:00+00:00", "close_date": "2018-01-22 22:05:00+00:00", "trade_duration": 40, "open_rate": 0.01644483, "close_rate": 0.01652726022556391, "open_at_end": false, "sell_reason": "roi", "profit": 8.243022556390922e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.080938507725528, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-23 00:05:00+00:00", "close_date": "2018-01-23 00:35:00+00:00", "trade_duration": 30, "open_rate": 4.331e-05, "close_rate": 4.3961278195488714e-05, "open_at_end": false, "sell_reason": "roi", "profit": 6.512781954887175e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2308.935580697299, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-23 01:50:00+00:00", "close_date": "2018-01-23 02:15:00+00:00", "trade_duration": 25, "open_rate": 3.2e-05, "close_rate": 3.2802005012531326e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.020050125313278e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3125.0000000000005, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 04:25:00+00:00", "close_date": "2018-01-23 05:15:00+00:00", "trade_duration": 50, "open_rate": 0.09167706, "close_rate": 0.09213659413533835, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004595341353383492, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0907854156754153, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 07:35:00+00:00", "close_date": "2018-01-23 09:00:00+00:00", "trade_duration": 85, "open_rate": 0.0692498, "close_rate": 0.06959691679197995, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003471167919799484, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4440474918339115, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 10:50:00+00:00", "close_date": "2018-01-23 13:05:00+00:00", "trade_duration": 135, "open_rate": 3.182e-05, "close_rate": 3.197949874686716e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.594987468671663e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3142.677561282213, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 11:05:00+00:00", "close_date": "2018-01-23 16:05:00+00:00", "trade_duration": 300, "open_rate": 0.04088, "close_rate": 0.04108491228070175, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002049122807017481, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4461839530332683, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 14:55:00+00:00", "close_date": "2018-01-23 15:35:00+00:00", "trade_duration": 40, "open_rate": 5.15e-05, "close_rate": 5.175814536340851e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.5814536340851513e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1941.747572815534, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 16:35:00+00:00", "close_date": "2018-01-24 00:05:00+00:00", "trade_duration": 450, "open_rate": 0.09071698, "close_rate": 0.09117170170426064, "open_at_end": false, "sell_reason": "roi", "profit": 0.00045472170426064107, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1023294646713329, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 17:25:00+00:00", "close_date": "2018-01-23 18:45:00+00:00", "trade_duration": 80, "open_rate": 3.128e-05, "close_rate": 3.1436791979949865e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5679197994986587e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3196.9309462915603, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 20:15:00+00:00", "close_date": "2018-01-23 22:00:00+00:00", "trade_duration": 105, "open_rate": 9.555e-05, "close_rate": 9.602894736842104e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.789473684210343e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1046.5724751439038, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 22:30:00+00:00", "close_date": "2018-01-23 23:10:00+00:00", "trade_duration": 40, "open_rate": 0.04080001, "close_rate": 0.0410045213283208, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020451132832080554, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.450979791426522, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 23:50:00+00:00", "close_date": "2018-01-24 03:35:00+00:00", "trade_duration": 225, "open_rate": 5.163e-05, "close_rate": 5.18887969924812e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.587969924812037e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1936.8584156498162, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 00:20:00+00:00", "close_date": "2018-01-24 01:50:00+00:00", "trade_duration": 90, "open_rate": 0.04040781, "close_rate": 0.04061035541353383, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002025454135338306, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.474769110228938, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 06:45:00+00:00", "close_date": "2018-01-24 07:25:00+00:00", "trade_duration": 40, "open_rate": 5.132e-05, "close_rate": 5.157724310776942e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.5724310776941724e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1948.5580670303975, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-24 14:15:00+00:00", "close_date": "2018-01-24 14:25:00+00:00", "trade_duration": 10, "open_rate": 5.198e-05, "close_rate": 5.432496240601503e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.344962406015033e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1923.8168526356292, "profit_abs": 0.0040000000000000036}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 14:50:00+00:00", "close_date": "2018-01-24 16:35:00+00:00", "trade_duration": 105, "open_rate": 3.054e-05, "close_rate": 3.069308270676692e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5308270676691466e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3274.3942370661425, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 15:10:00+00:00", "close_date": "2018-01-24 16:15:00+00:00", "trade_duration": 65, "open_rate": 9.263e-05, "close_rate": 9.309431077694236e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.6431077694236234e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1079.5638562020945, "profit_abs": 2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 22:40:00+00:00", "close_date": "2018-01-24 23:25:00+00:00", "trade_duration": 45, "open_rate": 5.514e-05, "close_rate": 5.54163909774436e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.7639097744360576e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1813.5654697134569, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 00:50:00+00:00", "close_date": "2018-01-25 01:30:00+00:00", "trade_duration": 40, "open_rate": 4.921e-05, "close_rate": 4.9456666666666664e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.4666666666666543e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.1072952651903, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 08:15:00+00:00", "close_date": "2018-01-25 12:15:00+00:00", "trade_duration": 240, "open_rate": 0.0026, "close_rate": 0.002613032581453634, "open_at_end": false, "sell_reason": "roi", "profit": 1.3032581453634e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.46153846153847, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 10:25:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 350, "open_rate": 0.02799871, "close_rate": 0.028139054411027563, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014034441102756326, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.571593119825878, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 11:00:00+00:00", "close_date": "2018-01-25 11:45:00+00:00", "trade_duration": 45, "open_rate": 0.04078902, "close_rate": 0.0409934762406015, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020445624060149575, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4516401717913303, "profit_abs": -1.3877787807814457e-17}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:05:00+00:00", "close_date": "2018-01-25 13:45:00+00:00", "trade_duration": 40, "open_rate": 2.89e-05, "close_rate": 2.904486215538847e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.4486215538846723e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3460.2076124567475, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:20:00+00:00", "close_date": "2018-01-25 14:05:00+00:00", "trade_duration": 45, "open_rate": 0.041103, "close_rate": 0.04130903007518797, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020603007518796984, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4329124394813033, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-25 15:45:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 30, "open_rate": 5.428e-05, "close_rate": 5.509624060150376e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.162406015037611e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1842.2991893883568, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 17:45:00+00:00", "close_date": "2018-01-25 23:15:00+00:00", "trade_duration": 330, "open_rate": 5.414e-05, "close_rate": 5.441137844611528e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.713784461152774e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1847.063169560399, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 21:15:00+00:00", "close_date": "2018-01-25 21:55:00+00:00", "trade_duration": 40, "open_rate": 0.04140777, "close_rate": 0.0416153277443609, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002075577443608964, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.415005686130888, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 02:05:00+00:00", "close_date": "2018-01-26 02:45:00+00:00", "trade_duration": 40, "open_rate": 0.00254309, "close_rate": 0.002555837318295739, "open_at_end": false, "sell_reason": "roi", "profit": 1.2747318295739177e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.32224183965177, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 02:55:00+00:00", "close_date": "2018-01-26 15:10:00+00:00", "trade_duration": 735, "open_rate": 5.607e-05, "close_rate": 5.6351052631578935e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.810526315789381e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1783.4849295523454, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 06:10:00+00:00", "close_date": "2018-01-26 09:25:00+00:00", "trade_duration": 195, "open_rate": 0.00253806, "close_rate": 0.0025507821052631577, "open_at_end": false, "sell_reason": "roi", "profit": 1.2722105263157733e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.400171784748984, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 07:25:00+00:00", "close_date": "2018-01-26 09:55:00+00:00", "trade_duration": 150, "open_rate": 0.0415, "close_rate": 0.04170802005012531, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020802005012530989, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4096385542168677, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-26 09:55:00+00:00", "close_date": "2018-01-26 10:25:00+00:00", "trade_duration": 30, "open_rate": 5.321e-05, "close_rate": 5.401015037593984e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.00150375939842e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1879.3459875963165, "profit_abs": 0.000999999999999987}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 16:05:00+00:00", "close_date": "2018-01-26 16:45:00+00:00", "trade_duration": 40, "open_rate": 0.02772046, "close_rate": 0.02785940967418546, "open_at_end": false, "sell_reason": "roi", "profit": 0.00013894967418546025, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.6074437437185387, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 23:35:00+00:00", "close_date": "2018-01-27 00:15:00+00:00", "trade_duration": 40, "open_rate": 0.09461341, "close_rate": 0.09508766268170424, "open_at_end": false, "sell_reason": "roi", "profit": 0.00047425268170424306, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0569326272036914, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 00:35:00+00:00", "close_date": "2018-01-27 01:30:00+00:00", "trade_duration": 55, "open_rate": 5.615e-05, "close_rate": 5.643145363408521e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.814536340852038e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1780.9439002671415, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.07877175, "open_date": "2018-01-27 00:45:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 4560, "open_rate": 5.556e-05, "close_rate": 5.144e-05, "open_at_end": true, "sell_reason": "force_sell", "profit": -4.120000000000001e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1799.8560115190785, "profit_abs": -0.007896868250539965}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 02:30:00+00:00", "close_date": "2018-01-27 11:25:00+00:00", "trade_duration": 535, "open_rate": 0.06900001, "close_rate": 0.06934587471177944, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003458647117794422, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492751522789635, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 06:25:00+00:00", "close_date": "2018-01-27 07:05:00+00:00", "trade_duration": 40, "open_rate": 0.09449985, "close_rate": 0.0949735334586466, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004736834586466093, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.058202737887944, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.04815133, "open_date": "2018-01-27 09:40:00+00:00", "close_date": "2018-01-30 04:40:00+00:00", "trade_duration": 4020, "open_rate": 0.0410697, "close_rate": 0.03928809, "open_at_end": true, "sell_reason": "force_sell", "profit": -0.001781610000000003, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4348850855983852, "profit_abs": -0.004827170578309559}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 11:45:00+00:00", "close_date": "2018-01-27 12:30:00+00:00", "trade_duration": 45, "open_rate": 0.0285, "close_rate": 0.02864285714285714, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014285714285713902, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5087719298245617, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 12:35:00+00:00", "close_date": "2018-01-27 15:25:00+00:00", "trade_duration": 170, "open_rate": 0.02866372, "close_rate": 0.02880739779448621, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014367779448621124, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.4887307020861216, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 15:50:00+00:00", "close_date": "2018-01-27 16:50:00+00:00", "trade_duration": 60, "open_rate": 0.095381, "close_rate": 0.09585910025062656, "open_at_end": false, "sell_reason": "roi", "profit": 0.00047810025062657024, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0484268355332824, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 17:05:00+00:00", "close_date": "2018-01-27 17:45:00+00:00", "trade_duration": 40, "open_rate": 0.06759092, "close_rate": 0.06792972160401002, "open_at_end": false, "sell_reason": "roi", "profit": 0.00033880160401002224, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4794886650455417, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 23:40:00+00:00", "close_date": "2018-01-28 01:05:00+00:00", "trade_duration": 85, "open_rate": 0.00258501, "close_rate": 0.002597967443609022, "open_at_end": false, "sell_reason": "roi", "profit": 1.2957443609021985e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.684569885609726, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 02:25:00+00:00", "close_date": "2018-01-28 08:10:00+00:00", "trade_duration": 345, "open_rate": 0.06698502, "close_rate": 0.0673207845112782, "open_at_end": false, "sell_reason": "roi", "profit": 0.00033576451127818874, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4928710926711672, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 10:25:00+00:00", "close_date": "2018-01-28 16:30:00+00:00", "trade_duration": 365, "open_rate": 0.0677177, "close_rate": 0.06805713709273183, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003394370927318202, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4767187899175547, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-28 20:35:00+00:00", "close_date": "2018-01-28 21:35:00+00:00", "trade_duration": 60, "open_rate": 5.215e-05, "close_rate": 5.2411403508771925e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.6140350877192417e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1917.5455417066157, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-28 22:00:00+00:00", "close_date": "2018-01-28 22:30:00+00:00", "trade_duration": 30, "open_rate": 0.00273809, "close_rate": 0.002779264285714285, "open_at_end": false, "sell_reason": "roi", "profit": 4.117428571428529e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.5218089982433, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-29 00:00:00+00:00", "close_date": "2018-01-29 00:30:00+00:00", "trade_duration": 30, "open_rate": 0.00274632, "close_rate": 0.002787618045112782, "open_at_end": false, "sell_reason": "roi", "profit": 4.129804511278194e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.412362725392526, "profit_abs": 0.0010000000000000148}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-29 02:15:00+00:00", "close_date": "2018-01-29 03:00:00+00:00", "trade_duration": 45, "open_rate": 0.01622478, "close_rate": 0.016306107218045113, "open_at_end": false, "sell_reason": "roi", "profit": 8.132721804511231e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.163411768911504, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 03:05:00+00:00", "close_date": "2018-01-29 03:45:00+00:00", "trade_duration": 40, "open_rate": 0.069, "close_rate": 0.06934586466165413, "open_at_end": false, "sell_reason": "roi", "profit": 0.00034586466165412166, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492753623188406, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 05:20:00+00:00", "close_date": "2018-01-29 06:55:00+00:00", "trade_duration": 95, "open_rate": 8.755e-05, "close_rate": 8.798884711779448e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.3884711779447504e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1142.204454597373, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 07:00:00+00:00", "close_date": "2018-01-29 19:25:00+00:00", "trade_duration": 745, "open_rate": 0.06825763, "close_rate": 0.06859977350877192, "open_at_end": false, "sell_reason": "roi", "profit": 0.00034214350877191657, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4650376815016872, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 19:45:00+00:00", "close_date": "2018-01-29 20:25:00+00:00", "trade_duration": 40, "open_rate": 0.06713892, "close_rate": 0.06747545593984962, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003365359398496137, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4894490408841845, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0199116, "open_date": "2018-01-29 23:30:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 315, "open_rate": 8.934e-05, "close_rate": 8.8e-05, "open_at_end": true, "sell_reason": "force_sell", "profit": -1.3399999999999973e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1119.3194537721067, "profit_abs": -0.0019961383478844796}], "results_per_pair": [{"key": "TRX/BTC", "trades": 15, "profit_mean": 0.0023467073333333323, "profit_mean_pct": 0.23467073333333321, "profit_sum": 0.035200609999999986, "profit_sum_pct": 3.5200609999999988, "profit_total_abs": 0.0035288616521155086, "profit_total_pct": 1.1733536666666662, "duration_avg": "2:28:00", "wins": 9, "draws": 2, "losses": 4}, {"key": "ADA/BTC", "trades": 29, "profit_mean": -0.0011598141379310352, "profit_mean_pct": -0.11598141379310352, "profit_sum": -0.03363461000000002, "profit_sum_pct": -3.3634610000000023, "profit_total_abs": -0.0033718682505400333, "profit_total_pct": -1.1211536666666675, "duration_avg": "5:35:00", "wins": 9, "draws": 11, "losses": 9}, {"key": "XLM/BTC", "trades": 21, "profit_mean": 0.0026243899999999994, "profit_mean_pct": 0.2624389999999999, "profit_sum": 0.05511218999999999, "profit_sum_pct": 5.511218999999999, "profit_total_abs": 0.005525000000000002, "profit_total_pct": 1.8370729999999995, "duration_avg": "3:21:00", "wins": 12, "draws": 3, "losses": 6}, {"key": "ETH/BTC", "trades": 21, "profit_mean": 0.0009500057142857142, "profit_mean_pct": 0.09500057142857142, "profit_sum": 0.01995012, "profit_sum_pct": 1.9950119999999998, "profit_total_abs": 0.0019999999999999463, "profit_total_pct": 0.6650039999999999, "duration_avg": "2:17:00", "wins": 5, "draws": 10, "losses": 6}, {"key": "XMR/BTC", "trades": 16, "profit_mean": -0.0027899012500000007, "profit_mean_pct": -0.2789901250000001, "profit_sum": -0.04463842000000001, "profit_sum_pct": -4.463842000000001, "profit_total_abs": -0.0044750000000000345, "profit_total_pct": -1.4879473333333337, "duration_avg": "8:41:00", "wins": 6, "draws": 5, "losses": 5}, {"key": "ZEC/BTC", "trades": 21, "profit_mean": -0.00039290904761904774, "profit_mean_pct": -0.03929090476190478, "profit_sum": -0.008251090000000003, "profit_sum_pct": -0.8251090000000003, "profit_total_abs": -0.000827170578309569, "profit_total_pct": -0.27503633333333344, "duration_avg": "4:17:00", "wins": 8, "draws": 7, "losses": 6}, {"key": "NXT/BTC", "trades": 12, "profit_mean": -0.0012261025000000006, "profit_mean_pct": -0.12261025000000006, "profit_sum": -0.014713230000000008, "profit_sum_pct": -1.4713230000000008, "profit_total_abs": -0.0014750000000000874, "profit_total_pct": -0.4904410000000003, "duration_avg": "0:57:00", "wins": 4, "draws": 3, "losses": 5}, {"key": "LTC/BTC", "trades": 8, "profit_mean": 0.00748129625, "profit_mean_pct": 0.748129625, "profit_sum": 0.05985037, "profit_sum_pct": 5.985037, "profit_total_abs": 0.006000000000000019, "profit_total_pct": 1.9950123333333334, "duration_avg": "1:59:00", "wins": 5, "draws": 2, "losses": 1}, {"key": "ETC/BTC", "trades": 20, "profit_mean": 0.0022568569999999997, "profit_mean_pct": 0.22568569999999996, "profit_sum": 0.04513713999999999, "profit_sum_pct": 4.513713999999999, "profit_total_abs": 0.004525000000000001, "profit_total_pct": 1.504571333333333, "duration_avg": "1:45:00", "wins": 11, "draws": 4, "losses": 5}, {"key": "DASH/BTC", "trades": 16, "profit_mean": 0.0018703237499999997, "profit_mean_pct": 0.18703237499999997, "profit_sum": 0.029925179999999996, "profit_sum_pct": 2.9925179999999996, "profit_total_abs": 0.002999999999999961, "profit_total_pct": 0.9975059999999999, "duration_avg": "3:03:00", "wins": 4, "draws": 7, "losses": 5}, {"key": "TOTAL", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}], "sell_reason_summary": [{"sell_reason": "roi", "trades": 170, "wins": 73, "draws": 54, "losses": 43, "profit_mean": 0.005398268352941177, "profit_mean_pct": 0.54, "profit_sum": 0.91770562, "profit_sum_pct": 91.77, "profit_total_abs": 0.09199999999999964, "profit_pct_total": 30.59}, {"sell_reason": "stop_loss", "trades": 6, "wins": 0, "draws": 0, "losses": 6, "profit_mean": -0.10448878000000002, "profit_mean_pct": -10.45, "profit_sum": -0.6269326800000001, "profit_sum_pct": -62.69, "profit_total_abs": -0.06284999999999992, "profit_pct_total": -20.9}, {"sell_reason": "force_sell", "trades": 3, "wins": 0, "draws": 0, "losses": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.89, "profit_sum": -0.14683468, "profit_sum_pct": -14.68, "profit_total_abs": -0.014720177176734003, "profit_pct_total": -4.89}], "left_open_trades": [{"key": "TRX/BTC", "trades": 1, "profit_mean": -0.0199116, "profit_mean_pct": -1.9911600000000003, "profit_sum": -0.0199116, "profit_sum_pct": -1.9911600000000003, "profit_total_abs": -0.0019961383478844796, "profit_total_pct": -0.6637200000000001, "duration_avg": "5:15:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ADA/BTC", "trades": 1, "profit_mean": -0.07877175, "profit_mean_pct": -7.877175, "profit_sum": -0.07877175, "profit_sum_pct": -7.877175, "profit_total_abs": -0.007896868250539965, "profit_total_pct": -2.625725, "duration_avg": "3 days, 4:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ZEC/BTC", "trades": 1, "profit_mean": -0.04815133, "profit_mean_pct": -4.815133, "profit_sum": -0.04815133, "profit_sum_pct": -4.815133, "profit_total_abs": -0.004827170578309559, "profit_total_pct": -1.6050443333333335, "duration_avg": "2 days, 19:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "TOTAL", "trades": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.894489333333333, "profit_sum": -0.14683468, "profit_sum_pct": -14.683468, "profit_total_abs": -0.014720177176734003, "profit_total_pct": -4.8944893333333335, "duration_avg": "2 days, 1:25:00", "wins": 0, "draws": 0, "losses": 3}], "total_trades": 179, "backtest_start": "2018-01-30 04:45:00+00:00", "backtest_start_ts": 1517287500, "backtest_end": "2018-01-30 04:45:00+00:00", "backtest_end_ts": 1517287500, "backtest_days": 0, "trades_per_day": null, "market_change": 0.25, "stake_amount": 0.1, "max_drawdown": 0.21142322000000008, "drawdown_start": "2018-01-24 14:25:00+00:00", "drawdown_start_ts": 1516803900.0, "drawdown_end": "2018-01-30 04:45:00+00:00", "drawdown_end_ts": 1517287500.0}, "TestStrategy": {"trades": [{"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:20:00+00:00", "trade_duration": 5, "open_rate": 9.64e-05, "close_rate": 0.00010074887218045112, "open_at_end": false, "sell_reason": "roi", "profit": 4.348872180451118e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1037.344398340249, "profit_abs": 0.00399999999999999}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:30:00+00:00", "trade_duration": 15, "open_rate": 4.756e-05, "close_rate": 4.9705563909774425e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.1455639097744267e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2102.6072329688814, "profit_abs": 0.00399999999999999}, {"pair": "XLM/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:35:00+00:00", "trade_duration": 10, "open_rate": 3.339e-05, "close_rate": 3.489631578947368e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.506315789473681e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2994.908655286014, "profit_abs": 0.0040000000000000036}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:40:00+00:00", "trade_duration": 15, "open_rate": 9.696e-05, "close_rate": 0.00010133413533834584, "open_at_end": false, "sell_reason": "roi", "profit": 4.3741353383458455e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1031.3531353135315, "profit_abs": 0.00399999999999999}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 07:35:00+00:00", "close_date": "2018-01-10 08:35:00+00:00", "trade_duration": 60, "open_rate": 0.0943, "close_rate": 0.09477268170426063, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004726817042606385, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0604453870625663, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 07:40:00+00:00", "close_date": "2018-01-10 08:10:00+00:00", "trade_duration": 30, "open_rate": 0.02719607, "close_rate": 0.02760503345864661, "open_at_end": false, "sell_reason": "roi", "profit": 0.00040896345864661204, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.677001860930642, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 08:15:00+00:00", "close_date": "2018-01-10 09:55:00+00:00", "trade_duration": 100, "open_rate": 0.04634952, "close_rate": 0.046581848421052625, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002323284210526272, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1575196463739, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 14:45:00+00:00", "close_date": "2018-01-10 15:50:00+00:00", "trade_duration": 65, "open_rate": 3.066e-05, "close_rate": 3.081368421052631e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5368421052630647e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3261.5786040443577, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 16:35:00+00:00", "close_date": "2018-01-10 17:15:00+00:00", "trade_duration": 40, "open_rate": 0.0168999, "close_rate": 0.016984611278195488, "open_at_end": false, "sell_reason": "roi", "profit": 8.471127819548868e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.917194776300452, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 16:40:00+00:00", "close_date": "2018-01-10 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.09132568, "close_rate": 0.0917834528320802, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004577728320801916, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0949822656672252, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 18:50:00+00:00", "close_date": "2018-01-10 19:45:00+00:00", "trade_duration": 55, "open_rate": 0.08898003, "close_rate": 0.08942604518796991, "open_at_end": false, "sell_reason": "roi", "profit": 0.00044601518796991146, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1238476768326557, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 22:15:00+00:00", "close_date": "2018-01-10 23:00:00+00:00", "trade_duration": 45, "open_rate": 0.08560008, "close_rate": 0.08602915308270676, "open_at_end": false, "sell_reason": "roi", "profit": 0.00042907308270676014, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1682232072680307, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 22:50:00+00:00", "close_date": "2018-01-10 23:20:00+00:00", "trade_duration": 30, "open_rate": 0.00249083, "close_rate": 0.0025282860902255634, "open_at_end": false, "sell_reason": "roi", "profit": 3.745609022556351e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 40.147260150231055, "profit_abs": 0.000999999999999987}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 23:15:00+00:00", "close_date": "2018-01-11 00:15:00+00:00", "trade_duration": 60, "open_rate": 3.022e-05, "close_rate": 3.037147869674185e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5147869674185174e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3309.0668431502318, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-10 23:40:00+00:00", "close_date": "2018-01-11 00:05:00+00:00", "trade_duration": 25, "open_rate": 0.002437, "close_rate": 0.0024980776942355883, "open_at_end": false, "sell_reason": "roi", "profit": 6.107769423558838e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.03405826836274, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 00:00:00+00:00", "close_date": "2018-01-11 00:35:00+00:00", "trade_duration": 35, "open_rate": 0.04771803, "close_rate": 0.04843559436090225, "open_at_end": false, "sell_reason": "roi", "profit": 0.0007175643609022495, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0956439316543456, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-11 03:40:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 45, "open_rate": 3.651e-05, "close_rate": 3.2859000000000005e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -3.650999999999996e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2738.9756231169545, "profit_abs": -0.01047499999999997}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 03:55:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 30, "open_rate": 0.08824105, "close_rate": 0.08956798308270676, "open_at_end": false, "sell_reason": "roi", "profit": 0.0013269330827067605, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1332594070446804, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 04:00:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 50, "open_rate": 0.00243, "close_rate": 0.002442180451127819, "open_at_end": false, "sell_reason": "roi", "profit": 1.2180451127819219e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.1522633744856, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:55:00+00:00", "trade_duration": 25, "open_rate": 0.04545064, "close_rate": 0.046589753784461146, "open_at_end": false, "sell_reason": "roi", "profit": 0.001139113784461146, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.200189040242338, "profit_abs": 0.001999999999999988}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 20, "open_rate": 3.372e-05, "close_rate": 3.456511278195488e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.4511278195488e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2965.599051008304, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:55:00+00:00", "close_date": "2018-01-11 05:15:00+00:00", "trade_duration": 20, "open_rate": 0.02644, "close_rate": 0.02710265664160401, "open_at_end": false, "sell_reason": "roi", "profit": 0.0006626566416040071, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7821482602118004, "profit_abs": 0.001999999999999988}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:20:00+00:00", "close_date": "2018-01-11 12:00:00+00:00", "trade_duration": 40, "open_rate": 0.08812, "close_rate": 0.08856170426065162, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004417042606516125, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1348161597821154, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:35:00+00:00", "close_date": "2018-01-11 12:15:00+00:00", "trade_duration": 40, "open_rate": 0.02683577, "close_rate": 0.026970285137844607, "open_at_end": false, "sell_reason": "roi", "profit": 0.00013451513784460897, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7263696923919087, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 14:00:00+00:00", "close_date": "2018-01-11 14:25:00+00:00", "trade_duration": 25, "open_rate": 4.919e-05, "close_rate": 5.04228320802005e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.232832080200495e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.9335230737956, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 19:25:00+00:00", "close_date": "2018-01-11 20:35:00+00:00", "trade_duration": 70, "open_rate": 0.08784896, "close_rate": 0.08828930566416039, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004403456641603881, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1383174029607181, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:35:00+00:00", "close_date": "2018-01-11 23:30:00+00:00", "trade_duration": 55, "open_rate": 5.105e-05, "close_rate": 5.130588972431077e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.558897243107704e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1958.8638589618022, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:25:00+00:00", "trade_duration": 30, "open_rate": 3.96e-05, "close_rate": 4.019548872180451e-05, "open_at_end": false, "sell_reason": "roi", "profit": 5.954887218045116e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2525.252525252525, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:35:00+00:00", "trade_duration": 40, "open_rate": 2.885e-05, "close_rate": 2.899461152882205e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.4461152882205115e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3466.204506065858, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 23:30:00+00:00", "close_date": "2018-01-12 00:05:00+00:00", "trade_duration": 35, "open_rate": 0.02645, "close_rate": 0.026847744360902256, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003977443609022545, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.780718336483932, "profit_abs": 0.0010000000000000148}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 23:55:00+00:00", "close_date": "2018-01-12 01:15:00+00:00", "trade_duration": 80, "open_rate": 0.048, "close_rate": 0.04824060150375939, "open_at_end": false, "sell_reason": "roi", "profit": 0.00024060150375938838, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0833333333333335, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-12 21:15:00+00:00", "close_date": "2018-01-12 21:40:00+00:00", "trade_duration": 25, "open_rate": 4.692e-05, "close_rate": 4.809593984962405e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.1759398496240516e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2131.287297527707, "profit_abs": 0.001999999999999974}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 00:55:00+00:00", "close_date": "2018-01-13 06:20:00+00:00", "trade_duration": 325, "open_rate": 0.00256966, "close_rate": 0.0025825405012531327, "open_at_end": false, "sell_reason": "roi", "profit": 1.2880501253132587e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.91565421106294, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 10:55:00+00:00", "close_date": "2018-01-13 11:35:00+00:00", "trade_duration": 40, "open_rate": 6.262e-05, "close_rate": 6.293388471177944e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.138847117794446e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1596.933886937081, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 13:05:00+00:00", "close_date": "2018-01-15 14:10:00+00:00", "trade_duration": 2945, "open_rate": 4.73e-05, "close_rate": 4.753709273182957e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.3709273182957117e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2114.1649048625795, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:30:00+00:00", "close_date": "2018-01-13 14:45:00+00:00", "trade_duration": 75, "open_rate": 6.063e-05, "close_rate": 6.0933909774436085e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.039097744360846e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1649.348507339601, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:40:00+00:00", "close_date": "2018-01-13 23:30:00+00:00", "trade_duration": 590, "open_rate": 0.00011082, "close_rate": 0.00011137548872180448, "open_at_end": false, "sell_reason": "roi", "profit": 5.554887218044781e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 902.3641941887746, "profit_abs": -2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 15:15:00+00:00", "close_date": "2018-01-13 15:55:00+00:00", "trade_duration": 40, "open_rate": 5.93e-05, "close_rate": 5.9597243107769415e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.9724310776941686e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1686.3406408094436, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 16:30:00+00:00", "close_date": "2018-01-13 17:10:00+00:00", "trade_duration": 40, "open_rate": 0.04850003, "close_rate": 0.04874313791979949, "open_at_end": false, "sell_reason": "roi", "profit": 0.00024310791979949287, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0618543947292407, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 22:05:00+00:00", "close_date": "2018-01-14 06:25:00+00:00", "trade_duration": 500, "open_rate": 0.09825019, "close_rate": 0.09874267215538848, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004924821553884823, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0178097365511456, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 00:20:00+00:00", "close_date": "2018-01-14 22:55:00+00:00", "trade_duration": 1355, "open_rate": 6.018e-05, "close_rate": 6.048165413533834e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.0165413533833987e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1661.681621801263, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 12:45:00+00:00", "close_date": "2018-01-14 13:25:00+00:00", "trade_duration": 40, "open_rate": 0.09758999, "close_rate": 0.0980791628822055, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004891728822054991, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.024695258191952, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-14 15:30:00+00:00", "close_date": "2018-01-14 16:00:00+00:00", "trade_duration": 30, "open_rate": 0.00311, "close_rate": 0.0031567669172932328, "open_at_end": false, "sell_reason": "roi", "profit": 4.676691729323286e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.154340836012864, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 20:45:00+00:00", "close_date": "2018-01-14 22:15:00+00:00", "trade_duration": 90, "open_rate": 0.00312401, "close_rate": 0.003139669197994987, "open_at_end": false, "sell_reason": "roi", "profit": 1.5659197994987058e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.010140812609436, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 23:35:00+00:00", "close_date": "2018-01-15 00:30:00+00:00", "trade_duration": 55, "open_rate": 0.0174679, "close_rate": 0.017555458395989976, "open_at_end": false, "sell_reason": "roi", "profit": 8.755839598997492e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.724786608579165, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 23:45:00+00:00", "close_date": "2018-01-15 00:25:00+00:00", "trade_duration": 40, "open_rate": 0.07346846, "close_rate": 0.07383672295739348, "open_at_end": false, "sell_reason": "roi", "profit": 0.00036826295739347814, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.3611282991367997, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 02:25:00+00:00", "close_date": "2018-01-15 03:05:00+00:00", "trade_duration": 40, "open_rate": 0.097994, "close_rate": 0.09848519799498744, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004911979949874384, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.020470641059657, "profit_abs": -2.7755575615628914e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 07:20:00+00:00", "close_date": "2018-01-15 08:00:00+00:00", "trade_duration": 40, "open_rate": 0.09659, "close_rate": 0.09707416040100247, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004841604010024786, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0353038616834043, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-15 08:20:00+00:00", "close_date": "2018-01-15 08:55:00+00:00", "trade_duration": 35, "open_rate": 9.987e-05, "close_rate": 0.00010137180451127818, "open_at_end": false, "sell_reason": "roi", "profit": 1.501804511278178e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1001.3016921998599, "profit_abs": 0.0010000000000000009}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-15 12:10:00+00:00", "close_date": "2018-01-16 02:50:00+00:00", "trade_duration": 880, "open_rate": 0.0948969, "close_rate": 0.09537257368421052, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004756736842105175, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0537752023511833, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:10:00+00:00", "close_date": "2018-01-15 17:40:00+00:00", "trade_duration": 210, "open_rate": 0.071, "close_rate": 0.07135588972431077, "open_at_end": false, "sell_reason": "roi", "profit": 0.00035588972431077615, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4084507042253522, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:30:00+00:00", "close_date": "2018-01-15 15:10:00+00:00", "trade_duration": 40, "open_rate": 0.04600501, "close_rate": 0.046235611553884705, "open_at_end": false, "sell_reason": "roi", "profit": 0.00023060155388470588, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.173676301776698, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:10:00+00:00", "close_date": "2018-01-15 19:25:00+00:00", "trade_duration": 75, "open_rate": 9.438e-05, "close_rate": 9.485308270676693e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.7308270676692514e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1059.5465140919687, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:35:00+00:00", "close_date": "2018-01-15 19:15:00+00:00", "trade_duration": 40, "open_rate": 0.03040001, "close_rate": 0.030552391002506264, "open_at_end": false, "sell_reason": "roi", "profit": 0.0001523810025062626, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.2894726021471703, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-15 20:25:00+00:00", "close_date": "2018-01-16 08:25:00+00:00", "trade_duration": 720, "open_rate": 5.837e-05, "close_rate": 5.2533e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -5.8369999999999985e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1713.2088401576154, "profit_abs": -0.010474999999999984}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 20:40:00+00:00", "close_date": "2018-01-15 22:00:00+00:00", "trade_duration": 80, "open_rate": 0.046036, "close_rate": 0.04626675689223057, "open_at_end": false, "sell_reason": "roi", "profit": 0.00023075689223057277, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1722130506560084, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 00:30:00+00:00", "close_date": "2018-01-16 01:10:00+00:00", "trade_duration": 40, "open_rate": 0.0028685, "close_rate": 0.0028828784461152877, "open_at_end": false, "sell_reason": "roi", "profit": 1.4378446115287727e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 34.86142583231654, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 01:15:00+00:00", "close_date": "2018-01-16 02:35:00+00:00", "trade_duration": 80, "open_rate": 0.06731755, "close_rate": 0.0676549813283208, "open_at_end": false, "sell_reason": "roi", "profit": 0.00033743132832080025, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4854967241083492, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 07:45:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 55, "open_rate": 0.09217614, "close_rate": 0.09263817578947368, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004620357894736804, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0848794492804754, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:55:00+00:00", "trade_duration": 20, "open_rate": 0.0165, "close_rate": 0.016913533834586467, "open_at_end": false, "sell_reason": "roi", "profit": 0.00041353383458646656, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.0606060606060606, "profit_abs": 0.0020000000000000018}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 5, "open_rate": 7.953e-05, "close_rate": 8.311781954887218e-05, "open_at_end": false, "sell_reason": "roi", "profit": 3.587819548872171e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1257.387149503332, "profit_abs": 0.00399999999999999}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 08:45:00+00:00", "close_date": "2018-01-16 09:50:00+00:00", "trade_duration": 65, "open_rate": 0.045202, "close_rate": 0.04542857644110275, "open_at_end": false, "sell_reason": "roi", "profit": 0.00022657644110275071, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2122914915269236, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:45:00+00:00", "trade_duration": 30, "open_rate": 5.248e-05, "close_rate": 5.326917293233082e-05, "open_at_end": false, "sell_reason": "roi", "profit": 7.891729323308177e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1905.487804878049, "profit_abs": 0.0010000000000000009}, {"pair": "XMR/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:55:00+00:00", "trade_duration": 40, "open_rate": 0.02892318, "close_rate": 0.02906815834586466, "open_at_end": false, "sell_reason": "roi", "profit": 0.0001449783458646603, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.457434486802627, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 09:50:00+00:00", "close_date": "2018-01-16 10:10:00+00:00", "trade_duration": 20, "open_rate": 5.158e-05, "close_rate": 5.287273182957392e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.2927318295739246e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1938.735944164405, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:35:00+00:00", "trade_duration": 30, "open_rate": 0.02828232, "close_rate": 0.02870761804511278, "open_at_end": false, "sell_reason": "roi", "profit": 0.00042529804511277913, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5357778286929786, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:40:00+00:00", "trade_duration": 35, "open_rate": 0.04357584, "close_rate": 0.044231115789473675, "open_at_end": false, "sell_reason": "roi", "profit": 0.0006552757894736777, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.294849623093898, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 13:45:00+00:00", "close_date": "2018-01-16 14:20:00+00:00", "trade_duration": 35, "open_rate": 5.362e-05, "close_rate": 5.442631578947368e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.063157894736843e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1864.975755315181, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 17:30:00+00:00", "close_date": "2018-01-16 18:25:00+00:00", "trade_duration": 55, "open_rate": 5.302e-05, "close_rate": 5.328576441102756e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.6576441102756397e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.0807242549984, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:45:00+00:00", "trade_duration": 30, "open_rate": 0.09129999, "close_rate": 0.09267292218045112, "open_at_end": false, "sell_reason": "roi", "profit": 0.0013729321804511196, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0952903718828448, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:35:00+00:00", "trade_duration": 20, "open_rate": 3.808e-05, "close_rate": 3.903438596491228e-05, "open_at_end": false, "sell_reason": "roi", "profit": 9.543859649122774e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2626.0504201680674, "profit_abs": 0.0020000000000000018}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 19:00:00+00:00", "close_date": "2018-01-16 19:30:00+00:00", "trade_duration": 30, "open_rate": 0.02811012, "close_rate": 0.028532828571428567, "open_at_end": false, "sell_reason": "roi", "profit": 0.00042270857142856846, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.557437677249333, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 60, "open_rate": 0.00258379, "close_rate": 0.002325411, "open_at_end": false, "sell_reason": "stop_loss", "profit": -0.000258379, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.702835756775904, "profit_abs": -0.010474999999999984}, {"pair": "NXT/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 80, "open_rate": 2.559e-05, "close_rate": 2.3031e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -2.5590000000000004e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3907.7764751856193, "profit_abs": -0.010474999999999998}, {"pair": "TRX/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:35:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 50, "open_rate": 7.62e-05, "close_rate": 6.858e-05, "open_at_end": false, "sell_reason": "stop_loss", "profit": -7.619999999999998e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1312.3359580052495, "profit_abs": -0.010474999999999984}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:35:00+00:00", "trade_duration": 5, "open_rate": 0.00229844, "close_rate": 0.002402129022556391, "open_at_end": false, "sell_reason": "roi", "profit": 0.00010368902255639091, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 43.507770487809125, "profit_abs": 0.004000000000000017}, {"pair": "LTC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:40:00+00:00", "trade_duration": 10, "open_rate": 0.0151, "close_rate": 0.015781203007518795, "open_at_end": false, "sell_reason": "roi", "profit": 0.0006812030075187946, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.622516556291391, "profit_abs": 0.00399999999999999}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:40:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 5, "open_rate": 0.00235676, "close_rate": 0.00246308, "open_at_end": false, "sell_reason": "roi", "profit": 0.00010632000000000003, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 42.431134269081284, "profit_abs": 0.0040000000000000036}, {"pair": "DASH/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 22:45:00+00:00", "close_date": "2018-01-16 23:05:00+00:00", "trade_duration": 20, "open_rate": 0.0630692, "close_rate": 0.06464988170426066, "open_at_end": false, "sell_reason": "roi", "profit": 0.0015806817042606502, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.585559988076589, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:50:00+00:00", "close_date": "2018-01-16 22:55:00+00:00", "trade_duration": 5, "open_rate": 2.2e-05, "close_rate": 2.299248120300751e-05, "open_at_end": false, "sell_reason": "roi", "profit": 9.924812030075114e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 4545.454545454546, "profit_abs": 0.003999999999999976}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-17 03:30:00+00:00", "close_date": "2018-01-17 04:00:00+00:00", "trade_duration": 30, "open_rate": 4.974e-05, "close_rate": 5.048796992481203e-05, "open_at_end": false, "sell_reason": "roi", "profit": 7.479699248120277e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2010.454362685967, "profit_abs": 0.0010000000000000009}, {"pair": "TRX/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-17 03:55:00+00:00", "close_date": "2018-01-17 04:15:00+00:00", "trade_duration": 20, "open_rate": 7.108e-05, "close_rate": 7.28614536340852e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.7814536340851996e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1406.8655036578502, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 09:35:00+00:00", "close_date": "2018-01-17 10:15:00+00:00", "trade_duration": 40, "open_rate": 0.04327, "close_rate": 0.04348689223057644, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002168922305764362, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.3110700254217704, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:20:00+00:00", "close_date": "2018-01-17 17:00:00+00:00", "trade_duration": 400, "open_rate": 4.997e-05, "close_rate": 5.022047619047618e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.504761904761831e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2001.2007204322595, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:25:00+00:00", "trade_duration": 55, "open_rate": 0.06836818, "close_rate": 0.06871087764411027, "open_at_end": false, "sell_reason": "roi", "profit": 0.00034269764411026804, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4626687444363737, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:10:00+00:00", "trade_duration": 40, "open_rate": 3.63e-05, "close_rate": 3.648195488721804e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.8195488721804031e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2754.8209366391184, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:30:00+00:00", "close_date": "2018-01-17 22:05:00+00:00", "trade_duration": 575, "open_rate": 0.0281, "close_rate": 0.02824085213032581, "open_at_end": false, "sell_reason": "roi", "profit": 0.0001408521303258095, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5587188612099645, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:35:00+00:00", "close_date": "2018-01-17 16:55:00+00:00", "trade_duration": 260, "open_rate": 0.08651001, "close_rate": 0.08694364413533832, "open_at_end": false, "sell_reason": "roi", "profit": 0.00043363413533832607, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1559355963546878, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 05:00:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 55, "open_rate": 5.633e-05, "close_rate": 5.6612355889724306e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.8235588972430847e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1775.2529735487308, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 05:20:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 35, "open_rate": 0.06988494, "close_rate": 0.07093584135338346, "open_at_end": false, "sell_reason": "roi", "profit": 0.0010509013533834544, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.430923457900944, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 07:35:00+00:00", "close_date": "2018-01-18 08:15:00+00:00", "trade_duration": 40, "open_rate": 5.545e-05, "close_rate": 5.572794486215538e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.779448621553787e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1803.4265103697026, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 09:00:00+00:00", "close_date": "2018-01-18 09:40:00+00:00", "trade_duration": 40, "open_rate": 0.01633527, "close_rate": 0.016417151052631574, "open_at_end": false, "sell_reason": "roi", "profit": 8.188105263157511e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.121723118136401, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 16:40:00+00:00", "close_date": "2018-01-18 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.00269734, "close_rate": 0.002710860501253133, "open_at_end": false, "sell_reason": "roi", "profit": 1.3520501253133123e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.073561360451414, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-18 18:05:00+00:00", "close_date": "2018-01-18 18:30:00+00:00", "trade_duration": 25, "open_rate": 4.475e-05, "close_rate": 4.587155388471177e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.1215538847117757e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2234.63687150838, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 18:25:00+00:00", "close_date": "2018-01-18 18:55:00+00:00", "trade_duration": 30, "open_rate": 2.79e-05, "close_rate": 2.8319548872180444e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.1954887218044365e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3584.2293906810037, "profit_abs": 0.000999999999999987}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 20:10:00+00:00", "close_date": "2018-01-18 20:50:00+00:00", "trade_duration": 40, "open_rate": 0.04439326, "close_rate": 0.04461578260651629, "open_at_end": false, "sell_reason": "roi", "profit": 0.00022252260651629135, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2525942001105577, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 21:30:00+00:00", "close_date": "2018-01-19 00:35:00+00:00", "trade_duration": 185, "open_rate": 4.49e-05, "close_rate": 4.51250626566416e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.2506265664159932e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2227.1714922049, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 21:55:00+00:00", "close_date": "2018-01-19 05:05:00+00:00", "trade_duration": 430, "open_rate": 0.02855, "close_rate": 0.028693107769423555, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014310776942355607, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.502626970227671, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 22:10:00+00:00", "close_date": "2018-01-18 22:50:00+00:00", "trade_duration": 40, "open_rate": 5.796e-05, "close_rate": 5.8250526315789473e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.905263157894727e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1725.3278122843342, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 23:50:00+00:00", "close_date": "2018-01-19 00:30:00+00:00", "trade_duration": 40, "open_rate": 0.04340323, "close_rate": 0.04362079005012531, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002175600501253122, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.303975994413319, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-19 16:45:00+00:00", "close_date": "2018-01-19 17:35:00+00:00", "trade_duration": 50, "open_rate": 0.04454455, "close_rate": 0.04476783095238095, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002232809523809512, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.244943545282195, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:15:00+00:00", "close_date": "2018-01-19 19:55:00+00:00", "trade_duration": 160, "open_rate": 5.62e-05, "close_rate": 5.648170426065162e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.817042606516199e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1779.3594306049824, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:20:00+00:00", "close_date": "2018-01-19 20:15:00+00:00", "trade_duration": 175, "open_rate": 4.339e-05, "close_rate": 4.360749373433584e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.174937343358337e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2304.6784973496196, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-20 04:45:00+00:00", "close_date": "2018-01-20 17:35:00+00:00", "trade_duration": 770, "open_rate": 0.0001009, "close_rate": 0.00010140576441102755, "open_at_end": false, "sell_reason": "roi", "profit": 5.057644110275549e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 991.0802775024778, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 15:15:00+00:00", "trade_duration": 625, "open_rate": 0.00270505, "close_rate": 0.002718609147869674, "open_at_end": false, "sell_reason": "roi", "profit": 1.3559147869673764e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.96789338459548, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 07:00:00+00:00", "trade_duration": 130, "open_rate": 0.03000002, "close_rate": 0.030150396040100245, "open_at_end": false, "sell_reason": "roi", "profit": 0.00015037604010024672, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.3333311111125927, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 09:00:00+00:00", "close_date": "2018-01-20 09:40:00+00:00", "trade_duration": 40, "open_rate": 5.46e-05, "close_rate": 5.4873684210526304e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.736842105263053e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1831.5018315018317, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-20 18:25:00+00:00", "close_date": "2018-01-25 03:50:00+00:00", "trade_duration": 6325, "open_rate": 0.03082222, "close_rate": 0.027739998, "open_at_end": false, "sell_reason": "stop_loss", "profit": -0.0030822220000000025, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.244412634781012, "profit_abs": -0.010474999999999998}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 22:25:00+00:00", "close_date": "2018-01-20 23:15:00+00:00", "trade_duration": 50, "open_rate": 0.08969999, "close_rate": 0.09014961401002504, "open_at_end": false, "sell_reason": "roi", "profit": 0.00044962401002504593, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1148273260677064, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 02:50:00+00:00", "close_date": "2018-01-21 14:30:00+00:00", "trade_duration": 700, "open_rate": 0.01632501, "close_rate": 0.01640683962406015, "open_at_end": false, "sell_reason": "roi", "profit": 8.182962406014932e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.125570520324337, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 10:20:00+00:00", "close_date": "2018-01-21 11:00:00+00:00", "trade_duration": 40, "open_rate": 0.070538, "close_rate": 0.07089157393483708, "open_at_end": false, "sell_reason": "roi", "profit": 0.00035357393483707866, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.417675579120474, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 15:50:00+00:00", "close_date": "2018-01-21 18:45:00+00:00", "trade_duration": 175, "open_rate": 5.301e-05, "close_rate": 5.327571428571427e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.657142857142672e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.4365214110546, "profit_abs": -2.7755575615628914e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 16:20:00+00:00", "close_date": "2018-01-21 17:00:00+00:00", "trade_duration": 40, "open_rate": 3.955e-05, "close_rate": 3.9748245614035085e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.9824561403508552e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2528.4450063211125, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:45:00+00:00", "trade_duration": 30, "open_rate": 0.00258505, "close_rate": 0.002623922932330827, "open_at_end": false, "sell_reason": "roi", "profit": 3.8872932330826816e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.6839712964933, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:55:00+00:00", "trade_duration": 40, "open_rate": 3.903e-05, "close_rate": 3.922563909774435e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.9563909774435151e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2562.1316935690497, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 00:35:00+00:00", "close_date": "2018-01-22 10:35:00+00:00", "trade_duration": 600, "open_rate": 5.236e-05, "close_rate": 5.262245614035087e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.624561403508717e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1909.8548510313217, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 01:30:00+00:00", "close_date": "2018-01-22 02:10:00+00:00", "trade_duration": 40, "open_rate": 9.028e-05, "close_rate": 9.07325313283208e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.5253132832080657e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1107.6650420912717, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 12:25:00+00:00", "close_date": "2018-01-22 14:35:00+00:00", "trade_duration": 130, "open_rate": 0.002687, "close_rate": 0.002700468671679198, "open_at_end": false, "sell_reason": "roi", "profit": 1.3468671679197925e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.21622627465575, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 13:15:00+00:00", "close_date": "2018-01-22 13:55:00+00:00", "trade_duration": 40, "open_rate": 4.168e-05, "close_rate": 4.188892230576441e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.0892230576441054e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2399.232245681382, "profit_abs": 1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-22 14:00:00+00:00", "close_date": "2018-01-22 14:30:00+00:00", "trade_duration": 30, "open_rate": 8.821e-05, "close_rate": 8.953646616541353e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.326466165413529e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1133.6583153837435, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 15:55:00+00:00", "close_date": "2018-01-22 16:40:00+00:00", "trade_duration": 45, "open_rate": 5.172e-05, "close_rate": 5.1979248120300745e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.592481203007459e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1933.4880123743235, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-22 16:05:00+00:00", "close_date": "2018-01-22 16:25:00+00:00", "trade_duration": 20, "open_rate": 3.026e-05, "close_rate": 3.101839598997494e-05, "open_at_end": false, "sell_reason": "roi", "profit": 7.5839598997494e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3304.692663582287, "profit_abs": 0.0020000000000000157}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 19:50:00+00:00", "close_date": "2018-01-23 00:10:00+00:00", "trade_duration": 260, "open_rate": 0.07064, "close_rate": 0.07099408521303258, "open_at_end": false, "sell_reason": "roi", "profit": 0.00035408521303258167, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.415628539071348, "profit_abs": 1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 21:25:00+00:00", "close_date": "2018-01-22 22:05:00+00:00", "trade_duration": 40, "open_rate": 0.01644483, "close_rate": 0.01652726022556391, "open_at_end": false, "sell_reason": "roi", "profit": 8.243022556390922e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.080938507725528, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-23 00:05:00+00:00", "close_date": "2018-01-23 00:35:00+00:00", "trade_duration": 30, "open_rate": 4.331e-05, "close_rate": 4.3961278195488714e-05, "open_at_end": false, "sell_reason": "roi", "profit": 6.512781954887175e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2308.935580697299, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-23 01:50:00+00:00", "close_date": "2018-01-23 02:15:00+00:00", "trade_duration": 25, "open_rate": 3.2e-05, "close_rate": 3.2802005012531326e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.020050125313278e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3125.0000000000005, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 04:25:00+00:00", "close_date": "2018-01-23 05:15:00+00:00", "trade_duration": 50, "open_rate": 0.09167706, "close_rate": 0.09213659413533835, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004595341353383492, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0907854156754153, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 07:35:00+00:00", "close_date": "2018-01-23 09:00:00+00:00", "trade_duration": 85, "open_rate": 0.0692498, "close_rate": 0.06959691679197995, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003471167919799484, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4440474918339115, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 10:50:00+00:00", "close_date": "2018-01-23 13:05:00+00:00", "trade_duration": 135, "open_rate": 3.182e-05, "close_rate": 3.197949874686716e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.594987468671663e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3142.677561282213, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 11:05:00+00:00", "close_date": "2018-01-23 16:05:00+00:00", "trade_duration": 300, "open_rate": 0.04088, "close_rate": 0.04108491228070175, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002049122807017481, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4461839530332683, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 14:55:00+00:00", "close_date": "2018-01-23 15:35:00+00:00", "trade_duration": 40, "open_rate": 5.15e-05, "close_rate": 5.175814536340851e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.5814536340851513e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1941.747572815534, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 16:35:00+00:00", "close_date": "2018-01-24 00:05:00+00:00", "trade_duration": 450, "open_rate": 0.09071698, "close_rate": 0.09117170170426064, "open_at_end": false, "sell_reason": "roi", "profit": 0.00045472170426064107, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1023294646713329, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 17:25:00+00:00", "close_date": "2018-01-23 18:45:00+00:00", "trade_duration": 80, "open_rate": 3.128e-05, "close_rate": 3.1436791979949865e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5679197994986587e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3196.9309462915603, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 20:15:00+00:00", "close_date": "2018-01-23 22:00:00+00:00", "trade_duration": 105, "open_rate": 9.555e-05, "close_rate": 9.602894736842104e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.789473684210343e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1046.5724751439038, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 22:30:00+00:00", "close_date": "2018-01-23 23:10:00+00:00", "trade_duration": 40, "open_rate": 0.04080001, "close_rate": 0.0410045213283208, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020451132832080554, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.450979791426522, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 23:50:00+00:00", "close_date": "2018-01-24 03:35:00+00:00", "trade_duration": 225, "open_rate": 5.163e-05, "close_rate": 5.18887969924812e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.587969924812037e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1936.8584156498162, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 00:20:00+00:00", "close_date": "2018-01-24 01:50:00+00:00", "trade_duration": 90, "open_rate": 0.04040781, "close_rate": 0.04061035541353383, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002025454135338306, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.474769110228938, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 06:45:00+00:00", "close_date": "2018-01-24 07:25:00+00:00", "trade_duration": 40, "open_rate": 5.132e-05, "close_rate": 5.157724310776942e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.5724310776941724e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1948.5580670303975, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-24 14:15:00+00:00", "close_date": "2018-01-24 14:25:00+00:00", "trade_duration": 10, "open_rate": 5.198e-05, "close_rate": 5.432496240601503e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.344962406015033e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1923.8168526356292, "profit_abs": 0.0040000000000000036}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 14:50:00+00:00", "close_date": "2018-01-24 16:35:00+00:00", "trade_duration": 105, "open_rate": 3.054e-05, "close_rate": 3.069308270676692e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.5308270676691466e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3274.3942370661425, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 15:10:00+00:00", "close_date": "2018-01-24 16:15:00+00:00", "trade_duration": 65, "open_rate": 9.263e-05, "close_rate": 9.309431077694236e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.6431077694236234e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1079.5638562020945, "profit_abs": 2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 22:40:00+00:00", "close_date": "2018-01-24 23:25:00+00:00", "trade_duration": 45, "open_rate": 5.514e-05, "close_rate": 5.54163909774436e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.7639097744360576e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1813.5654697134569, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 00:50:00+00:00", "close_date": "2018-01-25 01:30:00+00:00", "trade_duration": 40, "open_rate": 4.921e-05, "close_rate": 4.9456666666666664e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.4666666666666543e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.1072952651903, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 08:15:00+00:00", "close_date": "2018-01-25 12:15:00+00:00", "trade_duration": 240, "open_rate": 0.0026, "close_rate": 0.002613032581453634, "open_at_end": false, "sell_reason": "roi", "profit": 1.3032581453634e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.46153846153847, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 10:25:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 350, "open_rate": 0.02799871, "close_rate": 0.028139054411027563, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014034441102756326, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.571593119825878, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 11:00:00+00:00", "close_date": "2018-01-25 11:45:00+00:00", "trade_duration": 45, "open_rate": 0.04078902, "close_rate": 0.0409934762406015, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020445624060149575, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4516401717913303, "profit_abs": -1.3877787807814457e-17}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:05:00+00:00", "close_date": "2018-01-25 13:45:00+00:00", "trade_duration": 40, "open_rate": 2.89e-05, "close_rate": 2.904486215538847e-05, "open_at_end": false, "sell_reason": "roi", "profit": 1.4486215538846723e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3460.2076124567475, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:20:00+00:00", "close_date": "2018-01-25 14:05:00+00:00", "trade_duration": 45, "open_rate": 0.041103, "close_rate": 0.04130903007518797, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020603007518796984, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4329124394813033, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-25 15:45:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 30, "open_rate": 5.428e-05, "close_rate": 5.509624060150376e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.162406015037611e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1842.2991893883568, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 17:45:00+00:00", "close_date": "2018-01-25 23:15:00+00:00", "trade_duration": 330, "open_rate": 5.414e-05, "close_rate": 5.441137844611528e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.713784461152774e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1847.063169560399, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 21:15:00+00:00", "close_date": "2018-01-25 21:55:00+00:00", "trade_duration": 40, "open_rate": 0.04140777, "close_rate": 0.0416153277443609, "open_at_end": false, "sell_reason": "roi", "profit": 0.0002075577443608964, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.415005686130888, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 02:05:00+00:00", "close_date": "2018-01-26 02:45:00+00:00", "trade_duration": 40, "open_rate": 0.00254309, "close_rate": 0.002555837318295739, "open_at_end": false, "sell_reason": "roi", "profit": 1.2747318295739177e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.32224183965177, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 02:55:00+00:00", "close_date": "2018-01-26 15:10:00+00:00", "trade_duration": 735, "open_rate": 5.607e-05, "close_rate": 5.6351052631578935e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.810526315789381e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1783.4849295523454, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 06:10:00+00:00", "close_date": "2018-01-26 09:25:00+00:00", "trade_duration": 195, "open_rate": 0.00253806, "close_rate": 0.0025507821052631577, "open_at_end": false, "sell_reason": "roi", "profit": 1.2722105263157733e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.400171784748984, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 07:25:00+00:00", "close_date": "2018-01-26 09:55:00+00:00", "trade_duration": 150, "open_rate": 0.0415, "close_rate": 0.04170802005012531, "open_at_end": false, "sell_reason": "roi", "profit": 0.00020802005012530989, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4096385542168677, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-26 09:55:00+00:00", "close_date": "2018-01-26 10:25:00+00:00", "trade_duration": 30, "open_rate": 5.321e-05, "close_rate": 5.401015037593984e-05, "open_at_end": false, "sell_reason": "roi", "profit": 8.00150375939842e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1879.3459875963165, "profit_abs": 0.000999999999999987}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 16:05:00+00:00", "close_date": "2018-01-26 16:45:00+00:00", "trade_duration": 40, "open_rate": 0.02772046, "close_rate": 0.02785940967418546, "open_at_end": false, "sell_reason": "roi", "profit": 0.00013894967418546025, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.6074437437185387, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 23:35:00+00:00", "close_date": "2018-01-27 00:15:00+00:00", "trade_duration": 40, "open_rate": 0.09461341, "close_rate": 0.09508766268170424, "open_at_end": false, "sell_reason": "roi", "profit": 0.00047425268170424306, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0569326272036914, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 00:35:00+00:00", "close_date": "2018-01-27 01:30:00+00:00", "trade_duration": 55, "open_rate": 5.615e-05, "close_rate": 5.643145363408521e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.814536340852038e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1780.9439002671415, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.07877175, "open_date": "2018-01-27 00:45:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 4560, "open_rate": 5.556e-05, "close_rate": 5.144e-05, "open_at_end": true, "sell_reason": "force_sell", "profit": -4.120000000000001e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1799.8560115190785, "profit_abs": -0.007896868250539965}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 02:30:00+00:00", "close_date": "2018-01-27 11:25:00+00:00", "trade_duration": 535, "open_rate": 0.06900001, "close_rate": 0.06934587471177944, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003458647117794422, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492751522789635, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 06:25:00+00:00", "close_date": "2018-01-27 07:05:00+00:00", "trade_duration": 40, "open_rate": 0.09449985, "close_rate": 0.0949735334586466, "open_at_end": false, "sell_reason": "roi", "profit": 0.0004736834586466093, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.058202737887944, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.04815133, "open_date": "2018-01-27 09:40:00+00:00", "close_date": "2018-01-30 04:40:00+00:00", "trade_duration": 4020, "open_rate": 0.0410697, "close_rate": 0.03928809, "open_at_end": true, "sell_reason": "force_sell", "profit": -0.001781610000000003, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4348850855983852, "profit_abs": -0.004827170578309559}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 11:45:00+00:00", "close_date": "2018-01-27 12:30:00+00:00", "trade_duration": 45, "open_rate": 0.0285, "close_rate": 0.02864285714285714, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014285714285713902, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5087719298245617, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 12:35:00+00:00", "close_date": "2018-01-27 15:25:00+00:00", "trade_duration": 170, "open_rate": 0.02866372, "close_rate": 0.02880739779448621, "open_at_end": false, "sell_reason": "roi", "profit": 0.00014367779448621124, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.4887307020861216, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 15:50:00+00:00", "close_date": "2018-01-27 16:50:00+00:00", "trade_duration": 60, "open_rate": 0.095381, "close_rate": 0.09585910025062656, "open_at_end": false, "sell_reason": "roi", "profit": 0.00047810025062657024, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0484268355332824, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 17:05:00+00:00", "close_date": "2018-01-27 17:45:00+00:00", "trade_duration": 40, "open_rate": 0.06759092, "close_rate": 0.06792972160401002, "open_at_end": false, "sell_reason": "roi", "profit": 0.00033880160401002224, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4794886650455417, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 23:40:00+00:00", "close_date": "2018-01-28 01:05:00+00:00", "trade_duration": 85, "open_rate": 0.00258501, "close_rate": 0.002597967443609022, "open_at_end": false, "sell_reason": "roi", "profit": 1.2957443609021985e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.684569885609726, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 02:25:00+00:00", "close_date": "2018-01-28 08:10:00+00:00", "trade_duration": 345, "open_rate": 0.06698502, "close_rate": 0.0673207845112782, "open_at_end": false, "sell_reason": "roi", "profit": 0.00033576451127818874, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4928710926711672, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 10:25:00+00:00", "close_date": "2018-01-28 16:30:00+00:00", "trade_duration": 365, "open_rate": 0.0677177, "close_rate": 0.06805713709273183, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003394370927318202, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4767187899175547, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-28 20:35:00+00:00", "close_date": "2018-01-28 21:35:00+00:00", "trade_duration": 60, "open_rate": 5.215e-05, "close_rate": 5.2411403508771925e-05, "open_at_end": false, "sell_reason": "roi", "profit": 2.6140350877192417e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1917.5455417066157, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-28 22:00:00+00:00", "close_date": "2018-01-28 22:30:00+00:00", "trade_duration": 30, "open_rate": 0.00273809, "close_rate": 0.002779264285714285, "open_at_end": false, "sell_reason": "roi", "profit": 4.117428571428529e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.5218089982433, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-29 00:00:00+00:00", "close_date": "2018-01-29 00:30:00+00:00", "trade_duration": 30, "open_rate": 0.00274632, "close_rate": 0.002787618045112782, "open_at_end": false, "sell_reason": "roi", "profit": 4.129804511278194e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.412362725392526, "profit_abs": 0.0010000000000000148}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-29 02:15:00+00:00", "close_date": "2018-01-29 03:00:00+00:00", "trade_duration": 45, "open_rate": 0.01622478, "close_rate": 0.016306107218045113, "open_at_end": false, "sell_reason": "roi", "profit": 8.132721804511231e-05, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.163411768911504, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 03:05:00+00:00", "close_date": "2018-01-29 03:45:00+00:00", "trade_duration": 40, "open_rate": 0.069, "close_rate": 0.06934586466165413, "open_at_end": false, "sell_reason": "roi", "profit": 0.00034586466165412166, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492753623188406, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 05:20:00+00:00", "close_date": "2018-01-29 06:55:00+00:00", "trade_duration": 95, "open_rate": 8.755e-05, "close_rate": 8.798884711779448e-05, "open_at_end": false, "sell_reason": "roi", "profit": 4.3884711779447504e-07, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1142.204454597373, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 07:00:00+00:00", "close_date": "2018-01-29 19:25:00+00:00", "trade_duration": 745, "open_rate": 0.06825763, "close_rate": 0.06859977350877192, "open_at_end": false, "sell_reason": "roi", "profit": 0.00034214350877191657, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4650376815016872, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 19:45:00+00:00", "close_date": "2018-01-29 20:25:00+00:00", "trade_duration": 40, "open_rate": 0.06713892, "close_rate": 0.06747545593984962, "open_at_end": false, "sell_reason": "roi", "profit": 0.0003365359398496137, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4894490408841845, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0199116, "open_date": "2018-01-29 23:30:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 315, "open_rate": 8.934e-05, "close_rate": 8.8e-05, "open_at_end": true, "sell_reason": "force_sell", "profit": -1.3399999999999973e-06, "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1119.3194537721067, "profit_abs": -0.0019961383478844796}], "results_per_pair": [{"key": "TRX/BTC", "trades": 15, "profit_mean": 0.0023467073333333323, "profit_mean_pct": 0.23467073333333321, "profit_sum": 0.035200609999999986, "profit_sum_pct": 3.5200609999999988, "profit_total_abs": 0.0035288616521155086, "profit_total_pct": 1.1733536666666662, "duration_avg": "2:28:00", "wins": 9, "draws": 2, "losses": 4}, {"key": "ADA/BTC", "trades": 29, "profit_mean": -0.0011598141379310352, "profit_mean_pct": -0.11598141379310352, "profit_sum": -0.03363461000000002, "profit_sum_pct": -3.3634610000000023, "profit_total_abs": -0.0033718682505400333, "profit_total_pct": -1.1211536666666675, "duration_avg": "5:35:00", "wins": 9, "draws": 11, "losses": 9}, {"key": "XLM/BTC", "trades": 21, "profit_mean": 0.0026243899999999994, "profit_mean_pct": 0.2624389999999999, "profit_sum": 0.05511218999999999, "profit_sum_pct": 5.511218999999999, "profit_total_abs": 0.005525000000000002, "profit_total_pct": 1.8370729999999995, "duration_avg": "3:21:00", "wins": 12, "draws": 3, "losses": 6}, {"key": "ETH/BTC", "trades": 21, "profit_mean": 0.0009500057142857142, "profit_mean_pct": 0.09500057142857142, "profit_sum": 0.01995012, "profit_sum_pct": 1.9950119999999998, "profit_total_abs": 0.0019999999999999463, "profit_total_pct": 0.6650039999999999, "duration_avg": "2:17:00", "wins": 5, "draws": 10, "losses": 6}, {"key": "XMR/BTC", "trades": 16, "profit_mean": -0.0027899012500000007, "profit_mean_pct": -0.2789901250000001, "profit_sum": -0.04463842000000001, "profit_sum_pct": -4.463842000000001, "profit_total_abs": -0.0044750000000000345, "profit_total_pct": -1.4879473333333337, "duration_avg": "8:41:00", "wins": 6, "draws": 5, "losses": 5}, {"key": "ZEC/BTC", "trades": 21, "profit_mean": -0.00039290904761904774, "profit_mean_pct": -0.03929090476190478, "profit_sum": -0.008251090000000003, "profit_sum_pct": -0.8251090000000003, "profit_total_abs": -0.000827170578309569, "profit_total_pct": -0.27503633333333344, "duration_avg": "4:17:00", "wins": 8, "draws": 7, "losses": 6}, {"key": "NXT/BTC", "trades": 12, "profit_mean": -0.0012261025000000006, "profit_mean_pct": -0.12261025000000006, "profit_sum": -0.014713230000000008, "profit_sum_pct": -1.4713230000000008, "profit_total_abs": -0.0014750000000000874, "profit_total_pct": -0.4904410000000003, "duration_avg": "0:57:00", "wins": 4, "draws": 3, "losses": 5}, {"key": "LTC/BTC", "trades": 8, "profit_mean": 0.00748129625, "profit_mean_pct": 0.748129625, "profit_sum": 0.05985037, "profit_sum_pct": 5.985037, "profit_total_abs": 0.006000000000000019, "profit_total_pct": 1.9950123333333334, "duration_avg": "1:59:00", "wins": 5, "draws": 2, "losses": 1}, {"key": "ETC/BTC", "trades": 20, "profit_mean": 0.0022568569999999997, "profit_mean_pct": 0.22568569999999996, "profit_sum": 0.04513713999999999, "profit_sum_pct": 4.513713999999999, "profit_total_abs": 0.004525000000000001, "profit_total_pct": 1.504571333333333, "duration_avg": "1:45:00", "wins": 11, "draws": 4, "losses": 5}, {"key": "DASH/BTC", "trades": 16, "profit_mean": 0.0018703237499999997, "profit_mean_pct": 0.18703237499999997, "profit_sum": 0.029925179999999996, "profit_sum_pct": 2.9925179999999996, "profit_total_abs": 0.002999999999999961, "profit_total_pct": 0.9975059999999999, "duration_avg": "3:03:00", "wins": 4, "draws": 7, "losses": 5}, {"key": "TOTAL", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}], "sell_reason_summary": [{"sell_reason": "roi", "trades": 170, "wins": 73, "draws": 54, "losses": 43, "profit_mean": 0.005398268352941177, "profit_mean_pct": 0.54, "profit_sum": 0.91770562, "profit_sum_pct": 91.77, "profit_total_abs": 0.09199999999999964, "profit_pct_total": 30.59}, {"sell_reason": "stop_loss", "trades": 6, "wins": 0, "draws": 0, "losses": 6, "profit_mean": -0.10448878000000002, "profit_mean_pct": -10.45, "profit_sum": -0.6269326800000001, "profit_sum_pct": -62.69, "profit_total_abs": -0.06284999999999992, "profit_pct_total": -20.9}, {"sell_reason": "force_sell", "trades": 3, "wins": 0, "draws": 0, "losses": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.89, "profit_sum": -0.14683468, "profit_sum_pct": -14.68, "profit_total_abs": -0.014720177176734003, "profit_pct_total": -4.89}], "left_open_trades": [{"key": "TRX/BTC", "trades": 1, "profit_mean": -0.0199116, "profit_mean_pct": -1.9911600000000003, "profit_sum": -0.0199116, "profit_sum_pct": -1.9911600000000003, "profit_total_abs": -0.0019961383478844796, "profit_total_pct": -0.6637200000000001, "duration_avg": "5:15:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ADA/BTC", "trades": 1, "profit_mean": -0.07877175, "profit_mean_pct": -7.877175, "profit_sum": -0.07877175, "profit_sum_pct": -7.877175, "profit_total_abs": -0.007896868250539965, "profit_total_pct": -2.625725, "duration_avg": "3 days, 4:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ZEC/BTC", "trades": 1, "profit_mean": -0.04815133, "profit_mean_pct": -4.815133, "profit_sum": -0.04815133, "profit_sum_pct": -4.815133, "profit_total_abs": -0.004827170578309559, "profit_total_pct": -1.6050443333333335, "duration_avg": "2 days, 19:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "TOTAL", "trades": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.894489333333333, "profit_sum": -0.14683468, "profit_sum_pct": -14.683468, "profit_total_abs": -0.014720177176734003, "profit_total_pct": -4.8944893333333335, "duration_avg": "2 days, 1:25:00", "wins": 0, "draws": 0, "losses": 3}], "total_trades": 179, "backtest_start": "2018-01-30 04:45:00+00:00", "backtest_start_ts": 1517287500, "backtest_end": "2018-01-30 04:45:00+00:00", "backtest_end_ts": 1517287500, "backtest_days": 0, "trades_per_day": null, "market_change": 0.25, "stake_amount": 0.1, "max_drawdown": 0.21142322000000008, "drawdown_start": "2018-01-24 14:25:00+00:00", "drawdown_start_ts": 1516803900.0, "drawdown_end": "2018-01-30 04:45:00+00:00", "drawdown_end_ts": 1517287500.0}}, "strategy_comparison": [{"key": "DefaultStrategy", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}, {"key": "TestStrategy", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}]} \ No newline at end of file +{"strategy": {"DefaultStrategy": {"trades": [{"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:20:00+00:00", "trade_duration": 5, "open_rate": 9.64e-05, "close_rate": 0.00010074887218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1037.344398340249, "profit_abs": 0.00399999999999999}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:30:00+00:00", "trade_duration": 15, "open_rate": 4.756e-05, "close_rate": 4.9705563909774425e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2102.6072329688814, "profit_abs": 0.00399999999999999}, {"pair": "XLM/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:35:00+00:00", "trade_duration": 10, "open_rate": 3.339e-05, "close_rate": 3.489631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2994.908655286014, "profit_abs": 0.0040000000000000036}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:40:00+00:00", "trade_duration": 15, "open_rate": 9.696e-05, "close_rate": 0.00010133413533834584, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1031.3531353135315, "profit_abs": 0.00399999999999999}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 07:35:00+00:00", "close_date": "2018-01-10 08:35:00+00:00", "trade_duration": 60, "open_rate": 0.0943, "close_rate": 0.09477268170426063, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0604453870625663, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 07:40:00+00:00", "close_date": "2018-01-10 08:10:00+00:00", "trade_duration": 30, "open_rate": 0.02719607, "close_rate": 0.02760503345864661, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.677001860930642, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 08:15:00+00:00", "close_date": "2018-01-10 09:55:00+00:00", "trade_duration": 100, "open_rate": 0.04634952, "close_rate": 0.046581848421052625, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1575196463739, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 14:45:00+00:00", "close_date": "2018-01-10 15:50:00+00:00", "trade_duration": 65, "open_rate": 3.066e-05, "close_rate": 3.081368421052631e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3261.5786040443577, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 16:35:00+00:00", "close_date": "2018-01-10 17:15:00+00:00", "trade_duration": 40, "open_rate": 0.0168999, "close_rate": 0.016984611278195488, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.917194776300452, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 16:40:00+00:00", "close_date": "2018-01-10 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.09132568, "close_rate": 0.0917834528320802, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0949822656672252, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 18:50:00+00:00", "close_date": "2018-01-10 19:45:00+00:00", "trade_duration": 55, "open_rate": 0.08898003, "close_rate": 0.08942604518796991, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1238476768326557, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 22:15:00+00:00", "close_date": "2018-01-10 23:00:00+00:00", "trade_duration": 45, "open_rate": 0.08560008, "close_rate": 0.08602915308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1682232072680307, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 22:50:00+00:00", "close_date": "2018-01-10 23:20:00+00:00", "trade_duration": 30, "open_rate": 0.00249083, "close_rate": 0.0025282860902255634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 40.147260150231055, "profit_abs": 0.000999999999999987}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 23:15:00+00:00", "close_date": "2018-01-11 00:15:00+00:00", "trade_duration": 60, "open_rate": 3.022e-05, "close_rate": 3.037147869674185e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3309.0668431502318, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-10 23:40:00+00:00", "close_date": "2018-01-11 00:05:00+00:00", "trade_duration": 25, "open_rate": 0.002437, "close_rate": 0.0024980776942355883, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.03405826836274, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 00:00:00+00:00", "close_date": "2018-01-11 00:35:00+00:00", "trade_duration": 35, "open_rate": 0.04771803, "close_rate": 0.04843559436090225, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0956439316543456, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-11 03:40:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 45, "open_rate": 3.651e-05, "close_rate": 3.2859000000000005e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2738.9756231169545, "profit_abs": -0.01047499999999997}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 03:55:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 30, "open_rate": 0.08824105, "close_rate": 0.08956798308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1332594070446804, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 04:00:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 50, "open_rate": 0.00243, "close_rate": 0.002442180451127819, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.1522633744856, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:55:00+00:00", "trade_duration": 25, "open_rate": 0.04545064, "close_rate": 0.046589753784461146, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.200189040242338, "profit_abs": 0.001999999999999988}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 20, "open_rate": 3.372e-05, "close_rate": 3.456511278195488e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2965.599051008304, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:55:00+00:00", "close_date": "2018-01-11 05:15:00+00:00", "trade_duration": 20, "open_rate": 0.02644, "close_rate": 0.02710265664160401, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7821482602118004, "profit_abs": 0.001999999999999988}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:20:00+00:00", "close_date": "2018-01-11 12:00:00+00:00", "trade_duration": 40, "open_rate": 0.08812, "close_rate": 0.08856170426065162, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1348161597821154, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:35:00+00:00", "close_date": "2018-01-11 12:15:00+00:00", "trade_duration": 40, "open_rate": 0.02683577, "close_rate": 0.026970285137844607, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7263696923919087, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 14:00:00+00:00", "close_date": "2018-01-11 14:25:00+00:00", "trade_duration": 25, "open_rate": 4.919e-05, "close_rate": 5.04228320802005e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.9335230737956, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 19:25:00+00:00", "close_date": "2018-01-11 20:35:00+00:00", "trade_duration": 70, "open_rate": 0.08784896, "close_rate": 0.08828930566416039, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1383174029607181, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:35:00+00:00", "close_date": "2018-01-11 23:30:00+00:00", "trade_duration": 55, "open_rate": 5.105e-05, "close_rate": 5.130588972431077e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1958.8638589618022, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:25:00+00:00", "trade_duration": 30, "open_rate": 3.96e-05, "close_rate": 4.019548872180451e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2525.252525252525, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:35:00+00:00", "trade_duration": 40, "open_rate": 2.885e-05, "close_rate": 2.899461152882205e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3466.204506065858, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 23:30:00+00:00", "close_date": "2018-01-12 00:05:00+00:00", "trade_duration": 35, "open_rate": 0.02645, "close_rate": 0.026847744360902256, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.780718336483932, "profit_abs": 0.0010000000000000148}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 23:55:00+00:00", "close_date": "2018-01-12 01:15:00+00:00", "trade_duration": 80, "open_rate": 0.048, "close_rate": 0.04824060150375939, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0833333333333335, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-12 21:15:00+00:00", "close_date": "2018-01-12 21:40:00+00:00", "trade_duration": 25, "open_rate": 4.692e-05, "close_rate": 4.809593984962405e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2131.287297527707, "profit_abs": 0.001999999999999974}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 00:55:00+00:00", "close_date": "2018-01-13 06:20:00+00:00", "trade_duration": 325, "open_rate": 0.00256966, "close_rate": 0.0025825405012531327, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.91565421106294, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 10:55:00+00:00", "close_date": "2018-01-13 11:35:00+00:00", "trade_duration": 40, "open_rate": 6.262e-05, "close_rate": 6.293388471177944e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1596.933886937081, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 13:05:00+00:00", "close_date": "2018-01-15 14:10:00+00:00", "trade_duration": 2945, "open_rate": 4.73e-05, "close_rate": 4.753709273182957e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2114.1649048625795, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:30:00+00:00", "close_date": "2018-01-13 14:45:00+00:00", "trade_duration": 75, "open_rate": 6.063e-05, "close_rate": 6.0933909774436085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1649.348507339601, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:40:00+00:00", "close_date": "2018-01-13 23:30:00+00:00", "trade_duration": 590, "open_rate": 0.00011082, "close_rate": 0.00011137548872180448, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 902.3641941887746, "profit_abs": -2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 15:15:00+00:00", "close_date": "2018-01-13 15:55:00+00:00", "trade_duration": 40, "open_rate": 5.93e-05, "close_rate": 5.9597243107769415e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1686.3406408094436, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 16:30:00+00:00", "close_date": "2018-01-13 17:10:00+00:00", "trade_duration": 40, "open_rate": 0.04850003, "close_rate": 0.04874313791979949, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0618543947292407, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 22:05:00+00:00", "close_date": "2018-01-14 06:25:00+00:00", "trade_duration": 500, "open_rate": 0.09825019, "close_rate": 0.09874267215538848, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0178097365511456, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 00:20:00+00:00", "close_date": "2018-01-14 22:55:00+00:00", "trade_duration": 1355, "open_rate": 6.018e-05, "close_rate": 6.048165413533834e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1661.681621801263, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 12:45:00+00:00", "close_date": "2018-01-14 13:25:00+00:00", "trade_duration": 40, "open_rate": 0.09758999, "close_rate": 0.0980791628822055, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.024695258191952, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-14 15:30:00+00:00", "close_date": "2018-01-14 16:00:00+00:00", "trade_duration": 30, "open_rate": 0.00311, "close_rate": 0.0031567669172932328, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.154340836012864, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 20:45:00+00:00", "close_date": "2018-01-14 22:15:00+00:00", "trade_duration": 90, "open_rate": 0.00312401, "close_rate": 0.003139669197994987, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.010140812609436, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 23:35:00+00:00", "close_date": "2018-01-15 00:30:00+00:00", "trade_duration": 55, "open_rate": 0.0174679, "close_rate": 0.017555458395989976, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.724786608579165, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 23:45:00+00:00", "close_date": "2018-01-15 00:25:00+00:00", "trade_duration": 40, "open_rate": 0.07346846, "close_rate": 0.07383672295739348, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.3611282991367997, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 02:25:00+00:00", "close_date": "2018-01-15 03:05:00+00:00", "trade_duration": 40, "open_rate": 0.097994, "close_rate": 0.09848519799498744, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.020470641059657, "profit_abs": -2.7755575615628914e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 07:20:00+00:00", "close_date": "2018-01-15 08:00:00+00:00", "trade_duration": 40, "open_rate": 0.09659, "close_rate": 0.09707416040100247, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0353038616834043, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-15 08:20:00+00:00", "close_date": "2018-01-15 08:55:00+00:00", "trade_duration": 35, "open_rate": 9.987e-05, "close_rate": 0.00010137180451127818, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1001.3016921998599, "profit_abs": 0.0010000000000000009}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-15 12:10:00+00:00", "close_date": "2018-01-16 02:50:00+00:00", "trade_duration": 880, "open_rate": 0.0948969, "close_rate": 0.09537257368421052, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0537752023511833, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:10:00+00:00", "close_date": "2018-01-15 17:40:00+00:00", "trade_duration": 210, "open_rate": 0.071, "close_rate": 0.07135588972431077, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4084507042253522, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:30:00+00:00", "close_date": "2018-01-15 15:10:00+00:00", "trade_duration": 40, "open_rate": 0.04600501, "close_rate": 0.046235611553884705, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.173676301776698, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:10:00+00:00", "close_date": "2018-01-15 19:25:00+00:00", "trade_duration": 75, "open_rate": 9.438e-05, "close_rate": 9.485308270676693e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1059.5465140919687, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:35:00+00:00", "close_date": "2018-01-15 19:15:00+00:00", "trade_duration": 40, "open_rate": 0.03040001, "close_rate": 0.030552391002506264, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.2894726021471703, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-15 20:25:00+00:00", "close_date": "2018-01-16 08:25:00+00:00", "trade_duration": 720, "open_rate": 5.837e-05, "close_rate": 5.2533e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1713.2088401576154, "profit_abs": -0.010474999999999984}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 20:40:00+00:00", "close_date": "2018-01-15 22:00:00+00:00", "trade_duration": 80, "open_rate": 0.046036, "close_rate": 0.04626675689223057, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1722130506560084, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 00:30:00+00:00", "close_date": "2018-01-16 01:10:00+00:00", "trade_duration": 40, "open_rate": 0.0028685, "close_rate": 0.0028828784461152877, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 34.86142583231654, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 01:15:00+00:00", "close_date": "2018-01-16 02:35:00+00:00", "trade_duration": 80, "open_rate": 0.06731755, "close_rate": 0.0676549813283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4854967241083492, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 07:45:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 55, "open_rate": 0.09217614, "close_rate": 0.09263817578947368, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0848794492804754, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:55:00+00:00", "trade_duration": 20, "open_rate": 0.0165, "close_rate": 0.016913533834586467, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.0606060606060606, "profit_abs": 0.0020000000000000018}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 5, "open_rate": 7.953e-05, "close_rate": 8.311781954887218e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1257.387149503332, "profit_abs": 0.00399999999999999}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 08:45:00+00:00", "close_date": "2018-01-16 09:50:00+00:00", "trade_duration": 65, "open_rate": 0.045202, "close_rate": 0.04542857644110275, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2122914915269236, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:45:00+00:00", "trade_duration": 30, "open_rate": 5.248e-05, "close_rate": 5.326917293233082e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1905.487804878049, "profit_abs": 0.0010000000000000009}, {"pair": "XMR/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:55:00+00:00", "trade_duration": 40, "open_rate": 0.02892318, "close_rate": 0.02906815834586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.457434486802627, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 09:50:00+00:00", "close_date": "2018-01-16 10:10:00+00:00", "trade_duration": 20, "open_rate": 5.158e-05, "close_rate": 5.287273182957392e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1938.735944164405, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:35:00+00:00", "trade_duration": 30, "open_rate": 0.02828232, "close_rate": 0.02870761804511278, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5357778286929786, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:40:00+00:00", "trade_duration": 35, "open_rate": 0.04357584, "close_rate": 0.044231115789473675, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.294849623093898, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 13:45:00+00:00", "close_date": "2018-01-16 14:20:00+00:00", "trade_duration": 35, "open_rate": 5.362e-05, "close_rate": 5.442631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1864.975755315181, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 17:30:00+00:00", "close_date": "2018-01-16 18:25:00+00:00", "trade_duration": 55, "open_rate": 5.302e-05, "close_rate": 5.328576441102756e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.0807242549984, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:45:00+00:00", "trade_duration": 30, "open_rate": 0.09129999, "close_rate": 0.09267292218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0952903718828448, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:35:00+00:00", "trade_duration": 20, "open_rate": 3.808e-05, "close_rate": 3.903438596491228e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2626.0504201680674, "profit_abs": 0.0020000000000000018}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 19:00:00+00:00", "close_date": "2018-01-16 19:30:00+00:00", "trade_duration": 30, "open_rate": 0.02811012, "close_rate": 0.028532828571428567, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.557437677249333, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 60, "open_rate": 0.00258379, "close_rate": 0.002325411, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.702835756775904, "profit_abs": -0.010474999999999984}, {"pair": "NXT/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 80, "open_rate": 2.559e-05, "close_rate": 2.3031e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3907.7764751856193, "profit_abs": -0.010474999999999998}, {"pair": "TRX/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:35:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 50, "open_rate": 7.62e-05, "close_rate": 6.858e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1312.3359580052495, "profit_abs": -0.010474999999999984}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:35:00+00:00", "trade_duration": 5, "open_rate": 0.00229844, "close_rate": 0.002402129022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 43.507770487809125, "profit_abs": 0.004000000000000017}, {"pair": "LTC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:40:00+00:00", "trade_duration": 10, "open_rate": 0.0151, "close_rate": 0.015781203007518795, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.622516556291391, "profit_abs": 0.00399999999999999}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:40:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 5, "open_rate": 0.00235676, "close_rate": 0.00246308, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 42.431134269081284, "profit_abs": 0.0040000000000000036}, {"pair": "DASH/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 22:45:00+00:00", "close_date": "2018-01-16 23:05:00+00:00", "trade_duration": 20, "open_rate": 0.0630692, "close_rate": 0.06464988170426066, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.585559988076589, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:50:00+00:00", "close_date": "2018-01-16 22:55:00+00:00", "trade_duration": 5, "open_rate": 2.2e-05, "close_rate": 2.299248120300751e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 4545.454545454546, "profit_abs": 0.003999999999999976}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-17 03:30:00+00:00", "close_date": "2018-01-17 04:00:00+00:00", "trade_duration": 30, "open_rate": 4.974e-05, "close_rate": 5.048796992481203e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2010.454362685967, "profit_abs": 0.0010000000000000009}, {"pair": "TRX/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-17 03:55:00+00:00", "close_date": "2018-01-17 04:15:00+00:00", "trade_duration": 20, "open_rate": 7.108e-05, "close_rate": 7.28614536340852e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1406.8655036578502, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 09:35:00+00:00", "close_date": "2018-01-17 10:15:00+00:00", "trade_duration": 40, "open_rate": 0.04327, "close_rate": 0.04348689223057644, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.3110700254217704, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:20:00+00:00", "close_date": "2018-01-17 17:00:00+00:00", "trade_duration": 400, "open_rate": 4.997e-05, "close_rate": 5.022047619047618e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2001.2007204322595, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:25:00+00:00", "trade_duration": 55, "open_rate": 0.06836818, "close_rate": 0.06871087764411027, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4626687444363737, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:10:00+00:00", "trade_duration": 40, "open_rate": 3.63e-05, "close_rate": 3.648195488721804e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2754.8209366391184, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:30:00+00:00", "close_date": "2018-01-17 22:05:00+00:00", "trade_duration": 575, "open_rate": 0.0281, "close_rate": 0.02824085213032581, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5587188612099645, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:35:00+00:00", "close_date": "2018-01-17 16:55:00+00:00", "trade_duration": 260, "open_rate": 0.08651001, "close_rate": 0.08694364413533832, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1559355963546878, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 05:00:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 55, "open_rate": 5.633e-05, "close_rate": 5.6612355889724306e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1775.2529735487308, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 05:20:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 35, "open_rate": 0.06988494, "close_rate": 0.07093584135338346, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.430923457900944, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 07:35:00+00:00", "close_date": "2018-01-18 08:15:00+00:00", "trade_duration": 40, "open_rate": 5.545e-05, "close_rate": 5.572794486215538e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1803.4265103697026, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 09:00:00+00:00", "close_date": "2018-01-18 09:40:00+00:00", "trade_duration": 40, "open_rate": 0.01633527, "close_rate": 0.016417151052631574, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.121723118136401, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 16:40:00+00:00", "close_date": "2018-01-18 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.00269734, "close_rate": 0.002710860501253133, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.073561360451414, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-18 18:05:00+00:00", "close_date": "2018-01-18 18:30:00+00:00", "trade_duration": 25, "open_rate": 4.475e-05, "close_rate": 4.587155388471177e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2234.63687150838, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 18:25:00+00:00", "close_date": "2018-01-18 18:55:00+00:00", "trade_duration": 30, "open_rate": 2.79e-05, "close_rate": 2.8319548872180444e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3584.2293906810037, "profit_abs": 0.000999999999999987}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 20:10:00+00:00", "close_date": "2018-01-18 20:50:00+00:00", "trade_duration": 40, "open_rate": 0.04439326, "close_rate": 0.04461578260651629, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2525942001105577, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 21:30:00+00:00", "close_date": "2018-01-19 00:35:00+00:00", "trade_duration": 185, "open_rate": 4.49e-05, "close_rate": 4.51250626566416e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2227.1714922049, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 21:55:00+00:00", "close_date": "2018-01-19 05:05:00+00:00", "trade_duration": 430, "open_rate": 0.02855, "close_rate": 0.028693107769423555, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.502626970227671, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 22:10:00+00:00", "close_date": "2018-01-18 22:50:00+00:00", "trade_duration": 40, "open_rate": 5.796e-05, "close_rate": 5.8250526315789473e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1725.3278122843342, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 23:50:00+00:00", "close_date": "2018-01-19 00:30:00+00:00", "trade_duration": 40, "open_rate": 0.04340323, "close_rate": 0.04362079005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.303975994413319, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-19 16:45:00+00:00", "close_date": "2018-01-19 17:35:00+00:00", "trade_duration": 50, "open_rate": 0.04454455, "close_rate": 0.04476783095238095, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.244943545282195, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:15:00+00:00", "close_date": "2018-01-19 19:55:00+00:00", "trade_duration": 160, "open_rate": 5.62e-05, "close_rate": 5.648170426065162e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1779.3594306049824, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:20:00+00:00", "close_date": "2018-01-19 20:15:00+00:00", "trade_duration": 175, "open_rate": 4.339e-05, "close_rate": 4.360749373433584e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2304.6784973496196, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-20 04:45:00+00:00", "close_date": "2018-01-20 17:35:00+00:00", "trade_duration": 770, "open_rate": 0.0001009, "close_rate": 0.00010140576441102755, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 991.0802775024778, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 15:15:00+00:00", "trade_duration": 625, "open_rate": 0.00270505, "close_rate": 0.002718609147869674, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.96789338459548, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 07:00:00+00:00", "trade_duration": 130, "open_rate": 0.03000002, "close_rate": 0.030150396040100245, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.3333311111125927, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 09:00:00+00:00", "close_date": "2018-01-20 09:40:00+00:00", "trade_duration": 40, "open_rate": 5.46e-05, "close_rate": 5.4873684210526304e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1831.5018315018317, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-20 18:25:00+00:00", "close_date": "2018-01-25 03:50:00+00:00", "trade_duration": 6325, "open_rate": 0.03082222, "close_rate": 0.027739998, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.244412634781012, "profit_abs": -0.010474999999999998}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 22:25:00+00:00", "close_date": "2018-01-20 23:15:00+00:00", "trade_duration": 50, "open_rate": 0.08969999, "close_rate": 0.09014961401002504, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1148273260677064, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 02:50:00+00:00", "close_date": "2018-01-21 14:30:00+00:00", "trade_duration": 700, "open_rate": 0.01632501, "close_rate": 0.01640683962406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.125570520324337, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 10:20:00+00:00", "close_date": "2018-01-21 11:00:00+00:00", "trade_duration": 40, "open_rate": 0.070538, "close_rate": 0.07089157393483708, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.417675579120474, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 15:50:00+00:00", "close_date": "2018-01-21 18:45:00+00:00", "trade_duration": 175, "open_rate": 5.301e-05, "close_rate": 5.327571428571427e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.4365214110546, "profit_abs": -2.7755575615628914e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 16:20:00+00:00", "close_date": "2018-01-21 17:00:00+00:00", "trade_duration": 40, "open_rate": 3.955e-05, "close_rate": 3.9748245614035085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2528.4450063211125, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:45:00+00:00", "trade_duration": 30, "open_rate": 0.00258505, "close_rate": 0.002623922932330827, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.6839712964933, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:55:00+00:00", "trade_duration": 40, "open_rate": 3.903e-05, "close_rate": 3.922563909774435e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2562.1316935690497, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 00:35:00+00:00", "close_date": "2018-01-22 10:35:00+00:00", "trade_duration": 600, "open_rate": 5.236e-05, "close_rate": 5.262245614035087e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1909.8548510313217, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 01:30:00+00:00", "close_date": "2018-01-22 02:10:00+00:00", "trade_duration": 40, "open_rate": 9.028e-05, "close_rate": 9.07325313283208e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1107.6650420912717, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 12:25:00+00:00", "close_date": "2018-01-22 14:35:00+00:00", "trade_duration": 130, "open_rate": 0.002687, "close_rate": 0.002700468671679198, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.21622627465575, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 13:15:00+00:00", "close_date": "2018-01-22 13:55:00+00:00", "trade_duration": 40, "open_rate": 4.168e-05, "close_rate": 4.188892230576441e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2399.232245681382, "profit_abs": 1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-22 14:00:00+00:00", "close_date": "2018-01-22 14:30:00+00:00", "trade_duration": 30, "open_rate": 8.821e-05, "close_rate": 8.953646616541353e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1133.6583153837435, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 15:55:00+00:00", "close_date": "2018-01-22 16:40:00+00:00", "trade_duration": 45, "open_rate": 5.172e-05, "close_rate": 5.1979248120300745e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1933.4880123743235, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-22 16:05:00+00:00", "close_date": "2018-01-22 16:25:00+00:00", "trade_duration": 20, "open_rate": 3.026e-05, "close_rate": 3.101839598997494e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3304.692663582287, "profit_abs": 0.0020000000000000157}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 19:50:00+00:00", "close_date": "2018-01-23 00:10:00+00:00", "trade_duration": 260, "open_rate": 0.07064, "close_rate": 0.07099408521303258, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.415628539071348, "profit_abs": 1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 21:25:00+00:00", "close_date": "2018-01-22 22:05:00+00:00", "trade_duration": 40, "open_rate": 0.01644483, "close_rate": 0.01652726022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.080938507725528, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-23 00:05:00+00:00", "close_date": "2018-01-23 00:35:00+00:00", "trade_duration": 30, "open_rate": 4.331e-05, "close_rate": 4.3961278195488714e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2308.935580697299, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-23 01:50:00+00:00", "close_date": "2018-01-23 02:15:00+00:00", "trade_duration": 25, "open_rate": 3.2e-05, "close_rate": 3.2802005012531326e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3125.0000000000005, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 04:25:00+00:00", "close_date": "2018-01-23 05:15:00+00:00", "trade_duration": 50, "open_rate": 0.09167706, "close_rate": 0.09213659413533835, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0907854156754153, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 07:35:00+00:00", "close_date": "2018-01-23 09:00:00+00:00", "trade_duration": 85, "open_rate": 0.0692498, "close_rate": 0.06959691679197995, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4440474918339115, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 10:50:00+00:00", "close_date": "2018-01-23 13:05:00+00:00", "trade_duration": 135, "open_rate": 3.182e-05, "close_rate": 3.197949874686716e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3142.677561282213, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 11:05:00+00:00", "close_date": "2018-01-23 16:05:00+00:00", "trade_duration": 300, "open_rate": 0.04088, "close_rate": 0.04108491228070175, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4461839530332683, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 14:55:00+00:00", "close_date": "2018-01-23 15:35:00+00:00", "trade_duration": 40, "open_rate": 5.15e-05, "close_rate": 5.175814536340851e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1941.747572815534, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 16:35:00+00:00", "close_date": "2018-01-24 00:05:00+00:00", "trade_duration": 450, "open_rate": 0.09071698, "close_rate": 0.09117170170426064, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1023294646713329, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 17:25:00+00:00", "close_date": "2018-01-23 18:45:00+00:00", "trade_duration": 80, "open_rate": 3.128e-05, "close_rate": 3.1436791979949865e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3196.9309462915603, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 20:15:00+00:00", "close_date": "2018-01-23 22:00:00+00:00", "trade_duration": 105, "open_rate": 9.555e-05, "close_rate": 9.602894736842104e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1046.5724751439038, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 22:30:00+00:00", "close_date": "2018-01-23 23:10:00+00:00", "trade_duration": 40, "open_rate": 0.04080001, "close_rate": 0.0410045213283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.450979791426522, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 23:50:00+00:00", "close_date": "2018-01-24 03:35:00+00:00", "trade_duration": 225, "open_rate": 5.163e-05, "close_rate": 5.18887969924812e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1936.8584156498162, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 00:20:00+00:00", "close_date": "2018-01-24 01:50:00+00:00", "trade_duration": 90, "open_rate": 0.04040781, "close_rate": 0.04061035541353383, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.474769110228938, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 06:45:00+00:00", "close_date": "2018-01-24 07:25:00+00:00", "trade_duration": 40, "open_rate": 5.132e-05, "close_rate": 5.157724310776942e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1948.5580670303975, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-24 14:15:00+00:00", "close_date": "2018-01-24 14:25:00+00:00", "trade_duration": 10, "open_rate": 5.198e-05, "close_rate": 5.432496240601503e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1923.8168526356292, "profit_abs": 0.0040000000000000036}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 14:50:00+00:00", "close_date": "2018-01-24 16:35:00+00:00", "trade_duration": 105, "open_rate": 3.054e-05, "close_rate": 3.069308270676692e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3274.3942370661425, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 15:10:00+00:00", "close_date": "2018-01-24 16:15:00+00:00", "trade_duration": 65, "open_rate": 9.263e-05, "close_rate": 9.309431077694236e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1079.5638562020945, "profit_abs": 2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 22:40:00+00:00", "close_date": "2018-01-24 23:25:00+00:00", "trade_duration": 45, "open_rate": 5.514e-05, "close_rate": 5.54163909774436e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1813.5654697134569, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 00:50:00+00:00", "close_date": "2018-01-25 01:30:00+00:00", "trade_duration": 40, "open_rate": 4.921e-05, "close_rate": 4.9456666666666664e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.1072952651903, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 08:15:00+00:00", "close_date": "2018-01-25 12:15:00+00:00", "trade_duration": 240, "open_rate": 0.0026, "close_rate": 0.002613032581453634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.46153846153847, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 10:25:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 350, "open_rate": 0.02799871, "close_rate": 0.028139054411027563, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.571593119825878, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 11:00:00+00:00", "close_date": "2018-01-25 11:45:00+00:00", "trade_duration": 45, "open_rate": 0.04078902, "close_rate": 0.0409934762406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4516401717913303, "profit_abs": -1.3877787807814457e-17}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:05:00+00:00", "close_date": "2018-01-25 13:45:00+00:00", "trade_duration": 40, "open_rate": 2.89e-05, "close_rate": 2.904486215538847e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3460.2076124567475, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:20:00+00:00", "close_date": "2018-01-25 14:05:00+00:00", "trade_duration": 45, "open_rate": 0.041103, "close_rate": 0.04130903007518797, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4329124394813033, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-25 15:45:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 30, "open_rate": 5.428e-05, "close_rate": 5.509624060150376e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1842.2991893883568, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 17:45:00+00:00", "close_date": "2018-01-25 23:15:00+00:00", "trade_duration": 330, "open_rate": 5.414e-05, "close_rate": 5.441137844611528e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1847.063169560399, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 21:15:00+00:00", "close_date": "2018-01-25 21:55:00+00:00", "trade_duration": 40, "open_rate": 0.04140777, "close_rate": 0.0416153277443609, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.415005686130888, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 02:05:00+00:00", "close_date": "2018-01-26 02:45:00+00:00", "trade_duration": 40, "open_rate": 0.00254309, "close_rate": 0.002555837318295739, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.32224183965177, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 02:55:00+00:00", "close_date": "2018-01-26 15:10:00+00:00", "trade_duration": 735, "open_rate": 5.607e-05, "close_rate": 5.6351052631578935e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1783.4849295523454, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 06:10:00+00:00", "close_date": "2018-01-26 09:25:00+00:00", "trade_duration": 195, "open_rate": 0.00253806, "close_rate": 0.0025507821052631577, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.400171784748984, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 07:25:00+00:00", "close_date": "2018-01-26 09:55:00+00:00", "trade_duration": 150, "open_rate": 0.0415, "close_rate": 0.04170802005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4096385542168677, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-26 09:55:00+00:00", "close_date": "2018-01-26 10:25:00+00:00", "trade_duration": 30, "open_rate": 5.321e-05, "close_rate": 5.401015037593984e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1879.3459875963165, "profit_abs": 0.000999999999999987}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 16:05:00+00:00", "close_date": "2018-01-26 16:45:00+00:00", "trade_duration": 40, "open_rate": 0.02772046, "close_rate": 0.02785940967418546, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.6074437437185387, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 23:35:00+00:00", "close_date": "2018-01-27 00:15:00+00:00", "trade_duration": 40, "open_rate": 0.09461341, "close_rate": 0.09508766268170424, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0569326272036914, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 00:35:00+00:00", "close_date": "2018-01-27 01:30:00+00:00", "trade_duration": 55, "open_rate": 5.615e-05, "close_rate": 5.643145363408521e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1780.9439002671415, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.07877175, "open_date": "2018-01-27 00:45:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 4560, "open_rate": 5.556e-05, "close_rate": 5.144e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1799.8560115190785, "profit_abs": -0.007896868250539965}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 02:30:00+00:00", "close_date": "2018-01-27 11:25:00+00:00", "trade_duration": 535, "open_rate": 0.06900001, "close_rate": 0.06934587471177944, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492751522789635, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 06:25:00+00:00", "close_date": "2018-01-27 07:05:00+00:00", "trade_duration": 40, "open_rate": 0.09449985, "close_rate": 0.0949735334586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.058202737887944, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.04815133, "open_date": "2018-01-27 09:40:00+00:00", "close_date": "2018-01-30 04:40:00+00:00", "trade_duration": 4020, "open_rate": 0.0410697, "close_rate": 0.03928809, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4348850855983852, "profit_abs": -0.004827170578309559}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 11:45:00+00:00", "close_date": "2018-01-27 12:30:00+00:00", "trade_duration": 45, "open_rate": 0.0285, "close_rate": 0.02864285714285714, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5087719298245617, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 12:35:00+00:00", "close_date": "2018-01-27 15:25:00+00:00", "trade_duration": 170, "open_rate": 0.02866372, "close_rate": 0.02880739779448621, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.4887307020861216, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 15:50:00+00:00", "close_date": "2018-01-27 16:50:00+00:00", "trade_duration": 60, "open_rate": 0.095381, "close_rate": 0.09585910025062656, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0484268355332824, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 17:05:00+00:00", "close_date": "2018-01-27 17:45:00+00:00", "trade_duration": 40, "open_rate": 0.06759092, "close_rate": 0.06792972160401002, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4794886650455417, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 23:40:00+00:00", "close_date": "2018-01-28 01:05:00+00:00", "trade_duration": 85, "open_rate": 0.00258501, "close_rate": 0.002597967443609022, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.684569885609726, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 02:25:00+00:00", "close_date": "2018-01-28 08:10:00+00:00", "trade_duration": 345, "open_rate": 0.06698502, "close_rate": 0.0673207845112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4928710926711672, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 10:25:00+00:00", "close_date": "2018-01-28 16:30:00+00:00", "trade_duration": 365, "open_rate": 0.0677177, "close_rate": 0.06805713709273183, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4767187899175547, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-28 20:35:00+00:00", "close_date": "2018-01-28 21:35:00+00:00", "trade_duration": 60, "open_rate": 5.215e-05, "close_rate": 5.2411403508771925e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1917.5455417066157, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-28 22:00:00+00:00", "close_date": "2018-01-28 22:30:00+00:00", "trade_duration": 30, "open_rate": 0.00273809, "close_rate": 0.002779264285714285, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.5218089982433, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-29 00:00:00+00:00", "close_date": "2018-01-29 00:30:00+00:00", "trade_duration": 30, "open_rate": 0.00274632, "close_rate": 0.002787618045112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.412362725392526, "profit_abs": 0.0010000000000000148}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-29 02:15:00+00:00", "close_date": "2018-01-29 03:00:00+00:00", "trade_duration": 45, "open_rate": 0.01622478, "close_rate": 0.016306107218045113, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.163411768911504, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 03:05:00+00:00", "close_date": "2018-01-29 03:45:00+00:00", "trade_duration": 40, "open_rate": 0.069, "close_rate": 0.06934586466165413, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492753623188406, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 05:20:00+00:00", "close_date": "2018-01-29 06:55:00+00:00", "trade_duration": 95, "open_rate": 8.755e-05, "close_rate": 8.798884711779448e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1142.204454597373, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 07:00:00+00:00", "close_date": "2018-01-29 19:25:00+00:00", "trade_duration": 745, "open_rate": 0.06825763, "close_rate": 0.06859977350877192, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4650376815016872, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 19:45:00+00:00", "close_date": "2018-01-29 20:25:00+00:00", "trade_duration": 40, "open_rate": 0.06713892, "close_rate": 0.06747545593984962, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4894490408841845, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0199116, "open_date": "2018-01-29 23:30:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 315, "open_rate": 8.934e-05, "close_rate": 8.8e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1119.3194537721067, "profit_abs": -0.0019961383478844796}], "results_per_pair": [{"key": "TRX/BTC", "trades": 15, "profit_mean": 0.0023467073333333323, "profit_mean_pct": 0.23467073333333321, "profit_sum": 0.035200609999999986, "profit_sum_pct": 3.5200609999999988, "profit_total_abs": 0.0035288616521155086, "profit_total_pct": 1.1733536666666662, "duration_avg": "2:28:00", "wins": 9, "draws": 2, "losses": 4}, {"key": "ADA/BTC", "trades": 29, "profit_mean": -0.0011598141379310352, "profit_mean_pct": -0.11598141379310352, "profit_sum": -0.03363461000000002, "profit_sum_pct": -3.3634610000000023, "profit_total_abs": -0.0033718682505400333, "profit_total_pct": -1.1211536666666675, "duration_avg": "5:35:00", "wins": 9, "draws": 11, "losses": 9}, {"key": "XLM/BTC", "trades": 21, "profit_mean": 0.0026243899999999994, "profit_mean_pct": 0.2624389999999999, "profit_sum": 0.05511218999999999, "profit_sum_pct": 5.511218999999999, "profit_total_abs": 0.005525000000000002, "profit_total_pct": 1.8370729999999995, "duration_avg": "3:21:00", "wins": 12, "draws": 3, "losses": 6}, {"key": "ETH/BTC", "trades": 21, "profit_mean": 0.0009500057142857142, "profit_mean_pct": 0.09500057142857142, "profit_sum": 0.01995012, "profit_sum_pct": 1.9950119999999998, "profit_total_abs": 0.0019999999999999463, "profit_total_pct": 0.6650039999999999, "duration_avg": "2:17:00", "wins": 5, "draws": 10, "losses": 6}, {"key": "XMR/BTC", "trades": 16, "profit_mean": -0.0027899012500000007, "profit_mean_pct": -0.2789901250000001, "profit_sum": -0.04463842000000001, "profit_sum_pct": -4.463842000000001, "profit_total_abs": -0.0044750000000000345, "profit_total_pct": -1.4879473333333337, "duration_avg": "8:41:00", "wins": 6, "draws": 5, "losses": 5}, {"key": "ZEC/BTC", "trades": 21, "profit_mean": -0.00039290904761904774, "profit_mean_pct": -0.03929090476190478, "profit_sum": -0.008251090000000003, "profit_sum_pct": -0.8251090000000003, "profit_total_abs": -0.000827170578309569, "profit_total_pct": -0.27503633333333344, "duration_avg": "4:17:00", "wins": 8, "draws": 7, "losses": 6}, {"key": "NXT/BTC", "trades": 12, "profit_mean": -0.0012261025000000006, "profit_mean_pct": -0.12261025000000006, "profit_sum": -0.014713230000000008, "profit_sum_pct": -1.4713230000000008, "profit_total_abs": -0.0014750000000000874, "profit_total_pct": -0.4904410000000003, "duration_avg": "0:57:00", "wins": 4, "draws": 3, "losses": 5}, {"key": "LTC/BTC", "trades": 8, "profit_mean": 0.00748129625, "profit_mean_pct": 0.748129625, "profit_sum": 0.05985037, "profit_sum_pct": 5.985037, "profit_total_abs": 0.006000000000000019, "profit_total_pct": 1.9950123333333334, "duration_avg": "1:59:00", "wins": 5, "draws": 2, "losses": 1}, {"key": "ETC/BTC", "trades": 20, "profit_mean": 0.0022568569999999997, "profit_mean_pct": 0.22568569999999996, "profit_sum": 0.04513713999999999, "profit_sum_pct": 4.513713999999999, "profit_total_abs": 0.004525000000000001, "profit_total_pct": 1.504571333333333, "duration_avg": "1:45:00", "wins": 11, "draws": 4, "losses": 5}, {"key": "DASH/BTC", "trades": 16, "profit_mean": 0.0018703237499999997, "profit_mean_pct": 0.18703237499999997, "profit_sum": 0.029925179999999996, "profit_sum_pct": 2.9925179999999996, "profit_total_abs": 0.002999999999999961, "profit_total_pct": 0.9975059999999999, "duration_avg": "3:03:00", "wins": 4, "draws": 7, "losses": 5}, {"key": "TOTAL", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}], "sell_reason_summary": [{"sell_reason": "roi", "trades": 170, "wins": 73, "draws": 54, "losses": 43, "profit_mean": 0.005398268352941177, "profit_mean_pct": 0.54, "profit_sum": 0.91770562, "profit_sum_pct": 91.77, "profit_total_abs": 0.09199999999999964, "profit_pct_total": 30.59}, {"sell_reason": "stop_loss", "trades": 6, "wins": 0, "draws": 0, "losses": 6, "profit_mean": -0.10448878000000002, "profit_mean_pct": -10.45, "profit_sum": -0.6269326800000001, "profit_sum_pct": -62.69, "profit_total_abs": -0.06284999999999992, "profit_pct_total": -20.9}, {"sell_reason": "force_sell", "trades": 3, "wins": 0, "draws": 0, "losses": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.89, "profit_sum": -0.14683468, "profit_sum_pct": -14.68, "profit_total_abs": -0.014720177176734003, "profit_pct_total": -4.89}], "left_open_trades": [{"key": "TRX/BTC", "trades": 1, "profit_mean": -0.0199116, "profit_mean_pct": -1.9911600000000003, "profit_sum": -0.0199116, "profit_sum_pct": -1.9911600000000003, "profit_total_abs": -0.0019961383478844796, "profit_total_pct": -0.6637200000000001, "duration_avg": "5:15:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ADA/BTC", "trades": 1, "profit_mean": -0.07877175, "profit_mean_pct": -7.877175, "profit_sum": -0.07877175, "profit_sum_pct": -7.877175, "profit_total_abs": -0.007896868250539965, "profit_total_pct": -2.625725, "duration_avg": "3 days, 4:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ZEC/BTC", "trades": 1, "profit_mean": -0.04815133, "profit_mean_pct": -4.815133, "profit_sum": -0.04815133, "profit_sum_pct": -4.815133, "profit_total_abs": -0.004827170578309559, "profit_total_pct": -1.6050443333333335, "duration_avg": "2 days, 19:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "TOTAL", "trades": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.894489333333333, "profit_sum": -0.14683468, "profit_sum_pct": -14.683468, "profit_total_abs": -0.014720177176734003, "profit_total_pct": -4.8944893333333335, "duration_avg": "2 days, 1:25:00", "wins": 0, "draws": 0, "losses": 3}], "total_trades": 179, "backtest_start": "2018-01-30 04:45:00+00:00", "backtest_start_ts": 1517287500, "backtest_end": "2018-01-30 04:45:00+00:00", "backtest_end_ts": 1517287500, "backtest_days": 0, "trades_per_day": null, "market_change": 0.25, "stake_amount": 0.1, "max_drawdown": 0.21142322000000008, "drawdown_start": "2018-01-24 14:25:00+00:00", "drawdown_start_ts": 1516803900.0, "drawdown_end": "2018-01-30 04:45:00+00:00", "drawdown_end_ts": 1517287500.0}, "TestStrategy": {"trades": [{"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:20:00+00:00", "trade_duration": 5, "open_rate": 9.64e-05, "close_rate": 0.00010074887218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1037.344398340249, "profit_abs": 0.00399999999999999}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:30:00+00:00", "trade_duration": 15, "open_rate": 4.756e-05, "close_rate": 4.9705563909774425e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2102.6072329688814, "profit_abs": 0.00399999999999999}, {"pair": "XLM/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:35:00+00:00", "trade_duration": 10, "open_rate": 3.339e-05, "close_rate": 3.489631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2994.908655286014, "profit_abs": 0.0040000000000000036}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:40:00+00:00", "trade_duration": 15, "open_rate": 9.696e-05, "close_rate": 0.00010133413533834584, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1031.3531353135315, "profit_abs": 0.00399999999999999}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 07:35:00+00:00", "close_date": "2018-01-10 08:35:00+00:00", "trade_duration": 60, "open_rate": 0.0943, "close_rate": 0.09477268170426063, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0604453870625663, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 07:40:00+00:00", "close_date": "2018-01-10 08:10:00+00:00", "trade_duration": 30, "open_rate": 0.02719607, "close_rate": 0.02760503345864661, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.677001860930642, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 08:15:00+00:00", "close_date": "2018-01-10 09:55:00+00:00", "trade_duration": 100, "open_rate": 0.04634952, "close_rate": 0.046581848421052625, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1575196463739, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 14:45:00+00:00", "close_date": "2018-01-10 15:50:00+00:00", "trade_duration": 65, "open_rate": 3.066e-05, "close_rate": 3.081368421052631e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3261.5786040443577, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 16:35:00+00:00", "close_date": "2018-01-10 17:15:00+00:00", "trade_duration": 40, "open_rate": 0.0168999, "close_rate": 0.016984611278195488, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.917194776300452, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 16:40:00+00:00", "close_date": "2018-01-10 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.09132568, "close_rate": 0.0917834528320802, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0949822656672252, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 18:50:00+00:00", "close_date": "2018-01-10 19:45:00+00:00", "trade_duration": 55, "open_rate": 0.08898003, "close_rate": 0.08942604518796991, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1238476768326557, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 22:15:00+00:00", "close_date": "2018-01-10 23:00:00+00:00", "trade_duration": 45, "open_rate": 0.08560008, "close_rate": 0.08602915308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1682232072680307, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 22:50:00+00:00", "close_date": "2018-01-10 23:20:00+00:00", "trade_duration": 30, "open_rate": 0.00249083, "close_rate": 0.0025282860902255634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 40.147260150231055, "profit_abs": 0.000999999999999987}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 23:15:00+00:00", "close_date": "2018-01-11 00:15:00+00:00", "trade_duration": 60, "open_rate": 3.022e-05, "close_rate": 3.037147869674185e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3309.0668431502318, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-10 23:40:00+00:00", "close_date": "2018-01-11 00:05:00+00:00", "trade_duration": 25, "open_rate": 0.002437, "close_rate": 0.0024980776942355883, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.03405826836274, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 00:00:00+00:00", "close_date": "2018-01-11 00:35:00+00:00", "trade_duration": 35, "open_rate": 0.04771803, "close_rate": 0.04843559436090225, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0956439316543456, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-11 03:40:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 45, "open_rate": 3.651e-05, "close_rate": 3.2859000000000005e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2738.9756231169545, "profit_abs": -0.01047499999999997}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 03:55:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 30, "open_rate": 0.08824105, "close_rate": 0.08956798308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1332594070446804, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 04:00:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 50, "open_rate": 0.00243, "close_rate": 0.002442180451127819, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.1522633744856, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:55:00+00:00", "trade_duration": 25, "open_rate": 0.04545064, "close_rate": 0.046589753784461146, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.200189040242338, "profit_abs": 0.001999999999999988}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 20, "open_rate": 3.372e-05, "close_rate": 3.456511278195488e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2965.599051008304, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:55:00+00:00", "close_date": "2018-01-11 05:15:00+00:00", "trade_duration": 20, "open_rate": 0.02644, "close_rate": 0.02710265664160401, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7821482602118004, "profit_abs": 0.001999999999999988}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:20:00+00:00", "close_date": "2018-01-11 12:00:00+00:00", "trade_duration": 40, "open_rate": 0.08812, "close_rate": 0.08856170426065162, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1348161597821154, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:35:00+00:00", "close_date": "2018-01-11 12:15:00+00:00", "trade_duration": 40, "open_rate": 0.02683577, "close_rate": 0.026970285137844607, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7263696923919087, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 14:00:00+00:00", "close_date": "2018-01-11 14:25:00+00:00", "trade_duration": 25, "open_rate": 4.919e-05, "close_rate": 5.04228320802005e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.9335230737956, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 19:25:00+00:00", "close_date": "2018-01-11 20:35:00+00:00", "trade_duration": 70, "open_rate": 0.08784896, "close_rate": 0.08828930566416039, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1383174029607181, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:35:00+00:00", "close_date": "2018-01-11 23:30:00+00:00", "trade_duration": 55, "open_rate": 5.105e-05, "close_rate": 5.130588972431077e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1958.8638589618022, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:25:00+00:00", "trade_duration": 30, "open_rate": 3.96e-05, "close_rate": 4.019548872180451e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2525.252525252525, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:35:00+00:00", "trade_duration": 40, "open_rate": 2.885e-05, "close_rate": 2.899461152882205e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3466.204506065858, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 23:30:00+00:00", "close_date": "2018-01-12 00:05:00+00:00", "trade_duration": 35, "open_rate": 0.02645, "close_rate": 0.026847744360902256, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.780718336483932, "profit_abs": 0.0010000000000000148}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 23:55:00+00:00", "close_date": "2018-01-12 01:15:00+00:00", "trade_duration": 80, "open_rate": 0.048, "close_rate": 0.04824060150375939, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0833333333333335, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-12 21:15:00+00:00", "close_date": "2018-01-12 21:40:00+00:00", "trade_duration": 25, "open_rate": 4.692e-05, "close_rate": 4.809593984962405e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2131.287297527707, "profit_abs": 0.001999999999999974}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 00:55:00+00:00", "close_date": "2018-01-13 06:20:00+00:00", "trade_duration": 325, "open_rate": 0.00256966, "close_rate": 0.0025825405012531327, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.91565421106294, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 10:55:00+00:00", "close_date": "2018-01-13 11:35:00+00:00", "trade_duration": 40, "open_rate": 6.262e-05, "close_rate": 6.293388471177944e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1596.933886937081, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 13:05:00+00:00", "close_date": "2018-01-15 14:10:00+00:00", "trade_duration": 2945, "open_rate": 4.73e-05, "close_rate": 4.753709273182957e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2114.1649048625795, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:30:00+00:00", "close_date": "2018-01-13 14:45:00+00:00", "trade_duration": 75, "open_rate": 6.063e-05, "close_rate": 6.0933909774436085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1649.348507339601, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:40:00+00:00", "close_date": "2018-01-13 23:30:00+00:00", "trade_duration": 590, "open_rate": 0.00011082, "close_rate": 0.00011137548872180448, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 902.3641941887746, "profit_abs": -2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 15:15:00+00:00", "close_date": "2018-01-13 15:55:00+00:00", "trade_duration": 40, "open_rate": 5.93e-05, "close_rate": 5.9597243107769415e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1686.3406408094436, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 16:30:00+00:00", "close_date": "2018-01-13 17:10:00+00:00", "trade_duration": 40, "open_rate": 0.04850003, "close_rate": 0.04874313791979949, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0618543947292407, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 22:05:00+00:00", "close_date": "2018-01-14 06:25:00+00:00", "trade_duration": 500, "open_rate": 0.09825019, "close_rate": 0.09874267215538848, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0178097365511456, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 00:20:00+00:00", "close_date": "2018-01-14 22:55:00+00:00", "trade_duration": 1355, "open_rate": 6.018e-05, "close_rate": 6.048165413533834e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1661.681621801263, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 12:45:00+00:00", "close_date": "2018-01-14 13:25:00+00:00", "trade_duration": 40, "open_rate": 0.09758999, "close_rate": 0.0980791628822055, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.024695258191952, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-14 15:30:00+00:00", "close_date": "2018-01-14 16:00:00+00:00", "trade_duration": 30, "open_rate": 0.00311, "close_rate": 0.0031567669172932328, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.154340836012864, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 20:45:00+00:00", "close_date": "2018-01-14 22:15:00+00:00", "trade_duration": 90, "open_rate": 0.00312401, "close_rate": 0.003139669197994987, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.010140812609436, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 23:35:00+00:00", "close_date": "2018-01-15 00:30:00+00:00", "trade_duration": 55, "open_rate": 0.0174679, "close_rate": 0.017555458395989976, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.724786608579165, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 23:45:00+00:00", "close_date": "2018-01-15 00:25:00+00:00", "trade_duration": 40, "open_rate": 0.07346846, "close_rate": 0.07383672295739348, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.3611282991367997, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 02:25:00+00:00", "close_date": "2018-01-15 03:05:00+00:00", "trade_duration": 40, "open_rate": 0.097994, "close_rate": 0.09848519799498744, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.020470641059657, "profit_abs": -2.7755575615628914e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 07:20:00+00:00", "close_date": "2018-01-15 08:00:00+00:00", "trade_duration": 40, "open_rate": 0.09659, "close_rate": 0.09707416040100247, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0353038616834043, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-15 08:20:00+00:00", "close_date": "2018-01-15 08:55:00+00:00", "trade_duration": 35, "open_rate": 9.987e-05, "close_rate": 0.00010137180451127818, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1001.3016921998599, "profit_abs": 0.0010000000000000009}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-15 12:10:00+00:00", "close_date": "2018-01-16 02:50:00+00:00", "trade_duration": 880, "open_rate": 0.0948969, "close_rate": 0.09537257368421052, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0537752023511833, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:10:00+00:00", "close_date": "2018-01-15 17:40:00+00:00", "trade_duration": 210, "open_rate": 0.071, "close_rate": 0.07135588972431077, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4084507042253522, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:30:00+00:00", "close_date": "2018-01-15 15:10:00+00:00", "trade_duration": 40, "open_rate": 0.04600501, "close_rate": 0.046235611553884705, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.173676301776698, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:10:00+00:00", "close_date": "2018-01-15 19:25:00+00:00", "trade_duration": 75, "open_rate": 9.438e-05, "close_rate": 9.485308270676693e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1059.5465140919687, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:35:00+00:00", "close_date": "2018-01-15 19:15:00+00:00", "trade_duration": 40, "open_rate": 0.03040001, "close_rate": 0.030552391002506264, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.2894726021471703, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-15 20:25:00+00:00", "close_date": "2018-01-16 08:25:00+00:00", "trade_duration": 720, "open_rate": 5.837e-05, "close_rate": 5.2533e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1713.2088401576154, "profit_abs": -0.010474999999999984}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 20:40:00+00:00", "close_date": "2018-01-15 22:00:00+00:00", "trade_duration": 80, "open_rate": 0.046036, "close_rate": 0.04626675689223057, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1722130506560084, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 00:30:00+00:00", "close_date": "2018-01-16 01:10:00+00:00", "trade_duration": 40, "open_rate": 0.0028685, "close_rate": 0.0028828784461152877, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 34.86142583231654, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 01:15:00+00:00", "close_date": "2018-01-16 02:35:00+00:00", "trade_duration": 80, "open_rate": 0.06731755, "close_rate": 0.0676549813283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4854967241083492, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 07:45:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 55, "open_rate": 0.09217614, "close_rate": 0.09263817578947368, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0848794492804754, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:55:00+00:00", "trade_duration": 20, "open_rate": 0.0165, "close_rate": 0.016913533834586467, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.0606060606060606, "profit_abs": 0.0020000000000000018}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 5, "open_rate": 7.953e-05, "close_rate": 8.311781954887218e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1257.387149503332, "profit_abs": 0.00399999999999999}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 08:45:00+00:00", "close_date": "2018-01-16 09:50:00+00:00", "trade_duration": 65, "open_rate": 0.045202, "close_rate": 0.04542857644110275, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2122914915269236, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:45:00+00:00", "trade_duration": 30, "open_rate": 5.248e-05, "close_rate": 5.326917293233082e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1905.487804878049, "profit_abs": 0.0010000000000000009}, {"pair": "XMR/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:55:00+00:00", "trade_duration": 40, "open_rate": 0.02892318, "close_rate": 0.02906815834586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.457434486802627, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 09:50:00+00:00", "close_date": "2018-01-16 10:10:00+00:00", "trade_duration": 20, "open_rate": 5.158e-05, "close_rate": 5.287273182957392e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1938.735944164405, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:35:00+00:00", "trade_duration": 30, "open_rate": 0.02828232, "close_rate": 0.02870761804511278, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5357778286929786, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:40:00+00:00", "trade_duration": 35, "open_rate": 0.04357584, "close_rate": 0.044231115789473675, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.294849623093898, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 13:45:00+00:00", "close_date": "2018-01-16 14:20:00+00:00", "trade_duration": 35, "open_rate": 5.362e-05, "close_rate": 5.442631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1864.975755315181, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 17:30:00+00:00", "close_date": "2018-01-16 18:25:00+00:00", "trade_duration": 55, "open_rate": 5.302e-05, "close_rate": 5.328576441102756e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.0807242549984, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:45:00+00:00", "trade_duration": 30, "open_rate": 0.09129999, "close_rate": 0.09267292218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0952903718828448, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:35:00+00:00", "trade_duration": 20, "open_rate": 3.808e-05, "close_rate": 3.903438596491228e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2626.0504201680674, "profit_abs": 0.0020000000000000018}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 19:00:00+00:00", "close_date": "2018-01-16 19:30:00+00:00", "trade_duration": 30, "open_rate": 0.02811012, "close_rate": 0.028532828571428567, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.557437677249333, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 60, "open_rate": 0.00258379, "close_rate": 0.002325411, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.702835756775904, "profit_abs": -0.010474999999999984}, {"pair": "NXT/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 80, "open_rate": 2.559e-05, "close_rate": 2.3031e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3907.7764751856193, "profit_abs": -0.010474999999999998}, {"pair": "TRX/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:35:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 50, "open_rate": 7.62e-05, "close_rate": 6.858e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1312.3359580052495, "profit_abs": -0.010474999999999984}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:35:00+00:00", "trade_duration": 5, "open_rate": 0.00229844, "close_rate": 0.002402129022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 43.507770487809125, "profit_abs": 0.004000000000000017}, {"pair": "LTC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:40:00+00:00", "trade_duration": 10, "open_rate": 0.0151, "close_rate": 0.015781203007518795, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.622516556291391, "profit_abs": 0.00399999999999999}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:40:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 5, "open_rate": 0.00235676, "close_rate": 0.00246308, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 42.431134269081284, "profit_abs": 0.0040000000000000036}, {"pair": "DASH/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 22:45:00+00:00", "close_date": "2018-01-16 23:05:00+00:00", "trade_duration": 20, "open_rate": 0.0630692, "close_rate": 0.06464988170426066, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.585559988076589, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:50:00+00:00", "close_date": "2018-01-16 22:55:00+00:00", "trade_duration": 5, "open_rate": 2.2e-05, "close_rate": 2.299248120300751e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 4545.454545454546, "profit_abs": 0.003999999999999976}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-17 03:30:00+00:00", "close_date": "2018-01-17 04:00:00+00:00", "trade_duration": 30, "open_rate": 4.974e-05, "close_rate": 5.048796992481203e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2010.454362685967, "profit_abs": 0.0010000000000000009}, {"pair": "TRX/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-17 03:55:00+00:00", "close_date": "2018-01-17 04:15:00+00:00", "trade_duration": 20, "open_rate": 7.108e-05, "close_rate": 7.28614536340852e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1406.8655036578502, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 09:35:00+00:00", "close_date": "2018-01-17 10:15:00+00:00", "trade_duration": 40, "open_rate": 0.04327, "close_rate": 0.04348689223057644, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.3110700254217704, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:20:00+00:00", "close_date": "2018-01-17 17:00:00+00:00", "trade_duration": 400, "open_rate": 4.997e-05, "close_rate": 5.022047619047618e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2001.2007204322595, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:25:00+00:00", "trade_duration": 55, "open_rate": 0.06836818, "close_rate": 0.06871087764411027, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4626687444363737, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:10:00+00:00", "trade_duration": 40, "open_rate": 3.63e-05, "close_rate": 3.648195488721804e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2754.8209366391184, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:30:00+00:00", "close_date": "2018-01-17 22:05:00+00:00", "trade_duration": 575, "open_rate": 0.0281, "close_rate": 0.02824085213032581, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5587188612099645, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:35:00+00:00", "close_date": "2018-01-17 16:55:00+00:00", "trade_duration": 260, "open_rate": 0.08651001, "close_rate": 0.08694364413533832, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1559355963546878, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 05:00:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 55, "open_rate": 5.633e-05, "close_rate": 5.6612355889724306e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1775.2529735487308, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 05:20:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 35, "open_rate": 0.06988494, "close_rate": 0.07093584135338346, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.430923457900944, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 07:35:00+00:00", "close_date": "2018-01-18 08:15:00+00:00", "trade_duration": 40, "open_rate": 5.545e-05, "close_rate": 5.572794486215538e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1803.4265103697026, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 09:00:00+00:00", "close_date": "2018-01-18 09:40:00+00:00", "trade_duration": 40, "open_rate": 0.01633527, "close_rate": 0.016417151052631574, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.121723118136401, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 16:40:00+00:00", "close_date": "2018-01-18 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.00269734, "close_rate": 0.002710860501253133, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.073561360451414, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-18 18:05:00+00:00", "close_date": "2018-01-18 18:30:00+00:00", "trade_duration": 25, "open_rate": 4.475e-05, "close_rate": 4.587155388471177e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2234.63687150838, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 18:25:00+00:00", "close_date": "2018-01-18 18:55:00+00:00", "trade_duration": 30, "open_rate": 2.79e-05, "close_rate": 2.8319548872180444e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3584.2293906810037, "profit_abs": 0.000999999999999987}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 20:10:00+00:00", "close_date": "2018-01-18 20:50:00+00:00", "trade_duration": 40, "open_rate": 0.04439326, "close_rate": 0.04461578260651629, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2525942001105577, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 21:30:00+00:00", "close_date": "2018-01-19 00:35:00+00:00", "trade_duration": 185, "open_rate": 4.49e-05, "close_rate": 4.51250626566416e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2227.1714922049, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 21:55:00+00:00", "close_date": "2018-01-19 05:05:00+00:00", "trade_duration": 430, "open_rate": 0.02855, "close_rate": 0.028693107769423555, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.502626970227671, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 22:10:00+00:00", "close_date": "2018-01-18 22:50:00+00:00", "trade_duration": 40, "open_rate": 5.796e-05, "close_rate": 5.8250526315789473e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1725.3278122843342, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 23:50:00+00:00", "close_date": "2018-01-19 00:30:00+00:00", "trade_duration": 40, "open_rate": 0.04340323, "close_rate": 0.04362079005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.303975994413319, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-19 16:45:00+00:00", "close_date": "2018-01-19 17:35:00+00:00", "trade_duration": 50, "open_rate": 0.04454455, "close_rate": 0.04476783095238095, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.244943545282195, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:15:00+00:00", "close_date": "2018-01-19 19:55:00+00:00", "trade_duration": 160, "open_rate": 5.62e-05, "close_rate": 5.648170426065162e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1779.3594306049824, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:20:00+00:00", "close_date": "2018-01-19 20:15:00+00:00", "trade_duration": 175, "open_rate": 4.339e-05, "close_rate": 4.360749373433584e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2304.6784973496196, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-20 04:45:00+00:00", "close_date": "2018-01-20 17:35:00+00:00", "trade_duration": 770, "open_rate": 0.0001009, "close_rate": 0.00010140576441102755, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 991.0802775024778, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 15:15:00+00:00", "trade_duration": 625, "open_rate": 0.00270505, "close_rate": 0.002718609147869674, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.96789338459548, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 07:00:00+00:00", "trade_duration": 130, "open_rate": 0.03000002, "close_rate": 0.030150396040100245, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.3333311111125927, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 09:00:00+00:00", "close_date": "2018-01-20 09:40:00+00:00", "trade_duration": 40, "open_rate": 5.46e-05, "close_rate": 5.4873684210526304e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1831.5018315018317, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-20 18:25:00+00:00", "close_date": "2018-01-25 03:50:00+00:00", "trade_duration": 6325, "open_rate": 0.03082222, "close_rate": 0.027739998, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.244412634781012, "profit_abs": -0.010474999999999998}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 22:25:00+00:00", "close_date": "2018-01-20 23:15:00+00:00", "trade_duration": 50, "open_rate": 0.08969999, "close_rate": 0.09014961401002504, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1148273260677064, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 02:50:00+00:00", "close_date": "2018-01-21 14:30:00+00:00", "trade_duration": 700, "open_rate": 0.01632501, "close_rate": 0.01640683962406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.125570520324337, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 10:20:00+00:00", "close_date": "2018-01-21 11:00:00+00:00", "trade_duration": 40, "open_rate": 0.070538, "close_rate": 0.07089157393483708, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.417675579120474, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 15:50:00+00:00", "close_date": "2018-01-21 18:45:00+00:00", "trade_duration": 175, "open_rate": 5.301e-05, "close_rate": 5.327571428571427e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.4365214110546, "profit_abs": -2.7755575615628914e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 16:20:00+00:00", "close_date": "2018-01-21 17:00:00+00:00", "trade_duration": 40, "open_rate": 3.955e-05, "close_rate": 3.9748245614035085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2528.4450063211125, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:45:00+00:00", "trade_duration": 30, "open_rate": 0.00258505, "close_rate": 0.002623922932330827, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.6839712964933, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:55:00+00:00", "trade_duration": 40, "open_rate": 3.903e-05, "close_rate": 3.922563909774435e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2562.1316935690497, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 00:35:00+00:00", "close_date": "2018-01-22 10:35:00+00:00", "trade_duration": 600, "open_rate": 5.236e-05, "close_rate": 5.262245614035087e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1909.8548510313217, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 01:30:00+00:00", "close_date": "2018-01-22 02:10:00+00:00", "trade_duration": 40, "open_rate": 9.028e-05, "close_rate": 9.07325313283208e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1107.6650420912717, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 12:25:00+00:00", "close_date": "2018-01-22 14:35:00+00:00", "trade_duration": 130, "open_rate": 0.002687, "close_rate": 0.002700468671679198, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.21622627465575, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 13:15:00+00:00", "close_date": "2018-01-22 13:55:00+00:00", "trade_duration": 40, "open_rate": 4.168e-05, "close_rate": 4.188892230576441e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2399.232245681382, "profit_abs": 1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-22 14:00:00+00:00", "close_date": "2018-01-22 14:30:00+00:00", "trade_duration": 30, "open_rate": 8.821e-05, "close_rate": 8.953646616541353e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1133.6583153837435, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 15:55:00+00:00", "close_date": "2018-01-22 16:40:00+00:00", "trade_duration": 45, "open_rate": 5.172e-05, "close_rate": 5.1979248120300745e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1933.4880123743235, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-22 16:05:00+00:00", "close_date": "2018-01-22 16:25:00+00:00", "trade_duration": 20, "open_rate": 3.026e-05, "close_rate": 3.101839598997494e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3304.692663582287, "profit_abs": 0.0020000000000000157}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 19:50:00+00:00", "close_date": "2018-01-23 00:10:00+00:00", "trade_duration": 260, "open_rate": 0.07064, "close_rate": 0.07099408521303258, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.415628539071348, "profit_abs": 1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 21:25:00+00:00", "close_date": "2018-01-22 22:05:00+00:00", "trade_duration": 40, "open_rate": 0.01644483, "close_rate": 0.01652726022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.080938507725528, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-23 00:05:00+00:00", "close_date": "2018-01-23 00:35:00+00:00", "trade_duration": 30, "open_rate": 4.331e-05, "close_rate": 4.3961278195488714e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2308.935580697299, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-23 01:50:00+00:00", "close_date": "2018-01-23 02:15:00+00:00", "trade_duration": 25, "open_rate": 3.2e-05, "close_rate": 3.2802005012531326e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3125.0000000000005, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 04:25:00+00:00", "close_date": "2018-01-23 05:15:00+00:00", "trade_duration": 50, "open_rate": 0.09167706, "close_rate": 0.09213659413533835, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0907854156754153, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 07:35:00+00:00", "close_date": "2018-01-23 09:00:00+00:00", "trade_duration": 85, "open_rate": 0.0692498, "close_rate": 0.06959691679197995, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4440474918339115, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 10:50:00+00:00", "close_date": "2018-01-23 13:05:00+00:00", "trade_duration": 135, "open_rate": 3.182e-05, "close_rate": 3.197949874686716e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3142.677561282213, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 11:05:00+00:00", "close_date": "2018-01-23 16:05:00+00:00", "trade_duration": 300, "open_rate": 0.04088, "close_rate": 0.04108491228070175, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4461839530332683, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 14:55:00+00:00", "close_date": "2018-01-23 15:35:00+00:00", "trade_duration": 40, "open_rate": 5.15e-05, "close_rate": 5.175814536340851e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1941.747572815534, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 16:35:00+00:00", "close_date": "2018-01-24 00:05:00+00:00", "trade_duration": 450, "open_rate": 0.09071698, "close_rate": 0.09117170170426064, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1023294646713329, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 17:25:00+00:00", "close_date": "2018-01-23 18:45:00+00:00", "trade_duration": 80, "open_rate": 3.128e-05, "close_rate": 3.1436791979949865e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3196.9309462915603, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 20:15:00+00:00", "close_date": "2018-01-23 22:00:00+00:00", "trade_duration": 105, "open_rate": 9.555e-05, "close_rate": 9.602894736842104e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1046.5724751439038, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 22:30:00+00:00", "close_date": "2018-01-23 23:10:00+00:00", "trade_duration": 40, "open_rate": 0.04080001, "close_rate": 0.0410045213283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.450979791426522, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 23:50:00+00:00", "close_date": "2018-01-24 03:35:00+00:00", "trade_duration": 225, "open_rate": 5.163e-05, "close_rate": 5.18887969924812e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1936.8584156498162, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 00:20:00+00:00", "close_date": "2018-01-24 01:50:00+00:00", "trade_duration": 90, "open_rate": 0.04040781, "close_rate": 0.04061035541353383, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.474769110228938, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 06:45:00+00:00", "close_date": "2018-01-24 07:25:00+00:00", "trade_duration": 40, "open_rate": 5.132e-05, "close_rate": 5.157724310776942e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1948.5580670303975, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-24 14:15:00+00:00", "close_date": "2018-01-24 14:25:00+00:00", "trade_duration": 10, "open_rate": 5.198e-05, "close_rate": 5.432496240601503e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1923.8168526356292, "profit_abs": 0.0040000000000000036}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 14:50:00+00:00", "close_date": "2018-01-24 16:35:00+00:00", "trade_duration": 105, "open_rate": 3.054e-05, "close_rate": 3.069308270676692e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3274.3942370661425, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 15:10:00+00:00", "close_date": "2018-01-24 16:15:00+00:00", "trade_duration": 65, "open_rate": 9.263e-05, "close_rate": 9.309431077694236e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1079.5638562020945, "profit_abs": 2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 22:40:00+00:00", "close_date": "2018-01-24 23:25:00+00:00", "trade_duration": 45, "open_rate": 5.514e-05, "close_rate": 5.54163909774436e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1813.5654697134569, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 00:50:00+00:00", "close_date": "2018-01-25 01:30:00+00:00", "trade_duration": 40, "open_rate": 4.921e-05, "close_rate": 4.9456666666666664e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.1072952651903, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 08:15:00+00:00", "close_date": "2018-01-25 12:15:00+00:00", "trade_duration": 240, "open_rate": 0.0026, "close_rate": 0.002613032581453634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.46153846153847, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 10:25:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 350, "open_rate": 0.02799871, "close_rate": 0.028139054411027563, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.571593119825878, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 11:00:00+00:00", "close_date": "2018-01-25 11:45:00+00:00", "trade_duration": 45, "open_rate": 0.04078902, "close_rate": 0.0409934762406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4516401717913303, "profit_abs": -1.3877787807814457e-17}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:05:00+00:00", "close_date": "2018-01-25 13:45:00+00:00", "trade_duration": 40, "open_rate": 2.89e-05, "close_rate": 2.904486215538847e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3460.2076124567475, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:20:00+00:00", "close_date": "2018-01-25 14:05:00+00:00", "trade_duration": 45, "open_rate": 0.041103, "close_rate": 0.04130903007518797, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4329124394813033, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-25 15:45:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 30, "open_rate": 5.428e-05, "close_rate": 5.509624060150376e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1842.2991893883568, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 17:45:00+00:00", "close_date": "2018-01-25 23:15:00+00:00", "trade_duration": 330, "open_rate": 5.414e-05, "close_rate": 5.441137844611528e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1847.063169560399, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 21:15:00+00:00", "close_date": "2018-01-25 21:55:00+00:00", "trade_duration": 40, "open_rate": 0.04140777, "close_rate": 0.0416153277443609, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.415005686130888, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 02:05:00+00:00", "close_date": "2018-01-26 02:45:00+00:00", "trade_duration": 40, "open_rate": 0.00254309, "close_rate": 0.002555837318295739, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.32224183965177, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 02:55:00+00:00", "close_date": "2018-01-26 15:10:00+00:00", "trade_duration": 735, "open_rate": 5.607e-05, "close_rate": 5.6351052631578935e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1783.4849295523454, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 06:10:00+00:00", "close_date": "2018-01-26 09:25:00+00:00", "trade_duration": 195, "open_rate": 0.00253806, "close_rate": 0.0025507821052631577, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.400171784748984, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 07:25:00+00:00", "close_date": "2018-01-26 09:55:00+00:00", "trade_duration": 150, "open_rate": 0.0415, "close_rate": 0.04170802005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4096385542168677, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-26 09:55:00+00:00", "close_date": "2018-01-26 10:25:00+00:00", "trade_duration": 30, "open_rate": 5.321e-05, "close_rate": 5.401015037593984e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1879.3459875963165, "profit_abs": 0.000999999999999987}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 16:05:00+00:00", "close_date": "2018-01-26 16:45:00+00:00", "trade_duration": 40, "open_rate": 0.02772046, "close_rate": 0.02785940967418546, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.6074437437185387, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 23:35:00+00:00", "close_date": "2018-01-27 00:15:00+00:00", "trade_duration": 40, "open_rate": 0.09461341, "close_rate": 0.09508766268170424, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0569326272036914, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 00:35:00+00:00", "close_date": "2018-01-27 01:30:00+00:00", "trade_duration": 55, "open_rate": 5.615e-05, "close_rate": 5.643145363408521e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1780.9439002671415, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.07877175, "open_date": "2018-01-27 00:45:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 4560, "open_rate": 5.556e-05, "close_rate": 5.144e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1799.8560115190785, "profit_abs": -0.007896868250539965}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 02:30:00+00:00", "close_date": "2018-01-27 11:25:00+00:00", "trade_duration": 535, "open_rate": 0.06900001, "close_rate": 0.06934587471177944, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492751522789635, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 06:25:00+00:00", "close_date": "2018-01-27 07:05:00+00:00", "trade_duration": 40, "open_rate": 0.09449985, "close_rate": 0.0949735334586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.058202737887944, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.04815133, "open_date": "2018-01-27 09:40:00+00:00", "close_date": "2018-01-30 04:40:00+00:00", "trade_duration": 4020, "open_rate": 0.0410697, "close_rate": 0.03928809, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4348850855983852, "profit_abs": -0.004827170578309559}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 11:45:00+00:00", "close_date": "2018-01-27 12:30:00+00:00", "trade_duration": 45, "open_rate": 0.0285, "close_rate": 0.02864285714285714, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5087719298245617, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 12:35:00+00:00", "close_date": "2018-01-27 15:25:00+00:00", "trade_duration": 170, "open_rate": 0.02866372, "close_rate": 0.02880739779448621, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.4887307020861216, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 15:50:00+00:00", "close_date": "2018-01-27 16:50:00+00:00", "trade_duration": 60, "open_rate": 0.095381, "close_rate": 0.09585910025062656, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0484268355332824, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 17:05:00+00:00", "close_date": "2018-01-27 17:45:00+00:00", "trade_duration": 40, "open_rate": 0.06759092, "close_rate": 0.06792972160401002, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4794886650455417, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 23:40:00+00:00", "close_date": "2018-01-28 01:05:00+00:00", "trade_duration": 85, "open_rate": 0.00258501, "close_rate": 0.002597967443609022, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.684569885609726, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 02:25:00+00:00", "close_date": "2018-01-28 08:10:00+00:00", "trade_duration": 345, "open_rate": 0.06698502, "close_rate": 0.0673207845112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4928710926711672, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 10:25:00+00:00", "close_date": "2018-01-28 16:30:00+00:00", "trade_duration": 365, "open_rate": 0.0677177, "close_rate": 0.06805713709273183, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4767187899175547, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-28 20:35:00+00:00", "close_date": "2018-01-28 21:35:00+00:00", "trade_duration": 60, "open_rate": 5.215e-05, "close_rate": 5.2411403508771925e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1917.5455417066157, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-28 22:00:00+00:00", "close_date": "2018-01-28 22:30:00+00:00", "trade_duration": 30, "open_rate": 0.00273809, "close_rate": 0.002779264285714285, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.5218089982433, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-29 00:00:00+00:00", "close_date": "2018-01-29 00:30:00+00:00", "trade_duration": 30, "open_rate": 0.00274632, "close_rate": 0.002787618045112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.412362725392526, "profit_abs": 0.0010000000000000148}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-29 02:15:00+00:00", "close_date": "2018-01-29 03:00:00+00:00", "trade_duration": 45, "open_rate": 0.01622478, "close_rate": 0.016306107218045113, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.163411768911504, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 03:05:00+00:00", "close_date": "2018-01-29 03:45:00+00:00", "trade_duration": 40, "open_rate": 0.069, "close_rate": 0.06934586466165413, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492753623188406, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 05:20:00+00:00", "close_date": "2018-01-29 06:55:00+00:00", "trade_duration": 95, "open_rate": 8.755e-05, "close_rate": 8.798884711779448e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1142.204454597373, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 07:00:00+00:00", "close_date": "2018-01-29 19:25:00+00:00", "trade_duration": 745, "open_rate": 0.06825763, "close_rate": 0.06859977350877192, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4650376815016872, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 19:45:00+00:00", "close_date": "2018-01-29 20:25:00+00:00", "trade_duration": 40, "open_rate": 0.06713892, "close_rate": 0.06747545593984962, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4894490408841845, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0199116, "open_date": "2018-01-29 23:30:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 315, "open_rate": 8.934e-05, "close_rate": 8.8e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1119.3194537721067, "profit_abs": -0.0019961383478844796}], "results_per_pair": [{"key": "TRX/BTC", "trades": 15, "profit_mean": 0.0023467073333333323, "profit_mean_pct": 0.23467073333333321, "profit_sum": 0.035200609999999986, "profit_sum_pct": 3.5200609999999988, "profit_total_abs": 0.0035288616521155086, "profit_total_pct": 1.1733536666666662, "duration_avg": "2:28:00", "wins": 9, "draws": 2, "losses": 4}, {"key": "ADA/BTC", "trades": 29, "profit_mean": -0.0011598141379310352, "profit_mean_pct": -0.11598141379310352, "profit_sum": -0.03363461000000002, "profit_sum_pct": -3.3634610000000023, "profit_total_abs": -0.0033718682505400333, "profit_total_pct": -1.1211536666666675, "duration_avg": "5:35:00", "wins": 9, "draws": 11, "losses": 9}, {"key": "XLM/BTC", "trades": 21, "profit_mean": 0.0026243899999999994, "profit_mean_pct": 0.2624389999999999, "profit_sum": 0.05511218999999999, "profit_sum_pct": 5.511218999999999, "profit_total_abs": 0.005525000000000002, "profit_total_pct": 1.8370729999999995, "duration_avg": "3:21:00", "wins": 12, "draws": 3, "losses": 6}, {"key": "ETH/BTC", "trades": 21, "profit_mean": 0.0009500057142857142, "profit_mean_pct": 0.09500057142857142, "profit_sum": 0.01995012, "profit_sum_pct": 1.9950119999999998, "profit_total_abs": 0.0019999999999999463, "profit_total_pct": 0.6650039999999999, "duration_avg": "2:17:00", "wins": 5, "draws": 10, "losses": 6}, {"key": "XMR/BTC", "trades": 16, "profit_mean": -0.0027899012500000007, "profit_mean_pct": -0.2789901250000001, "profit_sum": -0.04463842000000001, "profit_sum_pct": -4.463842000000001, "profit_total_abs": -0.0044750000000000345, "profit_total_pct": -1.4879473333333337, "duration_avg": "8:41:00", "wins": 6, "draws": 5, "losses": 5}, {"key": "ZEC/BTC", "trades": 21, "profit_mean": -0.00039290904761904774, "profit_mean_pct": -0.03929090476190478, "profit_sum": -0.008251090000000003, "profit_sum_pct": -0.8251090000000003, "profit_total_abs": -0.000827170578309569, "profit_total_pct": -0.27503633333333344, "duration_avg": "4:17:00", "wins": 8, "draws": 7, "losses": 6}, {"key": "NXT/BTC", "trades": 12, "profit_mean": -0.0012261025000000006, "profit_mean_pct": -0.12261025000000006, "profit_sum": -0.014713230000000008, "profit_sum_pct": -1.4713230000000008, "profit_total_abs": -0.0014750000000000874, "profit_total_pct": -0.4904410000000003, "duration_avg": "0:57:00", "wins": 4, "draws": 3, "losses": 5}, {"key": "LTC/BTC", "trades": 8, "profit_mean": 0.00748129625, "profit_mean_pct": 0.748129625, "profit_sum": 0.05985037, "profit_sum_pct": 5.985037, "profit_total_abs": 0.006000000000000019, "profit_total_pct": 1.9950123333333334, "duration_avg": "1:59:00", "wins": 5, "draws": 2, "losses": 1}, {"key": "ETC/BTC", "trades": 20, "profit_mean": 0.0022568569999999997, "profit_mean_pct": 0.22568569999999996, "profit_sum": 0.04513713999999999, "profit_sum_pct": 4.513713999999999, "profit_total_abs": 0.004525000000000001, "profit_total_pct": 1.504571333333333, "duration_avg": "1:45:00", "wins": 11, "draws": 4, "losses": 5}, {"key": "DASH/BTC", "trades": 16, "profit_mean": 0.0018703237499999997, "profit_mean_pct": 0.18703237499999997, "profit_sum": 0.029925179999999996, "profit_sum_pct": 2.9925179999999996, "profit_total_abs": 0.002999999999999961, "profit_total_pct": 0.9975059999999999, "duration_avg": "3:03:00", "wins": 4, "draws": 7, "losses": 5}, {"key": "TOTAL", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}], "sell_reason_summary": [{"sell_reason": "roi", "trades": 170, "wins": 73, "draws": 54, "losses": 43, "profit_mean": 0.005398268352941177, "profit_mean_pct": 0.54, "profit_sum": 0.91770562, "profit_sum_pct": 91.77, "profit_total_abs": 0.09199999999999964, "profit_pct_total": 30.59}, {"sell_reason": "stop_loss", "trades": 6, "wins": 0, "draws": 0, "losses": 6, "profit_mean": -0.10448878000000002, "profit_mean_pct": -10.45, "profit_sum": -0.6269326800000001, "profit_sum_pct": -62.69, "profit_total_abs": -0.06284999999999992, "profit_pct_total": -20.9}, {"sell_reason": "force_sell", "trades": 3, "wins": 0, "draws": 0, "losses": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.89, "profit_sum": -0.14683468, "profit_sum_pct": -14.68, "profit_total_abs": -0.014720177176734003, "profit_pct_total": -4.89}], "left_open_trades": [{"key": "TRX/BTC", "trades": 1, "profit_mean": -0.0199116, "profit_mean_pct": -1.9911600000000003, "profit_sum": -0.0199116, "profit_sum_pct": -1.9911600000000003, "profit_total_abs": -0.0019961383478844796, "profit_total_pct": -0.6637200000000001, "duration_avg": "5:15:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ADA/BTC", "trades": 1, "profit_mean": -0.07877175, "profit_mean_pct": -7.877175, "profit_sum": -0.07877175, "profit_sum_pct": -7.877175, "profit_total_abs": -0.007896868250539965, "profit_total_pct": -2.625725, "duration_avg": "3 days, 4:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ZEC/BTC", "trades": 1, "profit_mean": -0.04815133, "profit_mean_pct": -4.815133, "profit_sum": -0.04815133, "profit_sum_pct": -4.815133, "profit_total_abs": -0.004827170578309559, "profit_total_pct": -1.6050443333333335, "duration_avg": "2 days, 19:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "TOTAL", "trades": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.894489333333333, "profit_sum": -0.14683468, "profit_sum_pct": -14.683468, "profit_total_abs": -0.014720177176734003, "profit_total_pct": -4.8944893333333335, "duration_avg": "2 days, 1:25:00", "wins": 0, "draws": 0, "losses": 3}], "total_trades": 179, "backtest_start": "2018-01-30 04:45:00+00:00", "backtest_start_ts": 1517287500, "backtest_end": "2018-01-30 04:45:00+00:00", "backtest_end_ts": 1517287500, "backtest_days": 0, "trades_per_day": null, "market_change": 0.25, "stake_amount": 0.1, "max_drawdown": 0.21142322000000008, "drawdown_start": "2018-01-24 14:25:00+00:00", "drawdown_start_ts": 1516803900.0, "drawdown_end": "2018-01-30 04:45:00+00:00", "drawdown_end_ts": 1517287500.0}}, "strategy_comparison": [{"key": "DefaultStrategy", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}, {"key": "TestStrategy", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}]} \ No newline at end of file From 59ac4b9c9a2d0568252c82f8b48c833cb76bfd9e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Jun 2020 19:48:45 +0200 Subject: [PATCH 0222/1197] Test writing statistics --- tests/data/test_history.py | 2 +- tests/optimize/test_optimize_reports.py | 27 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index c2eb2d715..8b3a3c568 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -36,7 +36,7 @@ def _backup_file(file: Path, copy_file: bool = False) -> None: """ Backup existing file to avoid deleting the user file :param file: complete path to the file - :param touch_file: create an empty file in replacement + :param copy_file: keep file in place too. :return: None """ file_swp = str(file) + '.swp' diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index c46b96ab2..690847adc 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -2,23 +2,27 @@ from datetime import datetime from pathlib import Path import pandas as pd +import re import pytest from arrow import Arrow from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.edge import PairInfo +from freqtrade.data.btanalysis import get_latest_backtest_filename from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_edge_table, generate_pair_metrics, generate_sell_reason_stats, generate_strategy_metrics, store_backtest_result, + store_backtest_stats, text_table_bt_results, text_table_sell_reason, text_table_strategy) from freqtrade.strategy.interface import SellType from tests.conftest import patch_exchange +from tests.data.test_history import _backup_file, _clean_test_file def test_text_table_bt_results(default_conf, mocker): @@ -115,6 +119,29 @@ def test_generate_backtest_stats(default_conf, testdatadir): assert strat_stats['drawdown_end_ts'] == 0 assert strat_stats['drawdown_start_ts'] == 0 + # Test storing stats + filename = Path(testdatadir / 'btresult.json') + filename_last = Path(testdatadir / '.last_result.json') + _backup_file(filename_last, copy_file=True) + assert not filename.is_file() + + store_backtest_stats(filename, stats) + + # get real Filename (it's btresult-.json) + last_fn = get_latest_backtest_filename(filename_last.parent) + assert re.match(r"btresult-.*\.json", last_fn) + + filename1 = (testdatadir / last_fn) + assert filename1.is_file() + content = filename1.read_text() + assert 'max_drawdown' in content + assert 'strategy' in content + + assert filename_last.is_file() + + _clean_test_file(filename_last) + filename1.unlink() + def test_generate_pair_metrics(default_conf, mocker): From 59e0ca0aaab0543933d8bcd0961a62bb37db8916 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 09:04:19 +0200 Subject: [PATCH 0223/1197] Add pairlist to backtest-result --- freqtrade/optimize/optimize_reports.py | 1 + tests/optimize/test_optimize_reports.py | 2 ++ tests/testdata/backtest-result_multistrat.json | 2 +- tests/testdata/backtest-result_new.json | 2 +- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 6f9d3f34e..b93e60dca 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -250,6 +250,7 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], 'backtest_days': backtest_days, 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else None, 'market_change': market_change, + 'pairlist': list(btdata.keys()), 'stake_amount': config['stake_amount'] } result['strategy'][strategy] = strat_stats diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 690847adc..f908677d7 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -118,6 +118,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): assert strat_stats['drawdown_end'] == Arrow.fromtimestamp(0).datetime assert strat_stats['drawdown_end_ts'] == 0 assert strat_stats['drawdown_start_ts'] == 0 + assert strat_stats['pairlist'] == ['UNITTEST/BTC'] # Test storing stats filename = Path(testdatadir / 'btresult.json') @@ -136,6 +137,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): content = filename1.read_text() assert 'max_drawdown' in content assert 'strategy' in content + assert 'pairlist' in content assert filename_last.is_file() diff --git a/tests/testdata/backtest-result_multistrat.json b/tests/testdata/backtest-result_multistrat.json index 88f021cb8..0e5386ef3 100644 --- a/tests/testdata/backtest-result_multistrat.json +++ b/tests/testdata/backtest-result_multistrat.json @@ -1 +1 @@ -{"strategy": {"DefaultStrategy": {"trades": [{"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:20:00+00:00", "trade_duration": 5, "open_rate": 9.64e-05, "close_rate": 0.00010074887218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1037.344398340249, "profit_abs": 0.00399999999999999}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:30:00+00:00", "trade_duration": 15, "open_rate": 4.756e-05, "close_rate": 4.9705563909774425e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2102.6072329688814, "profit_abs": 0.00399999999999999}, {"pair": "XLM/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:35:00+00:00", "trade_duration": 10, "open_rate": 3.339e-05, "close_rate": 3.489631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2994.908655286014, "profit_abs": 0.0040000000000000036}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:40:00+00:00", "trade_duration": 15, "open_rate": 9.696e-05, "close_rate": 0.00010133413533834584, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1031.3531353135315, "profit_abs": 0.00399999999999999}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 07:35:00+00:00", "close_date": "2018-01-10 08:35:00+00:00", "trade_duration": 60, "open_rate": 0.0943, "close_rate": 0.09477268170426063, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0604453870625663, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 07:40:00+00:00", "close_date": "2018-01-10 08:10:00+00:00", "trade_duration": 30, "open_rate": 0.02719607, "close_rate": 0.02760503345864661, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.677001860930642, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 08:15:00+00:00", "close_date": "2018-01-10 09:55:00+00:00", "trade_duration": 100, "open_rate": 0.04634952, "close_rate": 0.046581848421052625, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1575196463739, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 14:45:00+00:00", "close_date": "2018-01-10 15:50:00+00:00", "trade_duration": 65, "open_rate": 3.066e-05, "close_rate": 3.081368421052631e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3261.5786040443577, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 16:35:00+00:00", "close_date": "2018-01-10 17:15:00+00:00", "trade_duration": 40, "open_rate": 0.0168999, "close_rate": 0.016984611278195488, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.917194776300452, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 16:40:00+00:00", "close_date": "2018-01-10 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.09132568, "close_rate": 0.0917834528320802, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0949822656672252, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 18:50:00+00:00", "close_date": "2018-01-10 19:45:00+00:00", "trade_duration": 55, "open_rate": 0.08898003, "close_rate": 0.08942604518796991, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1238476768326557, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 22:15:00+00:00", "close_date": "2018-01-10 23:00:00+00:00", "trade_duration": 45, "open_rate": 0.08560008, "close_rate": 0.08602915308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1682232072680307, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 22:50:00+00:00", "close_date": "2018-01-10 23:20:00+00:00", "trade_duration": 30, "open_rate": 0.00249083, "close_rate": 0.0025282860902255634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 40.147260150231055, "profit_abs": 0.000999999999999987}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 23:15:00+00:00", "close_date": "2018-01-11 00:15:00+00:00", "trade_duration": 60, "open_rate": 3.022e-05, "close_rate": 3.037147869674185e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3309.0668431502318, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-10 23:40:00+00:00", "close_date": "2018-01-11 00:05:00+00:00", "trade_duration": 25, "open_rate": 0.002437, "close_rate": 0.0024980776942355883, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.03405826836274, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 00:00:00+00:00", "close_date": "2018-01-11 00:35:00+00:00", "trade_duration": 35, "open_rate": 0.04771803, "close_rate": 0.04843559436090225, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0956439316543456, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-11 03:40:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 45, "open_rate": 3.651e-05, "close_rate": 3.2859000000000005e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2738.9756231169545, "profit_abs": -0.01047499999999997}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 03:55:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 30, "open_rate": 0.08824105, "close_rate": 0.08956798308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1332594070446804, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 04:00:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 50, "open_rate": 0.00243, "close_rate": 0.002442180451127819, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.1522633744856, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:55:00+00:00", "trade_duration": 25, "open_rate": 0.04545064, "close_rate": 0.046589753784461146, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.200189040242338, "profit_abs": 0.001999999999999988}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 20, "open_rate": 3.372e-05, "close_rate": 3.456511278195488e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2965.599051008304, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:55:00+00:00", "close_date": "2018-01-11 05:15:00+00:00", "trade_duration": 20, "open_rate": 0.02644, "close_rate": 0.02710265664160401, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7821482602118004, "profit_abs": 0.001999999999999988}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:20:00+00:00", "close_date": "2018-01-11 12:00:00+00:00", "trade_duration": 40, "open_rate": 0.08812, "close_rate": 0.08856170426065162, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1348161597821154, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:35:00+00:00", "close_date": "2018-01-11 12:15:00+00:00", "trade_duration": 40, "open_rate": 0.02683577, "close_rate": 0.026970285137844607, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7263696923919087, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 14:00:00+00:00", "close_date": "2018-01-11 14:25:00+00:00", "trade_duration": 25, "open_rate": 4.919e-05, "close_rate": 5.04228320802005e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.9335230737956, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 19:25:00+00:00", "close_date": "2018-01-11 20:35:00+00:00", "trade_duration": 70, "open_rate": 0.08784896, "close_rate": 0.08828930566416039, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1383174029607181, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:35:00+00:00", "close_date": "2018-01-11 23:30:00+00:00", "trade_duration": 55, "open_rate": 5.105e-05, "close_rate": 5.130588972431077e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1958.8638589618022, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:25:00+00:00", "trade_duration": 30, "open_rate": 3.96e-05, "close_rate": 4.019548872180451e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2525.252525252525, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:35:00+00:00", "trade_duration": 40, "open_rate": 2.885e-05, "close_rate": 2.899461152882205e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3466.204506065858, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 23:30:00+00:00", "close_date": "2018-01-12 00:05:00+00:00", "trade_duration": 35, "open_rate": 0.02645, "close_rate": 0.026847744360902256, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.780718336483932, "profit_abs": 0.0010000000000000148}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 23:55:00+00:00", "close_date": "2018-01-12 01:15:00+00:00", "trade_duration": 80, "open_rate": 0.048, "close_rate": 0.04824060150375939, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0833333333333335, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-12 21:15:00+00:00", "close_date": "2018-01-12 21:40:00+00:00", "trade_duration": 25, "open_rate": 4.692e-05, "close_rate": 4.809593984962405e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2131.287297527707, "profit_abs": 0.001999999999999974}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 00:55:00+00:00", "close_date": "2018-01-13 06:20:00+00:00", "trade_duration": 325, "open_rate": 0.00256966, "close_rate": 0.0025825405012531327, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.91565421106294, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 10:55:00+00:00", "close_date": "2018-01-13 11:35:00+00:00", "trade_duration": 40, "open_rate": 6.262e-05, "close_rate": 6.293388471177944e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1596.933886937081, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 13:05:00+00:00", "close_date": "2018-01-15 14:10:00+00:00", "trade_duration": 2945, "open_rate": 4.73e-05, "close_rate": 4.753709273182957e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2114.1649048625795, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:30:00+00:00", "close_date": "2018-01-13 14:45:00+00:00", "trade_duration": 75, "open_rate": 6.063e-05, "close_rate": 6.0933909774436085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1649.348507339601, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:40:00+00:00", "close_date": "2018-01-13 23:30:00+00:00", "trade_duration": 590, "open_rate": 0.00011082, "close_rate": 0.00011137548872180448, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 902.3641941887746, "profit_abs": -2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 15:15:00+00:00", "close_date": "2018-01-13 15:55:00+00:00", "trade_duration": 40, "open_rate": 5.93e-05, "close_rate": 5.9597243107769415e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1686.3406408094436, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 16:30:00+00:00", "close_date": "2018-01-13 17:10:00+00:00", "trade_duration": 40, "open_rate": 0.04850003, "close_rate": 0.04874313791979949, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0618543947292407, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 22:05:00+00:00", "close_date": "2018-01-14 06:25:00+00:00", "trade_duration": 500, "open_rate": 0.09825019, "close_rate": 0.09874267215538848, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0178097365511456, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 00:20:00+00:00", "close_date": "2018-01-14 22:55:00+00:00", "trade_duration": 1355, "open_rate": 6.018e-05, "close_rate": 6.048165413533834e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1661.681621801263, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 12:45:00+00:00", "close_date": "2018-01-14 13:25:00+00:00", "trade_duration": 40, "open_rate": 0.09758999, "close_rate": 0.0980791628822055, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.024695258191952, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-14 15:30:00+00:00", "close_date": "2018-01-14 16:00:00+00:00", "trade_duration": 30, "open_rate": 0.00311, "close_rate": 0.0031567669172932328, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.154340836012864, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 20:45:00+00:00", "close_date": "2018-01-14 22:15:00+00:00", "trade_duration": 90, "open_rate": 0.00312401, "close_rate": 0.003139669197994987, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.010140812609436, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 23:35:00+00:00", "close_date": "2018-01-15 00:30:00+00:00", "trade_duration": 55, "open_rate": 0.0174679, "close_rate": 0.017555458395989976, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.724786608579165, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 23:45:00+00:00", "close_date": "2018-01-15 00:25:00+00:00", "trade_duration": 40, "open_rate": 0.07346846, "close_rate": 0.07383672295739348, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.3611282991367997, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 02:25:00+00:00", "close_date": "2018-01-15 03:05:00+00:00", "trade_duration": 40, "open_rate": 0.097994, "close_rate": 0.09848519799498744, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.020470641059657, "profit_abs": -2.7755575615628914e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 07:20:00+00:00", "close_date": "2018-01-15 08:00:00+00:00", "trade_duration": 40, "open_rate": 0.09659, "close_rate": 0.09707416040100247, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0353038616834043, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-15 08:20:00+00:00", "close_date": "2018-01-15 08:55:00+00:00", "trade_duration": 35, "open_rate": 9.987e-05, "close_rate": 0.00010137180451127818, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1001.3016921998599, "profit_abs": 0.0010000000000000009}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-15 12:10:00+00:00", "close_date": "2018-01-16 02:50:00+00:00", "trade_duration": 880, "open_rate": 0.0948969, "close_rate": 0.09537257368421052, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0537752023511833, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:10:00+00:00", "close_date": "2018-01-15 17:40:00+00:00", "trade_duration": 210, "open_rate": 0.071, "close_rate": 0.07135588972431077, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4084507042253522, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:30:00+00:00", "close_date": "2018-01-15 15:10:00+00:00", "trade_duration": 40, "open_rate": 0.04600501, "close_rate": 0.046235611553884705, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.173676301776698, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:10:00+00:00", "close_date": "2018-01-15 19:25:00+00:00", "trade_duration": 75, "open_rate": 9.438e-05, "close_rate": 9.485308270676693e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1059.5465140919687, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:35:00+00:00", "close_date": "2018-01-15 19:15:00+00:00", "trade_duration": 40, "open_rate": 0.03040001, "close_rate": 0.030552391002506264, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.2894726021471703, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-15 20:25:00+00:00", "close_date": "2018-01-16 08:25:00+00:00", "trade_duration": 720, "open_rate": 5.837e-05, "close_rate": 5.2533e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1713.2088401576154, "profit_abs": -0.010474999999999984}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 20:40:00+00:00", "close_date": "2018-01-15 22:00:00+00:00", "trade_duration": 80, "open_rate": 0.046036, "close_rate": 0.04626675689223057, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1722130506560084, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 00:30:00+00:00", "close_date": "2018-01-16 01:10:00+00:00", "trade_duration": 40, "open_rate": 0.0028685, "close_rate": 0.0028828784461152877, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 34.86142583231654, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 01:15:00+00:00", "close_date": "2018-01-16 02:35:00+00:00", "trade_duration": 80, "open_rate": 0.06731755, "close_rate": 0.0676549813283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4854967241083492, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 07:45:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 55, "open_rate": 0.09217614, "close_rate": 0.09263817578947368, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0848794492804754, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:55:00+00:00", "trade_duration": 20, "open_rate": 0.0165, "close_rate": 0.016913533834586467, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.0606060606060606, "profit_abs": 0.0020000000000000018}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 5, "open_rate": 7.953e-05, "close_rate": 8.311781954887218e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1257.387149503332, "profit_abs": 0.00399999999999999}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 08:45:00+00:00", "close_date": "2018-01-16 09:50:00+00:00", "trade_duration": 65, "open_rate": 0.045202, "close_rate": 0.04542857644110275, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2122914915269236, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:45:00+00:00", "trade_duration": 30, "open_rate": 5.248e-05, "close_rate": 5.326917293233082e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1905.487804878049, "profit_abs": 0.0010000000000000009}, {"pair": "XMR/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:55:00+00:00", "trade_duration": 40, "open_rate": 0.02892318, "close_rate": 0.02906815834586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.457434486802627, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 09:50:00+00:00", "close_date": "2018-01-16 10:10:00+00:00", "trade_duration": 20, "open_rate": 5.158e-05, "close_rate": 5.287273182957392e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1938.735944164405, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:35:00+00:00", "trade_duration": 30, "open_rate": 0.02828232, "close_rate": 0.02870761804511278, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5357778286929786, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:40:00+00:00", "trade_duration": 35, "open_rate": 0.04357584, "close_rate": 0.044231115789473675, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.294849623093898, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 13:45:00+00:00", "close_date": "2018-01-16 14:20:00+00:00", "trade_duration": 35, "open_rate": 5.362e-05, "close_rate": 5.442631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1864.975755315181, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 17:30:00+00:00", "close_date": "2018-01-16 18:25:00+00:00", "trade_duration": 55, "open_rate": 5.302e-05, "close_rate": 5.328576441102756e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.0807242549984, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:45:00+00:00", "trade_duration": 30, "open_rate": 0.09129999, "close_rate": 0.09267292218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0952903718828448, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:35:00+00:00", "trade_duration": 20, "open_rate": 3.808e-05, "close_rate": 3.903438596491228e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2626.0504201680674, "profit_abs": 0.0020000000000000018}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 19:00:00+00:00", "close_date": "2018-01-16 19:30:00+00:00", "trade_duration": 30, "open_rate": 0.02811012, "close_rate": 0.028532828571428567, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.557437677249333, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 60, "open_rate": 0.00258379, "close_rate": 0.002325411, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.702835756775904, "profit_abs": -0.010474999999999984}, {"pair": "NXT/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 80, "open_rate": 2.559e-05, "close_rate": 2.3031e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3907.7764751856193, "profit_abs": -0.010474999999999998}, {"pair": "TRX/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:35:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 50, "open_rate": 7.62e-05, "close_rate": 6.858e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1312.3359580052495, "profit_abs": -0.010474999999999984}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:35:00+00:00", "trade_duration": 5, "open_rate": 0.00229844, "close_rate": 0.002402129022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 43.507770487809125, "profit_abs": 0.004000000000000017}, {"pair": "LTC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:40:00+00:00", "trade_duration": 10, "open_rate": 0.0151, "close_rate": 0.015781203007518795, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.622516556291391, "profit_abs": 0.00399999999999999}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:40:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 5, "open_rate": 0.00235676, "close_rate": 0.00246308, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 42.431134269081284, "profit_abs": 0.0040000000000000036}, {"pair": "DASH/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 22:45:00+00:00", "close_date": "2018-01-16 23:05:00+00:00", "trade_duration": 20, "open_rate": 0.0630692, "close_rate": 0.06464988170426066, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.585559988076589, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:50:00+00:00", "close_date": "2018-01-16 22:55:00+00:00", "trade_duration": 5, "open_rate": 2.2e-05, "close_rate": 2.299248120300751e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 4545.454545454546, "profit_abs": 0.003999999999999976}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-17 03:30:00+00:00", "close_date": "2018-01-17 04:00:00+00:00", "trade_duration": 30, "open_rate": 4.974e-05, "close_rate": 5.048796992481203e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2010.454362685967, "profit_abs": 0.0010000000000000009}, {"pair": "TRX/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-17 03:55:00+00:00", "close_date": "2018-01-17 04:15:00+00:00", "trade_duration": 20, "open_rate": 7.108e-05, "close_rate": 7.28614536340852e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1406.8655036578502, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 09:35:00+00:00", "close_date": "2018-01-17 10:15:00+00:00", "trade_duration": 40, "open_rate": 0.04327, "close_rate": 0.04348689223057644, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.3110700254217704, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:20:00+00:00", "close_date": "2018-01-17 17:00:00+00:00", "trade_duration": 400, "open_rate": 4.997e-05, "close_rate": 5.022047619047618e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2001.2007204322595, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:25:00+00:00", "trade_duration": 55, "open_rate": 0.06836818, "close_rate": 0.06871087764411027, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4626687444363737, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:10:00+00:00", "trade_duration": 40, "open_rate": 3.63e-05, "close_rate": 3.648195488721804e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2754.8209366391184, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:30:00+00:00", "close_date": "2018-01-17 22:05:00+00:00", "trade_duration": 575, "open_rate": 0.0281, "close_rate": 0.02824085213032581, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5587188612099645, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:35:00+00:00", "close_date": "2018-01-17 16:55:00+00:00", "trade_duration": 260, "open_rate": 0.08651001, "close_rate": 0.08694364413533832, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1559355963546878, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 05:00:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 55, "open_rate": 5.633e-05, "close_rate": 5.6612355889724306e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1775.2529735487308, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 05:20:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 35, "open_rate": 0.06988494, "close_rate": 0.07093584135338346, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.430923457900944, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 07:35:00+00:00", "close_date": "2018-01-18 08:15:00+00:00", "trade_duration": 40, "open_rate": 5.545e-05, "close_rate": 5.572794486215538e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1803.4265103697026, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 09:00:00+00:00", "close_date": "2018-01-18 09:40:00+00:00", "trade_duration": 40, "open_rate": 0.01633527, "close_rate": 0.016417151052631574, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.121723118136401, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 16:40:00+00:00", "close_date": "2018-01-18 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.00269734, "close_rate": 0.002710860501253133, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.073561360451414, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-18 18:05:00+00:00", "close_date": "2018-01-18 18:30:00+00:00", "trade_duration": 25, "open_rate": 4.475e-05, "close_rate": 4.587155388471177e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2234.63687150838, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 18:25:00+00:00", "close_date": "2018-01-18 18:55:00+00:00", "trade_duration": 30, "open_rate": 2.79e-05, "close_rate": 2.8319548872180444e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3584.2293906810037, "profit_abs": 0.000999999999999987}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 20:10:00+00:00", "close_date": "2018-01-18 20:50:00+00:00", "trade_duration": 40, "open_rate": 0.04439326, "close_rate": 0.04461578260651629, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2525942001105577, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 21:30:00+00:00", "close_date": "2018-01-19 00:35:00+00:00", "trade_duration": 185, "open_rate": 4.49e-05, "close_rate": 4.51250626566416e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2227.1714922049, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 21:55:00+00:00", "close_date": "2018-01-19 05:05:00+00:00", "trade_duration": 430, "open_rate": 0.02855, "close_rate": 0.028693107769423555, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.502626970227671, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 22:10:00+00:00", "close_date": "2018-01-18 22:50:00+00:00", "trade_duration": 40, "open_rate": 5.796e-05, "close_rate": 5.8250526315789473e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1725.3278122843342, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 23:50:00+00:00", "close_date": "2018-01-19 00:30:00+00:00", "trade_duration": 40, "open_rate": 0.04340323, "close_rate": 0.04362079005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.303975994413319, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-19 16:45:00+00:00", "close_date": "2018-01-19 17:35:00+00:00", "trade_duration": 50, "open_rate": 0.04454455, "close_rate": 0.04476783095238095, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.244943545282195, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:15:00+00:00", "close_date": "2018-01-19 19:55:00+00:00", "trade_duration": 160, "open_rate": 5.62e-05, "close_rate": 5.648170426065162e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1779.3594306049824, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:20:00+00:00", "close_date": "2018-01-19 20:15:00+00:00", "trade_duration": 175, "open_rate": 4.339e-05, "close_rate": 4.360749373433584e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2304.6784973496196, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-20 04:45:00+00:00", "close_date": "2018-01-20 17:35:00+00:00", "trade_duration": 770, "open_rate": 0.0001009, "close_rate": 0.00010140576441102755, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 991.0802775024778, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 15:15:00+00:00", "trade_duration": 625, "open_rate": 0.00270505, "close_rate": 0.002718609147869674, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.96789338459548, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 07:00:00+00:00", "trade_duration": 130, "open_rate": 0.03000002, "close_rate": 0.030150396040100245, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.3333311111125927, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 09:00:00+00:00", "close_date": "2018-01-20 09:40:00+00:00", "trade_duration": 40, "open_rate": 5.46e-05, "close_rate": 5.4873684210526304e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1831.5018315018317, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-20 18:25:00+00:00", "close_date": "2018-01-25 03:50:00+00:00", "trade_duration": 6325, "open_rate": 0.03082222, "close_rate": 0.027739998, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.244412634781012, "profit_abs": -0.010474999999999998}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 22:25:00+00:00", "close_date": "2018-01-20 23:15:00+00:00", "trade_duration": 50, "open_rate": 0.08969999, "close_rate": 0.09014961401002504, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1148273260677064, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 02:50:00+00:00", "close_date": "2018-01-21 14:30:00+00:00", "trade_duration": 700, "open_rate": 0.01632501, "close_rate": 0.01640683962406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.125570520324337, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 10:20:00+00:00", "close_date": "2018-01-21 11:00:00+00:00", "trade_duration": 40, "open_rate": 0.070538, "close_rate": 0.07089157393483708, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.417675579120474, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 15:50:00+00:00", "close_date": "2018-01-21 18:45:00+00:00", "trade_duration": 175, "open_rate": 5.301e-05, "close_rate": 5.327571428571427e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.4365214110546, "profit_abs": -2.7755575615628914e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 16:20:00+00:00", "close_date": "2018-01-21 17:00:00+00:00", "trade_duration": 40, "open_rate": 3.955e-05, "close_rate": 3.9748245614035085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2528.4450063211125, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:45:00+00:00", "trade_duration": 30, "open_rate": 0.00258505, "close_rate": 0.002623922932330827, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.6839712964933, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:55:00+00:00", "trade_duration": 40, "open_rate": 3.903e-05, "close_rate": 3.922563909774435e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2562.1316935690497, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 00:35:00+00:00", "close_date": "2018-01-22 10:35:00+00:00", "trade_duration": 600, "open_rate": 5.236e-05, "close_rate": 5.262245614035087e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1909.8548510313217, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 01:30:00+00:00", "close_date": "2018-01-22 02:10:00+00:00", "trade_duration": 40, "open_rate": 9.028e-05, "close_rate": 9.07325313283208e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1107.6650420912717, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 12:25:00+00:00", "close_date": "2018-01-22 14:35:00+00:00", "trade_duration": 130, "open_rate": 0.002687, "close_rate": 0.002700468671679198, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.21622627465575, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 13:15:00+00:00", "close_date": "2018-01-22 13:55:00+00:00", "trade_duration": 40, "open_rate": 4.168e-05, "close_rate": 4.188892230576441e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2399.232245681382, "profit_abs": 1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-22 14:00:00+00:00", "close_date": "2018-01-22 14:30:00+00:00", "trade_duration": 30, "open_rate": 8.821e-05, "close_rate": 8.953646616541353e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1133.6583153837435, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 15:55:00+00:00", "close_date": "2018-01-22 16:40:00+00:00", "trade_duration": 45, "open_rate": 5.172e-05, "close_rate": 5.1979248120300745e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1933.4880123743235, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-22 16:05:00+00:00", "close_date": "2018-01-22 16:25:00+00:00", "trade_duration": 20, "open_rate": 3.026e-05, "close_rate": 3.101839598997494e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3304.692663582287, "profit_abs": 0.0020000000000000157}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 19:50:00+00:00", "close_date": "2018-01-23 00:10:00+00:00", "trade_duration": 260, "open_rate": 0.07064, "close_rate": 0.07099408521303258, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.415628539071348, "profit_abs": 1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 21:25:00+00:00", "close_date": "2018-01-22 22:05:00+00:00", "trade_duration": 40, "open_rate": 0.01644483, "close_rate": 0.01652726022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.080938507725528, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-23 00:05:00+00:00", "close_date": "2018-01-23 00:35:00+00:00", "trade_duration": 30, "open_rate": 4.331e-05, "close_rate": 4.3961278195488714e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2308.935580697299, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-23 01:50:00+00:00", "close_date": "2018-01-23 02:15:00+00:00", "trade_duration": 25, "open_rate": 3.2e-05, "close_rate": 3.2802005012531326e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3125.0000000000005, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 04:25:00+00:00", "close_date": "2018-01-23 05:15:00+00:00", "trade_duration": 50, "open_rate": 0.09167706, "close_rate": 0.09213659413533835, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0907854156754153, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 07:35:00+00:00", "close_date": "2018-01-23 09:00:00+00:00", "trade_duration": 85, "open_rate": 0.0692498, "close_rate": 0.06959691679197995, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4440474918339115, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 10:50:00+00:00", "close_date": "2018-01-23 13:05:00+00:00", "trade_duration": 135, "open_rate": 3.182e-05, "close_rate": 3.197949874686716e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3142.677561282213, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 11:05:00+00:00", "close_date": "2018-01-23 16:05:00+00:00", "trade_duration": 300, "open_rate": 0.04088, "close_rate": 0.04108491228070175, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4461839530332683, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 14:55:00+00:00", "close_date": "2018-01-23 15:35:00+00:00", "trade_duration": 40, "open_rate": 5.15e-05, "close_rate": 5.175814536340851e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1941.747572815534, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 16:35:00+00:00", "close_date": "2018-01-24 00:05:00+00:00", "trade_duration": 450, "open_rate": 0.09071698, "close_rate": 0.09117170170426064, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1023294646713329, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 17:25:00+00:00", "close_date": "2018-01-23 18:45:00+00:00", "trade_duration": 80, "open_rate": 3.128e-05, "close_rate": 3.1436791979949865e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3196.9309462915603, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 20:15:00+00:00", "close_date": "2018-01-23 22:00:00+00:00", "trade_duration": 105, "open_rate": 9.555e-05, "close_rate": 9.602894736842104e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1046.5724751439038, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 22:30:00+00:00", "close_date": "2018-01-23 23:10:00+00:00", "trade_duration": 40, "open_rate": 0.04080001, "close_rate": 0.0410045213283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.450979791426522, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 23:50:00+00:00", "close_date": "2018-01-24 03:35:00+00:00", "trade_duration": 225, "open_rate": 5.163e-05, "close_rate": 5.18887969924812e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1936.8584156498162, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 00:20:00+00:00", "close_date": "2018-01-24 01:50:00+00:00", "trade_duration": 90, "open_rate": 0.04040781, "close_rate": 0.04061035541353383, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.474769110228938, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 06:45:00+00:00", "close_date": "2018-01-24 07:25:00+00:00", "trade_duration": 40, "open_rate": 5.132e-05, "close_rate": 5.157724310776942e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1948.5580670303975, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-24 14:15:00+00:00", "close_date": "2018-01-24 14:25:00+00:00", "trade_duration": 10, "open_rate": 5.198e-05, "close_rate": 5.432496240601503e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1923.8168526356292, "profit_abs": 0.0040000000000000036}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 14:50:00+00:00", "close_date": "2018-01-24 16:35:00+00:00", "trade_duration": 105, "open_rate": 3.054e-05, "close_rate": 3.069308270676692e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3274.3942370661425, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 15:10:00+00:00", "close_date": "2018-01-24 16:15:00+00:00", "trade_duration": 65, "open_rate": 9.263e-05, "close_rate": 9.309431077694236e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1079.5638562020945, "profit_abs": 2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 22:40:00+00:00", "close_date": "2018-01-24 23:25:00+00:00", "trade_duration": 45, "open_rate": 5.514e-05, "close_rate": 5.54163909774436e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1813.5654697134569, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 00:50:00+00:00", "close_date": "2018-01-25 01:30:00+00:00", "trade_duration": 40, "open_rate": 4.921e-05, "close_rate": 4.9456666666666664e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.1072952651903, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 08:15:00+00:00", "close_date": "2018-01-25 12:15:00+00:00", "trade_duration": 240, "open_rate": 0.0026, "close_rate": 0.002613032581453634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.46153846153847, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 10:25:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 350, "open_rate": 0.02799871, "close_rate": 0.028139054411027563, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.571593119825878, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 11:00:00+00:00", "close_date": "2018-01-25 11:45:00+00:00", "trade_duration": 45, "open_rate": 0.04078902, "close_rate": 0.0409934762406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4516401717913303, "profit_abs": -1.3877787807814457e-17}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:05:00+00:00", "close_date": "2018-01-25 13:45:00+00:00", "trade_duration": 40, "open_rate": 2.89e-05, "close_rate": 2.904486215538847e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3460.2076124567475, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:20:00+00:00", "close_date": "2018-01-25 14:05:00+00:00", "trade_duration": 45, "open_rate": 0.041103, "close_rate": 0.04130903007518797, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4329124394813033, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-25 15:45:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 30, "open_rate": 5.428e-05, "close_rate": 5.509624060150376e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1842.2991893883568, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 17:45:00+00:00", "close_date": "2018-01-25 23:15:00+00:00", "trade_duration": 330, "open_rate": 5.414e-05, "close_rate": 5.441137844611528e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1847.063169560399, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 21:15:00+00:00", "close_date": "2018-01-25 21:55:00+00:00", "trade_duration": 40, "open_rate": 0.04140777, "close_rate": 0.0416153277443609, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.415005686130888, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 02:05:00+00:00", "close_date": "2018-01-26 02:45:00+00:00", "trade_duration": 40, "open_rate": 0.00254309, "close_rate": 0.002555837318295739, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.32224183965177, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 02:55:00+00:00", "close_date": "2018-01-26 15:10:00+00:00", "trade_duration": 735, "open_rate": 5.607e-05, "close_rate": 5.6351052631578935e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1783.4849295523454, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 06:10:00+00:00", "close_date": "2018-01-26 09:25:00+00:00", "trade_duration": 195, "open_rate": 0.00253806, "close_rate": 0.0025507821052631577, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.400171784748984, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 07:25:00+00:00", "close_date": "2018-01-26 09:55:00+00:00", "trade_duration": 150, "open_rate": 0.0415, "close_rate": 0.04170802005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4096385542168677, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-26 09:55:00+00:00", "close_date": "2018-01-26 10:25:00+00:00", "trade_duration": 30, "open_rate": 5.321e-05, "close_rate": 5.401015037593984e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1879.3459875963165, "profit_abs": 0.000999999999999987}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 16:05:00+00:00", "close_date": "2018-01-26 16:45:00+00:00", "trade_duration": 40, "open_rate": 0.02772046, "close_rate": 0.02785940967418546, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.6074437437185387, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 23:35:00+00:00", "close_date": "2018-01-27 00:15:00+00:00", "trade_duration": 40, "open_rate": 0.09461341, "close_rate": 0.09508766268170424, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0569326272036914, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 00:35:00+00:00", "close_date": "2018-01-27 01:30:00+00:00", "trade_duration": 55, "open_rate": 5.615e-05, "close_rate": 5.643145363408521e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1780.9439002671415, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.07877175, "open_date": "2018-01-27 00:45:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 4560, "open_rate": 5.556e-05, "close_rate": 5.144e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1799.8560115190785, "profit_abs": -0.007896868250539965}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 02:30:00+00:00", "close_date": "2018-01-27 11:25:00+00:00", "trade_duration": 535, "open_rate": 0.06900001, "close_rate": 0.06934587471177944, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492751522789635, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 06:25:00+00:00", "close_date": "2018-01-27 07:05:00+00:00", "trade_duration": 40, "open_rate": 0.09449985, "close_rate": 0.0949735334586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.058202737887944, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.04815133, "open_date": "2018-01-27 09:40:00+00:00", "close_date": "2018-01-30 04:40:00+00:00", "trade_duration": 4020, "open_rate": 0.0410697, "close_rate": 0.03928809, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4348850855983852, "profit_abs": -0.004827170578309559}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 11:45:00+00:00", "close_date": "2018-01-27 12:30:00+00:00", "trade_duration": 45, "open_rate": 0.0285, "close_rate": 0.02864285714285714, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5087719298245617, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 12:35:00+00:00", "close_date": "2018-01-27 15:25:00+00:00", "trade_duration": 170, "open_rate": 0.02866372, "close_rate": 0.02880739779448621, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.4887307020861216, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 15:50:00+00:00", "close_date": "2018-01-27 16:50:00+00:00", "trade_duration": 60, "open_rate": 0.095381, "close_rate": 0.09585910025062656, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0484268355332824, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 17:05:00+00:00", "close_date": "2018-01-27 17:45:00+00:00", "trade_duration": 40, "open_rate": 0.06759092, "close_rate": 0.06792972160401002, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4794886650455417, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 23:40:00+00:00", "close_date": "2018-01-28 01:05:00+00:00", "trade_duration": 85, "open_rate": 0.00258501, "close_rate": 0.002597967443609022, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.684569885609726, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 02:25:00+00:00", "close_date": "2018-01-28 08:10:00+00:00", "trade_duration": 345, "open_rate": 0.06698502, "close_rate": 0.0673207845112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4928710926711672, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 10:25:00+00:00", "close_date": "2018-01-28 16:30:00+00:00", "trade_duration": 365, "open_rate": 0.0677177, "close_rate": 0.06805713709273183, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4767187899175547, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-28 20:35:00+00:00", "close_date": "2018-01-28 21:35:00+00:00", "trade_duration": 60, "open_rate": 5.215e-05, "close_rate": 5.2411403508771925e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1917.5455417066157, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-28 22:00:00+00:00", "close_date": "2018-01-28 22:30:00+00:00", "trade_duration": 30, "open_rate": 0.00273809, "close_rate": 0.002779264285714285, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.5218089982433, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-29 00:00:00+00:00", "close_date": "2018-01-29 00:30:00+00:00", "trade_duration": 30, "open_rate": 0.00274632, "close_rate": 0.002787618045112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.412362725392526, "profit_abs": 0.0010000000000000148}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-29 02:15:00+00:00", "close_date": "2018-01-29 03:00:00+00:00", "trade_duration": 45, "open_rate": 0.01622478, "close_rate": 0.016306107218045113, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.163411768911504, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 03:05:00+00:00", "close_date": "2018-01-29 03:45:00+00:00", "trade_duration": 40, "open_rate": 0.069, "close_rate": 0.06934586466165413, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492753623188406, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 05:20:00+00:00", "close_date": "2018-01-29 06:55:00+00:00", "trade_duration": 95, "open_rate": 8.755e-05, "close_rate": 8.798884711779448e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1142.204454597373, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 07:00:00+00:00", "close_date": "2018-01-29 19:25:00+00:00", "trade_duration": 745, "open_rate": 0.06825763, "close_rate": 0.06859977350877192, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4650376815016872, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 19:45:00+00:00", "close_date": "2018-01-29 20:25:00+00:00", "trade_duration": 40, "open_rate": 0.06713892, "close_rate": 0.06747545593984962, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4894490408841845, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0199116, "open_date": "2018-01-29 23:30:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 315, "open_rate": 8.934e-05, "close_rate": 8.8e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1119.3194537721067, "profit_abs": -0.0019961383478844796}], "results_per_pair": [{"key": "TRX/BTC", "trades": 15, "profit_mean": 0.0023467073333333323, "profit_mean_pct": 0.23467073333333321, "profit_sum": 0.035200609999999986, "profit_sum_pct": 3.5200609999999988, "profit_total_abs": 0.0035288616521155086, "profit_total_pct": 1.1733536666666662, "duration_avg": "2:28:00", "wins": 9, "draws": 2, "losses": 4}, {"key": "ADA/BTC", "trades": 29, "profit_mean": -0.0011598141379310352, "profit_mean_pct": -0.11598141379310352, "profit_sum": -0.03363461000000002, "profit_sum_pct": -3.3634610000000023, "profit_total_abs": -0.0033718682505400333, "profit_total_pct": -1.1211536666666675, "duration_avg": "5:35:00", "wins": 9, "draws": 11, "losses": 9}, {"key": "XLM/BTC", "trades": 21, "profit_mean": 0.0026243899999999994, "profit_mean_pct": 0.2624389999999999, "profit_sum": 0.05511218999999999, "profit_sum_pct": 5.511218999999999, "profit_total_abs": 0.005525000000000002, "profit_total_pct": 1.8370729999999995, "duration_avg": "3:21:00", "wins": 12, "draws": 3, "losses": 6}, {"key": "ETH/BTC", "trades": 21, "profit_mean": 0.0009500057142857142, "profit_mean_pct": 0.09500057142857142, "profit_sum": 0.01995012, "profit_sum_pct": 1.9950119999999998, "profit_total_abs": 0.0019999999999999463, "profit_total_pct": 0.6650039999999999, "duration_avg": "2:17:00", "wins": 5, "draws": 10, "losses": 6}, {"key": "XMR/BTC", "trades": 16, "profit_mean": -0.0027899012500000007, "profit_mean_pct": -0.2789901250000001, "profit_sum": -0.04463842000000001, "profit_sum_pct": -4.463842000000001, "profit_total_abs": -0.0044750000000000345, "profit_total_pct": -1.4879473333333337, "duration_avg": "8:41:00", "wins": 6, "draws": 5, "losses": 5}, {"key": "ZEC/BTC", "trades": 21, "profit_mean": -0.00039290904761904774, "profit_mean_pct": -0.03929090476190478, "profit_sum": -0.008251090000000003, "profit_sum_pct": -0.8251090000000003, "profit_total_abs": -0.000827170578309569, "profit_total_pct": -0.27503633333333344, "duration_avg": "4:17:00", "wins": 8, "draws": 7, "losses": 6}, {"key": "NXT/BTC", "trades": 12, "profit_mean": -0.0012261025000000006, "profit_mean_pct": -0.12261025000000006, "profit_sum": -0.014713230000000008, "profit_sum_pct": -1.4713230000000008, "profit_total_abs": -0.0014750000000000874, "profit_total_pct": -0.4904410000000003, "duration_avg": "0:57:00", "wins": 4, "draws": 3, "losses": 5}, {"key": "LTC/BTC", "trades": 8, "profit_mean": 0.00748129625, "profit_mean_pct": 0.748129625, "profit_sum": 0.05985037, "profit_sum_pct": 5.985037, "profit_total_abs": 0.006000000000000019, "profit_total_pct": 1.9950123333333334, "duration_avg": "1:59:00", "wins": 5, "draws": 2, "losses": 1}, {"key": "ETC/BTC", "trades": 20, "profit_mean": 0.0022568569999999997, "profit_mean_pct": 0.22568569999999996, "profit_sum": 0.04513713999999999, "profit_sum_pct": 4.513713999999999, "profit_total_abs": 0.004525000000000001, "profit_total_pct": 1.504571333333333, "duration_avg": "1:45:00", "wins": 11, "draws": 4, "losses": 5}, {"key": "DASH/BTC", "trades": 16, "profit_mean": 0.0018703237499999997, "profit_mean_pct": 0.18703237499999997, "profit_sum": 0.029925179999999996, "profit_sum_pct": 2.9925179999999996, "profit_total_abs": 0.002999999999999961, "profit_total_pct": 0.9975059999999999, "duration_avg": "3:03:00", "wins": 4, "draws": 7, "losses": 5}, {"key": "TOTAL", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}], "sell_reason_summary": [{"sell_reason": "roi", "trades": 170, "wins": 73, "draws": 54, "losses": 43, "profit_mean": 0.005398268352941177, "profit_mean_pct": 0.54, "profit_sum": 0.91770562, "profit_sum_pct": 91.77, "profit_total_abs": 0.09199999999999964, "profit_pct_total": 30.59}, {"sell_reason": "stop_loss", "trades": 6, "wins": 0, "draws": 0, "losses": 6, "profit_mean": -0.10448878000000002, "profit_mean_pct": -10.45, "profit_sum": -0.6269326800000001, "profit_sum_pct": -62.69, "profit_total_abs": -0.06284999999999992, "profit_pct_total": -20.9}, {"sell_reason": "force_sell", "trades": 3, "wins": 0, "draws": 0, "losses": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.89, "profit_sum": -0.14683468, "profit_sum_pct": -14.68, "profit_total_abs": -0.014720177176734003, "profit_pct_total": -4.89}], "left_open_trades": [{"key": "TRX/BTC", "trades": 1, "profit_mean": -0.0199116, "profit_mean_pct": -1.9911600000000003, "profit_sum": -0.0199116, "profit_sum_pct": -1.9911600000000003, "profit_total_abs": -0.0019961383478844796, "profit_total_pct": -0.6637200000000001, "duration_avg": "5:15:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ADA/BTC", "trades": 1, "profit_mean": -0.07877175, "profit_mean_pct": -7.877175, "profit_sum": -0.07877175, "profit_sum_pct": -7.877175, "profit_total_abs": -0.007896868250539965, "profit_total_pct": -2.625725, "duration_avg": "3 days, 4:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ZEC/BTC", "trades": 1, "profit_mean": -0.04815133, "profit_mean_pct": -4.815133, "profit_sum": -0.04815133, "profit_sum_pct": -4.815133, "profit_total_abs": -0.004827170578309559, "profit_total_pct": -1.6050443333333335, "duration_avg": "2 days, 19:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "TOTAL", "trades": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.894489333333333, "profit_sum": -0.14683468, "profit_sum_pct": -14.683468, "profit_total_abs": -0.014720177176734003, "profit_total_pct": -4.8944893333333335, "duration_avg": "2 days, 1:25:00", "wins": 0, "draws": 0, "losses": 3}], "total_trades": 179, "backtest_start": "2018-01-30 04:45:00+00:00", "backtest_start_ts": 1517287500, "backtest_end": "2018-01-30 04:45:00+00:00", "backtest_end_ts": 1517287500, "backtest_days": 0, "trades_per_day": null, "market_change": 0.25, "stake_amount": 0.1, "max_drawdown": 0.21142322000000008, "drawdown_start": "2018-01-24 14:25:00+00:00", "drawdown_start_ts": 1516803900.0, "drawdown_end": "2018-01-30 04:45:00+00:00", "drawdown_end_ts": 1517287500.0}, "TestStrategy": {"trades": [{"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:20:00+00:00", "trade_duration": 5, "open_rate": 9.64e-05, "close_rate": 0.00010074887218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1037.344398340249, "profit_abs": 0.00399999999999999}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:30:00+00:00", "trade_duration": 15, "open_rate": 4.756e-05, "close_rate": 4.9705563909774425e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2102.6072329688814, "profit_abs": 0.00399999999999999}, {"pair": "XLM/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:35:00+00:00", "trade_duration": 10, "open_rate": 3.339e-05, "close_rate": 3.489631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2994.908655286014, "profit_abs": 0.0040000000000000036}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:40:00+00:00", "trade_duration": 15, "open_rate": 9.696e-05, "close_rate": 0.00010133413533834584, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1031.3531353135315, "profit_abs": 0.00399999999999999}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 07:35:00+00:00", "close_date": "2018-01-10 08:35:00+00:00", "trade_duration": 60, "open_rate": 0.0943, "close_rate": 0.09477268170426063, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0604453870625663, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 07:40:00+00:00", "close_date": "2018-01-10 08:10:00+00:00", "trade_duration": 30, "open_rate": 0.02719607, "close_rate": 0.02760503345864661, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.677001860930642, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 08:15:00+00:00", "close_date": "2018-01-10 09:55:00+00:00", "trade_duration": 100, "open_rate": 0.04634952, "close_rate": 0.046581848421052625, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1575196463739, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 14:45:00+00:00", "close_date": "2018-01-10 15:50:00+00:00", "trade_duration": 65, "open_rate": 3.066e-05, "close_rate": 3.081368421052631e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3261.5786040443577, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 16:35:00+00:00", "close_date": "2018-01-10 17:15:00+00:00", "trade_duration": 40, "open_rate": 0.0168999, "close_rate": 0.016984611278195488, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.917194776300452, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 16:40:00+00:00", "close_date": "2018-01-10 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.09132568, "close_rate": 0.0917834528320802, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0949822656672252, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 18:50:00+00:00", "close_date": "2018-01-10 19:45:00+00:00", "trade_duration": 55, "open_rate": 0.08898003, "close_rate": 0.08942604518796991, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1238476768326557, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 22:15:00+00:00", "close_date": "2018-01-10 23:00:00+00:00", "trade_duration": 45, "open_rate": 0.08560008, "close_rate": 0.08602915308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1682232072680307, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 22:50:00+00:00", "close_date": "2018-01-10 23:20:00+00:00", "trade_duration": 30, "open_rate": 0.00249083, "close_rate": 0.0025282860902255634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 40.147260150231055, "profit_abs": 0.000999999999999987}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 23:15:00+00:00", "close_date": "2018-01-11 00:15:00+00:00", "trade_duration": 60, "open_rate": 3.022e-05, "close_rate": 3.037147869674185e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3309.0668431502318, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-10 23:40:00+00:00", "close_date": "2018-01-11 00:05:00+00:00", "trade_duration": 25, "open_rate": 0.002437, "close_rate": 0.0024980776942355883, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.03405826836274, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 00:00:00+00:00", "close_date": "2018-01-11 00:35:00+00:00", "trade_duration": 35, "open_rate": 0.04771803, "close_rate": 0.04843559436090225, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0956439316543456, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-11 03:40:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 45, "open_rate": 3.651e-05, "close_rate": 3.2859000000000005e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2738.9756231169545, "profit_abs": -0.01047499999999997}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 03:55:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 30, "open_rate": 0.08824105, "close_rate": 0.08956798308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1332594070446804, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 04:00:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 50, "open_rate": 0.00243, "close_rate": 0.002442180451127819, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.1522633744856, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:55:00+00:00", "trade_duration": 25, "open_rate": 0.04545064, "close_rate": 0.046589753784461146, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.200189040242338, "profit_abs": 0.001999999999999988}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 20, "open_rate": 3.372e-05, "close_rate": 3.456511278195488e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2965.599051008304, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:55:00+00:00", "close_date": "2018-01-11 05:15:00+00:00", "trade_duration": 20, "open_rate": 0.02644, "close_rate": 0.02710265664160401, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7821482602118004, "profit_abs": 0.001999999999999988}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:20:00+00:00", "close_date": "2018-01-11 12:00:00+00:00", "trade_duration": 40, "open_rate": 0.08812, "close_rate": 0.08856170426065162, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1348161597821154, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:35:00+00:00", "close_date": "2018-01-11 12:15:00+00:00", "trade_duration": 40, "open_rate": 0.02683577, "close_rate": 0.026970285137844607, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7263696923919087, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 14:00:00+00:00", "close_date": "2018-01-11 14:25:00+00:00", "trade_duration": 25, "open_rate": 4.919e-05, "close_rate": 5.04228320802005e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.9335230737956, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 19:25:00+00:00", "close_date": "2018-01-11 20:35:00+00:00", "trade_duration": 70, "open_rate": 0.08784896, "close_rate": 0.08828930566416039, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1383174029607181, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:35:00+00:00", "close_date": "2018-01-11 23:30:00+00:00", "trade_duration": 55, "open_rate": 5.105e-05, "close_rate": 5.130588972431077e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1958.8638589618022, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:25:00+00:00", "trade_duration": 30, "open_rate": 3.96e-05, "close_rate": 4.019548872180451e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2525.252525252525, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:35:00+00:00", "trade_duration": 40, "open_rate": 2.885e-05, "close_rate": 2.899461152882205e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3466.204506065858, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 23:30:00+00:00", "close_date": "2018-01-12 00:05:00+00:00", "trade_duration": 35, "open_rate": 0.02645, "close_rate": 0.026847744360902256, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.780718336483932, "profit_abs": 0.0010000000000000148}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 23:55:00+00:00", "close_date": "2018-01-12 01:15:00+00:00", "trade_duration": 80, "open_rate": 0.048, "close_rate": 0.04824060150375939, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0833333333333335, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-12 21:15:00+00:00", "close_date": "2018-01-12 21:40:00+00:00", "trade_duration": 25, "open_rate": 4.692e-05, "close_rate": 4.809593984962405e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2131.287297527707, "profit_abs": 0.001999999999999974}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 00:55:00+00:00", "close_date": "2018-01-13 06:20:00+00:00", "trade_duration": 325, "open_rate": 0.00256966, "close_rate": 0.0025825405012531327, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.91565421106294, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 10:55:00+00:00", "close_date": "2018-01-13 11:35:00+00:00", "trade_duration": 40, "open_rate": 6.262e-05, "close_rate": 6.293388471177944e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1596.933886937081, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 13:05:00+00:00", "close_date": "2018-01-15 14:10:00+00:00", "trade_duration": 2945, "open_rate": 4.73e-05, "close_rate": 4.753709273182957e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2114.1649048625795, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:30:00+00:00", "close_date": "2018-01-13 14:45:00+00:00", "trade_duration": 75, "open_rate": 6.063e-05, "close_rate": 6.0933909774436085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1649.348507339601, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:40:00+00:00", "close_date": "2018-01-13 23:30:00+00:00", "trade_duration": 590, "open_rate": 0.00011082, "close_rate": 0.00011137548872180448, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 902.3641941887746, "profit_abs": -2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 15:15:00+00:00", "close_date": "2018-01-13 15:55:00+00:00", "trade_duration": 40, "open_rate": 5.93e-05, "close_rate": 5.9597243107769415e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1686.3406408094436, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 16:30:00+00:00", "close_date": "2018-01-13 17:10:00+00:00", "trade_duration": 40, "open_rate": 0.04850003, "close_rate": 0.04874313791979949, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0618543947292407, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 22:05:00+00:00", "close_date": "2018-01-14 06:25:00+00:00", "trade_duration": 500, "open_rate": 0.09825019, "close_rate": 0.09874267215538848, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0178097365511456, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 00:20:00+00:00", "close_date": "2018-01-14 22:55:00+00:00", "trade_duration": 1355, "open_rate": 6.018e-05, "close_rate": 6.048165413533834e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1661.681621801263, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 12:45:00+00:00", "close_date": "2018-01-14 13:25:00+00:00", "trade_duration": 40, "open_rate": 0.09758999, "close_rate": 0.0980791628822055, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.024695258191952, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-14 15:30:00+00:00", "close_date": "2018-01-14 16:00:00+00:00", "trade_duration": 30, "open_rate": 0.00311, "close_rate": 0.0031567669172932328, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.154340836012864, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 20:45:00+00:00", "close_date": "2018-01-14 22:15:00+00:00", "trade_duration": 90, "open_rate": 0.00312401, "close_rate": 0.003139669197994987, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.010140812609436, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 23:35:00+00:00", "close_date": "2018-01-15 00:30:00+00:00", "trade_duration": 55, "open_rate": 0.0174679, "close_rate": 0.017555458395989976, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.724786608579165, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 23:45:00+00:00", "close_date": "2018-01-15 00:25:00+00:00", "trade_duration": 40, "open_rate": 0.07346846, "close_rate": 0.07383672295739348, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.3611282991367997, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 02:25:00+00:00", "close_date": "2018-01-15 03:05:00+00:00", "trade_duration": 40, "open_rate": 0.097994, "close_rate": 0.09848519799498744, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.020470641059657, "profit_abs": -2.7755575615628914e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 07:20:00+00:00", "close_date": "2018-01-15 08:00:00+00:00", "trade_duration": 40, "open_rate": 0.09659, "close_rate": 0.09707416040100247, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0353038616834043, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-15 08:20:00+00:00", "close_date": "2018-01-15 08:55:00+00:00", "trade_duration": 35, "open_rate": 9.987e-05, "close_rate": 0.00010137180451127818, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1001.3016921998599, "profit_abs": 0.0010000000000000009}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-15 12:10:00+00:00", "close_date": "2018-01-16 02:50:00+00:00", "trade_duration": 880, "open_rate": 0.0948969, "close_rate": 0.09537257368421052, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0537752023511833, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:10:00+00:00", "close_date": "2018-01-15 17:40:00+00:00", "trade_duration": 210, "open_rate": 0.071, "close_rate": 0.07135588972431077, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4084507042253522, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:30:00+00:00", "close_date": "2018-01-15 15:10:00+00:00", "trade_duration": 40, "open_rate": 0.04600501, "close_rate": 0.046235611553884705, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.173676301776698, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:10:00+00:00", "close_date": "2018-01-15 19:25:00+00:00", "trade_duration": 75, "open_rate": 9.438e-05, "close_rate": 9.485308270676693e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1059.5465140919687, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:35:00+00:00", "close_date": "2018-01-15 19:15:00+00:00", "trade_duration": 40, "open_rate": 0.03040001, "close_rate": 0.030552391002506264, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.2894726021471703, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-15 20:25:00+00:00", "close_date": "2018-01-16 08:25:00+00:00", "trade_duration": 720, "open_rate": 5.837e-05, "close_rate": 5.2533e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1713.2088401576154, "profit_abs": -0.010474999999999984}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 20:40:00+00:00", "close_date": "2018-01-15 22:00:00+00:00", "trade_duration": 80, "open_rate": 0.046036, "close_rate": 0.04626675689223057, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1722130506560084, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 00:30:00+00:00", "close_date": "2018-01-16 01:10:00+00:00", "trade_duration": 40, "open_rate": 0.0028685, "close_rate": 0.0028828784461152877, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 34.86142583231654, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 01:15:00+00:00", "close_date": "2018-01-16 02:35:00+00:00", "trade_duration": 80, "open_rate": 0.06731755, "close_rate": 0.0676549813283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4854967241083492, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 07:45:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 55, "open_rate": 0.09217614, "close_rate": 0.09263817578947368, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0848794492804754, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:55:00+00:00", "trade_duration": 20, "open_rate": 0.0165, "close_rate": 0.016913533834586467, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.0606060606060606, "profit_abs": 0.0020000000000000018}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 5, "open_rate": 7.953e-05, "close_rate": 8.311781954887218e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1257.387149503332, "profit_abs": 0.00399999999999999}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 08:45:00+00:00", "close_date": "2018-01-16 09:50:00+00:00", "trade_duration": 65, "open_rate": 0.045202, "close_rate": 0.04542857644110275, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2122914915269236, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:45:00+00:00", "trade_duration": 30, "open_rate": 5.248e-05, "close_rate": 5.326917293233082e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1905.487804878049, "profit_abs": 0.0010000000000000009}, {"pair": "XMR/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:55:00+00:00", "trade_duration": 40, "open_rate": 0.02892318, "close_rate": 0.02906815834586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.457434486802627, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 09:50:00+00:00", "close_date": "2018-01-16 10:10:00+00:00", "trade_duration": 20, "open_rate": 5.158e-05, "close_rate": 5.287273182957392e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1938.735944164405, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:35:00+00:00", "trade_duration": 30, "open_rate": 0.02828232, "close_rate": 0.02870761804511278, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5357778286929786, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:40:00+00:00", "trade_duration": 35, "open_rate": 0.04357584, "close_rate": 0.044231115789473675, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.294849623093898, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 13:45:00+00:00", "close_date": "2018-01-16 14:20:00+00:00", "trade_duration": 35, "open_rate": 5.362e-05, "close_rate": 5.442631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1864.975755315181, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 17:30:00+00:00", "close_date": "2018-01-16 18:25:00+00:00", "trade_duration": 55, "open_rate": 5.302e-05, "close_rate": 5.328576441102756e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.0807242549984, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:45:00+00:00", "trade_duration": 30, "open_rate": 0.09129999, "close_rate": 0.09267292218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0952903718828448, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:35:00+00:00", "trade_duration": 20, "open_rate": 3.808e-05, "close_rate": 3.903438596491228e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2626.0504201680674, "profit_abs": 0.0020000000000000018}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 19:00:00+00:00", "close_date": "2018-01-16 19:30:00+00:00", "trade_duration": 30, "open_rate": 0.02811012, "close_rate": 0.028532828571428567, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.557437677249333, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 60, "open_rate": 0.00258379, "close_rate": 0.002325411, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.702835756775904, "profit_abs": -0.010474999999999984}, {"pair": "NXT/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 80, "open_rate": 2.559e-05, "close_rate": 2.3031e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3907.7764751856193, "profit_abs": -0.010474999999999998}, {"pair": "TRX/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:35:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 50, "open_rate": 7.62e-05, "close_rate": 6.858e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1312.3359580052495, "profit_abs": -0.010474999999999984}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:35:00+00:00", "trade_duration": 5, "open_rate": 0.00229844, "close_rate": 0.002402129022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 43.507770487809125, "profit_abs": 0.004000000000000017}, {"pair": "LTC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:40:00+00:00", "trade_duration": 10, "open_rate": 0.0151, "close_rate": 0.015781203007518795, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.622516556291391, "profit_abs": 0.00399999999999999}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:40:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 5, "open_rate": 0.00235676, "close_rate": 0.00246308, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 42.431134269081284, "profit_abs": 0.0040000000000000036}, {"pair": "DASH/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 22:45:00+00:00", "close_date": "2018-01-16 23:05:00+00:00", "trade_duration": 20, "open_rate": 0.0630692, "close_rate": 0.06464988170426066, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.585559988076589, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:50:00+00:00", "close_date": "2018-01-16 22:55:00+00:00", "trade_duration": 5, "open_rate": 2.2e-05, "close_rate": 2.299248120300751e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 4545.454545454546, "profit_abs": 0.003999999999999976}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-17 03:30:00+00:00", "close_date": "2018-01-17 04:00:00+00:00", "trade_duration": 30, "open_rate": 4.974e-05, "close_rate": 5.048796992481203e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2010.454362685967, "profit_abs": 0.0010000000000000009}, {"pair": "TRX/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-17 03:55:00+00:00", "close_date": "2018-01-17 04:15:00+00:00", "trade_duration": 20, "open_rate": 7.108e-05, "close_rate": 7.28614536340852e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1406.8655036578502, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 09:35:00+00:00", "close_date": "2018-01-17 10:15:00+00:00", "trade_duration": 40, "open_rate": 0.04327, "close_rate": 0.04348689223057644, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.3110700254217704, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:20:00+00:00", "close_date": "2018-01-17 17:00:00+00:00", "trade_duration": 400, "open_rate": 4.997e-05, "close_rate": 5.022047619047618e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2001.2007204322595, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:25:00+00:00", "trade_duration": 55, "open_rate": 0.06836818, "close_rate": 0.06871087764411027, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4626687444363737, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:10:00+00:00", "trade_duration": 40, "open_rate": 3.63e-05, "close_rate": 3.648195488721804e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2754.8209366391184, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:30:00+00:00", "close_date": "2018-01-17 22:05:00+00:00", "trade_duration": 575, "open_rate": 0.0281, "close_rate": 0.02824085213032581, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5587188612099645, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:35:00+00:00", "close_date": "2018-01-17 16:55:00+00:00", "trade_duration": 260, "open_rate": 0.08651001, "close_rate": 0.08694364413533832, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1559355963546878, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 05:00:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 55, "open_rate": 5.633e-05, "close_rate": 5.6612355889724306e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1775.2529735487308, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 05:20:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 35, "open_rate": 0.06988494, "close_rate": 0.07093584135338346, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.430923457900944, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 07:35:00+00:00", "close_date": "2018-01-18 08:15:00+00:00", "trade_duration": 40, "open_rate": 5.545e-05, "close_rate": 5.572794486215538e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1803.4265103697026, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 09:00:00+00:00", "close_date": "2018-01-18 09:40:00+00:00", "trade_duration": 40, "open_rate": 0.01633527, "close_rate": 0.016417151052631574, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.121723118136401, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 16:40:00+00:00", "close_date": "2018-01-18 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.00269734, "close_rate": 0.002710860501253133, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.073561360451414, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-18 18:05:00+00:00", "close_date": "2018-01-18 18:30:00+00:00", "trade_duration": 25, "open_rate": 4.475e-05, "close_rate": 4.587155388471177e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2234.63687150838, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 18:25:00+00:00", "close_date": "2018-01-18 18:55:00+00:00", "trade_duration": 30, "open_rate": 2.79e-05, "close_rate": 2.8319548872180444e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3584.2293906810037, "profit_abs": 0.000999999999999987}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 20:10:00+00:00", "close_date": "2018-01-18 20:50:00+00:00", "trade_duration": 40, "open_rate": 0.04439326, "close_rate": 0.04461578260651629, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2525942001105577, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 21:30:00+00:00", "close_date": "2018-01-19 00:35:00+00:00", "trade_duration": 185, "open_rate": 4.49e-05, "close_rate": 4.51250626566416e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2227.1714922049, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 21:55:00+00:00", "close_date": "2018-01-19 05:05:00+00:00", "trade_duration": 430, "open_rate": 0.02855, "close_rate": 0.028693107769423555, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.502626970227671, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 22:10:00+00:00", "close_date": "2018-01-18 22:50:00+00:00", "trade_duration": 40, "open_rate": 5.796e-05, "close_rate": 5.8250526315789473e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1725.3278122843342, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 23:50:00+00:00", "close_date": "2018-01-19 00:30:00+00:00", "trade_duration": 40, "open_rate": 0.04340323, "close_rate": 0.04362079005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.303975994413319, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-19 16:45:00+00:00", "close_date": "2018-01-19 17:35:00+00:00", "trade_duration": 50, "open_rate": 0.04454455, "close_rate": 0.04476783095238095, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.244943545282195, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:15:00+00:00", "close_date": "2018-01-19 19:55:00+00:00", "trade_duration": 160, "open_rate": 5.62e-05, "close_rate": 5.648170426065162e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1779.3594306049824, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:20:00+00:00", "close_date": "2018-01-19 20:15:00+00:00", "trade_duration": 175, "open_rate": 4.339e-05, "close_rate": 4.360749373433584e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2304.6784973496196, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-20 04:45:00+00:00", "close_date": "2018-01-20 17:35:00+00:00", "trade_duration": 770, "open_rate": 0.0001009, "close_rate": 0.00010140576441102755, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 991.0802775024778, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 15:15:00+00:00", "trade_duration": 625, "open_rate": 0.00270505, "close_rate": 0.002718609147869674, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.96789338459548, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 07:00:00+00:00", "trade_duration": 130, "open_rate": 0.03000002, "close_rate": 0.030150396040100245, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.3333311111125927, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 09:00:00+00:00", "close_date": "2018-01-20 09:40:00+00:00", "trade_duration": 40, "open_rate": 5.46e-05, "close_rate": 5.4873684210526304e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1831.5018315018317, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-20 18:25:00+00:00", "close_date": "2018-01-25 03:50:00+00:00", "trade_duration": 6325, "open_rate": 0.03082222, "close_rate": 0.027739998, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.244412634781012, "profit_abs": -0.010474999999999998}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 22:25:00+00:00", "close_date": "2018-01-20 23:15:00+00:00", "trade_duration": 50, "open_rate": 0.08969999, "close_rate": 0.09014961401002504, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1148273260677064, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 02:50:00+00:00", "close_date": "2018-01-21 14:30:00+00:00", "trade_duration": 700, "open_rate": 0.01632501, "close_rate": 0.01640683962406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.125570520324337, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 10:20:00+00:00", "close_date": "2018-01-21 11:00:00+00:00", "trade_duration": 40, "open_rate": 0.070538, "close_rate": 0.07089157393483708, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.417675579120474, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 15:50:00+00:00", "close_date": "2018-01-21 18:45:00+00:00", "trade_duration": 175, "open_rate": 5.301e-05, "close_rate": 5.327571428571427e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.4365214110546, "profit_abs": -2.7755575615628914e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 16:20:00+00:00", "close_date": "2018-01-21 17:00:00+00:00", "trade_duration": 40, "open_rate": 3.955e-05, "close_rate": 3.9748245614035085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2528.4450063211125, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:45:00+00:00", "trade_duration": 30, "open_rate": 0.00258505, "close_rate": 0.002623922932330827, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.6839712964933, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:55:00+00:00", "trade_duration": 40, "open_rate": 3.903e-05, "close_rate": 3.922563909774435e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2562.1316935690497, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 00:35:00+00:00", "close_date": "2018-01-22 10:35:00+00:00", "trade_duration": 600, "open_rate": 5.236e-05, "close_rate": 5.262245614035087e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1909.8548510313217, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 01:30:00+00:00", "close_date": "2018-01-22 02:10:00+00:00", "trade_duration": 40, "open_rate": 9.028e-05, "close_rate": 9.07325313283208e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1107.6650420912717, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 12:25:00+00:00", "close_date": "2018-01-22 14:35:00+00:00", "trade_duration": 130, "open_rate": 0.002687, "close_rate": 0.002700468671679198, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.21622627465575, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 13:15:00+00:00", "close_date": "2018-01-22 13:55:00+00:00", "trade_duration": 40, "open_rate": 4.168e-05, "close_rate": 4.188892230576441e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2399.232245681382, "profit_abs": 1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-22 14:00:00+00:00", "close_date": "2018-01-22 14:30:00+00:00", "trade_duration": 30, "open_rate": 8.821e-05, "close_rate": 8.953646616541353e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1133.6583153837435, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 15:55:00+00:00", "close_date": "2018-01-22 16:40:00+00:00", "trade_duration": 45, "open_rate": 5.172e-05, "close_rate": 5.1979248120300745e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1933.4880123743235, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-22 16:05:00+00:00", "close_date": "2018-01-22 16:25:00+00:00", "trade_duration": 20, "open_rate": 3.026e-05, "close_rate": 3.101839598997494e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3304.692663582287, "profit_abs": 0.0020000000000000157}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 19:50:00+00:00", "close_date": "2018-01-23 00:10:00+00:00", "trade_duration": 260, "open_rate": 0.07064, "close_rate": 0.07099408521303258, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.415628539071348, "profit_abs": 1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 21:25:00+00:00", "close_date": "2018-01-22 22:05:00+00:00", "trade_duration": 40, "open_rate": 0.01644483, "close_rate": 0.01652726022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.080938507725528, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-23 00:05:00+00:00", "close_date": "2018-01-23 00:35:00+00:00", "trade_duration": 30, "open_rate": 4.331e-05, "close_rate": 4.3961278195488714e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2308.935580697299, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-23 01:50:00+00:00", "close_date": "2018-01-23 02:15:00+00:00", "trade_duration": 25, "open_rate": 3.2e-05, "close_rate": 3.2802005012531326e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3125.0000000000005, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 04:25:00+00:00", "close_date": "2018-01-23 05:15:00+00:00", "trade_duration": 50, "open_rate": 0.09167706, "close_rate": 0.09213659413533835, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0907854156754153, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 07:35:00+00:00", "close_date": "2018-01-23 09:00:00+00:00", "trade_duration": 85, "open_rate": 0.0692498, "close_rate": 0.06959691679197995, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4440474918339115, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 10:50:00+00:00", "close_date": "2018-01-23 13:05:00+00:00", "trade_duration": 135, "open_rate": 3.182e-05, "close_rate": 3.197949874686716e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3142.677561282213, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 11:05:00+00:00", "close_date": "2018-01-23 16:05:00+00:00", "trade_duration": 300, "open_rate": 0.04088, "close_rate": 0.04108491228070175, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4461839530332683, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 14:55:00+00:00", "close_date": "2018-01-23 15:35:00+00:00", "trade_duration": 40, "open_rate": 5.15e-05, "close_rate": 5.175814536340851e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1941.747572815534, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 16:35:00+00:00", "close_date": "2018-01-24 00:05:00+00:00", "trade_duration": 450, "open_rate": 0.09071698, "close_rate": 0.09117170170426064, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1023294646713329, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 17:25:00+00:00", "close_date": "2018-01-23 18:45:00+00:00", "trade_duration": 80, "open_rate": 3.128e-05, "close_rate": 3.1436791979949865e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3196.9309462915603, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 20:15:00+00:00", "close_date": "2018-01-23 22:00:00+00:00", "trade_duration": 105, "open_rate": 9.555e-05, "close_rate": 9.602894736842104e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1046.5724751439038, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 22:30:00+00:00", "close_date": "2018-01-23 23:10:00+00:00", "trade_duration": 40, "open_rate": 0.04080001, "close_rate": 0.0410045213283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.450979791426522, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 23:50:00+00:00", "close_date": "2018-01-24 03:35:00+00:00", "trade_duration": 225, "open_rate": 5.163e-05, "close_rate": 5.18887969924812e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1936.8584156498162, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 00:20:00+00:00", "close_date": "2018-01-24 01:50:00+00:00", "trade_duration": 90, "open_rate": 0.04040781, "close_rate": 0.04061035541353383, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.474769110228938, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 06:45:00+00:00", "close_date": "2018-01-24 07:25:00+00:00", "trade_duration": 40, "open_rate": 5.132e-05, "close_rate": 5.157724310776942e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1948.5580670303975, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-24 14:15:00+00:00", "close_date": "2018-01-24 14:25:00+00:00", "trade_duration": 10, "open_rate": 5.198e-05, "close_rate": 5.432496240601503e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1923.8168526356292, "profit_abs": 0.0040000000000000036}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 14:50:00+00:00", "close_date": "2018-01-24 16:35:00+00:00", "trade_duration": 105, "open_rate": 3.054e-05, "close_rate": 3.069308270676692e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3274.3942370661425, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 15:10:00+00:00", "close_date": "2018-01-24 16:15:00+00:00", "trade_duration": 65, "open_rate": 9.263e-05, "close_rate": 9.309431077694236e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1079.5638562020945, "profit_abs": 2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 22:40:00+00:00", "close_date": "2018-01-24 23:25:00+00:00", "trade_duration": 45, "open_rate": 5.514e-05, "close_rate": 5.54163909774436e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1813.5654697134569, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 00:50:00+00:00", "close_date": "2018-01-25 01:30:00+00:00", "trade_duration": 40, "open_rate": 4.921e-05, "close_rate": 4.9456666666666664e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.1072952651903, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 08:15:00+00:00", "close_date": "2018-01-25 12:15:00+00:00", "trade_duration": 240, "open_rate": 0.0026, "close_rate": 0.002613032581453634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.46153846153847, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 10:25:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 350, "open_rate": 0.02799871, "close_rate": 0.028139054411027563, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.571593119825878, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 11:00:00+00:00", "close_date": "2018-01-25 11:45:00+00:00", "trade_duration": 45, "open_rate": 0.04078902, "close_rate": 0.0409934762406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4516401717913303, "profit_abs": -1.3877787807814457e-17}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:05:00+00:00", "close_date": "2018-01-25 13:45:00+00:00", "trade_duration": 40, "open_rate": 2.89e-05, "close_rate": 2.904486215538847e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3460.2076124567475, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:20:00+00:00", "close_date": "2018-01-25 14:05:00+00:00", "trade_duration": 45, "open_rate": 0.041103, "close_rate": 0.04130903007518797, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4329124394813033, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-25 15:45:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 30, "open_rate": 5.428e-05, "close_rate": 5.509624060150376e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1842.2991893883568, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 17:45:00+00:00", "close_date": "2018-01-25 23:15:00+00:00", "trade_duration": 330, "open_rate": 5.414e-05, "close_rate": 5.441137844611528e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1847.063169560399, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 21:15:00+00:00", "close_date": "2018-01-25 21:55:00+00:00", "trade_duration": 40, "open_rate": 0.04140777, "close_rate": 0.0416153277443609, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.415005686130888, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 02:05:00+00:00", "close_date": "2018-01-26 02:45:00+00:00", "trade_duration": 40, "open_rate": 0.00254309, "close_rate": 0.002555837318295739, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.32224183965177, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 02:55:00+00:00", "close_date": "2018-01-26 15:10:00+00:00", "trade_duration": 735, "open_rate": 5.607e-05, "close_rate": 5.6351052631578935e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1783.4849295523454, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 06:10:00+00:00", "close_date": "2018-01-26 09:25:00+00:00", "trade_duration": 195, "open_rate": 0.00253806, "close_rate": 0.0025507821052631577, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.400171784748984, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 07:25:00+00:00", "close_date": "2018-01-26 09:55:00+00:00", "trade_duration": 150, "open_rate": 0.0415, "close_rate": 0.04170802005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4096385542168677, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-26 09:55:00+00:00", "close_date": "2018-01-26 10:25:00+00:00", "trade_duration": 30, "open_rate": 5.321e-05, "close_rate": 5.401015037593984e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1879.3459875963165, "profit_abs": 0.000999999999999987}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 16:05:00+00:00", "close_date": "2018-01-26 16:45:00+00:00", "trade_duration": 40, "open_rate": 0.02772046, "close_rate": 0.02785940967418546, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.6074437437185387, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 23:35:00+00:00", "close_date": "2018-01-27 00:15:00+00:00", "trade_duration": 40, "open_rate": 0.09461341, "close_rate": 0.09508766268170424, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0569326272036914, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 00:35:00+00:00", "close_date": "2018-01-27 01:30:00+00:00", "trade_duration": 55, "open_rate": 5.615e-05, "close_rate": 5.643145363408521e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1780.9439002671415, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.07877175, "open_date": "2018-01-27 00:45:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 4560, "open_rate": 5.556e-05, "close_rate": 5.144e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1799.8560115190785, "profit_abs": -0.007896868250539965}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 02:30:00+00:00", "close_date": "2018-01-27 11:25:00+00:00", "trade_duration": 535, "open_rate": 0.06900001, "close_rate": 0.06934587471177944, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492751522789635, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 06:25:00+00:00", "close_date": "2018-01-27 07:05:00+00:00", "trade_duration": 40, "open_rate": 0.09449985, "close_rate": 0.0949735334586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.058202737887944, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.04815133, "open_date": "2018-01-27 09:40:00+00:00", "close_date": "2018-01-30 04:40:00+00:00", "trade_duration": 4020, "open_rate": 0.0410697, "close_rate": 0.03928809, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4348850855983852, "profit_abs": -0.004827170578309559}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 11:45:00+00:00", "close_date": "2018-01-27 12:30:00+00:00", "trade_duration": 45, "open_rate": 0.0285, "close_rate": 0.02864285714285714, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5087719298245617, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 12:35:00+00:00", "close_date": "2018-01-27 15:25:00+00:00", "trade_duration": 170, "open_rate": 0.02866372, "close_rate": 0.02880739779448621, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.4887307020861216, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 15:50:00+00:00", "close_date": "2018-01-27 16:50:00+00:00", "trade_duration": 60, "open_rate": 0.095381, "close_rate": 0.09585910025062656, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0484268355332824, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 17:05:00+00:00", "close_date": "2018-01-27 17:45:00+00:00", "trade_duration": 40, "open_rate": 0.06759092, "close_rate": 0.06792972160401002, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4794886650455417, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 23:40:00+00:00", "close_date": "2018-01-28 01:05:00+00:00", "trade_duration": 85, "open_rate": 0.00258501, "close_rate": 0.002597967443609022, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.684569885609726, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 02:25:00+00:00", "close_date": "2018-01-28 08:10:00+00:00", "trade_duration": 345, "open_rate": 0.06698502, "close_rate": 0.0673207845112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4928710926711672, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 10:25:00+00:00", "close_date": "2018-01-28 16:30:00+00:00", "trade_duration": 365, "open_rate": 0.0677177, "close_rate": 0.06805713709273183, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4767187899175547, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-28 20:35:00+00:00", "close_date": "2018-01-28 21:35:00+00:00", "trade_duration": 60, "open_rate": 5.215e-05, "close_rate": 5.2411403508771925e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1917.5455417066157, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-28 22:00:00+00:00", "close_date": "2018-01-28 22:30:00+00:00", "trade_duration": 30, "open_rate": 0.00273809, "close_rate": 0.002779264285714285, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.5218089982433, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-29 00:00:00+00:00", "close_date": "2018-01-29 00:30:00+00:00", "trade_duration": 30, "open_rate": 0.00274632, "close_rate": 0.002787618045112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.412362725392526, "profit_abs": 0.0010000000000000148}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-29 02:15:00+00:00", "close_date": "2018-01-29 03:00:00+00:00", "trade_duration": 45, "open_rate": 0.01622478, "close_rate": 0.016306107218045113, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.163411768911504, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 03:05:00+00:00", "close_date": "2018-01-29 03:45:00+00:00", "trade_duration": 40, "open_rate": 0.069, "close_rate": 0.06934586466165413, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492753623188406, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 05:20:00+00:00", "close_date": "2018-01-29 06:55:00+00:00", "trade_duration": 95, "open_rate": 8.755e-05, "close_rate": 8.798884711779448e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1142.204454597373, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 07:00:00+00:00", "close_date": "2018-01-29 19:25:00+00:00", "trade_duration": 745, "open_rate": 0.06825763, "close_rate": 0.06859977350877192, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4650376815016872, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 19:45:00+00:00", "close_date": "2018-01-29 20:25:00+00:00", "trade_duration": 40, "open_rate": 0.06713892, "close_rate": 0.06747545593984962, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4894490408841845, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0199116, "open_date": "2018-01-29 23:30:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 315, "open_rate": 8.934e-05, "close_rate": 8.8e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1119.3194537721067, "profit_abs": -0.0019961383478844796}], "results_per_pair": [{"key": "TRX/BTC", "trades": 15, "profit_mean": 0.0023467073333333323, "profit_mean_pct": 0.23467073333333321, "profit_sum": 0.035200609999999986, "profit_sum_pct": 3.5200609999999988, "profit_total_abs": 0.0035288616521155086, "profit_total_pct": 1.1733536666666662, "duration_avg": "2:28:00", "wins": 9, "draws": 2, "losses": 4}, {"key": "ADA/BTC", "trades": 29, "profit_mean": -0.0011598141379310352, "profit_mean_pct": -0.11598141379310352, "profit_sum": -0.03363461000000002, "profit_sum_pct": -3.3634610000000023, "profit_total_abs": -0.0033718682505400333, "profit_total_pct": -1.1211536666666675, "duration_avg": "5:35:00", "wins": 9, "draws": 11, "losses": 9}, {"key": "XLM/BTC", "trades": 21, "profit_mean": 0.0026243899999999994, "profit_mean_pct": 0.2624389999999999, "profit_sum": 0.05511218999999999, "profit_sum_pct": 5.511218999999999, "profit_total_abs": 0.005525000000000002, "profit_total_pct": 1.8370729999999995, "duration_avg": "3:21:00", "wins": 12, "draws": 3, "losses": 6}, {"key": "ETH/BTC", "trades": 21, "profit_mean": 0.0009500057142857142, "profit_mean_pct": 0.09500057142857142, "profit_sum": 0.01995012, "profit_sum_pct": 1.9950119999999998, "profit_total_abs": 0.0019999999999999463, "profit_total_pct": 0.6650039999999999, "duration_avg": "2:17:00", "wins": 5, "draws": 10, "losses": 6}, {"key": "XMR/BTC", "trades": 16, "profit_mean": -0.0027899012500000007, "profit_mean_pct": -0.2789901250000001, "profit_sum": -0.04463842000000001, "profit_sum_pct": -4.463842000000001, "profit_total_abs": -0.0044750000000000345, "profit_total_pct": -1.4879473333333337, "duration_avg": "8:41:00", "wins": 6, "draws": 5, "losses": 5}, {"key": "ZEC/BTC", "trades": 21, "profit_mean": -0.00039290904761904774, "profit_mean_pct": -0.03929090476190478, "profit_sum": -0.008251090000000003, "profit_sum_pct": -0.8251090000000003, "profit_total_abs": -0.000827170578309569, "profit_total_pct": -0.27503633333333344, "duration_avg": "4:17:00", "wins": 8, "draws": 7, "losses": 6}, {"key": "NXT/BTC", "trades": 12, "profit_mean": -0.0012261025000000006, "profit_mean_pct": -0.12261025000000006, "profit_sum": -0.014713230000000008, "profit_sum_pct": -1.4713230000000008, "profit_total_abs": -0.0014750000000000874, "profit_total_pct": -0.4904410000000003, "duration_avg": "0:57:00", "wins": 4, "draws": 3, "losses": 5}, {"key": "LTC/BTC", "trades": 8, "profit_mean": 0.00748129625, "profit_mean_pct": 0.748129625, "profit_sum": 0.05985037, "profit_sum_pct": 5.985037, "profit_total_abs": 0.006000000000000019, "profit_total_pct": 1.9950123333333334, "duration_avg": "1:59:00", "wins": 5, "draws": 2, "losses": 1}, {"key": "ETC/BTC", "trades": 20, "profit_mean": 0.0022568569999999997, "profit_mean_pct": 0.22568569999999996, "profit_sum": 0.04513713999999999, "profit_sum_pct": 4.513713999999999, "profit_total_abs": 0.004525000000000001, "profit_total_pct": 1.504571333333333, "duration_avg": "1:45:00", "wins": 11, "draws": 4, "losses": 5}, {"key": "DASH/BTC", "trades": 16, "profit_mean": 0.0018703237499999997, "profit_mean_pct": 0.18703237499999997, "profit_sum": 0.029925179999999996, "profit_sum_pct": 2.9925179999999996, "profit_total_abs": 0.002999999999999961, "profit_total_pct": 0.9975059999999999, "duration_avg": "3:03:00", "wins": 4, "draws": 7, "losses": 5}, {"key": "TOTAL", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}], "sell_reason_summary": [{"sell_reason": "roi", "trades": 170, "wins": 73, "draws": 54, "losses": 43, "profit_mean": 0.005398268352941177, "profit_mean_pct": 0.54, "profit_sum": 0.91770562, "profit_sum_pct": 91.77, "profit_total_abs": 0.09199999999999964, "profit_pct_total": 30.59}, {"sell_reason": "stop_loss", "trades": 6, "wins": 0, "draws": 0, "losses": 6, "profit_mean": -0.10448878000000002, "profit_mean_pct": -10.45, "profit_sum": -0.6269326800000001, "profit_sum_pct": -62.69, "profit_total_abs": -0.06284999999999992, "profit_pct_total": -20.9}, {"sell_reason": "force_sell", "trades": 3, "wins": 0, "draws": 0, "losses": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.89, "profit_sum": -0.14683468, "profit_sum_pct": -14.68, "profit_total_abs": -0.014720177176734003, "profit_pct_total": -4.89}], "left_open_trades": [{"key": "TRX/BTC", "trades": 1, "profit_mean": -0.0199116, "profit_mean_pct": -1.9911600000000003, "profit_sum": -0.0199116, "profit_sum_pct": -1.9911600000000003, "profit_total_abs": -0.0019961383478844796, "profit_total_pct": -0.6637200000000001, "duration_avg": "5:15:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ADA/BTC", "trades": 1, "profit_mean": -0.07877175, "profit_mean_pct": -7.877175, "profit_sum": -0.07877175, "profit_sum_pct": -7.877175, "profit_total_abs": -0.007896868250539965, "profit_total_pct": -2.625725, "duration_avg": "3 days, 4:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ZEC/BTC", "trades": 1, "profit_mean": -0.04815133, "profit_mean_pct": -4.815133, "profit_sum": -0.04815133, "profit_sum_pct": -4.815133, "profit_total_abs": -0.004827170578309559, "profit_total_pct": -1.6050443333333335, "duration_avg": "2 days, 19:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "TOTAL", "trades": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.894489333333333, "profit_sum": -0.14683468, "profit_sum_pct": -14.683468, "profit_total_abs": -0.014720177176734003, "profit_total_pct": -4.8944893333333335, "duration_avg": "2 days, 1:25:00", "wins": 0, "draws": 0, "losses": 3}], "total_trades": 179, "backtest_start": "2018-01-30 04:45:00+00:00", "backtest_start_ts": 1517287500, "backtest_end": "2018-01-30 04:45:00+00:00", "backtest_end_ts": 1517287500, "backtest_days": 0, "trades_per_day": null, "market_change": 0.25, "stake_amount": 0.1, "max_drawdown": 0.21142322000000008, "drawdown_start": "2018-01-24 14:25:00+00:00", "drawdown_start_ts": 1516803900.0, "drawdown_end": "2018-01-30 04:45:00+00:00", "drawdown_end_ts": 1517287500.0}}, "strategy_comparison": [{"key": "DefaultStrategy", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}, {"key": "TestStrategy", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}]} \ No newline at end of file +{"strategy": {"DefaultStrategy": {"trades": [{"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:20:00+00:00", "trade_duration": 5, "open_rate": 9.64e-05, "close_rate": 0.00010074887218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1037.344398340249, "profit_abs": 0.00399999999999999}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:30:00+00:00", "trade_duration": 15, "open_rate": 4.756e-05, "close_rate": 4.9705563909774425e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2102.6072329688814, "profit_abs": 0.00399999999999999}, {"pair": "XLM/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:35:00+00:00", "trade_duration": 10, "open_rate": 3.339e-05, "close_rate": 3.489631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2994.908655286014, "profit_abs": 0.0040000000000000036}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:40:00+00:00", "trade_duration": 15, "open_rate": 9.696e-05, "close_rate": 0.00010133413533834584, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1031.3531353135315, "profit_abs": 0.00399999999999999}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 07:35:00+00:00", "close_date": "2018-01-10 08:35:00+00:00", "trade_duration": 60, "open_rate": 0.0943, "close_rate": 0.09477268170426063, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0604453870625663, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 07:40:00+00:00", "close_date": "2018-01-10 08:10:00+00:00", "trade_duration": 30, "open_rate": 0.02719607, "close_rate": 0.02760503345864661, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.677001860930642, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 08:15:00+00:00", "close_date": "2018-01-10 09:55:00+00:00", "trade_duration": 100, "open_rate": 0.04634952, "close_rate": 0.046581848421052625, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1575196463739, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 14:45:00+00:00", "close_date": "2018-01-10 15:50:00+00:00", "trade_duration": 65, "open_rate": 3.066e-05, "close_rate": 3.081368421052631e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3261.5786040443577, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 16:35:00+00:00", "close_date": "2018-01-10 17:15:00+00:00", "trade_duration": 40, "open_rate": 0.0168999, "close_rate": 0.016984611278195488, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.917194776300452, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 16:40:00+00:00", "close_date": "2018-01-10 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.09132568, "close_rate": 0.0917834528320802, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0949822656672252, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 18:50:00+00:00", "close_date": "2018-01-10 19:45:00+00:00", "trade_duration": 55, "open_rate": 0.08898003, "close_rate": 0.08942604518796991, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1238476768326557, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 22:15:00+00:00", "close_date": "2018-01-10 23:00:00+00:00", "trade_duration": 45, "open_rate": 0.08560008, "close_rate": 0.08602915308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1682232072680307, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 22:50:00+00:00", "close_date": "2018-01-10 23:20:00+00:00", "trade_duration": 30, "open_rate": 0.00249083, "close_rate": 0.0025282860902255634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 40.147260150231055, "profit_abs": 0.000999999999999987}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 23:15:00+00:00", "close_date": "2018-01-11 00:15:00+00:00", "trade_duration": 60, "open_rate": 3.022e-05, "close_rate": 3.037147869674185e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3309.0668431502318, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-10 23:40:00+00:00", "close_date": "2018-01-11 00:05:00+00:00", "trade_duration": 25, "open_rate": 0.002437, "close_rate": 0.0024980776942355883, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.03405826836274, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 00:00:00+00:00", "close_date": "2018-01-11 00:35:00+00:00", "trade_duration": 35, "open_rate": 0.04771803, "close_rate": 0.04843559436090225, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0956439316543456, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-11 03:40:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 45, "open_rate": 3.651e-05, "close_rate": 3.2859000000000005e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2738.9756231169545, "profit_abs": -0.01047499999999997}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 03:55:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 30, "open_rate": 0.08824105, "close_rate": 0.08956798308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1332594070446804, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 04:00:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 50, "open_rate": 0.00243, "close_rate": 0.002442180451127819, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.1522633744856, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:55:00+00:00", "trade_duration": 25, "open_rate": 0.04545064, "close_rate": 0.046589753784461146, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.200189040242338, "profit_abs": 0.001999999999999988}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 20, "open_rate": 3.372e-05, "close_rate": 3.456511278195488e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2965.599051008304, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:55:00+00:00", "close_date": "2018-01-11 05:15:00+00:00", "trade_duration": 20, "open_rate": 0.02644, "close_rate": 0.02710265664160401, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7821482602118004, "profit_abs": 0.001999999999999988}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:20:00+00:00", "close_date": "2018-01-11 12:00:00+00:00", "trade_duration": 40, "open_rate": 0.08812, "close_rate": 0.08856170426065162, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1348161597821154, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:35:00+00:00", "close_date": "2018-01-11 12:15:00+00:00", "trade_duration": 40, "open_rate": 0.02683577, "close_rate": 0.026970285137844607, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7263696923919087, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 14:00:00+00:00", "close_date": "2018-01-11 14:25:00+00:00", "trade_duration": 25, "open_rate": 4.919e-05, "close_rate": 5.04228320802005e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.9335230737956, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 19:25:00+00:00", "close_date": "2018-01-11 20:35:00+00:00", "trade_duration": 70, "open_rate": 0.08784896, "close_rate": 0.08828930566416039, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1383174029607181, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:35:00+00:00", "close_date": "2018-01-11 23:30:00+00:00", "trade_duration": 55, "open_rate": 5.105e-05, "close_rate": 5.130588972431077e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1958.8638589618022, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:25:00+00:00", "trade_duration": 30, "open_rate": 3.96e-05, "close_rate": 4.019548872180451e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2525.252525252525, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:35:00+00:00", "trade_duration": 40, "open_rate": 2.885e-05, "close_rate": 2.899461152882205e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3466.204506065858, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 23:30:00+00:00", "close_date": "2018-01-12 00:05:00+00:00", "trade_duration": 35, "open_rate": 0.02645, "close_rate": 0.026847744360902256, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.780718336483932, "profit_abs": 0.0010000000000000148}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 23:55:00+00:00", "close_date": "2018-01-12 01:15:00+00:00", "trade_duration": 80, "open_rate": 0.048, "close_rate": 0.04824060150375939, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0833333333333335, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-12 21:15:00+00:00", "close_date": "2018-01-12 21:40:00+00:00", "trade_duration": 25, "open_rate": 4.692e-05, "close_rate": 4.809593984962405e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2131.287297527707, "profit_abs": 0.001999999999999974}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 00:55:00+00:00", "close_date": "2018-01-13 06:20:00+00:00", "trade_duration": 325, "open_rate": 0.00256966, "close_rate": 0.0025825405012531327, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.91565421106294, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 10:55:00+00:00", "close_date": "2018-01-13 11:35:00+00:00", "trade_duration": 40, "open_rate": 6.262e-05, "close_rate": 6.293388471177944e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1596.933886937081, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 13:05:00+00:00", "close_date": "2018-01-15 14:10:00+00:00", "trade_duration": 2945, "open_rate": 4.73e-05, "close_rate": 4.753709273182957e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2114.1649048625795, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:30:00+00:00", "close_date": "2018-01-13 14:45:00+00:00", "trade_duration": 75, "open_rate": 6.063e-05, "close_rate": 6.0933909774436085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1649.348507339601, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:40:00+00:00", "close_date": "2018-01-13 23:30:00+00:00", "trade_duration": 590, "open_rate": 0.00011082, "close_rate": 0.00011137548872180448, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 902.3641941887746, "profit_abs": -2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 15:15:00+00:00", "close_date": "2018-01-13 15:55:00+00:00", "trade_duration": 40, "open_rate": 5.93e-05, "close_rate": 5.9597243107769415e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1686.3406408094436, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 16:30:00+00:00", "close_date": "2018-01-13 17:10:00+00:00", "trade_duration": 40, "open_rate": 0.04850003, "close_rate": 0.04874313791979949, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0618543947292407, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 22:05:00+00:00", "close_date": "2018-01-14 06:25:00+00:00", "trade_duration": 500, "open_rate": 0.09825019, "close_rate": 0.09874267215538848, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0178097365511456, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 00:20:00+00:00", "close_date": "2018-01-14 22:55:00+00:00", "trade_duration": 1355, "open_rate": 6.018e-05, "close_rate": 6.048165413533834e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1661.681621801263, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 12:45:00+00:00", "close_date": "2018-01-14 13:25:00+00:00", "trade_duration": 40, "open_rate": 0.09758999, "close_rate": 0.0980791628822055, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.024695258191952, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-14 15:30:00+00:00", "close_date": "2018-01-14 16:00:00+00:00", "trade_duration": 30, "open_rate": 0.00311, "close_rate": 0.0031567669172932328, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.154340836012864, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 20:45:00+00:00", "close_date": "2018-01-14 22:15:00+00:00", "trade_duration": 90, "open_rate": 0.00312401, "close_rate": 0.003139669197994987, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.010140812609436, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 23:35:00+00:00", "close_date": "2018-01-15 00:30:00+00:00", "trade_duration": 55, "open_rate": 0.0174679, "close_rate": 0.017555458395989976, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.724786608579165, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 23:45:00+00:00", "close_date": "2018-01-15 00:25:00+00:00", "trade_duration": 40, "open_rate": 0.07346846, "close_rate": 0.07383672295739348, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.3611282991367997, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 02:25:00+00:00", "close_date": "2018-01-15 03:05:00+00:00", "trade_duration": 40, "open_rate": 0.097994, "close_rate": 0.09848519799498744, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.020470641059657, "profit_abs": -2.7755575615628914e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 07:20:00+00:00", "close_date": "2018-01-15 08:00:00+00:00", "trade_duration": 40, "open_rate": 0.09659, "close_rate": 0.09707416040100247, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0353038616834043, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-15 08:20:00+00:00", "close_date": "2018-01-15 08:55:00+00:00", "trade_duration": 35, "open_rate": 9.987e-05, "close_rate": 0.00010137180451127818, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1001.3016921998599, "profit_abs": 0.0010000000000000009}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-15 12:10:00+00:00", "close_date": "2018-01-16 02:50:00+00:00", "trade_duration": 880, "open_rate": 0.0948969, "close_rate": 0.09537257368421052, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0537752023511833, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:10:00+00:00", "close_date": "2018-01-15 17:40:00+00:00", "trade_duration": 210, "open_rate": 0.071, "close_rate": 0.07135588972431077, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4084507042253522, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:30:00+00:00", "close_date": "2018-01-15 15:10:00+00:00", "trade_duration": 40, "open_rate": 0.04600501, "close_rate": 0.046235611553884705, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.173676301776698, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:10:00+00:00", "close_date": "2018-01-15 19:25:00+00:00", "trade_duration": 75, "open_rate": 9.438e-05, "close_rate": 9.485308270676693e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1059.5465140919687, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:35:00+00:00", "close_date": "2018-01-15 19:15:00+00:00", "trade_duration": 40, "open_rate": 0.03040001, "close_rate": 0.030552391002506264, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.2894726021471703, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-15 20:25:00+00:00", "close_date": "2018-01-16 08:25:00+00:00", "trade_duration": 720, "open_rate": 5.837e-05, "close_rate": 5.2533e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1713.2088401576154, "profit_abs": -0.010474999999999984}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 20:40:00+00:00", "close_date": "2018-01-15 22:00:00+00:00", "trade_duration": 80, "open_rate": 0.046036, "close_rate": 0.04626675689223057, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1722130506560084, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 00:30:00+00:00", "close_date": "2018-01-16 01:10:00+00:00", "trade_duration": 40, "open_rate": 0.0028685, "close_rate": 0.0028828784461152877, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 34.86142583231654, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 01:15:00+00:00", "close_date": "2018-01-16 02:35:00+00:00", "trade_duration": 80, "open_rate": 0.06731755, "close_rate": 0.0676549813283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4854967241083492, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 07:45:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 55, "open_rate": 0.09217614, "close_rate": 0.09263817578947368, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0848794492804754, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:55:00+00:00", "trade_duration": 20, "open_rate": 0.0165, "close_rate": 0.016913533834586467, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.0606060606060606, "profit_abs": 0.0020000000000000018}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 5, "open_rate": 7.953e-05, "close_rate": 8.311781954887218e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1257.387149503332, "profit_abs": 0.00399999999999999}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 08:45:00+00:00", "close_date": "2018-01-16 09:50:00+00:00", "trade_duration": 65, "open_rate": 0.045202, "close_rate": 0.04542857644110275, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2122914915269236, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:45:00+00:00", "trade_duration": 30, "open_rate": 5.248e-05, "close_rate": 5.326917293233082e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1905.487804878049, "profit_abs": 0.0010000000000000009}, {"pair": "XMR/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:55:00+00:00", "trade_duration": 40, "open_rate": 0.02892318, "close_rate": 0.02906815834586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.457434486802627, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 09:50:00+00:00", "close_date": "2018-01-16 10:10:00+00:00", "trade_duration": 20, "open_rate": 5.158e-05, "close_rate": 5.287273182957392e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1938.735944164405, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:35:00+00:00", "trade_duration": 30, "open_rate": 0.02828232, "close_rate": 0.02870761804511278, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5357778286929786, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:40:00+00:00", "trade_duration": 35, "open_rate": 0.04357584, "close_rate": 0.044231115789473675, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.294849623093898, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 13:45:00+00:00", "close_date": "2018-01-16 14:20:00+00:00", "trade_duration": 35, "open_rate": 5.362e-05, "close_rate": 5.442631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1864.975755315181, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 17:30:00+00:00", "close_date": "2018-01-16 18:25:00+00:00", "trade_duration": 55, "open_rate": 5.302e-05, "close_rate": 5.328576441102756e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.0807242549984, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:45:00+00:00", "trade_duration": 30, "open_rate": 0.09129999, "close_rate": 0.09267292218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0952903718828448, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:35:00+00:00", "trade_duration": 20, "open_rate": 3.808e-05, "close_rate": 3.903438596491228e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2626.0504201680674, "profit_abs": 0.0020000000000000018}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 19:00:00+00:00", "close_date": "2018-01-16 19:30:00+00:00", "trade_duration": 30, "open_rate": 0.02811012, "close_rate": 0.028532828571428567, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.557437677249333, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 60, "open_rate": 0.00258379, "close_rate": 0.002325411, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.702835756775904, "profit_abs": -0.010474999999999984}, {"pair": "NXT/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 80, "open_rate": 2.559e-05, "close_rate": 2.3031e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3907.7764751856193, "profit_abs": -0.010474999999999998}, {"pair": "TRX/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:35:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 50, "open_rate": 7.62e-05, "close_rate": 6.858e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1312.3359580052495, "profit_abs": -0.010474999999999984}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:35:00+00:00", "trade_duration": 5, "open_rate": 0.00229844, "close_rate": 0.002402129022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 43.507770487809125, "profit_abs": 0.004000000000000017}, {"pair": "LTC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:40:00+00:00", "trade_duration": 10, "open_rate": 0.0151, "close_rate": 0.015781203007518795, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.622516556291391, "profit_abs": 0.00399999999999999}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:40:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 5, "open_rate": 0.00235676, "close_rate": 0.00246308, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 42.431134269081284, "profit_abs": 0.0040000000000000036}, {"pair": "DASH/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 22:45:00+00:00", "close_date": "2018-01-16 23:05:00+00:00", "trade_duration": 20, "open_rate": 0.0630692, "close_rate": 0.06464988170426066, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.585559988076589, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:50:00+00:00", "close_date": "2018-01-16 22:55:00+00:00", "trade_duration": 5, "open_rate": 2.2e-05, "close_rate": 2.299248120300751e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 4545.454545454546, "profit_abs": 0.003999999999999976}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-17 03:30:00+00:00", "close_date": "2018-01-17 04:00:00+00:00", "trade_duration": 30, "open_rate": 4.974e-05, "close_rate": 5.048796992481203e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2010.454362685967, "profit_abs": 0.0010000000000000009}, {"pair": "TRX/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-17 03:55:00+00:00", "close_date": "2018-01-17 04:15:00+00:00", "trade_duration": 20, "open_rate": 7.108e-05, "close_rate": 7.28614536340852e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1406.8655036578502, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 09:35:00+00:00", "close_date": "2018-01-17 10:15:00+00:00", "trade_duration": 40, "open_rate": 0.04327, "close_rate": 0.04348689223057644, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.3110700254217704, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:20:00+00:00", "close_date": "2018-01-17 17:00:00+00:00", "trade_duration": 400, "open_rate": 4.997e-05, "close_rate": 5.022047619047618e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2001.2007204322595, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:25:00+00:00", "trade_duration": 55, "open_rate": 0.06836818, "close_rate": 0.06871087764411027, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4626687444363737, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:10:00+00:00", "trade_duration": 40, "open_rate": 3.63e-05, "close_rate": 3.648195488721804e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2754.8209366391184, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:30:00+00:00", "close_date": "2018-01-17 22:05:00+00:00", "trade_duration": 575, "open_rate": 0.0281, "close_rate": 0.02824085213032581, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5587188612099645, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:35:00+00:00", "close_date": "2018-01-17 16:55:00+00:00", "trade_duration": 260, "open_rate": 0.08651001, "close_rate": 0.08694364413533832, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1559355963546878, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 05:00:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 55, "open_rate": 5.633e-05, "close_rate": 5.6612355889724306e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1775.2529735487308, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 05:20:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 35, "open_rate": 0.06988494, "close_rate": 0.07093584135338346, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.430923457900944, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 07:35:00+00:00", "close_date": "2018-01-18 08:15:00+00:00", "trade_duration": 40, "open_rate": 5.545e-05, "close_rate": 5.572794486215538e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1803.4265103697026, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 09:00:00+00:00", "close_date": "2018-01-18 09:40:00+00:00", "trade_duration": 40, "open_rate": 0.01633527, "close_rate": 0.016417151052631574, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.121723118136401, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 16:40:00+00:00", "close_date": "2018-01-18 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.00269734, "close_rate": 0.002710860501253133, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.073561360451414, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-18 18:05:00+00:00", "close_date": "2018-01-18 18:30:00+00:00", "trade_duration": 25, "open_rate": 4.475e-05, "close_rate": 4.587155388471177e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2234.63687150838, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 18:25:00+00:00", "close_date": "2018-01-18 18:55:00+00:00", "trade_duration": 30, "open_rate": 2.79e-05, "close_rate": 2.8319548872180444e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3584.2293906810037, "profit_abs": 0.000999999999999987}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 20:10:00+00:00", "close_date": "2018-01-18 20:50:00+00:00", "trade_duration": 40, "open_rate": 0.04439326, "close_rate": 0.04461578260651629, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2525942001105577, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 21:30:00+00:00", "close_date": "2018-01-19 00:35:00+00:00", "trade_duration": 185, "open_rate": 4.49e-05, "close_rate": 4.51250626566416e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2227.1714922049, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 21:55:00+00:00", "close_date": "2018-01-19 05:05:00+00:00", "trade_duration": 430, "open_rate": 0.02855, "close_rate": 0.028693107769423555, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.502626970227671, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 22:10:00+00:00", "close_date": "2018-01-18 22:50:00+00:00", "trade_duration": 40, "open_rate": 5.796e-05, "close_rate": 5.8250526315789473e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1725.3278122843342, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 23:50:00+00:00", "close_date": "2018-01-19 00:30:00+00:00", "trade_duration": 40, "open_rate": 0.04340323, "close_rate": 0.04362079005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.303975994413319, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-19 16:45:00+00:00", "close_date": "2018-01-19 17:35:00+00:00", "trade_duration": 50, "open_rate": 0.04454455, "close_rate": 0.04476783095238095, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.244943545282195, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:15:00+00:00", "close_date": "2018-01-19 19:55:00+00:00", "trade_duration": 160, "open_rate": 5.62e-05, "close_rate": 5.648170426065162e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1779.3594306049824, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:20:00+00:00", "close_date": "2018-01-19 20:15:00+00:00", "trade_duration": 175, "open_rate": 4.339e-05, "close_rate": 4.360749373433584e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2304.6784973496196, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-20 04:45:00+00:00", "close_date": "2018-01-20 17:35:00+00:00", "trade_duration": 770, "open_rate": 0.0001009, "close_rate": 0.00010140576441102755, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 991.0802775024778, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 15:15:00+00:00", "trade_duration": 625, "open_rate": 0.00270505, "close_rate": 0.002718609147869674, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.96789338459548, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 07:00:00+00:00", "trade_duration": 130, "open_rate": 0.03000002, "close_rate": 0.030150396040100245, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.3333311111125927, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 09:00:00+00:00", "close_date": "2018-01-20 09:40:00+00:00", "trade_duration": 40, "open_rate": 5.46e-05, "close_rate": 5.4873684210526304e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1831.5018315018317, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-20 18:25:00+00:00", "close_date": "2018-01-25 03:50:00+00:00", "trade_duration": 6325, "open_rate": 0.03082222, "close_rate": 0.027739998, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.244412634781012, "profit_abs": -0.010474999999999998}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 22:25:00+00:00", "close_date": "2018-01-20 23:15:00+00:00", "trade_duration": 50, "open_rate": 0.08969999, "close_rate": 0.09014961401002504, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1148273260677064, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 02:50:00+00:00", "close_date": "2018-01-21 14:30:00+00:00", "trade_duration": 700, "open_rate": 0.01632501, "close_rate": 0.01640683962406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.125570520324337, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 10:20:00+00:00", "close_date": "2018-01-21 11:00:00+00:00", "trade_duration": 40, "open_rate": 0.070538, "close_rate": 0.07089157393483708, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.417675579120474, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 15:50:00+00:00", "close_date": "2018-01-21 18:45:00+00:00", "trade_duration": 175, "open_rate": 5.301e-05, "close_rate": 5.327571428571427e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.4365214110546, "profit_abs": -2.7755575615628914e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 16:20:00+00:00", "close_date": "2018-01-21 17:00:00+00:00", "trade_duration": 40, "open_rate": 3.955e-05, "close_rate": 3.9748245614035085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2528.4450063211125, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:45:00+00:00", "trade_duration": 30, "open_rate": 0.00258505, "close_rate": 0.002623922932330827, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.6839712964933, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:55:00+00:00", "trade_duration": 40, "open_rate": 3.903e-05, "close_rate": 3.922563909774435e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2562.1316935690497, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 00:35:00+00:00", "close_date": "2018-01-22 10:35:00+00:00", "trade_duration": 600, "open_rate": 5.236e-05, "close_rate": 5.262245614035087e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1909.8548510313217, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 01:30:00+00:00", "close_date": "2018-01-22 02:10:00+00:00", "trade_duration": 40, "open_rate": 9.028e-05, "close_rate": 9.07325313283208e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1107.6650420912717, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 12:25:00+00:00", "close_date": "2018-01-22 14:35:00+00:00", "trade_duration": 130, "open_rate": 0.002687, "close_rate": 0.002700468671679198, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.21622627465575, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 13:15:00+00:00", "close_date": "2018-01-22 13:55:00+00:00", "trade_duration": 40, "open_rate": 4.168e-05, "close_rate": 4.188892230576441e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2399.232245681382, "profit_abs": 1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-22 14:00:00+00:00", "close_date": "2018-01-22 14:30:00+00:00", "trade_duration": 30, "open_rate": 8.821e-05, "close_rate": 8.953646616541353e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1133.6583153837435, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 15:55:00+00:00", "close_date": "2018-01-22 16:40:00+00:00", "trade_duration": 45, "open_rate": 5.172e-05, "close_rate": 5.1979248120300745e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1933.4880123743235, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-22 16:05:00+00:00", "close_date": "2018-01-22 16:25:00+00:00", "trade_duration": 20, "open_rate": 3.026e-05, "close_rate": 3.101839598997494e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3304.692663582287, "profit_abs": 0.0020000000000000157}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 19:50:00+00:00", "close_date": "2018-01-23 00:10:00+00:00", "trade_duration": 260, "open_rate": 0.07064, "close_rate": 0.07099408521303258, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.415628539071348, "profit_abs": 1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 21:25:00+00:00", "close_date": "2018-01-22 22:05:00+00:00", "trade_duration": 40, "open_rate": 0.01644483, "close_rate": 0.01652726022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.080938507725528, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-23 00:05:00+00:00", "close_date": "2018-01-23 00:35:00+00:00", "trade_duration": 30, "open_rate": 4.331e-05, "close_rate": 4.3961278195488714e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2308.935580697299, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-23 01:50:00+00:00", "close_date": "2018-01-23 02:15:00+00:00", "trade_duration": 25, "open_rate": 3.2e-05, "close_rate": 3.2802005012531326e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3125.0000000000005, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 04:25:00+00:00", "close_date": "2018-01-23 05:15:00+00:00", "trade_duration": 50, "open_rate": 0.09167706, "close_rate": 0.09213659413533835, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0907854156754153, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 07:35:00+00:00", "close_date": "2018-01-23 09:00:00+00:00", "trade_duration": 85, "open_rate": 0.0692498, "close_rate": 0.06959691679197995, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4440474918339115, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 10:50:00+00:00", "close_date": "2018-01-23 13:05:00+00:00", "trade_duration": 135, "open_rate": 3.182e-05, "close_rate": 3.197949874686716e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3142.677561282213, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 11:05:00+00:00", "close_date": "2018-01-23 16:05:00+00:00", "trade_duration": 300, "open_rate": 0.04088, "close_rate": 0.04108491228070175, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4461839530332683, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 14:55:00+00:00", "close_date": "2018-01-23 15:35:00+00:00", "trade_duration": 40, "open_rate": 5.15e-05, "close_rate": 5.175814536340851e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1941.747572815534, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 16:35:00+00:00", "close_date": "2018-01-24 00:05:00+00:00", "trade_duration": 450, "open_rate": 0.09071698, "close_rate": 0.09117170170426064, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1023294646713329, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 17:25:00+00:00", "close_date": "2018-01-23 18:45:00+00:00", "trade_duration": 80, "open_rate": 3.128e-05, "close_rate": 3.1436791979949865e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3196.9309462915603, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 20:15:00+00:00", "close_date": "2018-01-23 22:00:00+00:00", "trade_duration": 105, "open_rate": 9.555e-05, "close_rate": 9.602894736842104e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1046.5724751439038, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 22:30:00+00:00", "close_date": "2018-01-23 23:10:00+00:00", "trade_duration": 40, "open_rate": 0.04080001, "close_rate": 0.0410045213283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.450979791426522, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 23:50:00+00:00", "close_date": "2018-01-24 03:35:00+00:00", "trade_duration": 225, "open_rate": 5.163e-05, "close_rate": 5.18887969924812e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1936.8584156498162, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 00:20:00+00:00", "close_date": "2018-01-24 01:50:00+00:00", "trade_duration": 90, "open_rate": 0.04040781, "close_rate": 0.04061035541353383, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.474769110228938, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 06:45:00+00:00", "close_date": "2018-01-24 07:25:00+00:00", "trade_duration": 40, "open_rate": 5.132e-05, "close_rate": 5.157724310776942e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1948.5580670303975, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-24 14:15:00+00:00", "close_date": "2018-01-24 14:25:00+00:00", "trade_duration": 10, "open_rate": 5.198e-05, "close_rate": 5.432496240601503e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1923.8168526356292, "profit_abs": 0.0040000000000000036}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 14:50:00+00:00", "close_date": "2018-01-24 16:35:00+00:00", "trade_duration": 105, "open_rate": 3.054e-05, "close_rate": 3.069308270676692e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3274.3942370661425, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 15:10:00+00:00", "close_date": "2018-01-24 16:15:00+00:00", "trade_duration": 65, "open_rate": 9.263e-05, "close_rate": 9.309431077694236e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1079.5638562020945, "profit_abs": 2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 22:40:00+00:00", "close_date": "2018-01-24 23:25:00+00:00", "trade_duration": 45, "open_rate": 5.514e-05, "close_rate": 5.54163909774436e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1813.5654697134569, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 00:50:00+00:00", "close_date": "2018-01-25 01:30:00+00:00", "trade_duration": 40, "open_rate": 4.921e-05, "close_rate": 4.9456666666666664e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.1072952651903, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 08:15:00+00:00", "close_date": "2018-01-25 12:15:00+00:00", "trade_duration": 240, "open_rate": 0.0026, "close_rate": 0.002613032581453634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.46153846153847, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 10:25:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 350, "open_rate": 0.02799871, "close_rate": 0.028139054411027563, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.571593119825878, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 11:00:00+00:00", "close_date": "2018-01-25 11:45:00+00:00", "trade_duration": 45, "open_rate": 0.04078902, "close_rate": 0.0409934762406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4516401717913303, "profit_abs": -1.3877787807814457e-17}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:05:00+00:00", "close_date": "2018-01-25 13:45:00+00:00", "trade_duration": 40, "open_rate": 2.89e-05, "close_rate": 2.904486215538847e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3460.2076124567475, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:20:00+00:00", "close_date": "2018-01-25 14:05:00+00:00", "trade_duration": 45, "open_rate": 0.041103, "close_rate": 0.04130903007518797, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4329124394813033, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-25 15:45:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 30, "open_rate": 5.428e-05, "close_rate": 5.509624060150376e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1842.2991893883568, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 17:45:00+00:00", "close_date": "2018-01-25 23:15:00+00:00", "trade_duration": 330, "open_rate": 5.414e-05, "close_rate": 5.441137844611528e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1847.063169560399, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 21:15:00+00:00", "close_date": "2018-01-25 21:55:00+00:00", "trade_duration": 40, "open_rate": 0.04140777, "close_rate": 0.0416153277443609, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.415005686130888, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 02:05:00+00:00", "close_date": "2018-01-26 02:45:00+00:00", "trade_duration": 40, "open_rate": 0.00254309, "close_rate": 0.002555837318295739, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.32224183965177, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 02:55:00+00:00", "close_date": "2018-01-26 15:10:00+00:00", "trade_duration": 735, "open_rate": 5.607e-05, "close_rate": 5.6351052631578935e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1783.4849295523454, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 06:10:00+00:00", "close_date": "2018-01-26 09:25:00+00:00", "trade_duration": 195, "open_rate": 0.00253806, "close_rate": 0.0025507821052631577, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.400171784748984, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 07:25:00+00:00", "close_date": "2018-01-26 09:55:00+00:00", "trade_duration": 150, "open_rate": 0.0415, "close_rate": 0.04170802005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4096385542168677, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-26 09:55:00+00:00", "close_date": "2018-01-26 10:25:00+00:00", "trade_duration": 30, "open_rate": 5.321e-05, "close_rate": 5.401015037593984e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1879.3459875963165, "profit_abs": 0.000999999999999987}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 16:05:00+00:00", "close_date": "2018-01-26 16:45:00+00:00", "trade_duration": 40, "open_rate": 0.02772046, "close_rate": 0.02785940967418546, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.6074437437185387, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 23:35:00+00:00", "close_date": "2018-01-27 00:15:00+00:00", "trade_duration": 40, "open_rate": 0.09461341, "close_rate": 0.09508766268170424, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0569326272036914, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 00:35:00+00:00", "close_date": "2018-01-27 01:30:00+00:00", "trade_duration": 55, "open_rate": 5.615e-05, "close_rate": 5.643145363408521e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1780.9439002671415, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.07877175, "open_date": "2018-01-27 00:45:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 4560, "open_rate": 5.556e-05, "close_rate": 5.144e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1799.8560115190785, "profit_abs": -0.007896868250539965}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 02:30:00+00:00", "close_date": "2018-01-27 11:25:00+00:00", "trade_duration": 535, "open_rate": 0.06900001, "close_rate": 0.06934587471177944, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492751522789635, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 06:25:00+00:00", "close_date": "2018-01-27 07:05:00+00:00", "trade_duration": 40, "open_rate": 0.09449985, "close_rate": 0.0949735334586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.058202737887944, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.04815133, "open_date": "2018-01-27 09:40:00+00:00", "close_date": "2018-01-30 04:40:00+00:00", "trade_duration": 4020, "open_rate": 0.0410697, "close_rate": 0.03928809, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4348850855983852, "profit_abs": -0.004827170578309559}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 11:45:00+00:00", "close_date": "2018-01-27 12:30:00+00:00", "trade_duration": 45, "open_rate": 0.0285, "close_rate": 0.02864285714285714, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5087719298245617, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 12:35:00+00:00", "close_date": "2018-01-27 15:25:00+00:00", "trade_duration": 170, "open_rate": 0.02866372, "close_rate": 0.02880739779448621, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.4887307020861216, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 15:50:00+00:00", "close_date": "2018-01-27 16:50:00+00:00", "trade_duration": 60, "open_rate": 0.095381, "close_rate": 0.09585910025062656, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0484268355332824, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 17:05:00+00:00", "close_date": "2018-01-27 17:45:00+00:00", "trade_duration": 40, "open_rate": 0.06759092, "close_rate": 0.06792972160401002, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4794886650455417, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 23:40:00+00:00", "close_date": "2018-01-28 01:05:00+00:00", "trade_duration": 85, "open_rate": 0.00258501, "close_rate": 0.002597967443609022, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.684569885609726, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 02:25:00+00:00", "close_date": "2018-01-28 08:10:00+00:00", "trade_duration": 345, "open_rate": 0.06698502, "close_rate": 0.0673207845112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4928710926711672, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 10:25:00+00:00", "close_date": "2018-01-28 16:30:00+00:00", "trade_duration": 365, "open_rate": 0.0677177, "close_rate": 0.06805713709273183, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4767187899175547, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-28 20:35:00+00:00", "close_date": "2018-01-28 21:35:00+00:00", "trade_duration": 60, "open_rate": 5.215e-05, "close_rate": 5.2411403508771925e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1917.5455417066157, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-28 22:00:00+00:00", "close_date": "2018-01-28 22:30:00+00:00", "trade_duration": 30, "open_rate": 0.00273809, "close_rate": 0.002779264285714285, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.5218089982433, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-29 00:00:00+00:00", "close_date": "2018-01-29 00:30:00+00:00", "trade_duration": 30, "open_rate": 0.00274632, "close_rate": 0.002787618045112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.412362725392526, "profit_abs": 0.0010000000000000148}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-29 02:15:00+00:00", "close_date": "2018-01-29 03:00:00+00:00", "trade_duration": 45, "open_rate": 0.01622478, "close_rate": 0.016306107218045113, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.163411768911504, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 03:05:00+00:00", "close_date": "2018-01-29 03:45:00+00:00", "trade_duration": 40, "open_rate": 0.069, "close_rate": 0.06934586466165413, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492753623188406, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 05:20:00+00:00", "close_date": "2018-01-29 06:55:00+00:00", "trade_duration": 95, "open_rate": 8.755e-05, "close_rate": 8.798884711779448e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1142.204454597373, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 07:00:00+00:00", "close_date": "2018-01-29 19:25:00+00:00", "trade_duration": 745, "open_rate": 0.06825763, "close_rate": 0.06859977350877192, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4650376815016872, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 19:45:00+00:00", "close_date": "2018-01-29 20:25:00+00:00", "trade_duration": 40, "open_rate": 0.06713892, "close_rate": 0.06747545593984962, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4894490408841845, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0199116, "open_date": "2018-01-29 23:30:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 315, "open_rate": 8.934e-05, "close_rate": 8.8e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1119.3194537721067, "profit_abs": -0.0019961383478844796}], "results_per_pair": [{"key": "TRX/BTC", "trades": 15, "profit_mean": 0.0023467073333333323, "profit_mean_pct": 0.23467073333333321, "profit_sum": 0.035200609999999986, "profit_sum_pct": 3.5200609999999988, "profit_total_abs": 0.0035288616521155086, "profit_total_pct": 1.1733536666666662, "duration_avg": "2:28:00", "wins": 9, "draws": 2, "losses": 4}, {"key": "ADA/BTC", "trades": 29, "profit_mean": -0.0011598141379310352, "profit_mean_pct": -0.11598141379310352, "profit_sum": -0.03363461000000002, "profit_sum_pct": -3.3634610000000023, "profit_total_abs": -0.0033718682505400333, "profit_total_pct": -1.1211536666666675, "duration_avg": "5:35:00", "wins": 9, "draws": 11, "losses": 9}, {"key": "XLM/BTC", "trades": 21, "profit_mean": 0.0026243899999999994, "profit_mean_pct": 0.2624389999999999, "profit_sum": 0.05511218999999999, "profit_sum_pct": 5.511218999999999, "profit_total_abs": 0.005525000000000002, "profit_total_pct": 1.8370729999999995, "duration_avg": "3:21:00", "wins": 12, "draws": 3, "losses": 6}, {"key": "ETH/BTC", "trades": 21, "profit_mean": 0.0009500057142857142, "profit_mean_pct": 0.09500057142857142, "profit_sum": 0.01995012, "profit_sum_pct": 1.9950119999999998, "profit_total_abs": 0.0019999999999999463, "profit_total_pct": 0.6650039999999999, "duration_avg": "2:17:00", "wins": 5, "draws": 10, "losses": 6}, {"key": "XMR/BTC", "trades": 16, "profit_mean": -0.0027899012500000007, "profit_mean_pct": -0.2789901250000001, "profit_sum": -0.04463842000000001, "profit_sum_pct": -4.463842000000001, "profit_total_abs": -0.0044750000000000345, "profit_total_pct": -1.4879473333333337, "duration_avg": "8:41:00", "wins": 6, "draws": 5, "losses": 5}, {"key": "ZEC/BTC", "trades": 21, "profit_mean": -0.00039290904761904774, "profit_mean_pct": -0.03929090476190478, "profit_sum": -0.008251090000000003, "profit_sum_pct": -0.8251090000000003, "profit_total_abs": -0.000827170578309569, "profit_total_pct": -0.27503633333333344, "duration_avg": "4:17:00", "wins": 8, "draws": 7, "losses": 6}, {"key": "NXT/BTC", "trades": 12, "profit_mean": -0.0012261025000000006, "profit_mean_pct": -0.12261025000000006, "profit_sum": -0.014713230000000008, "profit_sum_pct": -1.4713230000000008, "profit_total_abs": -0.0014750000000000874, "profit_total_pct": -0.4904410000000003, "duration_avg": "0:57:00", "wins": 4, "draws": 3, "losses": 5}, {"key": "LTC/BTC", "trades": 8, "profit_mean": 0.00748129625, "profit_mean_pct": 0.748129625, "profit_sum": 0.05985037, "profit_sum_pct": 5.985037, "profit_total_abs": 0.006000000000000019, "profit_total_pct": 1.9950123333333334, "duration_avg": "1:59:00", "wins": 5, "draws": 2, "losses": 1}, {"key": "ETC/BTC", "trades": 20, "profit_mean": 0.0022568569999999997, "profit_mean_pct": 0.22568569999999996, "profit_sum": 0.04513713999999999, "profit_sum_pct": 4.513713999999999, "profit_total_abs": 0.004525000000000001, "profit_total_pct": 1.504571333333333, "duration_avg": "1:45:00", "wins": 11, "draws": 4, "losses": 5}, {"key": "DASH/BTC", "trades": 16, "profit_mean": 0.0018703237499999997, "profit_mean_pct": 0.18703237499999997, "profit_sum": 0.029925179999999996, "profit_sum_pct": 2.9925179999999996, "profit_total_abs": 0.002999999999999961, "profit_total_pct": 0.9975059999999999, "duration_avg": "3:03:00", "wins": 4, "draws": 7, "losses": 5}, {"key": "TOTAL", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}], "sell_reason_summary": [{"sell_reason": "roi", "trades": 170, "wins": 73, "draws": 54, "losses": 43, "profit_mean": 0.005398268352941177, "profit_mean_pct": 0.54, "profit_sum": 0.91770562, "profit_sum_pct": 91.77, "profit_total_abs": 0.09199999999999964, "profit_pct_total": 30.59}, {"sell_reason": "stop_loss", "trades": 6, "wins": 0, "draws": 0, "losses": 6, "profit_mean": -0.10448878000000002, "profit_mean_pct": -10.45, "profit_sum": -0.6269326800000001, "profit_sum_pct": -62.69, "profit_total_abs": -0.06284999999999992, "profit_pct_total": -20.9}, {"sell_reason": "force_sell", "trades": 3, "wins": 0, "draws": 0, "losses": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.89, "profit_sum": -0.14683468, "profit_sum_pct": -14.68, "profit_total_abs": -0.014720177176734003, "profit_pct_total": -4.89}], "left_open_trades": [{"key": "TRX/BTC", "trades": 1, "profit_mean": -0.0199116, "profit_mean_pct": -1.9911600000000003, "profit_sum": -0.0199116, "profit_sum_pct": -1.9911600000000003, "profit_total_abs": -0.0019961383478844796, "profit_total_pct": -0.6637200000000001, "duration_avg": "5:15:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ADA/BTC", "trades": 1, "profit_mean": -0.07877175, "profit_mean_pct": -7.877175, "profit_sum": -0.07877175, "profit_sum_pct": -7.877175, "profit_total_abs": -0.007896868250539965, "profit_total_pct": -2.625725, "duration_avg": "3 days, 4:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ZEC/BTC", "trades": 1, "profit_mean": -0.04815133, "profit_mean_pct": -4.815133, "profit_sum": -0.04815133, "profit_sum_pct": -4.815133, "profit_total_abs": -0.004827170578309559, "profit_total_pct": -1.6050443333333335, "duration_avg": "2 days, 19:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "TOTAL", "trades": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.894489333333333, "profit_sum": -0.14683468, "profit_sum_pct": -14.683468, "profit_total_abs": -0.014720177176734003, "profit_total_pct": -4.8944893333333335, "duration_avg": "2 days, 1:25:00", "wins": 0, "draws": 0, "losses": 3}], "total_trades": 179, "backtest_start": "2018-01-30 04:45:00+00:00", "backtest_start_ts": 1517287500, "backtest_end": "2018-01-30 04:45:00+00:00", "backtest_end_ts": 1517287500, "backtest_days": 0, "trades_per_day": null, "market_change": 0.25, "stake_amount": 0.1, "max_drawdown": 0.21142322000000008, "drawdown_start": "2018-01-24 14:25:00+00:00", "drawdown_start_ts": 1516803900.0, "drawdown_end": "2018-01-30 04:45:00+00:00", "drawdown_end_ts": 1517287500.0, "pairlist": ["TRX/BTC", "ADA/BTC", "XLM/BTC", "ETH/BTC", "XMR/BTC", "ZEC/BTC","NXT/BTC", "LTC/BTC", "ETC/BTC", "DASH/BTC"]}, "TestStrategy": {"trades": [{"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:20:00+00:00", "trade_duration": 5, "open_rate": 9.64e-05, "close_rate": 0.00010074887218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1037.344398340249, "profit_abs": 0.00399999999999999}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:30:00+00:00", "trade_duration": 15, "open_rate": 4.756e-05, "close_rate": 4.9705563909774425e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2102.6072329688814, "profit_abs": 0.00399999999999999}, {"pair": "XLM/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:35:00+00:00", "trade_duration": 10, "open_rate": 3.339e-05, "close_rate": 3.489631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2994.908655286014, "profit_abs": 0.0040000000000000036}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:40:00+00:00", "trade_duration": 15, "open_rate": 9.696e-05, "close_rate": 0.00010133413533834584, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1031.3531353135315, "profit_abs": 0.00399999999999999}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 07:35:00+00:00", "close_date": "2018-01-10 08:35:00+00:00", "trade_duration": 60, "open_rate": 0.0943, "close_rate": 0.09477268170426063, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0604453870625663, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 07:40:00+00:00", "close_date": "2018-01-10 08:10:00+00:00", "trade_duration": 30, "open_rate": 0.02719607, "close_rate": 0.02760503345864661, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.677001860930642, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 08:15:00+00:00", "close_date": "2018-01-10 09:55:00+00:00", "trade_duration": 100, "open_rate": 0.04634952, "close_rate": 0.046581848421052625, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1575196463739, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 14:45:00+00:00", "close_date": "2018-01-10 15:50:00+00:00", "trade_duration": 65, "open_rate": 3.066e-05, "close_rate": 3.081368421052631e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3261.5786040443577, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 16:35:00+00:00", "close_date": "2018-01-10 17:15:00+00:00", "trade_duration": 40, "open_rate": 0.0168999, "close_rate": 0.016984611278195488, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.917194776300452, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 16:40:00+00:00", "close_date": "2018-01-10 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.09132568, "close_rate": 0.0917834528320802, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0949822656672252, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 18:50:00+00:00", "close_date": "2018-01-10 19:45:00+00:00", "trade_duration": 55, "open_rate": 0.08898003, "close_rate": 0.08942604518796991, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1238476768326557, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 22:15:00+00:00", "close_date": "2018-01-10 23:00:00+00:00", "trade_duration": 45, "open_rate": 0.08560008, "close_rate": 0.08602915308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1682232072680307, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 22:50:00+00:00", "close_date": "2018-01-10 23:20:00+00:00", "trade_duration": 30, "open_rate": 0.00249083, "close_rate": 0.0025282860902255634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 40.147260150231055, "profit_abs": 0.000999999999999987}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 23:15:00+00:00", "close_date": "2018-01-11 00:15:00+00:00", "trade_duration": 60, "open_rate": 3.022e-05, "close_rate": 3.037147869674185e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3309.0668431502318, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-10 23:40:00+00:00", "close_date": "2018-01-11 00:05:00+00:00", "trade_duration": 25, "open_rate": 0.002437, "close_rate": 0.0024980776942355883, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.03405826836274, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 00:00:00+00:00", "close_date": "2018-01-11 00:35:00+00:00", "trade_duration": 35, "open_rate": 0.04771803, "close_rate": 0.04843559436090225, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0956439316543456, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-11 03:40:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 45, "open_rate": 3.651e-05, "close_rate": 3.2859000000000005e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2738.9756231169545, "profit_abs": -0.01047499999999997}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 03:55:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 30, "open_rate": 0.08824105, "close_rate": 0.08956798308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1332594070446804, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 04:00:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 50, "open_rate": 0.00243, "close_rate": 0.002442180451127819, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.1522633744856, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:55:00+00:00", "trade_duration": 25, "open_rate": 0.04545064, "close_rate": 0.046589753784461146, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.200189040242338, "profit_abs": 0.001999999999999988}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 20, "open_rate": 3.372e-05, "close_rate": 3.456511278195488e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2965.599051008304, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:55:00+00:00", "close_date": "2018-01-11 05:15:00+00:00", "trade_duration": 20, "open_rate": 0.02644, "close_rate": 0.02710265664160401, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7821482602118004, "profit_abs": 0.001999999999999988}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:20:00+00:00", "close_date": "2018-01-11 12:00:00+00:00", "trade_duration": 40, "open_rate": 0.08812, "close_rate": 0.08856170426065162, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1348161597821154, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:35:00+00:00", "close_date": "2018-01-11 12:15:00+00:00", "trade_duration": 40, "open_rate": 0.02683577, "close_rate": 0.026970285137844607, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7263696923919087, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 14:00:00+00:00", "close_date": "2018-01-11 14:25:00+00:00", "trade_duration": 25, "open_rate": 4.919e-05, "close_rate": 5.04228320802005e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.9335230737956, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 19:25:00+00:00", "close_date": "2018-01-11 20:35:00+00:00", "trade_duration": 70, "open_rate": 0.08784896, "close_rate": 0.08828930566416039, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1383174029607181, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:35:00+00:00", "close_date": "2018-01-11 23:30:00+00:00", "trade_duration": 55, "open_rate": 5.105e-05, "close_rate": 5.130588972431077e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1958.8638589618022, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:25:00+00:00", "trade_duration": 30, "open_rate": 3.96e-05, "close_rate": 4.019548872180451e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2525.252525252525, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:35:00+00:00", "trade_duration": 40, "open_rate": 2.885e-05, "close_rate": 2.899461152882205e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3466.204506065858, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 23:30:00+00:00", "close_date": "2018-01-12 00:05:00+00:00", "trade_duration": 35, "open_rate": 0.02645, "close_rate": 0.026847744360902256, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.780718336483932, "profit_abs": 0.0010000000000000148}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 23:55:00+00:00", "close_date": "2018-01-12 01:15:00+00:00", "trade_duration": 80, "open_rate": 0.048, "close_rate": 0.04824060150375939, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0833333333333335, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-12 21:15:00+00:00", "close_date": "2018-01-12 21:40:00+00:00", "trade_duration": 25, "open_rate": 4.692e-05, "close_rate": 4.809593984962405e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2131.287297527707, "profit_abs": 0.001999999999999974}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 00:55:00+00:00", "close_date": "2018-01-13 06:20:00+00:00", "trade_duration": 325, "open_rate": 0.00256966, "close_rate": 0.0025825405012531327, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.91565421106294, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 10:55:00+00:00", "close_date": "2018-01-13 11:35:00+00:00", "trade_duration": 40, "open_rate": 6.262e-05, "close_rate": 6.293388471177944e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1596.933886937081, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 13:05:00+00:00", "close_date": "2018-01-15 14:10:00+00:00", "trade_duration": 2945, "open_rate": 4.73e-05, "close_rate": 4.753709273182957e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2114.1649048625795, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:30:00+00:00", "close_date": "2018-01-13 14:45:00+00:00", "trade_duration": 75, "open_rate": 6.063e-05, "close_rate": 6.0933909774436085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1649.348507339601, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:40:00+00:00", "close_date": "2018-01-13 23:30:00+00:00", "trade_duration": 590, "open_rate": 0.00011082, "close_rate": 0.00011137548872180448, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 902.3641941887746, "profit_abs": -2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 15:15:00+00:00", "close_date": "2018-01-13 15:55:00+00:00", "trade_duration": 40, "open_rate": 5.93e-05, "close_rate": 5.9597243107769415e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1686.3406408094436, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 16:30:00+00:00", "close_date": "2018-01-13 17:10:00+00:00", "trade_duration": 40, "open_rate": 0.04850003, "close_rate": 0.04874313791979949, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0618543947292407, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 22:05:00+00:00", "close_date": "2018-01-14 06:25:00+00:00", "trade_duration": 500, "open_rate": 0.09825019, "close_rate": 0.09874267215538848, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0178097365511456, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 00:20:00+00:00", "close_date": "2018-01-14 22:55:00+00:00", "trade_duration": 1355, "open_rate": 6.018e-05, "close_rate": 6.048165413533834e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1661.681621801263, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 12:45:00+00:00", "close_date": "2018-01-14 13:25:00+00:00", "trade_duration": 40, "open_rate": 0.09758999, "close_rate": 0.0980791628822055, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.024695258191952, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-14 15:30:00+00:00", "close_date": "2018-01-14 16:00:00+00:00", "trade_duration": 30, "open_rate": 0.00311, "close_rate": 0.0031567669172932328, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.154340836012864, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 20:45:00+00:00", "close_date": "2018-01-14 22:15:00+00:00", "trade_duration": 90, "open_rate": 0.00312401, "close_rate": 0.003139669197994987, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.010140812609436, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 23:35:00+00:00", "close_date": "2018-01-15 00:30:00+00:00", "trade_duration": 55, "open_rate": 0.0174679, "close_rate": 0.017555458395989976, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.724786608579165, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 23:45:00+00:00", "close_date": "2018-01-15 00:25:00+00:00", "trade_duration": 40, "open_rate": 0.07346846, "close_rate": 0.07383672295739348, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.3611282991367997, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 02:25:00+00:00", "close_date": "2018-01-15 03:05:00+00:00", "trade_duration": 40, "open_rate": 0.097994, "close_rate": 0.09848519799498744, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.020470641059657, "profit_abs": -2.7755575615628914e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 07:20:00+00:00", "close_date": "2018-01-15 08:00:00+00:00", "trade_duration": 40, "open_rate": 0.09659, "close_rate": 0.09707416040100247, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0353038616834043, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-15 08:20:00+00:00", "close_date": "2018-01-15 08:55:00+00:00", "trade_duration": 35, "open_rate": 9.987e-05, "close_rate": 0.00010137180451127818, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1001.3016921998599, "profit_abs": 0.0010000000000000009}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-15 12:10:00+00:00", "close_date": "2018-01-16 02:50:00+00:00", "trade_duration": 880, "open_rate": 0.0948969, "close_rate": 0.09537257368421052, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0537752023511833, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:10:00+00:00", "close_date": "2018-01-15 17:40:00+00:00", "trade_duration": 210, "open_rate": 0.071, "close_rate": 0.07135588972431077, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4084507042253522, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:30:00+00:00", "close_date": "2018-01-15 15:10:00+00:00", "trade_duration": 40, "open_rate": 0.04600501, "close_rate": 0.046235611553884705, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.173676301776698, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:10:00+00:00", "close_date": "2018-01-15 19:25:00+00:00", "trade_duration": 75, "open_rate": 9.438e-05, "close_rate": 9.485308270676693e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1059.5465140919687, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:35:00+00:00", "close_date": "2018-01-15 19:15:00+00:00", "trade_duration": 40, "open_rate": 0.03040001, "close_rate": 0.030552391002506264, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.2894726021471703, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-15 20:25:00+00:00", "close_date": "2018-01-16 08:25:00+00:00", "trade_duration": 720, "open_rate": 5.837e-05, "close_rate": 5.2533e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1713.2088401576154, "profit_abs": -0.010474999999999984}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 20:40:00+00:00", "close_date": "2018-01-15 22:00:00+00:00", "trade_duration": 80, "open_rate": 0.046036, "close_rate": 0.04626675689223057, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1722130506560084, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 00:30:00+00:00", "close_date": "2018-01-16 01:10:00+00:00", "trade_duration": 40, "open_rate": 0.0028685, "close_rate": 0.0028828784461152877, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 34.86142583231654, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 01:15:00+00:00", "close_date": "2018-01-16 02:35:00+00:00", "trade_duration": 80, "open_rate": 0.06731755, "close_rate": 0.0676549813283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4854967241083492, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 07:45:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 55, "open_rate": 0.09217614, "close_rate": 0.09263817578947368, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0848794492804754, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:55:00+00:00", "trade_duration": 20, "open_rate": 0.0165, "close_rate": 0.016913533834586467, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.0606060606060606, "profit_abs": 0.0020000000000000018}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 5, "open_rate": 7.953e-05, "close_rate": 8.311781954887218e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1257.387149503332, "profit_abs": 0.00399999999999999}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 08:45:00+00:00", "close_date": "2018-01-16 09:50:00+00:00", "trade_duration": 65, "open_rate": 0.045202, "close_rate": 0.04542857644110275, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2122914915269236, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:45:00+00:00", "trade_duration": 30, "open_rate": 5.248e-05, "close_rate": 5.326917293233082e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1905.487804878049, "profit_abs": 0.0010000000000000009}, {"pair": "XMR/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:55:00+00:00", "trade_duration": 40, "open_rate": 0.02892318, "close_rate": 0.02906815834586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.457434486802627, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 09:50:00+00:00", "close_date": "2018-01-16 10:10:00+00:00", "trade_duration": 20, "open_rate": 5.158e-05, "close_rate": 5.287273182957392e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1938.735944164405, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:35:00+00:00", "trade_duration": 30, "open_rate": 0.02828232, "close_rate": 0.02870761804511278, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5357778286929786, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:40:00+00:00", "trade_duration": 35, "open_rate": 0.04357584, "close_rate": 0.044231115789473675, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.294849623093898, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 13:45:00+00:00", "close_date": "2018-01-16 14:20:00+00:00", "trade_duration": 35, "open_rate": 5.362e-05, "close_rate": 5.442631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1864.975755315181, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 17:30:00+00:00", "close_date": "2018-01-16 18:25:00+00:00", "trade_duration": 55, "open_rate": 5.302e-05, "close_rate": 5.328576441102756e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.0807242549984, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:45:00+00:00", "trade_duration": 30, "open_rate": 0.09129999, "close_rate": 0.09267292218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0952903718828448, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:35:00+00:00", "trade_duration": 20, "open_rate": 3.808e-05, "close_rate": 3.903438596491228e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2626.0504201680674, "profit_abs": 0.0020000000000000018}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 19:00:00+00:00", "close_date": "2018-01-16 19:30:00+00:00", "trade_duration": 30, "open_rate": 0.02811012, "close_rate": 0.028532828571428567, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.557437677249333, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 60, "open_rate": 0.00258379, "close_rate": 0.002325411, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.702835756775904, "profit_abs": -0.010474999999999984}, {"pair": "NXT/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 80, "open_rate": 2.559e-05, "close_rate": 2.3031e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3907.7764751856193, "profit_abs": -0.010474999999999998}, {"pair": "TRX/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:35:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 50, "open_rate": 7.62e-05, "close_rate": 6.858e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1312.3359580052495, "profit_abs": -0.010474999999999984}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:35:00+00:00", "trade_duration": 5, "open_rate": 0.00229844, "close_rate": 0.002402129022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 43.507770487809125, "profit_abs": 0.004000000000000017}, {"pair": "LTC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:40:00+00:00", "trade_duration": 10, "open_rate": 0.0151, "close_rate": 0.015781203007518795, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.622516556291391, "profit_abs": 0.00399999999999999}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:40:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 5, "open_rate": 0.00235676, "close_rate": 0.00246308, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 42.431134269081284, "profit_abs": 0.0040000000000000036}, {"pair": "DASH/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 22:45:00+00:00", "close_date": "2018-01-16 23:05:00+00:00", "trade_duration": 20, "open_rate": 0.0630692, "close_rate": 0.06464988170426066, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.585559988076589, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:50:00+00:00", "close_date": "2018-01-16 22:55:00+00:00", "trade_duration": 5, "open_rate": 2.2e-05, "close_rate": 2.299248120300751e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 4545.454545454546, "profit_abs": 0.003999999999999976}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-17 03:30:00+00:00", "close_date": "2018-01-17 04:00:00+00:00", "trade_duration": 30, "open_rate": 4.974e-05, "close_rate": 5.048796992481203e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2010.454362685967, "profit_abs": 0.0010000000000000009}, {"pair": "TRX/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-17 03:55:00+00:00", "close_date": "2018-01-17 04:15:00+00:00", "trade_duration": 20, "open_rate": 7.108e-05, "close_rate": 7.28614536340852e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1406.8655036578502, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 09:35:00+00:00", "close_date": "2018-01-17 10:15:00+00:00", "trade_duration": 40, "open_rate": 0.04327, "close_rate": 0.04348689223057644, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.3110700254217704, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:20:00+00:00", "close_date": "2018-01-17 17:00:00+00:00", "trade_duration": 400, "open_rate": 4.997e-05, "close_rate": 5.022047619047618e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2001.2007204322595, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:25:00+00:00", "trade_duration": 55, "open_rate": 0.06836818, "close_rate": 0.06871087764411027, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4626687444363737, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:10:00+00:00", "trade_duration": 40, "open_rate": 3.63e-05, "close_rate": 3.648195488721804e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2754.8209366391184, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:30:00+00:00", "close_date": "2018-01-17 22:05:00+00:00", "trade_duration": 575, "open_rate": 0.0281, "close_rate": 0.02824085213032581, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5587188612099645, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:35:00+00:00", "close_date": "2018-01-17 16:55:00+00:00", "trade_duration": 260, "open_rate": 0.08651001, "close_rate": 0.08694364413533832, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1559355963546878, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 05:00:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 55, "open_rate": 5.633e-05, "close_rate": 5.6612355889724306e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1775.2529735487308, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 05:20:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 35, "open_rate": 0.06988494, "close_rate": 0.07093584135338346, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.430923457900944, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 07:35:00+00:00", "close_date": "2018-01-18 08:15:00+00:00", "trade_duration": 40, "open_rate": 5.545e-05, "close_rate": 5.572794486215538e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1803.4265103697026, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 09:00:00+00:00", "close_date": "2018-01-18 09:40:00+00:00", "trade_duration": 40, "open_rate": 0.01633527, "close_rate": 0.016417151052631574, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.121723118136401, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 16:40:00+00:00", "close_date": "2018-01-18 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.00269734, "close_rate": 0.002710860501253133, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.073561360451414, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-18 18:05:00+00:00", "close_date": "2018-01-18 18:30:00+00:00", "trade_duration": 25, "open_rate": 4.475e-05, "close_rate": 4.587155388471177e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2234.63687150838, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 18:25:00+00:00", "close_date": "2018-01-18 18:55:00+00:00", "trade_duration": 30, "open_rate": 2.79e-05, "close_rate": 2.8319548872180444e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3584.2293906810037, "profit_abs": 0.000999999999999987}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 20:10:00+00:00", "close_date": "2018-01-18 20:50:00+00:00", "trade_duration": 40, "open_rate": 0.04439326, "close_rate": 0.04461578260651629, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2525942001105577, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 21:30:00+00:00", "close_date": "2018-01-19 00:35:00+00:00", "trade_duration": 185, "open_rate": 4.49e-05, "close_rate": 4.51250626566416e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2227.1714922049, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 21:55:00+00:00", "close_date": "2018-01-19 05:05:00+00:00", "trade_duration": 430, "open_rate": 0.02855, "close_rate": 0.028693107769423555, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.502626970227671, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 22:10:00+00:00", "close_date": "2018-01-18 22:50:00+00:00", "trade_duration": 40, "open_rate": 5.796e-05, "close_rate": 5.8250526315789473e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1725.3278122843342, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 23:50:00+00:00", "close_date": "2018-01-19 00:30:00+00:00", "trade_duration": 40, "open_rate": 0.04340323, "close_rate": 0.04362079005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.303975994413319, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-19 16:45:00+00:00", "close_date": "2018-01-19 17:35:00+00:00", "trade_duration": 50, "open_rate": 0.04454455, "close_rate": 0.04476783095238095, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.244943545282195, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:15:00+00:00", "close_date": "2018-01-19 19:55:00+00:00", "trade_duration": 160, "open_rate": 5.62e-05, "close_rate": 5.648170426065162e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1779.3594306049824, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:20:00+00:00", "close_date": "2018-01-19 20:15:00+00:00", "trade_duration": 175, "open_rate": 4.339e-05, "close_rate": 4.360749373433584e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2304.6784973496196, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-20 04:45:00+00:00", "close_date": "2018-01-20 17:35:00+00:00", "trade_duration": 770, "open_rate": 0.0001009, "close_rate": 0.00010140576441102755, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 991.0802775024778, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 15:15:00+00:00", "trade_duration": 625, "open_rate": 0.00270505, "close_rate": 0.002718609147869674, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.96789338459548, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 07:00:00+00:00", "trade_duration": 130, "open_rate": 0.03000002, "close_rate": 0.030150396040100245, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.3333311111125927, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 09:00:00+00:00", "close_date": "2018-01-20 09:40:00+00:00", "trade_duration": 40, "open_rate": 5.46e-05, "close_rate": 5.4873684210526304e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1831.5018315018317, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-20 18:25:00+00:00", "close_date": "2018-01-25 03:50:00+00:00", "trade_duration": 6325, "open_rate": 0.03082222, "close_rate": 0.027739998, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.244412634781012, "profit_abs": -0.010474999999999998}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 22:25:00+00:00", "close_date": "2018-01-20 23:15:00+00:00", "trade_duration": 50, "open_rate": 0.08969999, "close_rate": 0.09014961401002504, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1148273260677064, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 02:50:00+00:00", "close_date": "2018-01-21 14:30:00+00:00", "trade_duration": 700, "open_rate": 0.01632501, "close_rate": 0.01640683962406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.125570520324337, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 10:20:00+00:00", "close_date": "2018-01-21 11:00:00+00:00", "trade_duration": 40, "open_rate": 0.070538, "close_rate": 0.07089157393483708, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.417675579120474, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 15:50:00+00:00", "close_date": "2018-01-21 18:45:00+00:00", "trade_duration": 175, "open_rate": 5.301e-05, "close_rate": 5.327571428571427e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.4365214110546, "profit_abs": -2.7755575615628914e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 16:20:00+00:00", "close_date": "2018-01-21 17:00:00+00:00", "trade_duration": 40, "open_rate": 3.955e-05, "close_rate": 3.9748245614035085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2528.4450063211125, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:45:00+00:00", "trade_duration": 30, "open_rate": 0.00258505, "close_rate": 0.002623922932330827, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.6839712964933, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:55:00+00:00", "trade_duration": 40, "open_rate": 3.903e-05, "close_rate": 3.922563909774435e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2562.1316935690497, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 00:35:00+00:00", "close_date": "2018-01-22 10:35:00+00:00", "trade_duration": 600, "open_rate": 5.236e-05, "close_rate": 5.262245614035087e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1909.8548510313217, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 01:30:00+00:00", "close_date": "2018-01-22 02:10:00+00:00", "trade_duration": 40, "open_rate": 9.028e-05, "close_rate": 9.07325313283208e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1107.6650420912717, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 12:25:00+00:00", "close_date": "2018-01-22 14:35:00+00:00", "trade_duration": 130, "open_rate": 0.002687, "close_rate": 0.002700468671679198, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.21622627465575, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 13:15:00+00:00", "close_date": "2018-01-22 13:55:00+00:00", "trade_duration": 40, "open_rate": 4.168e-05, "close_rate": 4.188892230576441e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2399.232245681382, "profit_abs": 1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-22 14:00:00+00:00", "close_date": "2018-01-22 14:30:00+00:00", "trade_duration": 30, "open_rate": 8.821e-05, "close_rate": 8.953646616541353e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1133.6583153837435, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 15:55:00+00:00", "close_date": "2018-01-22 16:40:00+00:00", "trade_duration": 45, "open_rate": 5.172e-05, "close_rate": 5.1979248120300745e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1933.4880123743235, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-22 16:05:00+00:00", "close_date": "2018-01-22 16:25:00+00:00", "trade_duration": 20, "open_rate": 3.026e-05, "close_rate": 3.101839598997494e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3304.692663582287, "profit_abs": 0.0020000000000000157}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 19:50:00+00:00", "close_date": "2018-01-23 00:10:00+00:00", "trade_duration": 260, "open_rate": 0.07064, "close_rate": 0.07099408521303258, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.415628539071348, "profit_abs": 1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 21:25:00+00:00", "close_date": "2018-01-22 22:05:00+00:00", "trade_duration": 40, "open_rate": 0.01644483, "close_rate": 0.01652726022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.080938507725528, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-23 00:05:00+00:00", "close_date": "2018-01-23 00:35:00+00:00", "trade_duration": 30, "open_rate": 4.331e-05, "close_rate": 4.3961278195488714e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2308.935580697299, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-23 01:50:00+00:00", "close_date": "2018-01-23 02:15:00+00:00", "trade_duration": 25, "open_rate": 3.2e-05, "close_rate": 3.2802005012531326e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3125.0000000000005, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 04:25:00+00:00", "close_date": "2018-01-23 05:15:00+00:00", "trade_duration": 50, "open_rate": 0.09167706, "close_rate": 0.09213659413533835, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0907854156754153, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 07:35:00+00:00", "close_date": "2018-01-23 09:00:00+00:00", "trade_duration": 85, "open_rate": 0.0692498, "close_rate": 0.06959691679197995, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4440474918339115, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 10:50:00+00:00", "close_date": "2018-01-23 13:05:00+00:00", "trade_duration": 135, "open_rate": 3.182e-05, "close_rate": 3.197949874686716e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3142.677561282213, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 11:05:00+00:00", "close_date": "2018-01-23 16:05:00+00:00", "trade_duration": 300, "open_rate": 0.04088, "close_rate": 0.04108491228070175, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4461839530332683, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 14:55:00+00:00", "close_date": "2018-01-23 15:35:00+00:00", "trade_duration": 40, "open_rate": 5.15e-05, "close_rate": 5.175814536340851e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1941.747572815534, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 16:35:00+00:00", "close_date": "2018-01-24 00:05:00+00:00", "trade_duration": 450, "open_rate": 0.09071698, "close_rate": 0.09117170170426064, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1023294646713329, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 17:25:00+00:00", "close_date": "2018-01-23 18:45:00+00:00", "trade_duration": 80, "open_rate": 3.128e-05, "close_rate": 3.1436791979949865e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3196.9309462915603, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 20:15:00+00:00", "close_date": "2018-01-23 22:00:00+00:00", "trade_duration": 105, "open_rate": 9.555e-05, "close_rate": 9.602894736842104e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1046.5724751439038, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 22:30:00+00:00", "close_date": "2018-01-23 23:10:00+00:00", "trade_duration": 40, "open_rate": 0.04080001, "close_rate": 0.0410045213283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.450979791426522, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 23:50:00+00:00", "close_date": "2018-01-24 03:35:00+00:00", "trade_duration": 225, "open_rate": 5.163e-05, "close_rate": 5.18887969924812e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1936.8584156498162, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 00:20:00+00:00", "close_date": "2018-01-24 01:50:00+00:00", "trade_duration": 90, "open_rate": 0.04040781, "close_rate": 0.04061035541353383, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.474769110228938, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 06:45:00+00:00", "close_date": "2018-01-24 07:25:00+00:00", "trade_duration": 40, "open_rate": 5.132e-05, "close_rate": 5.157724310776942e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1948.5580670303975, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-24 14:15:00+00:00", "close_date": "2018-01-24 14:25:00+00:00", "trade_duration": 10, "open_rate": 5.198e-05, "close_rate": 5.432496240601503e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1923.8168526356292, "profit_abs": 0.0040000000000000036}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 14:50:00+00:00", "close_date": "2018-01-24 16:35:00+00:00", "trade_duration": 105, "open_rate": 3.054e-05, "close_rate": 3.069308270676692e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3274.3942370661425, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 15:10:00+00:00", "close_date": "2018-01-24 16:15:00+00:00", "trade_duration": 65, "open_rate": 9.263e-05, "close_rate": 9.309431077694236e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1079.5638562020945, "profit_abs": 2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 22:40:00+00:00", "close_date": "2018-01-24 23:25:00+00:00", "trade_duration": 45, "open_rate": 5.514e-05, "close_rate": 5.54163909774436e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1813.5654697134569, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 00:50:00+00:00", "close_date": "2018-01-25 01:30:00+00:00", "trade_duration": 40, "open_rate": 4.921e-05, "close_rate": 4.9456666666666664e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.1072952651903, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 08:15:00+00:00", "close_date": "2018-01-25 12:15:00+00:00", "trade_duration": 240, "open_rate": 0.0026, "close_rate": 0.002613032581453634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.46153846153847, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 10:25:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 350, "open_rate": 0.02799871, "close_rate": 0.028139054411027563, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.571593119825878, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 11:00:00+00:00", "close_date": "2018-01-25 11:45:00+00:00", "trade_duration": 45, "open_rate": 0.04078902, "close_rate": 0.0409934762406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4516401717913303, "profit_abs": -1.3877787807814457e-17}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:05:00+00:00", "close_date": "2018-01-25 13:45:00+00:00", "trade_duration": 40, "open_rate": 2.89e-05, "close_rate": 2.904486215538847e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3460.2076124567475, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:20:00+00:00", "close_date": "2018-01-25 14:05:00+00:00", "trade_duration": 45, "open_rate": 0.041103, "close_rate": 0.04130903007518797, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4329124394813033, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-25 15:45:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 30, "open_rate": 5.428e-05, "close_rate": 5.509624060150376e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1842.2991893883568, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 17:45:00+00:00", "close_date": "2018-01-25 23:15:00+00:00", "trade_duration": 330, "open_rate": 5.414e-05, "close_rate": 5.441137844611528e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1847.063169560399, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 21:15:00+00:00", "close_date": "2018-01-25 21:55:00+00:00", "trade_duration": 40, "open_rate": 0.04140777, "close_rate": 0.0416153277443609, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.415005686130888, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 02:05:00+00:00", "close_date": "2018-01-26 02:45:00+00:00", "trade_duration": 40, "open_rate": 0.00254309, "close_rate": 0.002555837318295739, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.32224183965177, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 02:55:00+00:00", "close_date": "2018-01-26 15:10:00+00:00", "trade_duration": 735, "open_rate": 5.607e-05, "close_rate": 5.6351052631578935e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1783.4849295523454, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 06:10:00+00:00", "close_date": "2018-01-26 09:25:00+00:00", "trade_duration": 195, "open_rate": 0.00253806, "close_rate": 0.0025507821052631577, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.400171784748984, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 07:25:00+00:00", "close_date": "2018-01-26 09:55:00+00:00", "trade_duration": 150, "open_rate": 0.0415, "close_rate": 0.04170802005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4096385542168677, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-26 09:55:00+00:00", "close_date": "2018-01-26 10:25:00+00:00", "trade_duration": 30, "open_rate": 5.321e-05, "close_rate": 5.401015037593984e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1879.3459875963165, "profit_abs": 0.000999999999999987}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 16:05:00+00:00", "close_date": "2018-01-26 16:45:00+00:00", "trade_duration": 40, "open_rate": 0.02772046, "close_rate": 0.02785940967418546, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.6074437437185387, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 23:35:00+00:00", "close_date": "2018-01-27 00:15:00+00:00", "trade_duration": 40, "open_rate": 0.09461341, "close_rate": 0.09508766268170424, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0569326272036914, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 00:35:00+00:00", "close_date": "2018-01-27 01:30:00+00:00", "trade_duration": 55, "open_rate": 5.615e-05, "close_rate": 5.643145363408521e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1780.9439002671415, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.07877175, "open_date": "2018-01-27 00:45:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 4560, "open_rate": 5.556e-05, "close_rate": 5.144e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1799.8560115190785, "profit_abs": -0.007896868250539965}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 02:30:00+00:00", "close_date": "2018-01-27 11:25:00+00:00", "trade_duration": 535, "open_rate": 0.06900001, "close_rate": 0.06934587471177944, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492751522789635, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 06:25:00+00:00", "close_date": "2018-01-27 07:05:00+00:00", "trade_duration": 40, "open_rate": 0.09449985, "close_rate": 0.0949735334586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.058202737887944, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.04815133, "open_date": "2018-01-27 09:40:00+00:00", "close_date": "2018-01-30 04:40:00+00:00", "trade_duration": 4020, "open_rate": 0.0410697, "close_rate": 0.03928809, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4348850855983852, "profit_abs": -0.004827170578309559}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 11:45:00+00:00", "close_date": "2018-01-27 12:30:00+00:00", "trade_duration": 45, "open_rate": 0.0285, "close_rate": 0.02864285714285714, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5087719298245617, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 12:35:00+00:00", "close_date": "2018-01-27 15:25:00+00:00", "trade_duration": 170, "open_rate": 0.02866372, "close_rate": 0.02880739779448621, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.4887307020861216, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 15:50:00+00:00", "close_date": "2018-01-27 16:50:00+00:00", "trade_duration": 60, "open_rate": 0.095381, "close_rate": 0.09585910025062656, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0484268355332824, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 17:05:00+00:00", "close_date": "2018-01-27 17:45:00+00:00", "trade_duration": 40, "open_rate": 0.06759092, "close_rate": 0.06792972160401002, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4794886650455417, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 23:40:00+00:00", "close_date": "2018-01-28 01:05:00+00:00", "trade_duration": 85, "open_rate": 0.00258501, "close_rate": 0.002597967443609022, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.684569885609726, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 02:25:00+00:00", "close_date": "2018-01-28 08:10:00+00:00", "trade_duration": 345, "open_rate": 0.06698502, "close_rate": 0.0673207845112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4928710926711672, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 10:25:00+00:00", "close_date": "2018-01-28 16:30:00+00:00", "trade_duration": 365, "open_rate": 0.0677177, "close_rate": 0.06805713709273183, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4767187899175547, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-28 20:35:00+00:00", "close_date": "2018-01-28 21:35:00+00:00", "trade_duration": 60, "open_rate": 5.215e-05, "close_rate": 5.2411403508771925e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1917.5455417066157, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-28 22:00:00+00:00", "close_date": "2018-01-28 22:30:00+00:00", "trade_duration": 30, "open_rate": 0.00273809, "close_rate": 0.002779264285714285, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.5218089982433, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-29 00:00:00+00:00", "close_date": "2018-01-29 00:30:00+00:00", "trade_duration": 30, "open_rate": 0.00274632, "close_rate": 0.002787618045112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.412362725392526, "profit_abs": 0.0010000000000000148}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-29 02:15:00+00:00", "close_date": "2018-01-29 03:00:00+00:00", "trade_duration": 45, "open_rate": 0.01622478, "close_rate": 0.016306107218045113, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.163411768911504, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 03:05:00+00:00", "close_date": "2018-01-29 03:45:00+00:00", "trade_duration": 40, "open_rate": 0.069, "close_rate": 0.06934586466165413, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492753623188406, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 05:20:00+00:00", "close_date": "2018-01-29 06:55:00+00:00", "trade_duration": 95, "open_rate": 8.755e-05, "close_rate": 8.798884711779448e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1142.204454597373, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 07:00:00+00:00", "close_date": "2018-01-29 19:25:00+00:00", "trade_duration": 745, "open_rate": 0.06825763, "close_rate": 0.06859977350877192, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4650376815016872, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 19:45:00+00:00", "close_date": "2018-01-29 20:25:00+00:00", "trade_duration": 40, "open_rate": 0.06713892, "close_rate": 0.06747545593984962, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4894490408841845, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0199116, "open_date": "2018-01-29 23:30:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 315, "open_rate": 8.934e-05, "close_rate": 8.8e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1119.3194537721067, "profit_abs": -0.0019961383478844796}], "results_per_pair": [{"key": "TRX/BTC", "trades": 15, "profit_mean": 0.0023467073333333323, "profit_mean_pct": 0.23467073333333321, "profit_sum": 0.035200609999999986, "profit_sum_pct": 3.5200609999999988, "profit_total_abs": 0.0035288616521155086, "profit_total_pct": 1.1733536666666662, "duration_avg": "2:28:00", "wins": 9, "draws": 2, "losses": 4}, {"key": "ADA/BTC", "trades": 29, "profit_mean": -0.0011598141379310352, "profit_mean_pct": -0.11598141379310352, "profit_sum": -0.03363461000000002, "profit_sum_pct": -3.3634610000000023, "profit_total_abs": -0.0033718682505400333, "profit_total_pct": -1.1211536666666675, "duration_avg": "5:35:00", "wins": 9, "draws": 11, "losses": 9}, {"key": "XLM/BTC", "trades": 21, "profit_mean": 0.0026243899999999994, "profit_mean_pct": 0.2624389999999999, "profit_sum": 0.05511218999999999, "profit_sum_pct": 5.511218999999999, "profit_total_abs": 0.005525000000000002, "profit_total_pct": 1.8370729999999995, "duration_avg": "3:21:00", "wins": 12, "draws": 3, "losses": 6}, {"key": "ETH/BTC", "trades": 21, "profit_mean": 0.0009500057142857142, "profit_mean_pct": 0.09500057142857142, "profit_sum": 0.01995012, "profit_sum_pct": 1.9950119999999998, "profit_total_abs": 0.0019999999999999463, "profit_total_pct": 0.6650039999999999, "duration_avg": "2:17:00", "wins": 5, "draws": 10, "losses": 6}, {"key": "XMR/BTC", "trades": 16, "profit_mean": -0.0027899012500000007, "profit_mean_pct": -0.2789901250000001, "profit_sum": -0.04463842000000001, "profit_sum_pct": -4.463842000000001, "profit_total_abs": -0.0044750000000000345, "profit_total_pct": -1.4879473333333337, "duration_avg": "8:41:00", "wins": 6, "draws": 5, "losses": 5}, {"key": "ZEC/BTC", "trades": 21, "profit_mean": -0.00039290904761904774, "profit_mean_pct": -0.03929090476190478, "profit_sum": -0.008251090000000003, "profit_sum_pct": -0.8251090000000003, "profit_total_abs": -0.000827170578309569, "profit_total_pct": -0.27503633333333344, "duration_avg": "4:17:00", "wins": 8, "draws": 7, "losses": 6}, {"key": "NXT/BTC", "trades": 12, "profit_mean": -0.0012261025000000006, "profit_mean_pct": -0.12261025000000006, "profit_sum": -0.014713230000000008, "profit_sum_pct": -1.4713230000000008, "profit_total_abs": -0.0014750000000000874, "profit_total_pct": -0.4904410000000003, "duration_avg": "0:57:00", "wins": 4, "draws": 3, "losses": 5}, {"key": "LTC/BTC", "trades": 8, "profit_mean": 0.00748129625, "profit_mean_pct": 0.748129625, "profit_sum": 0.05985037, "profit_sum_pct": 5.985037, "profit_total_abs": 0.006000000000000019, "profit_total_pct": 1.9950123333333334, "duration_avg": "1:59:00", "wins": 5, "draws": 2, "losses": 1}, {"key": "ETC/BTC", "trades": 20, "profit_mean": 0.0022568569999999997, "profit_mean_pct": 0.22568569999999996, "profit_sum": 0.04513713999999999, "profit_sum_pct": 4.513713999999999, "profit_total_abs": 0.004525000000000001, "profit_total_pct": 1.504571333333333, "duration_avg": "1:45:00", "wins": 11, "draws": 4, "losses": 5}, {"key": "DASH/BTC", "trades": 16, "profit_mean": 0.0018703237499999997, "profit_mean_pct": 0.18703237499999997, "profit_sum": 0.029925179999999996, "profit_sum_pct": 2.9925179999999996, "profit_total_abs": 0.002999999999999961, "profit_total_pct": 0.9975059999999999, "duration_avg": "3:03:00", "wins": 4, "draws": 7, "losses": 5}, {"key": "TOTAL", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}], "sell_reason_summary": [{"sell_reason": "roi", "trades": 170, "wins": 73, "draws": 54, "losses": 43, "profit_mean": 0.005398268352941177, "profit_mean_pct": 0.54, "profit_sum": 0.91770562, "profit_sum_pct": 91.77, "profit_total_abs": 0.09199999999999964, "profit_pct_total": 30.59}, {"sell_reason": "stop_loss", "trades": 6, "wins": 0, "draws": 0, "losses": 6, "profit_mean": -0.10448878000000002, "profit_mean_pct": -10.45, "profit_sum": -0.6269326800000001, "profit_sum_pct": -62.69, "profit_total_abs": -0.06284999999999992, "profit_pct_total": -20.9}, {"sell_reason": "force_sell", "trades": 3, "wins": 0, "draws": 0, "losses": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.89, "profit_sum": -0.14683468, "profit_sum_pct": -14.68, "profit_total_abs": -0.014720177176734003, "profit_pct_total": -4.89}], "left_open_trades": [{"key": "TRX/BTC", "trades": 1, "profit_mean": -0.0199116, "profit_mean_pct": -1.9911600000000003, "profit_sum": -0.0199116, "profit_sum_pct": -1.9911600000000003, "profit_total_abs": -0.0019961383478844796, "profit_total_pct": -0.6637200000000001, "duration_avg": "5:15:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ADA/BTC", "trades": 1, "profit_mean": -0.07877175, "profit_mean_pct": -7.877175, "profit_sum": -0.07877175, "profit_sum_pct": -7.877175, "profit_total_abs": -0.007896868250539965, "profit_total_pct": -2.625725, "duration_avg": "3 days, 4:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ZEC/BTC", "trades": 1, "profit_mean": -0.04815133, "profit_mean_pct": -4.815133, "profit_sum": -0.04815133, "profit_sum_pct": -4.815133, "profit_total_abs": -0.004827170578309559, "profit_total_pct": -1.6050443333333335, "duration_avg": "2 days, 19:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "TOTAL", "trades": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.894489333333333, "profit_sum": -0.14683468, "profit_sum_pct": -14.683468, "profit_total_abs": -0.014720177176734003, "profit_total_pct": -4.8944893333333335, "duration_avg": "2 days, 1:25:00", "wins": 0, "draws": 0, "losses": 3}], "total_trades": 179, "backtest_start": "2018-01-30 04:45:00+00:00", "backtest_start_ts": 1517287500, "backtest_end": "2018-01-30 04:45:00+00:00", "backtest_end_ts": 1517287500, "backtest_days": 0, "trades_per_day": null, "market_change": 0.25, "stake_amount": 0.1, "max_drawdown": 0.21142322000000008, "drawdown_start": "2018-01-24 14:25:00+00:00", "drawdown_start_ts": 1516803900.0, "drawdown_end": "2018-01-30 04:45:00+00:00", "drawdown_end_ts": 1517287500.0,"pairlist": ["TRX/BTC", "ADA/BTC", "XLM/BTC", "ETH/BTC", "XMR/BTC", "ZEC/BTC","NXT/BTC", "LTC/BTC", "ETC/BTC", "DASH/BTC"]}}, "strategy_comparison": [{"key": "DefaultStrategy", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}, {"key": "TestStrategy", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}]} diff --git a/tests/testdata/backtest-result_new.json b/tests/testdata/backtest-result_new.json index a44597e82..f004e879a 100644 --- a/tests/testdata/backtest-result_new.json +++ b/tests/testdata/backtest-result_new.json @@ -1 +1 @@ -{"strategy": {"DefaultStrategy": {"trades": [{"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:20:00+00:00", "trade_duration": 5, "open_rate": 9.64e-05, "close_rate": 0.00010074887218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1037.344398340249, "profit_abs": 0.00399999999999999}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:30:00+00:00", "trade_duration": 15, "open_rate": 4.756e-05, "close_rate": 4.9705563909774425e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2102.6072329688814, "profit_abs": 0.00399999999999999}, {"pair": "XLM/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:35:00+00:00", "trade_duration": 10, "open_rate": 3.339e-05, "close_rate": 3.489631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2994.908655286014, "profit_abs": 0.0040000000000000036}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:40:00+00:00", "trade_duration": 15, "open_rate": 9.696e-05, "close_rate": 0.00010133413533834584, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1031.3531353135315, "profit_abs": 0.00399999999999999}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 07:35:00+00:00", "close_date": "2018-01-10 08:35:00+00:00", "trade_duration": 60, "open_rate": 0.0943, "close_rate": 0.09477268170426063, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0604453870625663, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 07:40:00+00:00", "close_date": "2018-01-10 08:10:00+00:00", "trade_duration": 30, "open_rate": 0.02719607, "close_rate": 0.02760503345864661, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.677001860930642, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 08:15:00+00:00", "close_date": "2018-01-10 09:55:00+00:00", "trade_duration": 100, "open_rate": 0.04634952, "close_rate": 0.046581848421052625, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1575196463739, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 14:45:00+00:00", "close_date": "2018-01-10 15:50:00+00:00", "trade_duration": 65, "open_rate": 3.066e-05, "close_rate": 3.081368421052631e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3261.5786040443577, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 16:35:00+00:00", "close_date": "2018-01-10 17:15:00+00:00", "trade_duration": 40, "open_rate": 0.0168999, "close_rate": 0.016984611278195488, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.917194776300452, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 16:40:00+00:00", "close_date": "2018-01-10 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.09132568, "close_rate": 0.0917834528320802, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0949822656672252, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 18:50:00+00:00", "close_date": "2018-01-10 19:45:00+00:00", "trade_duration": 55, "open_rate": 0.08898003, "close_rate": 0.08942604518796991, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1238476768326557, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 22:15:00+00:00", "close_date": "2018-01-10 23:00:00+00:00", "trade_duration": 45, "open_rate": 0.08560008, "close_rate": 0.08602915308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1682232072680307, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 22:50:00+00:00", "close_date": "2018-01-10 23:20:00+00:00", "trade_duration": 30, "open_rate": 0.00249083, "close_rate": 0.0025282860902255634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 40.147260150231055, "profit_abs": 0.000999999999999987}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 23:15:00+00:00", "close_date": "2018-01-11 00:15:00+00:00", "trade_duration": 60, "open_rate": 3.022e-05, "close_rate": 3.037147869674185e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3309.0668431502318, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-10 23:40:00+00:00", "close_date": "2018-01-11 00:05:00+00:00", "trade_duration": 25, "open_rate": 0.002437, "close_rate": 0.0024980776942355883, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.03405826836274, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 00:00:00+00:00", "close_date": "2018-01-11 00:35:00+00:00", "trade_duration": 35, "open_rate": 0.04771803, "close_rate": 0.04843559436090225, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0956439316543456, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-11 03:40:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 45, "open_rate": 3.651e-05, "close_rate": 3.2859000000000005e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2738.9756231169545, "profit_abs": -0.01047499999999997}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 03:55:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 30, "open_rate": 0.08824105, "close_rate": 0.08956798308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1332594070446804, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 04:00:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 50, "open_rate": 0.00243, "close_rate": 0.002442180451127819, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.1522633744856, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:55:00+00:00", "trade_duration": 25, "open_rate": 0.04545064, "close_rate": 0.046589753784461146, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.200189040242338, "profit_abs": 0.001999999999999988}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 20, "open_rate": 3.372e-05, "close_rate": 3.456511278195488e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2965.599051008304, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:55:00+00:00", "close_date": "2018-01-11 05:15:00+00:00", "trade_duration": 20, "open_rate": 0.02644, "close_rate": 0.02710265664160401, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7821482602118004, "profit_abs": 0.001999999999999988}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:20:00+00:00", "close_date": "2018-01-11 12:00:00+00:00", "trade_duration": 40, "open_rate": 0.08812, "close_rate": 0.08856170426065162, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1348161597821154, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:35:00+00:00", "close_date": "2018-01-11 12:15:00+00:00", "trade_duration": 40, "open_rate": 0.02683577, "close_rate": 0.026970285137844607, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7263696923919087, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 14:00:00+00:00", "close_date": "2018-01-11 14:25:00+00:00", "trade_duration": 25, "open_rate": 4.919e-05, "close_rate": 5.04228320802005e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.9335230737956, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 19:25:00+00:00", "close_date": "2018-01-11 20:35:00+00:00", "trade_duration": 70, "open_rate": 0.08784896, "close_rate": 0.08828930566416039, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1383174029607181, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:35:00+00:00", "close_date": "2018-01-11 23:30:00+00:00", "trade_duration": 55, "open_rate": 5.105e-05, "close_rate": 5.130588972431077e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1958.8638589618022, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:25:00+00:00", "trade_duration": 30, "open_rate": 3.96e-05, "close_rate": 4.019548872180451e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2525.252525252525, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:35:00+00:00", "trade_duration": 40, "open_rate": 2.885e-05, "close_rate": 2.899461152882205e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3466.204506065858, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 23:30:00+00:00", "close_date": "2018-01-12 00:05:00+00:00", "trade_duration": 35, "open_rate": 0.02645, "close_rate": 0.026847744360902256, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.780718336483932, "profit_abs": 0.0010000000000000148}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 23:55:00+00:00", "close_date": "2018-01-12 01:15:00+00:00", "trade_duration": 80, "open_rate": 0.048, "close_rate": 0.04824060150375939, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0833333333333335, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-12 21:15:00+00:00", "close_date": "2018-01-12 21:40:00+00:00", "trade_duration": 25, "open_rate": 4.692e-05, "close_rate": 4.809593984962405e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2131.287297527707, "profit_abs": 0.001999999999999974}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 00:55:00+00:00", "close_date": "2018-01-13 06:20:00+00:00", "trade_duration": 325, "open_rate": 0.00256966, "close_rate": 0.0025825405012531327, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.91565421106294, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 10:55:00+00:00", "close_date": "2018-01-13 11:35:00+00:00", "trade_duration": 40, "open_rate": 6.262e-05, "close_rate": 6.293388471177944e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1596.933886937081, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 13:05:00+00:00", "close_date": "2018-01-15 14:10:00+00:00", "trade_duration": 2945, "open_rate": 4.73e-05, "close_rate": 4.753709273182957e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2114.1649048625795, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:30:00+00:00", "close_date": "2018-01-13 14:45:00+00:00", "trade_duration": 75, "open_rate": 6.063e-05, "close_rate": 6.0933909774436085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1649.348507339601, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:40:00+00:00", "close_date": "2018-01-13 23:30:00+00:00", "trade_duration": 590, "open_rate": 0.00011082, "close_rate": 0.00011137548872180448, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 902.3641941887746, "profit_abs": -2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 15:15:00+00:00", "close_date": "2018-01-13 15:55:00+00:00", "trade_duration": 40, "open_rate": 5.93e-05, "close_rate": 5.9597243107769415e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1686.3406408094436, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 16:30:00+00:00", "close_date": "2018-01-13 17:10:00+00:00", "trade_duration": 40, "open_rate": 0.04850003, "close_rate": 0.04874313791979949, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0618543947292407, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 22:05:00+00:00", "close_date": "2018-01-14 06:25:00+00:00", "trade_duration": 500, "open_rate": 0.09825019, "close_rate": 0.09874267215538848, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0178097365511456, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 00:20:00+00:00", "close_date": "2018-01-14 22:55:00+00:00", "trade_duration": 1355, "open_rate": 6.018e-05, "close_rate": 6.048165413533834e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1661.681621801263, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 12:45:00+00:00", "close_date": "2018-01-14 13:25:00+00:00", "trade_duration": 40, "open_rate": 0.09758999, "close_rate": 0.0980791628822055, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.024695258191952, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-14 15:30:00+00:00", "close_date": "2018-01-14 16:00:00+00:00", "trade_duration": 30, "open_rate": 0.00311, "close_rate": 0.0031567669172932328, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.154340836012864, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 20:45:00+00:00", "close_date": "2018-01-14 22:15:00+00:00", "trade_duration": 90, "open_rate": 0.00312401, "close_rate": 0.003139669197994987, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.010140812609436, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 23:35:00+00:00", "close_date": "2018-01-15 00:30:00+00:00", "trade_duration": 55, "open_rate": 0.0174679, "close_rate": 0.017555458395989976, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.724786608579165, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 23:45:00+00:00", "close_date": "2018-01-15 00:25:00+00:00", "trade_duration": 40, "open_rate": 0.07346846, "close_rate": 0.07383672295739348, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.3611282991367997, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 02:25:00+00:00", "close_date": "2018-01-15 03:05:00+00:00", "trade_duration": 40, "open_rate": 0.097994, "close_rate": 0.09848519799498744, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.020470641059657, "profit_abs": -2.7755575615628914e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 07:20:00+00:00", "close_date": "2018-01-15 08:00:00+00:00", "trade_duration": 40, "open_rate": 0.09659, "close_rate": 0.09707416040100247, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0353038616834043, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-15 08:20:00+00:00", "close_date": "2018-01-15 08:55:00+00:00", "trade_duration": 35, "open_rate": 9.987e-05, "close_rate": 0.00010137180451127818, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1001.3016921998599, "profit_abs": 0.0010000000000000009}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-15 12:10:00+00:00", "close_date": "2018-01-16 02:50:00+00:00", "trade_duration": 880, "open_rate": 0.0948969, "close_rate": 0.09537257368421052, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0537752023511833, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:10:00+00:00", "close_date": "2018-01-15 17:40:00+00:00", "trade_duration": 210, "open_rate": 0.071, "close_rate": 0.07135588972431077, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4084507042253522, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:30:00+00:00", "close_date": "2018-01-15 15:10:00+00:00", "trade_duration": 40, "open_rate": 0.04600501, "close_rate": 0.046235611553884705, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.173676301776698, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:10:00+00:00", "close_date": "2018-01-15 19:25:00+00:00", "trade_duration": 75, "open_rate": 9.438e-05, "close_rate": 9.485308270676693e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1059.5465140919687, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:35:00+00:00", "close_date": "2018-01-15 19:15:00+00:00", "trade_duration": 40, "open_rate": 0.03040001, "close_rate": 0.030552391002506264, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.2894726021471703, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-15 20:25:00+00:00", "close_date": "2018-01-16 08:25:00+00:00", "trade_duration": 720, "open_rate": 5.837e-05, "close_rate": 5.2533e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1713.2088401576154, "profit_abs": -0.010474999999999984}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 20:40:00+00:00", "close_date": "2018-01-15 22:00:00+00:00", "trade_duration": 80, "open_rate": 0.046036, "close_rate": 0.04626675689223057, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1722130506560084, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 00:30:00+00:00", "close_date": "2018-01-16 01:10:00+00:00", "trade_duration": 40, "open_rate": 0.0028685, "close_rate": 0.0028828784461152877, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 34.86142583231654, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 01:15:00+00:00", "close_date": "2018-01-16 02:35:00+00:00", "trade_duration": 80, "open_rate": 0.06731755, "close_rate": 0.0676549813283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4854967241083492, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 07:45:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 55, "open_rate": 0.09217614, "close_rate": 0.09263817578947368, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0848794492804754, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:55:00+00:00", "trade_duration": 20, "open_rate": 0.0165, "close_rate": 0.016913533834586467, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.0606060606060606, "profit_abs": 0.0020000000000000018}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 5, "open_rate": 7.953e-05, "close_rate": 8.311781954887218e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1257.387149503332, "profit_abs": 0.00399999999999999}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 08:45:00+00:00", "close_date": "2018-01-16 09:50:00+00:00", "trade_duration": 65, "open_rate": 0.045202, "close_rate": 0.04542857644110275, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2122914915269236, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:45:00+00:00", "trade_duration": 30, "open_rate": 5.248e-05, "close_rate": 5.326917293233082e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1905.487804878049, "profit_abs": 0.0010000000000000009}, {"pair": "XMR/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:55:00+00:00", "trade_duration": 40, "open_rate": 0.02892318, "close_rate": 0.02906815834586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.457434486802627, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 09:50:00+00:00", "close_date": "2018-01-16 10:10:00+00:00", "trade_duration": 20, "open_rate": 5.158e-05, "close_rate": 5.287273182957392e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1938.735944164405, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:35:00+00:00", "trade_duration": 30, "open_rate": 0.02828232, "close_rate": 0.02870761804511278, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5357778286929786, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:40:00+00:00", "trade_duration": 35, "open_rate": 0.04357584, "close_rate": 0.044231115789473675, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.294849623093898, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 13:45:00+00:00", "close_date": "2018-01-16 14:20:00+00:00", "trade_duration": 35, "open_rate": 5.362e-05, "close_rate": 5.442631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1864.975755315181, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 17:30:00+00:00", "close_date": "2018-01-16 18:25:00+00:00", "trade_duration": 55, "open_rate": 5.302e-05, "close_rate": 5.328576441102756e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.0807242549984, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:45:00+00:00", "trade_duration": 30, "open_rate": 0.09129999, "close_rate": 0.09267292218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0952903718828448, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:35:00+00:00", "trade_duration": 20, "open_rate": 3.808e-05, "close_rate": 3.903438596491228e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2626.0504201680674, "profit_abs": 0.0020000000000000018}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 19:00:00+00:00", "close_date": "2018-01-16 19:30:00+00:00", "trade_duration": 30, "open_rate": 0.02811012, "close_rate": 0.028532828571428567, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.557437677249333, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 60, "open_rate": 0.00258379, "close_rate": 0.002325411, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.702835756775904, "profit_abs": -0.010474999999999984}, {"pair": "NXT/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 80, "open_rate": 2.559e-05, "close_rate": 2.3031e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3907.7764751856193, "profit_abs": -0.010474999999999998}, {"pair": "TRX/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:35:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 50, "open_rate": 7.62e-05, "close_rate": 6.858e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1312.3359580052495, "profit_abs": -0.010474999999999984}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:35:00+00:00", "trade_duration": 5, "open_rate": 0.00229844, "close_rate": 0.002402129022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 43.507770487809125, "profit_abs": 0.004000000000000017}, {"pair": "LTC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:40:00+00:00", "trade_duration": 10, "open_rate": 0.0151, "close_rate": 0.015781203007518795, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.622516556291391, "profit_abs": 0.00399999999999999}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:40:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 5, "open_rate": 0.00235676, "close_rate": 0.00246308, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 42.431134269081284, "profit_abs": 0.0040000000000000036}, {"pair": "DASH/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 22:45:00+00:00", "close_date": "2018-01-16 23:05:00+00:00", "trade_duration": 20, "open_rate": 0.0630692, "close_rate": 0.06464988170426066, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.585559988076589, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:50:00+00:00", "close_date": "2018-01-16 22:55:00+00:00", "trade_duration": 5, "open_rate": 2.2e-05, "close_rate": 2.299248120300751e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 4545.454545454546, "profit_abs": 0.003999999999999976}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-17 03:30:00+00:00", "close_date": "2018-01-17 04:00:00+00:00", "trade_duration": 30, "open_rate": 4.974e-05, "close_rate": 5.048796992481203e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2010.454362685967, "profit_abs": 0.0010000000000000009}, {"pair": "TRX/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-17 03:55:00+00:00", "close_date": "2018-01-17 04:15:00+00:00", "trade_duration": 20, "open_rate": 7.108e-05, "close_rate": 7.28614536340852e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1406.8655036578502, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 09:35:00+00:00", "close_date": "2018-01-17 10:15:00+00:00", "trade_duration": 40, "open_rate": 0.04327, "close_rate": 0.04348689223057644, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.3110700254217704, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:20:00+00:00", "close_date": "2018-01-17 17:00:00+00:00", "trade_duration": 400, "open_rate": 4.997e-05, "close_rate": 5.022047619047618e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2001.2007204322595, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:25:00+00:00", "trade_duration": 55, "open_rate": 0.06836818, "close_rate": 0.06871087764411027, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4626687444363737, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:10:00+00:00", "trade_duration": 40, "open_rate": 3.63e-05, "close_rate": 3.648195488721804e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2754.8209366391184, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:30:00+00:00", "close_date": "2018-01-17 22:05:00+00:00", "trade_duration": 575, "open_rate": 0.0281, "close_rate": 0.02824085213032581, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5587188612099645, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:35:00+00:00", "close_date": "2018-01-17 16:55:00+00:00", "trade_duration": 260, "open_rate": 0.08651001, "close_rate": 0.08694364413533832, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1559355963546878, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 05:00:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 55, "open_rate": 5.633e-05, "close_rate": 5.6612355889724306e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1775.2529735487308, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 05:20:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 35, "open_rate": 0.06988494, "close_rate": 0.07093584135338346, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.430923457900944, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 07:35:00+00:00", "close_date": "2018-01-18 08:15:00+00:00", "trade_duration": 40, "open_rate": 5.545e-05, "close_rate": 5.572794486215538e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1803.4265103697026, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 09:00:00+00:00", "close_date": "2018-01-18 09:40:00+00:00", "trade_duration": 40, "open_rate": 0.01633527, "close_rate": 0.016417151052631574, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.121723118136401, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 16:40:00+00:00", "close_date": "2018-01-18 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.00269734, "close_rate": 0.002710860501253133, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.073561360451414, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-18 18:05:00+00:00", "close_date": "2018-01-18 18:30:00+00:00", "trade_duration": 25, "open_rate": 4.475e-05, "close_rate": 4.587155388471177e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2234.63687150838, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 18:25:00+00:00", "close_date": "2018-01-18 18:55:00+00:00", "trade_duration": 30, "open_rate": 2.79e-05, "close_rate": 2.8319548872180444e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3584.2293906810037, "profit_abs": 0.000999999999999987}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 20:10:00+00:00", "close_date": "2018-01-18 20:50:00+00:00", "trade_duration": 40, "open_rate": 0.04439326, "close_rate": 0.04461578260651629, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2525942001105577, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 21:30:00+00:00", "close_date": "2018-01-19 00:35:00+00:00", "trade_duration": 185, "open_rate": 4.49e-05, "close_rate": 4.51250626566416e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2227.1714922049, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 21:55:00+00:00", "close_date": "2018-01-19 05:05:00+00:00", "trade_duration": 430, "open_rate": 0.02855, "close_rate": 0.028693107769423555, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.502626970227671, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 22:10:00+00:00", "close_date": "2018-01-18 22:50:00+00:00", "trade_duration": 40, "open_rate": 5.796e-05, "close_rate": 5.8250526315789473e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1725.3278122843342, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 23:50:00+00:00", "close_date": "2018-01-19 00:30:00+00:00", "trade_duration": 40, "open_rate": 0.04340323, "close_rate": 0.04362079005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.303975994413319, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-19 16:45:00+00:00", "close_date": "2018-01-19 17:35:00+00:00", "trade_duration": 50, "open_rate": 0.04454455, "close_rate": 0.04476783095238095, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.244943545282195, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:15:00+00:00", "close_date": "2018-01-19 19:55:00+00:00", "trade_duration": 160, "open_rate": 5.62e-05, "close_rate": 5.648170426065162e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1779.3594306049824, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:20:00+00:00", "close_date": "2018-01-19 20:15:00+00:00", "trade_duration": 175, "open_rate": 4.339e-05, "close_rate": 4.360749373433584e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2304.6784973496196, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-20 04:45:00+00:00", "close_date": "2018-01-20 17:35:00+00:00", "trade_duration": 770, "open_rate": 0.0001009, "close_rate": 0.00010140576441102755, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 991.0802775024778, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 15:15:00+00:00", "trade_duration": 625, "open_rate": 0.00270505, "close_rate": 0.002718609147869674, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.96789338459548, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 07:00:00+00:00", "trade_duration": 130, "open_rate": 0.03000002, "close_rate": 0.030150396040100245, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.3333311111125927, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 09:00:00+00:00", "close_date": "2018-01-20 09:40:00+00:00", "trade_duration": 40, "open_rate": 5.46e-05, "close_rate": 5.4873684210526304e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1831.5018315018317, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-20 18:25:00+00:00", "close_date": "2018-01-25 03:50:00+00:00", "trade_duration": 6325, "open_rate": 0.03082222, "close_rate": 0.027739998, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.244412634781012, "profit_abs": -0.010474999999999998}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 22:25:00+00:00", "close_date": "2018-01-20 23:15:00+00:00", "trade_duration": 50, "open_rate": 0.08969999, "close_rate": 0.09014961401002504, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1148273260677064, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 02:50:00+00:00", "close_date": "2018-01-21 14:30:00+00:00", "trade_duration": 700, "open_rate": 0.01632501, "close_rate": 0.01640683962406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.125570520324337, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 10:20:00+00:00", "close_date": "2018-01-21 11:00:00+00:00", "trade_duration": 40, "open_rate": 0.070538, "close_rate": 0.07089157393483708, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.417675579120474, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 15:50:00+00:00", "close_date": "2018-01-21 18:45:00+00:00", "trade_duration": 175, "open_rate": 5.301e-05, "close_rate": 5.327571428571427e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.4365214110546, "profit_abs": -2.7755575615628914e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 16:20:00+00:00", "close_date": "2018-01-21 17:00:00+00:00", "trade_duration": 40, "open_rate": 3.955e-05, "close_rate": 3.9748245614035085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2528.4450063211125, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:45:00+00:00", "trade_duration": 30, "open_rate": 0.00258505, "close_rate": 0.002623922932330827, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.6839712964933, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:55:00+00:00", "trade_duration": 40, "open_rate": 3.903e-05, "close_rate": 3.922563909774435e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2562.1316935690497, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 00:35:00+00:00", "close_date": "2018-01-22 10:35:00+00:00", "trade_duration": 600, "open_rate": 5.236e-05, "close_rate": 5.262245614035087e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1909.8548510313217, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 01:30:00+00:00", "close_date": "2018-01-22 02:10:00+00:00", "trade_duration": 40, "open_rate": 9.028e-05, "close_rate": 9.07325313283208e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1107.6650420912717, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 12:25:00+00:00", "close_date": "2018-01-22 14:35:00+00:00", "trade_duration": 130, "open_rate": 0.002687, "close_rate": 0.002700468671679198, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.21622627465575, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 13:15:00+00:00", "close_date": "2018-01-22 13:55:00+00:00", "trade_duration": 40, "open_rate": 4.168e-05, "close_rate": 4.188892230576441e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2399.232245681382, "profit_abs": 1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-22 14:00:00+00:00", "close_date": "2018-01-22 14:30:00+00:00", "trade_duration": 30, "open_rate": 8.821e-05, "close_rate": 8.953646616541353e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1133.6583153837435, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 15:55:00+00:00", "close_date": "2018-01-22 16:40:00+00:00", "trade_duration": 45, "open_rate": 5.172e-05, "close_rate": 5.1979248120300745e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1933.4880123743235, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-22 16:05:00+00:00", "close_date": "2018-01-22 16:25:00+00:00", "trade_duration": 20, "open_rate": 3.026e-05, "close_rate": 3.101839598997494e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3304.692663582287, "profit_abs": 0.0020000000000000157}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 19:50:00+00:00", "close_date": "2018-01-23 00:10:00+00:00", "trade_duration": 260, "open_rate": 0.07064, "close_rate": 0.07099408521303258, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.415628539071348, "profit_abs": 1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 21:25:00+00:00", "close_date": "2018-01-22 22:05:00+00:00", "trade_duration": 40, "open_rate": 0.01644483, "close_rate": 0.01652726022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.080938507725528, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-23 00:05:00+00:00", "close_date": "2018-01-23 00:35:00+00:00", "trade_duration": 30, "open_rate": 4.331e-05, "close_rate": 4.3961278195488714e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2308.935580697299, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-23 01:50:00+00:00", "close_date": "2018-01-23 02:15:00+00:00", "trade_duration": 25, "open_rate": 3.2e-05, "close_rate": 3.2802005012531326e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3125.0000000000005, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 04:25:00+00:00", "close_date": "2018-01-23 05:15:00+00:00", "trade_duration": 50, "open_rate": 0.09167706, "close_rate": 0.09213659413533835, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0907854156754153, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 07:35:00+00:00", "close_date": "2018-01-23 09:00:00+00:00", "trade_duration": 85, "open_rate": 0.0692498, "close_rate": 0.06959691679197995, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4440474918339115, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 10:50:00+00:00", "close_date": "2018-01-23 13:05:00+00:00", "trade_duration": 135, "open_rate": 3.182e-05, "close_rate": 3.197949874686716e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3142.677561282213, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 11:05:00+00:00", "close_date": "2018-01-23 16:05:00+00:00", "trade_duration": 300, "open_rate": 0.04088, "close_rate": 0.04108491228070175, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4461839530332683, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 14:55:00+00:00", "close_date": "2018-01-23 15:35:00+00:00", "trade_duration": 40, "open_rate": 5.15e-05, "close_rate": 5.175814536340851e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1941.747572815534, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 16:35:00+00:00", "close_date": "2018-01-24 00:05:00+00:00", "trade_duration": 450, "open_rate": 0.09071698, "close_rate": 0.09117170170426064, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1023294646713329, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 17:25:00+00:00", "close_date": "2018-01-23 18:45:00+00:00", "trade_duration": 80, "open_rate": 3.128e-05, "close_rate": 3.1436791979949865e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3196.9309462915603, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 20:15:00+00:00", "close_date": "2018-01-23 22:00:00+00:00", "trade_duration": 105, "open_rate": 9.555e-05, "close_rate": 9.602894736842104e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1046.5724751439038, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 22:30:00+00:00", "close_date": "2018-01-23 23:10:00+00:00", "trade_duration": 40, "open_rate": 0.04080001, "close_rate": 0.0410045213283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.450979791426522, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 23:50:00+00:00", "close_date": "2018-01-24 03:35:00+00:00", "trade_duration": 225, "open_rate": 5.163e-05, "close_rate": 5.18887969924812e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1936.8584156498162, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 00:20:00+00:00", "close_date": "2018-01-24 01:50:00+00:00", "trade_duration": 90, "open_rate": 0.04040781, "close_rate": 0.04061035541353383, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.474769110228938, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 06:45:00+00:00", "close_date": "2018-01-24 07:25:00+00:00", "trade_duration": 40, "open_rate": 5.132e-05, "close_rate": 5.157724310776942e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1948.5580670303975, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-24 14:15:00+00:00", "close_date": "2018-01-24 14:25:00+00:00", "trade_duration": 10, "open_rate": 5.198e-05, "close_rate": 5.432496240601503e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1923.8168526356292, "profit_abs": 0.0040000000000000036}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 14:50:00+00:00", "close_date": "2018-01-24 16:35:00+00:00", "trade_duration": 105, "open_rate": 3.054e-05, "close_rate": 3.069308270676692e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3274.3942370661425, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 15:10:00+00:00", "close_date": "2018-01-24 16:15:00+00:00", "trade_duration": 65, "open_rate": 9.263e-05, "close_rate": 9.309431077694236e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1079.5638562020945, "profit_abs": 2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 22:40:00+00:00", "close_date": "2018-01-24 23:25:00+00:00", "trade_duration": 45, "open_rate": 5.514e-05, "close_rate": 5.54163909774436e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1813.5654697134569, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 00:50:00+00:00", "close_date": "2018-01-25 01:30:00+00:00", "trade_duration": 40, "open_rate": 4.921e-05, "close_rate": 4.9456666666666664e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.1072952651903, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 08:15:00+00:00", "close_date": "2018-01-25 12:15:00+00:00", "trade_duration": 240, "open_rate": 0.0026, "close_rate": 0.002613032581453634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.46153846153847, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 10:25:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 350, "open_rate": 0.02799871, "close_rate": 0.028139054411027563, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.571593119825878, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 11:00:00+00:00", "close_date": "2018-01-25 11:45:00+00:00", "trade_duration": 45, "open_rate": 0.04078902, "close_rate": 0.0409934762406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4516401717913303, "profit_abs": -1.3877787807814457e-17}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:05:00+00:00", "close_date": "2018-01-25 13:45:00+00:00", "trade_duration": 40, "open_rate": 2.89e-05, "close_rate": 2.904486215538847e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3460.2076124567475, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:20:00+00:00", "close_date": "2018-01-25 14:05:00+00:00", "trade_duration": 45, "open_rate": 0.041103, "close_rate": 0.04130903007518797, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4329124394813033, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-25 15:45:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 30, "open_rate": 5.428e-05, "close_rate": 5.509624060150376e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1842.2991893883568, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 17:45:00+00:00", "close_date": "2018-01-25 23:15:00+00:00", "trade_duration": 330, "open_rate": 5.414e-05, "close_rate": 5.441137844611528e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1847.063169560399, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 21:15:00+00:00", "close_date": "2018-01-25 21:55:00+00:00", "trade_duration": 40, "open_rate": 0.04140777, "close_rate": 0.0416153277443609, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.415005686130888, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 02:05:00+00:00", "close_date": "2018-01-26 02:45:00+00:00", "trade_duration": 40, "open_rate": 0.00254309, "close_rate": 0.002555837318295739, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.32224183965177, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 02:55:00+00:00", "close_date": "2018-01-26 15:10:00+00:00", "trade_duration": 735, "open_rate": 5.607e-05, "close_rate": 5.6351052631578935e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1783.4849295523454, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 06:10:00+00:00", "close_date": "2018-01-26 09:25:00+00:00", "trade_duration": 195, "open_rate": 0.00253806, "close_rate": 0.0025507821052631577, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.400171784748984, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 07:25:00+00:00", "close_date": "2018-01-26 09:55:00+00:00", "trade_duration": 150, "open_rate": 0.0415, "close_rate": 0.04170802005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4096385542168677, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-26 09:55:00+00:00", "close_date": "2018-01-26 10:25:00+00:00", "trade_duration": 30, "open_rate": 5.321e-05, "close_rate": 5.401015037593984e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1879.3459875963165, "profit_abs": 0.000999999999999987}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 16:05:00+00:00", "close_date": "2018-01-26 16:45:00+00:00", "trade_duration": 40, "open_rate": 0.02772046, "close_rate": 0.02785940967418546, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.6074437437185387, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 23:35:00+00:00", "close_date": "2018-01-27 00:15:00+00:00", "trade_duration": 40, "open_rate": 0.09461341, "close_rate": 0.09508766268170424, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0569326272036914, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 00:35:00+00:00", "close_date": "2018-01-27 01:30:00+00:00", "trade_duration": 55, "open_rate": 5.615e-05, "close_rate": 5.643145363408521e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1780.9439002671415, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.07877175, "open_date": "2018-01-27 00:45:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 4560, "open_rate": 5.556e-05, "close_rate": 5.144e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1799.8560115190785, "profit_abs": -0.007896868250539965}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 02:30:00+00:00", "close_date": "2018-01-27 11:25:00+00:00", "trade_duration": 535, "open_rate": 0.06900001, "close_rate": 0.06934587471177944, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492751522789635, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 06:25:00+00:00", "close_date": "2018-01-27 07:05:00+00:00", "trade_duration": 40, "open_rate": 0.09449985, "close_rate": 0.0949735334586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.058202737887944, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.04815133, "open_date": "2018-01-27 09:40:00+00:00", "close_date": "2018-01-30 04:40:00+00:00", "trade_duration": 4020, "open_rate": 0.0410697, "close_rate": 0.03928809, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4348850855983852, "profit_abs": -0.004827170578309559}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 11:45:00+00:00", "close_date": "2018-01-27 12:30:00+00:00", "trade_duration": 45, "open_rate": 0.0285, "close_rate": 0.02864285714285714, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5087719298245617, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 12:35:00+00:00", "close_date": "2018-01-27 15:25:00+00:00", "trade_duration": 170, "open_rate": 0.02866372, "close_rate": 0.02880739779448621, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.4887307020861216, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 15:50:00+00:00", "close_date": "2018-01-27 16:50:00+00:00", "trade_duration": 60, "open_rate": 0.095381, "close_rate": 0.09585910025062656, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0484268355332824, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 17:05:00+00:00", "close_date": "2018-01-27 17:45:00+00:00", "trade_duration": 40, "open_rate": 0.06759092, "close_rate": 0.06792972160401002, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4794886650455417, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 23:40:00+00:00", "close_date": "2018-01-28 01:05:00+00:00", "trade_duration": 85, "open_rate": 0.00258501, "close_rate": 0.002597967443609022, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.684569885609726, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 02:25:00+00:00", "close_date": "2018-01-28 08:10:00+00:00", "trade_duration": 345, "open_rate": 0.06698502, "close_rate": 0.0673207845112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4928710926711672, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 10:25:00+00:00", "close_date": "2018-01-28 16:30:00+00:00", "trade_duration": 365, "open_rate": 0.0677177, "close_rate": 0.06805713709273183, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4767187899175547, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-28 20:35:00+00:00", "close_date": "2018-01-28 21:35:00+00:00", "trade_duration": 60, "open_rate": 5.215e-05, "close_rate": 5.2411403508771925e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1917.5455417066157, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-28 22:00:00+00:00", "close_date": "2018-01-28 22:30:00+00:00", "trade_duration": 30, "open_rate": 0.00273809, "close_rate": 0.002779264285714285, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.5218089982433, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-29 00:00:00+00:00", "close_date": "2018-01-29 00:30:00+00:00", "trade_duration": 30, "open_rate": 0.00274632, "close_rate": 0.002787618045112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.412362725392526, "profit_abs": 0.0010000000000000148}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-29 02:15:00+00:00", "close_date": "2018-01-29 03:00:00+00:00", "trade_duration": 45, "open_rate": 0.01622478, "close_rate": 0.016306107218045113, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.163411768911504, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 03:05:00+00:00", "close_date": "2018-01-29 03:45:00+00:00", "trade_duration": 40, "open_rate": 0.069, "close_rate": 0.06934586466165413, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492753623188406, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 05:20:00+00:00", "close_date": "2018-01-29 06:55:00+00:00", "trade_duration": 95, "open_rate": 8.755e-05, "close_rate": 8.798884711779448e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1142.204454597373, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 07:00:00+00:00", "close_date": "2018-01-29 19:25:00+00:00", "trade_duration": 745, "open_rate": 0.06825763, "close_rate": 0.06859977350877192, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4650376815016872, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 19:45:00+00:00", "close_date": "2018-01-29 20:25:00+00:00", "trade_duration": 40, "open_rate": 0.06713892, "close_rate": 0.06747545593984962, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4894490408841845, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0199116, "open_date": "2018-01-29 23:30:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 315, "open_rate": 8.934e-05, "close_rate": 8.8e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1119.3194537721067, "profit_abs": -0.0019961383478844796}], "results_per_pair": [{"key": "TRX/BTC", "trades": 15, "profit_mean": 0.0023467073333333323, "profit_mean_pct": 0.23467073333333321, "profit_sum": 0.035200609999999986, "profit_sum_pct": 3.5200609999999988, "profit_total_abs": 0.0035288616521155086, "profit_total_pct": 1.1733536666666662, "duration_avg": "2:28:00", "wins": 9, "draws": 2, "losses": 4}, {"key": "ADA/BTC", "trades": 29, "profit_mean": -0.0011598141379310352, "profit_mean_pct": -0.11598141379310352, "profit_sum": -0.03363461000000002, "profit_sum_pct": -3.3634610000000023, "profit_total_abs": -0.0033718682505400333, "profit_total_pct": -1.1211536666666675, "duration_avg": "5:35:00", "wins": 9, "draws": 11, "losses": 9}, {"key": "XLM/BTC", "trades": 21, "profit_mean": 0.0026243899999999994, "profit_mean_pct": 0.2624389999999999, "profit_sum": 0.05511218999999999, "profit_sum_pct": 5.511218999999999, "profit_total_abs": 0.005525000000000002, "profit_total_pct": 1.8370729999999995, "duration_avg": "3:21:00", "wins": 12, "draws": 3, "losses": 6}, {"key": "ETH/BTC", "trades": 21, "profit_mean": 0.0009500057142857142, "profit_mean_pct": 0.09500057142857142, "profit_sum": 0.01995012, "profit_sum_pct": 1.9950119999999998, "profit_total_abs": 0.0019999999999999463, "profit_total_pct": 0.6650039999999999, "duration_avg": "2:17:00", "wins": 5, "draws": 10, "losses": 6}, {"key": "XMR/BTC", "trades": 16, "profit_mean": -0.0027899012500000007, "profit_mean_pct": -0.2789901250000001, "profit_sum": -0.04463842000000001, "profit_sum_pct": -4.463842000000001, "profit_total_abs": -0.0044750000000000345, "profit_total_pct": -1.4879473333333337, "duration_avg": "8:41:00", "wins": 6, "draws": 5, "losses": 5}, {"key": "ZEC/BTC", "trades": 21, "profit_mean": -0.00039290904761904774, "profit_mean_pct": -0.03929090476190478, "profit_sum": -0.008251090000000003, "profit_sum_pct": -0.8251090000000003, "profit_total_abs": -0.000827170578309569, "profit_total_pct": -0.27503633333333344, "duration_avg": "4:17:00", "wins": 8, "draws": 7, "losses": 6}, {"key": "NXT/BTC", "trades": 12, "profit_mean": -0.0012261025000000006, "profit_mean_pct": -0.12261025000000006, "profit_sum": -0.014713230000000008, "profit_sum_pct": -1.4713230000000008, "profit_total_abs": -0.0014750000000000874, "profit_total_pct": -0.4904410000000003, "duration_avg": "0:57:00", "wins": 4, "draws": 3, "losses": 5}, {"key": "LTC/BTC", "trades": 8, "profit_mean": 0.00748129625, "profit_mean_pct": 0.748129625, "profit_sum": 0.05985037, "profit_sum_pct": 5.985037, "profit_total_abs": 0.006000000000000019, "profit_total_pct": 1.9950123333333334, "duration_avg": "1:59:00", "wins": 5, "draws": 2, "losses": 1}, {"key": "ETC/BTC", "trades": 20, "profit_mean": 0.0022568569999999997, "profit_mean_pct": 0.22568569999999996, "profit_sum": 0.04513713999999999, "profit_sum_pct": 4.513713999999999, "profit_total_abs": 0.004525000000000001, "profit_total_pct": 1.504571333333333, "duration_avg": "1:45:00", "wins": 11, "draws": 4, "losses": 5}, {"key": "DASH/BTC", "trades": 16, "profit_mean": 0.0018703237499999997, "profit_mean_pct": 0.18703237499999997, "profit_sum": 0.029925179999999996, "profit_sum_pct": 2.9925179999999996, "profit_total_abs": 0.002999999999999961, "profit_total_pct": 0.9975059999999999, "duration_avg": "3:03:00", "wins": 4, "draws": 7, "losses": 5}, {"key": "TOTAL", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}], "sell_reason_summary": [{"sell_reason": "roi", "trades": 170, "wins": 73, "draws": 54, "losses": 43, "profit_mean": 0.005398268352941177, "profit_mean_pct": 0.54, "profit_sum": 0.91770562, "profit_sum_pct": 91.77, "profit_total_abs": 0.09199999999999964, "profit_pct_total": 30.59}, {"sell_reason": "stop_loss", "trades": 6, "wins": 0, "draws": 0, "losses": 6, "profit_mean": -0.10448878000000002, "profit_mean_pct": -10.45, "profit_sum": -0.6269326800000001, "profit_sum_pct": -62.69, "profit_total_abs": -0.06284999999999992, "profit_pct_total": -20.9}, {"sell_reason": "force_sell", "trades": 3, "wins": 0, "draws": 0, "losses": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.89, "profit_sum": -0.14683468, "profit_sum_pct": -14.68, "profit_total_abs": -0.014720177176734003, "profit_pct_total": -4.89}], "left_open_trades": [{"key": "TRX/BTC", "trades": 1, "profit_mean": -0.0199116, "profit_mean_pct": -1.9911600000000003, "profit_sum": -0.0199116, "profit_sum_pct": -1.9911600000000003, "profit_total_abs": -0.0019961383478844796, "profit_total_pct": -0.6637200000000001, "duration_avg": "5:15:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ADA/BTC", "trades": 1, "profit_mean": -0.07877175, "profit_mean_pct": -7.877175, "profit_sum": -0.07877175, "profit_sum_pct": -7.877175, "profit_total_abs": -0.007896868250539965, "profit_total_pct": -2.625725, "duration_avg": "3 days, 4:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ZEC/BTC", "trades": 1, "profit_mean": -0.04815133, "profit_mean_pct": -4.815133, "profit_sum": -0.04815133, "profit_sum_pct": -4.815133, "profit_total_abs": -0.004827170578309559, "profit_total_pct": -1.6050443333333335, "duration_avg": "2 days, 19:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "TOTAL", "trades": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.894489333333333, "profit_sum": -0.14683468, "profit_sum_pct": -14.683468, "profit_total_abs": -0.014720177176734003, "profit_total_pct": -4.8944893333333335, "duration_avg": "2 days, 1:25:00", "wins": 0, "draws": 0, "losses": 3}], "total_trades": 179, "backtest_start": "2018-01-30 04:45:00+00:00", "backtest_start_ts": 1517287500, "backtest_end": "2018-01-30 04:45:00+00:00", "backtest_end_ts": 1517287500, "backtest_days": 0, "trades_per_day": null, "market_change": 0.25, "stake_amount": 0.1, "max_drawdown": 0.21142322000000008, "drawdown_start": "2018-01-24 14:25:00+00:00", "drawdown_start_ts": 1516803900.0, "drawdown_end": "2018-01-30 04:45:00+00:00", "drawdown_end_ts": 1517287500.0}}, "strategy_comparison": [{"key": "DefaultStrategy", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}]} +{"strategy": {"DefaultStrategy": {"trades": [{"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:20:00+00:00", "trade_duration": 5, "open_rate": 9.64e-05, "close_rate": 0.00010074887218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1037.344398340249, "profit_abs": 0.00399999999999999}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:15:00+00:00", "close_date": "2018-01-10 07:30:00+00:00", "trade_duration": 15, "open_rate": 4.756e-05, "close_rate": 4.9705563909774425e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2102.6072329688814, "profit_abs": 0.00399999999999999}, {"pair": "XLM/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:35:00+00:00", "trade_duration": 10, "open_rate": 3.339e-05, "close_rate": 3.489631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2994.908655286014, "profit_abs": 0.0040000000000000036}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-10 07:25:00+00:00", "close_date": "2018-01-10 07:40:00+00:00", "trade_duration": 15, "open_rate": 9.696e-05, "close_rate": 0.00010133413533834584, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1031.3531353135315, "profit_abs": 0.00399999999999999}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 07:35:00+00:00", "close_date": "2018-01-10 08:35:00+00:00", "trade_duration": 60, "open_rate": 0.0943, "close_rate": 0.09477268170426063, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0604453870625663, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 07:40:00+00:00", "close_date": "2018-01-10 08:10:00+00:00", "trade_duration": 30, "open_rate": 0.02719607, "close_rate": 0.02760503345864661, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.677001860930642, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 08:15:00+00:00", "close_date": "2018-01-10 09:55:00+00:00", "trade_duration": 100, "open_rate": 0.04634952, "close_rate": 0.046581848421052625, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1575196463739, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 14:45:00+00:00", "close_date": "2018-01-10 15:50:00+00:00", "trade_duration": 65, "open_rate": 3.066e-05, "close_rate": 3.081368421052631e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3261.5786040443577, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 16:35:00+00:00", "close_date": "2018-01-10 17:15:00+00:00", "trade_duration": 40, "open_rate": 0.0168999, "close_rate": 0.016984611278195488, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.917194776300452, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 16:40:00+00:00", "close_date": "2018-01-10 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.09132568, "close_rate": 0.0917834528320802, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0949822656672252, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 18:50:00+00:00", "close_date": "2018-01-10 19:45:00+00:00", "trade_duration": 55, "open_rate": 0.08898003, "close_rate": 0.08942604518796991, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1238476768326557, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-10 22:15:00+00:00", "close_date": "2018-01-10 23:00:00+00:00", "trade_duration": 45, "open_rate": 0.08560008, "close_rate": 0.08602915308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1682232072680307, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-10 22:50:00+00:00", "close_date": "2018-01-10 23:20:00+00:00", "trade_duration": 30, "open_rate": 0.00249083, "close_rate": 0.0025282860902255634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 40.147260150231055, "profit_abs": 0.000999999999999987}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-10 23:15:00+00:00", "close_date": "2018-01-11 00:15:00+00:00", "trade_duration": 60, "open_rate": 3.022e-05, "close_rate": 3.037147869674185e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3309.0668431502318, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-10 23:40:00+00:00", "close_date": "2018-01-11 00:05:00+00:00", "trade_duration": 25, "open_rate": 0.002437, "close_rate": 0.0024980776942355883, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.03405826836274, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 00:00:00+00:00", "close_date": "2018-01-11 00:35:00+00:00", "trade_duration": 35, "open_rate": 0.04771803, "close_rate": 0.04843559436090225, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0956439316543456, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-11 03:40:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 45, "open_rate": 3.651e-05, "close_rate": 3.2859000000000005e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2738.9756231169545, "profit_abs": -0.01047499999999997}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 03:55:00+00:00", "close_date": "2018-01-11 04:25:00+00:00", "trade_duration": 30, "open_rate": 0.08824105, "close_rate": 0.08956798308270676, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1332594070446804, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 04:00:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 50, "open_rate": 0.00243, "close_rate": 0.002442180451127819, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 41.1522633744856, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:55:00+00:00", "trade_duration": 25, "open_rate": 0.04545064, "close_rate": 0.046589753784461146, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.200189040242338, "profit_abs": 0.001999999999999988}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:30:00+00:00", "close_date": "2018-01-11 04:50:00+00:00", "trade_duration": 20, "open_rate": 3.372e-05, "close_rate": 3.456511278195488e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2965.599051008304, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 04:55:00+00:00", "close_date": "2018-01-11 05:15:00+00:00", "trade_duration": 20, "open_rate": 0.02644, "close_rate": 0.02710265664160401, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7821482602118004, "profit_abs": 0.001999999999999988}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:20:00+00:00", "close_date": "2018-01-11 12:00:00+00:00", "trade_duration": 40, "open_rate": 0.08812, "close_rate": 0.08856170426065162, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1348161597821154, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 11:35:00+00:00", "close_date": "2018-01-11 12:15:00+00:00", "trade_duration": 40, "open_rate": 0.02683577, "close_rate": 0.026970285137844607, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.7263696923919087, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-11 14:00:00+00:00", "close_date": "2018-01-11 14:25:00+00:00", "trade_duration": 25, "open_rate": 4.919e-05, "close_rate": 5.04228320802005e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.9335230737956, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 19:25:00+00:00", "close_date": "2018-01-11 20:35:00+00:00", "trade_duration": 70, "open_rate": 0.08784896, "close_rate": 0.08828930566416039, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1383174029607181, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:35:00+00:00", "close_date": "2018-01-11 23:30:00+00:00", "trade_duration": 55, "open_rate": 5.105e-05, "close_rate": 5.130588972431077e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1958.8638589618022, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:25:00+00:00", "trade_duration": 30, "open_rate": 3.96e-05, "close_rate": 4.019548872180451e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2525.252525252525, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 22:55:00+00:00", "close_date": "2018-01-11 23:35:00+00:00", "trade_duration": 40, "open_rate": 2.885e-05, "close_rate": 2.899461152882205e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3466.204506065858, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-11 23:30:00+00:00", "close_date": "2018-01-12 00:05:00+00:00", "trade_duration": 35, "open_rate": 0.02645, "close_rate": 0.026847744360902256, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.780718336483932, "profit_abs": 0.0010000000000000148}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-11 23:55:00+00:00", "close_date": "2018-01-12 01:15:00+00:00", "trade_duration": 80, "open_rate": 0.048, "close_rate": 0.04824060150375939, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0833333333333335, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-12 21:15:00+00:00", "close_date": "2018-01-12 21:40:00+00:00", "trade_duration": 25, "open_rate": 4.692e-05, "close_rate": 4.809593984962405e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2131.287297527707, "profit_abs": 0.001999999999999974}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 00:55:00+00:00", "close_date": "2018-01-13 06:20:00+00:00", "trade_duration": 325, "open_rate": 0.00256966, "close_rate": 0.0025825405012531327, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.91565421106294, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 10:55:00+00:00", "close_date": "2018-01-13 11:35:00+00:00", "trade_duration": 40, "open_rate": 6.262e-05, "close_rate": 6.293388471177944e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1596.933886937081, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-13 13:05:00+00:00", "close_date": "2018-01-15 14:10:00+00:00", "trade_duration": 2945, "open_rate": 4.73e-05, "close_rate": 4.753709273182957e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2114.1649048625795, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:30:00+00:00", "close_date": "2018-01-13 14:45:00+00:00", "trade_duration": 75, "open_rate": 6.063e-05, "close_rate": 6.0933909774436085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1649.348507339601, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 13:40:00+00:00", "close_date": "2018-01-13 23:30:00+00:00", "trade_duration": 590, "open_rate": 0.00011082, "close_rate": 0.00011137548872180448, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 902.3641941887746, "profit_abs": -2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 15:15:00+00:00", "close_date": "2018-01-13 15:55:00+00:00", "trade_duration": 40, "open_rate": 5.93e-05, "close_rate": 5.9597243107769415e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1686.3406408094436, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 16:30:00+00:00", "close_date": "2018-01-13 17:10:00+00:00", "trade_duration": 40, "open_rate": 0.04850003, "close_rate": 0.04874313791979949, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.0618543947292407, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-13 22:05:00+00:00", "close_date": "2018-01-14 06:25:00+00:00", "trade_duration": 500, "open_rate": 0.09825019, "close_rate": 0.09874267215538848, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0178097365511456, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 00:20:00+00:00", "close_date": "2018-01-14 22:55:00+00:00", "trade_duration": 1355, "open_rate": 6.018e-05, "close_rate": 6.048165413533834e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1661.681621801263, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 12:45:00+00:00", "close_date": "2018-01-14 13:25:00+00:00", "trade_duration": 40, "open_rate": 0.09758999, "close_rate": 0.0980791628822055, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.024695258191952, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-14 15:30:00+00:00", "close_date": "2018-01-14 16:00:00+00:00", "trade_duration": 30, "open_rate": 0.00311, "close_rate": 0.0031567669172932328, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.154340836012864, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 20:45:00+00:00", "close_date": "2018-01-14 22:15:00+00:00", "trade_duration": 90, "open_rate": 0.00312401, "close_rate": 0.003139669197994987, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 32.010140812609436, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-14 23:35:00+00:00", "close_date": "2018-01-15 00:30:00+00:00", "trade_duration": 55, "open_rate": 0.0174679, "close_rate": 0.017555458395989976, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 5.724786608579165, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-14 23:45:00+00:00", "close_date": "2018-01-15 00:25:00+00:00", "trade_duration": 40, "open_rate": 0.07346846, "close_rate": 0.07383672295739348, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.3611282991367997, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 02:25:00+00:00", "close_date": "2018-01-15 03:05:00+00:00", "trade_duration": 40, "open_rate": 0.097994, "close_rate": 0.09848519799498744, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.020470641059657, "profit_abs": -2.7755575615628914e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 07:20:00+00:00", "close_date": "2018-01-15 08:00:00+00:00", "trade_duration": 40, "open_rate": 0.09659, "close_rate": 0.09707416040100247, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0353038616834043, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-15 08:20:00+00:00", "close_date": "2018-01-15 08:55:00+00:00", "trade_duration": 35, "open_rate": 9.987e-05, "close_rate": 0.00010137180451127818, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1001.3016921998599, "profit_abs": 0.0010000000000000009}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-15 12:10:00+00:00", "close_date": "2018-01-16 02:50:00+00:00", "trade_duration": 880, "open_rate": 0.0948969, "close_rate": 0.09537257368421052, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0537752023511833, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:10:00+00:00", "close_date": "2018-01-15 17:40:00+00:00", "trade_duration": 210, "open_rate": 0.071, "close_rate": 0.07135588972431077, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4084507042253522, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 14:30:00+00:00", "close_date": "2018-01-15 15:10:00+00:00", "trade_duration": 40, "open_rate": 0.04600501, "close_rate": 0.046235611553884705, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.173676301776698, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:10:00+00:00", "close_date": "2018-01-15 19:25:00+00:00", "trade_duration": 75, "open_rate": 9.438e-05, "close_rate": 9.485308270676693e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1059.5465140919687, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 18:35:00+00:00", "close_date": "2018-01-15 19:15:00+00:00", "trade_duration": 40, "open_rate": 0.03040001, "close_rate": 0.030552391002506264, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.2894726021471703, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-15 20:25:00+00:00", "close_date": "2018-01-16 08:25:00+00:00", "trade_duration": 720, "open_rate": 5.837e-05, "close_rate": 5.2533e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1713.2088401576154, "profit_abs": -0.010474999999999984}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-15 20:40:00+00:00", "close_date": "2018-01-15 22:00:00+00:00", "trade_duration": 80, "open_rate": 0.046036, "close_rate": 0.04626675689223057, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.1722130506560084, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 00:30:00+00:00", "close_date": "2018-01-16 01:10:00+00:00", "trade_duration": 40, "open_rate": 0.0028685, "close_rate": 0.0028828784461152877, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 34.86142583231654, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 01:15:00+00:00", "close_date": "2018-01-16 02:35:00+00:00", "trade_duration": 80, "open_rate": 0.06731755, "close_rate": 0.0676549813283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4854967241083492, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 07:45:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 55, "open_rate": 0.09217614, "close_rate": 0.09263817578947368, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0848794492804754, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:55:00+00:00", "trade_duration": 20, "open_rate": 0.0165, "close_rate": 0.016913533834586467, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.0606060606060606, "profit_abs": 0.0020000000000000018}, {"pair": "TRX/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 08:35:00+00:00", "close_date": "2018-01-16 08:40:00+00:00", "trade_duration": 5, "open_rate": 7.953e-05, "close_rate": 8.311781954887218e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1257.387149503332, "profit_abs": 0.00399999999999999}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 08:45:00+00:00", "close_date": "2018-01-16 09:50:00+00:00", "trade_duration": 65, "open_rate": 0.045202, "close_rate": 0.04542857644110275, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2122914915269236, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:45:00+00:00", "trade_duration": 30, "open_rate": 5.248e-05, "close_rate": 5.326917293233082e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1905.487804878049, "profit_abs": 0.0010000000000000009}, {"pair": "XMR/BTC", "profit_percent": 0.0, "open_date": "2018-01-16 09:15:00+00:00", "close_date": "2018-01-16 09:55:00+00:00", "trade_duration": 40, "open_rate": 0.02892318, "close_rate": 0.02906815834586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.457434486802627, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 09:50:00+00:00", "close_date": "2018-01-16 10:10:00+00:00", "trade_duration": 20, "open_rate": 5.158e-05, "close_rate": 5.287273182957392e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1938.735944164405, "profit_abs": 0.001999999999999988}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:35:00+00:00", "trade_duration": 30, "open_rate": 0.02828232, "close_rate": 0.02870761804511278, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5357778286929786, "profit_abs": 0.0010000000000000009}, {"pair": "ZEC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 10:05:00+00:00", "close_date": "2018-01-16 10:40:00+00:00", "trade_duration": 35, "open_rate": 0.04357584, "close_rate": 0.044231115789473675, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.294849623093898, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 13:45:00+00:00", "close_date": "2018-01-16 14:20:00+00:00", "trade_duration": 35, "open_rate": 5.362e-05, "close_rate": 5.442631578947368e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1864.975755315181, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-16 17:30:00+00:00", "close_date": "2018-01-16 18:25:00+00:00", "trade_duration": 55, "open_rate": 5.302e-05, "close_rate": 5.328576441102756e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.0807242549984, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:45:00+00:00", "trade_duration": 30, "open_rate": 0.09129999, "close_rate": 0.09267292218045112, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0952903718828448, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 18:15:00+00:00", "close_date": "2018-01-16 18:35:00+00:00", "trade_duration": 20, "open_rate": 3.808e-05, "close_rate": 3.903438596491228e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2626.0504201680674, "profit_abs": 0.0020000000000000018}, {"pair": "XMR/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-16 19:00:00+00:00", "close_date": "2018-01-16 19:30:00+00:00", "trade_duration": 30, "open_rate": 0.02811012, "close_rate": 0.028532828571428567, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.557437677249333, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 60, "open_rate": 0.00258379, "close_rate": 0.002325411, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.702835756775904, "profit_abs": -0.010474999999999984}, {"pair": "NXT/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:25:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 80, "open_rate": 2.559e-05, "close_rate": 2.3031e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3907.7764751856193, "profit_abs": -0.010474999999999998}, {"pair": "TRX/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-16 21:35:00+00:00", "close_date": "2018-01-16 22:25:00+00:00", "trade_duration": 50, "open_rate": 7.62e-05, "close_rate": 6.858e-05, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1312.3359580052495, "profit_abs": -0.010474999999999984}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:35:00+00:00", "trade_duration": 5, "open_rate": 0.00229844, "close_rate": 0.002402129022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 43.507770487809125, "profit_abs": 0.004000000000000017}, {"pair": "LTC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:30:00+00:00", "close_date": "2018-01-16 22:40:00+00:00", "trade_duration": 10, "open_rate": 0.0151, "close_rate": 0.015781203007518795, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.622516556291391, "profit_abs": 0.00399999999999999}, {"pair": "ETC/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:40:00+00:00", "close_date": "2018-01-16 22:45:00+00:00", "trade_duration": 5, "open_rate": 0.00235676, "close_rate": 0.00246308, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 42.431134269081284, "profit_abs": 0.0040000000000000036}, {"pair": "DASH/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-16 22:45:00+00:00", "close_date": "2018-01-16 23:05:00+00:00", "trade_duration": 20, "open_rate": 0.0630692, "close_rate": 0.06464988170426066, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.585559988076589, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-16 22:50:00+00:00", "close_date": "2018-01-16 22:55:00+00:00", "trade_duration": 5, "open_rate": 2.2e-05, "close_rate": 2.299248120300751e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 4545.454545454546, "profit_abs": 0.003999999999999976}, {"pair": "ADA/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-17 03:30:00+00:00", "close_date": "2018-01-17 04:00:00+00:00", "trade_duration": 30, "open_rate": 4.974e-05, "close_rate": 5.048796992481203e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2010.454362685967, "profit_abs": 0.0010000000000000009}, {"pair": "TRX/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-17 03:55:00+00:00", "close_date": "2018-01-17 04:15:00+00:00", "trade_duration": 20, "open_rate": 7.108e-05, "close_rate": 7.28614536340852e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1406.8655036578502, "profit_abs": 0.001999999999999974}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 09:35:00+00:00", "close_date": "2018-01-17 10:15:00+00:00", "trade_duration": 40, "open_rate": 0.04327, "close_rate": 0.04348689223057644, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.3110700254217704, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:20:00+00:00", "close_date": "2018-01-17 17:00:00+00:00", "trade_duration": 400, "open_rate": 4.997e-05, "close_rate": 5.022047619047618e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2001.2007204322595, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:25:00+00:00", "trade_duration": 55, "open_rate": 0.06836818, "close_rate": 0.06871087764411027, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4626687444363737, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 10:30:00+00:00", "close_date": "2018-01-17 11:10:00+00:00", "trade_duration": 40, "open_rate": 3.63e-05, "close_rate": 3.648195488721804e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2754.8209366391184, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:30:00+00:00", "close_date": "2018-01-17 22:05:00+00:00", "trade_duration": 575, "open_rate": 0.0281, "close_rate": 0.02824085213032581, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5587188612099645, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-17 12:35:00+00:00", "close_date": "2018-01-17 16:55:00+00:00", "trade_duration": 260, "open_rate": 0.08651001, "close_rate": 0.08694364413533832, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1559355963546878, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 05:00:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 55, "open_rate": 5.633e-05, "close_rate": 5.6612355889724306e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1775.2529735487308, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 05:20:00+00:00", "close_date": "2018-01-18 05:55:00+00:00", "trade_duration": 35, "open_rate": 0.06988494, "close_rate": 0.07093584135338346, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.430923457900944, "profit_abs": 0.0010000000000000009}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 07:35:00+00:00", "close_date": "2018-01-18 08:15:00+00:00", "trade_duration": 40, "open_rate": 5.545e-05, "close_rate": 5.572794486215538e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1803.4265103697026, "profit_abs": -1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 09:00:00+00:00", "close_date": "2018-01-18 09:40:00+00:00", "trade_duration": 40, "open_rate": 0.01633527, "close_rate": 0.016417151052631574, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.121723118136401, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 16:40:00+00:00", "close_date": "2018-01-18 17:20:00+00:00", "trade_duration": 40, "open_rate": 0.00269734, "close_rate": 0.002710860501253133, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.073561360451414, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-18 18:05:00+00:00", "close_date": "2018-01-18 18:30:00+00:00", "trade_duration": 25, "open_rate": 4.475e-05, "close_rate": 4.587155388471177e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2234.63687150838, "profit_abs": 0.0020000000000000018}, {"pair": "NXT/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-18 18:25:00+00:00", "close_date": "2018-01-18 18:55:00+00:00", "trade_duration": 30, "open_rate": 2.79e-05, "close_rate": 2.8319548872180444e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3584.2293906810037, "profit_abs": 0.000999999999999987}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 20:10:00+00:00", "close_date": "2018-01-18 20:50:00+00:00", "trade_duration": 40, "open_rate": 0.04439326, "close_rate": 0.04461578260651629, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.2525942001105577, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 21:30:00+00:00", "close_date": "2018-01-19 00:35:00+00:00", "trade_duration": 185, "open_rate": 4.49e-05, "close_rate": 4.51250626566416e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2227.1714922049, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-18 21:55:00+00:00", "close_date": "2018-01-19 05:05:00+00:00", "trade_duration": 430, "open_rate": 0.02855, "close_rate": 0.028693107769423555, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.502626970227671, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 22:10:00+00:00", "close_date": "2018-01-18 22:50:00+00:00", "trade_duration": 40, "open_rate": 5.796e-05, "close_rate": 5.8250526315789473e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1725.3278122843342, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-18 23:50:00+00:00", "close_date": "2018-01-19 00:30:00+00:00", "trade_duration": 40, "open_rate": 0.04340323, "close_rate": 0.04362079005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.303975994413319, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-19 16:45:00+00:00", "close_date": "2018-01-19 17:35:00+00:00", "trade_duration": 50, "open_rate": 0.04454455, "close_rate": 0.04476783095238095, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.244943545282195, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:15:00+00:00", "close_date": "2018-01-19 19:55:00+00:00", "trade_duration": 160, "open_rate": 5.62e-05, "close_rate": 5.648170426065162e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1779.3594306049824, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-19 17:20:00+00:00", "close_date": "2018-01-19 20:15:00+00:00", "trade_duration": 175, "open_rate": 4.339e-05, "close_rate": 4.360749373433584e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2304.6784973496196, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-20 04:45:00+00:00", "close_date": "2018-01-20 17:35:00+00:00", "trade_duration": 770, "open_rate": 0.0001009, "close_rate": 0.00010140576441102755, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 991.0802775024778, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 15:15:00+00:00", "trade_duration": 625, "open_rate": 0.00270505, "close_rate": 0.002718609147869674, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.96789338459548, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 04:50:00+00:00", "close_date": "2018-01-20 07:00:00+00:00", "trade_duration": 130, "open_rate": 0.03000002, "close_rate": 0.030150396040100245, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.3333311111125927, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 09:00:00+00:00", "close_date": "2018-01-20 09:40:00+00:00", "trade_duration": 40, "open_rate": 5.46e-05, "close_rate": 5.4873684210526304e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1831.5018315018317, "profit_abs": -1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.10448878, "open_date": "2018-01-20 18:25:00+00:00", "close_date": "2018-01-25 03:50:00+00:00", "trade_duration": 6325, "open_rate": 0.03082222, "close_rate": 0.027739998, "open_at_end": false, "sell_reason": "stop_loss", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.244412634781012, "profit_abs": -0.010474999999999998}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-20 22:25:00+00:00", "close_date": "2018-01-20 23:15:00+00:00", "trade_duration": 50, "open_rate": 0.08969999, "close_rate": 0.09014961401002504, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1148273260677064, "profit_abs": 0.0}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 02:50:00+00:00", "close_date": "2018-01-21 14:30:00+00:00", "trade_duration": 700, "open_rate": 0.01632501, "close_rate": 0.01640683962406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.125570520324337, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 10:20:00+00:00", "close_date": "2018-01-21 11:00:00+00:00", "trade_duration": 40, "open_rate": 0.070538, "close_rate": 0.07089157393483708, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.417675579120474, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 15:50:00+00:00", "close_date": "2018-01-21 18:45:00+00:00", "trade_duration": 175, "open_rate": 5.301e-05, "close_rate": 5.327571428571427e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1886.4365214110546, "profit_abs": -2.7755575615628914e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-21 16:20:00+00:00", "close_date": "2018-01-21 17:00:00+00:00", "trade_duration": 40, "open_rate": 3.955e-05, "close_rate": 3.9748245614035085e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2528.4450063211125, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:45:00+00:00", "trade_duration": 30, "open_rate": 0.00258505, "close_rate": 0.002623922932330827, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.6839712964933, "profit_abs": 0.0010000000000000009}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-21 21:15:00+00:00", "close_date": "2018-01-21 21:55:00+00:00", "trade_duration": 40, "open_rate": 3.903e-05, "close_rate": 3.922563909774435e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2562.1316935690497, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 00:35:00+00:00", "close_date": "2018-01-22 10:35:00+00:00", "trade_duration": 600, "open_rate": 5.236e-05, "close_rate": 5.262245614035087e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1909.8548510313217, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 01:30:00+00:00", "close_date": "2018-01-22 02:10:00+00:00", "trade_duration": 40, "open_rate": 9.028e-05, "close_rate": 9.07325313283208e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1107.6650420912717, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 12:25:00+00:00", "close_date": "2018-01-22 14:35:00+00:00", "trade_duration": 130, "open_rate": 0.002687, "close_rate": 0.002700468671679198, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 37.21622627465575, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 13:15:00+00:00", "close_date": "2018-01-22 13:55:00+00:00", "trade_duration": 40, "open_rate": 4.168e-05, "close_rate": 4.188892230576441e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2399.232245681382, "profit_abs": 1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-22 14:00:00+00:00", "close_date": "2018-01-22 14:30:00+00:00", "trade_duration": 30, "open_rate": 8.821e-05, "close_rate": 8.953646616541353e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1133.6583153837435, "profit_abs": 0.0010000000000000148}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-22 15:55:00+00:00", "close_date": "2018-01-22 16:40:00+00:00", "trade_duration": 45, "open_rate": 5.172e-05, "close_rate": 5.1979248120300745e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1933.4880123743235, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-22 16:05:00+00:00", "close_date": "2018-01-22 16:25:00+00:00", "trade_duration": 20, "open_rate": 3.026e-05, "close_rate": 3.101839598997494e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3304.692663582287, "profit_abs": 0.0020000000000000157}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 19:50:00+00:00", "close_date": "2018-01-23 00:10:00+00:00", "trade_duration": 260, "open_rate": 0.07064, "close_rate": 0.07099408521303258, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.415628539071348, "profit_abs": 1.3877787807814457e-17}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-22 21:25:00+00:00", "close_date": "2018-01-22 22:05:00+00:00", "trade_duration": 40, "open_rate": 0.01644483, "close_rate": 0.01652726022556391, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.080938507725528, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-23 00:05:00+00:00", "close_date": "2018-01-23 00:35:00+00:00", "trade_duration": 30, "open_rate": 4.331e-05, "close_rate": 4.3961278195488714e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2308.935580697299, "profit_abs": 0.0010000000000000148}, {"pair": "NXT/BTC", "profit_percent": 0.01995012, "open_date": "2018-01-23 01:50:00+00:00", "close_date": "2018-01-23 02:15:00+00:00", "trade_duration": 25, "open_rate": 3.2e-05, "close_rate": 3.2802005012531326e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3125.0000000000005, "profit_abs": 0.0020000000000000018}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 04:25:00+00:00", "close_date": "2018-01-23 05:15:00+00:00", "trade_duration": 50, "open_rate": 0.09167706, "close_rate": 0.09213659413533835, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0907854156754153, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 07:35:00+00:00", "close_date": "2018-01-23 09:00:00+00:00", "trade_duration": 85, "open_rate": 0.0692498, "close_rate": 0.06959691679197995, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4440474918339115, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 10:50:00+00:00", "close_date": "2018-01-23 13:05:00+00:00", "trade_duration": 135, "open_rate": 3.182e-05, "close_rate": 3.197949874686716e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3142.677561282213, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 11:05:00+00:00", "close_date": "2018-01-23 16:05:00+00:00", "trade_duration": 300, "open_rate": 0.04088, "close_rate": 0.04108491228070175, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4461839530332683, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 14:55:00+00:00", "close_date": "2018-01-23 15:35:00+00:00", "trade_duration": 40, "open_rate": 5.15e-05, "close_rate": 5.175814536340851e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1941.747572815534, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-23 16:35:00+00:00", "close_date": "2018-01-24 00:05:00+00:00", "trade_duration": 450, "open_rate": 0.09071698, "close_rate": 0.09117170170426064, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.1023294646713329, "profit_abs": 0.0}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 17:25:00+00:00", "close_date": "2018-01-23 18:45:00+00:00", "trade_duration": 80, "open_rate": 3.128e-05, "close_rate": 3.1436791979949865e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3196.9309462915603, "profit_abs": -2.7755575615628914e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 20:15:00+00:00", "close_date": "2018-01-23 22:00:00+00:00", "trade_duration": 105, "open_rate": 9.555e-05, "close_rate": 9.602894736842104e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1046.5724751439038, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 22:30:00+00:00", "close_date": "2018-01-23 23:10:00+00:00", "trade_duration": 40, "open_rate": 0.04080001, "close_rate": 0.0410045213283208, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.450979791426522, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-23 23:50:00+00:00", "close_date": "2018-01-24 03:35:00+00:00", "trade_duration": 225, "open_rate": 5.163e-05, "close_rate": 5.18887969924812e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1936.8584156498162, "profit_abs": 1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 00:20:00+00:00", "close_date": "2018-01-24 01:50:00+00:00", "trade_duration": 90, "open_rate": 0.04040781, "close_rate": 0.04061035541353383, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.474769110228938, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 06:45:00+00:00", "close_date": "2018-01-24 07:25:00+00:00", "trade_duration": 40, "open_rate": 5.132e-05, "close_rate": 5.157724310776942e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1948.5580670303975, "profit_abs": 0.0}, {"pair": "ADA/BTC", "profit_percent": 0.03990025, "open_date": "2018-01-24 14:15:00+00:00", "close_date": "2018-01-24 14:25:00+00:00", "trade_duration": 10, "open_rate": 5.198e-05, "close_rate": 5.432496240601503e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1923.8168526356292, "profit_abs": 0.0040000000000000036}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 14:50:00+00:00", "close_date": "2018-01-24 16:35:00+00:00", "trade_duration": 105, "open_rate": 3.054e-05, "close_rate": 3.069308270676692e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3274.3942370661425, "profit_abs": 0.0}, {"pair": "TRX/BTC", "profit_percent": 0.0, "open_date": "2018-01-24 15:10:00+00:00", "close_date": "2018-01-24 16:15:00+00:00", "trade_duration": 65, "open_rate": 9.263e-05, "close_rate": 9.309431077694236e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1079.5638562020945, "profit_abs": 2.7755575615628914e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-24 22:40:00+00:00", "close_date": "2018-01-24 23:25:00+00:00", "trade_duration": 45, "open_rate": 5.514e-05, "close_rate": 5.54163909774436e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1813.5654697134569, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 00:50:00+00:00", "close_date": "2018-01-25 01:30:00+00:00", "trade_duration": 40, "open_rate": 4.921e-05, "close_rate": 4.9456666666666664e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2032.1072952651903, "profit_abs": 1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-25 08:15:00+00:00", "close_date": "2018-01-25 12:15:00+00:00", "trade_duration": 240, "open_rate": 0.0026, "close_rate": 0.002613032581453634, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.46153846153847, "profit_abs": 1.3877787807814457e-17}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 10:25:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 350, "open_rate": 0.02799871, "close_rate": 0.028139054411027563, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.571593119825878, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 11:00:00+00:00", "close_date": "2018-01-25 11:45:00+00:00", "trade_duration": 45, "open_rate": 0.04078902, "close_rate": 0.0409934762406015, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4516401717913303, "profit_abs": -1.3877787807814457e-17}, {"pair": "NXT/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:05:00+00:00", "close_date": "2018-01-25 13:45:00+00:00", "trade_duration": 40, "open_rate": 2.89e-05, "close_rate": 2.904486215538847e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3460.2076124567475, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 13:20:00+00:00", "close_date": "2018-01-25 14:05:00+00:00", "trade_duration": 45, "open_rate": 0.041103, "close_rate": 0.04130903007518797, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4329124394813033, "profit_abs": 1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-25 15:45:00+00:00", "close_date": "2018-01-25 16:15:00+00:00", "trade_duration": 30, "open_rate": 5.428e-05, "close_rate": 5.509624060150376e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1842.2991893883568, "profit_abs": 0.0010000000000000148}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 17:45:00+00:00", "close_date": "2018-01-25 23:15:00+00:00", "trade_duration": 330, "open_rate": 5.414e-05, "close_rate": 5.441137844611528e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1847.063169560399, "profit_abs": -1.3877787807814457e-17}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-25 21:15:00+00:00", "close_date": "2018-01-25 21:55:00+00:00", "trade_duration": 40, "open_rate": 0.04140777, "close_rate": 0.0416153277443609, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.415005686130888, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 02:05:00+00:00", "close_date": "2018-01-26 02:45:00+00:00", "trade_duration": 40, "open_rate": 0.00254309, "close_rate": 0.002555837318295739, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.32224183965177, "profit_abs": 1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 02:55:00+00:00", "close_date": "2018-01-26 15:10:00+00:00", "trade_duration": 735, "open_rate": 5.607e-05, "close_rate": 5.6351052631578935e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1783.4849295523454, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETC/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 06:10:00+00:00", "close_date": "2018-01-26 09:25:00+00:00", "trade_duration": 195, "open_rate": 0.00253806, "close_rate": 0.0025507821052631577, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 39.400171784748984, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 07:25:00+00:00", "close_date": "2018-01-26 09:55:00+00:00", "trade_duration": 150, "open_rate": 0.0415, "close_rate": 0.04170802005012531, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4096385542168677, "profit_abs": 0.0}, {"pair": "XLM/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-26 09:55:00+00:00", "close_date": "2018-01-26 10:25:00+00:00", "trade_duration": 30, "open_rate": 5.321e-05, "close_rate": 5.401015037593984e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1879.3459875963165, "profit_abs": 0.000999999999999987}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-26 16:05:00+00:00", "close_date": "2018-01-26 16:45:00+00:00", "trade_duration": 40, "open_rate": 0.02772046, "close_rate": 0.02785940967418546, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.6074437437185387, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": 0.0, "open_date": "2018-01-26 23:35:00+00:00", "close_date": "2018-01-27 00:15:00+00:00", "trade_duration": 40, "open_rate": 0.09461341, "close_rate": 0.09508766268170424, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0569326272036914, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 00:35:00+00:00", "close_date": "2018-01-27 01:30:00+00:00", "trade_duration": 55, "open_rate": 5.615e-05, "close_rate": 5.643145363408521e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1780.9439002671415, "profit_abs": -1.3877787807814457e-17}, {"pair": "ADA/BTC", "profit_percent": -0.07877175, "open_date": "2018-01-27 00:45:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 4560, "open_rate": 5.556e-05, "close_rate": 5.144e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1799.8560115190785, "profit_abs": -0.007896868250539965}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 02:30:00+00:00", "close_date": "2018-01-27 11:25:00+00:00", "trade_duration": 535, "open_rate": 0.06900001, "close_rate": 0.06934587471177944, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492751522789635, "profit_abs": 0.0}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 06:25:00+00:00", "close_date": "2018-01-27 07:05:00+00:00", "trade_duration": 40, "open_rate": 0.09449985, "close_rate": 0.0949735334586466, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.058202737887944, "profit_abs": 0.0}, {"pair": "ZEC/BTC", "profit_percent": -0.04815133, "open_date": "2018-01-27 09:40:00+00:00", "close_date": "2018-01-30 04:40:00+00:00", "trade_duration": 4020, "open_rate": 0.0410697, "close_rate": 0.03928809, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 2.4348850855983852, "profit_abs": -0.004827170578309559}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 11:45:00+00:00", "close_date": "2018-01-27 12:30:00+00:00", "trade_duration": 45, "open_rate": 0.0285, "close_rate": 0.02864285714285714, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.5087719298245617, "profit_abs": 0.0}, {"pair": "XMR/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 12:35:00+00:00", "close_date": "2018-01-27 15:25:00+00:00", "trade_duration": 170, "open_rate": 0.02866372, "close_rate": 0.02880739779448621, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 3.4887307020861216, "profit_abs": -1.3877787807814457e-17}, {"pair": "ETH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 15:50:00+00:00", "close_date": "2018-01-27 16:50:00+00:00", "trade_duration": 60, "open_rate": 0.095381, "close_rate": 0.09585910025062656, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.0484268355332824, "profit_abs": 1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 17:05:00+00:00", "close_date": "2018-01-27 17:45:00+00:00", "trade_duration": 40, "open_rate": 0.06759092, "close_rate": 0.06792972160401002, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4794886650455417, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": -0.0, "open_date": "2018-01-27 23:40:00+00:00", "close_date": "2018-01-28 01:05:00+00:00", "trade_duration": 85, "open_rate": 0.00258501, "close_rate": 0.002597967443609022, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 38.684569885609726, "profit_abs": -1.3877787807814457e-17}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 02:25:00+00:00", "close_date": "2018-01-28 08:10:00+00:00", "trade_duration": 345, "open_rate": 0.06698502, "close_rate": 0.0673207845112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4928710926711672, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-28 10:25:00+00:00", "close_date": "2018-01-28 16:30:00+00:00", "trade_duration": 365, "open_rate": 0.0677177, "close_rate": 0.06805713709273183, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4767187899175547, "profit_abs": -1.3877787807814457e-17}, {"pair": "XLM/BTC", "profit_percent": 0.0, "open_date": "2018-01-28 20:35:00+00:00", "close_date": "2018-01-28 21:35:00+00:00", "trade_duration": 60, "open_rate": 5.215e-05, "close_rate": 5.2411403508771925e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1917.5455417066157, "profit_abs": 0.0}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-28 22:00:00+00:00", "close_date": "2018-01-28 22:30:00+00:00", "trade_duration": 30, "open_rate": 0.00273809, "close_rate": 0.002779264285714285, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.5218089982433, "profit_abs": 0.0010000000000000009}, {"pair": "ETC/BTC", "profit_percent": 0.00997506, "open_date": "2018-01-29 00:00:00+00:00", "close_date": "2018-01-29 00:30:00+00:00", "trade_duration": 30, "open_rate": 0.00274632, "close_rate": 0.002787618045112782, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 36.412362725392526, "profit_abs": 0.0010000000000000148}, {"pair": "LTC/BTC", "profit_percent": 0.0, "open_date": "2018-01-29 02:15:00+00:00", "close_date": "2018-01-29 03:00:00+00:00", "trade_duration": 45, "open_rate": 0.01622478, "close_rate": 0.016306107218045113, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 6.163411768911504, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 03:05:00+00:00", "close_date": "2018-01-29 03:45:00+00:00", "trade_duration": 40, "open_rate": 0.069, "close_rate": 0.06934586466165413, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4492753623188406, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 05:20:00+00:00", "close_date": "2018-01-29 06:55:00+00:00", "trade_duration": 95, "open_rate": 8.755e-05, "close_rate": 8.798884711779448e-05, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1142.204454597373, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 07:00:00+00:00", "close_date": "2018-01-29 19:25:00+00:00", "trade_duration": 745, "open_rate": 0.06825763, "close_rate": 0.06859977350877192, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4650376815016872, "profit_abs": 0.0}, {"pair": "DASH/BTC", "profit_percent": -0.0, "open_date": "2018-01-29 19:45:00+00:00", "close_date": "2018-01-29 20:25:00+00:00", "trade_duration": 40, "open_rate": 0.06713892, "close_rate": 0.06747545593984962, "open_at_end": false, "sell_reason": "roi", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1.4894490408841845, "profit_abs": -1.3877787807814457e-17}, {"pair": "TRX/BTC", "profit_percent": -0.0199116, "open_date": "2018-01-29 23:30:00+00:00", "close_date": "2018-01-30 04:45:00+00:00", "trade_duration": 315, "open_rate": 8.934e-05, "close_rate": 8.8e-05, "open_at_end": true, "sell_reason": "force_sell", "open_fee": 0.0025, "close_fee": 0.0025, "amount": 1119.3194537721067, "profit_abs": -0.0019961383478844796}], "results_per_pair": [{"key": "TRX/BTC", "trades": 15, "profit_mean": 0.0023467073333333323, "profit_mean_pct": 0.23467073333333321, "profit_sum": 0.035200609999999986, "profit_sum_pct": 3.5200609999999988, "profit_total_abs": 0.0035288616521155086, "profit_total_pct": 1.1733536666666662, "duration_avg": "2:28:00", "wins": 9, "draws": 2, "losses": 4}, {"key": "ADA/BTC", "trades": 29, "profit_mean": -0.0011598141379310352, "profit_mean_pct": -0.11598141379310352, "profit_sum": -0.03363461000000002, "profit_sum_pct": -3.3634610000000023, "profit_total_abs": -0.0033718682505400333, "profit_total_pct": -1.1211536666666675, "duration_avg": "5:35:00", "wins": 9, "draws": 11, "losses": 9}, {"key": "XLM/BTC", "trades": 21, "profit_mean": 0.0026243899999999994, "profit_mean_pct": 0.2624389999999999, "profit_sum": 0.05511218999999999, "profit_sum_pct": 5.511218999999999, "profit_total_abs": 0.005525000000000002, "profit_total_pct": 1.8370729999999995, "duration_avg": "3:21:00", "wins": 12, "draws": 3, "losses": 6}, {"key": "ETH/BTC", "trades": 21, "profit_mean": 0.0009500057142857142, "profit_mean_pct": 0.09500057142857142, "profit_sum": 0.01995012, "profit_sum_pct": 1.9950119999999998, "profit_total_abs": 0.0019999999999999463, "profit_total_pct": 0.6650039999999999, "duration_avg": "2:17:00", "wins": 5, "draws": 10, "losses": 6}, {"key": "XMR/BTC", "trades": 16, "profit_mean": -0.0027899012500000007, "profit_mean_pct": -0.2789901250000001, "profit_sum": -0.04463842000000001, "profit_sum_pct": -4.463842000000001, "profit_total_abs": -0.0044750000000000345, "profit_total_pct": -1.4879473333333337, "duration_avg": "8:41:00", "wins": 6, "draws": 5, "losses": 5}, {"key": "ZEC/BTC", "trades": 21, "profit_mean": -0.00039290904761904774, "profit_mean_pct": -0.03929090476190478, "profit_sum": -0.008251090000000003, "profit_sum_pct": -0.8251090000000003, "profit_total_abs": -0.000827170578309569, "profit_total_pct": -0.27503633333333344, "duration_avg": "4:17:00", "wins": 8, "draws": 7, "losses": 6}, {"key": "NXT/BTC", "trades": 12, "profit_mean": -0.0012261025000000006, "profit_mean_pct": -0.12261025000000006, "profit_sum": -0.014713230000000008, "profit_sum_pct": -1.4713230000000008, "profit_total_abs": -0.0014750000000000874, "profit_total_pct": -0.4904410000000003, "duration_avg": "0:57:00", "wins": 4, "draws": 3, "losses": 5}, {"key": "LTC/BTC", "trades": 8, "profit_mean": 0.00748129625, "profit_mean_pct": 0.748129625, "profit_sum": 0.05985037, "profit_sum_pct": 5.985037, "profit_total_abs": 0.006000000000000019, "profit_total_pct": 1.9950123333333334, "duration_avg": "1:59:00", "wins": 5, "draws": 2, "losses": 1}, {"key": "ETC/BTC", "trades": 20, "profit_mean": 0.0022568569999999997, "profit_mean_pct": 0.22568569999999996, "profit_sum": 0.04513713999999999, "profit_sum_pct": 4.513713999999999, "profit_total_abs": 0.004525000000000001, "profit_total_pct": 1.504571333333333, "duration_avg": "1:45:00", "wins": 11, "draws": 4, "losses": 5}, {"key": "DASH/BTC", "trades": 16, "profit_mean": 0.0018703237499999997, "profit_mean_pct": 0.18703237499999997, "profit_sum": 0.029925179999999996, "profit_sum_pct": 2.9925179999999996, "profit_total_abs": 0.002999999999999961, "profit_total_pct": 0.9975059999999999, "duration_avg": "3:03:00", "wins": 4, "draws": 7, "losses": 5}, {"key": "TOTAL", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}], "sell_reason_summary": [{"sell_reason": "roi", "trades": 170, "wins": 73, "draws": 54, "losses": 43, "profit_mean": 0.005398268352941177, "profit_mean_pct": 0.54, "profit_sum": 0.91770562, "profit_sum_pct": 91.77, "profit_total_abs": 0.09199999999999964, "profit_pct_total": 30.59}, {"sell_reason": "stop_loss", "trades": 6, "wins": 0, "draws": 0, "losses": 6, "profit_mean": -0.10448878000000002, "profit_mean_pct": -10.45, "profit_sum": -0.6269326800000001, "profit_sum_pct": -62.69, "profit_total_abs": -0.06284999999999992, "profit_pct_total": -20.9}, {"sell_reason": "force_sell", "trades": 3, "wins": 0, "draws": 0, "losses": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.89, "profit_sum": -0.14683468, "profit_sum_pct": -14.68, "profit_total_abs": -0.014720177176734003, "profit_pct_total": -4.89}], "left_open_trades": [{"key": "TRX/BTC", "trades": 1, "profit_mean": -0.0199116, "profit_mean_pct": -1.9911600000000003, "profit_sum": -0.0199116, "profit_sum_pct": -1.9911600000000003, "profit_total_abs": -0.0019961383478844796, "profit_total_pct": -0.6637200000000001, "duration_avg": "5:15:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ADA/BTC", "trades": 1, "profit_mean": -0.07877175, "profit_mean_pct": -7.877175, "profit_sum": -0.07877175, "profit_sum_pct": -7.877175, "profit_total_abs": -0.007896868250539965, "profit_total_pct": -2.625725, "duration_avg": "3 days, 4:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "ZEC/BTC", "trades": 1, "profit_mean": -0.04815133, "profit_mean_pct": -4.815133, "profit_sum": -0.04815133, "profit_sum_pct": -4.815133, "profit_total_abs": -0.004827170578309559, "profit_total_pct": -1.6050443333333335, "duration_avg": "2 days, 19:00:00", "wins": 0, "draws": 0, "losses": 1}, {"key": "TOTAL", "trades": 3, "profit_mean": -0.04894489333333333, "profit_mean_pct": -4.894489333333333, "profit_sum": -0.14683468, "profit_sum_pct": -14.683468, "profit_total_abs": -0.014720177176734003, "profit_total_pct": -4.8944893333333335, "duration_avg": "2 days, 1:25:00", "wins": 0, "draws": 0, "losses": 3}], "total_trades": 179, "backtest_start": "2018-01-30 04:45:00+00:00", "backtest_start_ts": 1517287500, "backtest_end": "2018-01-30 04:45:00+00:00", "backtest_end_ts": 1517287500, "backtest_days": 0, "trades_per_day": null, "market_change": 0.25, "stake_amount": 0.1, "max_drawdown": 0.21142322000000008, "drawdown_start": "2018-01-24 14:25:00+00:00", "drawdown_start_ts": 1516803900.0, "drawdown_end": "2018-01-30 04:45:00+00:00", "drawdown_end_ts": 1517287500.0, "pairlist": ["TRX/BTC", "ADA/BTC", "XLM/BTC", "ETH/BTC", "XMR/BTC", "ZEC/BTC","NXT/BTC", "LTC/BTC", "ETC/BTC", "DASH/BTC"]}}, "strategy_comparison": [{"key": "DefaultStrategy", "trades": 179, "profit_mean": 0.0008041243575418989, "profit_mean_pct": 0.0804124357541899, "profit_sum": 0.1439382599999999, "profit_sum_pct": 14.39382599999999, "profit_total_abs": 0.014429822823265714, "profit_total_pct": 4.797941999999996, "duration_avg": "3:40:00", "wins": 73, "draws": 54, "losses": 52}]} From 2ed808da1f4f45342a0a8fd41b158fb7b0e26b0d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 09:27:19 +0200 Subject: [PATCH 0224/1197] Extract .last_result.json to constant --- freqtrade/constants.py | 2 ++ freqtrade/data/btanalysis.py | 5 +-- freqtrade/optimize/optimize_reports.py | 5 ++- tests/data/test_btanalysis.py | 3 +- tests/optimize/test_optimize_reports.py | 46 ++++++++++++------------- 5 files changed, 32 insertions(+), 29 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index ccb05a60f..1b414adb4 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -33,6 +33,8 @@ DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume'] # it has wide consequences for stored trades files DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost'] +LAST_BT_RESULT_FN = '.last_result.json' + USERPATH_HYPEROPTS = 'hyperopts' USERPATH_STRATEGIES = 'strategies' USERPATH_NOTEBOOKS = 'notebooks' diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 17d3fed14..0ae1809f3 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -10,6 +10,7 @@ import pandas as pd from datetime import timezone from freqtrade import persistence +from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.misc import json_load from freqtrade.persistence import Trade @@ -34,7 +35,7 @@ def get_latest_backtest_filename(directory: Union[Path, str]) -> str: directory = Path(directory) if not directory.is_dir(): raise ValueError(f"Directory '{directory}' does not exist.") - filename = directory / '.last_result.json' + filename = directory / LAST_BT_RESULT_FN if not filename.is_file(): raise ValueError(f"Directory '{directory}' does not seem to contain backtest statistics yet.") @@ -43,7 +44,7 @@ def get_latest_backtest_filename(directory: Union[Path, str]) -> str: data = json_load(file) if 'latest_backtest' not in data: - raise ValueError("Invalid '.last_result.json' format.") + raise ValueError(f"Invalid '{LAST_BT_RESULT_FN}' format.") return data['latest_backtest'] diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index b93e60dca..d1c45bd94 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -7,7 +7,7 @@ from arrow import Arrow from pandas import DataFrame from tabulate import tabulate -from freqtrade.constants import DATETIME_PRINT_FORMAT +from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data.btanalysis import calculate_max_drawdown, calculate_market_change from freqtrade.misc import file_dump_json @@ -21,8 +21,7 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N ).with_suffix(recordfilename.suffix) file_dump_json(filename, stats) - latest_filename = Path.joinpath(recordfilename.parent, - '.last_result.json') + latest_filename = Path.joinpath(recordfilename.parent, LAST_BT_RESULT_FN) file_dump_json(latest_filename, {'latest_backtest': str(filename.name)}) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 63fe26eaa..144dc5162 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -6,6 +6,7 @@ from arrow import Arrow from pandas import DataFrame, DateOffset, Timestamp, to_datetime from freqtrade.configuration import TimeRange +from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, analyze_trade_parallelism, calculate_market_change, @@ -73,7 +74,7 @@ def test_load_backtest_data_new_format(testdatadir): load_backtest_data(str("filename") + "nofile") with pytest.raises(ValueError, match=r"Unknown dataformat."): - load_backtest_data(testdatadir / '.last_result.json') + load_backtest_data(testdatadir / LAST_BT_RESULT_FN) def test_load_backtest_data_multi(testdatadir): diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index f908677d7..2431fa716 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -1,15 +1,15 @@ -from datetime import datetime +import re from pathlib import Path import pandas as pd -import re import pytest from arrow import Arrow from freqtrade.configuration import TimeRange +from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.data import history -from freqtrade.edge import PairInfo from freqtrade.data.btanalysis import get_latest_backtest_filename +from freqtrade.edge import PairInfo from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_edge_table, generate_pair_metrics, @@ -93,25 +93,25 @@ def test_generate_backtest_stats(default_conf, testdatadir): # Above sample had no loosing trade assert strat_stats['max_drawdown'] == 0.0 - results = {'DefStrat': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", - "UNITTEST/BTC", "UNITTEST/BTC"], - "profit_percent": [0.003312, 0.010801, -0.013803, 0.002780], - "profit_abs": [0.000003, 0.000011, -0.000014, 0.000003], - "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, - Arrow(2017, 11, 14, 21, 36, 00).datetime, - Arrow(2017, 11, 14, 22, 12, 00).datetime, - Arrow(2017, 11, 14, 22, 44, 00).datetime], - "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, - Arrow(2017, 11, 14, 22, 10, 00).datetime, - Arrow(2017, 11, 14, 22, 43, 00).datetime, - Arrow(2017, 11, 14, 22, 58, 00).datetime], - "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], - "close_rate": [0.002546, 0.003014, 0.0032903, 0.003217], - "trade_duration": [123, 34, 31, 14], - "open_at_end": [False, False, False, True], - "sell_reason": [SellType.ROI, SellType.STOP_LOSS, - SellType.ROI, SellType.FORCE_SELL] - })} + results = {'DefStrat': pd.DataFrame( + {"pair": ["UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC"], + "profit_percent": [0.003312, 0.010801, -0.013803, 0.002780], + "profit_abs": [0.000003, 0.000011, -0.000014, 0.000003], + "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, + Arrow(2017, 11, 14, 21, 36, 00).datetime, + Arrow(2017, 11, 14, 22, 12, 00).datetime, + Arrow(2017, 11, 14, 22, 44, 00).datetime], + "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, + Arrow(2017, 11, 14, 22, 10, 00).datetime, + Arrow(2017, 11, 14, 22, 43, 00).datetime, + Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], + "close_rate": [0.002546, 0.003014, 0.0032903, 0.003217], + "trade_duration": [123, 34, 31, 14], + "open_at_end": [False, False, False, True], + "sell_reason": [SellType.ROI, SellType.STOP_LOSS, + SellType.ROI, SellType.FORCE_SELL] + })} assert strat_stats['max_drawdown'] == 0.0 assert strat_stats['drawdown_start'] == Arrow.fromtimestamp(0).datetime @@ -122,7 +122,7 @@ def test_generate_backtest_stats(default_conf, testdatadir): # Test storing stats filename = Path(testdatadir / 'btresult.json') - filename_last = Path(testdatadir / '.last_result.json') + filename_last = Path(testdatadir / LAST_BT_RESULT_FN) _backup_file(filename_last, copy_file=True) assert not filename.is_file() From 7c5587aeaafb0948e507d34cac6914a89b09af0e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 09:45:23 +0200 Subject: [PATCH 0225/1197] exportfilename can be a file or directory --- freqtrade/configuration/configuration.py | 2 +- freqtrade/data/btanalysis.py | 7 +++++-- freqtrade/optimize/optimize_reports.py | 13 +++++++++---- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 139e42084..bbd6ce747 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -199,7 +199,7 @@ class Configuration: config['exportfilename'] = Path(config['exportfilename']) else: config['exportfilename'] = (config['user_data_dir'] - / 'backtest_results/backtest-result.json') + / 'backtest_results') def _process_optimize_options(self, config: Dict[str, Any]) -> None: diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 0ae1809f3..07834d729 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -38,7 +38,8 @@ def get_latest_backtest_filename(directory: Union[Path, str]) -> str: filename = directory / LAST_BT_RESULT_FN if not filename.is_file(): - raise ValueError(f"Directory '{directory}' does not seem to contain backtest statistics yet.") + raise ValueError( + f"Directory '{directory}' does not seem to contain backtest statistics yet.") with filename.open() as file: data = json_load(file) @@ -57,9 +58,11 @@ def load_backtest_stats(filename: Union[Path, str]) -> Dict[str, Any]: """ if isinstance(filename, str): filename = Path(filename) + if filename.is_dir(): + filename = get_latest_backtest_filename(filename) if not filename.is_file(): raise ValueError(f"File {filename} does not exist.") - + logger.info(f"Loading backtest result from {filename}") with filename.open() as file: data = json_load(file) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index d1c45bd94..d0e29d98f 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -16,12 +16,17 @@ logger = logging.getLogger(__name__) def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> None: - filename = Path.joinpath(recordfilename.parent, - f'{recordfilename.stem}-{datetime.now().isoformat()}' - ).with_suffix(recordfilename.suffix) + if recordfilename.is_dir(): + filename = recordfilename / \ + f'backtest-result-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}.json' + else: + filename = Path.joinpath( + recordfilename.parent, + f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}' + ).with_suffix(recordfilename.suffix) file_dump_json(filename, stats) - latest_filename = Path.joinpath(recordfilename.parent, LAST_BT_RESULT_FN) + latest_filename = Path.joinpath(filename.parent, LAST_BT_RESULT_FN) file_dump_json(latest_filename, {'latest_backtest': str(filename.name)}) From d999fa2a7e82b3e1fb1c9e0da9b726379f1dc52d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 09:51:49 +0200 Subject: [PATCH 0226/1197] Test autogetting result filename --- freqtrade/data/btanalysis.py | 2 +- tests/data/test_btanalysis.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 07834d729..6931b1685 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -59,7 +59,7 @@ def load_backtest_stats(filename: Union[Path, str]) -> Dict[str, Any]: if isinstance(filename, str): filename = Path(filename) if filename.is_dir(): - filename = get_latest_backtest_filename(filename) + filename = filename / get_latest_backtest_filename(filename) if not filename.is_file(): raise ValueError(f"File {filename} does not exist.") logger.info(f"Loading backtest result from {filename}") diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 144dc5162..5e44b7d87 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -70,6 +70,10 @@ def test_load_backtest_data_new_format(testdatadir): bt_data2 = load_backtest_data(str(filename)) assert bt_data.equals(bt_data2) + # Test loading from folder (must yield same result) + bt_data3 = load_backtest_data(testdatadir) + assert bt_data.equals(bt_data3) + with pytest.raises(ValueError, match=r"File .* does not exist\."): load_backtest_data(str("filename") + "nofile") From 16a842f9f60f1c916d77a243e2f2e169dc0611fc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 28 Jun 2020 10:17:08 +0200 Subject: [PATCH 0227/1197] Have plotting support folder-based exportfilename --- freqtrade/plot/plotting.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index eee338a42..16afaec3b 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -8,7 +8,9 @@ from freqtrade.configuration import TimeRange from freqtrade.data.btanalysis import (calculate_max_drawdown, combine_dataframes_with_mean, create_cum_profit, - extract_trades_of_period, load_trades) + extract_trades_of_period, + get_latest_backtest_filename, + load_trades) from freqtrade.data.converter import trim_dataframe from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException @@ -51,16 +53,18 @@ def init_plotscript(config): ) no_trades = False + filename = config.get('exportfilename') if config.get('no_trades', False): no_trades = True - elif not config['exportfilename'].is_file() and config['trade_source'] == 'file': - logger.warning("Backtest file is missing skipping trades.") - no_trades = True + elif config['trade_source'] == 'file': + if not filename.is_dir() and not filename.is_file(): + logger.warning("Backtest file is missing skipping trades.") + no_trades = True trades = load_trades( config['trade_source'], db_url=config.get('db_url'), - exportfilename=config.get('exportfilename'), + exportfilename=filename, no_trades=no_trades ) trades = trim_dataframe(trades, timerange, 'open_date') From 619eb183fe2454bce969760129385e142946ec4e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jul 2020 06:58:06 +0200 Subject: [PATCH 0228/1197] Allow strategy for plot-profit to allow loading of multi-backtest files --- docs/plotting.md | 8 +++++++- freqtrade/commands/arguments.py | 2 +- freqtrade/plot/plotting.py | 4 ++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/plotting.md b/docs/plotting.md index d3a2df1c1..7de5626b2 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -224,7 +224,8 @@ Possible options for the `freqtrade plot-profit` subcommand: ``` usage: freqtrade plot-profit [-h] [-v] [--logfile FILE] [-V] [-c PATH] - [-d PATH] [--userdir PATH] [-p PAIRS [PAIRS ...]] + [-d PATH] [--userdir PATH] [-s NAME] + [--strategy-path PATH] [-p PAIRS [PAIRS ...]] [--timerange TIMERANGE] [--export EXPORT] [--export-filename PATH] [--db-url PATH] [--trade-source {DB,file}] [-i TIMEFRAME] @@ -270,6 +271,11 @@ Common arguments: --userdir PATH, --user-data-dir PATH Path to userdata directory. +Strategy arguments: + -s NAME, --strategy NAME + Specify strategy class name which will be used by the + bot. + --strategy-path PATH Specify additional strategy lookup path. ``` The `-p/--pairs` argument, can be used to limit the pairs that are considered for this calculation. diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 72f2a02f0..6114fc589 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -354,7 +354,7 @@ class Arguments: plot_profit_cmd = subparsers.add_parser( 'plot-profit', help='Generate plot showing profits.', - parents=[_common_parser], + parents=[_common_parser, _strategy_parser], ) plot_profit_cmd.set_defaults(func=start_plot_profit) self._build_args(optionlist=ARGS_PLOT_PROFIT, parser=plot_profit_cmd) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 16afaec3b..b11f093d9 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -9,7 +9,6 @@ from freqtrade.data.btanalysis import (calculate_max_drawdown, combine_dataframes_with_mean, create_cum_profit, extract_trades_of_period, - get_latest_backtest_filename, load_trades) from freqtrade.data.converter import trim_dataframe from freqtrade.data.history import load_data @@ -65,7 +64,8 @@ def init_plotscript(config): config['trade_source'], db_url=config.get('db_url'), exportfilename=filename, - no_trades=no_trades + no_trades=no_trades, + strategy=config.get("strategy"), ) trades = trim_dataframe(trades, timerange, 'open_date') From d56f9655e2b019cf95af75c613257b9049661944 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jul 2020 07:20:43 +0200 Subject: [PATCH 0229/1197] Update notebook with new statistics example --- docs/plotting.md | 2 +- docs/strategy_analysis_example.md | 44 +++++++++++++++-- freqtrade/data/btanalysis.py | 2 +- .../templates/strategy_analysis_example.ipynb | 47 +++++++++++++++++-- 4 files changed, 85 insertions(+), 10 deletions(-) diff --git a/docs/plotting.md b/docs/plotting.md index 7de5626b2..09eb6ddb5 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -285,7 +285,7 @@ Examples: Use custom backtest-export file ``` bash -freqtrade plot-profit -p LTC/BTC --export-filename user_data/backtest_results/backtest-result-Strategy005.json +freqtrade plot-profit -p LTC/BTC --export-filename user_data/backtest_results/backtest-result.json ``` Use custom database diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index 6b4ad567f..8915ebe37 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -18,7 +18,7 @@ config = Configuration.from_files([]) # config = Configuration.from_files(["config.json"]) # Define some constants -config["timeframe"] = "5m" +config["ticker_interval"] = "5m" # Name of the strategy class config["strategy"] = "SampleStrategy" # Location of the data @@ -33,7 +33,7 @@ pair = "BTC_USDT" from freqtrade.data.history import load_pair_history candles = load_pair_history(datadir=data_location, - timeframe=config["timeframe"], + timeframe=config["ticker_interval"], pair=pair) # Confirm success @@ -85,10 +85,44 @@ Analyze a trades dataframe (also used below for plotting) ```python -from freqtrade.data.btanalysis import load_backtest_data +from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats -# Load backtest results -trades = load_backtest_data(config["user_data_dir"] / "backtest_results/backtest-result.json") +# if backtest_dir points to a directory, it'll automatically load the last backtest file. +backtest_dir = config["user_data_dir"] / "backtest_results" +# backtest_dir can also point to a specific file +# backtest_dir = config["user_data_dir"] / "backtest_results/backtest-result-2020-07-01_20-04-22.json" +``` + + +```python +# You can get the full backtest statistics by using the following command. +# This contains all information used to generate the backtest result. +stats = load_backtest_stats(backtest_dir) + +strategy = 'SampleStrategy' +# All statistics are available per strategy, so if `--strategy-list` was used during backtest, this will be reflected here as well. +# Example usages: +print(stats['strategy'][strategy]['results_per_pair']) +# Get pairlist used for this backtest +print(stats['strategy'][strategy]['pairlist']) +# Get market change (average change of all pairs from start to end of the backtest period) +print(stats['strategy'][strategy]['market_change']) +# Maximum drawdown () +print(stats['strategy'][strategy]['max_drawdown']) +# Maximum drawdown start and end +print(stats['strategy'][strategy]['drawdown_start']) +print(stats['strategy'][strategy]['drawdown_end']) + + +# Get strategy comparison (only relevant if multiple strategies were compared) +print(stats['strategy_comparison']) + +``` + + +```python +# Load backtested trades as dataframe +trades = load_backtest_data(backtest_dir) # Show value-counts per pair trades.groupby("pair")["sell_reason"].value_counts() diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 6931b1685..cf6e18e64 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -72,7 +72,7 @@ def load_backtest_stats(filename: Union[Path, str]) -> Dict[str, Any]: def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = None) -> pd.DataFrame: """ Load backtest data file. - :param filename: pathlib.Path object, or string pointing to the file. + :param filename: pathlib.Path object, or string pointing to a file or directory :param strategy: Strategy to load - mainly relevant for multi-strategy backtests Can also serve as protection to load the correct result. :return: a dataframe with the analysis results diff --git a/freqtrade/templates/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb index dffa308ce..31a5b536a 100644 --- a/freqtrade/templates/strategy_analysis_example.ipynb +++ b/freqtrade/templates/strategy_analysis_example.ipynb @@ -136,10 +136,51 @@ "metadata": {}, "outputs": [], "source": [ - "from freqtrade.data.btanalysis import load_backtest_data\n", + "from freqtrade.data.btanalysis import load_backtest_data, load_backtest_stats\n", "\n", - "# Load backtest results\n", - "trades = load_backtest_data(config[\"user_data_dir\"] / \"backtest_results/backtest-result.json\")\n", + "# if backtest_dir points to a directory, it'll automatically load the last backtest file.\n", + "backtest_dir = config[\"user_data_dir\"] / \"backtest_results\"\n", + "# backtest_dir can also point to a specific file \n", + "# backtest_dir = config[\"user_data_dir\"] / \"backtest_results/backtest-result-2020-07-01_20-04-22.json\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# You can get the full backtest statistics by using the following command.\n", + "# This contains all information used to generate the backtest result.\n", + "stats = load_backtest_stats(backtest_dir)\n", + "\n", + "strategy = 'SampleStrategy'\n", + "# All statistics are available per strategy, so if `--strategy-list` was used during backtest, this will be reflected here as well.\n", + "# Example usages:\n", + "print(stats['strategy'][strategy]['results_per_pair'])\n", + "# Get pairlist used for this backtest\n", + "print(stats['strategy'][strategy]['pairlist'])\n", + "# Get market change (average change of all pairs from start to end of the backtest period)\n", + "print(stats['strategy'][strategy]['market_change'])\n", + "# Maximum drawdown ()\n", + "print(stats['strategy'][strategy]['max_drawdown'])\n", + "# Maximum drawdown start and end\n", + "print(stats['strategy'][strategy]['drawdown_start'])\n", + "print(stats['strategy'][strategy]['drawdown_end'])\n", + "\n", + "\n", + "# Get strategy comparison (only relevant if multiple strategies were compared)\n", + "print(stats['strategy_comparison'])\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load backtested trades as dataframe\n", + "trades = load_backtest_data(backtest_dir)\n", "\n", "# Show value-counts per pair\n", "trades.groupby(\"pair\")[\"sell_reason\"].value_counts()" From 804c42933d5d1eb58e102dbbcb66e55f801b0bc8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jul 2020 08:02:27 +0200 Subject: [PATCH 0230/1197] Document summary-statistics --- docs/backtesting.md | 69 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index ecd48bdc9..52506215d 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -157,17 +157,28 @@ A backtesting result will look like that: | ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 | 0 | 0 | | LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 | 0 | 0 | | TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 | 0 | 0 | +============ SUMMARY METRICS ============= +| Metric | Value | +|------------------+---------------------| +| Total trades | 429 | +| First trade | 2019-01-01 18:30:00 | +| First trade Pair | EOS/USDT | +| Backtesting from | 2019-01-01 00:00:00 | +| Backtesting to | 2019-05-01 00:00:00 | +| Trades per day | 3.575 | +| | | +| Max Drawdown | 50.63% | +| Drawdown Start | 2019-02-15 14:10:00 | +| Drawdown End | 2019-04-11 18:15:00 | +| Market change | -5.88% | +========================================== + ``` +### Backtesting report table + The 1st table contains all trades the bot made, including "left open trades". -The 2nd table contains a recap of sell reasons. -This table can tell you which area needs some additional work (i.e. all `sell_signal` trades are losses, so we should disable the sell-signal or work on improving that). - -The 3rd table contains all trades the bot had to `forcesell` at the end of the backtest period to present a full picture. -This is necessary to simulate realistic behaviour, since the backtest period has to end at some point, while realistically, you could leave the bot running forever. -These trades are also included in the first table, but are extracted separately for clarity. - The last line will give you the overall performance of your strategy, here: @@ -196,6 +207,50 @@ On the other hand, if you set a too high `minimal_roi` like `"0": 0.55` (55%), there is almost no chance that the bot will ever reach this profit. Hence, keep in mind that your performance is an integral mix of all different elements of the strategy, your configuration, and the crypto-currency pairs you have set up. +### Sell reasons table + +The 2nd table contains a recap of sell reasons. +This table can tell you which area needs some additional work (i.e. all `sell_signal` trades are losses, so we should disable the sell-signal or work on improving that). + +### Left open trades table + +The 3rd table contains all trades the bot had to `forcesell` at the end of the backtest period to present a full picture. +This is necessary to simulate realistic behaviour, since the backtest period has to end at some point, while realistically, you could leave the bot running forever. +These trades are also included in the first table, but are extracted separately for clarity. + +### Summary metrics + +The last element of the backtest report is the summary metrics table. +It contains some useful key metrics about your strategy. + +``` +============ SUMMARY METRICS ============= +| Metric | Value | +|------------------+---------------------| +| Total trades | 429 | +| First trade | 2019-01-01 18:30:00 | +| First trade Pair | EOS/USDT | +| Backtesting from | 2019-01-01 00:00:00 | +| Backtesting to | 2019-05-01 00:00:00 | +| Trades per day | 3.575 | +| | | +| Max Drawdown | 50.63% | +| Drawdown Start | 2019-02-15 14:10:00 | +| Drawdown End | 2019-04-11 18:15:00 | +| Market change | -5.88% | +========================================== + +``` + +- `Total trades`: Identical to the total trades of the backtest output table. +- `First trade`: First trade entered. +- `First trade pair`: Which pair was part of the first trade +- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined as `--timerange from-to`) +- `Trades per day`: Total trades / Backtest duration (this will give you information about how many trades to expect from the strategy) +- `Max Drawdown`: Maximum drawown experienced. a value of 50% means that from highest to subsequent lowest point, a 50% drop was experiened). +- `Drawdown Start` / `Drawdown End`: From when to when was this large drawdown (can also be visualized via `plot-dataframe` subcommand) +- `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column. + ### Assumptions made by backtesting Since backtesting lacks some detailed information about what happens within a candle, it needs to take a few assumptions: From 42868ad24ac8812ec295867fd5f9728aaad9635a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jul 2020 19:30:29 +0200 Subject: [PATCH 0231/1197] Add best / worst day to statistics --- docs/backtesting.md | 11 +++++++---- freqtrade/optimize/optimize_reports.py | 8 ++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 52506215d..6c01e1c62 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -233,6 +233,8 @@ It contains some useful key metrics about your strategy. | Backtesting from | 2019-01-01 00:00:00 | | Backtesting to | 2019-05-01 00:00:00 | | Trades per day | 3.575 | +| Best day | 25.27% | +| Worst day | -30.67% | | | | | Max Drawdown | 50.63% | | Drawdown Start | 2019-02-15 14:10:00 | @@ -244,11 +246,12 @@ It contains some useful key metrics about your strategy. - `Total trades`: Identical to the total trades of the backtest output table. - `First trade`: First trade entered. -- `First trade pair`: Which pair was part of the first trade -- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined as `--timerange from-to`) -- `Trades per day`: Total trades / Backtest duration (this will give you information about how many trades to expect from the strategy) +- `First trade pair`: Which pair was part of the first trade. +- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined as `--timerange from-to`). +- `Trades per day`: Total trades / Backtest duration (this will give you information about how many trades to expect from the strategy). +- `Best day` / `Worst day`: Best and worst day based on daily profit. - `Max Drawdown`: Maximum drawown experienced. a value of 50% means that from highest to subsequent lowest point, a 50% drop was experiened). -- `Drawdown Start` / `Drawdown End`: From when to when was this large drawdown (can also be visualized via `plot-dataframe` subcommand) +- `Drawdown Start` / `Drawdown End`: From when to when was this large drawdown (can also be visualized via `plot-dataframe` subcommand). - `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column. ### Assumptions made by backtesting diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index d0e29d98f..4f169c53a 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -239,6 +239,9 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], max_open_trades=max_open_trades, results=results.loc[results['open_at_end']], skip_nan=True) + daily_profit = results.resample('1d', on='close_date')['profit_percent'].sum() + worst = min(daily_profit) + best = max(daily_profit) backtest_days = (max_date - min_date).days strat_stats = { @@ -252,6 +255,9 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], 'backtest_end': max_date.datetime, 'backtest_end_ts': max_date.timestamp, 'backtest_days': backtest_days, + 'backtest_best_day': best, + 'backtest_worst_day': worst, + 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else None, 'market_change': market_change, 'pairlist': list(btdata.keys()), @@ -366,6 +372,8 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Backtesting from', strat_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)), ('Backtesting to', strat_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)), ('Trades per day', strat_results['trades_per_day']), + ('Best day', f"{round(strat_results['backtest_best_day'] * 100, 2)}%"), + ('Worst day', f"{round(strat_results['backtest_worst_day'] * 100, 2)}%"), ('', ''), # Empty line to improve readability ('Max Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"), ('Drawdown Start', strat_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)), From 8e0ff4bd86effd9d44fb0d0fd82c10ce246c6141 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jul 2020 19:45:45 +0200 Subject: [PATCH 0232/1197] Add Win / draw / losing days --- freqtrade/optimize/optimize_reports.py | 28 ++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 4f169c53a..33157d50a 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -210,6 +210,23 @@ def generate_edge_table(results: dict) -> str: floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore +def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: + daily_profit = results.resample('1d', on='close_date')['profit_percent'].sum() + worst = min(daily_profit) + best = max(daily_profit) + winning_days = sum(daily_profit > 0) + draw_days = sum(daily_profit == 0) + losing_days = sum(daily_profit < 0) + + return { + 'backtest_best_day': best, + 'backtest_worst_day': worst, + 'winning_days': winning_days, + 'draw_days': draw_days, + 'losing_days': losing_days, + } + + def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], all_results: Dict[str, DataFrame], min_date: Arrow, max_date: Arrow @@ -239,9 +256,7 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], max_open_trades=max_open_trades, results=results.loc[results['open_at_end']], skip_nan=True) - daily_profit = results.resample('1d', on='close_date')['profit_percent'].sum() - worst = min(daily_profit) - best = max(daily_profit) + daily_stats = generate_daily_stats(results) backtest_days = (max_date - min_date).days strat_stats = { @@ -255,13 +270,12 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], 'backtest_end': max_date.datetime, 'backtest_end_ts': max_date.timestamp, 'backtest_days': backtest_days, - 'backtest_best_day': best, - 'backtest_worst_day': worst, 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else None, 'market_change': market_change, 'pairlist': list(btdata.keys()), - 'stake_amount': config['stake_amount'] + 'stake_amount': config['stake_amount'], + **daily_stats, } result['strategy'][strategy] = strat_stats @@ -374,6 +388,8 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Trades per day', strat_results['trades_per_day']), ('Best day', f"{round(strat_results['backtest_best_day'] * 100, 2)}%"), ('Worst day', f"{round(strat_results['backtest_worst_day'] * 100, 2)}%"), + ('Days win/draw/lose', f"{strat_results['winning_days']} / " + f"{strat_results['draw_days']} / {strat_results['losing_days']}"), ('', ''), # Empty line to improve readability ('Max Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"), ('Drawdown Start', strat_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)), From 987188e41f15e5914686c2637f4ddee418af9be8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jul 2020 19:58:02 +0200 Subject: [PATCH 0233/1197] Add avgduration for winners and losers --- docs/backtesting.md | 40 ++++++++++++++------------ freqtrade/optimize/optimize_reports.py | 9 ++++++ 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 6c01e1c62..cb20c3e43 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -224,23 +224,26 @@ The last element of the backtest report is the summary metrics table. It contains some useful key metrics about your strategy. ``` -============ SUMMARY METRICS ============= -| Metric | Value | -|------------------+---------------------| -| Total trades | 429 | -| First trade | 2019-01-01 18:30:00 | -| First trade Pair | EOS/USDT | -| Backtesting from | 2019-01-01 00:00:00 | -| Backtesting to | 2019-05-01 00:00:00 | -| Trades per day | 3.575 | -| Best day | 25.27% | -| Worst day | -30.67% | -| | | -| Max Drawdown | 50.63% | -| Drawdown Start | 2019-02-15 14:10:00 | -| Drawdown End | 2019-04-11 18:15:00 | -| Market change | -5.88% | -========================================== +=============== SUMMARY METRICS =============== +| Metric | Value | +|-----------------------+---------------------| + +| Total trades | 429 | +| First trade | 2019-01-01 18:30:00 | +| First trade Pair | EOS/USDT | +| Backtesting from | 2019-01-01 00:00:00 | +| Backtesting to | 2019-05-01 00:00:00 | +| Trades per day | 3.575 | +| Best day | 25.27% | +| Worst day | -30.67% | +| Avg. Duration Winners | 4:23:00 | +| Avg. Duration Loser | 6:55:00 | +| | | +| Max Drawdown | 50.63% | +| Drawdown Start | 2019-02-15 14:10:00 | +| Drawdown End | 2019-04-11 18:15:00 | +| Market change | -5.88% | +=============================================== ``` @@ -250,10 +253,11 @@ It contains some useful key metrics about your strategy. - `Backtesting from` / `Backtesting to`: Backtesting range (usually defined as `--timerange from-to`). - `Trades per day`: Total trades / Backtest duration (this will give you information about how many trades to expect from the strategy). - `Best day` / `Worst day`: Best and worst day based on daily profit. +- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. - `Max Drawdown`: Maximum drawown experienced. a value of 50% means that from highest to subsequent lowest point, a 50% drop was experiened). - `Drawdown Start` / `Drawdown End`: From when to when was this large drawdown (can also be visualized via `plot-dataframe` subcommand). - `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column. - + ### Assumptions made by backtesting Since backtesting lacks some detailed information about what happens within a candle, it needs to take a few assumptions: diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 33157d50a..f9b38caf0 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -218,12 +218,19 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: draw_days = sum(daily_profit == 0) losing_days = sum(daily_profit < 0) + winning_trades = results.loc[results['profit_percent'] > 0] + losing_trades = results.loc[results['profit_percent'] < 0] + return { 'backtest_best_day': best, 'backtest_worst_day': worst, 'winning_days': winning_days, 'draw_days': draw_days, 'losing_days': losing_days, + 'winner_holding_avg': (timedelta(minutes=round(winning_trades['trade_duration'].mean())) + if not winning_trades.empty else '0:00'), + 'loser_holding_avg': (timedelta(minutes=round(losing_trades['trade_duration'].mean())) + if not losing_trades.empty else '0:00'), } @@ -390,6 +397,8 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Worst day', f"{round(strat_results['backtest_worst_day'] * 100, 2)}%"), ('Days win/draw/lose', f"{strat_results['winning_days']} / " f"{strat_results['draw_days']} / {strat_results['losing_days']}"), + ('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"), + ('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"), ('', ''), # Empty line to improve readability ('Max Drawdown', f"{round(strat_results['max_drawdown'] * 100, 2)}%"), ('Drawdown Start', strat_results['drawdown_start'].strftime(DATETIME_PRINT_FORMAT)), From 523437d9707e941c8ddd2e7a5585c97199300c80 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jul 2020 20:03:33 +0200 Subject: [PATCH 0234/1197] Add tst for daily stats --- tests/optimize/test_optimize_reports.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 2431fa716..6c1009e22 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -1,4 +1,5 @@ import re +from datetime import timedelta from pathlib import Path import pandas as pd @@ -8,9 +9,11 @@ from arrow import Arrow from freqtrade.configuration import TimeRange from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.data import history -from freqtrade.data.btanalysis import get_latest_backtest_filename +from freqtrade.data.btanalysis import (get_latest_backtest_filename, + load_backtest_data) from freqtrade.edge import PairInfo from freqtrade.optimize.optimize_reports import (generate_backtest_stats, + generate_daily_stats, generate_edge_table, generate_pair_metrics, generate_sell_reason_stats, @@ -170,6 +173,21 @@ def test_generate_pair_metrics(default_conf, mocker): pytest.approx(pair_results[-1]['profit_sum_pct']) == pair_results[-1]['profit_sum'] * 100) +def test_generate_daily_stats(testdatadir): + + filename = testdatadir / "backtest-result_new.json" + bt_data = load_backtest_data(filename) + res = generate_daily_stats(bt_data) + assert isinstance(res, dict) + assert round(res['backtest_best_day'], 4) == 0.1796 + assert round(res['backtest_worst_day'], 4) == -0.1468 + assert res['winning_days'] == 14 + assert res['draw_days'] == 4 + assert res['losing_days'] == 3 + assert res['winner_holding_avg'] == timedelta(seconds=1440) + assert res['loser_holding_avg'] == timedelta(days=1, seconds=21420) + + def test_text_table_sell_reason(default_conf): results = pd.DataFrame( From 0d15a87af8de45799677a221c7311390b7dbb7b3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jul 2020 20:15:20 +0200 Subject: [PATCH 0235/1197] Remove old store_backtest method --- freqtrade/optimize/backtesting.py | 2 - freqtrade/optimize/optimize_reports.py | 20 ------- tests/optimize/test_backtesting.py | 1 + tests/optimize/test_optimize_reports.py | 74 ------------------------- 4 files changed, 1 insertion(+), 96 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 18881f9db..3cd4f4fa2 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -21,7 +21,6 @@ from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, - store_backtest_result, store_backtest_stats) from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.persistence import Trade @@ -421,7 +420,6 @@ class Backtesting: stats = generate_backtest_stats(self.config, data, all_results, min_date=min_date, max_date=max_date) if self.config.get('export', False): - store_backtest_result(self.config['exportfilename'], all_results) store_backtest_stats(self.config['exportfilename'], stats) # Show backtest results diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index f9b38caf0..67c8e3077 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -30,26 +30,6 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N file_dump_json(latest_filename, {'latest_backtest': str(filename.name)}) -def store_backtest_result(recordfilename: Path, all_results: Dict[str, DataFrame]) -> None: - """ - Stores backtest results to file (one file per strategy) - :param recordfilename: Destination filename - :param all_results: Dict of Dataframes, one results dataframe per strategy - """ - for strategy, results in all_results.items(): - records = backtest_result_to_list(results) - - if records: - filename = recordfilename - if len(all_results) > 1: - # Inject strategy to filename - filename = Path.joinpath( - recordfilename.parent, - f'{recordfilename.stem}-{strategy}').with_suffix(recordfilename.suffix) - logger.info(f'Dumping backtest results to {filename}') - file_dump_json(filename, records) - - def backtest_result_to_list(results: DataFrame) -> List[List]: """ Converts a list of Backtest-results to list diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 2c855fbc0..04417848f 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -703,6 +703,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): generate_pair_metrics=MagicMock(), generate_sell_reason_stats=sell_reason_mock, generate_strategy_metrics=strat_summary, + generate_daily_stats=MagicMock(), ) patched_configuration_load_config_file(mocker, default_conf) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 6c1009e22..2fab4578c 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -18,13 +18,11 @@ from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_pair_metrics, generate_sell_reason_stats, generate_strategy_metrics, - store_backtest_result, store_backtest_stats, text_table_bt_results, text_table_sell_reason, text_table_strategy) from freqtrade.strategy.interface import SellType -from tests.conftest import patch_exchange from tests.data.test_history import _backup_file, _clean_test_file @@ -308,75 +306,3 @@ def test_generate_edge_table(edge_conf, mocker): assert generate_edge_table(results).count('| ETH/BTC |') == 1 assert generate_edge_table(results).count( '| Risk Reward Ratio | Required Risk Reward | Expectancy |') == 1 - - -def test_backtest_record(default_conf, fee, mocker): - names = [] - records = [] - patch_exchange(mocker) - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - mocker.patch( - 'freqtrade.optimize.optimize_reports.file_dump_json', - new=lambda n, r: (names.append(n), records.append(r)) - ) - - results = {'DefStrat': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", - "UNITTEST/BTC", "UNITTEST/BTC"], - "profit_percent": [0.003312, 0.010801, 0.013803, 0.002780], - "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], - "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, - Arrow(2017, 11, 14, 21, 36, 00).datetime, - Arrow(2017, 11, 14, 22, 12, 00).datetime, - Arrow(2017, 11, 14, 22, 44, 00).datetime], - "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, - Arrow(2017, 11, 14, 22, 10, 00).datetime, - Arrow(2017, 11, 14, 22, 43, 00).datetime, - Arrow(2017, 11, 14, 22, 58, 00).datetime], - "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], - "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], - "trade_duration": [123, 34, 31, 14], - "open_at_end": [False, False, False, True], - "sell_reason": [SellType.ROI, SellType.STOP_LOSS, - SellType.ROI, SellType.FORCE_SELL] - })} - store_backtest_result(Path("backtest-result.json"), results) - # Assert file_dump_json was only called once - assert names == [Path('backtest-result.json')] - records = records[0] - # Ensure records are of correct type - assert len(records) == 4 - - # reset test to test with strategy name - names = [] - records = [] - results['Strat'] = results['DefStrat'] - results['Strat2'] = results['DefStrat'] - store_backtest_result(Path("backtest-result.json"), results) - assert names == [ - Path('backtest-result-DefStrat.json'), - Path('backtest-result-Strat.json'), - Path('backtest-result-Strat2.json'), - ] - records = records[0] - # Ensure records are of correct type - assert len(records) == 4 - - # ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117) - # Below follows just a typecheck of the schema/type of trade-records - oix = None - for (pair, profit, date_buy, date_sell, buy_index, dur, - openr, closer, open_at_end, sell_reason) in records: - assert pair == 'UNITTEST/BTC' - assert isinstance(profit, float) - # FIX: buy/sell should be converted to ints - assert isinstance(date_buy, float) - assert isinstance(date_sell, float) - assert isinstance(openr, float) - assert isinstance(closer, float) - assert isinstance(open_at_end, bool) - assert isinstance(sell_reason, str) - isinstance(buy_index, pd._libs.tslib.Timestamp) - if oix: - assert buy_index > oix - oix = buy_index - assert dur > 0 From ea5e47657a9bf4d3969db4a762ea73d283d3abc3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jul 2020 20:26:55 +0200 Subject: [PATCH 0236/1197] Remove ticker_interval from jupyter notebook --- docs/strategy_analysis_example.md | 4 ++-- freqtrade/templates/strategy_analysis_example.ipynb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index 8915ebe37..90e39fd76 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -18,7 +18,7 @@ config = Configuration.from_files([]) # config = Configuration.from_files(["config.json"]) # Define some constants -config["ticker_interval"] = "5m" +config["timeframe"] = "5m" # Name of the strategy class config["strategy"] = "SampleStrategy" # Location of the data @@ -33,7 +33,7 @@ pair = "BTC_USDT" from freqtrade.data.history import load_pair_history candles = load_pair_history(datadir=data_location, - timeframe=config["ticker_interval"], + timeframe=config["timeframe"], pair=pair) # Confirm success diff --git a/freqtrade/templates/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb index 31a5b536a..c6e64c74e 100644 --- a/freqtrade/templates/strategy_analysis_example.ipynb +++ b/freqtrade/templates/strategy_analysis_example.ipynb @@ -34,7 +34,7 @@ "# config = Configuration.from_files([\"config.json\"])\n", "\n", "# Define some constants\n", - "config[\"ticker_interval\"] = \"5m\"\n", + "config[\"timeframe\"] = \"5m\"\n", "# Name of the strategy class\n", "config[\"strategy\"] = \"SampleStrategy\"\n", "# Location of the data\n", @@ -53,7 +53,7 @@ "from freqtrade.data.history import load_pair_history\n", "\n", "candles = load_pair_history(datadir=data_location,\n", - " timeframe=config[\"ticker_interval\"],\n", + " timeframe=config[\"timeframe\"],\n", " pair=pair)\n", "\n", "# Confirm success\n", From 1fc4451d2f59e7631131c9409a101c3c6abf63e1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jul 2020 20:32:04 +0200 Subject: [PATCH 0237/1197] Avoid \ linebreak --- freqtrade/optimize/optimize_reports.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 67c8e3077..63fbfb48c 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -17,8 +17,8 @@ logger = logging.getLogger(__name__) def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> None: if recordfilename.is_dir(): - filename = recordfilename / \ - f'backtest-result-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}.json' + filename = (recordfilename / + f'backtest-result-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}.json') else: filename = Path.joinpath( recordfilename.parent, From c4a9a79be08fdc26865ac23b53879b81076f3c3f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Jul 2020 09:43:49 +0200 Subject: [PATCH 0238/1197] Apply suggested documentation changes from code review Co-authored-by: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> --- docs/bot-basics.md | 4 ++-- docs/strategy-advanced.md | 12 ++++++------ docs/strategy-customization.md | 4 ++-- freqtrade/strategy/interface.py | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/bot-basics.md b/docs/bot-basics.md index 728dcf46e..44f493456 100644 --- a/docs/bot-basics.md +++ b/docs/bot-basics.md @@ -1,10 +1,10 @@ # Freqtrade basics -This page will try to teach you some basic concepts on how freqtrade works and operates. +This page provides you some basic concepts on how Freqtrade works and operates. ## Freqtrade terminology -* Trade: Open position +* Trade: Open position. * Open Order: Order which is currently placed on the exchange, and is not yet complete. * Pair: Tradable pair, usually in the format of Quote/Base (e.g. XRP/USDT). * Timeframe: Candle length to use (e.g. `"5m"`, `"1h"`, ...). diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index c576cb46e..a5977e5dc 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -3,7 +3,7 @@ This page explains some advanced concepts available for strategies. If you're just getting started, please be familiar with the methods described in the [Strategy Customization](strategy-customization.md) documentation and with the [Freqtrade basics](bot-basics.md) first. -[Freqtrade basics](bot-basics.md) describes in which sequence each method defined below is called, which can be helpful to understand which method to use. +[Freqtrade basics](bot-basics.md) describes in which sequence each method described below is called, which can be helpful to understand which method to use for your custom needs. !!! Note All callback methods described below should only be implemented in a strategy if they are also actively used. @@ -97,8 +97,8 @@ class Awesomestrategy(IStrategy): ## Bot loop start callback -A simple callback which is called at the start of every bot iteration. -This can be used to perform calculations which are pair independent. +A simple callback which is called once at the start of every bot throttling iteration. +This can be used to perform calculations which are pair independent (apply to all pairs), loading of external data, etc. ``` python import requests @@ -111,7 +111,7 @@ class Awesomestrategy(IStrategy): """ Called at the start of the bot iteration (one loop). Might be used to perform pair-independent tasks - (e.g. gather some remote ressource for comparison) + (e.g. gather some remote resource for comparison) :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. """ if self.config['runmode'].value in ('live', 'dry_run'): @@ -125,7 +125,7 @@ class Awesomestrategy(IStrategy): ### Trade entry (buy order) confirmation -`confirm_trade_entry()` an be used to abort a trade entry at the latest second (maybe because the price is not what we expect). +`confirm_trade_entry()` can be used to abort a trade entry at the latest second (maybe because the price is not what we expect). ``` python class Awesomestrategy(IStrategy): @@ -158,7 +158,7 @@ class Awesomestrategy(IStrategy): ### Trade exit (sell order) confirmation -`confirm_trade_exit()` an be used to abort a trade exit (sell) at the latest second (maybe because the price is not what we expect). +`confirm_trade_exit()` can be used to abort a trade exit (sell) at the latest second (maybe because the price is not what we expect). ``` python from freqtrade.persistence import Trade diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 0a1049f3b..50fec79dc 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -1,8 +1,8 @@ # Strategy Customization -This page explains how to customize your strategies, and add new indicators. +This page explains how to customize your strategies, add new indicators and set up trading rules. -Please familiarize yourself with [Freqtrade basics](bot-basics.md) first. +Please familiarize yourself with [Freqtrade basics](bot-basics.md) first, which provides overall info on how the bot operates. ## Install a custom strategy file diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index f8e59ac7b..f3c5e154d 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -194,7 +194,7 @@ class IStrategy(ABC): """ Called at the start of the bot iteration (one loop). Might be used to perform pair-independent tasks - (e.g. gather some remote ressource for comparison) + (e.g. gather some remote resource for comparison) :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. """ pass From 75318525a96b018fe138aaaf3a51773a6b437e88 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Jul 2020 16:41:19 +0200 Subject: [PATCH 0239/1197] Update docs/strategy-advanced.md Co-authored-by: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> --- docs/strategy-advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index a5977e5dc..e4bab303e 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -6,7 +6,7 @@ If you're just getting started, please be familiar with the methods described in [Freqtrade basics](bot-basics.md) describes in which sequence each method described below is called, which can be helpful to understand which method to use for your custom needs. !!! Note - All callback methods described below should only be implemented in a strategy if they are also actively used. + All callback methods described below should only be implemented in a strategy if they are actually used. ## Custom order timeout rules From f63045b0e982940615dbe48d176d2fb9397837f0 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2020 09:11:49 +0000 Subject: [PATCH 0240/1197] Bump ccxt from 1.30.48 to 1.30.64 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.30.48 to 1.30.64. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.30.48...1.30.64) 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 2948b8f35..2f225c93c 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.30.48 +ccxt==1.30.64 SQLAlchemy==1.3.18 python-telegram-bot==12.8 arrow==0.15.7 From 4c8bee1e5d23c947ae2e3a26157a0a7373249826 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2020 09:12:15 +0000 Subject: [PATCH 0241/1197] Bump mkdocs-material from 5.3.3 to 5.4.0 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 5.3.3 to 5.4.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/5.3.3...5.4.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 a0505c84b..3a236ee87 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==5.3.3 +mkdocs-material==5.4.0 mdx_truly_sane_lists==1.2 From 93dd70c77ddc717219b5d9cb9007f28d04469bfd Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2020 09:13:05 +0000 Subject: [PATCH 0242/1197] Bump joblib from 0.15.1 to 0.16.0 Bumps [joblib](https://github.com/joblib/joblib) from 0.15.1 to 0.16.0. - [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.15.1...0.16.0) 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 2784bc156..da34e54b2 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -6,5 +6,5 @@ scipy==1.5.0 scikit-learn==0.23.1 scikit-optimize==0.7.4 filelock==3.0.12 -joblib==0.15.1 +joblib==0.16.0 progressbar2==3.51.4 From deb34d287990c5959afc34b04c270751f8336a21 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Jul 2020 19:58:28 +0000 Subject: [PATCH 0243/1197] Bump scipy from 1.5.0 to 1.5.1 Bumps [scipy](https://github.com/scipy/scipy) from 1.5.0 to 1.5.1. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.5.0...v1.5.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 da34e54b2..4773d9877 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.5.0 +scipy==1.5.1 scikit-learn==0.23.1 scikit-optimize==0.7.4 filelock==3.0.12 From 2e45859aef515efa5136054835f876fcb4e614f3 Mon Sep 17 00:00:00 2001 From: gambcl Date: Wed, 8 Jul 2020 18:06:30 +0100 Subject: [PATCH 0244/1197] Added range checks to min_days_listed in AgeFilter --- freqtrade/exchange/exchange.py | 5 +++++ freqtrade/pairlist/AgeFilter.py | 10 ++++++++++ tests/pairlist/test_pairlist.py | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a3a548176..8aab225c6 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -187,6 +187,11 @@ class Exchange: def timeframes(self) -> List[str]: return list((self._api.timeframes or {}).keys()) + @property + def ohlcv_candle_limit(self) -> int: + """exchange ohlcv candle limit""" + return int(self._ohlcv_candle_limit) + @property def markets(self) -> Dict: """exchange ccxt markets""" diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index b489a59bc..101f19cbe 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -23,6 +23,16 @@ class AgeFilter(IPairList): super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) self._min_days_listed = pairlistconfig.get('min_days_listed', 10) + + if self._min_days_listed < 1: + self.log_on_refresh(logger.info, "min_days_listed must be >= 1, " + "ignoring filter") + if self._min_days_listed > exchange.ohlcv_candle_limit: + self._min_days_listed = min(self._min_days_listed, exchange.ohlcv_candle_limit) + self.log_on_refresh(logger.info, "min_days_listed exceeds " + "exchange max request size " + f"({exchange.ohlcv_candle_limit}), using " + f"min_days_listed={self._min_days_listed}") self._enabled = self._min_days_listed >= 1 @property diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index a2644fe8c..6b96501c9 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -524,6 +524,38 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers): assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == lrf +def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tickers, caplog) -> None: + default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, + {'method': 'AgeFilter', 'min_days_listed': -1}] + + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers + ) + + get_patched_freqtradebot(mocker, default_conf) + + assert log_has_re(r'min_days_listed must be >= 1, ' + r'ignoring filter', caplog) + + +def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tickers, caplog) -> None: + default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, + {'method': 'AgeFilter', 'min_days_listed': 99999}] + + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers + ) + + get_patched_freqtradebot(mocker, default_conf) + + assert log_has_re(r'^min_days_listed exceeds ' + r'exchange max request size', caplog) + + def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_history_list): mocker.patch.multiple('freqtrade.exchange.Exchange', From 091285ba43a9b17fd47b434e67f388dbf63f90cd Mon Sep 17 00:00:00 2001 From: gambcl Date: Wed, 8 Jul 2020 18:32:14 +0100 Subject: [PATCH 0245/1197] Fix flake8 error in test_pairlist.py --- tests/pairlist/test_pairlist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 6b96501c9..f95f001c9 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -524,7 +524,7 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers): assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == lrf -def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tickers, caplog) -> None: +def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tickers, caplog): default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, {'method': 'AgeFilter', 'min_days_listed': -1}] @@ -540,7 +540,7 @@ def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tick r'ignoring filter', caplog) -def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tickers, caplog) -> None: +def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tickers, caplog): default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, {'method': 'AgeFilter', 'min_days_listed': 99999}] From 14eab9be04569a16fea2a56ccb636fb0d205a267 Mon Sep 17 00:00:00 2001 From: gambcl Date: Wed, 8 Jul 2020 22:02:04 +0100 Subject: [PATCH 0246/1197] Added min_price, max_price to PriceFilter --- config_full.json.example | 2 +- docs/configuration.md | 15 ++++++++-- freqtrade/pairlist/PriceFilter.py | 49 ++++++++++++++++++++++++++----- tests/pairlist/test_pairlist.py | 17 ++++++++--- 4 files changed, 67 insertions(+), 16 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index e1be01690..d5bfd3fe1 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -66,7 +66,7 @@ }, {"method": "AgeFilter", "min_days_listed": 10}, {"method": "PrecisionFilter"}, - {"method": "PriceFilter", "low_price_ratio": 0.01}, + {"method": "PriceFilter", "low_price_ratio": 0.01, "min_price": 0.00000010}, {"method": "SpreadFilter", "max_spread_ratio": 0.005} ], "exchange": { diff --git a/docs/configuration.md b/docs/configuration.md index e7a79361a..74bacacc0 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -662,16 +662,25 @@ Filters low-value coins which would not allow setting stoplosses. #### PriceFilter -The `PriceFilter` allows filtering of pairs by price. +The `PriceFilter` allows filtering of pairs by price. Currently the following price filters are supported: +* `min_price` +* `max_price` +* `low_price_ratio` -Currently, only `low_price_ratio` setting is implemented, where a raise of 1 price unit (pip) is below the `low_price_ratio` ratio. +The `min_price` setting removes pairs where the price is below the specified price. This is useful if you wish to avoid trading very low-priced pairs. +This option is disabled by default, and will only apply if set to <> 0. + +The `max_price` setting removes pairs where the price is above the specified price. This is useful if you wish to trade only low-priced pairs. +This option is disabled by default, and will only apply if set to <> 0. + +The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio. This option is disabled by default, and will only apply if set to <> 0. Calculation example: Min price precision is 8 decimals. If price is 0.00000011 - one step would be 0.00000012 - which is almost 10% higher than the previous value. -These pairs are dangerous since it may be impossible to place the desired stoploss - and often result in high losses. Here is what the PriceFilters takes over. +These pairs are dangerous since it may be impossible to place the desired stoploss - and often result in high losses. #### ShuffleFilter diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/pairlist/PriceFilter.py index 29dd88a76..1afc60999 100644 --- a/freqtrade/pairlist/PriceFilter.py +++ b/freqtrade/pairlist/PriceFilter.py @@ -18,7 +18,11 @@ class PriceFilter(IPairList): super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) self._low_price_ratio = pairlistconfig.get('low_price_ratio', 0) - self._enabled = self._low_price_ratio != 0 + self._min_price = pairlistconfig.get('min_price', 0) + self._max_price = pairlistconfig.get('max_price', 0) + self._enabled = (self._low_price_ratio != 0) or \ + (self._min_price != 0) or \ + (self._max_price != 0) @property def needstickers(self) -> bool: @@ -33,7 +37,18 @@ class PriceFilter(IPairList): """ Short whitelist method description - used for startup-messages """ - return f"{self.name} - Filtering pairs priced below {self._low_price_ratio * 100}%." + active_price_filters = [] + if self._low_price_ratio != 0: + active_price_filters.append(f"below {self._low_price_ratio * 100}%") + if self._min_price != 0: + active_price_filters.append(f"below {self._min_price:.8f}") + if self._max_price != 0: + active_price_filters.append(f"above {self._max_price:.8f}") + + if len(active_price_filters): + return f"{self.name} - Filtering pairs priced {' or '.join(active_price_filters)}." + + return f"{self.name} - No price filters configured." def _validate_pair(self, ticker) -> bool: """ @@ -46,10 +61,28 @@ class PriceFilter(IPairList): f"Removed {ticker['symbol']} from whitelist, because " "ticker['last'] is empty (Usually no trade in the last 24h).") return False - compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last']) - changeperc = compare / ticker['last'] - if changeperc > self._low_price_ratio: - self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " - f"because 1 unit is {changeperc * 100:.3f}%") - return False + + # Perform low_price_ratio check. + if self._low_price_ratio != 0: + compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last']) + changeperc = compare / ticker['last'] + if changeperc > self._low_price_ratio: + self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " + f"because 1 unit is {changeperc * 100:.3f}%") + return False + + # Perform min_price check. + if self._min_price != 0: + if ticker['last'] < self._min_price: + self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " + f"because last price < {self._min_price:.8f}") + return False + + # Perform max_price check. + if self._max_price != 0: + if ticker['last'] > self._max_price: + self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " + f"because last price > {self._max_price:.8f}") + return False + return True diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index f95f001c9..09cbe9d5f 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -275,11 +275,16 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "PriceFilter", "low_price_ratio": 0.03}], "USDT", ['ETH/USDT', 'NANO/USDT']), - # Hot is removed by precision_filter, Fuel by low_price_filter. + # Hot is removed by precision_filter, Fuel by low_price_ratio, Ripple by min_price. ([{"method": "VolumePairList", "number_assets": 6, "sort_key": "quoteVolume"}, {"method": "PrecisionFilter"}, - {"method": "PriceFilter", "low_price_ratio": 0.02}], - "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']), + {"method": "PriceFilter", "low_price_ratio": 0.02, "min_price": 0.01}], + "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']), + # Hot is removed by precision_filter, Fuel by low_price_ratio, Ethereum by max_price. + ([{"method": "VolumePairList", "number_assets": 6, "sort_key": "quoteVolume"}, + {"method": "PrecisionFilter"}, + {"method": "PriceFilter", "low_price_ratio": 0.02, "max_price": 0.05}], + "BTC", ['TKN/BTC', 'LTC/BTC', 'XRP/BTC']), # HOT and XRP are removed because below 1250 quoteVolume ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume", "min_value": 1250}], @@ -319,7 +324,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): "BTC", 'filter_at_the_beginning'), # OperationalException expected # PriceFilter after StaticPairList ([{"method": "StaticPairList"}, - {"method": "PriceFilter", "low_price_ratio": 0.02}], + {"method": "PriceFilter", "low_price_ratio": 0.02, "min_price": 0.000001, "max_price": 0.1}], "BTC", ['ETH/BTC', 'TKN/BTC']), # PriceFilter only ([{"method": "PriceFilter", "low_price_ratio": 0.02}], @@ -396,6 +401,10 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t r'would be <= stop limit.*', caplog) if pairlist['method'] == 'PriceFilter' and whitelist_result: assert (log_has_re(r'^Removed .* from whitelist, because 1 unit is .*%$', caplog) or + log_has_re(r'^Removed .* from whitelist, ' + r'because last price < .*%$', caplog) or + log_has_re(r'^Removed .* from whitelist, ' + r'because last price > .*%$', caplog) or log_has_re(r"^Removed .* from whitelist, because ticker\['last'\] " r"is empty.*", caplog)) if pairlist['method'] == 'VolumePairList': From 40bdc93653bc97666145f914e5dcc2ea49b49c8a Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 10 Jul 2020 20:21:33 +0200 Subject: [PATCH 0247/1197] Add test for short_desc of priceFilter --- freqtrade/pairlist/PriceFilter.py | 6 +++--- tests/pairlist/test_pairlist.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/pairlist/PriceFilter.py index 1afc60999..5ee1df078 100644 --- a/freqtrade/pairlist/PriceFilter.py +++ b/freqtrade/pairlist/PriceFilter.py @@ -20,9 +20,9 @@ class PriceFilter(IPairList): self._low_price_ratio = pairlistconfig.get('low_price_ratio', 0) self._min_price = pairlistconfig.get('min_price', 0) self._max_price = pairlistconfig.get('max_price', 0) - self._enabled = (self._low_price_ratio != 0) or \ - (self._min_price != 0) or \ - (self._max_price != 0) + self._enabled = ((self._low_price_ratio != 0) or + (self._min_price != 0) or + (self._max_price != 0)) @property def needstickers(self) -> bool: diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 09cbe9d5f..cf54a09ae 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -588,6 +588,36 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_his assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count +@pytest.mark.parametrize("pairlistconfig,expected", [ + ({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010, + "max_price": 1.0}, "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below " + "0.1% or below 0.00000010 or above 1.00000000.'}]" + ), + ({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010}, + "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.1% or below 0.00000010.'}]" + ), + ({"method": "PriceFilter", "low_price_ratio": 0.001, "max_price": 1.00010000}, + "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.1% or above 1.00010000.'}]" + ), + ({"method": "PriceFilter", "min_price": 0.00002000}, + "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.00002000.'}]" + ), + ({"method": "PriceFilter"}, + "[{'PriceFilter': 'PriceFilter - No price filters configured.'}]" + ), +]) +def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, expected): + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True) + ) + whitelist_conf['pairlists'] = [pairlistconfig] + + freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) + short_desc = str(freqtrade.pairlists.short_desc()) + assert short_desc == expected + + def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog): mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) From 588043af86d86cce4b90f65e34a8f49e98767db8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Jul 2020 07:29:11 +0200 Subject: [PATCH 0248/1197] Fix documentation brackets, add delete trade hints --- docs/sql_cheatsheet.md | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index 1d396b8ce..3d34d6fe2 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -100,8 +100,8 @@ UPDATE trades SET is_open=0, close_date=, close_rate=, - close_profit=close_rate/open_rate-1, - close_profit_abs = (amount * * (1 - fee_close) - (amount * open_rate * 1 - fee_open)), + close_profit = close_rate / open_rate - 1, + close_profit_abs = (amount * * (1 - fee_close) - (amount * (open_rate * 1 - fee_open))), sell_reason= WHERE id=; ``` @@ -111,24 +111,39 @@ WHERE id=; ```sql UPDATE trades SET is_open=0, - close_date='2017-12-20 03:08:45.103418', + close_date='2020-06-20 03:08:45.103418', close_rate=0.19638016, close_profit=0.0496, - close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * open_rate * 1 - fee_open)) + close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * open_rate * (1 - fee_open))) sell_reason='force_sell' WHERE id=31; ``` -## Insert manually a new trade +## Manually insert a new trade ```sql INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date) -VALUES ('bittrex', 'ETH/BTC', 1, 0.0025, 0.0025, , , , '') +VALUES ('binance', 'ETH/BTC', 1, 0.0025, 0.0025, , , , '') ``` -##### Example: +### Insert trade example ```sql INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date) -VALUES ('bittrex', 'ETH/BTC', 1, 0.0025, 0.0025, 0.00258580, 0.002, 0.7715262081, '2017-11-28 12:44:24.000000') +VALUES ('binance', 'ETH/BTC', 1, 0.0025, 0.0025, 0.00258580, 0.002, 0.7715262081, '2020-06-28 12:44:24.000000') ``` + +## Remove trade from the database + +Maybe you'd like to remove a trade from the database, because something went wrong. + +```sql +DELETE FROM trades WHERE id = ; +``` + +```sql +DELETE FROM trades WHERE id = 31; +``` + +!!! Warning + This will remove this trade from the database. Please make sure you got the correct id and **NEVER** run this query without the where clause. From ecbca3fab023f7e82661fdd4eb10bb71c2a9e036 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Jul 2020 07:29:34 +0200 Subject: [PATCH 0249/1197] Add sqlite3 to dockerfile --- Dockerfile | 2 +- Dockerfile.armhf | 2 +- docs/sql_cheatsheet.md | 9 +++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index b6333fb13..29808b383 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM python:3.8.3-slim-buster RUN apt-get update \ - && apt-get -y install curl build-essential libssl-dev \ + && apt-get -y install curl build-essential libssl-dev sqlite3 \ && apt-get clean \ && pip install --upgrade pip diff --git a/Dockerfile.armhf b/Dockerfile.armhf index d6e2aa3a1..45ed2dac9 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,7 +1,7 @@ FROM --platform=linux/arm/v7 python:3.7.7-slim-buster RUN apt-get update \ - && apt-get -y install curl build-essential libssl-dev libatlas3-base libgfortran5 \ + && apt-get -y install curl build-essential libssl-dev libatlas3-base libgfortran5 sqlite3 \ && apt-get clean \ && pip install --upgrade pip \ && echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index 3d34d6fe2..56f76b5b7 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -13,6 +13,15 @@ Feel free to use a visual Database editor like SqliteBrowser if you feel more co sudo apt-get install sqlite3 ``` +### Using sqlite3 via docker-compose + +The freqtrade docker image does contain sqlite3, so you can edit the database without having to install anything on the host system. + +``` bash +docker-compose exec freqtrade /bin/bash +sqlite3 .sqlite +``` + ## Open the DB ```bash From f0a1a1720f861ba534be18d144b3ed08751975aa Mon Sep 17 00:00:00 2001 From: HumanBot Date: Sat, 11 Jul 2020 15:21:54 -0400 Subject: [PATCH 0250/1197] removed duplicate removed duplicate word using using --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index e7a79361a..09a1e76fe 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -275,7 +275,7 @@ the static list of pairs) if we should buy. The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds. This allows to buy using limit orders, sell using -limit-orders, and create stoplosses using using market orders. It also allows to set the +limit-orders, and create stoplosses using market orders. It also allows to set the stoploss "on exchange" which means stoploss order would be placed immediately once the buy order is fulfilled. If `stoploss_on_exchange` and `trailing_stop` are both set, then the bot will use `stoploss_on_exchange_interval` to check and update the stoploss on exchange periodically. From 422825ea1b57494716697e550884fd00b8bac80f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jul 2020 09:50:53 +0200 Subject: [PATCH 0251/1197] Add ohlcv_get_available_data to find available data --- freqtrade/data/history/idatahandler.py | 9 +++++++++ freqtrade/data/history/jsondatahandler.py | 14 +++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index d5d7c16db..e255710cb 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -13,6 +13,7 @@ from typing import List, Optional, Type from pandas import DataFrame from freqtrade.configuration import TimeRange +from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.converter import (clean_ohlcv_dataframe, trades_remove_duplicates, trim_dataframe) from freqtrade.exchange import timeframe_to_seconds @@ -28,6 +29,14 @@ class IDataHandler(ABC): def __init__(self, datadir: Path) -> None: self._datadir = datadir + @abstractclassmethod + def ohlcv_get_available_data(cls, datadir: Path) -> ListPairsWithTimeframes: + """ + Returns a list of all pairs with ohlcv data available in this datadir + :param datadir: Directory to search for ohlcv files + :return: List of Pair + """ + @abstractclassmethod def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]: """ diff --git a/freqtrade/data/history/jsondatahandler.py b/freqtrade/data/history/jsondatahandler.py index 01320f129..79a848d07 100644 --- a/freqtrade/data/history/jsondatahandler.py +++ b/freqtrade/data/history/jsondatahandler.py @@ -8,7 +8,8 @@ from pandas import DataFrame, read_json, to_datetime from freqtrade import misc from freqtrade.configuration import TimeRange -from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS +from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, + ListPairsWithTimeframes) from freqtrade.data.converter import trades_dict_to_list from .idatahandler import IDataHandler, TradeList @@ -21,6 +22,17 @@ class JsonDataHandler(IDataHandler): _use_zip = False _columns = DEFAULT_DATAFRAME_COLUMNS + @classmethod + def ohlcv_get_available_data(cls, datadir: Path) -> ListPairsWithTimeframes: + """ + Returns a list of all pairs with ohlcv data available in this datadir + :param datadir: Directory to search for ohlcv files + :return: List of Pair + """ + _tmp = [re.search(r'^([a-zA-Z_]+)\-(\S+)(?=.json)', p.name) + for p in datadir.glob(f"*.{cls._get_file_extension()}")] + return [(match[1].replace('_', '/'), match[2]) for match in _tmp if len(match.groups()) > 1] + @classmethod def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]: """ From d4fc52d2d5b6e9bd6a5fe06ce902ea6460375db4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jul 2020 09:56:46 +0200 Subject: [PATCH 0252/1197] Add tests for ohlcv_get_available_data --- freqtrade/data/history/jsondatahandler.py | 5 +++-- tests/data/test_history.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/history/jsondatahandler.py b/freqtrade/data/history/jsondatahandler.py index 79a848d07..4ef35cf55 100644 --- a/freqtrade/data/history/jsondatahandler.py +++ b/freqtrade/data/history/jsondatahandler.py @@ -29,9 +29,10 @@ class JsonDataHandler(IDataHandler): :param datadir: Directory to search for ohlcv files :return: List of Pair """ - _tmp = [re.search(r'^([a-zA-Z_]+)\-(\S+)(?=.json)', p.name) + _tmp = [re.search(r'^([a-zA-Z_]+)\-(\d+\S+)(?=.json)', p.name) for p in datadir.glob(f"*.{cls._get_file_extension()}")] - return [(match[1].replace('_', '/'), match[2]) for match in _tmp if len(match.groups()) > 1] + return [(match[1].replace('_', '/'), match[2]) for match in _tmp + if match and len(match.groups()) > 1] @classmethod def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]: diff --git a/tests/data/test_history.py b/tests/data/test_history.py index c2eb2d715..d84c212b1 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -631,6 +631,20 @@ def test_jsondatahandler_ohlcv_get_pairs(testdatadir): assert set(pairs) == {'UNITTEST/BTC'} +def test_jsondatahandler_ohlcv_get_available_data(testdatadir): + paircombs = JsonDataHandler.ohlcv_get_available_data(testdatadir) + # Convert to set to avoid failures due to sorting + assert set(paircombs) == {('UNITTEST/BTC', '5m'), ('ETH/BTC', '5m'), ('XLM/BTC', '5m'), + ('TRX/BTC', '5m'), ('LTC/BTC', '5m'), ('XMR/BTC', '5m'), + ('ZEC/BTC', '5m'), ('UNITTEST/BTC', '1m'), ('ADA/BTC', '5m'), + ('ETC/BTC', '5m'), ('NXT/BTC', '5m'), ('DASH/BTC', '5m'), + ('XRP/ETH', '1m'), ('XRP/ETH', '5m'), ('UNITTEST/BTC', '30m'), + ('UNITTEST/BTC', '8m')} + + paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir) + assert set(paircombs) == {('UNITTEST/BTC', '8m')} + + def test_jsondatahandler_trades_get_pairs(testdatadir): pairs = JsonGzDataHandler.trades_get_pairs(testdatadir) # Convert to set to avoid failures due to sorting From 02afde857d204f91d9170bb9bf22439b07e8c2cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jul 2020 09:57:00 +0200 Subject: [PATCH 0253/1197] Add list-data command --- freqtrade/commands/__init__.py | 3 ++- freqtrade/commands/arguments.py | 15 +++++++++++++-- freqtrade/commands/data_commands.py | 23 +++++++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index 2d0c7733c..4ce3eb421 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -9,7 +9,8 @@ Note: Be careful with file-scoped imports in these subfiles. from freqtrade.commands.arguments import Arguments from freqtrade.commands.build_config_commands import start_new_config from freqtrade.commands.data_commands import (start_convert_data, - start_download_data) + start_download_data, + start_list_data) from freqtrade.commands.deploy_commands import (start_create_userdir, start_new_hyperopt, start_new_strategy) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 72f2a02f0..a49d917a5 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -54,6 +54,8 @@ ARGS_BUILD_HYPEROPT = ["user_data_dir", "hyperopt", "template"] ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"] ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"] +ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv"] + ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchange", "timeframes", "erase", "dataformat_ohlcv", "dataformat_trades"] @@ -78,7 +80,7 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop "print_json", "hyperopt_show_no_header"] NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes", - "list-markets", "list-pairs", "list-strategies", + "list-markets", "list-pairs", "list-strategies", "list-data", "list-hyperopts", "hyperopt-list", "hyperopt-show", "plot-dataframe", "plot-profit", "show-trades"] @@ -159,7 +161,7 @@ class Arguments: self._build_args(optionlist=['version'], parser=self.parser) from freqtrade.commands import (start_create_userdir, start_convert_data, - start_download_data, + start_download_data, start_list_data, start_hyperopt_list, start_hyperopt_show, start_list_exchanges, start_list_hyperopts, start_list_markets, start_list_strategies, @@ -233,6 +235,15 @@ class Arguments: convert_trade_data_cmd.set_defaults(func=partial(start_convert_data, ohlcv=False)) self._build_args(optionlist=ARGS_CONVERT_DATA, parser=convert_trade_data_cmd) + # Add list-data subcommand + list_data_cmd = subparsers.add_parser( + 'list-data', + help='List downloaded data.', + parents=[_common_parser], + ) + list_data_cmd.set_defaults(func=start_list_data) + self._build_args(optionlist=ARGS_LIST_DATA, parser=list_data_cmd) + # Add backtesting subcommand backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.', parents=[_common_parser, _strategy_parser]) diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index fc3a49f1d..4a37bdc08 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -1,5 +1,6 @@ import logging import sys +from collections import defaultdict from typing import Any, Dict, List import arrow @@ -11,6 +12,7 @@ from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data, refresh_backtest_trades_data) from freqtrade.exceptions import OperationalException +from freqtrade.exchange import timeframe_to_minutes from freqtrade.resolvers import ExchangeResolver from freqtrade.state import RunMode @@ -88,3 +90,24 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None: convert_trades_format(config, convert_from=args['format_from'], convert_to=args['format_to'], erase=args['erase']) + + +def start_list_data(args: Dict[str, Any]) -> None: + """ + List available backtest data + """ + + config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) + + from freqtrade.data.history.idatahandler import get_datahandlerclass + from tabulate import tabulate + dhc = get_datahandlerclass(config['dataformat_ohlcv']) + paircombs = dhc.ohlcv_get_available_data(config['datadir']) + + print(f"Found {len(paircombs)} pair / timeframe combinations.") + groupedpair = defaultdict(list) + for pair, timeframe in sorted(paircombs, key=lambda x: (x[0], timeframe_to_minutes(x[1]))): + groupedpair[pair].append(timeframe) + + print(tabulate([(pair, ', '.join(timeframes)) for pair, timeframes in groupedpair.items()], + headers=("pairs", "timeframe"))) From 5bb81abce26b4e1bc44f7bc28cc0cf04397fea57 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jul 2020 10:01:37 +0200 Subject: [PATCH 0254/1197] Add test for start_list_data --- tests/commands/test_commands.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 46350beff..9c741e102 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -6,12 +6,12 @@ import pytest from freqtrade.commands import (start_convert_data, start_create_userdir, start_download_data, start_hyperopt_list, - start_hyperopt_show, start_list_exchanges, - start_list_hyperopts, start_list_markets, - start_list_strategies, start_list_timeframes, - start_new_hyperopt, start_new_strategy, - start_show_trades, start_test_pairlist, - start_trading) + start_hyperopt_show, start_list_data, + start_list_exchanges, start_list_hyperopts, + start_list_markets, start_list_strategies, + start_list_timeframes, start_new_hyperopt, + start_new_strategy, start_show_trades, + start_test_pairlist, start_trading) from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode @@ -1043,6 +1043,23 @@ def test_convert_data_trades(mocker, testdatadir): assert trades_mock.call_args[1]['erase'] is False +def test_start_list_data(testdatadir, capsys): + args = [ + "list-data", + "--data-format-ohlcv", + "json", + "--datadir", + str(testdatadir), + ] + pargs = get_args(args) + pargs['config'] = None + start_list_data(pargs) + captured = capsys.readouterr() + assert "Found 16 pair / timeframe combinations." in captured.out + assert "\npairs timeframe\n" in captured.out + assert "\nUNITTEST/BTC 1m, 5m, 8m, 30m\n" in captured.out + + @pytest.mark.usefixtures("init_persistence") def test_show_trades(mocker, fee, capsys, caplog): mocker.patch("freqtrade.persistence.init") From 33c3990972495f1754744b98775f63e99a85aa0b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jul 2020 10:05:47 +0200 Subject: [PATCH 0255/1197] Add documentation for list-data command --- docs/data-download.md | 49 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/docs/data-download.md b/docs/data-download.md index 3fb775e69..7fbad0b6c 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -158,6 +158,55 @@ It'll also remove original jsongz data files (`--erase` parameter). freqtrade convert-trade-data --format-from jsongz --format-to json --datadir ~/.freqtrade/data/kraken --erase ``` +### Subcommand list-data + +You can get a list of downloaded data using the `list-data` subcommand. + +``` +usage: freqtrade list-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] + [--userdir PATH] [--exchange EXCHANGE] + [--data-format-ohlcv {json,jsongz}] + +optional arguments: + -h, --help show this help message and exit + --exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no + config is provided. + --data-format-ohlcv {json,jsongz} + Storage format for downloaded candle (OHLCV) data. + (default: `json`). + +Common arguments: + -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). + --logfile FILE Log to the file specified. Special values are: + 'syslog', 'journald'. See the documentation for more + details. + -V, --version show program's version number and exit + -c PATH, --config PATH + Specify configuration file (default: + `userdir/config.json` or `config.json` whichever + exists). Multiple --config options may be used. Can be + set to `-` to read config from stdin. + -d PATH, --datadir PATH + Path to directory with historical backtesting data. + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + +``` + +#### Example list-data + +```bash +> freqtrade list-data --userdir ~/.freqtrade/user_data/ + +Found 33 pair / timeframe combinations. +pairs timeframe +---------- ----------------------------------------- +ADA/BTC 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d +ADA/ETH 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d +ETH/BTC 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d +ETH/USDT 5m, 15m, 30m, 1h, 2h, 4h +``` + ### Pairs file In alternative to the whitelist from `config.json`, a `pairs.json` file can be used. From b035d9e2671b57cbb09ab340d432349b934182b1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jul 2020 10:23:09 +0200 Subject: [PATCH 0256/1197] Update return type comment --- freqtrade/commands/data_commands.py | 5 +++-- freqtrade/data/history/idatahandler.py | 2 +- freqtrade/data/history/jsondatahandler.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 4a37bdc08..d3f70b9ec 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -99,9 +99,10 @@ def start_list_data(args: Dict[str, Any]) -> None: config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) - from freqtrade.data.history.idatahandler import get_datahandlerclass + from freqtrade.data.history.idatahandler import get_datahandler from tabulate import tabulate - dhc = get_datahandlerclass(config['dataformat_ohlcv']) + dhc = get_datahandler(config['datadir'], config['dataformat_ohlcv']) + paircombs = dhc.ohlcv_get_available_data(config['datadir']) print(f"Found {len(paircombs)} pair / timeframe combinations.") diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index e255710cb..96d288e01 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -34,7 +34,7 @@ class IDataHandler(ABC): """ Returns a list of all pairs with ohlcv data available in this datadir :param datadir: Directory to search for ohlcv files - :return: List of Pair + :return: List of Tuples of (pair, timeframe) """ @abstractclassmethod diff --git a/freqtrade/data/history/jsondatahandler.py b/freqtrade/data/history/jsondatahandler.py index 4ef35cf55..2e7c0f773 100644 --- a/freqtrade/data/history/jsondatahandler.py +++ b/freqtrade/data/history/jsondatahandler.py @@ -27,7 +27,7 @@ class JsonDataHandler(IDataHandler): """ Returns a list of all pairs with ohlcv data available in this datadir :param datadir: Directory to search for ohlcv files - :return: List of Pair + :return: List of Tuples of (pair, timeframe) """ _tmp = [re.search(r'^([a-zA-Z_]+)\-(\d+\S+)(?=.json)', p.name) for p in datadir.glob(f"*.{cls._get_file_extension()}")] From ed2e35ba5d01fda20ec867d1b28ca020c78752f3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jul 2020 12:36:16 +0200 Subject: [PATCH 0257/1197] Update docs/sql_cheatsheet.md Co-authored-by: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> --- docs/sql_cheatsheet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index 56f76b5b7..f4cb473ff 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -155,4 +155,4 @@ DELETE FROM trades WHERE id = 31; ``` !!! Warning - This will remove this trade from the database. Please make sure you got the correct id and **NEVER** run this query without the where clause. + This will remove this trade from the database. Please make sure you got the correct id and **NEVER** run this query without the `where` clause. From 79af6180bddee3a442c730b24d47f4496980b402 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2020 09:00:50 +0000 Subject: [PATCH 0258/1197] Bump pytest-mock from 3.1.1 to 3.2.0 Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.1.1 to 3.2.0. - [Release notes](https://github.com/pytest-dev/pytest-mock/releases) - [Changelog](https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.1.1...v3.2.0) 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 ed4f8f713..249aa9089 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,7 +11,7 @@ mypy==0.782 pytest==5.4.3 pytest-asyncio==0.14.0 pytest-cov==2.10.0 -pytest-mock==3.1.1 +pytest-mock==3.2.0 pytest-random-order==1.0.4 # Convert jupyter notebooks to markdown documents From 58eb26d73a44d782a56ae5a64bf5477dbde3f98e Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2020 09:01:14 +0000 Subject: [PATCH 0259/1197] Bump pycoingecko from 1.2.0 to 1.3.0 Bumps [pycoingecko](https://github.com/man-c/pycoingecko) from 1.2.0 to 1.3.0. - [Release notes](https://github.com/man-c/pycoingecko/releases) - [Changelog](https://github.com/man-c/pycoingecko/blob/master/CHANGELOG.md) - [Commits](https://github.com/man-c/pycoingecko/compare/1.2.0...1.3.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 2f225c93c..34340612d 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -11,7 +11,7 @@ wrapt==1.12.1 jsonschema==3.2.0 TA-Lib==0.4.18 tabulate==0.8.7 -pycoingecko==1.2.0 +pycoingecko==1.3.0 jinja2==2.11.2 # find first, C search in arrays From d1e4e463ae7d2c9f7361e68581a1617652869ba6 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2020 09:01:58 +0000 Subject: [PATCH 0260/1197] Bump ccxt from 1.30.64 to 1.30.93 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.30.64 to 1.30.93. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.30.64...1.30.93) 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 2f225c93c..fc38f4ee6 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.30.64 +ccxt==1.30.93 SQLAlchemy==1.3.18 python-telegram-bot==12.8 arrow==0.15.7 From 50573bd3978464d012f5c76408a4699b19c86fc0 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2020 09:02:07 +0000 Subject: [PATCH 0261/1197] Bump coveralls from 2.0.0 to 2.1.1 Bumps [coveralls](https://github.com/coveralls-clients/coveralls-python) from 2.0.0 to 2.1.1. - [Release notes](https://github.com/coveralls-clients/coveralls-python/releases) - [Changelog](https://github.com/coveralls-clients/coveralls-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/coveralls-clients/coveralls-python/compare/2.0.0...2.1.1) 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 ed4f8f713..2b9c4c8f9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,7 +3,7 @@ -r requirements-plot.txt -r requirements-hyperopt.txt -coveralls==2.0.0 +coveralls==2.1.1 flake8==3.8.3 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 From 0b36693accbf5b0435ebf5daeea0485737d443dd Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Jul 2020 19:48:21 +0200 Subject: [PATCH 0262/1197] Add filter for stoploss_on_exchange_limit_ratio to constants --- freqtrade/constants.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 8a5332475..1dadc6e16 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -156,7 +156,9 @@ CONF_SCHEMA = { 'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'stoploss_on_exchange': {'type': 'boolean'}, - 'stoploss_on_exchange_interval': {'type': 'number'} + 'stoploss_on_exchange_interval': {'type': 'number'}, + 'stoploss_on_exchange_limit_ratio': {'type': 'number', 'minimum': 0.0, + 'maximum': 1.0} }, 'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] }, From 01f325a9e4cd65a0bb117f031f25d0da593002c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Jul 2020 21:15:33 +0200 Subject: [PATCH 0263/1197] Send timeframe min and ms in show_config response --- freqtrade/rpc/rpc.py | 4 ++++ tests/rpc/test_rpc_apiserver.py | 2 ++ 2 files changed, 6 insertions(+) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e0eb12d23..c73fcbf54 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -12,6 +12,8 @@ import arrow from numpy import NAN, mean from freqtrade.exceptions import ExchangeError, PricingError + +from freqtrade.exchange import timeframe_to_msecs, timeframe_to_minutes from freqtrade.misc import shorten_date from freqtrade.persistence import Trade from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -103,6 +105,8 @@ class RPC: 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'), 'ticker_interval': config['timeframe'], # DEPRECATED 'timeframe': config['timeframe'], + 'timeframe_ms': timeframe_to_msecs(config['timeframe']), + 'timeframe_min': timeframe_to_minutes(config['timeframe']), 'exchange': config['exchange']['name'], 'strategy': config['strategy'], 'forcebuy_enabled': config.get('forcebuy_enable', False), diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 45aa57588..355b63f48 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -326,6 +326,8 @@ def test_api_show_config(botclient, mocker): assert rc.json['exchange'] == 'bittrex' assert rc.json['ticker_interval'] == '5m' assert rc.json['timeframe'] == '5m' + assert rc.json['timeframe_ms'] == 300000 + assert rc.json['timeframe_min'] == 5 assert rc.json['state'] == 'running' assert not rc.json['trailing_stop'] assert 'bid_strategy' in rc.json From 62c55b18631398c7447b5eed355fa495f0a299af Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Jul 2020 06:55:34 +0200 Subject: [PATCH 0264/1197] Enhance formatting, Add pair filter --- docs/data-download.md | 5 ++++- freqtrade/commands/arguments.py | 2 +- freqtrade/commands/data_commands.py | 6 +++++- tests/commands/test_commands.py | 21 +++++++++++++++++++-- 4 files changed, 29 insertions(+), 5 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index 7fbad0b6c..a2bbec837 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -166,6 +166,7 @@ You can get a list of downloaded data using the `list-data` subcommand. usage: freqtrade list-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [--exchange EXCHANGE] [--data-format-ohlcv {json,jsongz}] + [-p PAIRS [PAIRS ...]] optional arguments: -h, --help show this help message and exit @@ -174,6 +175,9 @@ optional arguments: --data-format-ohlcv {json,jsongz} Storage format for downloaded candle (OHLCV) data. (default: `json`). + -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] + Show profits for only these pairs. Pairs are space- + separated. Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). @@ -190,7 +194,6 @@ Common arguments: Path to directory with historical backtesting data. --userdir PATH, --user-data-dir PATH Path to userdata directory. - ``` #### Example list-data diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index a49d917a5..e6f6f8167 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -54,7 +54,7 @@ ARGS_BUILD_HYPEROPT = ["user_data_dir", "hyperopt", "template"] ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"] ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"] -ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv"] +ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"] ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchange", "timeframes", "erase", "dataformat_ohlcv", "dataformat_trades"] diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index d3f70b9ec..13b796a1e 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -105,10 +105,14 @@ def start_list_data(args: Dict[str, Any]) -> None: paircombs = dhc.ohlcv_get_available_data(config['datadir']) + if args['pairs']: + paircombs = [comb for comb in paircombs if comb[0] in args['pairs']] + print(f"Found {len(paircombs)} pair / timeframe combinations.") groupedpair = defaultdict(list) for pair, timeframe in sorted(paircombs, key=lambda x: (x[0], timeframe_to_minutes(x[1]))): groupedpair[pair].append(timeframe) print(tabulate([(pair, ', '.join(timeframes)) for pair, timeframes in groupedpair.items()], - headers=("pairs", "timeframe"))) + headers=("Pair", "Timeframe"), + tablefmt='psql', stralign='right')) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 9c741e102..ffced956d 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1056,8 +1056,25 @@ def test_start_list_data(testdatadir, capsys): start_list_data(pargs) captured = capsys.readouterr() assert "Found 16 pair / timeframe combinations." in captured.out - assert "\npairs timeframe\n" in captured.out - assert "\nUNITTEST/BTC 1m, 5m, 8m, 30m\n" in captured.out + assert "\n| Pair | Timeframe |\n" in captured.out + assert "\n| UNITTEST/BTC | 1m, 5m, 8m, 30m |\n" in captured.out + + args = [ + "list-data", + "--data-format-ohlcv", + "json", + "--pairs", "XRP/ETH", + "--datadir", + str(testdatadir), + ] + pargs = get_args(args) + pargs['config'] = None + start_list_data(pargs) + captured = capsys.readouterr() + assert "Found 2 pair / timeframe combinations." in captured.out + assert "\n| Pair | Timeframe |\n" in captured.out + assert "UNITTEST/BTC" not in captured.out + assert "\n| XRP/ETH | 1m, 5m |\n" in captured.out @pytest.mark.usefixtures("init_persistence") From ae55d54967b22c0b9d41d533194ebb96a3b63d82 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 14 Jul 2020 06:33:57 +0000 Subject: [PATCH 0265/1197] Bump python from 3.8.3-slim-buster to 3.8.4-slim-buster Bumps python from 3.8.3-slim-buster to 3.8.4-slim-buster. Signed-off-by: dependabot-preview[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 29808b383..f27167cc5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8.3-slim-buster +FROM python:3.8.4-slim-buster RUN apt-get update \ && apt-get -y install curl build-essential libssl-dev sqlite3 \ From 0228b63418bb35d680c9c4bfe8e0c2b492cf3b6a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Jul 2020 16:42:47 +0200 Subject: [PATCH 0266/1197] Don't print empty table --- freqtrade/commands/data_commands.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 13b796a1e..aa0b826b5 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -113,6 +113,7 @@ def start_list_data(args: Dict[str, Any]) -> None: for pair, timeframe in sorted(paircombs, key=lambda x: (x[0], timeframe_to_minutes(x[1]))): groupedpair[pair].append(timeframe) - print(tabulate([(pair, ', '.join(timeframes)) for pair, timeframes in groupedpair.items()], - headers=("Pair", "Timeframe"), - tablefmt='psql', stralign='right')) + if groupedpair: + print(tabulate([(pair, ', '.join(timeframes)) for pair, timeframes in groupedpair.items()], + headers=("Pair", "Timeframe"), + tablefmt='psql', stralign='right')) From 2417898d0078143be347d50cecacfe04a9900fae Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Jul 2020 19:27:52 +0200 Subject: [PATCH 0267/1197] Apply documentation suggestions from code review Co-authored-by: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> --- docs/backtesting.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index cb20c3e43..7d6759df0 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -210,18 +210,18 @@ Hence, keep in mind that your performance is an integral mix of all different el ### Sell reasons table The 2nd table contains a recap of sell reasons. -This table can tell you which area needs some additional work (i.e. all `sell_signal` trades are losses, so we should disable the sell-signal or work on improving that). +This table can tell you which area needs some additional work (e,g. all or many of the `sell_signal` trades are losses, so we should disable the sell-signal or work on improving that). ### Left open trades table -The 3rd table contains all trades the bot had to `forcesell` at the end of the backtest period to present a full picture. +The 3rd table contains all trades the bot had to `forcesell` at the end of the backtesting period to present you the full picture. This is necessary to simulate realistic behaviour, since the backtest period has to end at some point, while realistically, you could leave the bot running forever. -These trades are also included in the first table, but are extracted separately for clarity. +These trades are also included in the first table, but are also shown separately in this table for clarity. ### Summary metrics The last element of the backtest report is the summary metrics table. -It contains some useful key metrics about your strategy. +It contains some useful key metrics about performance of your strategy on backtesting data. ``` =============== SUMMARY METRICS =============== @@ -250,12 +250,12 @@ It contains some useful key metrics about your strategy. - `Total trades`: Identical to the total trades of the backtest output table. - `First trade`: First trade entered. - `First trade pair`: Which pair was part of the first trade. -- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined as `--timerange from-to`). -- `Trades per day`: Total trades / Backtest duration (this will give you information about how many trades to expect from the strategy). +- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option). +- `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). - `Best day` / `Worst day`: Best and worst day based on daily profit. - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. -- `Max Drawdown`: Maximum drawown experienced. a value of 50% means that from highest to subsequent lowest point, a 50% drop was experiened). -- `Drawdown Start` / `Drawdown End`: From when to when was this large drawdown (can also be visualized via `plot-dataframe` subcommand). +- `Max Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced). +- `Drawdown Start` / `Drawdown End`: Start and end datetimes for this largest drawdown (can also be visualized via the `plot-dataframe` subcommand). - `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column. ### Assumptions made by backtesting From bdf611352e7e516d6789cb6b5a40f89c38953f59 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Jul 2020 19:34:01 +0200 Subject: [PATCH 0268/1197] Update summary-metrics output --- docs/backtesting.md | 40 ++++++++++++++------------ freqtrade/optimize/optimize_reports.py | 4 +-- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 7d6759df0..b1dcd5dba 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -157,22 +157,25 @@ A backtesting result will look like that: | ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 | 0 | 0 | | LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 | 0 | 0 | | TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 | 0 | 0 | -============ SUMMARY METRICS ============= -| Metric | Value | -|------------------+---------------------| -| Total trades | 429 | -| First trade | 2019-01-01 18:30:00 | -| First trade Pair | EOS/USDT | -| Backtesting from | 2019-01-01 00:00:00 | -| Backtesting to | 2019-05-01 00:00:00 | -| Trades per day | 3.575 | -| | | -| Max Drawdown | 50.63% | -| Drawdown Start | 2019-02-15 14:10:00 | -| Drawdown End | 2019-04-11 18:15:00 | -| Market change | -5.88% | -========================================== - +=============== SUMMARY METRICS =============== +| Metric | Value | +|-----------------------+---------------------| +| Backtesting from | 2019-01-01 00:00:00 | +| Backtesting to | 2019-05-01 00:00:00 | +| Total trades | 429 | +| First trade | 2019-01-01 18:30:00 | +| First trade Pair | EOS/USDT | +| Trades per day | 3.575 | +| Best day | 25.27% | +| Worst day | -30.67% | +| Avg. Duration Winners | 4:23:00 | +| Avg. Duration Loser | 6:55:00 | +| | | +| Max Drawdown | 50.63% | +| Drawdown Start | 2019-02-15 14:10:00 | +| Drawdown End | 2019-04-11 18:15:00 | +| Market change | -5.88% | +=============================================== ``` ### Backtesting report table @@ -227,12 +230,11 @@ It contains some useful key metrics about performance of your strategy on backte =============== SUMMARY METRICS =============== | Metric | Value | |-----------------------+---------------------| - +| Backtesting from | 2019-01-01 00:00:00 | +| Backtesting to | 2019-05-01 00:00:00 | | Total trades | 429 | | First trade | 2019-01-01 18:30:00 | | First trade Pair | EOS/USDT | -| Backtesting from | 2019-01-01 00:00:00 | -| Backtesting to | 2019-05-01 00:00:00 | | Trades per day | 3.575 | | Best day | 25.27% | | Worst day | -30.67% | diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 63fbfb48c..3a42ba4a9 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -367,11 +367,11 @@ def text_table_add_metrics(strat_results: Dict) -> str: if len(strat_results['trades']) > 0: min_trade = min(strat_results['trades'], key=lambda x: x['open_date']) metrics = [ + ('Backtesting from', strat_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)), + ('Backtesting to', strat_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)), ('Total trades', strat_results['total_trades']), ('First trade', min_trade['open_date'].strftime(DATETIME_PRINT_FORMAT)), ('First trade Pair', min_trade['pair']), - ('Backtesting from', strat_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)), - ('Backtesting to', strat_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)), ('Trades per day', strat_results['trades_per_day']), ('Best day', f"{round(strat_results['backtest_best_day'] * 100, 2)}%"), ('Worst day', f"{round(strat_results['backtest_worst_day'] * 100, 2)}%"), From 82c68f07cd281e86ea39db9571cd3a4cae6a7481 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Jul 2020 20:16:18 +0200 Subject: [PATCH 0269/1197] Add stoploss-distance (to current price) to /status output --- freqtrade/rpc/rpc.py | 1 + freqtrade/rpc/telegram.py | 21 +++++++++++---------- tests/rpc/test_rpc.py | 2 ++ tests/rpc/test_rpc_apiserver.py | 1 + tests/rpc/test_rpc_telegram.py | 4 +++- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c73fcbf54..4e4d3ed6a 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -158,6 +158,7 @@ class RPC: current_profit_abs=current_profit_abs, stoploss_current_dist=stoploss_current_dist, stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8), + stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2), stoploss_entry_dist=stoploss_entry_dist, stoploss_entry_dist_ratio=round(stoploss_entry_dist_ratio, 8), open_order='({} {} rem={:.8f})'.format( diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 13cc1afaf..09be60795 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -236,17 +236,18 @@ class Telegram(RPC): ("*Close Profit:* `{close_profit_pct}`" if r['close_profit_pct'] is not None else ""), "*Current Profit:* `{current_profit_pct:.2f}%`", - - # Adding initial stoploss only if it is different from stoploss - "*Initial Stoploss:* `{initial_stop_loss:.8f}` " + - ("`({initial_stop_loss_pct:.2f}%)`") if ( - r['stop_loss'] != r['initial_stop_loss'] - and r['initial_stop_loss_pct'] is not None) else "", - - # Adding stoploss and stoploss percentage only if it is not None - "*Stoploss:* `{stop_loss:.8f}` " + - ("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else ""), ] + if (r['stop_loss'] != r['initial_stop_loss'] + and r['initial_stop_loss_pct'] is not None): + # Adding initial stoploss only if it is different from stoploss + lines.append("*Initial Stoploss:* `{initial_stop_loss:.8f}` " + "`({initial_stop_loss_pct:.2f}%)`") + + # Adding stoploss and stoploss percentage only if it is not None + lines.append("*Stoploss:* `{stop_loss:.8f}` " + + ("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else "")) + lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` " + "`({stoploss_current_dist_pct:.2f}%)`") if r['open_order']: if r['sell_order_status']: lines.append("*Open Order:* `{open_order}` - `{sell_order_status}`") diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index de9327ba9..6d2c38868 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -100,6 +100,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'initial_stop_loss_ratio': -0.1, 'stoploss_current_dist': -1.1080000000000002e-06, 'stoploss_current_dist_ratio': -0.10081893, + 'stoploss_current_dist_pct': -10.08, 'stoploss_entry_dist': -0.00010475, 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, @@ -163,6 +164,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'initial_stop_loss_ratio': -0.1, 'stoploss_current_dist': ANY, 'stoploss_current_dist_ratio': ANY, + 'stoploss_current_dist_pct': ANY, 'stoploss_entry_dist': -0.00010475, 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 355b63f48..04f9fc493 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -552,6 +552,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'initial_stop_loss_ratio': -0.1, 'stoploss_current_dist': -1.1080000000000002e-06, 'stoploss_current_dist_ratio': -0.10081893, + 'stoploss_current_dist_pct': -10.08, 'stoploss_entry_dist': -0.00010475, 'stoploss_entry_dist_ratio': -0.10448878, 'trade_id': 1, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 0a4352f5b..a13d2e6c7 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -144,7 +144,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None: assert log_has('Exception occurred within Telegram module', caplog) -def test_status(default_conf, update, mocker, fee, ticker,) -> None: +def test_telegram_status(default_conf, update, mocker, fee, ticker,) -> None: update.message.chat.id = "123" default_conf['telegram']['enabled'] = False default_conf['telegram']['chat_id'] = "123" @@ -174,6 +174,8 @@ def test_status(default_conf, update, mocker, fee, ticker,) -> None: 'stop_loss': 1.099e-05, 'sell_order_status': None, 'initial_stop_loss_pct': -0.05, + 'stoploss_current_dist': 1e-08, + 'stoploss_current_dist_pct': -0.02, 'stop_loss_pct': -0.01, 'open_order': '(limit buy rem=0.00000000)' }]), From 1051ab917ad999d09eb2964940a5e3f5381dc22c Mon Sep 17 00:00:00 2001 From: gambcl Date: Wed, 15 Jul 2020 12:40:54 +0100 Subject: [PATCH 0270/1197] Replaced logging with OperationalException when AgeFilter given invalid parameters --- freqtrade/pairlist/AgeFilter.py | 12 +++++------- tests/pairlist/test_pairlist.py | 15 +++++++-------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index 101f19cbe..7b6b126c3 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -5,6 +5,7 @@ import logging import arrow from typing import Any, Dict +from freqtrade.exceptions import OperationalException from freqtrade.misc import plural from freqtrade.pairlist.IPairList import IPairList @@ -25,14 +26,11 @@ class AgeFilter(IPairList): self._min_days_listed = pairlistconfig.get('min_days_listed', 10) if self._min_days_listed < 1: - self.log_on_refresh(logger.info, "min_days_listed must be >= 1, " - "ignoring filter") + raise OperationalException("AgeFilter requires min_days_listed must be >= 1") if self._min_days_listed > exchange.ohlcv_candle_limit: - self._min_days_listed = min(self._min_days_listed, exchange.ohlcv_candle_limit) - self.log_on_refresh(logger.info, "min_days_listed exceeds " - "exchange max request size " - f"({exchange.ohlcv_candle_limit}), using " - f"min_days_listed={self._min_days_listed}") + raise OperationalException("AgeFilter requires min_days_listed must not exceed " + "exchange max request size " + f"({exchange.ohlcv_candle_limit})") self._enabled = self._min_days_listed >= 1 @property diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index cf54a09ae..e23102162 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -543,10 +543,9 @@ def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tick get_tickers=tickers ) - get_patched_freqtradebot(mocker, default_conf) - - assert log_has_re(r'min_days_listed must be >= 1, ' - r'ignoring filter', caplog) + with pytest.raises(OperationalException, + match=r'AgeFilter requires min_days_listed must be >= 1'): + get_patched_freqtradebot(mocker, default_conf) def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tickers, caplog): @@ -559,10 +558,10 @@ def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tick get_tickers=tickers ) - get_patched_freqtradebot(mocker, default_conf) - - assert log_has_re(r'^min_days_listed exceeds ' - r'exchange max request size', caplog) + with pytest.raises(OperationalException, + match=r'AgeFilter requires min_days_listed must not exceed ' + r'exchange max request size \([0-9]+\)'): + get_patched_freqtradebot(mocker, default_conf) def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_history_list): From c1191400a4f5c705b394e209c30810dd0d1e669f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Jul 2020 19:20:07 +0200 Subject: [PATCH 0271/1197] Allow 0 fee value by correctly checking for None --- freqtrade/optimize/backtesting.py | 2 +- tests/optimize/test_backtesting.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index e5014dd5a..214c92e0e 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -101,7 +101,7 @@ class Backtesting: if len(self.pairlists.whitelist) == 0: raise OperationalException("No pair in whitelist.") - if config.get('fee'): + if config.get('fee', None) is not None: self.fee = config['fee'] else: self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0]) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 67da38648..caa40fe84 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -308,6 +308,11 @@ def test_data_with_fee(default_conf, mocker, testdatadir) -> None: assert backtesting.fee == 0.1234 assert fee_mock.call_count == 0 + default_conf['fee'] = 0.0 + backtesting = Backtesting(default_conf) + assert backtesting.fee == 0.0 + assert fee_mock.call_count == 0 + def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None: patch_exchange(mocker) From 5cebc9f39df700205711776c23949e7fc57ee7eb Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Jul 2020 19:28:40 +0200 Subject: [PATCH 0272/1197] Move stoploss_on_exchange_limit_ratio to configuration schema --- freqtrade/exchange/exchange.py | 7 ------- tests/exchange/test_exchange.py | 22 ---------------------- tests/test_configuration.py | 8 ++++++++ 3 files changed, 8 insertions(+), 29 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index d91a33926..fd9c83d51 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -389,13 +389,6 @@ class Exchange: f'On exchange stoploss is not supported for {self.name}.' ) - # Limit price threshold: As limit price should always be below stop-price - # Used for limit stoplosses on exchange - limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) - if limit_price_pct >= 1.0 or limit_price_pct <= 0.0: - raise OperationalException( - "stoploss_on_exchange_limit_ratio should be < 1.0 and > 0.0") - def validate_order_time_in_force(self, order_time_in_force: Dict) -> None: """ Checks if order time in force configured in strategy/config are supported diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 1101f3e74..60c4847f6 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -746,28 +746,6 @@ def test_validate_order_types(default_conf, mocker): match=r'On exchange stoploss is not supported for .*'): Exchange(default_conf) - default_conf['order_types'] = { - 'buy': 'limit', - 'sell': 'limit', - 'stoploss': 'limit', - 'stoploss_on_exchange': False, - 'stoploss_on_exchange_limit_ratio': 1.05 - } - with pytest.raises(OperationalException, - match=r'stoploss_on_exchange_limit_ratio should be < 1.0 and > 0.0'): - Exchange(default_conf) - - default_conf['order_types'] = { - 'buy': 'limit', - 'sell': 'limit', - 'stoploss': 'limit', - 'stoploss_on_exchange': False, - 'stoploss_on_exchange_limit_ratio': -0.1 - } - with pytest.raises(OperationalException, - match=r'stoploss_on_exchange_limit_ratio should be < 1.0 and > 0.0'): - Exchange(default_conf) - def test_validate_order_types_not_in_config(default_conf, mocker): api_mock = MagicMock() diff --git a/tests/test_configuration.py b/tests/test_configuration.py index cccc87670..ca5d6eadc 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -871,6 +871,14 @@ def test_load_config_default_exchange_name(all_conf) -> None: validate_config_schema(all_conf) +def test_load_config_stoploss_exchange_limit_ratio(all_conf) -> None: + all_conf['order_types']['stoploss_on_exchange_limit_ratio'] = 1.15 + + with pytest.raises(ValidationError, + match=r"1.15 is greater than the maximum"): + validate_config_schema(all_conf) + + @pytest.mark.parametrize("keys", [("exchange", "sandbox", False), ("exchange", "key", ""), ("exchange", "secret", ""), From d13cb4c05569e1116f07f5fef0fb896f42c13b7d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Jul 2020 19:49:51 +0200 Subject: [PATCH 0273/1197] Introduce safe_value_fallback_2 --- freqtrade/exchange/exchange.py | 6 ++--- freqtrade/freqtradebot.py | 4 ++-- freqtrade/misc.py | 16 ++++++++++++- tests/test_misc.py | 42 +++++++++++++++++++++++----------- 4 files changed, 49 insertions(+), 19 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a3a548176..5c4e4c530 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -24,7 +24,7 @@ from freqtrade.exceptions import (DDosProtection, ExchangeError, InvalidOrderException, OperationalException, RetryableOrderError, TemporaryError) from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async -from freqtrade.misc import deep_merge_dicts, safe_value_fallback +from freqtrade.misc import deep_merge_dicts, safe_value_fallback2 CcxtModuleType = Any @@ -1139,7 +1139,7 @@ class Exchange: if fee_curr in self.get_pair_base_currency(order['symbol']): # Base currency - divide by amount return round( - order['fee']['cost'] / safe_value_fallback(order, order, 'filled', 'amount'), 8) + order['fee']['cost'] / safe_value_fallback2(order, order, 'filled', 'amount'), 8) elif fee_curr in self.get_pair_quote_currency(order['symbol']): # Quote currency - divide by cost return round(order['fee']['cost'] / order['cost'], 8) if order['cost'] else None @@ -1152,7 +1152,7 @@ class Exchange: comb = self.get_valid_pair_combination(fee_curr, self._config['stake_currency']) tick = self.fetch_ticker(comb) - fee_to_quote_rate = safe_value_fallback(tick, tick, 'last', 'ask') + fee_to_quote_rate = safe_value_fallback2(tick, tick, 'last', 'ask') return round((order['fee']['cost'] * fee_to_quote_rate) / order['cost'], 8) except ExchangeError: return None diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ab7c2b527..9a0b3c40f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -20,7 +20,7 @@ from freqtrade.edge import Edge from freqtrade.exceptions import (DependencyException, ExchangeError, InvalidOrderException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date -from freqtrade.misc import safe_value_fallback +from freqtrade.misc import safe_value_fallback2 from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -984,7 +984,7 @@ class FreqtradeBot: logger.info('Buy order %s for %s.', reason, trade) # Using filled to determine the filled amount - filled_amount = safe_value_fallback(corder, order, 'filled', 'filled') + filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled') if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC): logger.info('Buy order fully cancelled. Removing %s from database.', trade) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index ac6084eb7..623f6cb8f 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -134,7 +134,21 @@ def round_dict(d, n): return {k: (round(v, n) if isinstance(v, float) else v) for k, v in d.items()} -def safe_value_fallback(dict1: dict, dict2: dict, key1: str, key2: str, default_value=None): +def safe_value_fallback(obj: dict, key1: str, key2: str, default_value=None): + """ + Search a value in obj, return this if it's not None. + Then search key2 in obj - return that if it's not none - then use default_value. + Else falls back to None. + """ + if key1 in obj and obj[key1] is not None: + return obj[key1] + else: + if key2 in obj and obj[key2] is not None: + return obj[key2] + return default_value + + +def safe_value_fallback2(dict1: dict, dict2: dict, key1: str, key2: str, default_value=None): """ Search a value in dict1, return this if it's not None. Fall back to dict2 - return key2 from dict2 if it's not None. diff --git a/tests/test_misc.py b/tests/test_misc.py index 9fd6164d5..a185cbba4 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -11,7 +11,7 @@ from freqtrade.misc import (datesarray_to_datetimearray, file_dump_json, file_load_json, format_ms_time, pair_to_filename, plural, render_template, render_template_with_fallback, safe_value_fallback, - shorten_date) + safe_value_fallback2, shorten_date) def test_shorten_date() -> None: @@ -96,24 +96,40 @@ def test_format_ms_time() -> None: def test_safe_value_fallback(): + dict1 = {'keya': None, 'keyb': 2, 'keyc': 5, 'keyd': None} + assert safe_value_fallback(dict1, 'keya', 'keyb') == 2 + assert safe_value_fallback(dict1, 'keyb', 'keya') == 2 + + assert safe_value_fallback(dict1, 'keyb', 'keyc') == 2 + assert safe_value_fallback(dict1, 'keya', 'keyc') == 5 + + assert safe_value_fallback(dict1, 'keyc', 'keyb') == 5 + + assert safe_value_fallback(dict1, 'keya', 'keyd') is None + + assert safe_value_fallback(dict1, 'keyNo', 'keyNo') is None + assert safe_value_fallback(dict1, 'keyNo', 'keyNo', 55) == 55 + + +def test_safe_value_fallback2(): dict1 = {'keya': None, 'keyb': 2, 'keyc': 5, 'keyd': None} dict2 = {'keya': 20, 'keyb': None, 'keyc': 6, 'keyd': None} - assert safe_value_fallback(dict1, dict2, 'keya', 'keya') == 20 - assert safe_value_fallback(dict2, dict1, 'keya', 'keya') == 20 + assert safe_value_fallback2(dict1, dict2, 'keya', 'keya') == 20 + assert safe_value_fallback2(dict2, dict1, 'keya', 'keya') == 20 - assert safe_value_fallback(dict1, dict2, 'keyb', 'keyb') == 2 - assert safe_value_fallback(dict2, dict1, 'keyb', 'keyb') == 2 + assert safe_value_fallback2(dict1, dict2, 'keyb', 'keyb') == 2 + assert safe_value_fallback2(dict2, dict1, 'keyb', 'keyb') == 2 - assert safe_value_fallback(dict1, dict2, 'keyc', 'keyc') == 5 - assert safe_value_fallback(dict2, dict1, 'keyc', 'keyc') == 6 + assert safe_value_fallback2(dict1, dict2, 'keyc', 'keyc') == 5 + assert safe_value_fallback2(dict2, dict1, 'keyc', 'keyc') == 6 - assert safe_value_fallback(dict1, dict2, 'keyd', 'keyd') is None - assert safe_value_fallback(dict2, dict1, 'keyd', 'keyd') is None - assert safe_value_fallback(dict2, dict1, 'keyd', 'keyd', 1234) == 1234 + assert safe_value_fallback2(dict1, dict2, 'keyd', 'keyd') is None + assert safe_value_fallback2(dict2, dict1, 'keyd', 'keyd') is None + assert safe_value_fallback2(dict2, dict1, 'keyd', 'keyd', 1234) == 1234 - assert safe_value_fallback(dict1, dict2, 'keyNo', 'keyNo') is None - assert safe_value_fallback(dict2, dict1, 'keyNo', 'keyNo') is None - assert safe_value_fallback(dict2, dict1, 'keyNo', 'keyNo', 1234) == 1234 + assert safe_value_fallback2(dict1, dict2, 'keyNo', 'keyNo') is None + assert safe_value_fallback2(dict2, dict1, 'keyNo', 'keyNo') is None + assert safe_value_fallback2(dict2, dict1, 'keyNo', 'keyNo', 1234) == 1234 def test_plural() -> None: From c826f7a7077715f71416dd77001d927b858b0184 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Jul 2020 20:15:29 +0200 Subject: [PATCH 0274/1197] Add amount_requested to database --- freqtrade/persistence.py | 9 ++++++--- tests/test_persistence.py | 6 ++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index a6c1de402..bdbb7628a 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, 'timeframe'): + if not has_column(cols, 'amount_requested'): logger.info(f'Running database migration - backup available as {table_back_name}') fee_open = get_column_def(cols, 'fee_open', 'fee') @@ -119,6 +119,7 @@ def check_migrate(engine) -> None: cols, 'close_profit_abs', f"(amount * close_rate * (1 - {fee_close})) - {open_trade_price}") sell_order_status = get_column_def(cols, 'sell_order_status', 'null') + amount_requested = get_column_def(cols, 'amount_requested', 'amount') # Schema migration necessary engine.execute(f"alter table trades rename to {table_back_name}") @@ -134,7 +135,7 @@ def check_migrate(engine) -> None: fee_open, fee_open_cost, fee_open_currency, fee_close, fee_close_cost, fee_open_currency, open_rate, open_rate_requested, close_rate, close_rate_requested, close_profit, - stake_amount, amount, open_date, close_date, open_order_id, + stake_amount, amount, amount_requested, open_date, close_date, open_order_id, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stoploss_order_id, stoploss_last_update, max_rate, min_rate, sell_reason, sell_order_status, strategy, @@ -153,7 +154,7 @@ def check_migrate(engine) -> None: {fee_close_cost} fee_close_cost, {fee_close_currency} fee_close_currency, open_rate, {open_rate_requested} open_rate_requested, close_rate, {close_rate_requested} close_rate_requested, close_profit, - stake_amount, amount, open_date, close_date, open_order_id, + stake_amount, amount, {amount_requested}, open_date, close_date, open_order_id, {stop_loss} stop_loss, {stop_loss_pct} stop_loss_pct, {initial_stop_loss} initial_stop_loss, {initial_stop_loss_pct} initial_stop_loss_pct, @@ -215,6 +216,7 @@ class Trade(_DECL_BASE): close_profit_abs = Column(Float) stake_amount = Column(Float, nullable=False) amount = Column(Float) + amount_requested = Column(Float) open_date = Column(DateTime, nullable=False, default=datetime.utcnow) close_date = Column(DateTime) open_order_id = Column(String) @@ -256,6 +258,7 @@ class Trade(_DECL_BASE): 'is_open': self.is_open, 'exchange': self.exchange, 'amount': round(self.amount, 8), + 'amount_requested': round(self.amount_requested, 8), 'stake_amount': round(self.stake_amount, 8), 'strategy': self.strategy, 'ticker_interval': self.timeframe, # DEPRECATED diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 8dd27e53a..c39b2015e 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -457,6 +457,7 @@ def test_migrate_old(mocker, default_conf, fee): assert trade.close_rate_requested is None assert trade.is_open == 1 assert trade.amount == amount + assert trade.amount_requested == amount assert trade.stake_amount == default_conf.get("stake_amount") assert trade.pair == "ETC/BTC" assert trade.exchange == "bittrex" @@ -546,6 +547,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert trade.close_rate_requested is None assert trade.is_open == 1 assert trade.amount == amount + assert trade.amount_requested == amount assert trade.stake_amount == default_conf.get("stake_amount") assert trade.pair == "ETC/BTC" assert trade.exchange == "binance" @@ -725,6 +727,7 @@ def test_to_json(default_conf, fee): pair='ETH/BTC', stake_amount=0.001, amount=123.0, + amount_requested=123.0, fee_open=fee.return_value, fee_close=fee.return_value, open_date=arrow.utcnow().shift(hours=-2).datetime, @@ -757,6 +760,7 @@ def test_to_json(default_conf, fee): 'close_rate': None, 'close_rate_requested': None, 'amount': 123.0, + 'amount_requested': 123.0, 'stake_amount': 0.001, 'close_profit': None, 'close_profit_abs': None, @@ -786,6 +790,7 @@ def test_to_json(default_conf, fee): pair='XRP/BTC', stake_amount=0.001, amount=100.0, + amount_requested=101.0, fee_open=fee.return_value, fee_close=fee.return_value, open_date=arrow.utcnow().shift(hours=-2).datetime, @@ -808,6 +813,7 @@ def test_to_json(default_conf, fee): 'open_rate': 0.123, 'close_rate': 0.125, 'amount': 100.0, + 'amount_requested': 101.0, 'stake_amount': 0.001, 'stop_loss': None, 'stop_loss_abs': None, From eafab38db3b35d66ba927b0bc69934c40178bdde Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Jul 2020 20:20:14 +0200 Subject: [PATCH 0275/1197] Complete implementation of amount_requested --- freqtrade/freqtradebot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9a0b3c40f..c1149b8c2 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -532,6 +532,7 @@ class FreqtradeBot: # we assume the order is executed at the price requested buy_limit_filled_price = buy_limit_requested + amount_requested = amount if order_status == 'expired' or order_status == 'rejected': order_tif = self.strategy.order_time_in_force['buy'] @@ -568,6 +569,7 @@ class FreqtradeBot: pair=pair, stake_amount=stake_amount, amount=amount, + amount_requested=amount_requested, fee_open=fee, fee_close=fee, open_rate=buy_limit_filled_price, From c1c018d8feb1843c18271f821dd5d36185db3d13 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Jul 2020 20:27:00 +0200 Subject: [PATCH 0276/1197] Fix tests that require amount_requested --- tests/conftest.py | 3 +++ tests/rpc/test_rpc.py | 2 ++ tests/rpc/test_rpc_apiserver.py | 3 +++ 3 files changed, 8 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 43dc8ca78..8501a98b9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -176,6 +176,7 @@ def create_mock_trades(fee): pair='ETH/BTC', stake_amount=0.001, amount=123.0, + amount_requested=123.0, fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.123, @@ -188,6 +189,7 @@ def create_mock_trades(fee): pair='ETC/BTC', stake_amount=0.001, amount=123.0, + amount_requested=123.0, fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.123, @@ -204,6 +206,7 @@ def create_mock_trades(fee): pair='ETC/BTC', stake_amount=0.001, amount=123.0, + amount_requested=124.0, fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.123, diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index de9327ba9..ddbbe395c 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -80,6 +80,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'close_rate': None, 'current_rate': 1.099e-05, 'amount': 91.07468124, + 'amount_requested': 91.07468124, 'stake_amount': 0.001, 'close_profit': None, 'close_profit_pct': None, @@ -143,6 +144,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'close_rate': None, 'current_rate': ANY, 'amount': 91.07468124, + 'amount_requested': 91.07468124, 'stake_amount': 0.001, 'close_profit': None, 'close_profit_pct': None, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 355b63f48..13c11d29d 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -520,6 +520,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): assert_response(rc) assert len(rc.json) == 1 assert rc.json == [{'amount': 91.07468124, + 'amount_requested': 91.07468124, 'base_currency': 'BTC', 'close_date': None, 'close_date_hum': None, @@ -641,6 +642,7 @@ def test_api_forcebuy(botclient, mocker, fee): fbuy_mock = MagicMock(return_value=Trade( pair='ETH/ETH', amount=1, + amount_requested=1, exchange='bittrex', stake_amount=1, open_rate=0.245441, @@ -657,6 +659,7 @@ def test_api_forcebuy(botclient, mocker, fee): data='{"pair": "ETH/BTC"}') assert_response(rc) assert rc.json == {'amount': 1, + 'amount_requested': 1, 'trade_id': None, 'close_date': None, 'close_date_hum': None, From 3721736aafbcbd06b8cfd1e94e8244360d481b7b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Jul 2020 20:28:07 +0200 Subject: [PATCH 0277/1197] Convert to real amount before placing order to keep the correct amount in the database --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c1149b8c2..5c0de94a1 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -523,7 +523,7 @@ class FreqtradeBot: time_in_force=time_in_force): logger.info(f"User requested abortion of buying {pair}") return False - + amount = self.exchange.amount_to_precision(pair, amount) order = self.exchange.buy(pair=pair, ordertype=order_type, amount=amount, rate=buy_limit_requested, time_in_force=time_in_force) From 98f2e79f27292b8e8d6feac5c6cd00660635c069 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Jul 2020 20:51:52 +0200 Subject: [PATCH 0278/1197] Adjust tests to use correctly trimmed amount --- freqtrade/persistence.py | 2 +- tests/rpc/test_rpc.py | 8 ++++---- tests/rpc/test_rpc_apiserver.py | 6 +++--- tests/rpc/test_rpc_telegram.py | 12 ++++++------ tests/test_freqtradebot.py | 20 ++++++++++---------- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index bdbb7628a..245b1c790 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -276,7 +276,7 @@ class Trade(_DECL_BASE): 'open_timestamp': int(self.open_date.timestamp() * 1000), 'open_rate': self.open_rate, 'open_rate_requested': self.open_rate_requested, - 'open_trade_price': self.open_trade_price, + 'open_trade_price': round(self.open_trade_price, 8), 'close_date_hum': (arrow.get(self.close_date).humanize() if self.close_date else None), diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index ddbbe395c..2d5370e1e 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -79,8 +79,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'open_rate': 1.098e-05, 'close_rate': None, 'current_rate': 1.099e-05, - 'amount': 91.07468124, - 'amount_requested': 91.07468124, + 'amount': 91.07468123, + 'amount_requested': 91.07468123, 'stake_amount': 0.001, 'close_profit': None, 'close_profit_pct': None, @@ -143,8 +143,8 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'open_rate': 1.098e-05, 'close_rate': None, 'current_rate': ANY, - 'amount': 91.07468124, - 'amount_requested': 91.07468124, + 'amount': 91.07468123, + 'amount_requested': 91.07468123, 'stake_amount': 0.001, 'close_profit': None, 'close_profit_pct': None, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 13c11d29d..c7259bdc6 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -519,8 +519,8 @@ def test_api_status(botclient, mocker, ticker, fee, markets): rc = client_get(client, f"{BASE_URI}/status") assert_response(rc) assert len(rc.json) == 1 - assert rc.json == [{'amount': 91.07468124, - 'amount_requested': 91.07468124, + assert rc.json == [{'amount': 91.07468123, + 'amount_requested': 91.07468123, 'base_currency': 'BTC', 'close_date': None, 'close_date_hum': None, @@ -696,7 +696,7 @@ def test_api_forcebuy(botclient, mocker, fee): 'min_rate': None, 'open_order_id': '123456', 'open_rate_requested': None, - 'open_trade_price': 0.2460546025, + 'open_trade_price': 0.24605460, 'sell_reason': None, 'sell_order_status': None, 'strategy': None, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 0a4352f5b..669c6dc89 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -690,7 +690,7 @@ def test_reload_config_handle(default_conf, update, mocker) -> None: assert 'reloading config' in msg_mock.call_args_list[0][0][0] -def test_forcesell_handle(default_conf, update, ticker, fee, +def test_telegram_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) @@ -729,7 +729,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee, 'pair': 'ETH/BTC', 'gain': 'profit', 'limit': 1.173e-05, - 'amount': 91.07468123861567, + 'amount': 91.07468123, 'order_type': 'limit', 'open_rate': 1.098e-05, 'current_rate': 1.173e-05, @@ -743,8 +743,8 @@ def test_forcesell_handle(default_conf, update, ticker, fee, } == last_msg -def test_forcesell_down_handle(default_conf, update, ticker, fee, - ticker_sell_down, mocker) -> None: +def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, + ticker_sell_down, mocker) -> None: mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) @@ -788,7 +788,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, 'pair': 'ETH/BTC', 'gain': 'loss', 'limit': 1.043e-05, - 'amount': 91.07468123861567, + 'amount': 91.07468123, 'order_type': 'limit', 'open_rate': 1.098e-05, 'current_rate': 1.043e-05, @@ -836,7 +836,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None 'pair': 'ETH/BTC', 'gain': 'loss', 'limit': 1.099e-05, - 'amount': 91.07468123861567, + 'amount': 91.07468123, 'order_type': 'limit', 'open_rate': 1.098e-05, 'current_rate': 1.099e-05, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index ada0d87fd..685b269d7 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -595,7 +595,7 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, freqtrade.create_trade('ETH/BTC') rate, amount = buy_mock.call_args[1]['rate'], buy_mock.call_args[1]['amount'] - assert rate * amount >= default_conf['stake_amount'] + assert rate * amount <= default_conf['stake_amount'] def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order, @@ -782,7 +782,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, assert trade.open_date is not None assert trade.exchange == 'bittrex' assert trade.open_rate == 0.00001098 - assert trade.amount == 91.07468123861567 + assert trade.amount == 91.07468123 assert log_has( 'Buy signal found: about create a new trade with stake_amount: 0.001 ...', caplog @@ -1009,7 +1009,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: call_args = buy_mm.call_args_list[0][1] assert call_args['pair'] == pair assert call_args['rate'] == bid - assert call_args['amount'] == stake_amount / bid + assert call_args['amount'] == round(stake_amount / bid, 8) buy_rate_mock.reset_mock() # Should create an open trade with an open order id @@ -1029,7 +1029,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: call_args = buy_mm.call_args_list[1][1] assert call_args['pair'] == pair assert call_args['rate'] == fix_price - assert call_args['amount'] == stake_amount / fix_price + assert call_args['amount'] == round(stake_amount / fix_price, 8) # In case of closed order limit_buy_order['status'] = 'closed' @@ -1407,7 +1407,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, assert freqtrade.handle_stoploss_on_exchange(trade) is False cancel_order_mock.assert_called_once_with(100, 'ETH/BTC') - stoploss_order_mock.assert_called_once_with(amount=85.32423208191126, + stoploss_order_mock.assert_called_once_with(amount=85.32423208, pair='ETH/BTC', order_types=freqtrade.strategy.order_types, stop_price=0.00002346 * 0.95) @@ -1595,7 +1595,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, # stoploss should be set to 1% as trailing is on assert trade.stop_loss == 0.00002346 * 0.99 cancel_order_mock.assert_called_once_with(100, 'NEO/BTC') - stoploss_order_mock.assert_called_once_with(amount=2132892.491467577, + stoploss_order_mock.assert_called_once_with(amount=2132892.49146757, pair='NEO/BTC', order_types=freqtrade.strategy.order_types, stop_price=0.00002346 * 0.99) @@ -2577,7 +2577,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N 'pair': 'ETH/BTC', 'gain': 'profit', 'limit': 1.172e-05, - 'amount': 91.07468123861567, + 'amount': 91.07468123, 'order_type': 'limit', 'open_rate': 1.098e-05, 'current_rate': 1.173e-05, @@ -2626,7 +2626,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) 'pair': 'ETH/BTC', 'gain': 'loss', 'limit': 1.044e-05, - 'amount': 91.07468123861567, + 'amount': 91.07468123, 'order_type': 'limit', 'open_rate': 1.098e-05, 'current_rate': 1.043e-05, @@ -2682,7 +2682,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe 'pair': 'ETH/BTC', 'gain': 'loss', 'limit': 1.08801e-05, - 'amount': 91.07468123861567, + 'amount': 91.07468123, 'order_type': 'limit', 'open_rate': 1.098e-05, 'current_rate': 1.043e-05, @@ -2887,7 +2887,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, 'pair': 'ETH/BTC', 'gain': 'profit', 'limit': 1.172e-05, - 'amount': 91.07468123861567, + 'amount': 91.07468123, 'order_type': 'market', 'open_rate': 1.098e-05, 'current_rate': 1.173e-05, From de46744aa9c80437767e86cf77aa95e8c43cddbd Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Jul 2020 21:02:31 +0200 Subject: [PATCH 0279/1197] Use filled before amount for order data closes #3579 --- freqtrade/exchange/exchange.py | 1 - freqtrade/freqtradebot.py | 11 ++++++----- freqtrade/persistence.py | 3 ++- tests/rpc/test_rpc_telegram.py | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 5c4e4c530..e6ea75a63 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1167,7 +1167,6 @@ class Exchange: return (order['fee']['cost'], order['fee']['currency'], self.calculate_fee_rate(order)) - # calculate rate ? (order['fee']['cost'] / (order['amount'] * order['price'])) def is_exchange_bad(exchange_name: str) -> bool: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5c0de94a1..5da7223c4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -20,7 +20,7 @@ from freqtrade.edge import Edge from freqtrade.exceptions import (DependencyException, ExchangeError, InvalidOrderException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date -from freqtrade.misc import safe_value_fallback2 +from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -553,14 +553,14 @@ class FreqtradeBot: order['filled'], order['amount'], order['remaining'] ) stake_amount = order['cost'] - amount = order['amount'] + amount = order['filled'] buy_limit_filled_price = order['price'] order_id = None # in case of FOK the order may be filled immediately and fully elif order_status == 'closed': stake_amount = order['cost'] - amount = order['amount'] + amount = safe_value_fallback(order, 'filled', 'amount') buy_limit_filled_price = order['price'] # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL @@ -1249,7 +1249,8 @@ class FreqtradeBot: # Try update amount (binance-fix) try: new_amount = self.get_real_amount(trade, order, order_amount) - if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC): + if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount, + abs_tol=constants.MATH_CLOSE_PREC): order['amount'] = new_amount order.pop('filled', None) trade.recalc_open_trade_price() @@ -1295,7 +1296,7 @@ class FreqtradeBot: """ # Init variables if order_amount is None: - order_amount = order['amount'] + order_amount = safe_value_fallback(order, 'filled', 'amount') # Only run for closed orders if trade.fee_updated(order.get('side', '')) or order['status'] == 'open': return order_amount diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 245b1c790..7dc533c07 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -17,6 +17,7 @@ from sqlalchemy.orm.session import sessionmaker from sqlalchemy.pool import StaticPool from freqtrade.exceptions import OperationalException +from freqtrade.misc import safe_value_fallback logger = logging.getLogger(__name__) @@ -376,7 +377,7 @@ class Trade(_DECL_BASE): if order_type in ('market', 'limit') and order['side'] == 'buy': # Update open rate and actual amount self.open_rate = Decimal(order['price']) - self.amount = Decimal(order.get('filled', order['amount'])) + self.amount = Decimal(safe_value_fallback(order, 'filled', 'amount')) self.recalc_open_trade_price() logger.info('%s_BUY has been fulfilled for %s.', order_type.upper(), self) self.open_order_id = None diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 669c6dc89..08d4dc7ec 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -691,7 +691,7 @@ def test_reload_config_handle(default_conf, update, mocker) -> None: def test_telegram_forcesell_handle(default_conf, update, ticker, fee, - ticker_sell_up, mocker) -> None: + ticker_sell_up, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) From eaf2b53d591001674a9a9ceb24c9e2cefae00387 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2020 05:10:46 +0000 Subject: [PATCH 0280/1197] Update Dependabot config file --- .dependabot/config.yml | 17 ----------------- .github/dependabot.yml | 13 +++++++++++++ 2 files changed, 13 insertions(+), 17 deletions(-) delete mode 100644 .dependabot/config.yml create mode 100644 .github/dependabot.yml diff --git a/.dependabot/config.yml b/.dependabot/config.yml deleted file mode 100644 index 66b91e99f..000000000 --- a/.dependabot/config.yml +++ /dev/null @@ -1,17 +0,0 @@ -version: 1 - -update_configs: - - package_manager: "python" - directory: "/" - update_schedule: "weekly" - allowed_updates: - - match: - update_type: "all" - target_branch: "develop" - - - package_manager: "docker" - directory: "/" - update_schedule: "daily" - allowed_updates: - - match: - update_type: "all" diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..44ff606b4 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: +- package-ecosystem: docker + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 +- package-ecosystem: pip + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 10 + target-branch: develop From cd7ba99528576e2961d4e7f38bc47e7c3216c658 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2020 07:23:16 +0000 Subject: [PATCH 0281/1197] Bump ccxt from 1.30.93 to 1.31.37 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.30.93 to 1.31.37. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.30.93...1.31.37) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 1cfc44ab4..d5c5fd832 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.30.93 +ccxt==1.31.37 SQLAlchemy==1.3.18 python-telegram-bot==12.8 arrow==0.15.7 From 49395601e98cc0ba8824f96aab92ed7aebb430d7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Jul 2020 10:02:06 +0200 Subject: [PATCH 0282/1197] Improve informative pair sample --- docs/strategy-customization.md | 47 +++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 50fec79dc..98c71b4b2 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -392,9 +392,9 @@ Imagine you've developed a strategy that trades the `5m` timeframe using signals The strategy might look something like this: -*Scan through the top 10 pairs by volume using the `VolumePairList` every 5 minutes and use a 14 day ATR to buy and sell.* +*Scan through the top 10 pairs by volume using the `VolumePairList` every 5 minutes and use a 14 day RSI to buy and sell.* -Due to the limited available data, it's very difficult to resample our `5m` candles into daily candles for use in a 14 day ATR. Most exchanges limit us to just 500 candles which effectively gives us around 1.74 daily candles. We need 14 days at least! +Due to the limited available data, it's very difficult to resample our `5m` candles into daily candles for use in a 14 day RSI. Most exchanges limit us to just 500 candles which effectively gives us around 1.74 daily candles. We need 14 days at least! Since we can't resample our data we will have to use an informative pair; and since our whitelist will be dynamic we don't know which pair(s) to use. @@ -410,18 +410,49 @@ class SampleStrategy(IStrategy): def informative_pairs(self): - # get access to all pairs available in whitelist. + # get access to all pairs available in whitelist. pairs = self.dp.current_whitelist() # Assign tf to each pair so they can be downloaded and cached for strategy. informative_pairs = [(pair, '1d') for pair in pairs] return informative_pairs - def populate_indicators(self, dataframe, metadata): + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + + inf_tf = '1d' # Get the informative pair informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe='1d') - # Get the 14 day ATR. - atr = ta.ATR(informative, timeperiod=14) + # Get the 14 day rsi + informative['rsi'] = ta.RSI(informative, timeperiod=14) + + # Rename columns to be unique + informative.columns = [f"{col}_{inf_tf}" for col in informative.columns] + # Assuming inf_tf = '1d' - then the columns will now be: + # date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d + + # Combine the 2 dataframes + # all indicators on the informative sample MUST be calculated before this point + dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_{inf_tf}', how='left') + # FFill to have the 1d value available in every row throughout the day. + # Without this, comparisons would only work once per day. + dataframe = dataframe.ffill() + # Calculate rsi of the original dataframe (5m timeframe) + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + # Do other stuff + # ... + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + + dataframe.loc[ + ( + (qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30 + (dataframe['rsi_1d'] < 30) & # Ensure daily RSI is < 30 + (dataframe['volume'] > 0) # Ensure this candle had volume (important for backtesting) + ), + 'buy'] = 1 + ``` #### *get_pair_dataframe(pair, timeframe)* @@ -460,7 +491,7 @@ if self.dp: !!! Warning "Warning in hyperopt" This option cannot currently be used during hyperopt. - + #### *orderbook(pair, maximum)* ``` python @@ -493,6 +524,7 @@ if self.dp: data returned from the exchange and add appropriate error handling / defaults. *** + ### Additional data (Wallets) The strategy provides access to the `Wallets` object. This contains the current balances on the exchange. @@ -516,6 +548,7 @@ if self.wallets: - `get_total(asset)` - total available balance - sum of the 2 above *** + ### Additional data (Trades) A history of Trades can be retrieved in the strategy by querying the database. From 3271c773a76d93b361a8816d6e3ee39e20468186 Mon Sep 17 00:00:00 2001 From: Alex Pham <20041501+thopd88@users.noreply.github.com> Date: Sun, 19 Jul 2020 21:30:55 +0700 Subject: [PATCH 0283/1197] Fix SQL syntax error when compare pair strings First happens in Postgres --- freqtrade/rpc/rpc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c73fcbf54..69faff533 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -523,7 +523,7 @@ class RPC: # check if valid pair # check if pair already has an open pair - trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair.is_(pair)]).first() + trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() if trade: raise RPCException(f'position for {pair} already open - id: {trade.id}') @@ -532,7 +532,7 @@ class RPC: # execute buy if self._freqtrade.execute_buy(pair, stakeamount, price): - trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair.is_(pair)]).first() + trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first() return trade else: return None From dd3a2675b53c30e9f3ff23e9799d2cfd41352fb0 Mon Sep 17 00:00:00 2001 From: thopd88 <20041501+thopd88@users.noreply.github.com> Date: Sun, 19 Jul 2020 22:02:53 +0700 Subject: [PATCH 0284/1197] Add telegram trades command to list recent trades --- freqtrade/rpc/telegram.py | 43 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 13cc1afaf..72dbd2ea7 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -92,6 +92,7 @@ class Telegram(RPC): CommandHandler('stop', self._stop), CommandHandler('forcesell', self._forcesell), CommandHandler('forcebuy', self._forcebuy), + CommandHandler('trades', self._trades), CommandHandler('performance', self._performance), CommandHandler('daily', self._daily), CommandHandler('count', self._count), @@ -496,6 +497,48 @@ class Telegram(RPC): except RPCException as e: self._send_msg(str(e)) + @authorized_only + def _trades(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /trades + Returns last n recent trades. + :param bot: telegram bot + :param update: message update + :return: None + """ + stake_cur = self._config['stake_currency'] + fiat_disp_cur = self._config.get('fiat_display_currency', '') + try: + nrecent = int(context.args[0]) + except (TypeError, ValueError, IndexError): + nrecent = 10 + try: + trades = self._rpc_trade_history( + nrecent + ) + trades_tab = tabulate( + [[trade['open_date'], + trade['pair'], + f"{trade['open_rate']}", + f"{trade['stake_amount']}", + trade['close_date'], + f"{trade['close_rate']}", + f"{trade['close_profit_abs']}"] for trade in trades['trades']], + headers=[ + 'Open Date', + 'Pair', + 'Open rate', + 'Stake Amount', + 'Close date', + 'Close rate', + 'Profit', + ], + tablefmt='simple') + message = f'{nrecent} recent trades:\n
{trades_tab}
' + self._send_msg(message, parse_mode=ParseMode.HTML) + except RPCException as e: + self._send_msg(str(e)) + @authorized_only def _performance(self, update: Update, context: CallbackContext) -> None: """ From 08fdd7d86331c5a2c55c69c130350a7b21a104e3 Mon Sep 17 00:00:00 2001 From: thopd88 <20041501+thopd88@users.noreply.github.com> Date: Sun, 19 Jul 2020 22:10:59 +0700 Subject: [PATCH 0285/1197] Add telegram /delete command to delete tradeid code inspired from _rpc_forcesell --- freqtrade/rpc/rpc.py | 22 ++++++++++++++++++++++ freqtrade/rpc/telegram.py | 19 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c73fcbf54..2a3feea25 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -537,6 +537,28 @@ class RPC: else: return None + def _rpc_delete(self, trade_id: str) -> Dict[str, str]: + """ + Handler for delete . + Delete the given trade + """ + def _exec_delete(trade: Trade) -> None: + Trade.session.delete(trade) + Trade.session.flush() + + with self._freqtrade._sell_lock: + trade = Trade.get_trades( + trade_filter=[Trade.id == trade_id, ] + ).first() + if not trade: + logger.warning('delete: Invalid argument received') + raise RPCException('invalid argument') + + _exec_delete(trade) + Trade.session.flush() + self._freqtrade.wallets.update() + return {'result': f'Deleted trade {trade_id}.'} + def _rpc_performance(self) -> List[Dict[str, Any]]: """ Handler for performance. diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 72dbd2ea7..474376938 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -93,6 +93,7 @@ class Telegram(RPC): CommandHandler('forcesell', self._forcesell), CommandHandler('forcebuy', self._forcebuy), CommandHandler('trades', self._trades), + CommandHandler('delete', self._delete), CommandHandler('performance', self._performance), CommandHandler('daily', self._daily), CommandHandler('count', self._count), @@ -497,6 +498,24 @@ class Telegram(RPC): except RPCException as e: self._send_msg(str(e)) + @authorized_only + def _delete(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /delete . + Delete the given trade + :param bot: telegram bot + :param update: message update + :return: None + """ + + trade_id = context.args[0] if len(context.args) > 0 else None + try: + msg = self._rpc_delete(trade_id) + self._send_msg('Delete Result: `{result}`'.format(**msg)) + + except RPCException as e: + self._send_msg(str(e)) + @authorized_only def _trades(self, update: Update, context: CallbackContext) -> None: """ From 37a9edfa35a8321ca38d353ab081a17263ea8f68 Mon Sep 17 00:00:00 2001 From: Pan Long Date: Mon, 20 Jul 2020 00:37:06 +0800 Subject: [PATCH 0286/1197] Correct a typo in stop loss doc. --- docs/stoploss.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index ed00c1e33..bf7270dff 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -84,7 +84,7 @@ This option can be used with or without `trailing_stop_positive`, but uses `trai ``` python trailing_stop_positive_offset = 0.011 - trailing_only_offset_is_reached = true + trailing_only_offset_is_reached = True ``` Simplified example: From 28f4a1101e58e38df1fddc22ff573e852a80de33 Mon Sep 17 00:00:00 2001 From: thopd88 <20041501+thopd88@users.noreply.github.com> Date: Mon, 20 Jul 2020 10:54:17 +0700 Subject: [PATCH 0287/1197] Revert "Add telegram /delete command to delete tradeid" This reverts commit 08fdd7d86331c5a2c55c69c130350a7b21a104e3. --- freqtrade/rpc/rpc.py | 22 ---------------------- freqtrade/rpc/telegram.py | 19 ------------------- 2 files changed, 41 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2a3feea25..c73fcbf54 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -537,28 +537,6 @@ class RPC: else: return None - def _rpc_delete(self, trade_id: str) -> Dict[str, str]: - """ - Handler for delete . - Delete the given trade - """ - def _exec_delete(trade: Trade) -> None: - Trade.session.delete(trade) - Trade.session.flush() - - with self._freqtrade._sell_lock: - trade = Trade.get_trades( - trade_filter=[Trade.id == trade_id, ] - ).first() - if not trade: - logger.warning('delete: Invalid argument received') - raise RPCException('invalid argument') - - _exec_delete(trade) - Trade.session.flush() - self._freqtrade.wallets.update() - return {'result': f'Deleted trade {trade_id}.'} - def _rpc_performance(self) -> List[Dict[str, Any]]: """ Handler for performance. diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 474376938..72dbd2ea7 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -93,7 +93,6 @@ class Telegram(RPC): CommandHandler('forcesell', self._forcesell), CommandHandler('forcebuy', self._forcebuy), CommandHandler('trades', self._trades), - CommandHandler('delete', self._delete), CommandHandler('performance', self._performance), CommandHandler('daily', self._daily), CommandHandler('count', self._count), @@ -498,24 +497,6 @@ class Telegram(RPC): except RPCException as e: self._send_msg(str(e)) - @authorized_only - def _delete(self, update: Update, context: CallbackContext) -> None: - """ - Handler for /delete . - Delete the given trade - :param bot: telegram bot - :param update: message update - :return: None - """ - - trade_id = context.args[0] if len(context.args) > 0 else None - try: - msg = self._rpc_delete(trade_id) - self._send_msg('Delete Result: `{result}`'.format(**msg)) - - except RPCException as e: - self._send_msg(str(e)) - @authorized_only def _trades(self, update: Update, context: CallbackContext) -> None: """ From eaa7370174e1b5188192e99bb205f2f03e8063b0 Mon Sep 17 00:00:00 2001 From: thopd88 <20041501+thopd88@users.noreply.github.com> Date: Mon, 20 Jul 2020 11:08:18 +0700 Subject: [PATCH 0288/1197] add /delete command --- freqtrade/rpc/rpc.py | 22 ++++++++++++++++++++++ freqtrade/rpc/telegram.py | 20 ++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c73fcbf54..b76259ad2 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -536,6 +536,28 @@ class RPC: return trade else: return None + + def _rpc_delete(self, trade_id: str) -> Dict[str, str]: + """ + Handler for delete . + Delete the given trade + """ + def _exec_delete(trade: Trade) -> None: + Trade.session.delete(trade) + Trade.session.flush() + + with self._freqtrade._sell_lock: + trade = Trade.get_trades( + trade_filter=[Trade.id == trade_id, ] + ).first() + if not trade: + logger.warning('delete: Invalid argument received') + raise RPCException('invalid argument') + + _exec_delete(trade) + Trade.session.flush() + self._freqtrade.wallets.update() + return {'result': f'Deleted trade {trade_id}.'} def _rpc_performance(self) -> List[Dict[str, Any]]: """ diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 13cc1afaf..7d006c4e5 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -92,6 +92,7 @@ class Telegram(RPC): CommandHandler('stop', self._stop), CommandHandler('forcesell', self._forcesell), CommandHandler('forcebuy', self._forcebuy), + CommandHandler('delete', self._delete), CommandHandler('performance', self._performance), CommandHandler('daily', self._daily), CommandHandler('count', self._count), @@ -496,6 +497,24 @@ class Telegram(RPC): except RPCException as e: self._send_msg(str(e)) + @authorized_only + def _delete(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /delete . + Delete the given trade + :param bot: telegram bot + :param update: message update + :return: None + """ + + trade_id = context.args[0] if len(context.args) > 0 else None + try: + msg = self._rpc_delete(trade_id) + self._send_msg('Delete Result: `{result}`'.format(**msg)) + + except RPCException as e: + self._send_msg(str(e)) + @authorized_only def _performance(self, update: Update, context: CallbackContext) -> None: """ @@ -613,6 +632,7 @@ class Telegram(RPC): "*/forcesell |all:* `Instantly sells the given trade or all trades, " "regardless of profit`\n" f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}" + "*/delete :* `Instantly delete the given trade in the database`\n" "*/performance:* `Show performance of each finished trade grouped by pair`\n" "*/daily :* `Shows profit or loss per day, over the last n days`\n" "*/count:* `Show number of trades running compared to allowed number of trades`" From 811028ae926c47ee60e1904da2a3468e2c753bdd Mon Sep 17 00:00:00 2001 From: gautier pialat Date: Mon, 20 Jul 2020 07:17:34 +0200 Subject: [PATCH 0289/1197] missing coma in sql request --- docs/sql_cheatsheet.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index f4cb473ff..748b16928 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -123,7 +123,7 @@ SET is_open=0, close_date='2020-06-20 03:08:45.103418', close_rate=0.19638016, close_profit=0.0496, - close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * open_rate * (1 - fee_open))) + close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * open_rate * (1 - fee_open))), sell_reason='force_sell' WHERE id=31; ``` From 4c97527b041234d6865d24fb490963b1c17b88ea Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Jul 2020 19:11:15 +0200 Subject: [PATCH 0290/1197] FIx failing test --- freqtrade/rpc/telegram.py | 2 -- tests/rpc/test_rpc_telegram.py | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 72dbd2ea7..343b26072 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -506,8 +506,6 @@ class Telegram(RPC): :param update: message update :return: None """ - stake_cur = self._config['stake_currency'] - fiat_disp_cur = self._config.get('fiat_display_currency', '') try: nrecent = int(context.args[0]) except (TypeError, ValueError, IndexError): diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 0a4352f5b..1ea211584 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -60,7 +60,7 @@ def test__init__(default_conf, mocker) -> None: assert telegram._config == default_conf -def test_init(default_conf, mocker, caplog) -> None: +def test_telegram_init(default_conf, mocker, caplog) -> None: start_polling = MagicMock() mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling)) @@ -72,7 +72,7 @@ def test_init(default_conf, mocker, caplog) -> None: assert start_polling.start_polling.call_count == 1 message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " - "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], " + "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], " "['performance'], ['daily'], ['count'], ['reload_config', 'reload_conf'], " "['show_config', 'show_conf'], ['stopbuy'], ['whitelist'], ['blacklist'], " "['edge'], ['help'], ['version']]") From 47748961697994c82fa30b8b825699382ae0d446 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Jul 2020 19:39:12 +0200 Subject: [PATCH 0291/1197] Evaluate average before price in order returns --- freqtrade/exchange/exchange.py | 1 + freqtrade/freqtradebot.py | 4 ++-- freqtrade/persistence.py | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e6ea75a63..9858eb518 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -475,6 +475,7 @@ class Exchange: "id": order_id, 'pair': pair, 'price': rate, + 'average': rate, 'amount': _amount, 'cost': _amount * rate, 'type': ordertype, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5da7223c4..8c5b5b460 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -554,14 +554,14 @@ class FreqtradeBot: ) stake_amount = order['cost'] amount = order['filled'] - buy_limit_filled_price = order['price'] + buy_limit_filled_price = safe_value_fallback(order, 'average', 'price') order_id = None # in case of FOK the order may be filled immediately and fully elif order_status == 'closed': stake_amount = order['cost'] amount = safe_value_fallback(order, 'filled', 'amount') - buy_limit_filled_price = order['price'] + buy_limit_filled_price = safe_value_fallback(order, 'average', 'price') # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 7dc533c07..fdb816eab 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -369,20 +369,20 @@ class Trade(_DECL_BASE): """ order_type = order['type'] # Ignore open and cancelled orders - if order['status'] == 'open' or order['price'] is None: + if order['status'] == 'open' or safe_value_fallback(order, 'average', 'price') is None: return logger.info('Updating trade (id=%s) ...', self.id) if order_type in ('market', 'limit') and order['side'] == 'buy': # Update open rate and actual amount - self.open_rate = Decimal(order['price']) + self.open_rate = Decimal(safe_value_fallback(order, 'average', 'price')) self.amount = Decimal(safe_value_fallback(order, 'filled', 'amount')) 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': - self.close(order['price']) + self.close(safe_value_fallback(order, 'average', 'price')) logger.info('%s_SELL has been fulfilled for %s.', order_type.upper(), self) elif order_type in ('stop_loss_limit', 'stop-loss', 'stop'): self.stoploss_order_id = None From 21dcef113431e4b42be7acc33d6353f1db33d96e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Jul 2020 19:50:29 +0200 Subject: [PATCH 0292/1197] Add trade_id to webhooks allowing for easier corelation of different messages --- docs/webhook-config.md | 4 ++++ freqtrade/freqtradebot.py | 4 ++++ tests/rpc/test_rpc_telegram.py | 3 +++ tests/test_freqtradebot.py | 4 ++++ 4 files changed, 15 insertions(+) diff --git a/docs/webhook-config.md b/docs/webhook-config.md index 70a41dd46..db6d4d1ef 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -47,6 +47,7 @@ Different payloads can be configured for different events. Not all fields are ne The fields in `webhook.webhookbuy` are filled when the bot executes a buy. Parameters are filled using string.format. Possible parameters are: +* `trade_id` * `exchange` * `pair` * `limit` @@ -63,6 +64,7 @@ Possible parameters are: The fields in `webhook.webhookbuycancel` are filled when the bot cancels a buy order. Parameters are filled using string.format. Possible parameters are: +* `trade_id` * `exchange` * `pair` * `limit` @@ -79,6 +81,7 @@ Possible parameters are: The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format. Possible parameters are: +* `trade_id` * `exchange` * `pair` * `gain` @@ -100,6 +103,7 @@ Possible parameters are: The fields in `webhook.webhooksellcancel` are filled when the bot cancels a sell order. Parameters are filled using string.format. Possible parameters are: +* `trade_id` * `exchange` * `pair` * `gain` diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 38afe3230..a6d96ef77 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -598,6 +598,7 @@ class FreqtradeBot: Sends rpc notification when a buy occured. """ msg = { + 'trade_id': trade.id, 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, @@ -621,6 +622,7 @@ class FreqtradeBot: current_rate = self.get_buy_rate(trade.pair, False) msg = { + 'trade_id': trade.id, 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, @@ -1149,6 +1151,7 @@ class FreqtradeBot: msg = { 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, 'gain': gain, @@ -1191,6 +1194,7 @@ class FreqtradeBot: msg = { 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, 'gain': gain, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 0a4352f5b..631817624 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -725,6 +725,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee, last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': 'profit', @@ -784,6 +785,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': 'loss', @@ -832,6 +834,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None msg = rpc_mock.call_args_list[0][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': 'loss', diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index ada0d87fd..c7089abfe 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2572,6 +2572,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N assert rpc_mock.call_count == 1 last_msg = rpc_mock.call_args_list[-1][0][0] assert { + 'trade_id': 1, 'type': RPCMessageType.SELL_NOTIFICATION, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', @@ -2622,6 +2623,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': 'loss', @@ -2678,6 +2680,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe assert { 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': 'loss', @@ -2883,6 +2886,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, + 'trade_id': 1, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', 'gain': 'profit', From 7d6708fc6a3fa56381f72882ab09a330e207f792 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Jul 2020 20:03:03 +0200 Subject: [PATCH 0293/1197] Reduce severity of hyperopt "does not provide" messages closes #3371 --- freqtrade/resolvers/hyperopt_resolver.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 633363134..abbfee6ed 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -42,14 +42,14 @@ class HyperOptResolver(IResolver): extra_dir=config.get('hyperopt_path')) if not hasattr(hyperopt, 'populate_indicators'): - logger.warning("Hyperopt class does not provide populate_indicators() method. " - "Using populate_indicators from the strategy.") + logger.info("Hyperopt class does not provide populate_indicators() method. " + "Using populate_indicators from the strategy.") if not hasattr(hyperopt, 'populate_buy_trend'): - logger.warning("Hyperopt class does not provide populate_buy_trend() method. " - "Using populate_buy_trend from the strategy.") + logger.info("Hyperopt class does not provide populate_buy_trend() method. " + "Using populate_buy_trend from the strategy.") if not hasattr(hyperopt, 'populate_sell_trend'): - logger.warning("Hyperopt class does not provide populate_sell_trend() method. " - "Using populate_sell_trend from the strategy.") + logger.info("Hyperopt class does not provide populate_sell_trend() method. " + "Using populate_sell_trend from the strategy.") return hyperopt From 939f91734f2723a72f6545feef2edfef8beaa9c4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 21 Jul 2020 20:34:19 +0200 Subject: [PATCH 0294/1197] Test confirming 0 division ... --- tests/conftest.py | 52 +++++++++++++++++++++++++++++++-- tests/pairlist/test_pairlist.py | 9 ++++-- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 43dc8ca78..e2bdf7da5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -661,7 +661,8 @@ def shitcoinmarkets(markets): Fixture with shitcoin markets - used to test filters in pairlists """ shitmarkets = deepcopy(markets) - shitmarkets.update({'HOT/BTC': { + shitmarkets.update({ + 'HOT/BTC': { 'id': 'HOTBTC', 'symbol': 'HOT/BTC', 'base': 'HOT', @@ -766,7 +767,32 @@ def shitcoinmarkets(markets): "spot": True, "future": False, "active": True - }, + }, + 'ADADOUBLE/USDT': { + "percentage": True, + "tierBased": False, + "taker": 0.001, + "maker": 0.001, + "precision": { + "base": 8, + "quote": 8, + "amount": 2, + "price": 4 + }, + "limits": { + }, + "id": "ADADOUBLEUSDT", + "symbol": "ADADOUBLE/USDT", + "base": "ADADOUBLE", + "quote": "USDT", + "baseId": "ADADOUBLE", + "quoteId": "USDT", + "info": {}, + "type": "spot", + "spot": True, + "future": False, + "active": True + }, }) return shitmarkets @@ -1388,6 +1414,28 @@ def tickers(): "quoteVolume": 0.0, "info": {} }, + "ADADOUBLE/USDT": { + "symbol": "ADADOUBLE/USDT", + "timestamp": 1580469388244, + "datetime": "2020-01-31T11:16:28.244Z", + "high": None, + "low": None, + "bid": 0.7305, + "bidVolume": None, + "ask": 0.7342, + "askVolume": None, + "vwap": None, + "open": None, + "close": None, + "last": 0, + "previousClose": None, + "change": None, + "percentage": 2.628, + "average": None, + "baseVolume": 0.0, + "quoteVolume": 0.0, + "info": {} + }, }) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index e23102162..efe4a784b 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -235,7 +235,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}], "BTC", ['HOT/BTC', 'FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']), ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], - "USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT']), + "USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT', 'ADADOUBLE/USDT']), # No pair for ETH, VolumePairList ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], "ETH", []), @@ -303,11 +303,11 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # ShuffleFilter ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "ShuffleFilter", "seed": 77}], - "USDT", ['ETH/USDT', 'ADAHALF/USDT', 'NANO/USDT']), + "USDT", ['ADADOUBLE/USDT', 'ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT']), # ShuffleFilter, other seed ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "ShuffleFilter", "seed": 42}], - "USDT", ['NANO/USDT', 'ETH/USDT', 'ADAHALF/USDT']), + "USDT", ['ADAHALF/USDT', 'NANO/USDT', 'ADADOUBLE/USDT', 'ETH/USDT']), # ShuffleFilter, no seed ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "ShuffleFilter"}], @@ -347,6 +347,9 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}, {"method": "StaticPairList"}], "BTC", 'static_in_the_middle'), + ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, + {"method": "PriceFilter", "low_price_ratio": 0.02}], + "USDT", ['ETH/USDT', 'NANO/USDT']), ]) def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, ohlcv_history_list, pairlists, base_currency, From 6a10c715fae72a466afb040dae8e1a55334c38f8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 21 Jul 2020 20:34:29 +0200 Subject: [PATCH 0295/1197] Fix 0 division (if last = 0, something went wrong!) --- freqtrade/pairlist/PriceFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/pairlist/PriceFilter.py index 5ee1df078..b3b2f43dc 100644 --- a/freqtrade/pairlist/PriceFilter.py +++ b/freqtrade/pairlist/PriceFilter.py @@ -56,7 +56,7 @@ class PriceFilter(IPairList): :param ticker: ticker dict as returned from ccxt.load_markets() :return: True if the pair can stay, false if it should be removed """ - if ticker['last'] is None: + if ticker['last'] is None or ticker['last'] == 0: self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, because " "ticker['last'] is empty (Usually no trade in the last 24h).") From 2a5f8d889588e258191bc67f524d3b72d6a47d66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 22 Jul 2020 06:22:45 +0000 Subject: [PATCH 0296/1197] Bump python from 3.8.4-slim-buster to 3.8.5-slim-buster Bumps python from 3.8.4-slim-buster to 3.8.5-slim-buster. Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f27167cc5..e1220e3b8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8.4-slim-buster +FROM python:3.8.5-slim-buster RUN apt-get update \ && apt-get -y install curl build-essential libssl-dev sqlite3 \ From f5f529cacedd2fe9cb2807b1e6ce4b0b520158f2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Jul 2020 15:15:50 +0200 Subject: [PATCH 0297/1197] Use correct initialization of DataProvider --- freqtrade/plot/plotting.py | 8 +++++--- tests/test_plotting.py | 4 +++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index db83448c0..a933c6a76 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -10,12 +10,13 @@ from freqtrade.data.btanalysis import (calculate_max_drawdown, create_cum_profit, extract_trades_of_period, load_trades) from freqtrade.data.converter import trim_dataframe +from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_prev_date from freqtrade.misc import pair_to_filename -from freqtrade.resolvers import StrategyResolver -from freqtrade.data.dataprovider import DataProvider +from freqtrade.resolvers import ExchangeResolver, StrategyResolver +from freqtrade.strategy import IStrategy logger = logging.getLogger(__name__) @@ -468,6 +469,8 @@ def load_and_plot_trades(config: Dict[str, Any]): """ strategy = StrategyResolver.load_strategy(config) + exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config) + IStrategy.dp = DataProvider(config, exchange) plot_elements = init_plotscript(config) trades = plot_elements['trades'] pair_counter = 0 @@ -475,7 +478,6 @@ def load_and_plot_trades(config: Dict[str, Any]): pair_counter += 1 logger.info("analyse pair %s", pair) - strategy.dp = DataProvider(config, config["exchange"]) df_analyzed = strategy.analyze_ticker(data, {'pair': pair}) trades_pair = trades.loc[trades['pair'] == pair] trades_pair = extract_trades_of_period(df_analyzed, trades_pair) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 05805eb24..8f4512c4b 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -21,7 +21,7 @@ from freqtrade.plot.plotting import (add_indicators, add_profit, load_and_plot_trades, plot_profit, plot_trades, store_plot_file) from freqtrade.resolvers import StrategyResolver -from tests.conftest import get_args, log_has, log_has_re +from tests.conftest import get_args, log_has, log_has_re, patch_exchange def fig_generating_mock(fig, *args, **kwargs): @@ -316,6 +316,8 @@ def test_start_plot_dataframe(mocker): def test_load_and_plot_trades(default_conf, mocker, caplog, testdatadir): + patch_exchange(mocker) + default_conf['trade_source'] = 'file' default_conf["datadir"] = testdatadir default_conf['exportfilename'] = testdatadir / "backtest-result_test.json" From f6bde8bd9cbad8a50b75d5c26c16011b3d5448d0 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 22 Jul 2020 21:43:15 +0300 Subject: [PATCH 0298/1197] Improve exception message wordings --- freqtrade/pairlist/AgeFilter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index 7b6b126c3..d199154ba 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -26,9 +26,9 @@ class AgeFilter(IPairList): self._min_days_listed = pairlistconfig.get('min_days_listed', 10) if self._min_days_listed < 1: - raise OperationalException("AgeFilter requires min_days_listed must be >= 1") + raise OperationalException("AgeFilter requires min_days_listed be >= 1") if self._min_days_listed > exchange.ohlcv_candle_limit: - raise OperationalException("AgeFilter requires min_days_listed must not exceed " + raise OperationalException("AgeFilter requires min_days_listed be not exceeding " "exchange max request size " f"({exchange.ohlcv_candle_limit})") self._enabled = self._min_days_listed >= 1 From 5213abf510de8a3b43b671c9cfab65a6f81896b1 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 22 Jul 2020 21:44:39 +0300 Subject: [PATCH 0299/1197] AgeFilter is always enabled --- freqtrade/pairlist/AgeFilter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index d199154ba..56e56ceeb 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -31,7 +31,6 @@ class AgeFilter(IPairList): raise OperationalException("AgeFilter requires min_days_listed be not exceeding " "exchange max request size " f"({exchange.ohlcv_candle_limit})") - self._enabled = self._min_days_listed >= 1 @property def needstickers(self) -> bool: From daee414d7a83e15123e025d4c6107ad0435f9d0f Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 22 Jul 2020 21:51:25 +0300 Subject: [PATCH 0300/1197] Fix docs formatting --- docs/configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/configuration.md b/docs/configuration.md index a200d6411..0e3d23927 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -663,6 +663,7 @@ Filters low-value coins which would not allow setting stoplosses. #### PriceFilter The `PriceFilter` allows filtering of pairs by price. Currently the following price filters are supported: + * `min_price` * `max_price` * `low_price_ratio` From a1e292f56a068a2183aa59e59036bb5ce8abe0c5 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 22 Jul 2020 22:09:30 +0300 Subject: [PATCH 0301/1197] Improve docs --- docs/configuration.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 0e3d23927..f39a3c62d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -669,19 +669,21 @@ The `PriceFilter` allows filtering of pairs by price. Currently the following pr * `low_price_ratio` The `min_price` setting removes pairs where the price is below the specified price. This is useful if you wish to avoid trading very low-priced pairs. -This option is disabled by default, and will only apply if set to <> 0. +This option is disabled by default (or set to 0), and will only apply if set to > 0. The `max_price` setting removes pairs where the price is above the specified price. This is useful if you wish to trade only low-priced pairs. -This option is disabled by default, and will only apply if set to <> 0. +This option is disabled by default (or set to 0), and will only apply if set to > 0. The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio. -This option is disabled by default, and will only apply if set to <> 0. +This option is disabled by default (or set to 0), and will only apply if set to > 0. + +For `PriceFiler` at least one of its `min_price`, `max_price` or `low_price_ratio` settings must be applied. Calculation example: -Min price precision is 8 decimals. If price is 0.00000011 - one step would be 0.00000012 - which is almost 10% higher than the previous value. +Min price precision for SHITCOIN/BTC is 8 decimals. If its price is 0.00000011 - one price step above would be 0.00000012, which is ~9% higher than the previous price value. You may filter out this pair by using PriceFilter with `low_price_ratio` set to 0.09 (9%) or with `min_price` set to 0.00000011, correspondingly. -These pairs are dangerous since it may be impossible to place the desired stoploss - and often result in high losses. +Low priced pairs are dangerous since they are often illiquid and it may also be impossible to place the desired stoploss, which can often result in high losses. Consider using PriceFilter with `low_price_ratio` set to a value which is less than the absolute value of your stoploss (for example, if your stoploss is -5% (-0.05), then the value for `low_price_ratio` can be 0.04 or even 0.02). #### ShuffleFilter From c78199d3d9fdd3c2a040a0fb476c053d60b6d7a1 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 22 Jul 2020 22:21:30 +0300 Subject: [PATCH 0302/1197] Add checks for parameters of PriceFilter --- freqtrade/pairlist/PriceFilter.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/pairlist/PriceFilter.py index b3b2f43dc..ae3ab9230 100644 --- a/freqtrade/pairlist/PriceFilter.py +++ b/freqtrade/pairlist/PriceFilter.py @@ -4,6 +4,7 @@ Price pair list filter import logging from typing import Any, Dict +from freqtrade.exceptions import OperationalException from freqtrade.pairlist.IPairList import IPairList @@ -18,11 +19,17 @@ class PriceFilter(IPairList): super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) self._low_price_ratio = pairlistconfig.get('low_price_ratio', 0) + if self._low_price_ratio < 0: + raise OperationalException("PriceFilter requires low_price_ratio be >= 0") self._min_price = pairlistconfig.get('min_price', 0) + if self._min_price < 0: + raise OperationalException("PriceFilter requires min_price be >= 0") self._max_price = pairlistconfig.get('max_price', 0) - self._enabled = ((self._low_price_ratio != 0) or - (self._min_price != 0) or - (self._max_price != 0)) + if self._max_price < 0: + raise OperationalException("PriceFilter requires max_price be >= 0") + self._enabled = ((self._low_price_ratio > 0) or + (self._min_price > 0) or + (self._max_price > 0)) @property def needstickers(self) -> bool: From 5c2481082ef11633c56bd5d631550b746b18d271 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 22 Jul 2020 22:46:30 +0300 Subject: [PATCH 0303/1197] Add tests for PriceFilter --- tests/pairlist/test_pairlist.py | 56 +++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index efe4a784b..5a9472ef9 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -590,34 +590,58 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_his assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count -@pytest.mark.parametrize("pairlistconfig,expected", [ +@pytest.mark.parametrize("pairlistconfig,desc_expected,exception_expected", [ ({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010, - "max_price": 1.0}, "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below " - "0.1% or below 0.00000010 or above 1.00000000.'}]" - ), + "max_price": 1.0}, + "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below " + "0.1% or below 0.00000010 or above 1.00000000.'}]", + None + ), ({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010}, - "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.1% or below 0.00000010.'}]" - ), + "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.1% or below 0.00000010.'}]", + None + ), ({"method": "PriceFilter", "low_price_ratio": 0.001, "max_price": 1.00010000}, - "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.1% or above 1.00010000.'}]" - ), + "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.1% or above 1.00010000.'}]", + None + ), ({"method": "PriceFilter", "min_price": 0.00002000}, - "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.00002000.'}]" - ), + "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.00002000.'}]", + None + ), ({"method": "PriceFilter"}, - "[{'PriceFilter': 'PriceFilter - No price filters configured.'}]" - ), + "[{'PriceFilter': 'PriceFilter - No price filters configured.'}]", + None + ), + ({"method": "PriceFilter", "low_price_ratio": -0.001}, + None, + "PriceFilter requires low_price_ratio be >= 0" + ), # OperationalException expected + ({"method": "PriceFilter", "min_price": -0.00000010}, + None, + "PriceFilter requires min_price be >= 0" + ), # OperationalException expected + ({"method": "PriceFilter", "max_price": -1.00010000}, + None, + "PriceFilter requires max_price be >= 0" + ), # OperationalException expected ]) -def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, expected): +def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, + desc_expected, exception_expected): mocker.patch.multiple('freqtrade.exchange.Exchange', markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True) ) whitelist_conf['pairlists'] = [pairlistconfig] - freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) - short_desc = str(freqtrade.pairlists.short_desc()) - assert short_desc == expected + if desc_expected is not None: + freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) + short_desc = str(freqtrade.pairlists.short_desc()) + assert short_desc == desc_expected + else: # # OperationalException expected + with pytest.raises(OperationalException, + match=exception_expected): + freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog): From 50767cd5694a79a4ac10ccba28f677470eb16725 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 22 Jul 2020 22:48:29 +0300 Subject: [PATCH 0304/1197] Adjust tests for AgeFilter --- tests/pairlist/test_pairlist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 5a9472ef9..27b13bf69 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -547,7 +547,7 @@ def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tick ) with pytest.raises(OperationalException, - match=r'AgeFilter requires min_days_listed must be >= 1'): + match=r'AgeFilter requires min_days_listed be >= 1'): get_patched_freqtradebot(mocker, default_conf) @@ -562,7 +562,7 @@ def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tick ) with pytest.raises(OperationalException, - match=r'AgeFilter requires min_days_listed must not exceed ' + match=r'AgeFilter requires min_days_listed be not exceeding ' r'exchange max request size \([0-9]+\)'): get_patched_freqtradebot(mocker, default_conf) From f48250b4142acbe91c43e18f246b9f5889263095 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 22 Jul 2020 22:56:24 +0300 Subject: [PATCH 0305/1197] Make flake happy --- tests/pairlist/test_pairlist.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 27b13bf69..c235367be 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -596,35 +596,35 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_his "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below " "0.1% or below 0.00000010 or above 1.00000000.'}]", None - ), + ), ({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010}, "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.1% or below 0.00000010.'}]", None - ), + ), ({"method": "PriceFilter", "low_price_ratio": 0.001, "max_price": 1.00010000}, "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.1% or above 1.00010000.'}]", None - ), + ), ({"method": "PriceFilter", "min_price": 0.00002000}, "[{'PriceFilter': 'PriceFilter - Filtering pairs priced below 0.00002000.'}]", None - ), + ), ({"method": "PriceFilter"}, "[{'PriceFilter': 'PriceFilter - No price filters configured.'}]", None - ), + ), ({"method": "PriceFilter", "low_price_ratio": -0.001}, None, "PriceFilter requires low_price_ratio be >= 0" - ), # OperationalException expected + ), # OperationalException expected ({"method": "PriceFilter", "min_price": -0.00000010}, None, "PriceFilter requires min_price be >= 0" - ), # OperationalException expected + ), # OperationalException expected ({"method": "PriceFilter", "max_price": -1.00010000}, None, "PriceFilter requires max_price be >= 0" - ), # OperationalException expected + ), # OperationalException expected ]) def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, desc_expected, exception_expected): @@ -638,7 +638,7 @@ def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) short_desc = str(freqtrade.pairlists.short_desc()) assert short_desc == desc_expected - else: # # OperationalException expected + else: # OperationalException expected with pytest.raises(OperationalException, match=exception_expected): freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) From 0502fe0496382e6cc2a04161ad3a15f4f9f40e26 Mon Sep 17 00:00:00 2001 From: thopd88 <20041501+thopd88@users.noreply.github.com> Date: Thu, 23 Jul 2020 09:36:05 +0700 Subject: [PATCH 0306/1197] New /trades 3 columns and exclude open trades --- freqtrade/rpc/telegram.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 343b26072..87a0cdd62 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -5,6 +5,7 @@ This module manage Telegram communication """ import json import logging +import arrow from typing import Any, Callable, Dict from tabulate import tabulate @@ -506,6 +507,7 @@ class Telegram(RPC): :param update: message update :return: None """ + stake_cur = self._config['stake_currency'] try: nrecent = int(context.args[0]) except (TypeError, ValueError, IndexError): @@ -515,21 +517,13 @@ class Telegram(RPC): nrecent ) trades_tab = tabulate( - [[trade['open_date'], + [[arrow.get(trade['open_date']).humanize(), trade['pair'], - f"{trade['open_rate']}", - f"{trade['stake_amount']}", - trade['close_date'], - f"{trade['close_rate']}", - f"{trade['close_profit_abs']}"] for trade in trades['trades']], + f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"] for trade in trades['trades'] if trade['close_profit'] is not None], headers=[ 'Open Date', 'Pair', - 'Open rate', - 'Stake Amount', - 'Close date', - 'Close rate', - 'Profit', + f'Profit ({stake_cur})', ], tablefmt='simple') message = f'{nrecent} recent trades:\n
{trades_tab}
' From a3daf8e41c209e061a3e50f1b5fcba3802bf6421 Mon Sep 17 00:00:00 2001 From: thopd88 <20041501+thopd88@users.noreply.github.com> Date: Thu, 23 Jul 2020 09:47:53 +0700 Subject: [PATCH 0307/1197] Fix line too long --- freqtrade/rpc/telegram.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 87a0cdd62..943d092db 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -519,7 +519,8 @@ class Telegram(RPC): trades_tab = tabulate( [[arrow.get(trade['open_date']).humanize(), trade['pair'], - f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"] for trade in trades['trades'] if trade['close_profit'] is not None], + f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"] + for trade in trades['trades'] if trade['close_profit'] is not None], headers=[ 'Open Date', 'Pair', From 0bad55637e5e5907d9133df6973c1926056ac6d0 Mon Sep 17 00:00:00 2001 From: thopd88 <20041501+thopd88@users.noreply.github.com> Date: Thu, 23 Jul 2020 10:12:52 +0700 Subject: [PATCH 0308/1197] fix flake8 indent error --- 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 943d092db..66583fa53 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -520,7 +520,7 @@ class Telegram(RPC): [[arrow.get(trade['open_date']).humanize(), trade['pair'], f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"] - for trade in trades['trades'] if trade['close_profit'] is not None], + for trade in trades['trades'] if trade['close_profit'] is not None], headers=[ 'Open Date', 'Pair', From 0f18b2a0d45d3056cd52830f3081b359895bb044 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 23 Jul 2020 07:12:14 +0200 Subject: [PATCH 0309/1197] Add test and fix case where no trades were closed yet --- freqtrade/rpc/telegram.py | 3 ++- tests/rpc/test_rpc_telegram.py | 35 ++++++++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 66583fa53..153be1e25 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -527,7 +527,8 @@ class Telegram(RPC): f'Profit ({stake_cur})', ], tablefmt='simple') - message = f'{nrecent} recent trades:\n
{trades_tab}
' + message = (f"{min(trades['trades_count'], nrecent)} recent trades:\n" + + (f"
{trades_tab}
" if trades['trades_count'] > 0 else '')) self._send_msg(message, parse_mode=ParseMode.HTML) except RPCException as e: self._send_msg(str(e)) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 1ea211584..f011b631d 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -21,8 +21,9 @@ from freqtrade.rpc import RPCMessageType from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.state import State from freqtrade.strategy.interface import SellType -from tests.conftest import (get_patched_freqtradebot, log_has, patch_exchange, - patch_get_signal, patch_whitelist) +from tests.conftest import (create_mock_trades, get_patched_freqtradebot, + log_has, patch_exchange, patch_get_signal, + patch_whitelist) class DummyCls(Telegram): @@ -1143,6 +1144,36 @@ def test_edge_enabled(edge_conf, update, mocker) -> None: assert 'Pair Winrate Expectancy Stoploss' in msg_mock.call_args_list[0][0][0] +def test_telegram_trades(mocker, update, default_conf, fee): + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(freqtradebot) + context = MagicMock() + context.args = [] + + telegram._trades(update=update, context=context) + assert "0 recent trades:" in msg_mock.call_args_list[0][0][0] + assert "
" not in msg_mock.call_args_list[0][0][0]
+
+    msg_mock.reset_mock()
+    create_mock_trades(fee)
+
+    context = MagicMock()
+    context.args = [5]
+    telegram._trades(update=update, context=context)
+    msg_mock.call_count == 1
+    assert "3 recent trades:" in msg_mock.call_args_list[0][0][0]
+    assert "Profit (" in msg_mock.call_args_list[0][0][0]
+    assert "Open Date" in msg_mock.call_args_list[0][0][0]
+    assert "
" in msg_mock.call_args_list[0][0][0]
+
+
 def test_help_handle(default_conf, update, mocker) -> None:
     msg_mock = MagicMock()
     mocker.patch.multiple(

From 8300eb59d4b448b4f04da34b80efd603a32a6002 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Thu, 23 Jul 2020 07:49:44 +0200
Subject: [PATCH 0310/1197] Extend create_mock_trades to create 4 trades

2 closed, and 2 open trades
---
 tests/commands/test_commands.py |  2 +-
 tests/conftest.py               | 14 ++++++++++++++
 tests/data/test_btanalysis.py   |  2 +-
 tests/test_freqtradebot.py      |  2 +-
 tests/test_persistence.py       |  6 +++---
 5 files changed, 20 insertions(+), 6 deletions(-)

diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py
index ffced956d..3ec7e4798 100644
--- a/tests/commands/test_commands.py
+++ b/tests/commands/test_commands.py
@@ -1089,7 +1089,7 @@ def test_show_trades(mocker, fee, capsys, caplog):
     pargs = get_args(args)
     pargs['config'] = None
     start_show_trades(pargs)
-    assert log_has("Printing 3 Trades: ", caplog)
+    assert log_has("Printing 4 Trades: ", caplog)
     captured = capsys.readouterr()
     assert "Trade(id=1" in captured.out
     assert "Trade(id=2" in captured.out
diff --git a/tests/conftest.py b/tests/conftest.py
index 43dc8ca78..fe8d54480 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -199,6 +199,20 @@ def create_mock_trades(fee):
     )
     Trade.session.add(trade)
 
+    trade = Trade(
+        pair='XRP/BTC',
+        stake_amount=0.001,
+        amount=123.0,
+        fee_open=fee.return_value,
+        fee_close=fee.return_value,
+        open_rate=0.05,
+        close_rate=0.06,
+        close_profit=0.01,
+        exchange='bittrex',
+        is_open=False,
+    )
+    Trade.session.add(trade)
+
     # Simulate prod entry
     trade = Trade(
         pair='ETC/BTC',
diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py
index b65db7fd8..718c02f05 100644
--- a/tests/data/test_btanalysis.py
+++ b/tests/data/test_btanalysis.py
@@ -43,7 +43,7 @@ def test_load_trades_from_db(default_conf, fee, mocker):
 
     trades = load_trades_from_db(db_url=default_conf['db_url'])
     assert init_mock.call_count == 1
-    assert len(trades) == 3
+    assert len(trades) == 4
     assert isinstance(trades, DataFrame)
     assert "pair" in trades.columns
     assert "open_time" in trades.columns
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index ada0d87fd..54e33be4d 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -4090,7 +4090,7 @@ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limi
     freqtrade = get_patched_freqtradebot(mocker, default_conf)
     create_mock_trades(fee)
     trades = Trade.query.all()
-    assert len(trades) == 3
+    assert len(trades) == 4
     freqtrade.cancel_all_open_orders()
     assert buy_mock.call_count == 1
     assert sell_mock.call_count == 1
diff --git a/tests/test_persistence.py b/tests/test_persistence.py
index 8dd27e53a..ab23243a5 100644
--- a/tests/test_persistence.py
+++ b/tests/test_persistence.py
@@ -989,7 +989,7 @@ def test_get_overall_performance(fee):
     create_mock_trades(fee)
     res = Trade.get_overall_performance()
 
-    assert len(res) == 1
+    assert len(res) == 2
     assert 'pair' in res[0]
     assert 'profit' in res[0]
     assert 'count' in res[0]
@@ -1004,5 +1004,5 @@ def test_get_best_pair(fee):
     create_mock_trades(fee)
     res = Trade.get_best_pair()
     assert len(res) == 2
-    assert res[0] == 'ETC/BTC'
-    assert res[1] == 0.005
+    assert res[0] == 'XRP/BTC'
+    assert res[1] == 0.01

From fdc84eef5905d81a69076199990d3e7bf999e938 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Thu, 23 Jul 2020 07:50:45 +0200
Subject: [PATCH 0311/1197] /trades shall only return closed trades

---
 freqtrade/rpc/rpc.py            |  5 +++--
 freqtrade/rpc/telegram.py       |  2 +-
 tests/rpc/test_rpc.py           | 11 +++++------
 tests/rpc/test_rpc_apiserver.py |  8 ++++----
 tests/rpc/test_rpc_telegram.py  |  2 +-
 5 files changed, 14 insertions(+), 14 deletions(-)

diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
index c73fcbf54..b39d5aec4 100644
--- a/freqtrade/rpc/rpc.py
+++ b/freqtrade/rpc/rpc.py
@@ -252,9 +252,10 @@ class RPC:
     def _rpc_trade_history(self, limit: int) -> Dict:
         """ Returns the X last trades """
         if limit > 0:
-            trades = Trade.get_trades().order_by(Trade.id.desc()).limit(limit)
+            trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
+                Trade.id.desc()).limit(limit)
         else:
-            trades = Trade.get_trades().order_by(Trade.id.desc()).all()
+            trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(Trade.id.desc()).all()
 
         output = [trade.to_json() for trade in trades]
 
diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 153be1e25..17f0e21f9 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -520,7 +520,7 @@ class Telegram(RPC):
                 [[arrow.get(trade['open_date']).humanize(),
                   trade['pair'],
                   f"{(100 * trade['close_profit']):.2f}% ({trade['close_profit_abs']})"]
-                 for trade in trades['trades'] if trade['close_profit'] is not None],
+                 for trade in trades['trades']],
                 headers=[
                     'Open Date',
                     'Pair',
diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py
index de9327ba9..e5859fcd9 100644
--- a/tests/rpc/test_rpc.py
+++ b/tests/rpc/test_rpc.py
@@ -284,12 +284,11 @@ def test_rpc_trade_history(mocker, default_conf, markets, fee):
     assert isinstance(trades['trades'][1], dict)
 
     trades = rpc._rpc_trade_history(0)
-    assert len(trades['trades']) == 3
-    assert trades['trades_count'] == 3
-    # The first trade is for ETH ... sorting is descending
-    assert trades['trades'][-1]['pair'] == 'ETH/BTC'
-    assert trades['trades'][0]['pair'] == 'ETC/BTC'
-    assert trades['trades'][1]['pair'] == 'ETC/BTC'
+    assert len(trades['trades']) == 2
+    assert trades['trades_count'] == 2
+    # The first closed trade is for ETC ... sorting is descending
+    assert trades['trades'][-1]['pair'] == 'ETC/BTC'
+    assert trades['trades'][0]['pair'] == 'XRP/BTC'
 
 
 def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py
index 355b63f48..f4d7b8ca3 100644
--- a/tests/rpc/test_rpc_apiserver.py
+++ b/tests/rpc/test_rpc_apiserver.py
@@ -368,12 +368,12 @@ def test_api_trades(botclient, mocker, ticker, fee, markets):
 
     rc = client_get(client, f"{BASE_URI}/trades")
     assert_response(rc)
-    assert len(rc.json['trades']) == 3
-    assert rc.json['trades_count'] == 3
-    rc = client_get(client, f"{BASE_URI}/trades?limit=2")
-    assert_response(rc)
     assert len(rc.json['trades']) == 2
     assert rc.json['trades_count'] == 2
+    rc = client_get(client, f"{BASE_URI}/trades?limit=1")
+    assert_response(rc)
+    assert len(rc.json['trades']) == 1
+    assert rc.json['trades_count'] == 1
 
 
 def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index f011b631d..cfe0ade6f 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -1168,7 +1168,7 @@ def test_telegram_trades(mocker, update, default_conf, fee):
     context.args = [5]
     telegram._trades(update=update, context=context)
     msg_mock.call_count == 1
-    assert "3 recent trades:" in msg_mock.call_args_list[0][0][0]
+    assert "2 recent trades:" in msg_mock.call_args_list[0][0][0]
     assert "Profit (" in msg_mock.call_args_list[0][0][0]
     assert "Open Date" in msg_mock.call_args_list[0][0][0]
     assert "
" in msg_mock.call_args_list[0][0][0]

From e0c14e6214a79e25d0c6b1d6b189001d89d89e4f Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Thu, 23 Jul 2020 07:54:45 +0200
Subject: [PATCH 0312/1197] Add /trades to help (so users know about it)

---
 docs/telegram-usage.md    | 1 +
 freqtrade/rpc/telegram.py | 1 +
 2 files changed, 2 insertions(+)

diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md
index f423a9376..250293d25 100644
--- a/docs/telegram-usage.md
+++ b/docs/telegram-usage.md
@@ -56,6 +56,7 @@ official commands. You can ask at any moment for help with `/help`.
 | `/show_config` | | Shows part of the current configuration with relevant settings to operation
 | `/status` | | Lists all open trades
 | `/status table` | | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
+| `/trades [limit]` | | List all recently closed trades in a table format.
 | `/count` | | Displays number of trades used and available
 | `/profit` | | Display a summary of your profit/loss from close trades and some stats about your performance
 | `/forcesell ` | | Instantly sells the given trade  (Ignoring `minimum_roi`).
diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 17f0e21f9..ab784c962 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -646,6 +646,7 @@ class Telegram(RPC):
                    "         *table :* `will display trades in a table`\n"
                    "                `pending buy orders are marked with an asterisk (*)`\n"
                    "                `pending sell orders are marked with a double asterisk (**)`\n"
+                   "*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
                    "*/profit:* `Lists cumulative profit from all finished trades`\n"
                    "*/forcesell |all:* `Instantly sells the given trade or all trades, "
                    "regardless of profit`\n"

From 0614e599661a459d27a96bdbec00b4221709301d Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 12 Jul 2020 20:06:59 +0200
Subject: [PATCH 0313/1197] Add tables dependency

---
 freqtrade/commands/arguments.py | 2 +-
 freqtrade/constants.py          | 2 +-
 requirements-common.txt         | 1 +
 setup.py                        | 1 +
 4 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py
index e6f6f8167..0899321db 100644
--- a/freqtrade/commands/arguments.py
+++ b/freqtrade/commands/arguments.py
@@ -15,7 +15,7 @@ ARGS_STRATEGY = ["strategy", "strategy_path"]
 
 ARGS_TRADE = ["db_url", "sd_notify", "dry_run"]
 
-ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange",
+ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv",
                         "max_open_trades", "stake_amount", "fee"]
 
 ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
diff --git a/freqtrade/constants.py b/freqtrade/constants.py
index 1dadc6e16..a5a5a3339 100644
--- a/freqtrade/constants.py
+++ b/freqtrade/constants.py
@@ -24,7 +24,7 @@ ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
 AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
                        'AgeFilter', 'PrecisionFilter', 'PriceFilter',
                        'ShuffleFilter', 'SpreadFilter']
-AVAILABLE_DATAHANDLERS = ['json', 'jsongz']
+AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5']
 DRY_RUN_WALLET = 1000
 MATH_CLOSE_PREC = 1e-14  # Precision used for float comparisons
 DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume']
diff --git a/requirements-common.txt b/requirements-common.txt
index d5c5fd832..604a769d4 100644
--- a/requirements-common.txt
+++ b/requirements-common.txt
@@ -13,6 +13,7 @@ TA-Lib==0.4.18
 tabulate==0.8.7
 pycoingecko==1.3.0
 jinja2==2.11.2
+tables==3.6.1
 
 # find first, C search in arrays
 py_find_1st==1.1.4
diff --git a/setup.py b/setup.py
index 6d832e3f5..b1b500cc8 100644
--- a/setup.py
+++ b/setup.py
@@ -85,6 +85,7 @@ setup(name='freqtrade',
           # from requirements.txt
           'numpy',
           'pandas',
+          'tables',
       ],
       extras_require={
           'api': api,

From 55591e287c567d1671811c441c15c3df0b0d0d85 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 12 Jul 2020 20:17:21 +0200
Subject: [PATCH 0314/1197] First version of hdf5handler - no proper support
 for trades yet

---
 freqtrade/data/history/hdf5datahandler.py | 184 ++++++++++++++++++++++
 freqtrade/data/history/idatahandler.py    |   7 +-
 2 files changed, 188 insertions(+), 3 deletions(-)
 create mode 100644 freqtrade/data/history/hdf5datahandler.py

diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py
new file mode 100644
index 000000000..21f521cc3
--- /dev/null
+++ b/freqtrade/data/history/hdf5datahandler.py
@@ -0,0 +1,184 @@
+import logging
+import re
+from pathlib import Path
+from typing import List, Optional
+
+import pandas as pd
+
+from freqtrade import misc
+from freqtrade.configuration import TimeRange
+from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
+
+from .idatahandler import IDataHandler, TradeList
+
+logger = logging.getLogger(__name__)
+
+
+class HDF5Handler(IDataHandler):
+
+    _columns = DEFAULT_DATAFRAME_COLUMNS
+
+    @classmethod
+    def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]:
+        """
+        Returns a list of all pairs with ohlcv data available in this datadir
+        for the specified timeframe
+        :param datadir: Directory to search for ohlcv files
+        :param timeframe: Timeframe to search pairs for
+        :return: List of Pairs
+        """
+
+        _tmp = [re.search(r'^(\S+)(?=\-' + timeframe + '.h5)', p.name)
+                for p in datadir.glob(f"*{timeframe}.{cls._get_file_extension()}")]
+        # Check if regex found something and only return these results
+        return [match[0].replace('_', '/') for match in _tmp if match]
+
+    def ohlcv_store(self, pair: str, timeframe: str, data: pd.DataFrame) -> None:
+        """
+        Store data in hdf5 file.
+        :param pair: Pair - used to generate filename
+        :timeframe: Timeframe - used to generate filename
+        :data: Dataframe containing OHLCV data
+        :return: None
+        """
+        key = self._pair_ohlcv_key(pair, timeframe)
+        _data = data.copy()
+        # Convert date to int
+        # _data['date'] = _data['date'].astype(np.int64) // 1000 // 1000
+
+        filename = self._pair_data_filename(self._datadir, pair, timeframe)
+        ds = pd.HDFStore(filename, mode='a', complevel=9)
+        ds.put(key, _data.loc[:, self._columns], format='table', data_columns=['date'])
+
+        ds.close()
+
+    def _ohlcv_load(self, pair: str, timeframe: str,
+                    timerange: Optional[TimeRange] = None) -> pd.DataFrame:
+        """
+        Internal method used to load data for one pair from disk.
+        Implements the loading and conversion to a Pandas dataframe.
+        Timerange trimming and dataframe validation happens outside of this method.
+        :param pair: Pair to load data
+        :param timeframe: Timeframe (e.g. "5m")
+        :param timerange: Limit data to be loaded to this timerange.
+                        Optionally implemented by subclasses to avoid loading
+                        all data where possible.
+        :return: DataFrame with ohlcv data, or empty DataFrame
+        """
+        key = self._pair_ohlcv_key(pair, timeframe)
+        filename = self._pair_data_filename(self._datadir, pair, timeframe)
+
+        if not filename.exists():
+            return pd.DataFrame(columns=self._columns)
+        where = []
+        if timerange:
+            if timerange.starttype == 'date':
+                where.append(f"date >= Timestamp({timerange.startts * 1e9})")
+            if timerange.stoptype == 'date':
+                where.append(f"date < Timestamp({timerange.stopts * 1e9})")
+
+        pairdata = pd.read_hdf(filename, key=key, mode="r", where=where)
+
+        if list(pairdata.columns) != self._columns:
+            raise ValueError("Wrong dataframe format")
+        pairdata = pairdata.astype(dtype={'open': 'float', 'high': 'float',
+                                          'low': 'float', 'close': 'float', 'volume': 'float'})
+        return pairdata
+
+    def ohlcv_purge(self, pair: str, timeframe: str) -> bool:
+        """
+        Remove data for this pair
+        :param pair: Delete data for this pair.
+        :param timeframe: Timeframe (e.g. "5m")
+        :return: True when deleted, false if file did not exist.
+        """
+        filename = self._pair_data_filename(self._datadir, pair, timeframe)
+        if filename.exists():
+            filename.unlink()
+            return True
+        return False
+
+    def ohlcv_append(self, pair: str, timeframe: str, data: pd.DataFrame) -> None:
+        """
+        Append data to existing data structures
+        :param pair: Pair
+        :param timeframe: Timeframe this ohlcv data is for
+        :param data: Data to append.
+        """
+        raise NotImplementedError()
+
+    @classmethod
+    def trades_get_pairs(cls, datadir: Path) -> List[str]:
+        """
+        Returns a list of all pairs for which trade data is available in this
+        :param datadir: Directory to search for ohlcv files
+        :return: List of Pairs
+        """
+        _tmp = [re.search(r'^(\S+)(?=\-trades.h5)', p.name)
+                for p in datadir.glob(f"*trades.{cls._get_file_extension()}")]
+        # Check if regex found something and only return these results to avoid exceptions.
+        return [match[0].replace('_', '/') for match in _tmp if match]
+
+    def trades_store(self, pair: str, data: TradeList) -> None:
+        """
+        Store trades data (list of Dicts) to file
+        :param pair: Pair - used for filename
+        :param data: List of Lists containing trade data,
+                     column sequence as in DEFAULT_TRADES_COLUMNS
+        """
+        key = self._pair_trades_key(pair)
+        ds = pd.HDFStore(self.filename_trades, mode='a', complevel=9)
+        ds.put(key, pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS),
+               format='table', data_columns=['timestamp'])
+        ds.close()
+
+    def trades_append(self, pair: str, data: TradeList):
+        """
+        Append data to existing files
+        :param pair: Pair - used for filename
+        :param data: List of Lists containing trade data,
+                     column sequence as in DEFAULT_TRADES_COLUMNS
+        """
+        raise NotImplementedError()
+
+    def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
+        """
+        Load a pair from file, either .json.gz or .json
+        # TODO: respect timerange ...
+        :param pair: Load trades for this pair
+        :param timerange: Timerange to load trades for - currently not implemented
+        :return: List of trades
+        """
+        raise NotImplementedError()
+
+    def trades_purge(self, pair: str) -> bool:
+        """
+        Remove data for this pair
+        :param pair: Delete data for this pair.
+        :return: True when deleted, false if file did not exist.
+        """
+        filename = self._pair_trades_filename(self._datadir, pair)
+        if filename.exists():
+            filename.unlink()
+            return True
+        return False
+
+    @classmethod
+    def _pair_ohlcv_key(cls, pair: str, timeframe: str) -> Path:
+        return f"{pair}/ohlcv/tf_{timeframe}"
+
+    @classmethod
+    def _pair_trades_key(cls, pair: str) -> Path:
+        return f"{pair}/trades"
+
+    @classmethod
+    def _pair_data_filename(cls, datadir: Path, pair: str, timeframe: str) -> Path:
+        pair_s = misc.pair_to_filename(pair)
+        filename = datadir.joinpath(f'{pair_s}-{timeframe}.h5')
+        return filename
+
+    @classmethod
+    def _pair_trades_filename(cls, datadir: Path, pair: str) -> Path:
+        pair_s = misc.pair_to_filename(pair)
+        filename = datadir.joinpath(f'{pair_s}-trades.h5')
+        return filename
diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py
index 96d288e01..be3e34e04 100644
--- a/freqtrade/data/history/idatahandler.py
+++ b/freqtrade/data/history/idatahandler.py
@@ -50,9 +50,7 @@ class IDataHandler(ABC):
     @abstractmethod
     def ohlcv_store(self, pair: str, timeframe: str, data: DataFrame) -> None:
         """
-        Store data in json format "values".
-            format looks as follows:
-            [[,,,,]]
+        Store ohlcv data.
         :param pair: Pair - used to generate filename
         :timeframe: Timeframe - used to generate filename
         :data: Dataframe containing OHLCV data
@@ -239,6 +237,9 @@ def get_datahandlerclass(datatype: str) -> Type[IDataHandler]:
     elif datatype == 'jsongz':
         from .jsondatahandler import JsonGzDataHandler
         return JsonGzDataHandler
+    elif datatype == 'hdf5':
+        from .hdf5datahandler import HDF5Handler
+        return HDF5Handler
     else:
         raise ValueError(f"No datahandler for datatype {datatype} available.")
 

From d4540c846ae9fdf427364b8dd1664b3d0b604347 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 12 Jul 2020 20:41:25 +0200
Subject: [PATCH 0315/1197] Add trades_load method

---
 freqtrade/data/history/hdf5datahandler.py | 26 ++++++++++++++++-------
 1 file changed, 18 insertions(+), 8 deletions(-)

diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py
index 21f521cc3..c99436b3c 100644
--- a/freqtrade/data/history/hdf5datahandler.py
+++ b/freqtrade/data/history/hdf5datahandler.py
@@ -7,7 +7,7 @@ import pandas as pd
 
 from freqtrade import misc
 from freqtrade.configuration import TimeRange
-from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
+from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS
 
 from .idatahandler import IDataHandler, TradeList
 
@@ -29,7 +29,7 @@ class HDF5Handler(IDataHandler):
         """
 
         _tmp = [re.search(r'^(\S+)(?=\-' + timeframe + '.h5)', p.name)
-                for p in datadir.glob(f"*{timeframe}.{cls._get_file_extension()}")]
+                for p in datadir.glob(f"*{timeframe}.h5")]
         # Check if regex found something and only return these results
         return [match[0].replace('_', '/') for match in _tmp if match]
 
@@ -43,8 +43,6 @@ class HDF5Handler(IDataHandler):
         """
         key = self._pair_ohlcv_key(pair, timeframe)
         _data = data.copy()
-        # Convert date to int
-        # _data['date'] = _data['date'].astype(np.int64) // 1000 // 1000
 
         filename = self._pair_data_filename(self._datadir, pair, timeframe)
         ds = pd.HDFStore(filename, mode='a', complevel=9)
@@ -115,7 +113,7 @@ class HDF5Handler(IDataHandler):
         :return: List of Pairs
         """
         _tmp = [re.search(r'^(\S+)(?=\-trades.h5)', p.name)
-                for p in datadir.glob(f"*trades.{cls._get_file_extension()}")]
+                for p in datadir.glob("*trades.h5")]
         # Check if regex found something and only return these results to avoid exceptions.
         return [match[0].replace('_', '/') for match in _tmp if match]
 
@@ -143,13 +141,25 @@ class HDF5Handler(IDataHandler):
 
     def _trades_load(self, pair: str, timerange: Optional[TimeRange] = None) -> TradeList:
         """
-        Load a pair from file, either .json.gz or .json
-        # TODO: respect timerange ...
+        Load a pair from h5 file.
         :param pair: Load trades for this pair
         :param timerange: Timerange to load trades for - currently not implemented
         :return: List of trades
         """
-        raise NotImplementedError()
+        key = self._pair_trades_key(pair)
+        filename = self._pair_trades_filename(self._datadir, pair)
+
+        if not filename.exists():
+            return []
+        where = []
+        if timerange:
+            if timerange.starttype == 'date':
+                where.append(f"timestamp >= {timerange.startts * 1e3}")
+            if timerange.stoptype == 'date':
+                where.append(f"timestamp < {timerange.stopts * 1e3}")
+
+        trades = pd.read_hdf(filename, key=key, mode="r", where=where)
+        return trades.values.tolist()
 
     def trades_purge(self, pair: str) -> bool:
         """

From 31df42e7376b024c6a5cf7e6b8c01aff40be1734 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Fri, 24 Jul 2020 17:30:16 +0200
Subject: [PATCH 0316/1197] Implement get_available_data

---
 freqtrade/data/history/hdf5datahandler.py | 18 ++++++++++++++++--
 1 file changed, 16 insertions(+), 2 deletions(-)

diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py
index c99436b3c..82298f38b 100644
--- a/freqtrade/data/history/hdf5datahandler.py
+++ b/freqtrade/data/history/hdf5datahandler.py
@@ -7,7 +7,9 @@ import pandas as pd
 
 from freqtrade import misc
 from freqtrade.configuration import TimeRange
-from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS
+from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS,
+                                 DEFAULT_TRADES_COLUMNS,
+                                 ListPairsWithTimeframes)
 
 from .idatahandler import IDataHandler, TradeList
 
@@ -18,6 +20,18 @@ class HDF5Handler(IDataHandler):
 
     _columns = DEFAULT_DATAFRAME_COLUMNS
 
+    @classmethod
+    def ohlcv_get_available_data(cls, datadir: Path) -> ListPairsWithTimeframes:
+        """
+        Returns a list of all pairs with ohlcv data available in this datadir
+        :param datadir: Directory to search for ohlcv files
+        :return: List of Tuples of (pair, timeframe)
+        """
+        _tmp = [re.search(r'^([a-zA-Z_]+)\-(\d+\S+)(?=.h5)', p.name)
+                for p in datadir.glob("*.h5")]
+        return [(match[1].replace('_', '/'), match[2]) for match in _tmp
+                if match and len(match.groups()) > 1]
+
     @classmethod
     def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]:
         """
@@ -45,7 +59,7 @@ class HDF5Handler(IDataHandler):
         _data = data.copy()
 
         filename = self._pair_data_filename(self._datadir, pair, timeframe)
-        ds = pd.HDFStore(filename, mode='a', complevel=9)
+        ds = pd.HDFStore(filename, mode='a', complevel=9, complib='blosc')
         ds.put(key, _data.loc[:, self._columns], format='table', data_columns=['date'])
 
         ds.close()

From 0f08addfbea44fd3bd3ce83bbffc2e81c5290333 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Fri, 24 Jul 2020 17:37:07 +0200
Subject: [PATCH 0317/1197] Don't store empty arrays

---
 freqtrade/data/converter.py | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py
index 46b653eb0..100a578a2 100644
--- a/freqtrade/data/converter.py
+++ b/freqtrade/data/converter.py
@@ -255,7 +255,8 @@ def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to:
                                   drop_incomplete=False,
                                   startup_candles=0)
             logger.info(f"Converting {len(data)} candles for {pair}")
-            trg.ohlcv_store(pair=pair, timeframe=timeframe, data=data)
-            if erase and convert_from != convert_to:
-                logger.info(f"Deleting source data for {pair} / {timeframe}")
-                src.ohlcv_purge(pair=pair, timeframe=timeframe)
+            if len(data) > 0:
+                trg.ohlcv_store(pair=pair, timeframe=timeframe, data=data)
+                if erase and convert_from != convert_to:
+                    logger.info(f"Deleting source data for {pair} / {timeframe}")
+                    src.ohlcv_purge(pair=pair, timeframe=timeframe)

From 3171ad33b756183e7240c38314abca4832583340 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Fri, 24 Jul 2020 17:44:29 +0200
Subject: [PATCH 0318/1197] Add blosc compression

---
 freqtrade/data/history/hdf5datahandler.py | 2 +-
 requirements-common.txt                   | 1 +
 setup.py                                  | 1 +
 3 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py
index 82298f38b..d27b28c2d 100644
--- a/freqtrade/data/history/hdf5datahandler.py
+++ b/freqtrade/data/history/hdf5datahandler.py
@@ -139,7 +139,7 @@ class HDF5Handler(IDataHandler):
                      column sequence as in DEFAULT_TRADES_COLUMNS
         """
         key = self._pair_trades_key(pair)
-        ds = pd.HDFStore(self.filename_trades, mode='a', complevel=9)
+        ds = pd.HDFStore(self.filename_trades, mode='a', complevel=9, complib='blosc')
         ds.put(key, pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS),
                format='table', data_columns=['timestamp'])
         ds.close()
diff --git a/requirements-common.txt b/requirements-common.txt
index 604a769d4..3e58f15b4 100644
--- a/requirements-common.txt
+++ b/requirements-common.txt
@@ -14,6 +14,7 @@ tabulate==0.8.7
 pycoingecko==1.3.0
 jinja2==2.11.2
 tables==3.6.1
+blosc==1.9.1
 
 # find first, C search in arrays
 py_find_1st==1.1.4
diff --git a/setup.py b/setup.py
index b1b500cc8..7213d3092 100644
--- a/setup.py
+++ b/setup.py
@@ -86,6 +86,7 @@ setup(name='freqtrade',
           'numpy',
           'pandas',
           'tables',
+          'blosc',
       ],
       extras_require={
           'api': api,

From 861e7099ccf14427313b79c5ceecd300963b3459 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Fri, 24 Jul 2020 19:23:37 +0200
Subject: [PATCH 0319/1197] Rename hdf5handler to hdf5DataHandler

---
 freqtrade/data/history/hdf5datahandler.py | 2 +-
 freqtrade/data/history/idatahandler.py    | 4 ++--
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py
index d27b28c2d..debbcce8b 100644
--- a/freqtrade/data/history/hdf5datahandler.py
+++ b/freqtrade/data/history/hdf5datahandler.py
@@ -16,7 +16,7 @@ from .idatahandler import IDataHandler, TradeList
 logger = logging.getLogger(__name__)
 
 
-class HDF5Handler(IDataHandler):
+class HDF5DataHandler(IDataHandler):
 
     _columns = DEFAULT_DATAFRAME_COLUMNS
 
diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py
index be3e34e04..01b14f501 100644
--- a/freqtrade/data/history/idatahandler.py
+++ b/freqtrade/data/history/idatahandler.py
@@ -238,8 +238,8 @@ def get_datahandlerclass(datatype: str) -> Type[IDataHandler]:
         from .jsondatahandler import JsonGzDataHandler
         return JsonGzDataHandler
     elif datatype == 'hdf5':
-        from .hdf5datahandler import HDF5Handler
-        return HDF5Handler
+        from .hdf5datahandler import HDF5DataHandler
+        return HDF5DataHandler
     else:
         raise ValueError(f"No datahandler for datatype {datatype} available.")
 

From 6a0c84b64924db4889363e8b062d769c46f7deb4 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Fri, 24 Jul 2020 19:23:56 +0200
Subject: [PATCH 0320/1197] Add tests for hdf5

---
 tests/data/test_history.py | 18 ++++++++++++++----
 1 file changed, 14 insertions(+), 4 deletions(-)

diff --git a/tests/data/test_history.py b/tests/data/test_history.py
index d84c212b1..fd4f5a449 100644
--- a/tests/data/test_history.py
+++ b/tests/data/test_history.py
@@ -1,5 +1,6 @@
 # pragma pylint: disable=missing-docstring, protected-access, C0103
 
+from freqtrade.data.history.hdf5datahandler import HDF5DataHandler
 import json
 import uuid
 from pathlib import Path
@@ -12,6 +13,7 @@ from pandas import DataFrame
 from pandas.testing import assert_frame_equal
 
 from freqtrade.configuration import TimeRange
+from freqtrade.constants import AVAILABLE_DATAHANDLERS
 from freqtrade.data.converter import ohlcv_to_dataframe
 from freqtrade.data.history.history_utils import (
     _download_pair_history, _download_trades_history,
@@ -682,14 +684,16 @@ def test_jsondatahandler_trades_purge(mocker, testdatadir):
     assert dh.trades_purge('UNITTEST/NONEXIST')
 
 
-def test_jsondatahandler_ohlcv_append(testdatadir):
-    dh = JsonGzDataHandler(testdatadir)
+@pytest.mark.parametrize('datahandler', AVAILABLE_DATAHANDLERS)
+def test_datahandler_ohlcv_append(datahandler, testdatadir, ):
+    dh = get_datahandler(testdatadir, datahandler)
     with pytest.raises(NotImplementedError):
         dh.ohlcv_append('UNITTEST/ETH', '5m', DataFrame())
 
 
-def test_jsondatahandler_trades_append(testdatadir):
-    dh = JsonGzDataHandler(testdatadir)
+@pytest.mark.parametrize('datahandler', AVAILABLE_DATAHANDLERS)
+def test_datahandler_trades_append(datahandler, testdatadir):
+    dh = get_datahandler(testdatadir, datahandler)
     with pytest.raises(NotImplementedError):
         dh.trades_append('UNITTEST/ETH', [])
 
@@ -702,6 +706,9 @@ def test_gethandlerclass():
     assert cl == JsonGzDataHandler
     assert issubclass(cl, IDataHandler)
     assert issubclass(cl, JsonDataHandler)
+    cl = get_datahandlerclass('hdf5')
+    assert cl == HDF5DataHandler
+    assert issubclass(cl, IDataHandler)
     with pytest.raises(ValueError, match=r"No datahandler for .*"):
         get_datahandlerclass('DeadBeef')
 
@@ -713,3 +720,6 @@ def test_get_datahandler(testdatadir):
     assert type(dh) == JsonGzDataHandler
     dh1 = get_datahandler(testdatadir, 'jsongz', dh)
     assert id(dh1) == id(dh)
+
+    dh = get_datahandler(testdatadir, 'hdf5')
+    assert type(dh) == HDF5DataHandler

From e26e658f99bc82f9cd194a0dfb2239397cfc60e2 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Fri, 24 Jul 2020 19:33:27 +0200
Subject: [PATCH 0321/1197] Improve a few tests

---
 tests/data/test_history.py | 17 +++++++++++++----
 1 file changed, 13 insertions(+), 4 deletions(-)

diff --git a/tests/data/test_history.py b/tests/data/test_history.py
index fd4f5a449..2c0665b69 100644
--- a/tests/data/test_history.py
+++ b/tests/data/test_history.py
@@ -622,7 +622,7 @@ def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog):
     _clean_test_file(file5)
 
 
-def test_jsondatahandler_ohlcv_get_pairs(testdatadir):
+def test_datahandler_ohlcv_get_pairs(testdatadir):
     pairs = JsonDataHandler.ohlcv_get_pairs(testdatadir, '5m')
     # Convert to set to avoid failures due to sorting
     assert set(pairs) == {'UNITTEST/BTC', 'XLM/BTC', 'ETH/BTC', 'TRX/BTC', 'LTC/BTC',
@@ -632,8 +632,11 @@ def test_jsondatahandler_ohlcv_get_pairs(testdatadir):
     pairs = JsonGzDataHandler.ohlcv_get_pairs(testdatadir, '8m')
     assert set(pairs) == {'UNITTEST/BTC'}
 
+    pairs = HDF5DataHandler.ohlcv_get_pairs(testdatadir, '5m')
+    assert set(pairs) == {'UNITTEST/BTC'}
 
-def test_jsondatahandler_ohlcv_get_available_data(testdatadir):
+
+def test_datahandler_ohlcv_get_available_data(testdatadir):
     paircombs = JsonDataHandler.ohlcv_get_available_data(testdatadir)
     # Convert to set to avoid failures due to sorting
     assert set(paircombs) == {('UNITTEST/BTC', '5m'), ('ETH/BTC', '5m'), ('XLM/BTC', '5m'),
@@ -645,6 +648,8 @@ def test_jsondatahandler_ohlcv_get_available_data(testdatadir):
 
     paircombs = JsonGzDataHandler.ohlcv_get_available_data(testdatadir)
     assert set(paircombs) == {('UNITTEST/BTC', '8m')}
+    paircombs = HDF5DataHandler.ohlcv_get_available_data(testdatadir)
+    assert set(paircombs) == {('UNITTEST/BTC', '5m')}
 
 
 def test_jsondatahandler_trades_get_pairs(testdatadir):
@@ -655,12 +660,14 @@ def test_jsondatahandler_trades_get_pairs(testdatadir):
 
 def test_jsondatahandler_ohlcv_purge(mocker, testdatadir):
     mocker.patch.object(Path, "exists", MagicMock(return_value=False))
-    mocker.patch.object(Path, "unlink", MagicMock())
+    unlinkmock = mocker.patch.object(Path, "unlink", MagicMock())
     dh = JsonGzDataHandler(testdatadir)
     assert not dh.ohlcv_purge('UNITTEST/NONEXIST', '5m')
+    assert unlinkmock.call_count == 0
 
     mocker.patch.object(Path, "exists", MagicMock(return_value=True))
     assert dh.ohlcv_purge('UNITTEST/NONEXIST', '5m')
+    assert unlinkmock.call_count == 1
 
 
 def test_jsondatahandler_trades_load(mocker, testdatadir, caplog):
@@ -676,12 +683,14 @@ def test_jsondatahandler_trades_load(mocker, testdatadir, caplog):
 
 def test_jsondatahandler_trades_purge(mocker, testdatadir):
     mocker.patch.object(Path, "exists", MagicMock(return_value=False))
-    mocker.patch.object(Path, "unlink", MagicMock())
+    unlinkmock = mocker.patch.object(Path, "unlink", MagicMock())
     dh = JsonGzDataHandler(testdatadir)
     assert not dh.trades_purge('UNITTEST/NONEXIST')
+    assert unlinkmock.call_count == 0
 
     mocker.patch.object(Path, "exists", MagicMock(return_value=True))
     assert dh.trades_purge('UNITTEST/NONEXIST')
+    assert unlinkmock.call_count == 1
 
 
 @pytest.mark.parametrize('datahandler', AVAILABLE_DATAHANDLERS)

From 0a28818b46bf1ded08579c41e951db45ff31fdc9 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Fri, 24 Jul 2020 19:37:37 +0200
Subject: [PATCH 0322/1197] Add some tests for hdf5

---
 freqtrade/data/history/hdf5datahandler.py |   3 ++-
 tests/data/test_history.py                |   6 ++++++
 tests/testdata/UNITTEST_BTC-5m.h5         | Bin 0 -> 261142 bytes
 tests/testdata/XRP_ETH-trades.h5          | Bin 0 -> 310513 bytes
 4 files changed, 8 insertions(+), 1 deletion(-)
 create mode 100644 tests/testdata/UNITTEST_BTC-5m.h5
 create mode 100644 tests/testdata/XRP_ETH-trades.h5

diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py
index debbcce8b..6a4f45fa9 100644
--- a/freqtrade/data/history/hdf5datahandler.py
+++ b/freqtrade/data/history/hdf5datahandler.py
@@ -139,7 +139,8 @@ class HDF5DataHandler(IDataHandler):
                      column sequence as in DEFAULT_TRADES_COLUMNS
         """
         key = self._pair_trades_key(pair)
-        ds = pd.HDFStore(self.filename_trades, mode='a', complevel=9, complib='blosc')
+        ds = pd.HDFStore(self._pair_trades_filename(self._datadir, pair),
+                         mode='a', complevel=9, complib='blosc')
         ds.put(key, pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS),
                format='table', data_columns=['timestamp'])
         ds.close()
diff --git a/tests/data/test_history.py b/tests/data/test_history.py
index 2c0665b69..003e90fbb 100644
--- a/tests/data/test_history.py
+++ b/tests/data/test_history.py
@@ -707,6 +707,12 @@ def test_datahandler_trades_append(datahandler, testdatadir):
         dh.trades_append('UNITTEST/ETH', [])
 
 
+def test_hdf5datahandler_trades_get_pairs(testdatadir):
+    pairs = HDF5DataHandler.trades_get_pairs(testdatadir)
+    # Convert to set to avoid failures due to sorting
+    assert set(pairs) == {'XRP/ETH'}
+
+
 def test_gethandlerclass():
     cl = get_datahandlerclass('json')
     assert cl == JsonDataHandler
diff --git a/tests/testdata/UNITTEST_BTC-5m.h5 b/tests/testdata/UNITTEST_BTC-5m.h5
new file mode 100644
index 0000000000000000000000000000000000000000..52232af9e6a3fb48a11b1e28434d9ade31b363d2
GIT binary patch
literal 261142
zcmeFZcT`l%wl~@|NREnRC{U6l$w4GYh9*c$&Z$WvIf-OR5+n%<2m&e~IcG&Oq5={{
zP!L1}lqQQvc&iEf?6c23@4WYo`^O#M-8IHqRjX>wS+i!%nzdH_pzo+CD3K7-5yCJD
zA0LJTBR?#j+zyTt)h_m7)yeZoyTx$<>o|`(&I{m21uPgY4AVw)Tu**1pZQ1}Z2Guf
z2dStCgD6krpXA5Qu$Q<;CH#|$zw`f97En>VauqXRgX2-3_*2iZpPd*(S5s9-NAa4@
z$@7U{%AQpG|I8_l{q?VYO$=i`l!Wnb%&O5*)lr8W7!nU@8T5s
z@&0wf<6`e*?F~Jg#F78*I8|`$fG6??j-OYK-o#JT_&fiL1y17BU+wijJx&==91SG<
zcs&0pPSsN$5&j*gjt%y|EciBlB+<#!*9>S*q+9=|vLr#R(He?WYFigPh#_Pz7Sx
z(N2s7JK}e6wn6!tTe~=UKul0N16{Pxb;8f-Sn#pj
zBl|eJIGZ2oYUzuDbj8R;{#7ogssJIRumIsdgz33J<0o_#f;zBwu(Kz06^2SqEfXZTomY%NSe+=*+^GtF$K1fST%;>ysI73<->G5ZKAKL4<
zduUXAe?2#KwsbakN4fsXxhYhS5eHfE#4iXA`$IWfyVy85+Z{GSH_SQipU-D?b(DB9
zG3p=v;2rU~ySun!>;%#O*^fLt-5snw|J)DW;mkRqyiph*Acr>0qdimhu*;KqJIRS*
z<;V4>4*PL%wspZ&VCLg4be+&~+CYATnGPGwbT~uK?}3?I$UH)XnwB1Vn1zCw3_Vqh
z17c>w!{fxoL@``qgdYFmCgPX|38>-7R|Fu>gv3MgJ^dgb5+t;Cy$O}fb#>&SMdj&Sk_mGYqI@HG4_0Z=IDgN0u|Lnm>
zHsgZmIH3#E1!j&giGo??4|@s|!GsSWxfty+ZJ3$M?$@*ftkj`hY0
zz_jDSprzwsjiEcfFuJZBz+ePWjv>WAMz*lwa2N~p45cJ!h<|Knr;oM5&^po4n;%j@
zx1;?F{s-E2!4y&$O2^&9w2zIsec@@DDlvS&tLD|9Kz2
z4w?5*y+ga>{i$CEXHS$J${p&<^^gGTQ2Rgj+he<${~-Vqk1zd+|7c%2-tREm?a^)s
z?c_(hA!dU-+A1*{hB>GuZ8)AEoG(=R3_o!CqBU--}dSNt|7
zFHRVL`}4SMom?zEMTP&=Kj9%i#vlGM+N1pkV*R7vf8B>q?ENSHKeRsL|9{u}Xr2!H
z{X^TM^?^AWJsB^i-P7R)3R3wn{bN7JJ@gYzq?Z0QsO8`J$4Q*PeBWZQg_C%3lB4sE
zCX97pFzK|T
z8kOVkn4>2cY^nXI{p9(>alHFu!#QdAJOA4Pe_P;h3;b_}RaDgw>Kd9_SGAGXbaeIf4GfKp
zO-#+qfrX`&wGGPF&fdZCy3-A37gslT4^J;|AK#mPxBLSFgFtXd=YCcR`i91)=I1Y3
zz{}RQ_Kwc3?w(h@uiw1w>mPVGI5a#mI`)42!^GsrPg7v}^UUm*xv%rz78aMj|5!$^
ztgfwZY;OJB{g={mu1-+|bZg9*E8*{^%j)IJZA^3MG00yKrf@;^Q
zI0+J|8=`q-f|3YBK2;f8vq-%#+If0yu=gXdLf-vWq0_F61-;?OepR#mJlV!br~w;;K5=J7@CeTgOx7jB+zlX
zt$=>l?ryWfn`4Pqb5`DNSAOKt>*N2fvH5Xb2u&kCAd&>arz?F33A>SvFbdn0noR1!
zX8{126PRF^++J4~>>>s{N=2_W+WZLdY;Oe#I
zV}Mtmu(g3Fp2mhtzUNgmGHb{!AwtJq=TJ~s2QEM7`d!It5V}Cyb=>#p+jah{
z70>mk;C6N@3_5X>Qiabhc{R?3+o6~&0gJ#C$9>cO!S04F{v@RN>mLrhXBD~`cfN@6
zRO(&gsR4SQAAH(r%#GM-X^Tr_#i+h~c^}b@61U
zIp9;AW=LtiH1R}@#al^^Z9@L;Zb#{WDy_$A@&5TIT{a-bBV%@!Bw2Xk_wOO+>Tkw-
z?}o)*Dw9qweG6?2YYZX-5gSis>q920x)MP47DO`s1OCu+F2~N@5
zm2+C9S#8krI*5F!s6f5G(RjbnBtt%R&so1LdB?o=)JnTPp$Qa)eDhTi5c$)VUWIWl
z(d>;W3g4~KSpW2LWu>d`noTlZ0P9Cn^QcesxU4%e#wNX(Qca%3hWh
z3O37z8_LRxdPFg%ze(K%qYh7a@87l?e3tHl6GdnuSHPd_LX)2^$*cAzhQYo02gn;w
zk-+|Zy%1J7syJ$|QMjt(`-@W|_CVx~*AM3XM-pI`^E=7AHIiB~_(T0tq~0v&g0kGS
zTIO7?G~>E-f^o-;e5*7q
z*%+VPjss}fL_!+LpkJNenkqt~YvhuA(PK9sQ9V}k7d1(>(>wApB|yk%e}fXQoMRM
z5;$n3do%?GT&ph}tU6PwZ%xt=92~qhJ#W*nHhedlBml^U==$=LsMEdlC8?{O3JhVb
zw>*uz(lWRbShrFa7()UitdfU1xN7_AGh{ejbBpSS>VH`Wq$*sL8LdtaLBvJ_6e^m{
zgj*2#ls_|eAnokmyG#F#3%GI`V~5E{nGu`&$H*0xdUj`!M8rgBdV%xoGP
zV^dY7@j(AL4JgJvtAb2X{tT6|
zr!U{!bfv_
zjN-oR3PJi!hOD<3@qmFrYEvVv?3!d*iE8NQ?=ELK_b8ROHBg;@!(j`cV;)R|b
zYJ&n#{T0zoU7%!UkXE>VnS^`nH&(1_lByl=>KsqYG~-BG^y
zNd;c6lg0ejV#IEu0b2Xu>K2zGQvi;I_%-
z$pk$13J}Bc^CfOG`4SuZZ
ziPj78#PeDEvR}!%SB1XgozKThn>Oq;xYq++R7XSB_m^Hc=U&Cx`$b;QR9@$OlFi-y
zlZ>`S_cUw~top46*z>}F^77W=!`qhB?7NyBUqum+jkN7^*=$rHfY7~c#=B$}mbpr>
zT7#JHJSrkdcu4z@lqzX8Y?Z^B;}e)&mOSoc8EWgJE9FNsZw<
zpQ8fIlw^k$N}a3}svPg?3xRZhy0JowSEi4Jh=&H*t>Q|yIpr5t@P{v~S?PGE)vANl
zXO+rzwR$jTHax3W^Gqs@wVGWS(U~#aD(sXM;dR(Ro!sn0mXLK6`3OuOhrJGW@^*>O
zOk3L`;qsGu%4qs};MUCbqOSRetcF``-FP1}UpiXxhPZ6I8@GSAuH(F8F$mmI!@kPI
zZF8Y^Yfz#WYw(fn`q!`c!;bG7m@F9J?3!Te&AnZ&y1HF%Zpo;{H*xXzYMWcOmgT0U
z!h_%~%}pA$r$jthfjC
zQ?>UHy#zPVMFG$G9t&J_{-o!&H^Ukt?5T>7FPtm~-972AZrw~1=XQAi827C)p~khn
z$LtN?Jz0_?v^9x)cmaF)-GWuP$It1#0D0y;mNj5&>li7@vR8&eZ=@g+WGdY$R1)dzTPdVY%${_4wn*W`dc?SWQ@e^jgKZgqwE2)@je
z+CBpqj^bW>#*+8^`58mew|GWu;*OIa2FZF9Y+5Em&MF2IgEiuV%Galo@zlO}WYnAP
zT|l43A$#uid~Ylbm%UZ)dLtqo3O6LuZ!uH#CH_w>X&dkDJ%xlGS((j=>By>9Kqcqp7U>+ckck2(^)cptOYlS4ux`Gl>{oK^U=b|j~WB2KLIovCkcHa_j-b^=7&P;
z^Z0f01}(ssYCRm7eI>jrjBZN~zg0h#WXIGsYoTC=-k5)N>Qp>%k;X(UYoU4ZiNQSp_EH2%;5@FXC9WDXC{_CpOF#P%pq8cGN&#!^r
zQUKvK_L=7r!Sl%cDxKS|uFLQ{Tqm`OBoK(6IsPy2*L%!WwC1tffQTTS8uFxR9mmxE=H6g59&BIIH4@B<@|n%KY^SK)(*{DZa3z
z@MvG6xj}9CoNq_C(Lm~x`x=p9pi5&c>nmUk3Txh!3D+61iN()1U5b1qD|>O`>J*xR
z8OUla&6P%(O8N4(vi80&!*0@R9`X!7{U%?ne_E{>TMb}0%koMYtfACf(3#=P@_W*c
z=EH}>la0!K7Fn8aJbD4-)d~!6%?MR9-6ANV{qAQX9(y|2>U{>+nK+N88Hh9Q^ANKqw-?rSP2aA&ejy)#pdpGaU
z{OBjX%6cV{Johd5n(c;q(aBons%cI_aT7P*s-Kf5j$4*v;03OA>gVWP@FOR8PyWI8
zR6mYnCN9SEtIflDy{f}3Xrx7c@G9MbD;NzC&Zc53NVt#Jtr03M_@q=Z*i$%WucyP1
z!wv@2%V7Z
zBRrQR_*}+&KK;ub!<*IznGzq=@|cH)*o7`T?E*>ucK6ut8P=t!-u*R?v3No_C!8l!
z`F#Y1r|qIxTq+pGbMrf}yNtYyiW6KIzDCU4-f>VTdcc|JCN06WqL2+V;$!pmKfZ5d
zf)~O^=Jc-?Ni|RwXGk|igl`Nrmi86{CgZTr&Q~s;x}9vK7c!<0FnLyezit?A#28_N{bndBf9`H}I%!l8g)p&gM7QJ-p1&((Ymg}@cb%_9b{j|J95eJTcetxUv
zyLTTY6O2~t8>lQeInjgcO$j*Q+qc1dq9(KLZ?l}(EVQ(Ion}JpD?{kcAp3rMj=}tV
zQ2RRn6b(&nQ3J}Nb$3)G&)K=9Ro`*2@H!&}+Hq6M2XMjE)GxcZM~W{ScUTC1uc$ca
z)JaZkH3+E6Pr08?QVObY^Q$_ZpaYEZ&1{J9OU@D#UuDaV*W=*oxG%dYcaH)90x}cU
zr{)9${mv?Fgm>aID(-%I?P|HZu#&dp8XSGKT=4$d+y
zYyWUJ$?@z`3J|bfXny6b(JSq%aXKYzTCxRs17Xi^E7drn#_g){L<1l>k-vXyJVTye
zGpdtK2QA6`Mhnd&Nj0E(eXE%zR|JgrIfRevqp_tt{J0;Du0O$g@6PJ3d6PrkS5rqv
zS5X;Qeb`grR-7cc(nV`Yvo!ShHfNq6afynXska}6$obJw|4xFO)j
z3#AlqpYHXPAdbg!dCYaj)=r{g
zM#P73-I=|Z59}9;z+iwU=_O4wqL!tlbKCnLtuoCCtrkp30`i4AJj>oLA;C)<+g)YE
z`;C;)&~5A|S{>Nt%{}k$nj=~DdC2RZcFkcaZZla(>?Oyn&FCc77tI1Sj&14@R2{k6(68!4$8
zT){+CYFl+;bratM_(dV|!g)h@Gguf+S@$!&^g0>4t4m5Mlbt(f(1S0c^_o~AR*AWX
z_*YOk9e-QL06V}xI(`7xp*V%rBW|4zTPfaTRf?ma@G1?!(_6Yf;*P7NBh^O4X7mw%
zFIFsxv^eH2Ir+%X6y0FYo29{VoqFDHbWx%w4=+1CI(1WU;GA*1#Pl
zQ>pI={Q>5mG1booU8Q0>8_wm;`brsiHfb>w7U2MK!vwjjV#ys_$a^X0eM4WfnI-As
z+uFL*xidTv%bd*s8QkG+w(r*G!ocZYb5<5#{5)|R5HCVlhFgB(sxz^E1aEd-g5OK4
ztG_3e88@D@MHm~u$k5=@VldFJrca?{#|K!Za-F_DT|U(gX;x34=5gfdK3#29xNzZ0
zzS2#Ja~FaDX?LbUYhF=B0r>}#nxEVSr>ineLb`GidAX(6=YK27fo+=cYfZ?`LFia`
z+m!QmD8~i8yYPK+oewl~On;VT*o;H`%t>DFd%F~UgLHC0+1Q;U4o^-H``oYAI>nf=@wKwRC^bGb0zv|pe{
zyzUC;mq){#zZT%{DBt~B`1Z-x>K(YQD^nqPosH7Q@uTm!bA*?~>dnoE{;4TeXsf4&
z-=01Nss0WmB(8*p$|Ijgj=WEh?Is|!S=0yR>8AXXlyY{oJYJ8kuIh0(tE;Djh;l&z44Gn!IaBCBcjQNkE(;}lsRu)NCR+L6k
ze81=BMqQG`GxgAW0%AgRBqV+{hjKsh_$!&o~jg#R6C)#y$wwYft3Kg55pB(3|ZQvdg
z5uFSol4B!clM}P6s)uJvisPNM-Qz=Jlg+h-p$@3}W^#dLu>S)mCreEtYAw|5@bt=-
zmeLdy>dFP|Zo=3H2y3;0zpU!@Y*7S
zBD2lJsb@#~r6-bWMoXKQ+j8y{ILXKarQZy}2H$a5Ba)|cKW_@CPZEw77JfcBK*PT7
zPOQLBGoF^aC<_dcC`Cm%l#P?Xr%x5BLlX2$;g!P+_b7rFSO^j72wp%)xJOu#92ixV
zo_;f-X##iR6X^dmweaBGs4a~I5sUb;ckMTqhDMEh)l@78+^;d+sY&&(%4BwUvgdm
zhjn&iee(<-9^R>u6>xfB?Z;$WdGq${)_H6KCT?=_i{jFDcAnlgj$VqEmY%=^Y3St@
z92x259h{b)o_+7$z2xMC$c)Iy{QUg!$?P%EySoCNOuu{oYP#oRPfuf2VPQ&jRex_y
zO-)-yh1_rv?
z8XAU*ii#2If(479bakV*(1MR7%ONd!_AX`%(p%*@P9Obk@z5Q@4;
z6MY?HlkB?E!qSrL^z`KPbSNf0ZE1O0QdnB{eEM78%;y*F6JI7K(KtBdROC3P$#HOS
zpwHIPnO~<)V{dM5{=nJ7h7*tw5s;Iy^0A&LXQd*bq6Cn2_ykzF02g3pq9rHiA*Z9K
z`@Khwcd$FRyG2G!1Q-Rlqy+?+6=Y=<5DE$kVj5SjYHPa(d&ef1zozZWm6n8~I|VAE%mF?UiG3mCk%u67z97EH@b&kSQ`wt&eJ$->}}
z_sL>UL%TVufh)n9nYZ6OpaH4TT!vNE4##tDYKqz~rHhA}fSvoN1w=6HFT8K0SH
z8e0vj;+VbzgViu6Ll?{&qquzFeiX*x(UGd449
znu=!@_8rzR%?F#tj-AFKo)&{)H#9WJ9D<~=kVi80B_^$Pdv_}hHRtkTKfU&?gY6;q
zr)^EmCr^G~>qP?3GVQxmPc_}R3l9pBWdcvSYKBvGoFF)K=i^ou9IgbElte_f_>4~7
zsdLyZ`F(z$yUnHIht!Tj9<{w@OJi*w!0}iVQGykno}-$0^oT)IGnc%<>x+I7w(^c!
zaBdtf2f(#u{uy7~BknY0S3!HNeOE3npl9Fa!a3g3KKFzNA4Py?G8(gzrKo77h!D(pK!gwwdRG2AOGp@sY}|XXpB4rxucV#(=A-xoa+7{g&$Ojp#w#a8AkA
ziyx}OE7a@Nd4JTAG}ZxHF7giF$w`XW@=lATgSH_gq(4}Ml)ke0=H4M>C!|ExY`$^9@c&kmMUKB%{p%+%|=E>gRpI!m>}BP
ztNo+{2REI+P6`aI)sU&xaCJ^nq{4j%r{J^^XPPHg-U`utDh0BR(^>{;9R;6=awdbt
z8Qt<0nqx}`1}=G2?(IiCNOT73_rm(nD2{T6I&)NH(3=}pGyL}txC`Qe*b=F0>!
zKo&7jSLf5=E5BgZM}>@)=2N5^Z@rT5@8schRVsqPszC8b+#9hdgwU2Xu@4&lXLFSUO$57`%_rd|~OC~9FA{S&-e?3g=gt+m2nltNkv)PJ5}Oa&(%BBhCMXUQ
z7x4BEJ$0Z49Jj2jCMH~-@znE>8l;at;k})=staIq?I7!HA<7c4G6)R4HJOvc^YK$$czOkIt}Q7y
zZdBfNCDPc?7Y=;jhiA$u*t63Ow?~+CcJgMZR8((ow=@)_srE1s!K4s-;Q1`|#6)fw
zpY7dHH_)&#YOQKT-0!_JIoURPVcp<53OsnwJ^40&ZZIn|C@XmLtFnqao!x
z)h}NtxPJqS@18a$rK-Itr?^}idr}uNkV#it|c-JNRkP@m-*I+KlepB
z5&L@<+AYHR&hjq`wfDBRcPVrV4Z#Kr-hHdki^b~eS2noRYgAS7RTC}YyxOL`!h~Z3
zSnoi-Cm%2Gs`C2!W1E*QD$!U{fIO%kwZl3c+HaG|oVfrLvzVJyRjXY{$kVW~MSN&|
zt~IE!w2+6BtCDkb7O5tJt!pAU<$Id%g87++W@nlP_Tl
zv}Ss@HkAp*EU12g!`iwfEuVYdCXe*KjfA>1_d_UVSbwQ*6rO$Akvx4J45A6xtNK6_sM
zrzgCxM(>_keFuDpR##1onHGkv8Sh@$U1nts3NtaWR&fWRWvGur{>(g_7(GxcDWW%dI{`P_S;#>gMF6jAh&R??Xes9n(3f
zU(BW~T>$9XwC8kJf*fOWeyX{Uukps3%6uduA__<|OS_epcFPG^Dbx<^5(uT&*530W
z;e+c&5s6UUSncMF<_wFtFniKce|ur^;8vD}qOr2Z%5|&qy_J=M
zG$6~N$mwV)K4fOLouzHf$9K=*3VYt3$PR@MXWL+#X*Q_a-=`GZ-zS}%tjjHZ-lTEI
z;m)`sX>Gdl!Y^)a4ZHWC-H4pLuC6ywjVU(mH#Y@#?ULr;@*q==U!9o5aFC-sfS^#(
zCm+}&Bi-HI>*_YRC1d5QTx5+VJj`CXik`1y0*>w-JNe`mt}7j-l9hbFmv0zGx>)Jz
zPLq?@t_KcLS%6~YNJp8VT3aK0qlD#j$#>P4mA)p~cYQ5X%az*o-FyQKOvgXhbjHO_
zZQ@7&{8{93oiOUn_3LvbGt!yYJ^TpJz}p)g;p}ga()#$ZIohx6@#E*2#97Hdr9V7f
zP4ar{Rt0GK83PsE-bzXN%pRPY`AF&6O0S3W$>{c798pn1>iJISZ|>yf_4WBAZ(Q{h
zR`r)YxAB3LbHCt9N;{Fp^s3v40O(ZyFr1CrYb4&2B5?nnOG@gR-}=2H?jTgpfbg;(
zUODh&U@#?noET_cNedqlOwlH~@27;*L6{ELRjGcvj+O$ug(x(+%UeGP37sdS!%hED
zq{$2&I9|48cTIXu%q9xhtF+e{YMx4Ir_?xo%pszOo=6?H#+<8F)M;fdJ&csDibEsw-o%2V&J
zvmEeW=&h{0+jk_H@VwX0gD3~^c%K@^))T2_7_7R=!Tz+v8EbyUK?S4NtNdq&^j5u{Q%QqJ>3-Z*G)#b7W=;
zSkxKBRtUvGF6HHX1J2sL#zYLdX`=&I3rHHeB<-g3kWtEunRgiQDHqB=?mp&Am6eqT
zG9kPVMlHT)ES$AcA*j_XEDTVi{a~y|d*g#-@zic<3!ta>o}ZtJM60}Hr+t%Q)Q8|H
zM7a9-QBh5TtFeWHgJ6DUm)~)GF6qYj_#i+ipui4*e@H>I3h*G%xk6=Bl=8W`WpQ!&
z6s0q_;%=ok=2mxrw`eG4LDj;|xw$p2j;PCpEk28qe!fLpC@5y7^M_A`^Yh0~b#dF`
zm$Ve&y!xpcKZzLbp%=nO5fiI}smqts>e{ACgwbfAtss#NJc#bpal+cMVH&)fn{932
zyTsNYwX197Prd8+@4J1z$F6o|QS`U*XmKoo5XXhI0)VH;=Tlg7Zg!J?c>;~TQ+cXi
zGUASUob}f3L9mz6pfp&R8XV+b_O|wY7nV%^ILg*mWv;k*`8T6y1(l#X_pZ2k`
z>KbZ2pr$1UA*%J=k4!2|?C|O7@81Fw^J-TuAsnMqHl85H46g>
z2h~}QZlj|5^$R+5d@(Wgv%ouK)aEg1P43stJQi!vyvcmK$w2RwHZyLK`)+=|Zc1)u
z&-yyAfx-G3g>MTT$h6yP&g)%&V_R(7WHeE|$g&|-Wtga|X2C?G!t{wM0q80S`sL`w
z`t-)0=_)MTkO`1nj1347ZPI5Q3Q!V7<^v0hd}Z%;2Jz>vu1Wds6|Syy!Eo!)FIJw<
z3xCQzqF)3FB%&PE;s-Zx!hG@YDp+qPv(t|@l7uB(?5wYkvub1nl{_&qad8Knu@&604rsZ_fUcl&Mu1y->-
zySwk1&cD4%@F}h8nsu`f-&pY-cw6Aj$kx=WDd6Y&FzZ`ucXvgh6kWZELIK=Db;g*A
zZ2O&xt;|dnMQH%1puivND8apdN63&utRct{KhwQ);!1gSo08qWj?)}C)B9zbYcQ{e
z**noCP!+4qq*vq>+@Wmww7b{H6CCe(FmH9oO3ZrfO?xnz@jL3>TXI4?yG3xgC%m=@
zKNBk~6FB;L#KjAG?9F9nDvJ*f_lsJs9QaA3UcVz2RL8M`>IPj8zleQFztC|&794!p
zolC-#Yeh^+NGV=TBzn71|1KOfj(E3o(PqiU}Tg8
z^gO~BsU<$o)uqf>)=;C27nCXJ$PWWe#fRGS6O3I0=`SfGyok&jROoMz2
zgB?*(4h{+2x+M!YH|R^w|D9u;)$7%`IJZF;Q|`f^gVE9QROh$z4X9|Sexy>W(9ryl
zllwl!jx{K!Lv2rEA23Le55+8~+N_O+rku`!*5AUJ>Hd^-%GL2EC}s`F`l$KH%C@b$
zE5wA}2{0racnvRNqv3w(INu_
z@x8E!haBpGOdT$2KymMxIEMwwOZ2-rRl{V(($!R_H=LZDDQWU)&S`1R$spZj4|le^
z;9*eE-k{r5U8d+Yx~B1utnugLrgpEN#zz2^C2n5A`gKdc2NB)RLk$l}i((~2!
zqM+TYSF8G9v}ZludbCqudV15-hM0t;-YPoS!=ls~Hr&-!yS}a-
zbVJUHytTP?dfal4@wN8@Gg=lFT(CDIA#vkI`z5m_b@fwAO9K0f_pIWqmT=o{;JFGv
z_G|0hH^3{F)VoO
z-2quCW*A72kl4#J6}k{X_Il8h#(FQll%W(~4jmZs&`&|w=tm$J)DeyOesNnh?jjq4
z(u#XOk+94~DkVknTVf6QLSo_qXmX_GNxWCo)ipogwK!VVXL?CWs<&$qAtfcn&h6)I
z2}p8Uf9_%Bka#@TIQu(Fgw()btCH&LW!yEIOsyfys^NY_MmF8tx|WPGNRm9oZDBd>$6$IlP6flMgwmBV6_AS
zL5IWNNL97piE@-mv)@)UaIBAuxyRsB7!%Lo4sMivnaj#*>Ei6tndzc$$1Z{}&?iVO
zAI)>@Eo_DDXpm{k_0TP4;J!51mCVb=E1pTtYm8w4pLBTrl9JRi8J0oX8NF&=J8V=PGK!aM*%~cYRizY_eo@}+$G%H=bh}ileoD7
z0r!_r^l0Tkv`nqicB}$ZUp*R)hE(ZH|?`ws|L2xa@RD~Z_I
zsUNdfU+w)NpOuycoNn^kU#sWl;;!ccp~7q&Z{OCfrxc+|N~hP(j{d$^49IAS$$pXb
z^=)w}jR-qUow`}(`I`z}Dz2kb`!x14!!C&Za@j0*qYEkE>-+f2myM@gGIH+jjGkY=
z(7Wi+cN7(Yt}f9m*%dn&T%%jAb&PC7nd32ri+tH`(8{mqn)h_eRPaQ^c{|v
znCPUW#5-CFT1kmXs2B(XP3VF`7F4|(9TT%-Pc*ZuBVX92_50`BU-MANYQ5v*?Q`dj
zn6iBQt!qq-Vi}4V@RuTPKZzIe#%HFcO2Ab~Nk1JuQeyG0E*ElU7iX8A&?i3?O7inc
z{BR2kIXS_|Fel%xzyCP=;;+TUMat3$3UfnaN+)bxzK?l%g9URyBDe=PH1!p!tLqy#
zKE9s3)YLnUb8l7(mBxu{d`a=;Ae1gNJrNhHX^!0rrUzG$I
z$T7-szSyWL-41&YjRFZOm_Ao3mC|*
zt{(n2R}Dd*K?%wCl83|1jm)n}Q!UKo-hP~w=I;+&kbF+Ix3kl$TUvTbA3CG1@n8!Q
z5)k084fGWE4-E8!uFlSZfh`Eel9xy52{Y|ZP7@MRBJ^|!2?^UGbFy=QpCa|bM`^|B
znor|{EXzF&1qJsF<4ZF_aBxVa({nz50iUr1?CcmBzxJFxiw*to$lmS>_Gw0LIXPKF
zl%s>9DGE6G-qa%4?D>uGvmmJ1+FodhT;N9-xLIjwai2Q}HiyA<4+MRtZ!8evaszON
zkm8~Qpq95nIJr2eNeKw>L5I88jEu6fY7ioaG(adSNJ~S|CoMH00q)-J(wl{)e2{D#
zncUGd*wGvr7F-q|8d4P<)M2flplHs^b52oF#}-6(JTIy*yqk49)LZ$kPqN7Q7}%fZXb%frJf0)i{aA|kT#^Yikbrr*m+
z&dJG$<1_%13kzclyV!esyQ`~PD+}K~_V+Y3HPuwt^p8NmC-fQA*3;HA^di5hp{}8{
z(`Sa?unAuWxNh2a<=O-&)}
zlh{aARYgTr)zsVosbS6xBS%3SfC7tc<)8D>*
z`8EjJXXX}XaOlYx7?>Cs80e|UaIn$K2UKSmPHj?=;FA*od~$Mf1|}A60YTRDtin`O
zRE*5R!qC5+U6H#iDl2#X47e=KNKa39UYM5+hKIcisEFAFg=OXC1r$^e3K|H622j+{
z#z39n;js^^UsF+$p{J*(mnkS|&a+-Jb$3@&q;dBR@iGWi3hWi>DTXU%=J(yDB^Z2r
zdv+EZ8~6PA2FPRwXAWTM2ZRT(&k)E-3OnFBfF(mEYS_W41MGLOx|c9m?er-kXi_l1
zCIn~_Kup1O%rID;1#B7%FPnTC2bO)7nL!pd4X3vtgDOw)L-1s<=(GYXTW4Cx=ra}s
zTEl8&VGwM|3!6sFFzZgw9g6Q~-i6geP$$kb8_Yrs0$X7XkAFZ|CO>Q%W(sRyR$#_~
z;7llk8f2k=kUhaP&DUxNUJ~UxNk}2j?Z8|#AGnn&3QrKRSbra@7X0y}6qHK)n-_j&
z%6}ROFBv%47}znM_9P&9X#M#?dj`pMu{H4g`SVwmtv;KjUcJ4MoN;k2D^}Kto`wp{
z;n*|FZ5aXpnKgXzV!-XXAT|pA;7iGP9l|32{d4_tN##
zEw;XLXz_sIiVBjrf-IwF)jwolfgs&AFKt!rX5|GTDyssr`(GBRdbi&j)ad-^g8sv^
zch3S8uEQo3RGEA4xfj>EhsCO(+Mj(v(%;FLwsR?@k6<7Gm#nruJf4LN4*bY?LHIy?
z=B>wBWiiW$A~qOSG?UfR3}_fO*eNkD%C)sUGZsZ%Da~2NCGyQmX|6Ljw$}AXK
zHp=St?=-iKhMkq{V~e>t^K+>M8$#GH&wldXH1D!S5`~|-(DUtowL)^<^X4uTQW<$
zGQFbOwYEMfBDWsBXhvPesn}s+(o`+uO11+v4<69x$h3s*>}c)mBqe&o(i;CCqV77X
ziY`zaIH`2E;->2Wf^>s4NVlYPgOrFMB_Z9?Dcwl7NViCbbfa`gegp5l_gjmp5ByszsL89g}Q*LgKW-^tE(TndS4tI{Bntj
zq0`U+m(8EbnZuvCycY8yk~c+hbyXjqvm^bgx*8rogk(ZGPI~@KywSb-tO%(QAT|C%
z#7C~ppfPUy2Ho}V(g&=KeS&MmU2YVhwsW&g1f&ai2$io_8}#FEI~EX>QNqLvf*bAZ
zeH0Tc_)-(!wFAoW;SB!Z&T)MFrO|*I$9#uj8#30?7iYn#TmozE%iLh(^70B^0^v&p
z)x!8ATXLMLAKBbDRaKZ0=a`t{RhvTKC|w&H&EMZSK8zz^NN!M9SE}<%TCq-aw6w{^
zLBPm7h<;I3bC6wS-!Qr;>Wr#qS6b>Cf~OdQhlf{HRb2uP58(Ou5Ds35{$4`g%>Eu*
ze280R&@Awd+I%4tVPwRciUNdNv3QTOaAirEnPokpHmre9%9(_en4yl*JkKgtp-r%m
z;NZaV`bxaCAdobwn9t!+e*|f2_WlqeV#xv7V$M6z+iM#f9Ik9{Z~vmho8cV^|7m2l
z4O_#@&>JFlIc#oKP-d?$gn)qHEOr@cPNT2jzMU*3Dw-_1?X)BEf;`#kGx&_BiRQ~?
zDb!IJ=>7xyvtCgXhePl|GOjI)mC4{>mI*LZC}gwOC?
zU;%tTBG&!<3>PS?$zMrd&S*lHAnfNV9KkC(9M(UVa{hoPe-<#cp4{C0JKd6;wzW-w{-fuKhpwV5b_ga1^g@|l(}!vakh85yb4
zIGdh)(}{>skpiNkSiExHucTf86HE-RcPkW3^>6&|?oyr}9K^RCv>X76OR|a19;`QH
z_2I>h;l(!0)t?Tr5=2c+;d4xKrrz5Evvacyn7&h01Dua3j+!aARBG)Kvns>Wxvr31
z3!LXHV2Z7lcb)17TPD9LG1Tx}@2LG5P|VIwpRkTCQ1xD58pwTvKh5%=4lh&{VZf7o
zE-CnKFpHFymT|BUt*$lUpbg}<>P%VKIn4H5il|wARm73s2$x|?@N2}MJUOw5brS$-
z;6=2HVi``uzcws8?vv>BOREYuVW+*{X2;!Mf?ZsIOS4c(-zK>qnxyt~HyQ@`a;4J`N7hKC!&a05kr6-={P+
zpFPdEqEreo&oDgkpUc3_rNE`YJWcK$p9U}CQi6QlXAD(+@$oGl@seC8g3ZlS_UMFa
zcXuUscO9U=Vbvs?p&QVyS2DIiVP67p?`ceGtY<;ejwv;5
z%*^~Fo1=V=uf$>=_5NggRcX2hY_QzO!-uQL`)6IHTQ?)lWQx8ZXQesy@jXSp*u=v#
z0Iz0dgjrelD4Q#WlTUO4u*{=)uv*PDHNX4gM&a}=F9U%px4RTfbId7Nfb88e%r8Qg
zTDB3fplrjQW;d<>4-VjwXT8PMce$3W^^$e;g20@CNc89dD1`@OSsM*et~Z4$2)(27IPdtki#y8pP@4C{*>>8H(ndp8@f-$#%7c2
zf2rBmTkoe925pBl)5-Iyv^0BjLwoz`zo*QDXU?rt=HUx6UM0T6jo{GNx4}ho6n`2U
zI}W*-(B&x(Hg_5a;bdCGT2--g87JVlm6a_{pKVT`ot;ffyV2Cr(mFmi^K<-extdin
z=@bI;neEehHfNd$P3tqwEG)SC8z)id)=cT>W_>R%W>qdgW2RhNgXiAXmX@h2T1Fo(
z^J$u}uWyf0^7wP|BKb%7V8AydzbmC-@_C=Owx(14M4yeT>lzH!N_}y0K{Vew2zqzK
ziTbQy|2Q3UZF2XzR-@phasTPVU@$YU;|P)l72uMp$8^HN!o;GNI0zTgvAbzv(u!pU
z?-O8GFo%;Gj{rWF_u2T^Clfq8$?8vUaT?FS+qalv*QqHddU}cDbQq@g44JyH&zHaU6+Pd;
zJ8;)x0@zoKyc>-APDipKGP1_;83`kF28P<&O=pS8nk4PyKA^haB{IxOqjj@d!`yeCf4kY}U)&
zyhR`3Io`+9_RMg1cLN1!>6Db|wVg5Cr-%utK`V}w=E&Cbzm8>PGw^Wri<}Vm&fo@V
z@j>7I@ydMetMG7~gy!Z{NtzjVu4aKEO@RP`P8|8k15yGrMaxSY5;_5VXUD!}k6Ax)U{AU>
zUC_}IrL9)wsrgYugn-%WPTO(toc&k?&E@%1YH)ZXpdzxtys5~Xv?E=?5S{-01v$<7
zkbm#O)0wOe8F-rLcOvSjE06rGjI@QPJLSCqAB8M6Cq4c5
zh*jAB_Hcp0G`V)gg2*D7J7Gfa)xzf}z^P$aLVZ*4`iGH`(O4ZHGp6fG{rXY*e6f4q
zc3+o425|4Rvm4=RY-hdsSUq&e?8iz;$y!f@K}sqpctV5v6gwYHODW-PDz!e<);2WE1#Pp2AEmUb`UpO{Ze841-lUZK
zKW)b=y^)akHCC+YIR#oM&DX=`S_xYYF0?jTXAJ~3{htq5`*yEJ4codDu-pMe29I@y
zn!fM;{y(jUhtcltgxFo8Kgr4M(21Il{S(atLoF?Y?c62tuie~~pK{YaZcA+QB|XBh
zawAm<#=ye(1yG!*r_D7f5k~2>J%r^CjL>LFG0KlH!bq5ZBR1tEpaA;kf8?13&(C)f
z3L4jYj^O4Plq7K1;?19|xl(?hmYo8&pmjjDy*+9|N?5&AWh!EJ&dtSQjv$E7h*$V=
z!uB>8?vWl7Hy0C|@MrvVxnIOdE4#mMQw3*ZGe-Y>d_c|zFy!b44`)}bY~Gj$Qc_mr
zidRx6d3eMI?A%_<2z%)6gTq_Sea3nx&C=>RctZh!(l96QFes(p-^Zid+uMT%>eMhg
zyVMZwFgZ6F4esLi?GYwcHs6WysfmgjPh_gVp8y<_6PY&Qs;aiJMcKQa*41!{p`kvd
zKq=(%hVkb$Q%C^GUgMU7<95~vo|+SByr)sIf)fkd(vl9hAVu~>S63M9?%rAeXcPpy
zQcR2!>u;vf+2wLi2#*Oi4r0~UW#|jKUrD;3{j+od^N}HNp8_G(>+9dwLY57y>YPfH(Czg#EImDG$3xzFtm-icyio>!
z`tpB&dhsmxt#Jx(Jp#V~fc+H{I?c^BI6i)KZDsU&x!_&6i~9v#Ek1(i
z^G_V2b3C6HFq4JTs6p1>zXO=$42TZm)+fDlb90>|mh<7mpG@H!egTdw4suYUEuWno
zHt>UkDpe=0K)dz|3+2aMYHE)kPI#3GQ9vPZrE+;GsutLSA{{v>PJtz79Dcy-dYt6c
z6rj?4>ONNmTK|-u#Jfrek$H!k;NUbg;MDsna&nfIa{F>K
zPTrvOm8UBnXF&6&Li46@p89u%s&{3JMmJI~qy%BCAD*m$Nz$HZebkYT#hZq>$j-md
zn4V%XHnGZ`EEe;Xq*3L}fZ?zFe-C-?;#Q%C=T`w9{xs=l0s|UfpEQw*HoZhz2?u%?
zLy13QeJZWnt!T*qCK7XEF)|j<_330&m~U+0tT}-rI8$NE@3dN2f-(Gjk=DP~5A}60
z!uQF@Qc8%w63MN?=W
z2=1FsXCKC9&gyedIZ)b`oLNv8r0tZmA
zhCR`yh+wp4Fs%VGRGxc7zU{
zf+QnWzJ{A{bI%)-1&a>ta}(u%3*OJiQq!hm819~SH2?H<&6_qmZr8OK4sM&!*H&9hFVvX++bUs@-o5tJ(t;<=;Wyx}gA!{F%xr9tR_
zIXQds6=|<^n39d>Y`VJKek(a|C7Xwdhkxq|1+~jbi2IHKI5^jfO&cm*0s_YL^jD(M
z(b3H(QHMQOX<%uE=t~Xyo7p~95;<4<6A!(S3us_1x5(p&H3+4ZIts)G!(j2Ss0|v0
zk?rfh^Ycv*>ETCL#HYj7>I@~CyNwf6%?p5Tst^3l;+;tB+xsI@_Zvmh0hZD3x+3U
zV1Zeui3P&V
z#=p2-b{AG-qWv+R6?HlU#`o=(@gGP@!O;
zk9AUccvebfb>5HfmX-Y}E7KbQW&e!R2o06g;e6YC(ca>;5o+tM*!-#cml>$!@V)sW
z3n-EZ?Y>~FF
zM2&#J%g2{FC#2e4D17y0L7-abmAQ-93(~m-5K^N@!M9WX6W=|rp7!eM>PCr*3Rg6d
z2$@to{*m}-Il-un1GGizXlo-N6ln_y#d$`#yMLpl
zMNna$F3B?P@mO1fD#bAtBmn{D(^cQH78bs5A$~_3i;R7QBG*dMs=6#-AxDP25E`0O
z%jF~N^-4tKFYA=9un>5ys~fFAdV&pWenL(j*d=D!W!P0+ot9=|9F=C2TfGl*$yzLQ
zIpuKWm_o&`Q?bV7($W%RV{x$M>wZ6}Z6IdMERZNx*40I4ZFhHf-_3pUx4}ZR;7HVvdaC-}95S+&
z*3LXXSoy^7SSwU_<0w+Xzju2(FTnR~s-#r?){ja5WJTS`3^;zh%|E9PV${}_lSm#%
z!fsRvizUI42uUyK`JS7cs||Gh-?2P>3IHcR9nSaI*s|i`Idlv)r`VoM2We>x{O=$|
z75!E0x89#We^$s#%lns5)6=ssGb0`y9o=ltk4+$M6VcF8z`-+1aI63rjGh<`?d<4kdwXoKyevNy
zdM>koNpMPvuA#NLyQ3p8H-!E(4Gl339x>i^2ZoT4j?+gcKQ(bJDQTLUo3&#!Ts|&2
z9RmXjomVC(Vq^gdgE7YKKi;si019S)D>Kf$Z7Gw|<
zRGpdG+dsN8(9qQT9ePV;&2~jWASym6DXE|!J2^W$B|bSVGd1LMR#tXsbWn8ATW5E7
zA9NpIFLN6wCtGVNDIHNbASEfu4x{CLrJ$yx{zgH^2Abs@5?=Ow`vwid&d90xfV_d7
zBsI0LoQAxrqzE6>z~q&Xmw(9zBPS<+28Tw#4tSU;scG4{g@uIBaO5Rn&xD1aKZmeY
zK|x^&`4=xFUx$n^Rt1SOiK5+1OZ#1zriigg%OwFPTN4HLJ?Y
zi%3hdVzP2^@$)OHh>3|wiHXWWZ&J<{ModmfNX$-5i~t9Xm}8QV!#FwF**SS7g`t`8
z!cQb%%)NT}#YNT?5u&iUcVl~Y8-vHk7!mHmT*Q-o{O14L{bB77Va
z6jT&cKtS-6oSX(aT%VDXqvD`qAR^+BT^-!O-R!TdoShw?0d!(UMjRq?sMyIQBq1Th
z#KgkREvjN^<`NWJ+V$s86`K207TY9=QcaUq_^NJPZsMBHhBTuxit
z%snvRb69v1^qIv3xM-`ZQqxdTQ9mK3CBj|@&DA~4)wy}O`4baRpA#x{rliEi$HoSJ
zf}-@0*w{~)5i!Z%s_Qd7-9p@)Ei**nu&Szix~qS;*8dzIt}m^I(oW*)>il9T1|47c
z-3Ea<^Bu#(<6AdKL{NGNUtbUPGI8PB?huHV5Gn6)?+}=l@T8Y8;O>y_5M1sa-68lv
zNhikr0(o$?K2W~71c!PbTrT0jiIP*mwR5$@-95fTIPxLMd<3;P36{{{L|HTL5IL9N
z$nQ|@5XG0!d6rP2=rS|&zQqaGj=m%ZN1+E-11V^0zU+cpXj?S0I8Bvzld
zei25g7X7D+gG8No^fP#JWst3cmi_?SF2sNGl3NSOK^W(TVB2Y&x}&C6z(ZA)*djA7HUYXy(g}Jb~1JS3Lk=PzYL~c7DG)=+>z;9|F&mZ5NwML!M6BM&%*oti#Q?Jwy5d%CsgnjpMB8;
zf^B(vB+epZ{y?zpS1dy59!voy=3IeN5
zLpw++i3LFBJ#72q0k$P=I)CjeEZj@d82|dKrOc(=&Bl*LFknxmIq;a+yQue7QCH{N
z&)6Gh!TZ&lUIxxKL)RtqpF#eUC8ejxQQ#C^?xh>A9WC*GkZfmJlCWm&P3&!FB0MPF
zaE7gZ$Bbn#NWc1p3%`YbGZz%s&iWs0E8gf0uq#dsMUj2CSbKIn>UdS(xi}Mj`3|?v
z^kW(H8zB2NnR(nfzW`CZ6zuM+g8}?MGPcR+uN3x0sIXJw%xaQLy1pj
zZCBIGkMa(JZA%N4GZf;}1`?LJKn?jNAN)lxQUnJ>iQ7K$Ok)G$UsBUq1G2z~NM7+O
zWk40wEcnEqoFdvlS$_y6ifk?R&3~{hFmW^p0Q10o`o}O^Rpf7O^Obeg^&ofmJKlds
zuN6_O#}6nV*j60iWv2wO-ov(k|MD``{$(yQ{p+I^vob%eW)}V_S{w(AulJ6RCuV8t
z-%$Qo@@w%chKLySied=1rL{#}t`Y!sG<7vTgQ~U#ukE~w^z-yxv)xFmPLqDzn1%Jn
znxYhdiZGVsgttj=Zcvy2t|zT9SnR1R#+H7)Li&nEp?eS84mKMIuv82Z)HyZdWX40V
zZS1)XB?Q}|;<^_UdEY5i0)Fbxno)^YUy70WTHEF@PmgSCv4!tl+b1%AXr&d6fLz*}
zY&Ya%geZSG$hF1I-65LVlA_7wNiJ@3(C$u;0_op7$i1wQC?ME&X?0@6(tXqtv1E_)
zKi75yn(@~K!M5Q=U3ipnb7zU4Jd5;ixeR&G4Q@4jd>6iLLtsVTlmGlyB%u>nq$>7BUZ0lcgn9NX3g6Awztj^_L^d%)I
z%I7=g@}-XtfM8o%2)309!AvoDaBbf%{eC@OQ=9TJHZGWq4htMiX?T5nbil)7kI5%-
z_A3qQa@MCvL#}O@HhKL&$w}}4+Y))+!?sPtAr{Xyh(xF#Tw56P_*}};XP^qfwxJpp
zomwi&euNCtR9dOh*rsVcjarw|D;ob~C&AOH#s}E;+n;H=2!FLv5{!1pwIvXPd)71?
zI4eC4)}J>&z_wEo^xBbRQ&Z~jLi(TIQfttRL9Xr8i$-8kvfvTfKpZ>OivYQ{W-5%P
zksYiEZlX`GqA|!142=N<+rF8SnL7N<&fY$Lo9+@sYMEj^b^NV?a!Vk~-FOH*`T)VU
zEVAb74L<%dRmzP;{NsmdDPYpN&g7r0%!x0cy;`^qhhSTl40CgTN@E{xsC(dqkV?XVLsYY4iBY|->S%fPy2U(n(b0uG3nqjdx
zh3BVavoR#M2Rk{Ob@|6?(VP^GJ894$-3Tc}Xk2Kgd$m5PftwC9CXZNCreW`zor<37
zm{F>Zgy5EFf-tj_MIM|!bZr5tDs6fdb3xMoRNDYZwbfgH6bZ_jLZcc--W=zG*`TRQ
z*Cn{h9dxe~7o2noveE;ODm`F{gQ!d}nx*!aiFs`UQf-wo-;dzzYZd8my{wppR9kRp
z4jBCP7A^3~e8^cK)z*FNk%pIae}#|1(W(e`p&VGNPw8vGrb~G)(=pZJ?VW_u5}m;Afqy+b-sHTF*!!PqYl?jI!9^73iK!`;5)^D-P?#|uVITQb
zPD=GHm}oV_e}*~DL8`5P*Q9i$Gthh{e&p@F$WP(zSV;&&&h?B(;hdJ33@%w@haJ_!
zOu^e(+{lemQ~TZ`ER)p8q~ci^q}mqN_*JS~RvwqvQ2U9vVp~y?v(6^G!g`QN9a(c>c-b>ie`%J|mp&Y$p^@8Sn&}Y~HE@VK{#6C=^;ko!
zZTarAox>nVwZ#jARNKUjFCD0Q_o^*RFnBsP(E$ZFCpYpc!S|}IH;GIsJeIefcSdow
z5Tx3I4yTyk5Nqp;mfG>3YCC3SLJX04^nX-S8=?aQfwTgKO=|Z&Yimoqlss*Gf}!Uj
z$+Hquq~?qPskW`aDly+ZzLtSIzQY#>uYM87snxA*JxlxmskXY1YRerC
zskR`k0Ag+NajfeQVK_*$k{lJvIK*(sNOYc%YCHArt^+XYL#%D`41vqX$n(}eSIvJE
z?0B5&k~9}t_x6kJU<#o2ZNBG##yjpGC92(Wh_Gj{!3WhA6MhVqRY&w31+-6+bn6=#
zRW9n%sm8tAeNb)rAk{V@GOn;3Qfp&a$AbleJxR>#LL2IqxMe
z@?#jFATokjTNa45J^NbpjN^*&LACw6aj)8{$W_TB2EhJ_a(e)XwUs$Um~QdwwTD#O
zk{3|`iFz}
z&cZwN-wYr{a!o5iL+Q`dy=v>2!06i1R&!g8Wq&X(
zPjOnctBlm7C>uaCo|4r6sJ6@xs%=g-6Pq!kaC!vB$#pPk1E@*fTp`2gQSxjtvO~Dj
zOCM3`x7X@Pyda(9kJiCmK?W{XRLt)!FiSQX-5)J8d)6dLDN)V_56{28t2FdqkCp-I
z6;D|acp1(qM_U;2bxarIU(@m_&p#b=+)p%>hg4etv9|ZBEe5j5W9qf@%zFLB8{!x}
z8a&^Yn^yBj0Y|_#K)a%cE$4?2Ge)9zCE$Cz;EFBpk+%h@wwn^>OFbYylcBY{(qbJ_
zZTZd9INDKoc7>7LE+`?@)=k7HNERf2TObfVML68P>3L&dV=jiCWrU^tbf_f$TCoki
z=HLmK&pI2UH=7O~>@bKBVD_+Qqq~VknYujM`A@ag0RdC!*LK2>!i$!nCz8-mUWtI8
zAdStwF7v>O$eRq;4B*-d$ds*!aK;8hs%;&(zDA1azAmy^4LzIfKx9Ml21F`@tbWI4
zz`)F8q^++bwx*~nAjSe$lfY=z!VOYw0p7|;M#+0a=KTyi(c-1^IuQlIfoSY
zCEALn!aD?zI5JP-(^48^}!ZLec0J2uU7Zc(GyKK|;wS8Zh~(UUxZmP`qx
z+OD}ks%JjzBRdZD+{0oHIl1wromeZ9(rft#s8O7j07_Ep*_dL^=R8T&m5JPzlp|-S;X`AIg
z51F?3kZGHEz?(8jm!M7k
z(T{?K4*gv%K=^>#8bPS-pwceR%!6qg%6(dDdD^ts{6V-9JW_I%dx_Q2oG*7aPsk#~
zl5X~{jSMnvTOiZ+WPmXM#6I_aKyBe;?@e3vXB-Z)oZm`!9!y&fA7fTP#)OXvp|%*U
z#WZ)#>pd6Us{D#kY20!7Za4$Iywa0^Bz`~8QRg1DP5p`TG7Dv>BI$KDC1l!G4<3&x
zMv#D&5guil*n8A=<0#UsRsY|SV`Lp>?fXZ@<|BB_;V|Hsied9;zxXudx@1sD6*6s2
zJm@W-F&$aVH#ceavpWKb&U@50)bY-dw?r}%iB`?*OL%FkpRnV2xMV0irzjY!>fEP;
zP}}Rlvwg{sh`VEX457Ov^!%i0szKi5!x1o_B2^AI0S|k8IFtsNw*8e$b}nVI?vQCq
zGs;i_oI!tFV?Tu2s+NB}By&B6Oxsx{G*?<;$g~~uFQv{O0+W}@(@o?8<{M3!jw%Rr
zd!>kcX}%>w(#UANa8DuA7Tlw@>=pN@ZBcLkWkRKkjtl}{Oqz|65@Tl+CPE|7*~kpk
zHDYGfL68-zbFQ%pnVRaOdF3U57CZK0ythpSbbXYKp)}7L>Wk=|`Q(``>g&3?YlJDDPIH}>xBF&|U1#4LYgm?zr8A!ORpyG(R9Dh~>=5>%0b
zcnG!aGjFWvFCnG+bZ##OAk$Vr(_H&vZ{GXWZCF0oufyreD)Q#>tI-;AydJB`9`Q`V
zT=OdT)%eE#!Bf~8FskM={;136)<~ygTT;UdzSjKDv}H3Eh!Q>AZvY-fEDZVT-xZ^f
z#nC7QTsVmJ~IDU(PSLm|@^z6b1gF*1)>qK|oc)()_Hrjj%!HJF)w|2gSNKOt_1
zt3?XNibyN_xpz;MD2yiFy%J1bj^=IEHHS>wKsSaib(&}ZwB^ou;|Y^>$|@fjxWD^!
zUt2-0yk$9f4579?8jc^oDyXW_Q!oE#+J;=tf7{v3LauC@0NQ
zvS}ghJy=HnvFpur8Vx%PK&b6UPW3T|&YLhNmB%`g1H?q#mEE>jE;%Pz*N|xoO2_Qg
z9BN}7M)%`Z|1)jj7~ruX(-uw(!GDFj4@4%i;yLvq^5Pu3GH8|twq)@lwju6kkU*xb
zHe}j<0896%?IDEP>S~(!5w!j=>p$j0!_q)0
zkdbQIvEx->`?NXHgK4V;nYNX^(~;mm)OPN(Ni3B%xwK1(#U3`lj*g78vE1T9!y)q2h$b|aO??zEDB4pY&
zdKuoEwj>Xxt(Y+)kou=9CNx!8R7Uc6SL;g_WZDXGBVf{M%rM-9&BdgY00_0!e?V=K
zzsBL!B+tcbZakQ_@Q`WCnWpv_pxvXkNc6jyM?&t~LZffB_$iu0yBr|XcD231NoW=5
zQ&zelv}_g?ck^Cny;P2j?Zgm@i=w7FX@gALeH7@qCs|~#o_^s^i%3a<1qKFp%KK#f
zXWAxs1U@R4hX)X9+nWB!`Wf>AWZGg)3wsI0V2MoY#+V;Nrfrh~3@AM1X0G1%@(M(O
zf9d{zrfuGXX^SeBCIJu`#qgwHV^oaa=Xx05@8!Yj3$W!&O9dg*)-O$O#|LCx^)dM#
zZ`w++uaiudK&CBK{!_&ZYcw7wH^$D0?vDk-Ms9}ZZTTT%CYgJH9$6++!-=$
z9g2I!o~9MW9Tkp`{Ab!8=3`+7c~XLsVDA;tNn9s0AFMbatwb`QL#8dGI>$T6w1s~&
zb@o@LMN`u22SnO#gB^^CnaSYA(h}ubl|s$%mMUIE-fjw7^+Alw-gChs0+4s<(aW0x
znYL-a7+9Yph!9+;Y;SEXF0ScrN>&P}=mH5-tA$`J`*p~)_4S2J+jhvbmDz?&+xXAc
zm{<;I05WaSCNkKeDeviY{UXAbDEFqV*Tgu@g@@O7S;B89iDQsyn^dS@a6gzu)-d#+
zX={)u744-(_>z?7sSiK<@M!Bu+rxLHK(xILS3dEewY8gGKM1t_5(I&^r`R)Cx>Dl}
zZFQI+^14s_oA)FgJ>JNS9K5Cl68nU
z3o^)x{OaLBXwlFC48jS=n{HTPS*j3d`xF9gQ(^xL+S;`^u8abM`1#wLxtY#AyTDif
zfwpd_{ufM=51_5^c?S4Guf^sDiMCp^5NO+2Q<@2Zwqp=zJCfU2>gyku0xTfWRz*cc
z^!Wq?+MYn5tzlgD#(trnlQ$3jeyO1u$R8M9oPQ8)V%_n_^Epx*qlvNi~`o$Lkw
zU$ngkZL{-yybKJzGUI|=Y+d{r3)d}*B2@Or?
zgEU(JlY<(wkb-RBe9zfB+n5>X$V(b%>MJNI>6*XfSscn
zt@LwQSq>RNVJV2Sl~*v-*3&aIhU5f2RYNmFTZpsO)7DYD=WHb;`6RgIEp-0lY~`)3
zHMLd$<7^e=RaE)q6}2JGR!UD6;%xO)A2?eWA)Aa0H-MToc(@Oqt)Peirz+&xszRQv
zii8RjgF^jYSpb2yA_h>sQ(I3&PhL{*KhL(HsjRUqGaTx8#zKcn?$?~Q+|1Hy$g`c8
zTp5Er+m#i_v&DiuTXY;80L7n(|LfTzpCX`~A)+E+LVx(rvn5A;IQcL_nynb5*+Qd4
z_z#-xHQ9ff?KL(c2Kfv2$HD^a6ylI(D+&kx2eb8sFx%=bqCbDehZZ2q7LN$BZ1s(V
zBwqq2cN=kSZ2}@<$f_0M5t6gAwDJmsEZgw^v21~hlZ)wpm@UtLnC*D$y=8lPdT?^E
zH8GG28ncoMA4;AfVX=+zeSO8%J;RV?8D0D
zjwgi?+~OUg2vqsJL%%~bzJtGGK&N07g;6~BdGTP`dfy=iXFd{zEZfZI(5oqMDDZs{
zp@t{As2~OM(j%xC%CY2kpMr)xShlg0U?`)Xl@L<&Crvp~1|^4uIol4fH7OgPV%AGPX!
zO8hPIQT)|om0Vt(Bf1}OA7ArQ721zFbAblfvNXDhU|mrbkZ^C>f6eQUbR|bpW$$oO{z91o8{}@+l%DFU)%dA
zhK(%pl9Nx;-LliXS{I!O)%@*CFkJ1NLXTJcmU_5v^p+goQ&c^V1^?}Dc|8a%Bbha<
z8;Lq*m+(`?Z|bY5yUr8Pws_0;m~5QFm2|oG`Ll_&1l?MT*QjL8rGGZd*RXZ~7<_r<
zTX=zEMVZw9ndN9Hz&G*R&z2Ak`G^GC^9n==EF}>g6vU1YW|es6V-3F-l(#{g>;;JHWMBI(NgQC6zFb#!PY!>rjrQw$AV%}=Zx-pV20G^esuy^$NzGv(
z=CM{0=07QW)^jcMYJ-S%qlr7sQm)_%i?iT!cSB|c0Mn*>7;&Q
zPjK5inC#V;jbiyN^1F}`o%~cJT`t>rU{Y=CSL?U_mT`cQgGq3{V48#6V|-@0lXU+h
zhnJC#L{6J>`bMM!gW3-GTCv=i{&z9m_w#&!hvQTF;#U8)7DhsY9$M6QY`7Id;26mr
zYx^;nW~ITwvoYSK`Ti(K$4cO%>3oxE@pwPWd{5~=bHoZB2hJPNn@0Vumdg8)
zt?I(ANn~tOoct1~%=xKxGGuJ*;3Rl$CR~pNm3HmK5fEl~+k8`fTGC|%BqzQElWX1D
z9`HYnA-PdO=*um*6Nz~gwmC~*yR(k42V|V#xVAhg*{tY)3fyrh_E?YM&$3YIeR%0c
zp?u4i83?GPJpT$*JG>lkG)tI}BoI&Xg!)zvFh4YDDuT^|0smITmSV2h(tZt
z0B&IjIoinQTUVk5-nH(#3XU4(?kuGOu8G>dVsHPV#)6kPf6o+%YLCPPk=Q2sNvP@?
zH#QuKqfG-*pLEacy+Q-AsDX)@%ykOUzlI4P<6-S=Q?*7)2&bmxO8j#9pqu&`Sn`Ot
zi)TyD9pzA<3cCK}rPS*2+G(MxSm}!&kT5!*0R*#{k|xsTb(hDN#2QCPe5WglT*$NO
zyjz&0>#C1sya38i!3E`%+y0s2sF3#5MbS=iO@qhFiu9|0EtR_((!}BBy8&AVjE2_K(u`98^mQzzONt48X-Szs;R*44BW~02l8fYp2KIEz6#G-ebW6VesFVLmh5Id
z{@+(hfcR5L&*Vg({-&TPO38lyZ}HtP7)_9k0LZ=@uVlP_W6Y<=%J69o-yr6*IBb1Xw41S?xf4-r*8$7XUZg6`
z)05Wl4smG1RlBre{kxeYWHe$^6hWdq?aP1-bk$As&V9Pof|Fi7oM1#UHy~D#16*u_
zovLKZd3xZI@{J0;fzU@$jr|Pul5#IUp$}!EA186F6yq)}zRzd?k=m3RYGjVF&kF;E
zzYAZI#)`a%*t8ct&N=X+?uhHS1fdLss2SfqkJFP-hY$J!(A$#hvyMs+zYj4
zo(6P53#ZZAfit8tLyBzr@gHLyeRBQ`E$Z(e4G6XDBJz*qc8Ouy%`R(3{Y6@?I>^{F-c;ft-6zjm@bB5Y>1EmchV=R<&+#(
zNDZ{3eOSU7oUq=GK-n{VUCAlj?a@pU5KTamX+!5OvB&BZ9W)W~g3<4m$=1Hq#Bx#Y$R-^c5VE93*3
zNsO%UBm;yV$1ca?M!a^^bh#k=YrlF#yix42^HHAJUF-VSjZBQIoe}l-M;)*RNzV!p
zH|q2%>gmpJ`-G$Y-}D@OXP@NM_ec8o3Nr&tQH)yDfaxhIqvoD8^1o#yN8wHiXd)b5
ze~k5W`rS*BamVR^EpTlk*3cTOiCx+_lp#foA#V%&((^v4ik@XNH=oF8?mNF
zksxAmy+}0WWa0LRMjlO>Dzs$r;C}IEp(@l!`SWj&Bgh#OLXS!j#
z?pAGJH924dcf(SSNUUH}V8!&Qc-J?A!PqS3nOW6DpFBq=SR3qyS9rBQ8_^o8&6hqS
z!%!-|bTy4>a8_y%^WiYr4kSjaFIhwwy%$WDJ#(^}N^mS7(Y^Ej`cY$DbMSa)_A4Ol
z2`8QQ2%D9Y=*w0}SDr|ba8i?|4t!&RH26Mv(#8RtHTG2ys-`6I63o`O5B{v)GFv3S
zEv0bJl|Bm3*e5LjQ_OhpqoeZ!6?$>pYq?g2IhsBkAxg+79maK;2zs5r0gb++TRHE)
z+HZe6alyRl5&2clFMaO)?aHqdHBn)hSsAR`QTIfMqu`HT^ERNlGwiLDJc>b-eD@r{
zX!>eM)e69tXpgWSy>Z=9WVcP8+%K%wS!63&rqI}2!m_tT3Q8W}$8)xnsI3=&Hv*V1
zK2lDL?>yhml->4|V)>Z=1)nu23t-L{4EDpe-Yfo=z_hv?;J5H|X=hx&7W6uhpI(`f
z(gKtfp!}nf8&@k-`iLtiF-hfgg8zwv@m{^~o4LyagZZ
zUu#dZy)Qe<3bt;w4~dY3T}|${r>sB0rnmZYeYq7X(HnQJP83hUpvRw|2nQA?@>jTh
z{WN=?3tFv)8GRCzZ{$fW*8SgjV0${h&eWATsb@&aW0>mlivwj`+sT$jjGMVakY)37M}P|6)gmybvR
zp8Zmy_}=lICy8l4Bi5R^b%V{#Hr6OXHE(_9Ovz_I0N}+H@N2`dQR%FoB(}{Z;lSbl
zTN)Ti*C~0z8MYS!bsWp$pD;-0ypEKt`+sb`1yoh-7xjB+q>%>UK|nydBorh?1nKVX
zK6H0?hm;`FDoCS(sDN}zHwH?V()U6C_xs*E#&rx1zj@Z4EW|mEgT41z>#%%a6H^4s
z^qo%IC(Tzs)kPYe4u1eDmkrH=+hW1H4+h*2>0h!N+H|Ox)a)H^(Z?%~o0EJ1xvghj
z1HZy6$PP#6eevrvd{BZ_bBBJGuQv5feA$0X0toMK8B`dWblDFt#y*PKw-a%U-Z~=4
zr^@ZS?&2z4aSmQ+(Ac-f7?m$6#;Dwrr%iJhXS7HRTq^h^plHO(imV4BdfWY?+VZOZ(WVWJsXbE<)}OCfpYXx46-#H>Pq_n3=OY@VwBI0lHYF@Z=Vme3QM)}}
zOWYYs1Wz{fu|6)(_q6&;;XKXw@qB}zhu^3mHo>Gl0rAp+91B?Y&3~l)!~Vo@XAEf^3F{)IAe*}s`0qD&`Lj(wKWGKRvk4f
z0sAN94l0Gu=S*|cGh080-nX-*5&kH5YD4j(
z&Xk^0zEx&`9FTm@x=;J&w1J>Uam)%KM{nVZ&xE3>YW-T2CN$U?TQ37pnE)yr@+&
zNPhjN#%Fk1yI#UDt&
zoxzhg53_g}zD2A>W!ZkQEqU+tCTBO*^lm>8e}$#24tzgQ3Ua&up()o!g|}rXZnLqx
zo>H)-+!hl6FN#++UF5HQmeYO|)l+Rf89l*qVUkLc#y1(TD)I37W3YWwXLw#K_F&?^
z2uSWuR5cOe)qXEWb)v3NnX9NJcnL%T<`fEoT=!A5CwZMH2op{WOKD%vyye@xTib~h
z{9Xj`nw*nqX_b00bb4sjhz`rR#*gXy-s4|r=lR~?waVTNcI!<_cKmi!jlF~lyoD3O
z$$7S)Wa(RJe{MBM7W+M62rTACt1lW^2dzrp{Vse(g&FCA74Wg0kWa3O!_`gs!|jhIkoD28Q9u|#d%%uS-!DA8)mF_#Mn0>BZ;MPqf#UHAhghU17
z1H*G|uPJ4p-eO%p*X#E(n8wi0k^|&r>2r2!jjRtStSi?upD@}yBh?<`preaq7M*yV
z-fcf}DD^Kexi3Uf?%wsI&>6BnX}WK_QYt#zQSd(u~ns`>frW*HS0<741;
zIq=zl37W9yiHn0g!YDjt8z{oJyQ6h
zOiM1S)j*TmeH#Z44B_7m6M67LkM^fD)oT3nG-GpVo-;`V>1Vc>amSX37@+IM-AlGt
za6@3eW=Q)y+tmis3qwjp=i=B&EJHaSwWHE|WrakbzA?E=F4sx%Xmo
z!WCoDsbWUdh$NNnxwt+1r#G|kCiE{MYIbQxPaiDK?W(?1Wjm*uThg29cvW(%axlqM
z!$lOJDk9hi?WdmFNVa{97&@5XNXiS&EB_FzpoIPnhvTj=I3fRS#E<5e_5=ma?ZJ>WLx%=l;t_kSr
zTjF;ew3)3qltVl3a!$XOqFl6uhoKp){objJ`J5XU-(-DD(CT{P&*bUwUQHET}n
zuZ4M6%{zg{d5(1kOjIn;2qbc2$ESadW9*z2N%1bQ(XSACRa0(Zg?$Tg4lovU=}p(0
zrPW66e10YWm4xaPl{%!uuxf`bL*q
zMpWs+AELTk>xuoq&_?JQUDRkMl}*wYKP}-cZv82~nHYW4g$fFCx2Q}xp7t35_(z8(AM2TA)ZSPk
z+hd*BqJftDd*QXZ6eH_+8BPk9z?75x%G=C0cL5AXfUMb$~KLElgd)6s-m{K`dDd?(DoTJMH6
zz$QcY3*FT$iM|oaPti7#zp~vKdmps1=>$v@rsITiJqK8Mk_OXk{t6l@jrij^Z9%s8
zjgu3fHB$AzV9`-DP%;B}>VsB9i<;{%~*bjXv~`
zJuO-LBVFhC-jTt80TDrF_E&t1xIaC!ZugT<0GC=W3=B5*WFCAkd24+>zuOGoMCG_i
z1h$?$DOR6;9}X^kxH@r3?n|KxCeRYf@V!%FUnMNP!y;KkXwe{YJ5L6TA&Gp#4@zuG
zxI3oY9kTJ*Wv!{pQ01Zpz7!9jyT}j&Qj9r!n+G(6(Ou4+XnHduCDv1;%X0P6m>Y*R
z>`|0|070qc#g}&fpal=nKA)I(48YO2(NM+zfPAc4|M0cD~T
zeD-&KJR37<$gLdoLZ^v=p!eZrgY=*5zC&y+?S>%r)T5Hp#yPzyy
zY!COl)Ga>?2{VPJ2y2k*YGn24siVn>*ejYTd$vcfnLO?Ik>c+VlN0d;DO9IGs(IIk
zeplri&uMA%pRq4qR#jM-n#vvM%YU#O(gv!g!GaJTwat=r%Fl)fgM?-=(%BjV#PQ8c
zA|4_Alxuf|t^;*i^
z1vg8Hh*CZ3==L$E$T8?456>YHpxKV8I#&tL{J`9^=!%oGD_A4@BQ}d^K|KrDHfq*-
zmgYwNq+;=z9>jX2P$T>j(X2*ubo==aT6303P)nfQh1Yj3ZBX-zm+H@fTHxMnX2Qp9
zp|=dHK5n-%S-|?xMg$_pjNbVb&N<1SNe6NU)S3IWvrI}a^lL<;{>%dbr+{Af(8HTw
zg_Nw6gw6>u^WyvpJ%)t#Ecnw?-$nNTKC#}#5~t$&nja3SM}{3%=~(ig9{Yg%hA58_
zgLilC0CkcNZkIwOv7PiK#uA2jeKH#)+4YEdp{H^6bsW`J+yprmijJ(w3B2rQkvLd5>BO)V0
zlQY8Oyn=vBM1W_QXNXIPPwbP#KsR?wq^DO1(%C=Q)g{TvF(d#4x&**QxJVZ}JC^{Y
zgSU4|fOB$QX0%&ma$0smLRth!PD_dKjScn+ii$5tO^VA1f0mXSpOO-s=$DsYkRBZb
zLfj)A++5uJ{X#5t-CdAYuJ-QUzJ4JA!Hz!O$COkd}=>_cUTpbME9F6=ve1a1_oxEHfE$w5Ad;)?a
zQ~d2bgF}E_OmtXuRB}o{P>^#VGA6>qUdPDY#@f)_$=%Mw+s7KjxLdlogxa~d+w0lG
znbek6_NqEICMvoPmLC3o0T%AS(#+D--CWmP-@roK$jr#x&cw{e#oE}-)Kk|c#6;T_
z`1*%O#)r83xcNpU`MQ}S10y1QBYXn9Y>_@bo@P##z}>>m(Zt2l(#$0cX>Vs_X=Pw(
ztz&QQ>Emhd9^~)p0qoq(-R)hBZ8dD{+ynem!jLv@NS|P%5J#IBU$;P{G4L}C3G(%|
z^9b>9^K`Rt4f3>fN7|Yr&0S2bBJI6g%z(MJnW=k%n~kx(xw#X(U&9KvbaAt>^KrCw
zv9_}I0Pb!sE*3V(;IOE;?8t=Z02iOourRNfFwe+Pf24h2FmUxpy1-tp@cxfLH!mws
zPj~M?r(jb{9k^NAI@8V;SlFAJYU_Hsn>{jeaj{NFO_6?a&~ffJhH(OZeAB>biS)+S?-yO+0Lzf{{UX=15y-BYSI4S95b0
zmmpwni}bYdGqpCda5wdF)=PEuba8hyb+&g4a`*A@bg>1VmNsS%j!08$XAcJ-7dLxf
z4-4mzaC>tnH#;vkFLzsDsuy7E>F(*L|)>g65i>0%RXY31mGjByHcba!zCep#tr
zVeXbbKK{-w&Ni-IL1BK*Zf@=_?$KVMK4$j*z~0)*%Gbfp+Beu9PC>ACcXG8sTKan0
z#JhTjBq#WL0Ql1n4G!^x_jI@-!_7@yZ9QGQyuIDctlg33My5y~;OrM@YHDF;Wo7T}
z>f#cPbg{NI(Qvi3wzhHi(X=o%p)>O}HA7nX__?|p+1gn-ntH35yBgS;>N_KCZCq?5
zmag6f9uM!*_#Xfp`l)0&M6(O)Qnm_qhd0lUA|0ZhVBwu|6UyOei9fIK|Gx+PwHX5-
z`5ec2TqGaWx{oL0#OMQS{H|6T2wzvMqwrsPP)Xnhro^6N+7AolT$pupq#WwCGe&Q!
zHPQsHJ$ganH78k$L+Ly&{Lg;;&))ja?zdzjN-YY#L;t+V|GcXIyw|;8bqpG#EVKo~X
z+5Z_g{uyij8gZP)V3ivhJ7Gs?41*n^QKo_b8lPg=!D<&Y>cWoDNDVte>{xc%~8i)9^Vf6qSqy8D?{u!_RHCpv@!)hEf(!q|*cK(3tke5E^$hT40q78q5C~ZT}hB
z{u-lxslaMAG>ZQ-BK{fQ|25Kueuq_QX#4^@LSq)}2#q>#8KAL&DiBs@pwSU_ghnpd
z5gNBOTA=aqD>7Jhg+}y$#!vr@k$;V=536An8ycVdGaCFe(*8C2E=a>_AvE&Aj?lOd
zJ3?bURUkAHd(6P9AT)l49icH6c7#TSS7gvwj^_%i1JG#l&v^5napkX3yaXFoEuiuD
zKV$Phqt9RCm%D|qdJ2t+up=}o!;a8M
z>TPIz^UoOY&v^f@@yVqHtad=7`adJ-KjXw-BVXnztZGB!PuLL}t6)cHbQlqY#?PFo
zu(}0}A+RGfO2LlMh-NczW&C&>RwJO1@}F_~pE38Z@wa{ltlogem;a0||BSqUjfs2O
zCZgaqG|Iw`(1-;)LSqkSy5mFoUW8v$+z{dhFRW`Kw#$;Qm8;3*XA^HLY-#c>M
z6qzrl>*6f$Q-a;zX93zg*H+(TI?FMfPP~XRy-T{;Rpg`2fw9<}VlM;eeh*iM8441v
zcjkF0v7*e@Cs<2R@Ap3s(&5AX`Yy{A$TOXPc@<+WO19lo?5DwnwcMKKC`*4lS{-2|
zO#JI(KJZduLz`<%vX!Da94rsf6Ttu8k>mFG*5y=PoW*^L-Cj@Wx2>R
zg7YsgW6VU!wtAlWX>ejKwWc}9fC%6C+1R1s>1{DXQV3J
zNQmg?hkQ@v+o)i!A<;&X=3t;KSeGB~TYENA;nu}uZLEbD`OfDOe@$+Hz0#KM^zi1%
zSWTp{$n}j+1>UOc=<`jS=+Tx?0Te?a$Vq%z!4h-m#oo~JS^D%@3`Xe~*-Kkz(Qmk;l2
zd$y|r)5YYgSaUJ*?a#&jnq1h+pzXi$`E->Y^sDqpU!^A$rU&Q$#3$a8f#XKsReEr)
z((~vlJ<MAu9SE*S+
z=eTZim6gJ)tSnq*g%)OI1;Ihia+MORtCW;prDW}YDOo|`AmhA>hr?Aos;=Vk^Itqd
zIj9b2_zubRaJAd`I
zbHk}bWdC)Yg1;yk1rfJkjm@RuR5EISM5llr2dnYy3Uv$V$%q@zq@u6zE4%9z4wC9h
z;9xhMUy)wXH^|a4e~AMrfrBzcW`cv#d?A5@s{iym)H1P9bGku2Ol|;m%O%vsKcIdP
zHv%=6JJb}0P`4r;KwYv9Q8s=QBDV)DQ5r$rhVl^VXFs7%7=xO}vkhu0V~B51+
zf~CZ9sCm8KLQQP~HQFPnpKtI%oiqWp_dBR*OrJxIE)TJ6lOJO87g+LXhx&$DIn)>m
zP;Uu9oicd?YTpiswB{8MF(1Ry_8q8Gr|6*edk?imCDd4oX;AM7LOl(j_V2`k+Oi5F
zwo*F8T_ISSp@%x)!!@X_s-afSfO=0j0P5MB5CcErK(wxbC6!F5_eFxBo@0Re6E4&?
zFZQ8U&4PGvHyGkPBP?~{L2di;5Nfq-sPBb9y})D#bvHiLua2Nr&sl(4G!)|EEqjPP
z1h7>58)}W*C8)*1pk{V}`ZJ*h)OCL#YCc(pct0GLSRA2-S%doZG1Pf0P{Wu(&FTa-
zu{PB8C*n}&uR?_Rf_U2*magkS4Wk8h!5Y+21CZEU=*iHo5(e`FQ}s7%Urlh(q6bNI
zrS=d_@DNfxsdEOHA=IL898ks#K`*iyYBK#Is4rwBaIVTIvBR+7*784f`2eaPToO2l
zxZ*uT>(#Ri@XNp_;h2|y29GwVjGp~Z{p5jaj7kCr#q-U-hJU~6FJAccIQ9PyGIVHz!!eMle&8J2sC}Twuv5rFobAiWCV9d
zAxt2enwpwHG>2$mVPOf;3L>0wX#>#~qMeJByM~sSn3x1H86q5bn+h=vVtRUd2E#KaeflMtt-rlujzK!iiQ
z=OE5QTv%9Ggt!E8d3kvS;wr?owY9Ghzd`)|{reAy>k#2i!Cw$JAZ~7MZb96J2xoEc
zLfnJ6zrTL~@em>$E%+PaABb=f@d?CJh-YVK=MXRG5vcH&X5G686gWZ=6*cH`fE0la
z>lhdqn6Qoo5gSgvgNOqWE{?~8hz}7CNhX9y1d*7S_&P)qh@_;XWDv%JT)ynBQq;IC-(_FL0MSzw7BG1>GQJkipr|$ninr$)q=X$^$m?p
z%`L5MZ{EIZ?|9$&;p3;S?w-%RefRg~y6Y
z$||ZrOl$TFnZV@4{*9SNU2M6@VJmAiG
z(w!*%i|M1*J5<{>~}%ux|=z#RAEwc8Zv@Y_@0qbg#2#`
zU3H2kWo3y1)E*Y_@?%bK!Ds`qa@L+f;>`I@=tQNy_+n7s{cgJlrw?-Ifl@eQL^0ZM
z5~nOje+(Bp&Ja7hc1*+S&5CSQ=VEJ4M!<-gHMHd-Q2i!dUen!@67xpFu=|&n^53~~
z_ZnYb-U$&2XSTZ&Z<$5K>DCp-c;4^P(G27mC-$5D#gOf(>T5tW0EQ)WyZ{=FIA<&D2K~z>AvA
zh}_#OXXO-$EGI!qR{L|3y70PZOe$>qOBTn`0Djy%rvs?JAcQkzFka~Ci5B$rHq3;LsTdl~G_>)%~o^A`NH_b0I5
z2E%QjZt9Mn018c(88yF_*fl`WcjT3)QjmdI)%-ArCT9BrkW+U>9Z0CXmByBBvwRUv
z9j5H5cX9~{$1Vcn&Mhj4R&RqlQ-=>n3c(8@zsCiX
zVPWCg%=@7e5;qIcaLmrq5({vCT1tIWA^ixpIZjOJw#xLH_P#!{Nx=v7>Rk?`_8l=Gc(ilbO1X|
zMz>*97fpC3DaoXJStdnPpUAd;o$=RSVu|0qWmSMi!UETLa-b)7AqtC#lh{oWg4!@W
z<-EFkb;*+}a(~1DOZe~FoH%;@$sDbn6rNv(oX<0JT{$-ngkGKm)Yw&agJ%Z`H0w+c
zICgFr;;U;ERKLh0H56lF-~V=S8V|V4hSCKhsB|78-MKzniy|y^FQ4d
zz!2Ql$esZ+0e0t3{?|E1w{crqmgHC|a&t}dO`~kDE#^lZ#)&q9%2#--!;%joJI8i~
z&(F{Cel_FOwcE$I`OL7&hM9)^0`24vY8_1Uc5+29#zIm>bLVvZYIjTYMApgt6gO1@Z!D5L6e*m
zD`MO9@n!K!(CAKM3;W}R9ibBO0AJ#KJjIr>3{c?PT;z%uNS%Avg7x|1%c!xZlv3s8
zx;E4t)6pyM5EWp$%>165=Z-Um5s=A$P?0Z7{B@`G!HDwv+tD}0v+&n|r(tPeAbZv}
z6(Ksldhl|Ut;UWC{#chJSL{u~L5pYt5Jj>occQ7wZZ4GAVxb^miP{%IjVR{R+RuKbEWp
z-;~_fthqZuZ4|`((6_MkbyxHEms-oqZp{|kvINX{af-kuPc|pdx>`4n=8ry=8E+ZB
zMI8VB#KsMY%`^+g{zGa|GUfH|~U
zFe+q8F&N1(tio;*YEPD5?6^oEUqC{1QXQ8S4$57K)_WFf_AC#U<}KVL2)FMdHJF6s
z)0^9H(iBnnzX2`(i(Y%Mo?D9y{)kPt$ARA?Lhz8i{b8;*r^2093062W>2n-sf-?a}
z57GYESAzTfMiasnZ_95e&s1TBR8CBQ{maAxw4iK_$m?`RG>H@DdF{x(#%?l#uH@F>
zwFWwNfVE9kKfRVx_FByH;7{hG_YzUuGsed`drd^t3Thh3)S&vIh$`-ffgNRrCSL2q
z^6R}z5A$Aa2eWHfoII~2ja3Fly*3S#L;{scGLsCho_+Zin4brjMn6}7hPRsLIXqGY
z!Rv52#~tDpiS+_I#{r^?O9#Rx=On?&LXXL$f;TL3VBbfML$YH@c&D{GKcuy+y;SUg
zczWfRq4+o2z1G|3lOV`k*P7<#~C61I{^4~dFTDE{F9er1?br2sve-PR(U(M
z7`OLX+OH9kA|s(Rl^2r=;s-^@>FX)8X=!f()u-5Y^xvcn12*3u>8+eqOlNo;5{5!6(9$rE`EPE4Ntih1+AZwU^;F>mj+A>fKg%YW8~{eb$UD
zDn=iH{B~cr&|N*%r2Of8bMcYniBG`=%5l0qlPx#}q5K?eK;Z$!=9cE#T=SH$l*}^O
zlt-&vk6wWs`&UbHrD`^~*X`1cKFiEAF;&evD5o)r$v5xodmP^IcuzX71?{bI99Lj=
zfzZHbD=V9;Et4e``e>mEHweG5d_lI%qIi);PhSs0HgmkZ_$c`f!>OrtcdmOqEz1^0
z-4qIa=)d*m<=yddFfOWQ2UvyQVY;B~8z^57&)XyaweyVBqK5n=(?l-L8344sEuqnS
zX4;I-zo>E8399H%($WY}FXhM<!2^!#3y(M)=
z`7jW5UkZIhU~)5V*&sn&zFk>S0+`WdicG_k^MKZOmJ*c$*=4-P#Q7@)HJ;n6B4=|Q
z6Yv|pd7pT0XICzNXkh3>CwHC4@Gv+qq=I}n(%AL1uMsFNNVl2HsW-;b3iplu;j2DB
z-dfl9xEYhP!5}Zld4>iAV?R^cF1*{~Qd7IfdFR7Re$H_k(t55p2T|-1>h>OvAY->w
zDI}?MU9s_#E+cBtrWb}_+4qIwN~Dgn1@_G18*q7+p*G#lIE-Iey#H%)=sU_;3kl6H
z#Z_at)$v{9qaN^Gm)*G{1LLMN*}JF~OASs1u~hR9YnHTB6$5fz5@V!*HJtxNJ6iQE
zEjjN6i*!L1dLM3z1jX(9T}U}+UfB0`
z43*m;2)tIAk0Q@T_wL1Z+NC_gmla&abSklDK+~$&Unf*dC<3*3*mt<1I7LQYnVq%w
zYC7Lt!2TpVO&l!noSEqOHT4mA!^J$iEuKzu*ljegQ?(L|SIXAiJ^P7A-y-_KsAXj(
z5ZnrXvzRsf<(>~+{*+i{5^lUO0Rn|Jlk{V4@`kWxA<&^?oTy=M(JVrCxi)d?
zBE2%c`&Yt+iCpQ
zkK5$U_!^_F9`3$6Ix=rfK;8C$IGQPVIjWb+xX|?5qfmFWTWQrgxW5&<`da@mJbbP=G0+gLLO=s%byjlAP+NM<8>lUQD_VzPSiP;T!fWO)!DxCbmhnI^9b~%&>dW{R+fTe+TYr@?j8E+=
zaa*$`T*R(@jh8&Q)b1ztRTLXicxZh}?>e;3NH&RaJRrDu<;Q2>Z?M#?}`}g{x
zPCCB3W&W`Ji$&tdyQ%9vXTIk5$>{92C$@jlf}7Y@<5&YBA?mLp(;mM1#AU*A-#K7a
zArj+f88xwWKt32&``mmRU>b4VOQDi;DW8i=jKMpa4N9sxW|ZJh+Xd$RKCBcozJwuKQ2AIDbZf>in1N?7}vbt~-LOX5JO6!M?Q}ofBXirhhdc<-`m^!H)A((<#PD#z_it*R$#UBh%!c|T?cc|P%buG
zQQ?eg7d_>$>XE0KD=kaqvIU4hOfmS4jUm|aqmn10-qzHRbTv+w1o12fboCB3cc=ri
zN-eT)$qcDst87|@fmjYwV)(ZMxLv~p_CKYM{X&(
z_1ACqmiatxPkbG1H|tfD8o|&|&Zk5zMsYyF#YjJd7ddzP?f~53TC7oyg4-@WacaPFIW;-#3T=Dhw6&~!3(D54EMv^=Bd8+Ste6N24x-&w@+x>#g-{KQUp1i8}
zN>$kYV`^jIJ!n$bUXt{s985fJ+iM!YB;u#7cWu9aob=kBpa9bb_kL8A=tl8|cO5wR
zv3#4O*gLJ5;Uk|1iLGeO^iT=cO{3x2p4rdGQme(_PxwY|aO!VTOz&v!MP8n4)~90Lj}|Dt)Mho1O9eGMyRrx^`IuIPb8^NVp5yKHZiR*#QZ=?qh|a
ztw)z5$pq%{yiF!u4+bCbJ~F5KRC5@SxtR|f#OCeNe!7jj)#rW-z&^BkRNH8DA^M`9
zlf1!~pz@XqI7b~FKqax3bBxWYn@4;wW8lh;#A&ya9`GOy{81R!0Hk-z$`qQjlz)3w
zh>9L9;!VEZFf8qu8|r%6$ZuMq+zCX!D^qQ627cvKZ>IQszG9=vi`w(K-Ol@?v#?se
zM)(?Fx`&R|;afhUyf`*0Ck_b(Fdbyiw72x(fI63_wB9bT^(v4?cSvo5JE5t}5d=;&d4l~i&r4n>J
z)x1=vmCo&xl>G?s+#Y|zYkVT__(bZP@0RVjmm~q;5>B0vVB7dft7fn#Hm}KePE=Fp
z5WiF|D0hT&$lfjsab;GUzaD+P~hi=_u@LIUpHo@fuAmwP9AdVG(|=BrJc
zFqi0;rj0UC>D8uP+0<_**^p&oj!Dot0?#ZjnuM&L?lYIH-mZKE%-%iBsH;C)c5-wX
zugBvyl&CVhSZBZNWWP&w-Rfbj7|@wxCgLPp_|&d(QYBq>UPkq~FQW#hs&f7q85?V?
z^bsuIKqX9ye3%kLfI{DY_xu)lMsxi{!V5gf+n3`QD=Ig@`{qm+?Vd@=Y3XSEgSYYa
zsT8079JQ5ZXZhPlyel@H0<*In?b8Q#`%kPD6kfkm__R(%(Vh?&XI-;zlMi>%y#%5B
z{2Gtn85hHSNjx=+0(%GhYUN=l;>S_%8r+Vh)=2>w_4@ic-9fQ>$%d0BsF=Zwuo
zl{7`h1I8yzDnYB@<=3n=N4NAaZ~d=)$|`*Gww@PJy?460yQ89Fpo;NFrv^6l=Ei^*&Nl&!TFF?8ubF#JLZzUV+S2Gs{>IYxOcl>b^114zF
z({JGJL~8*hpH;NoU2(jeXe=5#ctX2lA?j&l*8&6KyM-h@$n2)xzgh8l0Sg
za%R-|+;fSGp61^3IB)mv#@=8J1JhX&G6h|3=42jBu=GV2Lwq80)^u!|lN
z9O$1YC}=)X4s9WGGm~Ry-}>}nL$KbFg_+q6FjrzvrH-_zt(pnI|0`r>3p+_(LmSNhb^zZ
z^^w${^;cBURv9;+w$eM-8~y;_12hb@XI7Mw)`T1n?)?fCiX>=gy%x+SD|8w(gP!OP
zwmg4S4T=p6NsV|<=aH(}prVd!<6<9nzSm6&+|JL}2I(KnI&4Sf*w|4eyt$p-y+6dn
z#XCD^-y04-)1bNd_yi1hG>vjAyj2Lc;x%J0uHe*+#4Lp?4zo_tZr{DZZ?*+=Tn=N|
zGfwQf3`El=@4p69>^)`yrF^95A6K!Z{ijuTiW$C#Wz}
z&))k5m>p^w*q60dj*`w-xyPtbNg|{>Xr6hk@v&h$%Y4!Q01ldymbG!24!;*je(pV*
zP3diX6By2n;n?1nbCf;PHUwfOg%l@SWKX2GmtDx`eYoZ?`A|wPM5cDFKE15^T227O
zyWh6Va&_fG>&I@wt=EizOW3;~Kf(nw+wK%xJT#aJq8oiTZ#wFn-52;aqUUR5
zNhZckF2GL($gf>uuM-~Q{5(9oc8#2s@velJn6$K*{Cy!|86hDtX&@xZC@n1wui(f$
zl-D-W*U`~aRZ)=>;(?cq*x1;3Pyzal8w9(jdlSnO2C#Cok&|B|CnqGlyo7+sc#ntYo+uABEjc;a>FGYa%m=z!TUyt)_7_KH
zyU&&m56|i+$G1-pe=M)9jnDS?x3+?^-uKm&CAqmpMUC}Ejd?j~E%ndB9FP`BHi0{;
zylg~Z;@z9|?V~ez1!!$0TH~U2w&1ksDnL>qI?0*w@Js
z+r*5Fv2H7%Fw{y+cPTKCmm^JY<^!=j+a?f^cG-J)D;p?Wf$cY
zXJX^!W#T6y$0Ydm^=R>Md;8+Y;p_^SouB{lzCJsvD513UMOhj_OKRF#Lac+rDapzm
zTB5-oFvqyPEj;L%Fu;uQy=U$R6^EL+hNEl3&>oq9n33uV8bIrso>*Er`aOTKi@pBp
zLz1m_(ewI&p5E1w!J!x3-zLDs99#?te-_sXQJ1#2Hwf^F&`-BckB(M5N%6-9%BD_GW?_cZ5mNTIOxC$RTir=<^Ho)p
zFOTS)23)y!>tU!iFo~aSN(p}PBq_dU6O(j2p-|V=4~N3*ML^PrvJXu!TR>~FxU0Dx
z-VHjOo`sETTR4>ahfV!Q7YKn}ti>1IZwA5VfuXg!_Ntzq{+1U*J%bak-fwO!Es)au
zM!j}<6ZIDkxVE`@e9m-2Ps>HZ=LFcfuk*4ClTnf494ueF+eAa313l9tD<3LLYZ~57
zF0?&+)>${y6IB?X@W9s0^szpxkT|#{00@6yP)^UF?;Ok@qpl2fuD_WcfAT)sB=Ko+
zfaz1vkf33ruH>p~Wo-I5G9~5b)CZjFJn6<}Wocg4Oi?0Q;0aP8P+Qwr)-Oig(d)rO
zM;A3?Q!DQ{J=du6v8`D&3UK~;{E&qYM?lF%&&5I7Ptn0KA)zLL&e%efR#=jPXaWo!
zWAXA!B)IuF1V7+agHhuYWxpjz$YY_Yqhn)Z;R0-po&8<>+{7*Jic3Edh8yR2d8OD9
zvwagMtK@ta7mJ|x<>#%2o+sH0nFCYvmklSRMEK|=zjv2DPUAwjvs8vxxBCK6;O=n|MF_*Xm4Pz&Hwh{mGp@ID0;
zdJL4tMtU>}OnQ8JbP19ogo!&nMh781F+JK6K7yW#zC_}NMCB3z6a301LG;K#2dncpZ-bPf>>6r%pC6
zTg>{`D9jTk|1r-m@?|m9>07U$!w|oYdmTW_hZEsQFb;y~7qCM+ELt~0X
zi{-~o@CW`97ztdH^=z-$-}ABf5$Vb&BsjD<*qaiG5D_ILoFV~+IN6BePi~RD;_c+G
zB9_JB5)v2~VgnzKp321^wPIlbs?JSTR;f)^i?oBx2sL)balEt
z0Ldv}-pl_yOJ3}FuQjAP_nncxlLEcnkZt!jyXw>9Ja)$9FX@W%@RiuK=Z4OgnqHn9XYGN3VbRwCZa;_d
zWaA@aFn+2Y9kJ?wPYz{zLYof17xwsH2pcYg!$a3s8@g8)RVA>okCk=7#=_BSMMh!9
zZ)@A9AJlu!ou(hBYQu}Ft>uiARSba)K#7VfsBELVDEz>`xIwQ7HST$s?2hDfu^okr
z4x_I+cz_(b$}Kdyj!E28@tFp2>NZ|tj9aOW+)yH
zuPLcMeM&jc%ll)G7a%hTk;R>BJ!tFeC%5iKJ>Pof{&TjaPD({h=+h^!_uin1(Dy_n
z%%RKGK&)-{#>oJ7eD1`(n3uBEckbv{&Ef9=w|I0MRMew_nomzRjpFy@A>9?6X%lpWcX8z`?zyRj6FAA>_%66U-+&-OH$
zsP1S9JWRL~>p-oQxw7@AI`MJL<8PioCZ5^7@@t&+US;Js!z70>));@8&Q5{q(TVmH
zt(22yaDiR%b}^gD_+=3&LF_pW@TbslprRISl4cd99b}BVf>+AQO8TulWNKzp;oj&R
z$|O`*V?6HT;_3N^>N?)o0R5~~^j&xtl2uC-3&Lg94_3CWnSS9rd?>rB8`ZP{n!vm9
z_8B}x{i8>}#d-be>lN$qB0GkS`RHuBn{oUW%lpy7-wV6Jq_y#yQH_n;*X~^1VsqwJgo|pRAdXIFfl;eYy^aH-0HP
zH5Y&Jl@C+RxS`^0DEN7q|K+e?dmPRZ|AG7#(8K0Tc=4iF$tH8Ubkp=@gM}=~lWsB_sr-8$?!Kx`jSa9{wdr!PDTXIgilzRJ
zob9y3ueqZ!j-Onq{KM-D*P8dB@^sgj-o)}I5G(Hr>m4FuvDBE9AA-pkq4ce>gwfAD
zkVitI=_H`}6F%*;bQ_WW)LAh-b?tPmiIvI3WVi=7=aMLhFi!(w7emrCy<
zlc|!CVPZ0(&z&vy{s@efWDc%1f=q;7F-_>{d3d;48#3kJy=AgHJf!=uJ}>~r)_slN
z-PY~@YiVh*8?WoWX)tMd{pw(j{tF|0-Oc$8P-MHznc*9|ZIYZ&=PZ52m}B3HF&#Ej
zWE46k2^$-e1Pi2dOV*gkW4cV3iDQ^!W0;qV#?OrnNQ=wHb{7>6%}}KY4^?KUlA*~h
zkgKQn%p{@-RbgI`fg!Om`Q=TgG$W0F8h+O1M|bzF6-cS{rsJPU7s<(@M3I!ZJL2c3
z@BNbW5UQ5IU_nz!;L8*fR#DKmAUT9F{nu&DvQ_(n%?(hf`X*dRVoY;X6PRXZcGsr3
z3-)5`%M6}KzShk68fsh?o&1TWs#UbHiU$;4Utg~X%47Y*c|ysWua731$EK9w6wsEb
zX{PimA*cX2=7CIO;~WG$ymdFdW$OxQsC^k$`_k-ECI18!tLGkc*BJT=3EkZ_XqJ#e
z(`o6AtAD=qzW9|pzRqsYm?Ap}juvf-M;zs~d>u8+PzxTP<-eCD?ZqD7@x?ys+(!z%
z1Or4`#!K+~PYh7W{x1u!#v$f3G<0`qZq6*r!b1O)1-uQ*e1y0Bh8e4&!FTqOJOdF&
z7XKvX_|B-&v7@6H!xFf^oi%^CwT|1dbJN%{BtiQ~SDjDA%uY_)-(M~*(=};q`Wg~B~aVJlY@Rgbx1mf12)@)j^u;}%_3(!0;KXxe~Q1SL*
zk&(XTlU&Tk0a%$8Tw6ZrW=`;C5}dR9-(K9v
z#!3gW7kKl6wGI;k*iN$xHyOSJwRhO~9f~)`nIDsDMf(+j_Rv^gp?>($;$u5aIiIny
zpy>NYd8^5yayY_A`Ba9qpl(1ExrxeTX=(f9YHVZU`Nm<=Of;8h@waq++q>7-)2z)^NxrnIl1q9
zHRk54fy@yuZ3tS{3kh7+(N{m**4obGfGDn>1ixr^_oL7K=0(SfqN1xN*+Q*<%-dS0
z1kJI$UIQI-6KG-OiG$-()9b)Uta#Jdb1c&
z27CagHz6TGXZavTbabZRr+G0NBmWC&J-Xd)@T_BxaPG6RU3`ga@|S=AqS+6{CcWEh
z*;&MuIdw^uIYA5`=EN=i#C&NEz0SPU{Jd12>Duwy;PgbfJ(FMX=ou=f_oP*fZvi57
z?x=5?JHDCur{KsH^z2zLt+ep3^}lOu7`8Jc1|_7VD8r!JJ-Cd()V(D0TfLfB&HKuL
z01N}0=!Z=8oPZ$TAR5^GeB)RwaV$~o+bK;tKl)-d{iB8#l&Qv#KjMimAu0iQgoLcE
zX^BthlHKaJ^4{N&bhEM%o8%yivMZ9VYoH&4UJecp!T~Rm?9AVO=l*a%c^$tkkB(+<
zjv<3Rcb<)6KtBC9zqR#+E*hH2!YY=2xO&H@1~KOz2@#}@UD(rVBjEWdP4dqlI6Ne#
zt{)mJWu;2Oj}K|dsnifCVW;|(7!S
zH1q>jW6GFW93Q+hB}w`?A}wZSW);xh;YcMV!I~xD27yJag00MJ)_
zuCTB)hS5y2A8$!A1=vfmW)J#0NcrbGwjTc&+5^j5A9tIi!i$PB=oyk+gGHtA2b3w_
zj`ohaE9_F?DfWZVYmVoHQ{BH}+d{S2XX2t?=Hg+pU)rChss@P~m@VXisBV(EF^!~+
z^|7&qD~71983UAAF|m_E9884-?gU~wP|s7#{t4DSIx#uLGWyHZCe{$ME-Y+v>Mea;
z5~GjB5-8B$xBsAcTZ21J@p~#!V5AdYH|l1F#!Nu8ig~Y&DiK&1XG;1Zd6>PBja;>@zezK>s-qoa=bz7uuKKwF?*i
z8u@Ng;o@R^EI9ZDquIxQMgML;0HdtQ@Mk)azNC!-0Rbh#pR1L(CkSdAJW7nS6*woZ
z%fQIU$L@r~sZW019b_FH=-Sl6#$qiBQu)eau-~09Vio{*j~$v>a}H3o{fwAk$S^QX
zNz&YZY>7tufwU_z^!F6_IJ9e4isD?+Whq_VEjPK;)m63F`u_Sc6-V`v^AmqVAUZ4X
zZfVKd+(K6rcjtl>9j#R${98%!`ya#y>66^SMS#_75$;8}H+Q%!0yciguINe_F!X7>r
zl|pLYVMtmiNyjCbs&!^)fi*$h^ZTD4_+wX`5i6}d2CEnjEgk3mR1xu_7adiV9`pOIM7Kt7GlN5
z&ZNksjS;sJ7stlNZfIseHpt948p_f6xCxC7C**F%FFAO!AnX2
zCA=xx+@ldDGCn@uVJ4(r4a;wHN$icN`aRVkxRUAvWLEg=md{P&PJf5d3?_Yj>>Lst
zSn2t^2%FP1U5CUp9aNuw+8<+zU&Ksj@?OMQT*MiZ87Um8EPVRjsE)M@GXVATuK8Wb
z8U#JVK-YmN-mG;~<5Hu!hsW*Eej3;GVb7%8v1FOajgwQQIpE85JWN~4U@=4!4JPSVa>o3!7F_hbmB}DyCrGl|9uy1$3F1
zsJ&M3O6dNToQOY;HrJ;f5DUAS91y07PIVKJl!JLg$3Y40b-2pgRFl$uH&Scotuy~Z
zmofr(mg2C)*#bNJhz|Vz&EpUi<-v`9{`=LJ@IlEyU7Fwizly_nZQ@>@uQj-Xo*o^Y
zb~~X!sJNgP!e8|lNzHGz6PgvKx*CW!tiBg
zE@$A`)t3+YddDB$`Yo9$2V^$kk+DbQ&XZbN!H(CF+3qB&O@@B7&og$<<9clhdG5)t+P
zJanXTYuF-H$6z2{2K^Nu!}vL^ORvWP&LHT$@E#KVj=1njKfl3ilAUcN6+9N$`t=j%
zbWhIAk}ZKn3=64HiT29B|Ig~&PtPIZY*p||kLvU15DuT97cabDyvAO|3|bDDd1LBe
z2M^dU)x3ksAK*I^7re*yG!}DWf1z}#>hWXhqSE!@d#JQvKJn9U2piye_*i;i)~8f)
zraKr+^SBJf8pY%!LbuuG8BV&VVl|EA4yp~nuGd>2{M+_FdKr2B1!kDVv8HUfj#Dr45
zMFlg;N6L%o%P^QUs%)j!1+SGu`qQt%=3a|
z2)dWowCYID0!ko8&NBL=APtN_9i~T_A!qBdE1RH3zyMu4W@V+a=wJaY8%99zjjp=Mu#is-`H
z+w-0Cx5DFM{_53?hr7G`
zEBANr+_nhA2KxH?ke8Q30R8+gBr;^BPm!X*mUuzpEj6+#$`(<0>lrqw(duYfao}d4
z-_*>L!QV;_FKg8kFRMy*pF_@f1k)qm*9TJ(6O&jm9GjMwcA}c{6Kd_0JJt`~xG1Ev
zdG#>pujeQ(EMUP-PXdFAbaexWMpCH3>1kT5A_tdcFbfM}8)(kY&&y5CS6W3=ZYn-bc_eRpg@(T-ql|dNgoML!*4Ej
zj;Pz``s#^jRTajpV_OZ`CW{|&ba5g8V`XLKfalcWtNg+Fxh3IlRZjfpmuf+GA2DS4
zALlV$ATBZ)iOvDJfFD0}o+)CBK!GYlP}9_>yTzrX#qJoOpc$lJGa^NR*WYU++;NQjr>EbAV~VGw^?ct<>$M{6tONmi
zu08)4@c*Uw92j{fkL*wHOzuOUwDvKfr)Xrv&trs@v)0*HC->*ijSZJ@xJlJ#6O@~s
zh{&0LJ1?N^wNC2tqWA4LJ(6$N@Bh6xEWkNtVVN8odm~GQ8xR->q;XPD!U!cKGNz`E
zLfMQ6b4}s-x@HNY9K8~hx*rNC!GP;3x_N5l-Zii{Ffc&2GK|<;R<&eE3;!KcnPKr~
z5ghpR!ZeO2rvFWIAuGAOyY0Z$4wO|`E^lEbq`r3OifKq
znw$92Wf7I_?VoBqUZe#%UO60R?1egn3_T$W&W-Ka5+CM
z!(wl5iO|;bn4aM~3?D5{9_NkFrSb+Y~>F6Lw)lG8k0bjwcY%RsDHu
zfS-6Kb&`gg?*@uAkY>JNYxh*Ev3FZoP{x%9A0NLxcK#~}Eo1m;uWX@-4ea}r57*a8
zLC>nFI&nmjrA0@(Xvh=lintKY+3E
zlC{k*2F$??l1dVi-ud%2+7xRU`%ZhqnRcz2>MI}zztd3u%PZRxWa=8k%z}L3sKzbe
z*3EVn&GK4+bLJ!9%y^GgQefHh-Sac@J>%dylkmB54b|t<6Dp&0$-J&Jpt_oMMM$_G
zDoU=J{e^xs1(ks1gNYi}h75)B1vdr49#AhkEmbKjC@7Y?Q(Zv4{Q5O2@338tPZg8t
z&7tm=+gISfo73B?2~Lh?hv8yjN2L?o!bo+(LeIWY7GPu7J$nWe6gUoQIo5-9I%XH<
ze(KK6u(Hm~uU7utGrW4E^X!Eb;AJO8M`xiXz`{U3!(eA(Vi(a>(^1yZ)lpOwQiS?$
zuowO?V(I^~R9!=JGhM^??_J)%QsIJvRCcIXhfl%GMMU)Pc=LdWi;Iqyj)57f<2~Wx
zl9uMACBh;i#KlFsySqC$prj{(%7a+g$WRJ-adCEabA$EpRqX~5if|byi3p(}m5+jg
z2<0J2p{KjY#0|58*aC|%z_`GHQ(?i|S=_qE
zBY0?JuB{F=HPnKEriA$Tkf={kh??8h@_Pem7Zsb9f&C#wy+OLc#s)~|$6G5N!^S?H-*Sa^9^S$L&nc_hR|xuC^+ZV?trB5XVi7HMu80#0ry7UzC=E0LCd
zAtkNAEvRnf3~hi0J)J4(!A^E&MsL)F1u0vc^S-YZT2@;6cqFf4OJB9^2BdGLa&YRv3bF#WFth3a3KT)1sz|mv>@?G
z9B<6h&`6tnvh{Y1nz&|=d2Zi?1M{q}vIJW$9q;
z*Y?)M8O|9JKOPYlF##psJ<>S|9(35axLBA-yVK+2tABf&vZKSITU)nJw*DS>R#tA_
zPHZfJ{`rlWjq8b_q1(IOg>SB@g}Un1nbl2}%&5CT_B0b!pd$3OjKB0-B{Q>@?5OlT
zaQ1z3_UAA)J|njh&f%A`NZ`z0+Bn)eHa+!sZtG`zY<*Ic4WFnm7ZD~i7Z)=<0xA*I
zjD-mZiHcyHF6J!ahZMgm{8b#|T$5GfU5i$mof(i8IPK^2FHm
zPr0m23_m8+r-deisJsuk6%{q(GigyDQX}lOxC#F$X)CCzrKUwi#-{nY0CjNzadl{e
zMp-_OC_nf2*47RVL7u_>QPH07%{5*qS_6-_W=cvr;yie_6XIyLVnQ_3sDm??ml%Kg
z7Ut)XFfhpO@5u-W$tbCbaj1x>@QA6XNJ*)vD3S2c4wmi_?~y!>VIB9#WQ%N2m`V*>
z#1rdAQ;K$QWk)*8kCB49x#aCyW|Lvcwn{x=974r_?ZN)_c5rgrBfRNlHoR
z2f3IX_XuaysK!K#k+7V5#55?XM1^%y89&@`h91-lR=;qM=x+>Lg#D#Pm&ETxT||d1
z!4QqmMh7#7rC(s6Pi!@4$kBXoacJa|5^wUKIeQHa;d*lz)k0i0692gV!UlBFt
z9yt~$Hgs|S%~ZC_PwxNMJUl^bC-8;I^&{7`vjVWK;#?X&YQ5DoI#8jyqIW%a}xY?(vn9s
zG-Dc4nEm%k^$LJ-OeB3E5-OjjxP`fiL}3_!3H|EOA*11msH!R<(;cAc+!+!wBEEfp
zCA)ye!79s-VWl%n&UQt^Zc}5(J~0T?{$LM7`=dh6x1#WBQ*Q3&tSvNfTp7`TnZA*&
zpzstlH-{)FWZSM;g~%{vlKhMj)x2U_CxMz=@1jlwWN`EWMG50t6d}gv=xOL0_~8Q|
zYkI$6t6m^2txElG){6^5P*am%T+BDA!kd_gS$b}Psc?O5XQxA9cdc1kTYB;C25`D;
z811!b841KiRcOhNlNV-6-Q6K_&gum-b!2>Le-AE}VwSr2d1;C)AETU5Viy|NhcmMC
zi&*(s@f%$mAtpT@`EqR>i0_wGUd%rk$Xm)CA+s(05&&|!{aY}Kg;Ah$qQZ?8#s=CD0`P6F6?bXjwlK_kB
zyX))ryt$Ur_+%Cqdpp_SwsbW5`2f*OdisWGt03^9;q~e0sN1(1Jv|17^b-`O77mW1
zPzlKv3=Gpf5^F+$?fF^eeDwVIPP66-(U`5TmX;pMJLht(zdOki{2S1zQJEhS@>y7)
z|CwH5Ad9!*2n%w>NUU^h?V$mk*^lAAKF}BA*N2cUN6+AJ=kct9`|LR+NH{;b_lAg-MuJkzA+d0mX@mWAfX41jBw};a=3?OC#NC63aUYzBcjIY%y>TrcBpLM
zhqJ#{SK_oNa3ZwTH2s}Rv0^~c>+5Bw{I^kR)#SW&(;uuIQPp=ApxTk
za8g+F6Ju(|lnaF{qFmNAbiHFo*cm%`wLQ*j>9@p^5D1C;9i|Nsgm8zg_wDB#S{S9a
zy!v!w%*--k8g8Yfp^SC#W~P*S_3xGnhLgU2inV<3_3-fDq*?_jQ`0|#QUrj2^ryC!
zLzfI5`yTIH7T3#Peplyvdyg3r%1<1or=?B6qYjJeA7(?SOz3BXdZa(9-N+m2o<;P%
zRUSaTyK8*q3`PsHlIjcC(K@#x4BsvBaS1*B^c8-0r*lO{zEECf>lOW%!c8cgRY7={qUi7yy3@?sV!KnFN@a0%n-%#wP|EQx_Mv
z7G<(U3+yJ@n*Q`Ofu_ZSJ87(pbTTp%_#!!$Z-j=-0-jANnP>{O_HTBX&p=41^42tc
z9OUO&IFDOfYgGHnOcYl<(r%b(=X0-=y#~)>%P*s-$CL$trZ28AiUBlGA+^!Q~M+zfVkD5^;as6)r%BIU0wazF_w(ZuAN;XJpBNE@KUS@
z3;6v#ASLw>5co$*dVsP!zMtc}(}2yYb(-sY-#|7*XAYQh?!#Du4*eobWGtp#`fYG=
zaTU{j14K1{`Px@;bDIKFJno&HJuEC4T3VbeQ}U7$S+OA^Sz2FTT-*a}Twfl5eS-b<
zZ3&-juNijlikb79E;-7}Fy!VrV~D;`2r*{@!Wislo7x&pt4&+XF}X*a%jA!u8EArV
zL-<+!Gbj2dNc|O*6<9w{P@9?U?v_k28k$LFmT`Qv{V58EE?PU3lt;F>;^@z8Bp*N4
z#6H4(qweLEGMghNZAJ%v5Rv)P(`S8)8b~-gs_H&EvN}q|c4B3H`V8pROmH57
zBY6t*n6Q8x1WOztA=StPPi!2VTiM(hSy@^0a0M1VK#s?RXWB%dE6Wq{m48VVoA%+J
zz)ASz!
zs7I2u`lyqWc-hTd_0yihe}ezfx1BZ^d5^Fp=auY4W@hP5@Tut8W^78U3O!B$tA_n$DM*y8sL(Uv$XlSVa_;jGm
zsbjiq_t+vUtEF`>`f#9whZV3^mnFxrO-$6^KdJxu$bZc{i+wcgiXMfs@c5vG#)Hk#FUA#MJSyt9*Xrpc{?M^`AhUTHi+sEX
zAz_O^-z0ZGUdw(PXmV!2llw`o6gD}X-Qh9#JcTzkpio7P_e}JZAYbrM4jftf{rJHV
zSeDfMLy3^^TIqy6JNu-&O`XScxi~=nWD*2tW%+@MSlrW7d<(VyKs>yRw!o8vxa_r!G$8yWmf~cHiIL0=Yfb9$%}9G9ndpeN*Wi|FURzOWyP#-J;y(wzmGD
zAi(DA%b%0eMmp3|Su=u&`0P(?!i$wVCiclBMDG4CO+{d`$>a|%qqYlMv#tp5D<1)a
zY}vMwG4|E$_E?H6EpbzTGFh89MfIc#B6_7@jF
zFLbyT7b~tAsUMd^&C}v-e?&j`&Q2cC{VD)4lA5w!hwMa4;tk8*ljj6-_97xE@O1aq
zi~*TE;J~`GBXUnt&zqfhWGAd^hR{^JFvGulibPO*?jS4v^m!Y
zvsX|MTWfZ63*P#J@lo(ilN8{Cs;4)m9HujSXNy-97`q!@~HOwAT;lvSe!Z{EClDvv1F6P%kH*X>@cRUEbd
z>9s0TF(cy(O-)`#Mo|1LO9Lk+a$pqW*8A~J^5rnCc&!jxa#kCw{sKb9a1{~{p?+v*WraXc&!_4NtAIKJQmKW@g@j3}c@c+>8cur^E;d
zzPFF{YVBKC=!)y=T3A@L2-V_&q^G@-R#v)78p2p(WB3+hV>@GE&kQ&@%_~~+yu|g!
zz*w!Gs6hTJ9JKfBetzeo#;%d>(KFXIq)!ehGiIp#XX}7|>~~c=_
z(h#8`dHNd=piKEi+@7$c{cg0BjtgE_XzT)AmJN`+eo2S9`iC2WZw0L}fr^l-^;#<8
z0(my_3Rwv-92_jCx>XyCq`b=kITQan9AdP!srBcnO(Gd~bk(*d4!jIChUpfous^eE
zg0)`!Id@CuIxfPM8%N&$&&*#l>To0`ZZdNBq%t`JzJRvV)3X`hS<5U4r$Nl5CUCB6f%&A)uGE~mn$AjQ1g4Ph9dMjP$ipq!8K4t&^G2za!fw(ev(b%
zskAIO)F>lEN%=|UHUty%mZ<*+f@W=*Ge`7iK=|j+N@YUsR&p|ZIE~t}uC7A=vXj&2
zzPFZ-w-fqkpMuUCzN$BW@0kMZ?K^1=7Cx)?_V$>lp_WxJjc~4gw0I3LvCo+7OqL2T
zic|%WdpT`lVwl$A6!k4skAHmk%IoBd3o)r2}wwFf>|edMd%AM1fDuylf|b9bnDcl!a{e%1t6VebJ=>I)DB8up_atqj9l82^=YL
zN3*1)sYG+4nb~o1#U?!i@|EV#aXBKQAOdCiqeos|gXPc$@G)-rHGq*355H8kRP;Iq
zrvAk^Jpbv_>o*634Cv@gE}vg{Ogn;1sCufwzr;U+9W@djzv6fzDEze@8+u(H$*GhI
zX={VS!>lHU+XU5v;#--@O0MapbU9eySHA9>`V8!Rq9z)!Et2n%P_0
ziy?9{hZ^4N;zAY~so>%QdS8#u!g!hlV9r@ecsosgusI(GH|$@|R$6~d5On@bfn~T6
zv+>T(&d20Sg;Z&JyEF@5U)?jLXX4`3<>e>P($(o#-FP)M*G<@8*Z9`fZ{HBvzqO_d
zL<)3?k13xAf8hdZ8DH{KF}>#ZOteQxc%!0-y4V)bg60eeV=HzYfP)z5{C?6FM$5B<
z>6q^9F+Ol6sco}vBS~aPM1-*mhsqo{#QQdYaaK;JZD;87oBS>4s-1K*
zNK*0(m$JvX|N1(<6+Zs@`udn!bR{^$uXR5^AJG(vWmZ-m`>3pJuKY5RrlH}F0H(<@
zWwkO;#x`GgFNvBguT=f`aR*+@d*bTqAdgEGs5+b}{ahh*+hCD3!TGAzR(T6XLV3
z1~2oz^6jYoOExyYOE_0&C;Ss-u_z@One4rW1JHW;l7?%kz9A#yxtZKxpRjNsW#83R
zY4aOPRowjh#AHA+z8J3Zl`R8@gJW>%c-)iy^z=8uw!}u;8{?nKRi8qEb%@%V&qGPA
z6ce`}zeT|Nf~7non*4SOfRdt0}%Qn(mG8~jv&;_5l125uSV
z6UwQb9DrL=GO~NEXSd6Se0C8sGBf3j
zAGY@Q3F$e<@E9hUmE#i^Ie7pN)I2SP26x)p+l_|;3Hz=lo1bz%?CM9isOao`zS5M^hn#?I
zY-}ZEC5WG5f?z2#_2AfYCc
zX)Etw;0`M-YoPj>`qXDmRwX549EBiO0u$L78fMlXKh`W5pm4_-+QcKcH+Ql}Ehsay
zAkEa&V1=BV0=k@$lamjdm+mA~=GE5JDA;>?!c9%hBTY@yOs|5Zf`X)AiPSLim_t)j
zM^RP9969%apII0fN1*4#GAqjrq9UQV^ofz|2wL(AKdo3*l?94JXGCHpW*Q@=Bn+rA
zK+DURn1`lj4b9D_X1G5FT3Ju02P(gwo+?OW4!nh6H_
zeT|m;%i{Dcmrw+vJqo+|HLzyxM82Jm30(e#xzBC2|7jHwOVU_t}Vw}nLk6%%3#5jKMb?)y6
z{rM+isJOM5)_g36MzzR_eziN0rhN
zxpa?xkB`l-qtz?hVsf
zI*|goSF4)Gg{nu+eK}%8Z_?M!U-Pd;mlO83S}gZ1FZZ4TZKgA8I^+1x&OXq{5fafwMn4k?!)mn00IkB^>?PK<$pjX_*oR#8V^
z_Z4)$$_p}35TQR5P7x82iO>&b;e&mB69+^GhbZ{uxcF3nii&}ejuLWJ?+&2sY-4sB
z-Zy+OId@KRDRGLBvPg-#J6X$3H&a-PyTBK_LX!C@5@#
zA|i@V>s3%pLRvyXf`W?}pOl=E7?+p^x=cbZ$%lKS#l#d14IP|30wbZesgrl8x3?cu
zH%-p&N=pFQe(?z*-a63Sz$-B#TtvjHt9wK`K`}u?77k%uT|q%aK_Nk7*khWfOhkz2
zSBOwPd4G=%Lqx%)e|UH@BBH~Ce+P?mOM?TwH8nN0%|GCyL-qBG6VubR9eqQ%ySsfe
zGw{B?@3plxSy}0&yhI>9At5s}H#ajOE}=5P$JW%yh>DDbpPKBjqphNHaeEhhAA(5S
zp@g`osEjBRl~<bMq+fC~KvsC&Dv^fvc^Zhh%tZZRhXIOzl8jZEJ05NX-R$#T~RPF`qLA?)=V4>yaqPE>$8)Jn!E030iU9xrmb@=91o8~d3G6I
zTie!G+w*;TZuzLEDmy|^94Ll{RB|5yUwtc|?uY6elG?w4w3rnn&C4C2mQ?r)%^p*U}9o=Dk3Sax}&v!xVkzqr6QuNEIy*T
z;Y(FZe@jdEA=uvi_wR0ZYjJOHYqn)>|9bQI{FWZ$mVkf&(t)955S-2qRJZi^&$P5u
zRpm#ySboeatZR<*2}$`@9TxUhQ3lA!2ywGBJ$?EV_xSMmVzp~(aA|Aq&-|aEuGZG-
z!qjLGV5}AG4SkgkcaqT5;FN$Ob4dw#6PMJw#vg~bTt=*9#$HNF^vGOX!cK}ZGIBC9
zG8!5RGBOZMD??AS1r!ut>RV@L|7h+S$o>J}fsTn!MGK01g@goTw1c40Axc(tQd~SD
z!i9;dmg|4yR1E4;8ZV`on20cL?vXJ7F~Gv05*ASs5!F@F)m75ef1~+Kf{%=xUr0zv
zoQ;)+37T8t8~d}ly|+(rxDBxy+lSj2=NQnk
z!{OHQ_is=higFLTN0_IE-E-U{lS+z6!l=>ik?xW8jUPYwwoS$?u%=Efh^!XASGY&c
zrw+d-xJSN_Y=(XT`Th|p)bSLB-DBM&_$;DA3F;!+6R0doviK}Q5@xJr{74cr7iaM)
ztTPn~ETKr$=>frpVpS9<85NXtTf~uGRDmMQPl$C;88l^)#tK$1iLfXHla%X(Ez&4M
zktZ{3k+F0UmIOVz|H&c}(c&X)s8qTHSZQ=S+5TT66gS(4JA0CA>35dQa87#qM$zk4
zXrX^*+W8#V7V1So)1Z*E>)9VL{5)_|`E4|#e5XL>F_}Bkv5^CmbxuSn165mZZzk_>
zMe0bU|D0Vs>NCX(^KgoXIJ?@XE&GQ
z!Pzxy$W1u@U3vBd_&94YU}g2&LC!AIH&O#QY_B*FktA3Df-{P?(cnZMQ2S?8G5+W5
zN=g%;jYoBarw^h8@mtr?&RgX#APa$}?vF$H`H-`_T1rwgVUUcY)vh(lG%!PveKkWc
zOqd2j49)fuor^s8`uRIiyNAjH8V6n&cOgEih>GIwKp`svy*@4@Xg&Opv+ERa(m8F&
zX~6g3?6N-UrQhBrq}Ko=Z#^MrmlASzC$BnMtbgXpN<-7RcVQ#y(<^$`sSzNWn>v-w
z{&>%A?ZMgI%91K?v|x9jp0o|cjau)@j{v&vbFJxDEUX2yCee-$&hBiAZ(NoPG}%|TO6R^2Z0gkcv&ai*vh(pB
zU^@h3XdEn3)g)(^iix=dp>4eDJ~Xj6>J9D&WMY4FBZdkZuOVlb;s>c?<%VrUeD_nx
z+5O}GB~tAieB5KIgPh%d0`+WaYRK7@f}GumGxW1GlGPy7FU3EBs?7Y~2WK~dN8we{
zPnY`5qKFgqlFru&?XeB%XU^v;)TzsS+NSxD
zK;_7n%EI2Q%YR@cMtIe7up!Rk#!!sZZt#N--7&AT_I=p)8wW`2nhZbd~tEK
z@$~;XyQ_RfndzqHyQf>`ybI+Ro1m}29+P}4JQ1Um!k}K`qeMA;_F#$Rpt5QAy>jne
zd?475nhBitgSDVPg7e<#2rM!k#X`<*9A~48_U3||-I!q)s1XWw{&RL;=+&eP
zGmW3;$)t~cgPh$E{id-Gn(sc3j05MLtd^;izP`xi??-?B=j>8#Bsxr_*ed^to5mu-;VDmv+Mzi~PlV
zzeBp1WV_3l79$XoGszOe1eqm|vOU?@q~sS7xljY60-9rDvBcQ$OGtKNC2%2Uw{;SH
zZ8h8dJ6QShIppkyHysrKR{VSZw~(`&US@%8VygyRe_}z-t`6oP8$2tBrl>-YA{u?N
z>%rOmITA0hD-N7U#XfbcUl3aUKB4ZR4w@eby_*W-9;44$Vu$0V2)zVWoH<`oYNY+~
z^!U+B@pDq;Q{O#nK-8{ZqvP0P82XI{aI1!SxD!x2x8yq}Z^;8?VUL>sl;-rvXAxt&
z^hm`m0&>LSMCWM8+3klz&hD^_c$S(7mJ4njokHbmlWa*4P_XUT3l2PzSNjAxyL}Qq
z(AHCH*{Z__)ieKdc0r3$=gYlPEj*OQztl*vE9#Ss&GMipTYl$gQbeU3MgjUcodsu$
z+oOwZC-m5V)V@w+KdQyxb_{ZRX)>?12d@B*OM9>!$l2YQ{rf|f(x%Z|>k%Dgg_Cmq
z8JY536VySjh3z>vx$~5K*^G
zf@%Nf>^gp(zfOK5zXHw~~>XV+c_gKh^KgO$9DW-mA4&CNa6lPays-f{5h!P#w!
z;p=9Ee%zb9iFxc}$uo>L(a=n>3N`4*Ijz(b7`ezb%7mHAgKtX#(R
z^^mi>=)`t4baaV#$?Hfz&bzI2VU4WcjondzC
zoAM>{9iu-+9)DD?py9ivjaYxy67feg{7>2o!XKR7+i|lWM@SZ=`@PJ{yg7hPFPOKb
z11YCNPu_#>xK?1d%ZSq|O8LEb@9)_$Cj(Egx%CBdc5BVJHOu}n~LhsSTsC(bsY>C=^t_4HvVojkrBzhO+4ZUx7A)F?oL%JaC*Zp_omo~`7<=74
zzM^Z0bW%vT1xBB2n|0%7)+^6SVh#ukX0EHhRv3IG>;^fz#PVr-da`B@&hEKz-YgB>
z1t3I?ZAakr
zE^b|m30sh}+n@CLWdjcyBJwFBF)>A~6cQ&(RTHK=swLOMwixGrjf|Kc|UoCBsEQ5sXlhjnDru^TS0
zFYD20%R2vcy?qpwqgyE;0LpxweXXVU^50R^Ah=d#jPyVEl2oE-N)omipJn)Mu?sll
z`FR!4tTmr?)cx|+D|@o{hR$i>1wszw>{cZ}&Mshc;H^8;ox!HG-O`P%n5GK7jJN71
zTrv~ypt^>fT}wbYr~?Lgeg|NavAz7i&MxE272#0bzGAY5_sF2%>;JIz7Eo1nU)(M&
zphzQ93u&Z5r6m;*q`SN0(B0i3f*_5GG$J7(Aq|p}(g-L@2?$7j>&W}v`@dt{cMP`A
z-1Gcgdfv$yc=p-PDm4ATie7BUc{y1~hImUK>#=&nXTQ6!yn8UqPNxS1X(9}6kI43j
zJ$s9O%1|PwHSNNVM-vY{yHO7*5;$hT%v5$|A&P+Ymg#uUO;ax(c)3#*FXUU6#w?n~
zPqyxJK={wIySI$T20go3NYAbu-Vs|5A@ge%)_~V}&=zXB%X;P6?J;PGd66jZ6W@wK
zJpw(u%+5U2B{vAn!zZRs-@N^D9}h1isY?ImbnfNTci=1c2mhYf;o3Ge(=pY^$fT&y
zL}F;!{d{HF<&pqy|%XA<94bYOF~+9S4}!dK@5WBtC?{e)a?5G
zv+QbaBQ3ih5}gE1y#kv*WB~4j$}U~1a;VvDz~61I#ZhxUdX6&xjVm8d`{?WJH&oCS
zTCVj^v%4W5>T!QBIOzrMa7aKIM?mlTe(`0E9_fP;w$qcQuHmKQM
z9s0HF%+=?Li?r-Me-y8E?HW(y`%Q3d6l!({pk^0Qv;Fb2vN+|6SeMvF*NQOqCi{2*
zyDlI`vCGnF;f}yrhnC%8^3Mn}`B^ufO_Zt%b>%2@@E>V)dg~
z1bEan>pM^XGUxt|lftqF*ApU{pk|kjtoq=_G6Gd>gRwxQv(24YL_}+)2wHZ5VZCnc
ztjUn1WF@`kW@dPb70ElVC~#5Jc<2>MW*}P)vMpqxW_La^vXz*NR@n_LmqZ}WP})q_
zE)nI^8g9ZhP(*yjR>WM7F9`6vch>Mj8uwOklNZrU5ER=KkA^6CfSf=O)a+U;KGi!}
z@3MW?D&S{VbA6qTX%R#29lqhO5rEX}hTf?w8CE_=lWj&bJ?nLd!H@KM{Q_EcqasGC
zfh}=C!#gaf*?n-I3R-qg)6diNf*(IYLpww6^r>6`cL(m~5z;A6%QHM{0!
zWv9ndqH^A6PoF65(Uf`#IJCu|NuE+Nf~`Pv^nP2UX1BHM^U3nIprBw(@K50ya=|h8
ze$Aia!28{K35{j~;!TNr{!y_Eb!O(k%g3dDxOjzKH$3ZZJAh0{bn
zM{0IuG1GAQZkc3hnXkVPi;gYU*vg+$py!UZ1h+U3G;eaz}9vU~T$X_gIKim`35VoFoJi{Y3PA-F+m;?Sz@U)l#PyB&Q)
zhMPE`y=0y}dA|p0cK=y+%U>qT-W2@sS;*_9!`C+m-gr>aLC$*FkcP;oYNttCyxaR&
zOCBXk9^MlAAcf-Qv0)Q{n%(-g@IOCIX3l44TW4Er^`T|g{Cx{g*KKlW*-ZgZv%5zi
z(M3p5DGIN4zHB*&Ea3Lw<>rbk>2p9@c7fO%hL%XE*)1S}mR-sg8jJ$9aNH8F^MUnG
zoL{Z^O2L1cU5UVNpVlyhpSvns{j=;=DbL$_TtxJD0jSv>UU9mkQqMs8pJkWp20;_A
z;?7fO*&Q8w0bZC7Ws@dD&F+gU%dSkooHzd0?Odc~*WCYl0`Q+GNPSDSOB;oUwCq}{
zOQzRk22i5$H9*U*Dzxl^1-b<}q-HmP^7b17?HOh2blL-O0FttyW!KK$-vLZCHWviv
zEHIGBL(49Cbrnez){p-zyIw#LWP?z>+p1&pxZRFU%;%Bkn9#C&JFfry9BJ824tk7k
z0%nPI+;ljY7`c;{R6PaJh&EFV`!bGLH|jGmLZSolG{IQ!Mo5R1_Y615-@o72k(OPq
zdzDv~T^#%-VsF5R#-h;xHM?xV9X8LN-8~rPk~fEzU3X~NEs&jK-3DEY$H};u-`;X+3u187CAXIqQ
z{dr=(v$82Z%>k|847BW)n2|usu4!O4WptDm=x1iatGUE3K1>@}+slkXT6Sa4bcSP@
zg@nF}C6zmXVU;@O45ViF@<__mjHsh8`|ffdwCuvGLc!9)hArU9ECYyxnq6{sP9pmV
z8f9&0*+m?6Q)}nd1f>^?gn@0N4-(qdbPvRTV8ZjT2nhYKs%cv6lvLQ9Ah+QTNfupp@)`T
znxc3oC-8!2#Pdx32x@jennKGi_rCnh43DYFN4$8aQV0ouKeX(+97{$<_F*4_cR`}}
zt0kmR3<>vVa3(CfQ>}jwkk@C#AuYQHInvj_GpO0EDfOrCR%fr#v3$;*d^yQ$^qlmk
zm8ik<1S|bv;IzHIo)tIK+lZ>;n~26in(<)WLgrbkyjvx->@uO@fQnT$vRzZP;k{o>
zDCGjJ%Xe=bCKf}>?$nRW@Ks;RSHSZ+)a)XpCsU1uO6Y38UvI#1%#lP|c3amZcLELC
z0H*Ul&2C_mjNwPFH;E0;Po6I?vysgo9VcAgMeSt=f2JQ$=vRRODikc8>0=zxp7l->SQTNjX}#UC|G~rHV8Gl
z{3S@sZl?=p_V?9};vJ7r8>7F
ze?YmLiIZ0n4I;u#VFBK*SRGF~ACyC2HSGu~vx`B_-lLBB5lKff*-KR^n0oke7H
zv>ipIg=LU_U5O`1zb?Ex6$hV|nx2S^9u12cDcI%ag@Rr5ODNdQy<~;|B5s`ewZAet
z)N>B~x?jhq#xI~>cV%{Ud>rZ5ecun-%b{O4?-}&#*3{)?ylJS(3=Om}x4+9P5AO`c
zUReUo8=Hq`L&NJ6t3yQ|PI8UR19#S6u`{7(&B
z{Ja7qqY}K3eq8~5Ssnoq5s}AF0Xww&va#{pMf!E6IPVht#s9Uovk1Q~kQcL8=E3|F
z(yyEQ68d%X@@~$B6#b2lg??QQGKy7t}x>DR4A`gK#FU$=b_`gNOTXNQK~XMJif{nQ@i4om~JRg_*p
zzb@`f*(dP3Dl1Zhn}1I3_U(UuU5{iiS)UX%fb{ELU~!F?=IA(UkZ{Uf`E|=?8yb^<
zSh$%jHx&){@bDrI4FUO1|Lhd>>k?31`!fXny6^)x^TX9+(68IuUol+0FxcC^HMrDC
zg8r8R=kfv#7Z(Q=Y%R@Go={VB;_%9ovfbk0VY@>@NQC;w4KO22|^QXb{J`#JogLT%!Ny
z*j>88D^6|<)wsBMD0x&%m^Mq-@RwXs+TJel|0mToyOQb#p@A-FOAIK+X-fo1r!ED&v6Bv^F83164S5077z3ql300uYZHe#+yuXvyl@>*uK>V69
z_7WS4cpxphIPzXkv}oiFprksq{Ni_wA1W+TepJ$%g1Yc{Z_&V8M-F_Cq3tk0eUt2_9cdz^0?P%3_
zIN&yAKpE1e+b&H9FSONu#R6T$gPFRYGc9+Mb@0Ep#4GitHI^^uM{*L8Xq~Q=29C?AiOV6d(
zCvAKgh^9GeOl?`!mzOg(Sw(Re@`>{7Cf}uiq-Gp}ed#Td)XK>lFUoqeWEL=PsTGpi
ztZc>HSXeK10Yr8;7u7zpEIF#raLIZ~q44y)k-J?r);+hXIJ!xefeOC$cfD>PRm>mp
z;YiBpiBau&;etIwmMmNJbMcP-pkF3v?zH#fjA%J~@369f-C`x|qY>Ht(=hko%U_*j
z=XBj7aJP@LHQB|J=&M$lvh7OTHp8S}FIlCGd+2%_YDwcH5y&MLeut~X#w@}bqyIvD
zV?HxsMv5Kxq43Y6kDDkb=tUqsQ1|@z+eD%9QU;TcVZ8x;3yYr=_7eh>1BwQIcn67s
zh&-d=p~e^zYQIvi7pBkA)s~`$#D6U0bOw%^sa=|v-(NR~3FT#Mh1fO<;QID-!OcBCxrA^AURG>NwcxXYfo)`Wq)e
zkY*U(^8!8QTvW9MgDYa9EM!0|;)@R$E;H~P7c>c!XviwIR|(j;o4Ci*GPfC1<>t(?_%ZWD?{$ejT&6MAZz#IoJ
zF<1mvS=(sFCdudz`DB9ks3}LgoF+o&gApMqY7KNCeZCumt}9;o>jx!yY(BZspOn&q
z?TLY(f2Gmd%GqY71EcLn`J3H5B}2rBW0fhoLES&@azb^Ta~r`yo#_T~)d16N+oae#
zlJbM5z4rCL%=H|S$IWz?z0KRUbZ4JB==FeC=r>Ov$vGjaMRTIuXA6tRy33YIt0%Lq
zM=Ba#1;b5%Tdfg1mF1bED%&Q;*-G#HMB|tuQ|^d2u7X(qjLIJY?uZpR<6fudt
zKb7<4&uEcVp-9*8XaDR%3X1wfVepZd$EcDMG0(c_n#!zeq#h?s6>uxAlWZm+_!uog
z#SGkf$wPRsO}EXP>?L-M{4Z%22TH~DM3g?yH-A$q^*FGAQeh`<$%^?HYiXL);MB;T
zboF0Jt_7jQ&wO{DVkI!cE#-21egrpS_s6|B@)G*-kxX*nFM)5u<32Y+LE$xYsk?wF
zL3FkiS35Q~WQ6&tJ>82>y_olIVWSW*$UK~psG^Vud=!WUeXB6ETkUs*=eEE@bo4a?
zvmJ}ziIMD)FRyM}f@ObekK-&v+mW;x?5SHTk@XXG3G$XfJsKl+DPRF8k4*t8wX{%;Yo&0nfe>8
zPxxS03D*M{j>k%m6*pP=zT?A)WW>3OsnDU2l&n
z7NB@7WWacFZ+DeacIebl@J@`-$YZa^s0JTs);4$fdC!5Ko0dd7>U!I*)!6#Od6xY8
z4;lC^PO>h3w%!JLS?_v5)UKQ#KlsYdB{i{2`JCZ)P)v*L<@ZVtMa6D`-L(w|z<4II
zwnj7cqQyeCFKCiSQR
z4fwj8&S>oUoeQv~P1LtW+U>2_ONP!47SrN7vAF1t5;Xg-$juY8awP$wM^T-}TKpNO
z0>6C9Kes7lnHH6$d~K%}5>qWCyipqs+zkQ}&4}h+hPC!SUk`TR98i8is!8%VYwFiS
zsSs1CXu!j#vGVA)%L{`I(wvIzZ)m|}TL;}{uGBqEJwoe(d`U`CHFZQFf9-Bi4gSr=RJA$V0LJSsn)1R8ZE>2
zBL76RxY}n~+qMF1L}0DV=ySAk_>K8TP!w|QXy;CkyVTCSn-vSzn~01B&x0P;@aGI@
zzxB;ECqG~?FP>ivINp@z()Lq%-bYi00$KzK`x3t!?JMhdN@imWzt*&Fjrwq&Gw>Is
zN+^rI>IR6!mUNrXHL+2^CZL|mpp5^zSH?WEN^aH3D2jKhT|@=2=|7rF!i-6$r2bey
zLUY={H8)(Q+sv%aSb!mx?jX7iLVaAQFV{nInx2hKxvrxUu4U@~)Q=84pyVRPkbL7<()yz@gBPG@iPTY09vfr&0_ebXoq|h0~FldP%uXwh5Z~}5Sj1`47yfNL*%(@`slCuza
zvp!*TUIU-LDa=nUOHx0h(k#$$N&_aQWMVW{m*63ZB9K7~(5W9(SsI
zS|~5{FzoXyMe5bwI|`X11yVju0DekC*4mZ`ZL*3<=u^%4zC=b6leG^zB23Y0IUG;;
zH<;Mt>ZQz~H!B{W=!_(?&k-;#S!E8A?QXLSIL;}z$Oh7WiX+6IjBY=spPoHRWzo$+
zGeT|HdZ)O*m97Au=1&7r%+y&T>@Dt4W`a=iOWXbjuP>Q|a2JRxD|Hp9nQb_Ls?A-O
z^HHLw&*Vx-zSb`jX%X6^Tg*D#N*A2{HK1-14dy?0VR6i=hVQ0KK7Q!(VL&ORJN^mF
z_+OG&-EYr1a~;5Yw-wV22Bq}bItz}G$;q78wkAwd0_}gFU@~;>T>1%s@qnC43x#C=
zp$7MY5}fRBWws00k4iA6*PAVD3ZK~O0+?$^_AK#8TQX%(NKk#|K_Slbka~A?
zU0fkC03~muu=SE9{O4<8OafdkVW{MJ#!W^k>AKttWdy`)2v~O|eLwYGA
zRe(eHp}?rhLBr2e08ub^b0I@up2gmIJu=L3=746|R1K_s#b~-Fc-X>4L1Da=V&!B?
zsPz#6bb=CG=c*K3cJ+axi%-O>Z!vGlny=d-o(MwQ(}-a^Ug6KK@bA;5sTdCcme00S
zW9(pwtC7kE{rES6m$_!Y3GP(>PI7V&{(ZY~1{AhEzWFUx6tfEbMWIodU2uIvkHeEW
z*NJ7L}e-*{dvzI{n5q~ylLm9q?)5;
z+4^)mew4kI-V<0SywPVToSRR|dii9g8yryLD%++mE*i-PG?vx7CMET+gosqkqv9!!
z`Z{x18<%_q2B
zj7_gBk{k8kEfAxVi|yXN|8Aua)P^s##j^+dylh9XANvc+qEM&bW3j=ozh&K3_-2=W
z5=2J#YB!-hA|e;H7`9fxY;h+P)wlY#nuVdcq8wSO&J99yzYq481xG*3>!_Ad5r6&3
z*C|2I4#lVHEL*_W{l_}^v&=4=)8jOn&HgE$(kN=~7iBbBVFk+cR*`AQPo=OwpmUlp
z_u7cY+vO;?>VBvsYz;!c_Z`3>Ea9Tg7;c~CJ(8>n;B{0X`|4a
z4;d4Jg)CbSMA~1)DEp{wko~;DL|1Kr1!^Af-|uCkXKufUTPOW2Ia1mFRiR>EuH-|c
z)jNmA52b*%wJOc`NIeqw%e^&?@$9JWq7iZ}G$V8M+R~io;;+uZ$Z=v)V8Z6ZoHq>$
z9N5mzC^uizer=cFDQ_{c&%Ymb32s$sY(15qn=sSdOg;QJ
zV{Az`_oYQL3}ZhT{Zg06T4?dU!sC7k?r~Ao>SIJHeaP1L#@rhLer_kv+Q76S
zX^gb4GTuRu)0B7K(<+TutmU+2PVE{fTuKYwr<=en_J}Y#SgD$=@`(Oc$b;#!0Gm1+
zoj^ZKx6?p(wKPLudr=y&LvLxIC>l06sXksGcaOr8e#s${Rb_5oiO{tH6l_FyTyqvP
z%(?q&CS<^M)c3VoYhklTSK2-+_Jd<3EFiJfMUbNBGmBC%rn?rY$*q4Lsu(ENhuD#s
z%qI->P5^vn5<>;5eNF7EqBgeX#j^8suA)sbnD*?nXc0pU&4zC
zT|n}~K?_nSionH`;g_~fNLw-2^*@k(?UYA*=S$d8BpuL0=WbR>=J2GNG7$`y+_EBI
zm6U|&JkXsXc-x{8yXuqB8P#9&B_@1r$VH9t5-?IzzxFg*JH5}6seIfJJFLx(0YBT<*Q3{xm%ugXl$AW--qHk`R-cI%4vcPp
zI$Fw8`$A~Tdb~1t??8Q9H)FJGF^Vb%G}_kaOdE_4RuOkyvvYhjuZGQVElns)if7YQ
zM=vBbAFxoF=qf3{wS{rPOqJ<^=W~Bok1utRb;dIk%ImX-WT8|OnA{wCBWdAn99?mbU%^uLl>Pq
zI8)9)blrAcq+5jJGSkj00ixFrL{U{tmhs7$HOpyjQ)!^jBlmh{dy-XYh9vb%F;O
zosFPTta&`zD)_i&F^OJ}t<%N#^&@>7z2$p1MJBcNO)D7#yVL=x`>dcJlZL_XLl22l
zth!Y@SH{aVwfYcyLcaIk_KivLXpD&gPTWz71JO@QbKAPX>w{^|<=J+U#^kT|e=1XR
zKRHkN1S}H;vz#+D!%EW|!bzlpXUEWsCH(wDE+39NCibFw+y?Uldyj1ubgWzT2To1g
z`YDI)%Mq{B`MuMo;rx6r7yX&hus1>w1mEjCV2OIi>
z`)TXt30qk8f^>B%CtIWjSSBs^=f3&l9(RPqI=Zsd0HSBnH?%vL$TEd-$?(<^F&6_r
zs{i)xo4Rp0`JSh5@Es5m95MQ`v0^3k-X+21y_N|@iv8U>#%u~@IVH4Q|JS&HsWK=i
z1tpgc)%4d7-vc%0nqgrZLItxFeUr;gcqNt)Xngs@d4%Q9@bT%(&#j>zNw-Haez>lg
zGBnv=e{HZ=Gzc#+Qa^1QiD`SlJLw}5g*oOW>yc;Q)pW}`ndggjyx=HE*SNp+yk6ny
z0jjS;ORVa*VhJe@_Eu%tkG<8r33~IB0Iz8Fu6b2P=e%0PXI9S7D;VD^l@6(D`_(?~
zU6%`C901z=JE0e)8b9ABH!on5T>j)RYXc-Z_NR50#Hpo^>~DZ5_9C9CWW0G3^ajK$
z)yq>V!_sAz^IEAzyWv!;-@WJXX5gL%8gJ50^b%1AK=rdaM#pXR!Y82)LjkRM12s?{
zIFlU5$xviXf8o(6i~l1=Fn++yU$DN^+8j4XCmTxO4P?xUw|3A8+gDT0HQ&b)q(uA-
zdB!I7NkH&^WtfeSkQOz+e!q8&)rGga5b
zA;IYhAQ$bQ$L3tF@!CFXJut%>o^Be(tSgfWyL&D6(-Man71%sZW!>2Pk@REu)8w@m
zYkO0&PrnZ)O=3v=?Z*pwY~cW|Q-!g9FQ|MzV5d)?$LIJ(MlIjiDB$m9jp@uK#f>%w
z(6$maxymCy#}nw?|+Rq?4b%3%f<5{ZSP~r70Z94mHweyfJ#H
zU<+DFmGbt@59g@}?-bmr+G*x`>^qqRHZoGH-6yTZ_#6x#h=dyOy?q_QRF$7Y)}s*Z
zqCWW`5S;(G5!;Vf-udv%{wVzR)4k**@@*W!rDV*ZaC~m1BNs3xe*Cu6GbCGz?_TA@
z`1(Sxg0Ts$UvepgQneJ+4qh7|aSIDQbwC`)DCTK`$E{N>!^-ooj!8*R4e#6`8Ioj3
z0gju(l2UK(+oCA#iV;8hsyImaI!Gc#Z?=#yaQcB%9sGzBX_@|Ytc*9MUGFGP$ZoZ#
zp8Z`u9+Mc2C^DSR_)6IUOkEgs%+A~h6n-9E8y74Z%5fm-q25osP`_6;U0)F+1ddX7
zaE0H$Z;tGyC2h3}*!=aNaP-Trh*xkwdP!;~s7{qlP3@MvoG3l=StmPYnJ2fBT$(oyt}o{pi9b8)8nfnOX^#-
z-sS5e`P#dT*9+${uS>Kn>@@dkSb~8ioM-P>roO4@s7p6r6tQvsyxi0e%m&mN>&2&!
z^i@Gd?#Hc}IjtgY9xb7}SQOgNFJnZ6e)}&RVG_yh+oT)=;*O~wvR}AR>~8KKVXUKl
z!O(QllNNZXVwN?U{}h{%66hY(V?;5P$5qkWEhuC@7%Q@MdqP4vgw45)=`)zB
zV6S9gzetv?CuJHh3ER=+JZbjeA1)yJ?Ppbzi$x8V5Jn32A(x60`hl8!ekl7&ln*3_
zE96s)%ooucNIA-Y3udOnLFn5;77^FIwO3U+XXTpQ+`rBDw={
z%jI1V+P_Y73w(^1LiNu#$Tzcwd=I&vGXZD$Pbka@)a6En;+9!MT)9qz4vXJ+SIx|>
zZ8rGn!Hc})7N0;hhPJ7>n!)YleNCKp=K@JZ;!9_#NlvFYck2w`YN;8QoqzOL=82ON
zgR~bpqle&&Xbf~jqpp9ISV_z*fIFROJUI^5Hq=%T6fmR0pmMlBeTwo>R^7NFJ1Dkh
z4}5Rbt?77PNzjzM%d^XJgX>1ogCZ6o%JrSr`xVD+UM-*_FMWbh-q5l*S0g2KZG!c&
ze`1%Ja@IaBkA&E_x_K^eA{3>C0h(DoC(hFLPesAMV#%sI_#0`BDK{Yj&
zK|vaEe0n%v=2*dj9;eJHt}@+&CMmuyCLdpDT^Z>5LFK2(#g_hJU+Yy+%csfDWmepx
z?&!_W>~JyO^OJzhv}h-mHN_`3@A1g%K!@n$iZHRX)Yoyfw0mk}BV+8St8JyPqhVtPY;7HEZQQ+Gz1{pgtSs%_f+Bp}
z?OemWLVO&ZO>Heafs?PLfvcXgi=~~dtG$tmsb`RzhrO?}Il|N4-QUFA7&v=CMTE7J
zf~AR*shy*hk%O_ZiL0}Zm9vkniIus99xzkU(KdBLSeRM6*_b;7AoO*WO`b#;shgW=
zSoj+om;fy|4L$e)6hkRgH(Y84M7v!O2Mp-u{7p!J+P+
z-Vq_r_6Qf}I5!I`D@z9}`2K^nN1(r+mG)yxOGkM<87(af_y^N=u`o2Xvz0}d$~pl@
zO)E!T3l&R*nVm-jLf^^GO2gGVIwQz0Akx~x-8B+;IrzAEgnBu6xY$^@*;s2^*;v|o
z+1oiAnV30RBCM=|qk*B3y|`z?m*Ys+Q7uo+S%69)y&FJ
z)!Gi>WNK_>@9OO5VrA{_We04Wql4qaUIaS(TUc8ltgS3?Ljz#q
zXK7_@Xy^l{LD$Y$)7VzuSjWuX*g@eb!p+^=)SlAT&CFcSSl>d%63@M8>#>dPc@3MTbPhhGxD9@&+*>0X~7gJ|2GV
zUM?Z7(IMesVKJe8fqqtw2uEKR-vHp^?BeWZ7vP2PgU_U~pPh}Hjg5`5ld*=ixrwEo
zu{H2EHPtb7)U#1m*HbsL_lk6PbI`VRaW*rywYPG2wlo749=1W2Ht_$Cjj|a+*U3WH
z)YQ$(-q8(8?Gbj~R{p>vFeo}SGRZ9{DlGX$UV3_3e28Cgu&c9oxWAukfV&s)4)O4g
z_4E!1_loce@b4eVuJAT|I-`O|2Z9fxC-`gSVHNrI`Wz
zyO}$gxtKUVcSYE`m{~emyZJi=0W(L0vyZjCt%-f=$V_E=-D}2n3$Qncp=>36B8Qg7vSp>4`o;
z!Jbheekso){3BBmU0i&8J^Tah9eg9b;lQ~&7+IOy8=IJ#=~^SKy&P@*;J{hfy4kpU
zg@^fixC4ZPy@#E(pO&?Qy^oKbl_h*7F?F(avp2T0aB;GBas>!u_!8veuc2<`Xl$)z
zYHp^Yr)K>~RZmyL*u>1#Sp2^|h2du{km78(xv(}tZ;-(5TVwK(DDmSeYuf1~S(Fb5
zmZcZU*lk|iKl1SYzYq8qGXfyxNUx1CdRy%SRq*p`BhQ=?A6
zny{zUOdI}F={>ps^5aqhx0#29*X&2{XI`53a-?rAo`
z?k%_?3{JxpVK4&**^>`pFnUWBb}O!ePFF#080`2Lqz&7E-A7kJtgB!T4C?+1)>8(-
z?i^eZ2D9OcFlY#azcrg+P_g46eylz%K4paNsH!2ZP{W
z&~NDx?B-tuO|OFVFj)65NbETWy8>`U82k%YguzA_d|G`A21{u?q1ybA8XV93AVqSRYs5r6?#guyPjA`C{r;Ozjq(+Q{y5F**?$@2zk>{yE*
zQtTel9F3NQ89X4^_>|?Y#DY3olVBxIu|N0*-r2=_W3A&z2
z%3qTk=Uel0Ct14F@rpys>Ja1KYfVub~*-o0~Xrv@m{{jBSrHZC_f_Y-w$%8{;W79-Ru&En1qgq2W_v2LqR-VO
zT1!&?87d0ay@$8nmf@-#uM
zvDD2i8(Uiv1&SJX#uNz)$cKbYyubv7*h^
zCRj^Q91Io(>G0iHZ++n+&u}sIF2+oh4D9w5_-SxqFE^z+%Fv#CDUUD`B--rC@lv^q
zF<+l#BL%1qhu?WHYC|WCvq~-k&_{boQ&ZAos3v>(OUq{
zCY%f$R|81mDx
zQ)D9tV-7hOl>a*zjBqgMN~p+?lL_Zy1vwXY{&z0eFy)aSR3fKh`+uk682OsQPp|=}!UH)Kwf{R6t;p9DC~Gl&yM526jS@>fd5YSscn30;
zLdrk9?~BA~8gp@6?DlK_sAL!N(`WEp$?gL`$TPqKNuRKdaj{E(BSA6sD?HEd|Bq-b
zA$Q=wbF`Ladyti$&mu`606zYX6%smemErmF{eP4;2U#$%3s*U9j?NlB=DG?JDh>qU
z`Sf`t2?cdSrnmV=H&h{4{t*u9zM=YJ!59}cxJMipjls5392XT&4Pw=y2*iv<*b3=|
zHKW}-Sl?8K^^qv7GnbfP9oh$L`)XL@Yh=Ru_#woX%ghkN`eDoAJ*){dvta#K4Av_w
zunr#}fVE={L_)1>h$rH(^^FzQ5ub@*?NkeE?HpL2N<_kX^)9RjiD8YXJB77QF2pm*
zD2Qupur)*iYv=lNSnKA&S}GdW>+H_39wvq8^5FuaUOsF|$H4kKhbyc{$Y9-Y32Xg=
zAFzHD3u{g{NTauCNzpmo8d2bLWKg()q{nf_xY%6oNEjnGgy)-3|Iw?T@bNP7Fv;x!
zf2S|EjbPo32J6?qU@e<44r?AySW_6o`Xjn5tlw-xluMj|$m<1LlqRrl!FU4eqAge_
zO~RVbyA{?{rm)78hxOZSK3FGDL4>0Qk=hKluoT2`>Au4%Wg1q#?T~29CC`zk(BOQ*
z^4Sb*_{Fz<&=6=!6Zca@c?)1snTT&^PXMd;fB&n*!ePbY2qOc8{}U09fTvlV{(DgEDXel<
z#c?qtB0m#l(4wHjjVxZmeS@+fW@l&TK+J`hmzS3hu>fLWVc{!?uOYs9^QH*mTZqNQ
z#U&6+Awq9xIm8NxaEHVyi0>d)S69D>SOdS{htKa&A-70E`H*|b;EMvaVCYRvP0jGc
zM~FxjE5tU4?d|QKAa+3P?Ck7<*bNcxFxd;S4`P3R{{Y0#5C;bbhae6^92prIg*XNg
z?w|`dmq3|-I5{~v1#uc8G~j)OI16!ZZf+hTbeW+qE-u24y`wBcgkr>R5LY3tt*xy?
z{0(2uB2?B9LnMJnN=ixw@fJjKa`M{{DIh{YDiuU(h%_`b03t0!xb++zM0$vD
zTQEk5Oc0rwnOPvRLWB-UHi+yHIXE~tA#y>44t^f^3okD(A3Spp;{E&gA3)@XC?Fsp
z2vG>4u&}TQL{W$jA3hX=C=LoIyyQ*L_lA4GqD{{V=A5QBn(f+2=L425roKn#Z%5fKpyF$!XIbaV{FScuS$84oc5Vq#)q
z62xSPDJdz>Af`fm{``3w#B_)+UcAVFmqATN=?QXm9}0PzeQgFX49t{L**>=Mn`@0sK{r
zfsu)sh4n5QI|nBhHxDo0z55UN1q6kJMMNKpLFb_qkbd-7Mpo{LyuwpOC1n*=HFXV5
zEo~iLJ$?8#Ci49pU~XY)Wo=_?XYb(Xgm89ob#wRd^z!!c_45x13AUI>5lhx`vH|d;JF9O?(1EB4QF!vRmZ0DJZF^;kVu$
zI(p<&6?ch(vowZ${tn@HHu7)-dB9sLgHP7K=MwqO(*MtY{wSbTS@>hl2Ccv4J9hTm
zc%~NN-PNMys7b+P68ROckyvf;Zt6sN)4SE+dLQoB`b6fQK%erAW)mx0x)vH-QNy(m
zfYGWvOoAI3BiegpAUlX)e5^_kE09~WIC|02HN}9!Upi@i^-38^~=sc`k@&M
zi*9{{SG$Gt)qU@3v67y$YyaT)z)jpjW$)genO)5z&
zK5%>Y2i}F8yo>EQ&QkrMBeF354x3lg(AQ_x*sCF=v9xvk(fr{1?-2Et{7pupcblzJ
z_plDJKA`aP{iX^S&mq6f*o~J0Dyid)`C7{s*x7i$;_U2@P*B7Ml^RnxH?q1Mj(iGe
z0D3hshoZ4Z)%;o?@oR($0nCX7zKJ%7y;{osZG>G^G_FrEP0ANJQUGv
zsQ4x(`0{TZWhWm|QqBXE7mja3W>0@Q>axqOdc0j)%GOqus6cyYT!FdgiDsP#@(4T`
zD{kFyk??&&fukm=Z*IkFw6KGocArHm5O>v#6I?jsMdOo~ErW@1?Qc>N@%&-q>m}Oj
ztPcHTEpJr$8bCjp={-eEhfzJS`-F>|JLgNeA~rV7sDz({MCi&5lTpBJN~&tw&tzek
zxM%rUx?tU6&u0%;>W+Egi}8D6&A&OBFdQgw
z8&VW%!B$=^36`3gH10R_dWs#~Vy=329^dJ==6)yQJ#!HQWk9)JQIPKi_u~-Uk4MKT
zA0;tmR2Vk0Npx^ca2XHdhb_RfK3sA!!$>Ko%E0N|vgNt+3S+E&REhE1WWLaScXKOs08Ge6Gll#Vw3RphxG;kp{QjozF>k$<-IEpb*
zIJj2!z@0>~4Rn3mW(U@ylgwgmGhrKrKX5P5NH=;;%yAbF4%pYfe9_AF&#D75Y)h||
zUs8`er&MeyvDnnk#C_6l${*H197^>HR}Ys3-~_0dn^PJ|0vK?K2d#T62whgqbQ7{C
zu61-IPzXI18Ww8A;^9XdKm$FmGlk(#e?%IAkvqx(zt?_kKjPaUAZYH;eY%D@M1BLv
z%JQj>7jz%{In25o3A?m*2J|aN9(&bF_Qqkk3c0v|fcw{NU&>=){U{Esrg+>jT38_B8Ip!O2uKJA33><4s~PI_0V~QS_VsEhieAm7Vi3pV7W<&qq0RGOz^yUaead^hDakmxZiFcp7fHqb2}5pAvU_WPf8c50`w2#ZgL4j(Ftl&<
z9+{7nF{rsy+S&ZZ$2X^=6Oc7G!+!uuyfie3wC-+MY`gke2D496W(DO}(%?5v4#fto
z$_`%u%p2cTRA#;m|B^b=q_++T*3@*H8+X7T(hL?XTIdRdAFc}v_KA+{W#?jVoNbE^
zXlzV3u(8MhL&3ehtB*)u<^b9A<%Lda>2DVG^=cMB=ugQS%`K*c*e%l2l0LjTD3=9e
zrPS1~8lg#BR&%BOyQoAWd|kj#92W|Fm91+Y@{o?E@BKi-
zK4)b}vii_PT~*;J2yO`OBx-9hw0o3J-zOJdb3sD;>A4Rh8pxwN7jrD502G+6u6?p*
zJg#V$PGk*xrqs;W#3vg3gm0JKi&tsd>jheS!@a$vd)uR!_{8{)FM5p|4}YHguzWq7
z8{DuZw0{XMIVK2QcBrU!MmzA!0{nAwPPI-7(lins&>Z|N!YJ&)PdmFWT3XY_lz
z5;0BW8lqf%Epm6()~pCPe214kZ6bB?U7NtSy(ep=?CcR`A#fWghj+#M
zLQMqqBLuz_dUhrgz0IXSO!tR)NBbAA
z&eHEUyqxmKUNQw2xuIPhY_jw}TBjtYuG_ypC?fhlOkH(YlilBkfy9VW(s1ZTK)PE&
zx=R=_x<+@ZfP^SW2o9tX5D{q!rInJFk}gF@i%9(L=l$!ww!s)X+jYk|_c`BBCC;LT
zKZwUPeCddb3IJBf+}7E&R);k4^+jbMc6+tpDEfTgANds~v^*y;Vgznj7v>BgBhXZY
z$G^^M_DT5pRV#vO9@@4UzMTEu1^~cs3S}xgENElAs5%NkHP;3@a|xLzwmON}Yw;(|
zO4k7kt+ot~S;Ovb=Y@lVs<5ZP!BRoS{L;nzg0bL8js^{MpTC?PS{JBx^QU>DZ!;@9
zD?~U5va5*Np47z1=PwTe%;#{L$7yE!+B40%Vq!Cx>FKxv^1~p+NRuN*B0Vbuu=qSc
zmz#PVA!aTXI{4ZzRweT=E)Pzpw2Z9cI<-m%!CcS4>gOPp2U=q;7;|&MlVVTdIIYPz
zttENU-_PmXuRwOvAqz`+RIFjqv$&!n#rd_TOQpkIU5M8e^t_lHmm;w7ec$Gamv%6@
z()*9gpTr`m*+&{fS_iF+{CCo0ihP_w*&zy5jh%iv4WFKk3mn|n-zTGfJy%pzRO&VR
z=a!ZxNIjIbZC%X8kQ1W{P=^6@A#c*tix7e{hpqAEjSM?sfI%?e-EXq^&xO@v6u08~
zRB35hRHkRCUE|_>+YGAmnE`hT0x`H{PqcT7YD%Mwfgwry)ndwnFjUFypBb*Ze-}Yz
zR`$vxyV|1u%`=19laf!DAJ8q_A_k&?eqV*K_`D?m&b1Ri+gO!dRYGuQN^e+u^$T8!
z(?G^R0%1aSuCvY_7&A9~{~|q1D0(WrQ6eZTqLQn^$Iut2%FV$^es3q97L=Fw?b+~;
z98Y5jY0fL1+2a5CWMw6RZK}I4yR5PwSP)-ohX~%IBO#H0ZZ&QwXcS&dgBdir_jWjt
zp$J13f)9)(Y^fP(f2o9D8lk*@9sbphI`x3Pc+tV7J@zj!s#+G>il`_wd1ZL`Ct>`*
zNeMz1a&r3k7q#Cbbd>{2aKVz(uAP|
zpx!uZ6XTWOPBA7D(E4%2z#WL|{o?^ACFgJsg
z4jT@5Vx6#GzU{na6#_qZ>$!F&%xpiORcqgK3wk@5oi(2oY9=BQLGzZM`tC4zysl8}
zkKte0KlCfBtdQ`@u+1*RpCCKw
z^DYM-$93a>d?**{%{MH(gvIXms;sdej6jijLNZs{gSVMMCFXmf@>9#FSg{X`o_01}
zQoIGIG9o8Z()qMCD2<^6C_KJI^IAs#veYh(_w0s$sY9SMTjSi~qHUH0{rUFN8)7#v`|htwwuZwvvy-x-s~fkQX`;UU$N+_*H?rj&gQea^Mowi-h;!-wDOs3A
z96KBYL?oW>?zVtNrSl@<%Y4ELGE7}Z$rqZ&(8_46BbEHE=$wTTzmg;H8f1fHp^~1~
z%&wbBJKX^er0%mzbtOi1vqpJ?8ORnONUCA?goy>CL~4t3@TsME&e+}Gg~q3_K`J>s
zyFup~z~7b1HK<}MX_mCzTuZ83-oEWbCw=8aMSMtT?|qUFKxM8c17>QE?Edk=`7QCY
zk`jTeTP;~-C9})R4CJp~)qy{P|5{C1JKsG(w|t+ldx9G@v#x7i@@umgnpl6BKqr6_
zwvq#5hT&YdbZvfSv0i=rAJ|t$?ShE|Z=M^;2GTkM39OJ%sOLMdL6ZJCG4@JIKJfF|
zz`)yKWCls!0GAjsI6f~GRKV6}4z_m1Z)6o4CfOYlrv!N1lP>KPO2kG*0cRY+p-km2
zZ~U(IbHvk+l_pgD22dKT+H^fzPF7Y<4mdTM`qEBi_&FuKao+hp_ECLK#w2S*adsrP5w0&GY&Bx$wC7zgFFLu;)v;-oSf&s
zgx7jI-|B$fE$Z%&Z~k&s9v%Q3M?9EVUj9AqEYb8+B}$BzY=|}<6A*leK`1?SiS*3^
z9dxIsGRfNfr5RaSoqIJjNfusvmX@ir;RH?B0r?!kU~P@NXkqO}W~1|$N0~Q%aj?vZ
z$(XZ`ZfG_sM6hZuk^pq`RQ;PYsu0s>#kb)SIJgdI(jL^T8Sbt8ax_0v9tQ{}_5A|S
z;Z9GQNRBVR-DdEm`ocWRh_3hkM`X{Ci}nRMFWQh|=!SP9FZ+O)3{>b13VWYoM;R0)0f
zZQ!M25A5=;_n1R(acE-N#kqPmRA!>8g@$Ztq);c@MLd^wqHT!T(qwg>QuJ;lX6Vns#y
zy%^J*Qfc#~$>;;pHUEy7t+1i9kwkF1Wyhh2v%kcrten!FKs80G3Z0G`
z%x$~AgL<{lOpsQVvzOXBHGRw!d8WyT23Sd*$zTE=Esv>d!gmem+vu=9<@~9B8g!iR
zWM09@F^IoIJc#Wb(M0QLTIt}<<0XF)9mY6tiv}`7x|R&J~p(`Rt=q_SHet1u1r2SKhryRIkdExT&JHmVqE`eKj=7
zzAwrhY2gU^tb=sf#wJ`{&z1Olrj^vEr>n2OE4r(1DDg|tF;xYF3Z}~G$r|(Mg-7{*
zcl?q^OGo>s7fL>yn?$8JK7dPl0w=nuwZsW~`!HT!8s1~xM)f()*AB0z$^|Z`r>BYj
za)O#l(_iTmQ9J0LeH_2V?2F_l&P3b@78WXZ7_dK^lxl$l@1F+V>-DOL_!N}w;q*GW
z`pQN_gKJaTzin1^^_@sya-cD|vE9*eUvKP>
z9w>Gb_>)#I6nFyk+S)W<;~FF5VVn9QV`YVZWogP@dFo$!UKX?C)jk9Wt_?hBZWbt#
zL2+_C@6Vr~*T&_(jr%SoB}Jg~(X0Dcpw3tGBfkZGhAJAuJq!jP<r0
zD*Z#vc2NJpK}~wO@+#iJ@)m$yU7tATkLyIf)GZ03OUIMO8yY6gVM*A&KMC38SO$QA
zXO=I=NC#m8^YBmnm`M)n*nU$v{aw7xeZ#
z_WtBfb^WxZEi@BEXb3b?doJabHIktOsmn|1b=;ob-u{w&of5?5oS4+Xo?b3J{!UeRM-6wkCN;=t$&auHD;M~!8k1~EFjs%vZO
zBy1dsQ3JNM1&bwF$c2obj&qK)vPl}=r#0mALl?@QLXQQ*s*VDm%Nh6po^_B4GQrs;H^B
zswhk`fLG`h{_pvW-Lk@MMMW{iBC$zI>Loy(FlaCoSgQ?$NrSZN1&CF~o+hk5-`wze
z?GK&j9Oodno|6mz>&~tO#}-U6SdoVSiu~we(`U@d@yJd;2-F9z0b?na_qINC>GVV-
zvs>0Grc+z66O>;{d?X^7z$M?F4H*H@54KQPUornj_*4U0|=RZU*E1SLtm@%@x|xef|mLl
z%JDQt;YO~meWck%c--K>^hE2-V>#o#QHNwT~PtcYhWO%B#TVWmMJE
z0wbd#t-C<$JL*#j&%_i!rX2FXx1;mT$?@?F`I-!onMqUm_OrO{HzM2gMW*I5ncFk7
zAO-!Rr$<9h-o&sXCB=4QGqoER8MC*y`1APBpG(9M0x;6ik>E1o-DJFC{r&f9W%cjO
z(bnSH*51z7g@xUXeP~_`hwl>X?tk4nBqG3J6A=+6hA+WSh;|Xja5^}gg$0Nz39_)=
zgi{HK$|%WMimF&z+T2Haxw$=l;vJiin)(Q&W_d-1#f1k1`nKUz=7P>EX?{)l
z|6nsSsA}%+?CNc0=;h_*t7l?o6Z$B~J>Y&|i2Z$JSXfxvT?G{oAj+U*XeOn?A}b~%
zLdPz^AS4D^F8~P{eiuxpF>_kkdBKBPIv2;
zG?$>3Eu4&nPSsKxXo|_mut^yk8;J=576lpJTM}}Dw}g~rWR+x;?&>(|0DWU)OQfaq
zeM=V;XDGVL&eR6!1tCB&Q3)vtK|YZQATly8IM&PAE9QQryL+IQMQprZM1+@<&OO6>
z`WAYcVnA6*NnTm`uBz%?88J0gOLs4%v!}Vcm$R|4v$3tEEfOG+IwoeOCMHnEuZe+y
zv63KkQ<4@G;1{|n#0}x0xHq{}APYz?E_QYX26lEX3VJ~fD65SE4tX2i_XO+6(gOABpn>$_5xrgvpzcx8Z$l9DJ7yEF>|PvG~5*L8(u
zMK2$BRFtKRg0B1V=|l
zXG^>LS1WTrFD|f08ykxk7pMQONbra$h$s-?FC85tf{uk{mw|x+myQ^Ivaoi#zI(cU
ze2jOzkAwG@X#el-9>xAT4um^{8{`z^}6r5&);Vfq%J=M-KlB
zgVAw9u^cQ1)T~@r%*@Pe&=*i}0WN+r98N(vJAiXj00DY9AsiphFT(?XlB%G*yegE{
zrlYNcbkMO;l+sl(LK+L|YwAGQussSH5*8i`i8DY-N`7fWZCcE$*x0bxi1_$mhlC(o
zTOAo$9VHnDBd_})AT-n>$ihfVOv}c`#74)_(bC!4K*>Z#NleB-(?G)naxwMtyN`5t
zzwaL!W)tRRW235lN7W$A*FZ)|S5wAJ2dK!{SXx0)MEbj#`C2*{o0}l@
z+}vE*aA+JYHBGI99K#^$ycNpVBf!tt*b{=Z>Ad`Hkw|w>PXoyK(<>h8AOZI17%#6T
zdmLDEQ}YT$10Y=?`OlURhXJSgBaVuc3hv5v5svDIRkqqS(i3ZV-Nje
zQ{i8M39nqULiq3h;hGRzzFCWeB+rtKL9ns$S7QNBi(0f(UX1@29Y<>#g7SuxVPN=<
zYW1q2K~j?VY^&h=3zQHJ?VSd2HXQDFlkse^};NsHx+{pZ)oTR6q4T{S`Yr
z#c=U*%GMy)zn}nM%8$FAdYXAgDF1=Yez3VG;nd>dn^a$aST8^nTq=+QK2;}uxuL5Y
z3E@DBimdkup33r0zpG4iQn9lx2(V$O0D9K*YY_~l80$cLA>$q)m9X`I2THzY$h@N?
z-}?INYmi=&tFa}~Gc|=mZEn(8(9x;#eJw0M-1=s(0L-WUH_TY8XBEU^t7e9d2L@bj;WKOL3{+>V6>M*kkr-ly#QEBUGSD!!!!
z{I2wls~;ffzAAjtT6{CKW67ku_I@&iV4j_IEuOfafG;>9&pJYmy=#B(5!j}_vh&~c
zshZB*jE}#K*d%C%iw&lg*I@^@3IIjX5e6R0xcyQI7*7I>WHud6F|uB)q?*a7Os
zKRCy~oQ?^unk}83oz-{g?!LOZdbKDfcJWxSzaIe*7nPN{Gn$%Al%}J;MJIgOW4;Sx
zJ7Z%k)glOtQ*ABCfx&8*{fWJ1HZc^3KP)uvyvJhGetHev#($>hdKD21$ns}BJZ^s9
zFjGWOQ#;;t%v9Y}opOBWoHoBUc$&IZ2E?|uylH<7?+=GN
zocnoI4h+JRnLhNge|MuBJ$thY5_y>ZE&qklSa26IA`~kWPsOWfkTapn@D_t;P%m>0
zB2O(|?-#>-G~4_8MKy!oy}NUV6Xp=C^_5c%T0ZWQ0?fx+Zn%+}QJR22+8Q*!T5j`y
z^XBm_eyQcI6Z2a#+5mQ0#zrx<2>2LZJeTpWuRP!54zp_O6j6A_W+#g8^Nk
zvVjqZ#O}E38RWb8{pKPVXpUnqYvslC
z%kOk(@tjhIcd;9A1=a^FS
zOn|X!Y{;I-+&CDWD2-)d>B5ab*=cQTxD)x5`8)}hLx6X3y5_0fzHvcV+P>F&eDFKi
z!_S}ZSRTe9+q6)hQ*?oq>#X*UlJvH*9H!BrkpfkW$V1>n+K!9>@;I8DEI7GK7Vh&`
zhK!#+F~;234uF_!ptVZS_hX{dm6MZZ+&PmmyL=1YS<58t;9%S#WPDsz3P^+EH7I=4
zatGg^e}kxaUE+m>adB~S%-7d1rYY0Yu>cFr@c%|fMHM)Fq>7uu3Z1ydO_b!6l=w!w
zV_in=?SVNx{ZKWnNh{T1Dmi@qPnT=>^7
zie?Jgc&>P7iUOPRn*C1l$Z}>NR$l&;y8}sJptMQ+Gb4gx51)mFEZbX*Rk-@=k@spk
zSXqgWzl?up`AaUI;wY110n>)Ty#7!{(MV1~Z%}8!2}aFtj%FfgscLFwXT1*JCfy;K?J4Xj>+35EgxHen&*V5bBuFbP
z*Vl8as?x+kTJHLh7y_OYDR|`GdoUuwyJ<$)wtRzXN0^-bdQ~U_ShoI%ktm0l1=~kW
z86t-kZPPIY;e@q4e2YR%lPPUb!Xp#6!Sq;L&ofKD-nO<{#!=rgXZ|E<4`OrQEe>7s
zOt4;cq^v9%5ThfEv8rFD{NYJ3E^XC#mQu&Spij7s100r+!{yS>D)A
zEf6D1tX**EK}ScIo=!*SApo%o5s1c1+1A#1&6)Sy%wV)ynYFL4j|J5-Gd+#m)k=$#
zof1EULwwh2#3g!l6}V^fcuEHc*9W(~%1yHJGk2URVObyF9UE)R2DFbn4ME1QUneKJ
zv4fu|OX^MB)0j%Mm5jA}o0Zqm5|WRT=?*FZA`CI@AIw%=Vu8%&N}R4
zJ-ocQ#U>#j`t`9rD|32ha_<9SRtL9w)xl?J{H^Dtl{>O
zAM8_AIXfR70$?a^X!w*wfoque_+Ork(?u_PakuSKI!N)lUp3zHUTCRVZ*K0Mlg^Va
z6^Zov?x$PdMJ=rG|2l$S1O)I;C$QiL66Xagf8B%~1oJ*&I4iY=ONMy^rk
zQ1K$#MMJ~L$mm_?{(ee+rI?s3kWGQZMVvA+?9f~~_;%3;nJV~nb`y4XW=^BBapFu&
zK)t*?()@Jh?HoY)!w?_uIH~CANfxwPmG6WbX9&i3fpL27`}YUgj%B~s3kX<7Tbi!T
ztch8U#A}_m%Zua55CFK0jP~uU#9prP!tT)%>|3r3b#-#p&Fe>xT7LE1N&W#UE7#WY
z*A$#M&})`GuiheJ^D8S=D`(#xWe&G4S=;ykO(tv4Kf52}Qc_Av*mno0Hd=^FdhJSr
zVP?t6@!j`tfpr?Y`la7R4?SVC^7vDiQ%~0}9%{c?RVkTIlabZ+!vM99`9*U-$_lIk
zN=q?TGz$xLbqQsq1OljpXh21Es}n>E%pH|bMN>h4ffI;pJLr};H6>r=5
zV6-&&M37SkIr;jD&*75f6THnTGVM?9pa1-A&VmkR1CW?Vpdo@{<4IvpwEvbe)obUM
z+9ib_!yG?Ud>#3#eF(fGk~i{OwA;#f$>p`Rx5HnqBr!nvrC2=E!QC>YRV*01{bc@C
z(D!l5LcMMaGXw%3f3-zXk;p`{NyYrY8Mh6zi6{(ATguAPeQ4V_I0%oAXJ(dAmB3=N
zfYJKa3@g
z2W(}rpTxj~hDJ`unb?gS-p0o61$XSlFZOMStlVA#t#D@lSq!Wja#9s-$=N!uwRCNEi|>bs2mazwBmahAjmC
z07N-jIlq%q7yqW}@_^(;<$H4#6$2GRD`?K4H4>t^Gccm4cx!Yte{__AQJu*9)1L)u
z!DM>N<&_!PJa6&k6E*NV&o?Yv4e#%;eS>xux;jLX4Un#W{n}SiAt`BV<0JyUpV%`n
zsHjX#T-aP(a5uIfMb`{0pNps{w+cm5kaf6&7m0|g(>M1W9VSWA(&{UjI?42~{cXa6
z_-ohEUVYlQ;OF`}+ThQ~3!j~+^en#pwhzN8mp6Y{)s>}tdPb_$!a+p}ySG}zKa<&G
zXKUOfruA%l?WU-rqVjPpIhM9aR|=fz+SHExEKz5EINC4zOPwAXXBQTH=qA)%zg;XF
z-n9nkq?U=O1!PudD^DVG;#-lB#`8w0y+8l{mG-Hp=}-!i=lwC1NSZF=<}TYe;iG~`
z?tn8iCm+j3&qq)HOMN7B*1Cp5Wa)lJgQ8-{9#oN}vIbRzDZJa%HOc7mj%MgMyJnxA
zK@yF%W>QjOzN?UsI#5U4+??7ZFVCNH+qF;QMl5|tAI^Zrz(B(6VO{X)!2twivTO4+
zxXzbO`1}?}2EoGgI5-MoJ64z9NI5^zYl{LU9?C*O_a14A)MH9ZWm{xz2IdFe$yPa?
z-4{oZ$V^QDm8q#a9ibh$N4ooymAo`Ri7T6$mc-W6ei~_U6Uk)ifW5te0lI;F#slnt
zED^`g7d#b_k)PaMfA(067ves9w+;k6u~k#e&-E4-(CEQt)@heCar908JjYbiZGTxu
z?`v?^ir)~|x{l`x$5uT!50!V8Ce1@i=j(uW+=rQ-L-Y9x^HY80`#3qr8J{B~
zH8rn(zPLBjRH%GcM*v(O@XzD4fn=q0+;d47H*I9TTk&>5LG
z5q`)ZS^wDtwR|mdV_Je~iWL2DeX*kaTAe<22}Iib4Wh>v5z!tu!6WP0b0Xzi=f9(A
zeuuKx*Zg-Y=`tA4m(=D%lj`=puqz}bHEvie;a*F%E0yx{+@G8y@nr||Y9^+Kf8<>R
zU!~c?*CQGG`jR&p8Q~F^dwZ)9KOeIKR!3>ahSSPQ$28sT_I9cGcx!8HF9cT3O-MJM
zK6pa~rllt;q8>denV2|hy?)KxxQCm|!}CPCOoMPQ-GTlB4%$XuWn^^!3zn?2YOLC%
zb~nD6tp5ArcMtYT*UP;^Zw-i4L1n?FWYRpgma5Pn?!4}$?lseW!`}y-{_k#i8_s4I)6cXMT9lHj24Y&eNhBwx9cjDgxAr~b;y%4J}1d&Tn)C%wJB
zJ3B(%JUs!fCt*}{Vd0ZP9i8vlN0qpn!jtt~M*<5c?CP)dJiq>c;#0TZzOAgFWXe)(
zKecsWRD{Wf72Uk)v8SlOy`}Yt6iAX97<_+J{ip+GZPjhn!BV|OTwt)a2KZbaJ^F~t
zwEz~EdX_5A9;vaTYr4(KOKH-N`}@oLW2*45F|4dw$EBb&KOZ?ipFTgo^DSjjLOlLS
z^SkoUI)iY+sGCct24YK)0oTosk0n~2xJ?`LKh=zIY?yVyaPQtt@9#G?H69^A0Ytp%
z+SSTA-#AGDBu@HO=3aViq9UUIlVnIRpB?|cA2E51>xc*5mD%wxo^ab>9(5SjGD-rZCWZj6SO{myK&O2&uc77r8I
zaF~Q&ETyj6rmUx=B&QfgjqMMddatPPUYY6xHefq*xeGx-?{f}MnlR1$agaZldl2?8QaH`v$z+wcNiAU<|jz
z+wLiFq|mOWpco~O g)DRaJr=vq&t*t$N43T(@tVz>Wk7$Z`cye=jb0NM^c5cJ!
zKRTaoIXO9NEAWPn|KYlvdQ8`2{@lD%ySH)o+}souRo{}@v=DeJ_6`Ap@1fEY@vsTt6;cb%TjSSQ^SV7$=;LrWd^ziP3hx9MAYXKIyk7P
zIDobSgE6TC$*ZfY?8VZTY;20F&~r`A)m=!9N7>v*LIMY&^nH|+P+F51r0t;$Ot_og
z)u9$x>>=<^Jxs#-IZOZ~RHu3WVt)9r{&o7e#1;&q59Im>-jz`I;e-YD4TY#f1-l-5
zq`qAd%ww(ucOsAtRypR_^9Tm}OOPs~WocOp(j%{O3rG|qx6UbV1ujvQ5fc7lcILZ`
zeEm5+eVQ{j7fh#V1Z3l0)%G+u!(cL7LLwp=GL?`lulnUn6bcfjf&TuNzPtsOM#uU1
zmcG6?l$0I5zAmrcys;`5AJ6LUwgenBJ~|=Na8>r?41hivq8TFs*qTa8r@`zrh
zWYL4HtSj@1m&g4u+h-_?wYAmDwM4fiRPWbS%<9SN49X0I(G&mp!N}1P{VAD_
zesmPIMn_7ALi+f4Be?rP|Iy(VG>EJQ7Z~{%8Qz7SPOog*sx+@wTwaD;k~9HGbGZ0t
z&UdclW%0_sM@mXtP*6~_9}Q}!z~A5BZH5daS~@fuEt;X`CNJ;mK`)pR;j}f0kd}ZTyuID?KYz%t
zDG&%odmO;Z%t$ZD$%I2j^6&4-(bn40(H@j2w!4dm$IMIvCx!zk4(%Kc7nD|#fV6!g
za5xhSy%GZ*0}G1)B=EZ}C_p4YC2(6t3Bov_zGinnBq%5$EhjTJvn(_973M{3%EN@1
zurNO_b4cGutfi!+sHLN$X%6Z5fRDGgr?+>Am)C!?x!8oru#`t>xyiZtnMrBknW_2t
zAi*m(+}g}I&`sOOT3TB-cDOv`>uwLy`7Eb9eFt+Sw(4SNiksHAS*3xq@oNVhPPxC
zA=FY4LM^$3g(c-~Dc-%S;{Y5LbTm{gE%i(+&CR6D9E^<&^c58pWF#aMbd(g7?%V};
zmG0g(c66{udU-jgMTbK|LU(7Rot>vA(kmu1AT13fq^2Z9MM4j#+LVwGnGhcr85S7k
z9~=MhVdQ_PC842*zKx5Krm_kSzUtk(s(0CNh_W0qy)wMK{?jBP+`@t#Y;7I90v(ZN
z5N2rxZKi>Vk&c$Kx{{KNkN_mm#O38$f#Kk=v*A!f*d?bR2M2|qfRLD&0KcH57(X+#
zADC#E*lC!grF9^v(imxLV}pbO^pKABRuHyiYNV$HX*!^zsHz|%CoC)^1fQszXsjqL
zh3aOVADTb33{KBcj`yuE&Q6RkZ$c>L*2&_++>8ni9RxsrA0OY`{Pd~i^WgaS@Q2pU
z&JQ0tKiK1d!SU~Vy9pZ`8(8e#9`^U2qknjN*Le8&Fc=;lBn^b1Kw>-?9`61%{E7k(
z!Y7X|E-tPvFRm!ah^eW?DG;21g^PuSg^QC@T#$u@4o-XuwZnzc(vne9;!)yLQj!1?
z97>A+P)SNkN(A&ECnF=fB*h^iBZHGc&*5+ivj1MDp(KOBVAt1VG&G1<8~`m}n8ZXx
z1h~ZHB*eMJ#UYg|Ki$n+JS==%T>RXeYpd=TUpu8jy;T9Hz0An5=1_=14;=;$r!zbrrp{L>H;-ui@;DBaL5n<9Q-hl_NqKBp*A}{J})y30>YgWY!nO(%-x*5AV0jY(9j?k
z=)`b?p#T4j&|GYRrGbr!hJlvBeFFmw2k+}YXQ8RZg-gNuZev%mwg`GRcb{QQFa
zJ#5f!|9P%KzI9%{(2Jp=ZUMejP^U9TdO+e@S64STj{txFAV?|;LY$pLLIMN)JUu-;
zq1XHaJ=_C5Jp=s$;=Le^Y<&C;sQbrzHL>AblV0OWKzV2QIM=w>xVkHNBrDgmP$=4p
z3eF1tJvNJC*^h*5#B4X&5dWc$)c>WN(XUW2eZ*NoD6CMLtz19+e`up^{?&R1tA?Z#
z-05vv0dwaW5fR&aWOp|r=qKTf3}U+9tlxp3^CT`S
zDrOk~o92T9qsWOjJSWMWq5F3#zhxRCX_>EY*WoT4het+mkdOH2kQ2i)i
zv)z0=M1Z%CO>%dS*%H-~R@_nC(HEkH@w405Gh5{PQ`&Ws-7b*VRZ~y1rzfs{csPFDRoY@73
z>yH6%s>kz%uHWLnQRQc5Ha$|v#|zuw+lIs0dWB}9iTkBNn&M;SG7X8jp|HxI?(WFY
zKl#YL+}PNWeWIVG%fsj%kXWKv<(gQnN6DED2?cv>GmWLC{Y6ctmx}%we{pev3h7@O
zH}NyoY#)n5s{5MzzC2W?RJUWzO-&<;efPGJ&fqN8?AbH3ztz>%5;1;}S(91v-Q+eU
z%LZXR(C+#7?-{|#pn}=42&CQy5d2czuP77;6Z&dgY`mwlN4jTQ(kPUWFvUo;rNs0D
zIOyouvSl)7!pGp5xg=1rb=2D08ZMTT6S-m-mHIsy2nkw0iB*F|!5|+42X1a+;-9n4
z_^}e{1fEel>{>CLU?)e>JQ$C0CQrPrZRX{KEh(w#RlR7czsJd!8#i9vPz9o*sj6Sv
z7L1K)zl0h)I@0g^f3X2Mlao{k#3Uj)_!l64W}eK&1(i=C3-^dc{q#(HopW=u9y;>o
zX_FrwI=h2OvI?7hW&onQ*#a*w%XK846qhR_hlTEn*t2FNBn7o6-KJUBZ$yCK2%!P)U1
zje`S(6&1iCurP=oJdH_=X~u7E{#H~oP*YRsX`wuRj)QZKv)n7e^gPn29gJqKh;KR?
zIW{+MQA|;|9izswpq67ag-hiwE?ExEEW7v`yH3sJy!0LY75y@X-)J~?gMxHE?&iS-
z1T5U3i}>;J%1X6C>HyCNwp76UZFE`q_|2zZ5xWkr62DN{!;4hE{3a@{)z^Q;S4RK`
zQ+FNVW0{$jmgVYZ*@sR3IeeA?UN=a;(zjM6pWx@HyIe3Q5v#YQ7
zs*m)hrG57DB}eSMuUst0Rkv>ab#KRY?~5BRt|21{j0z6ui&glp;WN)M^6Z)U$VmE#
zuLf6PN%u3~cSxz5PdTo8=n+fxMR_Nv7YkofeB~%1s8+}YhqkB3T@GqY@Z|pEqx#5d0ATvjd%6M3c4IU
zi4;?sU~o`JPL8;Tg0Y9mgYNaX(@@dS&_`ciXa2?Xf}0jabYQlAwtn?!w?_C65z(yJ
z?Bt|yali*Z3VN>x6zf5C4=F%UP-Q>&7~5}oXN9={(ticB2v+fuuV0-dMvk_(Dc{Qh
z7gmo)Kgi?9<>KV(+foV^n<#Y
z5TOD8*RR{2XlYTGg8EW9IScKtUoq^%1sk}y{-g)bpMNDKZR>4IKMX2}iv=@2EcNw;
zFt(aaAu}PfBjBKDxSk7@O-_n<#`3@{FhT3`j?|O}HP)$<5G4Y-}9if_zxpKZf_`z2`7R
zIVzm2D81jEW$2UHSvw-{UNFTw&hw@42m_&XST(gf%9|3YGx#_;{|J+$*`+<5{Zn
zdWb#o6f%yOEr2wbo~+3Gfo8zDDiEV(PG3$BnJElzZPnEHh=omR6&^HIv76l}eE$c~
zq~tnpTK#!i`%PQ>>8yn+6;d9Zgz?SlbNh
z9D@p|<+0}e4H8w=ijE_#r*v92M?!bA)vQ;n1CxI}DJ8+bxOf(#+*{`$~PAAV3^g~6PP`wq7oIw
zlqYhj`Q-)U;)3Khcw9|lT%1-MZ@3Q4Qr>7E=>kg|KWQ6bD3xGb@ed$as~{z_M%Rl$
zv+$xZLSJXHv$eFwMO3Vxi|$~wwzt6`xz>5jo_YEXX4QKY(H5kF!Hi)L=1WTm505D=
z`Zy|#*3{y;6eg*>G^xC$B?(&16+uG|(As$F!f`%T)YY|ARFqc~=MuglGl7UpZ{tlp
zo|^kZ?`V$U>Aux3m__xJZde3<*dI{e=U=<7eU
zu(h?tXt5}>EG+@d%uK-ZB|&Y$T6e(~H><6O|NcSLK+MczxiM6HrywVwprC_hs;Ev6
z6lPXqm3XE=yrAGNG?7yJiIpZL@|gne3MC3(rMdeSBOM(R21Tf-_UalPE-Yb{5tiTP
zKk{3GKb0xb^6KW7Xt5+aA_F|Gl3l~DD>ytUT<#JdUvQ)fup3^yp;{%y+X%o-(O$M|
z{#-I^5uB@SK0J)f5)z8`2XuwWv0^dI6wI3x#l`jxA8+!V#6O$znb_OY=I`oK82}No
z!^2&cK0a8_x}i^skKa^1D?bUue?WX+-bamy5WuAzuBrsF~%*`yWZH$zhHvWSz
zU%pr2Gg%l%RuHF^R#-=2J|4Kw#
z1N@Nk4iERffCxv#j<;^~zwe)3)aTPSA0)1-s`|XR7*6oe=BxJ=+y~koXbQ$DHpTFE
zhXF5tAzv@a-N^m#dL2VnmQPLh@+!oGm%nr8?<}qmZb^1Ya+ovZ|Fi>@BT7{H`Nf=Q
z<$J0K0mIydKvz^bUO9yJ{8(CIX6C5nzUunZFq-dX@@)S4HsFa2S$=Pz-BN8yO4?HJ
zbc2kH%k()fW5{92;PiCT^crYxZeJTL*u!9+D$8RmEe#|Cl|Aj+q@;4|>&xmL^Ay0&
z;pUIaOX$0uE~Zy_SGq**$KSY-G-9bUr@b8)7w-p_1eeH21l!vkEA&Ph8q_zbN?pFp
z|7cZjOUlY}K%D`=MMv{Z@__XI;YP>Um}7}y^i)m=*jqsIPZ=&^BOs6G2*N}*ezx}t
z3Y$fcu%SgkgfKjrD=e(Bu@URS>m6?i?&kRUMMu{`YQ^mA@}4D*E{@CD%gcaE(iT!u
zn8CCS0J})!F7m5!ubop=D9eu}BdeKWsp8AaPJd_r5#W*yRH2sHCGgRA4GpSRm$&Ap
zZY5u;6JJheF=T1YOZOd;oVT|}=HK!y2@U;Z0l?Z`{o;Q>a-#qbK-=FzgqT^FslAW9!6-67Q|9drKM__Ia1Zq#cV(!4l
z@4~+uW8Xf%1AnTRP@z4$p?#IV<{T;(2I4af-?me8usIEUwi&o{)n3TXZfHn2n$u|2
zMuzQzhK9P;F5?si`))1Lfck)N?R(N#tPnOQs=Ym2Sh%%I9-kzttFN5LRTcV6eR%DoLa4#pPTZ4tA;uvK5ct3o&
zJm^SG?KU`AK;3yV@yka&`|2^Byu1|7y$USI)5_B_SzCr`HT=?8(wNQsi)2TVK3_h2
zdp9@o^mKAUXjxz{tvOK(jaTxuwY6lBZc$2V4F#P9>sLbv+trh;rLy;Mv!b7Tf
zUJRHpFmN!ZwYHjUKn61^dZ7Wj)VjI9_4-*Iee>5GQ=1*pXp!-?*Twy8XJ8_a56f7I
zJ^15DdYF+xif)-AJ$w}vb!EQtqC1C#*B+#>8t;##tiLY%+|SG`X}=oh2(cR{dz0$s
z^&Lsg&1*mnsn-NUT*`^-t(~2P!=D@;7Rhz7QKrwH;bQv^9Z6XLBff)_lml5>TH5+2
zPKR*X@Jk+|$3{F?&<5J4!QoN_YVy_&7ZbQKF~MA$*Lu+O>Gka(H-r
zH~?zL`YT#Wy?H^~hatVJwwWKZib7FBl9*R|qIx(qG|H({Q&VX)%I3GGdT?-b
zbaZ$m5AB>}!QQ*uw+RU=^3mvgh}R?-kQXpz1_7dRgK2|1WpW=l8vyzl@8mv$M-NbI<*nSI-9O2S!IJ6B-k)6LJ><0(i^9hx|$>d3gjy
zqnF*ym!qSM6rGPMFe}Qqux1_;IN!X|s7QQuvgwD>CwZSQ?9#5a9{7`#k
zGtbSZW}181LUnj$t;(+okT~;sdXXE^*REVI^;y~#*LiC=uu)%C9<3JD2qKP_=@bkwwN+(=LFYy?)KwY8WI
zKg`39e=Ca3M2N}0SjCDnH@D6{4VjoA-CzN1tIvOIteMBY$-WncX*$5|>AC6u+>hin
z4UK=2vhwUKxKH)0Q$18hR%TUJTU&n-&$zZO=;_)x@6)H4PE1Tg%HU-&T!gk%U*C(n
zm*hi4lwf56XDzL)QoNmcd})%BP5^4?Sw%&&JT)Rrzn=BjeL!d@QUWEPyZV(cb>L`CGHJ^gUVmo6^^p%EZcm^7X4qv{~pB!MVyA6SJkIsZ^RVCMIDzhEolka>v4i
z)AT6)TLu&cz4B*pn*RLU*w`4OySv7-Z>+q{p7ucd$RC{Gg{!%i8we%C40%oCnBf2QH5uTa>?Oj>h{a=
zgXdfT&dxYFNl0O0F{RDT!t*fRZob1qKZ_3}?YJ;^=jR6_ECR8rs+Yfv2nO_0yYnFy
zn3uP=*O-^T*UH7kHNQ^8)!5i`b}?^ZG2!T#xC0;4l411D?r!}4KD?4jO-~mmK&a-0
zU^qBfe`#!_OK|D{9T=&vscG=d>1%49!!Be-#%774!B_SsnvKoBU14D*pdC)g6X7)(
zhu9bihlYOnvMal4GRR`V$W)ccH@NsZ6J%#wXXkhfrVI{ub(L*|goIq_>Y}03ypfay
zUCI;_4xG;zjg^wuV1jcNiarbtSFb+CdK?1hva1~8FameGXMrSO)D4a4li!|T~0te*y1HA6P5R5VyxSahm??_fQL
zvf@n~R(IK?BrOF5kdYA;P2Y9AvdbqTas@M6OkDhsjr+biW%wP^!9w4@$eFL*9|fL$b!H{Nnc+yHZG7=K;Vf?xze~o
zE3_9r&((5g_)Dax=Tkq-Z+nGASVa~Wd!?k>C>9}o%DKD*q-w_f*4Ea3{P@u;B|X0Y
z2lkTb_;_f3zL~M{1LJKYV4am%l$4y56fP%cVA#?t2fA+d4h=4R)YHVzIj@PD2o
zVJ(e`h6>||h@F|Ak5o`tSWH1wK^a!H9KF1~qoTZng72Had1PZ-WB4*ORmDU_IoO!Q
zukrHo-;k1$l-D($&w$}ixLGr`a
z92pIVAWXWNG2%Yf8o-IG4e`jt6o=y2L@)V
zH*RqANl1`OiEwi>Ffa*ovNAGWIlkO(DSlvwAWA66O-h1a&##MK*4M*$5VNc6JqtEP
zcV}m3duu1m8NYmK|MV&E$&=~|c7fVW|o(iSC^L$Ru5Lk;7{&uY-~{M&dsf_?;_96;DtZI_l@dwZvU
z{=&E2K@5=c5)cx=s^iA_`NsL~#?su{+Bxz0_V0s393uEpMB<$PLH_*<4=?xwm)jj3
zZ11i?Pk49l==k^;^PCiu^n7jg0O#x+wks(p;G+<8Ac+tNd?X$gG714s4v)^yk+gIG
zu<#2D-+%*43bLwl+N`vkQc^O4{6J8UA1-=8*`ZKQC<{xtmz7&sN?Cb|Z)jAMzkgt$
zKM1n5u(Gn$mJ$*Y6_-*$7@Jz@nHiWF$nlH7aNbB;i`GF;PwvJIIVmX-2`M=!_G(*M
zd0AOmTU)^+ZwbFt6r~gur8M;9r0fvjx|E2NHp&bo2S=co_*FSAEd?!YZCN2nNh!&z
za)vPW$E8tK)iX3Sw}rQ~u&}qYhZHY*HGmYkrJkiFymT~ytwn8p0}}&1Js4E$s_CIn
zD05qss+@|6xgCNI{-2Q%O#M-Y9v+6)*6?pehB|sWh9+*DqBvewD3p%Q{3RxqR0BqH
zGYifo<|QV@0tJFaU;%?)L4t)C78MbA3vk08))57)Ma1(3as&(KLNMZebW|C-(MX11
zX;2uBVzrxIK;S4aEZ`s(FisXQ2^J9Si`cXa*wRr%(NZk9Pml{&;Ol1zh@xg;V8Ms+
zqXPE=DdWQL|6l6(-R_4P!J~~+VHaDnw@_(~`67^8krY4vAtwi6&^Vj}Q0C+(ty`}<
z@(3)@O6zlumatm2R{H}bu686+_T?&&SnG%pow~c4$kX@7CFK4;rM0fa?#ve|t$d^J
zq1!cNlTnl|W~`;x7cr6Mc%{ixf9hj*;{aC!pE&1Vktkqk-0AF$R$8Og;={T|yRL{h
zeAw^0H9%yOMh}%%ad5^ujaFLw?uXOpI%HIJ*zQgRsI|IjOhctL(IGbp6yg`Fk;0WC
zJj@OMQ(DIjh_xx4C-Ij5DXl={r_BAXj7Lyu#Xu{qjXeIFn<({+64~w>#B+g(6~OGh
zZ^Konv!I(GL|Vb$Ed5un&L2NE_cU?I(bdN8Pq?wV@+IsX8EZ#P&a=
zRrC-n^Nhs0mY(n}0z)*?n)0Txc=ie-jW`gL+&3|eVDBak34
z?9z6)@;ve%(n{aljrf%X>en>X4~7hw8He|uX&*QTd_TrA*LHR`GX1LVd>ZR-3kuB_
z;bvLAXr(n!$Vief15dGTi+O2cV)%XYUQx0VK+wGMMk}qm#3TQZ*48ri%4mqRY8jmw
z=&P9Sf%}&uOvS%Zbp$KFX?B?x%-)EaXH;2Se;mAWp%ESK57&mHmDYkcB2j<%hodvN
zjz*mTVc%3XA4FO?8EP|r1E{nXv(alk#Fm(M^**~PU
zg-xRffks-Ho~MqJ0IKopI>{d@0~>!bZu28#kZ7dUTXwNxZ1nB*zO;t|K)s;qQ`9>=
zW5a!24T!WJl(gIEs+JjjO4Q7KONrU35AryRPyeU1UL^?(WYch@2;9B&J&zD~aO76y
zw*yaPpwfz+Z14`29KY1`^@+9Re2w`{K_U=ReKI%s6AyH_JoA4X9VdK!-{_K<%pQ%j
ze#O~GJck`egZE9*x4=Vz0B_8Z@Z5X!ViAu#UChcR_i1TE{voY>R9733V2$9u0R+#@oTJJpi)CIKa-JPyNrBxckJH}|@2^wilAW7eXNGlHO$JLh%
zfaK1WhlP6q_#OCeIfVoF8yabS?{pdC?Oid(^p12C=<1Tv$1mCMKaXC0{(Jx2?7aVz
zdv1O314WBht3g!~wLqdtqNVs``{Co2NfysRWqq&2Mfx3&(b~Xe6?d(2p+~^(Mrg+S
zFAEbS+i8Jgvy4aYK1Dy5SZp`S)(DmhpV+?uG*D?hE}W=^O6$eKM9&o=jrH}8`iZv_
zHzz78dLGDTR?K*2^LVZUh-`G*n`C$vN=wl8^RA+qDQjw7n^9o}kL(%yW
zM+^A#c97g|5UsSn`wwZ&5?Jn>8HGq|9k90#hE_;KIr
zz7<2I^+Yl|!2RN3`6=c~Rrn9lr%utNPr0Xs`sRFo!l3FVR9cY*p{rp$f+LpN)TG#n
zqeN3KqECxyO(D_>(xB36qo(|nzbfb1+>X<;w_|i;7h@QXJOUUNyS49#0aRL7%e?yj
zw2iA8v`H(xP0O>t!LRwKZ0}_DHrxT>DWF*yAJdL8O_pW}rHjFFRdRi0Zv$`XjU42TjB4$|ABsemSWafyrDD@UEJ%s=e*B
zvw5MjPtIEq24t#RFl|a;g)ycKki1q!BdsIm-}Q%8!{Y5V`{<{&mIs4UilNdv3M)&x
z^!UGY|B$n-dAsc$|2qkyO2CS^`{i_3szw?bt$rTFudBldq0#ygZL}KKR)e~_?ffwD
z(VOIt;e`P-TCsNoq0xHusqg2|^!r;vrl64hjeaT%M&(z!M5m9=nnE`NCPp*vHHa&I
zKKY5t5|ILEq*XJv!<4Wsh$p;)k&hXlIvT$5Fr(;NogLIGWwK&we)AIql?sGV>+knSp_d
z0eD%T8o%3I3B^)~w5lDY%J;yI;~_^2w*Ir1PIW~_|X?dW(5w|ltz0Aw=7gx?u4K_jiuXuU0CT=VRPX@3JWT8;VCmso|s
z7#e9+oz#pMWvnFo{#<^Yw|IwRhuiaK@BlPgdu&0u0~%=^*qK`@EtN%vbEEtl8c=0i
zIGyL>{ZoXei09HjXGc7`Af+<
zNlI7~hr=c4aHl|-H6b@=q;x~)5ARGbAXc}36KHT`%DYi5D0h-tdR|x!jn<~NQVz7y
z`nnW2T<8ks=FZTSzAVKn1%O;2wY2x$l6l*Dp9@YDG+IGvsX<%6!Jj^U{pdNuD&{u~
zKYz-FQ6YZMnUshc9@E6)heD)PJTa@WQCo@|dXTm#vnv40)
zJJY*&IieqzJ_Go4HpQA%R+ASelW3zg`9Gs|C%kW-xc14*Kg{6t?b}D~?V;D+xQ&>h
zjn*a||I*Rbl8>}|>^6S)hgksqg&`eDnA>YcUd9H&e@1H(G+JAR4ZNL4xt$fy09Xo<
z3jsG{q(13cUVL}_&uCrwXS9mQwH=%Q0cNz(I{um?*D=ROUlJOvWF8)K&}bb9o1PX_
zR0P_SI`ZD$;m~L;h<+~!jaH?#m_5R=k5_Xsawu~54S>$iRuSb-N%_!db!X2!q4V(N
zzhf68jOcz__Q3p0?hcT)heoR>G+O(c?Q&!8L!Vuih6Icp>
zMk`MCFOH>Y@fXg2Huj;>y7$f*<)I25WP@0^bos
z{`zOMIzB!)?}0{Z$m0%7tC3ufQ#@fYEWvDCW6pxW#;GwFFC$?%(ST?=irenUc|
z_1gSWnsjnp;
z$k9pe6ucx?j%O_40;FGhDpa7+Dk<@n+7pqPmb17!L2-M#LHAs7=?M#tBp`H97!oQt
zVx`&X9w9ln+_Csav=YKBCm&6;t^*5de974z9m~(1*P%1ibSM1|B`un0tw@w{uQ*A}
z1?)3Hulj^dupfH5yR^$VoI#>>?Y7{9r@s_7S$;FnC4!WomkTk%7242fMH8*|lH}wO
z;RRFVVKRkR#l^U2LFX?zB=vh}v=ZtfM_|VhpG(%pMf<`yi6CXVAY8Q#WHtvByRLE*dO>4jHb
zT%)|#6G;R{h3kd61Fu|0R!gKfeP9DFsaB9^6-#mO{%OrfoP6mft3Ke65@}#AmS@^`
zQ27{O?y8Uk+voq7F%*Gucy(P`nlfq2z8cBQn(&pF%iJv>?l(I284|4}^nP7vp;g;Y
zLz(QdzP|C;Pt}jfZxoDPQbzcxxcVp79CZ1+F+ic!&rh5Fp7!nOtEB2OgP#DxPjPfw
zCokJ5%J=HPB&uTDa0CjicYB8T*CPg&cxMUuUO7i%IVVG*)#E>*m34iZTkj(jS`~$XmKL_K@H*M*Kw39iXx+LR
z=V$(YV8084nU~RnvhFW1$>0NZvPdraC$#!Np;aJatb#6_ezN=S2(Y#OY(3&~
z=_-C_i5yemKcUqE3aw9yU1d(Y=Su)U6Ri{x(PNL4>CRZ7(8?G6bPx)yyo-s6J+!ps
z;LTQuot*|GS`{Al^gyBY|)}>?t=e1Bq54qn-C}i%SUYd((2-+>Srjl(q-~OW2Id01PZO{jWxij8?$o~Z-nWjmH`T_QjN@f2WX)+
zEksKhW0yNQ@S5Up!O!L9->VHD3WD=QiO(qmt*Pze}e*ZQ`$jg5us*zf|bl<&n
zz6yobv|dIiw65S_4d~$m4X67UR=O~e)VLS8_n**;Nij3IMoqIQ1%=ja@Z+BPKcW>g
zf(hfF(5m5t;ax*2wQk^I_9A^m7k1}{1R8nKL@QRQi=S`EIgRUqBJIoEjfclahra}0
z(A&{MqILKm(Mn;KSJvHi4u#gC%k*A@tjFjHvznT{y$4Kbu6qV5$xY|d@=1w_OXpfs
z&}Z$*@9u>as(t;l!m5H)xM>yiS=S(7e2LT5b$P+&Ha4cm_aew1g#S|f{SRmj2(m@M
zl4J2DdGY3X1N2!No@}(NumF+)xd*p6+=Kl5I{*2sO2HwSe_D9n0bm*%zoBz*p6qnUhVN{WZlf
zB{hi^`>C~T2(kwPtx#ya0fAP)z#vN~v%o4Ti2yK;4U&Z!_u@}k(mj&s&@jC0_H6Kf
zLaQj*{YB(hpVA;p9iQe^%>s=E^9=>&bl`ns`wR-LX|m#MY$pv3c^qqKp!L(YAlX~m
zQxIqc?e#3xDEV9uz4Uaen`*LOyio&MnBv-^cM?U>Kr0{kPiQ@fAv=UZ>p!5C5$2}X
zqS@cF=c0jD5I7%+7FwHnxa0@qNK+IIAkb=2Q&0$(BbSUrpcVA`(4vLbOKj35gftpx
zmFsZn@TI9$nY}m6kAJiR{t2xB3awL6XpO3+JLg0Lt&8lysV-@YR_Ov{R+K)$ScgKZ
zljr%l2@z4woNxEsoEHRIy}XD`($c};;ZL!VuQPvgoZck)I<QjfP^m%@`WYP!gO7!7_HQ~a2E
zYK+Rtni%gx1FecTuHSfOT(O*Fvx){?{OY|D5j=KsAdF4YVR@D47W%&^kN2
zu>{s;XV+%`pU}FC23mIz5NN$}1;Iy;yoyAEIow06QxbfbfnQa@XjgPt?mz)*liw0V~jNMFCwH2>vh{ytQSt(gnmH&iRcXwxRZYf+~tb)SrtisSjtGtXtprp2(z9PSZ4jO2MjY>KJJ^TTYefIs+tludza*RYaQ;
z3av7t{Bp9?g0Mx&%lj{(&oHYi?#nw*yiT?R^}kl
zI=9uoz73()tqlaE1Tl{gdwa)!q0kB-(Ym{Paq*96-CH?3ffdNp3qmaX9Z0l7p_LI(
zLJ}C4jt+@MgMWHBbF{pSakPGb0Y3ugSjPv*jV&AmBw7*34*30#Zap2G!+*fiBTOFe
zCjug3Bpxmq#>OS)(VwIL^I1tK0X73G4<9K9I2T8-q|j$&M*><}
z4qjd%LUsax23ldC@%Q!`+GnLc|4s9Ukxzh}oEC|Pl(?iJ1xS8Uf|008WfWl_MgvM1bxY~EE*Y^=_wn@$w@$;RYk?f8O^i$`}+C@1ch1#120=kO-(h;e?F@U
z+GoA4AgAr&fijm9mX*CJrv$Vtoh-Fowf^U`+88=nI%!*ISh%2hR%ghw!q1$igP*^%
zi#7`StPTz?u(_$~<>di=Rx1-<0~G3tsi`fx0tq|0ZV+ho3U&y&?cii?=<8wbZ)oQM
zb7g-t(CQ9LFhqthRCn*h-10z2Qre{h#u4+fg)+c{$T|pEkGPfxdtX(A>j^`KPP$qYz}|ZWmep}^YPG5(gM4KqB@&Uh
znz{a_eNfJ48q`)IexoU&1U#wZDb@G+dAxvndLtgDVzgvcSqf
zMRs!er7U|eqJ`8Ys(b69w^t7Gye9f+LBYIzyAM3lE1TLPX-WI?(W?p$Qy-=5y#l5`
zcT{^HQ0aS<%ys~a5Gq}qg;BRc9G~iyd7omJ`uhac47;BEub&F7VKr3)4>omL56Ar6
zjkKIun!k57=YD=4Y5S$o=)wC_`Jp3~H`potfG0-H7%eTJLQYwDTreUsJG@%&?S`|7
zyxV~4uLL*S*M`eP_{l`qs=5*-v59c~8QeyEJC*kLDk-u!odrP$ojT(}Xy4D!nca@l
zvw;p;R|(z&Zz?K$#_st>U3q$d<(Mzx-I~~#;b-LollTZ&=f1tSkxy*Rn?T(@IhsRtxG$$sf87Y^7<>!t
zPOH7qtny6R;@a!LcFx8>j2G9irHxml2#JbffZRpIOXC@C*PG?e+GGU!Hx?T91v+&2
z>R6u_t5poKOMnuW3hhWGfg%)oKg5rO@3;&Hk&(IdemV!JYK?
zE`@9!_&9s`!(*E&+O>6W>k@SY8Ho
zFRBY$V;_C7CF9>bFtxfY?<2{B`mP}MEyFE}|Z|Pm%
z9uoRUbt;r-fbm|jq&opG)k`+&a^s!N4*zp-J{;;JoFUpdm>w~t>Pu-Y(rACJ_Op`w
zpcxg`n2YLLuv8?pz!1am{U;8#JH1%0cz-!|yPa6xvHqL8852X&+kklQs)cpFS#-tg
zp{b|3xEe$UW4}`If5<(`98L&Ji^&5c`hS=>!Xjzj)|t-um>!slZcRQFH2*@^>Yy{n
z=8_T&npZIGXi3ruRC-sKg$j2}FZ0m{n
z+XUGr6!jack(a@%RqNT~)<6MnByrySR`PK&6*U9jT>kz^i-Y(xNQjq26z??Tt^&Wn
zJZ1rUgz!pn6`uWPuC7EYnyDWT+RX-foCRC@&qAndS91J{Pb&QC)qDnI~Px*Hn)GE5@in{(FZvSvllw
z{B@=!N>fB2TbeBsLpHK?Jr9HY0oUAO9y0m!xf)~JV6f^Qp1d;^c;Xd;)DL8NSj8aF
zxJrD46swHc9a>1?n(6fGA3YHq1elr3`poZMt=AXsA8^a)W9+yud5IRNlKN9W}DU8-~IFx0>}UIuU4w65aEM48^BD3l3r8#DgO{o;jL-^tnjq>vP-A!#d)T5eRF
zrsS>Nw#UgmI$?^#&!qOaEn-&vZRYe8box29cjhsejk-n?UW}h_E|a%&+T2+xUL;E9
z;(fL&1Ik@`y|_DVkRO+b|VJ
zs!m&e{pUc;W&a1p#i!z*jIkntO^ygX-e3JF^~P%vzHbj&;-aIUaJFF9vWSylVklEZ
z0+!YvPEN!M4w{Sm!CDn(DF-_AR3z&Lx|ei)vpMM&=Ro$tRK{)Xu#>=OS(N;tzBcFY
z=I-W+*1xvwuDQY|jQ4=IBXX`*TKgz@x#2brbFTGaFXHWV9Xk#8_a|E33
z3;e#@lr6UCbv7ZqN|BtvbuqMOhJDvU0L@E+5?QLfcmA<3*NM2(=o?G1+)cI|MIPFsg0z+3XAHmwpXgI>EK#4N!ne*Cd
z@P}8!vnFw=RD$4f#wVYeuiN9tGD%;bI%FL7OFde@o;6f7tF(1BG_$e;INp>mEsCCg
zDptm#^^|#ySn1{+1H(>hv*a*wE_|1dn!vYp5ved!wdGtvibeT}DujBMEd0#_7NT+F
z+#Z?cJy=-g%8WW!rB2v+tDA+9&KbvY?NjR?kbM$`AG`k*aV-Ws`Fs0s5_KGF*>a9e
zrRXZh982w=vkB_%bw|-prj$bVAg?I!^{4i_xKTREhy|61GLL7|wBvi0^0rP+5vvQgQ}-Q)3(rm27ZeUStXvYd~HOoK-;7uHlMfQHLM
z-l#vLW)C}*U$TmtVU@lB2{i^%N{ly&2*>IHV|UkPLz*MV3LXp7Xh?w~e-ht$RK*DVL1@Iam$=j$JOWMHO{`B&z@bop5*nPu;
zoCxkqva;jF*tg*H%2GF{aM~xb5`2QDC>u@AfTCvg5B+Zu4AZk0d|k_+?}_`WAg4gx
z#HzX~bMxXzY~_)m)&f3X;oXN*N8ElEAmc^VFI7KXc5l*b**VF~N3{erZY>>u8@rB3
zA1W~_G=p;v={IZo+CRL#BkxOe;@mB1@V>4dR_lz3z3|EEu}eLuyB+fA-7}MwmCdkR
z>`z^sra}vosgctS1kVpox$-q`0b3S{{jAAf{Lf9uWY=8GN`tDsgt+8%zBF6M4yP(G
z?11+f-U7n(BW5*BhapEFHF)1ecl#3gnh_X1;$0<{W^n=1|DJn6e@E{#V$?(te%(zNS1q(>-8XYk++@W)e`BX!eI
zHepkz!2g;XB2V42A~5v0rS8$bm8HT_D|leuu~pIWQrsmn;*aj?*Yz~*SGKRW&9$P~
zn(Pld)F^?>NTH?BK1<(xnaPvU&7D?NpSv2P#a=RZrt$_23vWq+oD4b^(QZ+m(u=mF
z%rBXU>&#w@BDuUPE*TjWjAq?(AY2~r=R-QAHgChbQRZ5YEe8I>h|PQDS(9HJ5^Y)8
zCqYU)W31LYX=mfV*ZikStn0odmi;K=#$wejbIfAw?a~?W!KVs&2gRH3_yH4%AMUPeSA)0uudc`16JR7mT{s
zIs;Sk*)bbNluv*`L=z29_UxR=&0`CZyas|B#CAMjLCkR=o{T^#@huKm0_U#QlA~OX
z&rPf(rn0}!^BUP|eDK=&JG|5pURLY?8eQt*X##xthsNADS)QyqDVO$Fm)l+rR%&OP
z)t=FJ14QOLKL3|Ry-J_+(v|wYMg72|>P39)F(;nA7CYD8I`B&BE}o3XRmVc7E^67Q
z8gq`mkE6&V#dKCUDZ}z(2bMvndZ3Iww$I5@O%Rz2#iYv>)>rm?%W+q_dSD(8H7MqWGrl5
zTz|WvaH~##X#__&V+eo4ZolxT3irA1&(L)c%tO67G35!wf2fByj*dn!YLqOTwkF!U(S?0i(+OM0Jx6EV2ItWonEn@!x77u-LXhzpRL3(u8v
zSnbWR&*?wN){RaJZ|8G$>I&;Nz6PqxoH@(MCNx~z@CSBAiI>ldLFNmhYPM4`s*Z-lIgT}uoF8;X8w8r{mutE7ei`
zKb?+vF3Ozquoaai@MAv>dEvYXGVQ%0G#`6|-J|lm4A}mmBR#FkX0QA7I({mL8EQz2
z@qwW;N7FC!R^hZ^)kI(!_S4~6EI-Ru0*9h!MB;A&na}-1QW6{mj}&4JEg#{5oCQ46
z-xmE1;>99oHJ<4^W%0)*D)^^%{c;9NljJxfwOO%s@
z;XJ1L8AdNn|5ebKRErY7p@7|2uuUpXcO+Z`#OVa*bU1duR+pL8Zw>?6Eh7TXfWg@E
zFV*o3bG~?&IRW9^2z}r9RO`bSuj{lR5BDw(HOoTH-npcgV;1bVd`N@BZ*~Da-~J*iL2lBgu{@3h~B$?+J@{v)Ne+O}!AOLNXQWrM6aq
zfr(FA&B1qtG8ZmI%NZt{m8C@T4P&Gyr=)!8@SFp#wkx!@EC_Th`%3y
zn|Lm(a!VPNeta5@^Gb)t<^ipz@&G9axKfZ%y;qC%@b%%J{Id`8NP+^@g2?DXJSrzU
z#{hPBP{7@K;r%?4Qa*M?7>oU`nJP|ncx9^jq{^~-_A2!N9cU>E&YwxLyId}J;>#Jg
zck3>pD+sKV_HB(QsQ=zmVgmlQgnZW0oNl~57@FDPGj39FTPz3!)YEEkw^L!vb25PQ
zKpq=*;ueLip|LeTMD51L4)FWAL$G?~w5Ok3d@>i{rs=!oL($*&v+^!mK=#+XYNn4C
z_+jd$pFT*{vJZ)ZHyqPfKix5Tv%0TaXGgzY&HlQ7TuMw3DTR^uh-mv4H|WzF?Uu1-
zyk`=XTNYG$Cs~8hld7Y|v5M8^fyhB4UlB0>>6XW;`bO%S2+=G@Kuu^){B!|tzb!rk
z#R`5nUv~f~C%zG*;k~TOct^bE7=pSpU}}RR$M8mrM+*>vi@>z2*dx~!*`E?X_!*ul#oPxkMgV67YP@N
z&n#qvTpngcu6O$5B#3530<6}z2WGo>$VOWCGhc0OS6!=g(R$`9Stj>@j)-LArw-WA
z4!I@IV@e$Pe)5wTn_cWw)2*H9cH(*a5rcyKjzDSvOq3L72|8TF9AbAOPBc`XZ}^Q*`R1BR0#rj~3YnQ=vo4uq#ZSBXn0^a}$;;1N#?>T@3au
zx5^V5SM}mo?~w^)1Z)!{lU}yXHCg>kWdYfRe!1f-X+2H8jK6ONA72fKeWLo&1ku7Vy2K%wV)NzIt0YdprhL60NOGFC@t@t3vyG8QJQsSR`yG)NJ0fM*Nfz8s`ZHgHjVA!^92m{@Q2^
zCwbOCE^~0tc~nDx{qsx~U3$j2GjMscPs#Wm>zR~hOH8`yyKnxuBTxJO24Rl35?nn2z41hT3`f#-NKJ>7TeL{m
zMeqwRD~~jX=Edms@2ZUNLHu}C`E7wU_hgxC7sp*r#usS`@-HH*uodC8pLr$})04~5`0nHwd5Vu(7&kz0mmN*Q&Q_Cy8mWU!1%0DH8)0H>
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
M0Koj;+GPBH05iAmr~m)}

literal 0
HcmV?d00001

diff --git a/tests/testdata/XRP_ETH-trades.h5 b/tests/testdata/XRP_ETH-trades.h5
new file mode 100644
index 0000000000000000000000000000000000000000..c13789e2acc9f44a81dbcc5907a7050fc3b933c7
GIT binary patch
literal 310513
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

T;^Q^qt>Gk#(5KGYRvJ&9zjaUv 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%0AdKwn?Z%@4G@`=UNuYIClBntX=}?+m{x-Wg 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

    p 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 42Oawj1bj`}UciI)yUx96!Ft_&el5e!G%N>J$JI z2lqd9rbB%Bw(was7=n!i$h-zWfaZL@g3FFyvd1j znwzsgc~auiva`!tU$|bl#4jw4J^;KPiei}5QM9#9LD%~5i2ccTn_-Dnk}<)>CvPU9 zci8D0m!Vl~Y|)nDin{0ZEigII+0`=;cn^~3wY>e&cRuG?SOZaoRHSsjbRHpg83ODAq;(mKb!e&yVDLgd7gLICkE|NOb8Vg$fF6WHJ> z_y`*&$B;u1dm1c0!dV!^#ZKqFT-_~;J59t<{wb%W1$2KG*%Bbn<+FY{U0fZ?1Uob+cP2+#TR!08JN z47_~h8ijH_1Or!q&~RvIz=<~s&Tni)z!ej-LFa>oEyzqFv_8mfGQyRVlmxpNY&0;b zY4_l6=`dJBGH3{rNkVX058&V-9pTN+$$gxc4;d{gDu$vVP+a&nboy$ zTqk>lP!A1!0|P-gnp;|7>g}ItsLb^vXW?Ewy(9$bqaZ>s_xSQ9O!&NhGxm04N^|Nx zv{Gv-5b0Lp`PX?b~Nb#3i8eBU4NcwfhmSSb!14)!G_ z5CG7}N=ER&=p-Yo92{KS+yEejTR`pM1cfk!Sy)(jlZZGOVc)!2V#^jxN?LmBM)qYe zq<+2S3L*!x+sFtL1aB$O5N1V1B_$(W_lH=y;qxlzr)6T8O*E-tYYglVIaNr*HRNrHTB)Gq}=sE}t|>Kz6r zf~3L64N%}_1ZiZf4{dxEJo{D=@W?T;Tdo1P8-zP;COV6xs+GVJ6WKIvK$bRtklL zkdPq+bcz7HGkI-H24jcG8ai?Wj*o&VV?1~Y z86nGnc2XFWN=yJ*!HiiEB-a)p=jLK6%w&X4(xzj3@Dv0isX+oL$UZoS5HM{L24BoX zsingk3x+KGrXcc|D<8SCl2loVP}&ZF8PRE#2o1qA5QLP(96_(#NK6SE9X=wph$w7- zfZMnlQWS`Q_AyZqN-n0&L|Gss6ks7@2m-TK+;rsALxiqPqR=6O0E@%9L&ouR23*NG zhET{0NZ$gZ?*LpPB~KzC5g6o#K%qd(1Ny#zAT%iuQ&`2w4$axB#sR*H{VUkRc zDu&cPrg)@JTZ)T`l$)vzvv`OwhL9-~nmy9bh>fMuV<`-Gl0salln;~A#_ob1g1$5g z03Rv76olS(k=|B0UP3|0?)W}(Af|_JqmwaaJcWi}1R;bZPob#s(P%$>NeHbkG3Pyl z0o8_*>BnD^+lr7jC^wR48`9QBL+B$2{m~PI;)?hJZ<~U&<&zkQe4cNhuO|Z-jPgkE z2xV_Bf*2r38B~{aDH*|&fwad46?u;?N~0~%F=43KKr-YQV@2rFML0&E^aKqdFc2sV z5*Jf0h4Bz$!&Ca8&bH!l2p`4-<;99skP&eT8G&5UQpt2}&@o>qtU!6&Bm@+%h$O)t z4{jC+e~_3@KOcBAJ_Q)FZiH5EinU4^KoCL=@krN&;j zusEWpMwYV;lt!u6^2Qo$@*Xl^kwj~0g(Iiqtra9BLdg=I(lRQCJy736JLHtq&Xc9d zCK3|1ByUXVunH!TYGLGTOd*?_Q&3wuYjf+H;it*ea5bW}7Z%|W8+bO7OjdHWgT-A7 z4bD*mM5^^e5|QY9hPWYudYMOCn{NQ}G>tc2 zHhxvq%&n}0c9E%S)+vVQg$qhVYc)t8Y#Vs_x~0mE3q%@K-95r3<${?3h@firMsa(U z)T~`yc1Z8kQ=`g|Z$_vDMFa-jC9)~0t6`|pSq%*tK1(&KiVUBOjOhV;>W#fJkVF-= z^J*#=eN|AkbDjwrD)1d@Y8pgiB3T{0V~oyL(4N^i983-@)?hnBwsSSzAt9}T!QJ*# zfaOQD(8M&!NB3m`B99Cp@&sK!8DA}xK&3!*ha47YVoj7j;LY92|*sWfYMmwmC<)HZ4yb3rNUWU{ZX1j)!l zfqH^+6N#50D@YS10u=%SFbP8m4E1nVfh-+Np->iJ12`&0ZNXShUd4Y8+3pNA+q%jF zefW@i?4en3xc!r|Bx5Jz+cLxyjq}!7uIBbx*3MaR4Gc#e-6zHR z;PvS28+%hyZrXZeo^Y(rl%Wn9XGuQ_v`(qK4b;Jy+^n>yT7N%>sHi;+u?F|4m`aw0 z=A9#vE>M;ls082(O)W8usN{%I{coS5o;z)>KqOlkOPHNhlaM)zlI1Xa;=UxZ{}Brd zGZk$$88sa@b2(q~hMrK(ys0QumdQ2i9eY)X_U6)RzUJmcG7-3!hBA9))c4AmkiW4-w8(^_vuL!*DgKj!sHQ(oS`v3?=3) z)y0);3NR?y&?r9%d_zuW4!`F$26Ik-rTb;UsO2gQfu`)#Xiu} zTOX^*uaoF|cq+W+7FsT>v3jRq+%z`i??>F@;^Ob52inJ+zp5$SX3Cl@(_TRDo-2Ph z`nGj4#bd{zo3_p&RyA7ei^R)PZM-L14y#-@L%A#M6!H%ZkC%Qr#gQJ&@6he)+^;k| ze9`&X&r82*-f*EghmR$)U$9CINa!9&*-zqCusvS@o#0^A$P#^Qb*nY%NhHP|B8{^b z=?S+T!QF+S*glDyGW6pv1vGo>q+?mADYNlVtbUcvia_XIS)RO7g;CfvuGea9R6KWT za;x?#x{cRmeBRyYao!;=vH1nvf|2Lx-(axcuGeTax%=}^7(jmsnVOoSfaA+M)14N6 zi}AVI!nnRYV$NMc)nWJPW@t8e1uyRZbWp=zeY8f-5O=8Z<@t_6%M_1F<$D9Zlkd>a zUaL!twy5Uv58VuoX-4eyAHs?G=DT*OxpaC*%*7Q{Odr2eh^o#UdG|~$I6SLtO3kXD zc5mCzWcT7)&L|fb%B-&+%ZzY5D%;88J*Qn%_bptwVet}aT-Z)YOEP)CCt5zsVb{~1 zb7Oj1Wtu&bL;gZhi+0@33vVZaL|xjQcQ>F;Ke&8Os*Mz2j~|BWk4^nHXk1qy)hB)9 zDm=IIK(g{LH2G6^*GbFFUuxbG=gqENdw={-Fa5l>(LSX`NA+rtz{BXHXV2u1^uCeI z9e8p%*L7yCVp9~awBitNH5avDd0^X4^vA28Zc=Y#v3q3^kEO)U<{Q!UEiUIgW!>rB zx2V8&H4Z)KCE(CW%+L%7*mXA`E5XM8%F!@uN9(My7grQ?iBVrr7XK)sQ}I{4w*Eb@uoLlz7P?Y>^D1F>l8gbFWy#E zLdMFDy1A5HL7xp>A^Mlq_um=%cttK%*09Piv6h89Mm>tS{+M_rN(I#=rin)_cqcuv z8!_t}JkEXO%~o@dsaDb0LWAHWfora)=&IvTxJDjbtJU^wY=qsd`1%9kk%ZvwTKB`w z?3CU25k>DvO%BLj=uLT%mbO3s`@VLcnSQ&`ot2;{q6`*sh%dI-BeT;auei9PrdUVm z95{ua$nvfat&l)ma(#F=^5j=ZGJ?>D0%2PDP6>?s}E$K2BL zO~~2&vEeQ*A-2e~mfaTz5(1S@qk0-m_4dcVf7;viErQ@kdcbCWH_z;~hQ@-2;gJ_p zwGHT|zZ^q}9JEx=C!*YJ`jqIus(Ri`C*G8DUh#?&i!;_kziRkS`|%VHI^O(!Oe4Nz z>;2xqmYJL)&afYpV6lDsD$uDHm0x2oEqz|MHweah?Bwl}_#2~>BHAof<0@xoXG_th zp@BM0zXmgDeSLz6AXT?N=Xo|xP7>S(srxS-vg$?)jCAYxyz*aKS$W00ue$VGtBb3T zeNE|@=O&o&33Vt&H<_(h@lBbTPsIm6dpjB(aydTUH)QJr{#Cte4}n5&k1o5bhl$Ii zrKRQMf-7pXv9U4#>G{{DT=o7(7-<+6u)%ycy?w{Qd7{c(Z*jwjavFS-$P_{QAOnY_axhimFYKI zSLT`U*Ks_Jh6hHN&L1=?*cDDHl0Zo(1fQUrR#x~;P1E=f@VO5A#;#F9|EkO8dLQ0T zFd0L=uk4T0$xlAenS4z*DAn%b=FQtT8y#<``C{yIgnzzR63utc{rktxLE_o1&(_k{ zUn*0JO7ikle-7^+59JZ7aCv}Q-B&pO?R5CI)7m+w-I|3)U+o~NJ}xY5@MrD&!}93p zS2SWM>CV|Y9~F_VE-f~<+_iHhk7t6urTIK~d9oo!?Yt9e`eWTQ$*$|}qKQ?TfNzx$^ecXDskHJ2=e|LYswj7>)dQys4ssX(j zO%u)QDe`g2D(XRth6=oCzmjQok!r}Hp6@qKSe0?Ny_QZ>_>SEdC^}PVjul)ps#YA zDYWzU+szJlOcCZ0j~QS6r(4NCwHau-qQ_VEtT@}Rx460Y9A^s!tZ8X+>Jm-Uo?|^ z{MIqm)V~Bf!j9x~EIxvi%axZ<-#2H}ZFZK$oed6lovNNl*Ry<(s57*_d;8xb_k-H! ztI-^3y-D*!Ynrnb%u*IlrVUtVzrKJPJxWWnzjDCQ@wdcbZK}c1 zdsw8X-%0kv1xW5r7$CsdkMQBcxf4I3VGy1U^-Fi*|IVvV%~U7#-q)KnotgTu<9z8Z z)hxR9{_E)H%Ggtf=CfaaPq(o-8EsHO^pU+0nV@WFZFw0lV$kG_?y9-ETh$FUTrO_< zUNw8kCn#n(EtDxIN8sja=!nnN`)CR+ac#&X$GR)1zSJR@0L?D$z#k_F!)_UqRV)MtdiG{Y^CJVwxm}I$Tsbz2q`+;u}S5GBU%OdCwLnPK!BC%3Rc) z_r!tN5R_H)-8*dADO=h8Qg*3(kWy0C4`r9UH0H@*9cH0V2R+e{9H;YHr)6cie*aeF zT7B=N?KztDSfTgqk;d4zk6PDHqOlw&#vEBrk=raLLh3fT<`xwx7ZH4!@1*{E6ne>o zB!xDzy`)+W`|Zh)mNI)Nrt(@l^zu!=%sXdCy7Gml+Dhq~BGT?nMzgucSR$EA}} zvf>%hE`o|7$hmc4vw{5-|gRU*Mv zeC>p`()g;eoUu46f)g%J8=uH1JTWnzK4Hk#BD^hG*i=8Gb@#Y<_#s*>m)xN5-*I6z z3y2(QHHL;^cU@^ar;$oVhTunqN3Z+MUmQGk3>EYDSN$%RFM3{17`<>>c>IK1mqF3# z3gMmx=9>FnqEB@(bBBrXcFl3&32QWMyi|4~Fv*IG`Yk)kCH4>!+3n>S7 z+_Rftl~R58GgeAUO1z_N_71wX@crPTq>>q-a6wxECB+sk+{~JH-CdwS`fJe275(n0!ofL#6V=BDy8g<)F+^Q% zX=*-?Z~8oO93OLYI&IH@-KXhwVeZ*A|K%glwYdZh!}$2tY;GEYW>HM5h03p$`bPFw zU~!#RS=Vf?YA=X(7=9eo-!T~2jJ`R1c_;EmM(SmFxP54svBQkMOK(T(1dA7A{R2j(gls$HwdpKZNA;e9p*2P`1)ng)!1 z)%@DAyqeT5F#r6PA=~AuQuNr7q!Ww&fe}GP-UW`&x8&;CMxL#(N@e5v*=Ky<0J#Uv z5&tahll3$()FjjJ8^0cV*vQt|tr^>8xS15d=lJ~-^lTW1QMzm#eQnJvq{+<2E^Akl z0I3Kj?bQZzruF>t(7wlW7k7nC+9)>pF5%)QHoiMyc_8w~wVi0|YPxZ{ zq^7IN&xynD?Bf(2@wD=*2@+v^UZ${m;AD#aj@nz=*WKOyYFB!CdM8ZbYn|SHc@NvQ z?IAO{%5usj=*@XfKC@&W2Z$Ll{vNfuB3H&bkl+;1w6?R@I^2U(4(&dtqfv8cM_kFr z)2Eu;o*kyGq_zg6?g(Ig{4gYBEB*o6Yj5v8OZz2Pz4A-;pg2|FYn@25kJPADWUY8E zr-p0;dOh+iC!{z@P0c!09^UXIhp37kHhsoMxEH+*qHx0M%&w=^Y0`Jy z+0OfoKstM_){!FvUAiV=x@7U)I!|oAHoCL5clWc0#gm)`Qi~F#y}y3e1fa1m9A;8_ zrDXCFPi+Zx?4{AlV`()y{4WiK2iRKJWHZqj(Mc?HX7*y(Olb9t=HI`fm2#q9WPJ@< zXwBa?i##zF9!>jDQG-O7Y}NRih!Id4dw1LIJ1w)bbDdLD=I6^O`bUpSzCU^Lru+t*`qz#e zxOE4+;?OU&e9khz&u}rcWq1pEm@RfQo4PSYDdv{0R@Wj+=|s}>tN2X!SzadnfX+mH z^qXgK#3$86UQ6jGPe%Lughwuwerb+d`3vfh5@eobjm~ZkkTH04ltxn)p#A76RZ`+B zageDfkSyK*=yPRvOD?)#t~u=@y__H+zelfc$!uV#mSn-#&Ii9QgW1m7uqa-v8$BxlA;i1Ri z-1^G)Tggn~uQ2J)e5dy^rcTtT~c07wCI;&3;K~G?@p4w65{`iqL(L* zVl%(%Nm62f8?86t|&l zRD}jh8=75q_AWG@aOn1@?VEMBKD_smW!7fSL?Hp~B`%Y;=J0F5KJ~xRRBG+RhQW{{ z50jE+od(xBfAKMgHhYT;O!bC>n@2?hNQqaLG$!jFrGGgn1LMb*%~_X1z^X|S-c96SJ% zOS!N+Svi^0{waTw)YrL6HFFDqSMShPaO<9p3keC4oZa6U)^qlNbHi1wOQ~C#YZ^7( zP`#yu-uyFE1?L`0B~$3f)>VnA%_~1Q*ZurjXQK1gKL*`K+pE*AH+|d)7ze-RhONV+S~m72U=S{5A@e5M=)|QmJ@+JKiVQD=zj~!B?Fh;hp(M=T2DC^AmwXM3TK`U z7ENw!i4WoYl1v#wcdc1j_EkwNRZ$}rbtKQYy9c#}571~=pVc$8c5KfHM@`p0@96Ec z>nk{RY_pP>UQKfTh5ElK-_GYa{XJ-`9iNV}*Q{MbD^{aar!~#=yxwe2I>+teb5&hD zzA#4V*wT3|RNKWP!Dxq(N>}8kpfLkyUUl^j5%qn(zgr|4ez(79uRu?9mwcNwNRAIq zN;deGTwPI8Vw+vE_k*Fkm%%=E*7tHOyMhy#3`zpAYxraCWau$?oxf%GnA;{kUV(F* z7H@qQzgp44PYIL&$ArR%&iLQ@}-G$9mDN zqC%IiYrQsp^R~Y80Lt;~v~niiqNl*itGlQ8^pzWVQ!3T-4!k}jrE7w_Z$+a4635pJ zuIhhW@Ru`|{KHe}e#b*qVe@M@PetK(l5_db(QD^LM52M$lKPV zo>6B;ylB#QCmsL(-8E%%P9nd<&FB3eS*Nn9g)M$QRp`a~YGL8Y0i(&+UyM!;46v{y zC)uAY;^p<>?T%z=l6{48jK{T;Z^hi0ss30_`rg4m?s24>?{3*Bhm#2(Q_JV zOG$aNKDB4`I z+WKvgXpxfKyP~nl49EdP@uR}tZQDvzinr-rl0gg2Zv~jil$7We6rF7=%C{=7an zv2CIk74xZ%+Dj<8Tbo%zuq(Ndc%{0ZR$b7y{;7koq_W2(f{W8^3L#XN-I)f*4F)s} z9b+>iA?l~^-hEi)#^j_PzeG(+vdC^|@R{U1c|n`A8Wp##CS-E%$m_7Jt|sSmwB4Go z%a$}IzAGV+_zBhO%jjOh46)?SZHceuu16fAcH_EUxz9Lw$H@}pA4fXQ&)-A)$%W+o;sPA1=m=MNlkCZ3vSuh8c`#J6ZJ_~kq*My+6Y9Q8lv z=wVL8B+ldne9So(GfwRj+Vm(W*nN{AYW>s5>Z^t9=*1m7?1{@_Yu=K7`nr$3yDPdo z3iG@h+<;bRU*AOx^}7>(bE#;_Zn=1UzO`1&mZ}(aVrhNpvZxGdj!jIAdb#Q^ZC^Ix z=a0DV_+IL}A!p|&JIqcB0!iLO!|>>l$T;`_qu%2;{W7??9G!xFZkZM#yHtZ7!T#IV}W5rYG1$;maXZS~pc_%mEwoO>$O zJ@qa%Ej2YRuKhLs=WJzVeQs)dLgQ34imyp5PWMY3jE#tmNUM!bdfvBMlLTRt?O{XF zvDCUj^z}1Zd~)i$_UQJ~u&~C*FC&tlQzOFg6-7NIGwl@>vuI;NO-0SO#lCj@Vrp9L zaK!h)pHr{Cx3?$vHw{9FS~^-&S+cs2cE1AHflG5M{qEGXw2Cr(%f`9X&a{a&v@&@q z!oB?!e&*v0p4zcESF_$X`Eengnnnegj8~~BHLa8vJrlsel5J874mTD@1b61g3yz#Q}OHHk)uKD<&9iLi*D}XrmFKe7 zRjGJtY+!8cvyx`~+@n}XA~ltfN}YU7MN>zIex+04J5p2A(@N6X@1~}v7L7=;WAv6X zWO8#l+i-hR|1(-xG!>RElZ$78$d^`;5s}f}P{;+KyY{K6inWJhv9Iq_r;?{oT0+EB zX-!)6@SB(AVKv`Vo1}Tt_#;vW-=+<|OzTd^qtXJY3#An`6?0T-X-)b_8a{P!-My1g zGtgW3b2>GZiloBQ<;>mo_Sdh&9v2NJ;ag`H-B;tDrBh) zH#1`!-ME6ENwqCbNTj~aX>S_F^VP}>p}f5KM&9yIi_OC5dU1F0pSUz!I-{1WzoMC| zwuftlTcQ!SwG)R^+}hhxTV7sMGWq#$>b+WA!xsoYES!OMA*^w!ssHtKf4}r}E!>k| z5WKmlSz*a(sh>Zxi+skRtgmpjWuKS$<{#m3-x`P5T3^Vp#o_lBbHxZu)#A4PL6O=e zK`z1%xp!kNKYrvWt~}wuM^L%aGYRzXz0`<^bTo__H&9+35s{YsA_D*JPbYq~uOca} zqq-!lye%~>t^sXNh-gkLT<>^SUs+jB%}R|+eK=5&@VXr$Of&K2uka{TR_SyN?GZJt zxgt7s1Zt)u4d|5xagXBeK8h=EM@Rbm3On)b13%JwKa6H}HsY3e@#(2G)eCVUi|^mX z#i97S-)3GGET-ZgKE%zf-@ls{H?uT?@9l4|c@a1E=XERkqVsn@@(URW6WSTXe}A)@Twlh-(WX?0=#z2w|S7>#4K4kR?SCeQhvukk)%Yd8!EZy%dJH~T?_{uw!wZul zyzpx?1UCM{E&(rsstlHYPVz%`^lvV&L`lvgo1|2#aH|2!SMnMVWy|7|2lu z!4h-CaF`3(m4SGX&XWSMAd)y1ifv^uM>1Hb6om8ylV>m~U^}s0Iq00@yYrJ>fq^`8+m1;vWyP$i z^!~u5qMECzAq$Qw{g{)i3?3kDsD)lOcY>|+bme#bl zSF^Xa)*)X`40a2D{P^%q>8sG+bz}OxrelTY3R}YvF)l&DHI;4i^S$%o_#;PV6`<>- z+xVuSz)TlhP!IO>Y}xX;7DI4ut{8#@gA3A@oi{JP5_o0)49CN>n`d_qyVQL;{SyU2 z_F{Ohy*Yc29tE#uvdtb#4>E$Ao5T>d<>i=|D4UgKG7Uk4g6!<>+6@lEq;AaT6~GRz z!~_Qi$8>y-=-PfJxNC20KXSd zC*(Xb!h8HB?~K|bwh%X{`rB?6n1ux!o0Q}FdYZnzQhHs;UTv2<+PjT_7Q$gFFSU3JPo(rrheR zUy9cC<>szW9REJNx9j_~qa!JV_DYbtumBHOc0*wD{YHO$Hn!v0}C+hQ|)eHdaGE_`+VK`)s>e%qDixK~Rl9e;y%|5p)FY>WT+$_u5t08jEw^fUKAL zIeAD}*lF8wCx>M}EiVHoSbu|l=s2vbc%atS8CS0G4u19}lRpdb6yR|_4}Ea=WqqNz z)bsu^&iY+ooJCwx|7Dy94{jLe!42aS0OL#oIhB1I#_7r>CwKR*&s{DqDwXTyhH-ju z#XS${gcLg531Hm0)5cpU9<2`GGzR+Wn%@}h8x!mXRtSli4TPoiLynH*elpqVsE5b( z>))At7^Tvb4!-JNa zYwQRvu(lFoW99;5+1c5Od3lGS2AZ2;!5)?}neN=VHA+L!>FH4}C!X`LCnH1r1|TB{ z)0d{F+d;hpuKxZVu;RwFcBiyde^l`McL0b-NBhR^nOI*>h={;zxwS*SstXJaQ)J!H zg5_dV7Qkz^?F9+FSy_OSNLmHpUMA{q{ob1xG^d$0YSNw-J5e`0EI+V-t!s%dn*`z4 zHRaLP{qoGV`I7rZ95soj*a{LXhB<>S6>m*~hS_3uwY62U)!P~%dcD2x7o@cu%Pc(E znzD#>j<#CfFt@-N0miA7knldUIzbDZH=3H5nb$|_mY0_!B7z`!>ky6M*3=Z-M5X$A zYpv5)O_`ZTy4Zq1z*fuGBn&je6O33ajGx~`l2}%TFEbskt}YM(3Y|62u&HS;ST4)# zsZ)47zahxlR{Yn)Q|Od@mzKARcR_{q^fYmr7`!e^H8FYWfDD6m4(}OGPY1e1myS@9 z0~x{jPk?m-zGm5a)P>#!MLs$j5)vE2#s)!1%e~n=NlDqu(C#f)^Ych1(FXPh^SChw zak_!bmXUz|XOlHC5i*jMbtyePD?Qy;Yc@JMx?q)> zM1`Bm%JQS5SwtPY>~M&Re?fr@sIJQd5MqQpg@nXSy4VIB9SO3Ij$s90o=HjY_>qtx zkuHI9WM+1yE85ZlUsYiP&K#)Y>>mG#Ou*)KNm~$OW8Ii%-8>~6gHA0&(Y{+&$x=~~ zka*7q8wS|g@+f9Hy18XW$L>iIax`=lYC>Yk2>v%ImV)3kVTFuoPb_mRLkPk9`o=<6 z4hOFg-x3W%NEa+TtbdP%4ja!zEFRz5`mQxLw!J+zKNft&nI~eq$_7+q9mJ z3;U#Z;lYZaF>7zP7+*K;L$k8v6WsV6TU#TtBVb=@m84AD%uE#(H#b|GloZ<(M}lCh zj*dTMOiBv+YoVc<+q7gwMLTG;?6aAtAYb)t_2nVqk0d-^POw4Qxm&~tzl^*Ha|ExEhr+OJ;_Z<1zd-jq2!s;Kwo^{3~Vc6T1u)Y$FZ z0tBNwtgLA}H?!UWO;b^6ZME@zguTbJvRXm%24QNCWo|BV?&3wYTed<6ZyhQubPD-v z1+^+FVt);47B>r>BVHX{-D``khYxcdK6I_ANo#nUCriRH!SZtL&0wJIDJheaeW1WN z(JTx>PGX3V*Y*(Go;N+e0%5PI!vlwR*rb~!C(CA%5acD8=*#h!w{OF8rpQ{Eha4Hf zxgP=(y?AkU*3FkUT73>m){gFH+;vdOl z4Gm*sMZd2V6{&%Ve)Nro^+RQk4HNYwA*csKxx5^&t*k6oSC?8>$I=u4?4Dn|X2O-= zA(ftf8~s+eU7(juBy^FUlal}qeuDP+`1no$|M9<^8`9w5AiUtds;38MO-!CV z`3ibYCO^5&tx8AGz(6Tb+Up#QgEkK)fsIGdz4;sf!3TbFJSE2jtIk8=zMNNYRaakH zDn9D#drCsW?{w_XfPmQ0ZCc)2odle=sw0VDq8w)vDG08r1WdFmF*0$(L^U-NH%t^> z!9>4;5b_30)IbVGSO|}7qLtO~u*h&?A~$#9zCVS>bGQ4Ba zab(1*&X_W%aa%W%2?*Q6ke+?JH(j7ELr?81;Ns0NCcHfLjLJa(n6~-B{B*n-yN8Fu?c?lAPgKTnzke%5qNU3xOo&8t zD)pF}1ewgwY|;xHw*ldBZvo~9AIangEDs*&Dl6yqgN>edzkS=n@HX_|agB{UPzRlz z(7TodBhAXv*keHk$W!aF9~?tc&!10tOgsT{Jl^G|i_5!rQc@=@`1nqIOXMUL3C$~CqOo+8*p^=Gt0t~&mogLKFlw(V z%1Mh-K-qungG6iH%gmI_tojNVyyM4D7k`L+xg2XZ$2a3F`cjy_U}`GN2!)3zDBA}X#iPV)z8n*P=dc1 z%orCA10XT0-ri>MdTZ-1*7FA7upkXSMuqntMMhq~1vV<#-YzYz0a}1F#K1-sVFZeB zlIN@3s``#qDk!WgtUGm(nl zB=X+A&+}f_^Shq+y{`Aa=l$d7;yCB*z1Mi(d#`n``~IwJIlDTuU@@i6Tnr>FB;D6b=#Y=jTr(Z znLoh}x11L_ITfD#Jymt}s_)gSi!fkkzkK<}j~`1*+1XSaV=^|Dk~|uI16t0^%m^Y0 zE3QiH(7(C_=sRZd)g%Lhlok1uwe_v)H}v6@6Z*%bjvceHxw6#IkQN?3BV=1};DD`V z<+)qB`ginorvls=v_%8F31&TlAaK=Y%{V7mau}kc>C8K@XgWa_TcRA;vr0I`DMn}2 zWDzuDW!2N$j+Zg=@tNh*xh^8Q8BbcPKwn?qk%{w-6BBxSKB{U4B_-uIX2f>JVSlbO zZrQTsBNRk&QBhH`rnB?Fu>%KEqOM$-np&Eg90CuG$1N=|Ps(S?&wm~T_W}L`*<|59 z>g$v$yMe=p4=)`)JUR*^WG1NSt9Xg+!ub}ic40X%Jt{}%ewBS5hfvew?%KO|E7oj0 zK-o7QpsL#P0Cqli?&Ay$&)B!1u&|Ks;-_4GPoF;h@Igy!GdiktUhnwvQFr$fjr*jf z@0#hUu)cXiSk79{{fxFqOCuJ9{;aJX1i9t@#*1N`Di{P$j#*nD1m!$-?BKzJui$Rx z>FKnCiXkbf9p6%XeU2Sl30q-+x!Tu9yB|G*x;#IBad7bbmI|gF6A1|*q1aavhK<1g z(IcTOFns84=>lox>>T*=1%jzNI=+7HpkT}&KYnOw>4D~I0q__A-x}Tm4iA7B<)s@G zw^y$K#Rjf%fWKqT1^!)j!2y3rBQq-_Jsma@e3tr@O2W{SXV9J%B|t3u58Vrj@+|t*HlUT3vO`F%g!E3L34xB%h37uOasW$SMa2a{0jC zE2*f+&(F-weMPH!4KRUX0O*xKRiIBuOad=*va?@iXJ)3QJxfhbPtQtA%gM>eg_OYy zr?@z$3>2%loQ`4qw2F6c-qln?E_JlJ>iQ40)zwWD46DNlgt~@0GR9b0SyD!Uvx`6t zH)@y<7WPUi#s+mSFD)-F&IcJN&WGCLEW+?9FXwe>@hixs9KwZP<>kG4RroRwuy>$^ zg(Xnnl9I9wEvzD8Y-Ob-rKRP72*i1~fvNZQEi}cuntFgD;0fr%2jKDnim(+Zf9)L| zK>F)wYw7IlZ0Q0>U2{z>6sfreYC)sf+CwN9?5Xgk4F5obn`(8<1}qQ2@*mqb5QsfJ zZH?XCpSpWKed_)49~k16-oCye80U3&e;ydk^!yJPdCd_jg=wIpV#~4OODRPXXqvQQBxnvj`-&evt z1|yu&$sa$!n`UNa7OMYaZhrpf!mlYDJJHKJ`VHIG1NW%{R2aEDDT&yL8uT&FyrXJ zz$k8k4MD%M^$+yJvBAMUI>yg3Iy5vkJTykaM81EY{Pyj~^yK8s92942YHAJ)GjJ=N znf~$P*RLrG#`tS)Zjp>JEG>|*Jr4=9)6=gX6DB4`Nf;xYJ~T+hn1_Z2`#*ict2n+6 zeI18N(!Y;>Cu7);o%@)6OwG8N%B5(-_8*qq232n3^Bg;n0sL53FRhmz$M3^L=dk_k)W~C8Q4f1I1?FG z1b}VF03$;%+cN<=j)y9YAIHJGE!FVUA#yp&&+gOY<1d}Eyv^H>VwWXA*X}cwY392 z`JsyZR4R3adSj8m?ti@kd97>Z_#;|1mq+4&P6M0J?nJuLo%OIk#fvw_j?J1CiHEW9 zV}|H=u)K59*{Y2J)7mPoKt#uXga9y*jN~@Vzsb zMf0wz_1UvEyMb){1qOX1Xk`35FPJk#iq#dQ*zN4D3QzXK4p5bH@h}7DuVj9NjybZm zj!o_*INM*;B-#{ZMhN|`w`5Sgrtv29@Pl#(26XP^TH?RESI3Qmm&~eUf>>Bag=1D9 z^MCmK&A9ug@menLMF+3>m+5~4SvF_wrEzb{lY&8f2z$e(5VpXkC!$H4mG%0B3p%*8 zf_)Edi->TwvNF&6it0YiT(n6mxP|_uXy+a4JRhK+zp_HojXP5P#`PgX9ua+RYig^L z6ZQyR2Fu{Bn3x|X-@ctMsyQO}xBN~^i&PX#g?QYbD1}-yl+V!b_b7Sap#8RcF7JNW)e$!h{j#=y z>?mRK;qYVcaGHp9$CtkxbWm(I@k|eEEsq;xY z1(7>ezw-*brQXnx@R2{2mBu(}_;bYQVEuM!>DS?gM3z{RWcqH6jXjzBJxy8$^6%^-=7)hVAZZP6R|>+(op02^;@?_R*MXuoIR^8ujA>t z|FqO`wH+w9-W-`GU1F_GnK^p%rjDwrl*@9v+^Tc4hliJ!;G5|@#W`a{rSbr0%G9Au zCLy7`s>6tzTb_}T`4*eYLe5Q#ngk*W?o)Bo&|vfXH__2kS`@z$!j_d-XG+)I3%`{t|0 z-Hh7Crq=d$SX%*RWbA{)P&a!AXOAb9&h8&Rtmc2ZXLGKs_;WIWk(1N>606tS1~Arb z7l%85w1UFEeFtIwa`*^*I;v*q?i&pE1IC!F!dE1Wy%?ru!0USR<}C%LRPUQUh+i)% zD(Rkt3C+q1#&33~Ygt$K-o3=HW8b+*$Zog7K3kZNj&TbRM_4T{hQg8+%qLbdMurV9 z-fU|}$_Edss#-w*ws}hUzi&xPOHNi(^SOER=c`?LYsT%{#vRnRfelituxEh#;|MsJ^VZ)n3=D#> zqGHYqbOsEukw}*&`5$-xn0IhEuS1J$eESvmOz%s{Z06eYk7TX% z+ir$un$a>bAaaS%D2v(a)~&kt;8fMqx8Kq4c}7NN=J@!PE3qAWj2%8s{#aR)KX?A3 zj48KiP2`}gZt{R}_> z0oyEZ%Bco5dD!zAzN1@SP9~8$71=p$;$OG6{Ww(1ek1T&a`BYkoxpn(OeErd^kWs+ zvckjD%IX}fSbBT=2RwK#C{L|n6%;&f0Tvub$Dz*7uED`=4gvyMFJ5hM?BDL}N7eP; zz)*Y4{i^p(K{aRR8$pQ~Sy?Z+__qps+zO12Nk-4IV8_6clF|y`^Y{JvwfK8^Wd(>_ z&^k;^!lt-8?^Ul`VJTFoc)_c84PRy$c5Y~6MLVN{itrwl{kFDu?%gjd?-}_^U=$M* zx3lw3d0zAm)zmk@l)SIMf8xi?U&2-*!gop?;g*y0111ktT3Y>Oe0*_nWK>XayTcV%Z||V!=;Y_wAG)@x?BB0yg{%taT!)4r z;_AYh#EGm#tUtd3tF%Aw5e}xYtBcnJ_%WS%^v5o)6kN>l~<#$LD z*0(S)dvH&0SVF)*X~PV%xJ1V=Ch&=0{sUzJTm2f`i~gxJEPHNO zBjEzs1`b`iv;k`(x9&iU~;2kSS@M8<_CO13mz)$nbGi` z4r9|s3PymwCu59YiEE=XL)ULqxC_S^VH1Z>V8R1q)t4{*V2L7u7z_^4H!=mToC=23 zz<{;vf5Xek(f;o-@c)jN3)aK1EpYu6`2XK{x&IX}C%eIu+q3Z^xA7vs@dCKG|F1tz z7y|{YHyD9J-nc3SL!@Smgwfcx(MV(pMxnaV$TWgRuyO5C<`u&%X6?%<^Yq{OQA>>kv8GAxBwM)F<>Q;C~Zb0;K*_F1d?qjd&g)L ztc#XOn&~1T+<*~sF#@9)#zi6lJZ%I0MW@J8aBgPOoKY$1Fz(Ju#;8;%I1M-8$8I{b zWMQN^i)8X@rlu^7hDUnuRK*?e?Q{l=yR?^vQ7FI_f|vY!{2>z!+5#A3j8HG84H(}q zJdcTDgi%-V{XCRj>;#7SQYPq4U>Kv}Vk|U_ic!;`R?Orm3{t_*F;K`P12ziQ=H;=G zB)*G@l1U*=kZd@hiFj$yjFKcO=^n0(x$T55f&p7T=0>4mq<%2rk$kv4NNzZtws9iG ziUJ7}XjFg#GE*qfk8LDY0tK3vk(0)2j&WT4LIKYUGJcIfJ3+_Kup1ejF?~nIXv-8_ zoCyQB2e=%0mC6!HqaGv*Eu0cSq0(qHWwHuyB?&s<{oG!h!hmCt3XMd-gBUOx9){OL z2Qje1t$~7>liuJS%%wO^(=o!&uuzvtL=3kBb0$XGh0#feNEqulg%X0}O*AqWq(a&0 z16~19-~b$opi3yckV_>Lh~hyb8)@obZEUnk5^Wnze=mu~9S&P_&yh2ybZAD9Bfg)J zM29yRCynfdfow(5F{t=wOb;L6fQEfvN+yx06sr1P+?I-Q(E#yBp?Z;M7}n-S>V{V2 zrS#IswfG1t{qRGKPUQ;0ABq~mql$?En199$`Op@dDEU|=l*WzDOBtQpXmd;u8U%8K z7LOv6e93h9U`vWBwZ(8DZaVa+v*R*ON~Tcg?{~HkXd4YmAYmT3rRY_tEyj72>P#Y0 zO4BG5d;%wu1W_Ai4w46VdcYr2Jjf0-j21~1GdgG=POJUL+zvQ9OZiEz~w6ftqY-7OaqFp<66Ku z%Ww->JW%eqwL3aY7v||cH)qP~sLqt+QM1qrJKrGU;H7-!bBV^@N zi9l+TAc`I$5eb$1FM#@y;e0tUFY3w>hf*|>|(jP(;V{V!3~$i_F(zJ0cLk2uNRi;Mq!_~`8Cx&bVhE>1!6Jl*G)*L}_c>PD1oi0Ugn-GbBG)@r3?yFntUwe>XWEq9xY)%3e^3{M-Mn~i#0>tbk#g2mj z`1n%n?V`?|GJF^pNxbcf;vNvSiDxO~SjSLvTvJ!$0=>rLV(y=aQ^}}!8LY7g1 zWJ^hDSy^ePQ)Wjo=}_zYp$X)t33pB@X(o^NcEToJ!FDc^(3P^1VkI^SmSoH1#}Blf1FXnm;(z$@n+gGvH*Q>WK1$Y& z&`xlRzi50);t3rdDQfhS#`La@|J{Qd_aHTTbrT)KP*`f`%W(sx=bpJTeT9bC2i(t} zO1dVJe?_)%_hRBy;z^VT&M0?Kf1708bY9Fi)dF1>t*yi2KY2^zs{4vAU5!M8do`UW z+}iH38F=$aeDIwZ`<}IP=fmjG{YnYWUS|Lu9FsHNBx_}yrM zb#k-7C3Us_qsLKTgKl(e<){?(1NRz}d3(&Yl)}Q9Y_S3A!0b<-(}Y%ZH`Yx$a=4~W zqfDlMx9bJ-1qs{Zl;Ztg5=%XTFTB=aMy<_xDK=V+saYmcGj_r1r?w}Z8ox)7S#64s zZ&--(DMT`rxXy}tJ$wGS$Chn>b#~YVV!2{PTr`Kf3%Jmg@fgK2wtxR}UfogH zccJ>Hp_7kg{?|j>Xsh0w3%O~(P;x*4m*v1r3WH>!bM#2b>zfrr1fe3w;_q%Mi7^IG zB@p4XPwu%VkKac5zZ-lT-yyB~YW~Xb{%@C$Hw#(mN5xj5VW!<5CSM5%s2l3Jw0@@x zxXM0X>5bNxv?|Fkv!m+7 z3K3E1*(BngfOp@oUCZGMTz(s>K0qdaJ^1_I$C#st!OTnrwp@G1`-yYkezWKgbIPM8 zL&nArnQQs^`IoZWP}rQo^SeHdlpNhy0)NnNRi&IIy~JRbkk@ywX@u^6-H);!-G4T4<{ zbTTfV-x9ZqrA^vZk}q2I^g6rrqYs5uKP4r6w*GxvX&@w!f4@=w;~z_6#~)TJ^?cxSbf^;O9IgrMWG zCb7J+`P$%zDeT-E$tUQ@xw%6jWCZocSuP+2Al8Py|=4@xvVlOm`HVS`;3Yjc7nkWJR&`0Gj9nM^s z59HWqy;cx8oFTR6`w)tH^`XHznp2RJJh2<4OL(vIAUC~YebMIYqI|Q;vD;j^b8@)X zb7Z8{xSdpy+VL%~mDY`gMk`ET$Lk0BB%HkKQW0>PF?2-un2MmFHTw58op(?XqBr(8 z_8fmB#FSW+@)P{K>V6XO`2%Xa21tM_;lokgb1(g4_xSpj?3fJFIq+U*LjSkc`*%!d zz6eF3`+i?LO=cAcrxsm<3R;)nXd0W9#8cT5TrF$eZf+)YqQ4M6^E6GGnnz)dzE>;# zzK#~?(zvCn`Cih&id;RDI>b3!cR;2=XK3)k$>=P(-wKISo@dW?huXM%p3IokzB`OG zUpS2H`TTis^30!*?B1TRLAptfFFG0)s(Qn@P}zJP&A4pcs#T}DBLBDQe%+nxY0^7y z-73AxUjDt@j#UKvOhm^;tb;#zg30sH{^}AFDN`zg%4q+f!&ZkeH7_llnjgqpz3Onn zi@E0hOyz?)TN~!6sa4_g5l(l_dXgzMRQtP#)|o46HHVE|<5?3eZ;($FXO_196s?vo zvVE97<0toSP`Una{AEN!3K=8>;N_@zV0^+Zki*4oO}e#IRNgNuuW53_W)VegmZJ!Mv&9s#NcLkyx8OC*$NK z4pAJu9v;;p?y96cH_Ji&-^j|eeUjnxEq;nE^JUyCT{bJ6Va;Ge2)(y$UE&o~6yH0PC^=bT( z-aKp~(Xq0hjBbkU44fA#6?#-vCDPvd`jdC~&U&2ZQ3k1hf3oKu#&3u;9i1YoVl!v; z%+lzkbkTrAZ@b+Q2h1+fA}NsLak&K&4ogp$R=H4btmj(j6}Mh79{0O#-!+?uSA^x; zk%TV+@&4MzE%>47_f`yByu+(9uTrg2beWZv^~UMPyX<=zzN0G-7^5n!ZfY0({aejg zojT6Co9VS2-%FLG$c5=@4pvU&=_U}%+PF|wMyMI7RgsRzZ1!Z_$~a}5=}7b&ia3{n zD!Hy+wL4S77jnQpT<}2l-vu_K7|OPr{j@3KwOvp6k0UcH9W5`1w84HAQ_h|{BK@1D z^osv>PKQ16<#`JNV2h+Xt4(fn>vgsrsOozXqVu!GCS>dTgP)}>LZ2Q?c&f03T(VBy zUA@q-BJ2R#zHP$@XlmM_(P zgZ85PqYzayGjlhZvpVszdl=mQTJ;Z2?!8vbTPBtdaR9sWZDtM&F-0w+Si%$Wy!mUV zzldxf8#9-VGiaAeC&>p;moh9YB!xRH&}aZWIKSRLprS>VwAxy{#(VikCaPwUBdZ7> zCgm`FMOL?O-+o01*`eyCKe_Grw9L55mps!5Vg9p6c4fCuHVUBHimUuaq-!_6c+9%X z6pJ?f3_3hKEINMfYKOv+GnO)IDDkwpSr9^p7B4A!CD=3{B1&4<=xoVmtCvX{E|{zT zfEH!%y=&(7jYx~lN#MUo>#@w#(>pBAB-kft#@J>2!x2pq&UUqjhQuv^={x3}g#4tF zOH!#TpG*kfd+Nx~p-pI6Ng|;U=jPd!Z&4~os~{>Xoyp18-rKi5<;oP>@_|$6hEUoL zQ{BH`kLlZlbt1Qe*8{hF5ekU=LJ;ZHkL7eaj4t>O$M4;p;;zAUMD4JHv03iYfyb6& zVKFVybLk#Z(VeK?Qfer0jvjxgiK|xu7u;T{;90qIU?R$dx>u})YnBJKE>j#gM@3Qn z9$9cwnfwTQURN+ZlH3zkIiUY8EP)V@`gYpIvhC&OekOBzZT0J;G3SCqEym;C0z%q$ z=`yn!n~@qPiL>5NaAJDfJi*G~F7udCv8t-lxg&L&#dvoTVI4gW>z^#n5G{Kdad2Cb zhKAI`FQ=ZmFn63e^SN~Kcq4x)5>HB^l3>AOOk#>CO4u3bZjmdq+g3BHQURQ7_x$^U z?*IAjt)kA$_=A;jDKY=f1x89)b_liH@uwyKbBV^-{H?o0#4Mbg@SN<^`u`3ezWV&? z+Q%zwBMe|BASCAPUkcl$)v~5flPs5)mwyBgWYLk_JL2@k(+7l15q@^~3l?KB`=J2~ zXFN1XSY5rE0z1O06qU~%~!EyBQlS|53^Jh+hhH{08k9PkWd}Paw3Vb&ozQet8|K!OL!g)e> zuh@ixWL1mHX@P*gwa=%lk+H*(Tbc@UUr!Pt!Q8;?l zY0tGo=~CmU%CvlYQ`@S{>~Xtvk{acwI!|wyU1y!2h`5lL;&Svo+&QlTH4OK7X;=Hl$N? zHPP}>qLQ>*+OhkTPLXrJcPZ>s^1V&u<5SD+e`|mai07U*7&dd)soTrSI(Abv$H4dT zgQ`~V$B}H|?!}DWNS^#BAh`JACe^w^_7EERw1k+`{GV!~)q@8LGK#JTT+!uL*~)>w zGa{q_ZM*G9LQR&q-g!2?ed=^MIIYO>-sb&CPQ2gb+xqfqkL%QQ1P9VFkGQWeJks^( zjrg5a?xXcfNO^Hs;r``EvvTI$YZI=gm3KC#?29qm)pLKp5H0knhyjYu$!b^n@`91$ zzSb4HtdBaz>3(DV*Ao-&XI*#tc!#hVNoDNd6!KJ^x}fbh>yC zCue?)`*4eL@#*?(Pydu$nvE+r(w5(K?w(`xgJXPCCL*OZ{Min)*FAj9f7{-@oAnf( zQbSo}@jO%0*P3a%Vp58I2@VA;&x3+gykreInzIaIV!BaeEn4kyFp!oOmW2()I7vy@ zOzVR_vKP%fZ|raNo9#sqfBrG6MoZ8AZm`=jD>@?5ar?QziOVN-gEx!c|7p^SB4*Mb zTxvxnE#~|lUE+6k^xV2ZJ7qnoFt7db_6Z1GK@2Y5H^;q8QAjG#@x8MfqJ@k5pdN#o~CC28H340xLD)GgV%_skzPTQ-_ z61I7kX24$V>%%wg{4edd=R z<3}zRZ+?G)_vbgWJJ=gM@O*xT(W?2-^;e(5U(CZG zj#>PSTl1Gr_rfR5)%LTqT@Ct8^FtN{c_}`P(G=FiVMOaGJXBKc{23UD@+SsZTJMWb z=kJR%EYk9QnUsrUmr<8aM3Im7h`&j{_~ti7X_w#jm6nQJFWceUb6WO9u|FHitpo`m z)LhriJM+(0i?%HCtRP=B_#_^qpIr3Jo+%f7X1mU#bTo9=IWZ^sV|hK>jv53$e#Gu3 zQu$@--@hq7CG@s3aO$dgNwd+{J0m*>j#-C)<-K&IAw>S^v3t*)A~ki;k+`gT_bO+) z_knuzVNW_a*Qu;czUo{+b`#cyVIWry~$ zO-Nz#?ZGjiSX;68=ExCI+#87y!nqVe0}(bXy!ZM39#r}LEP_2;>_|E-o=A3fOF@HxMIX5{nH3x|>9 zio=y&&-6>Kj~R)im!oa|j`n8S(~=QK3tM7p_Mdu*`exe4rWg1B@pUn*?DFw^rn-H1 z#mR%2HFdvh_H1Uk1DySofT16PJE|mo%G}78A8O^jJkJ??#&=-acgJGxuIy~YMgTvF zbMD8}@4dTC=@lS9yqz8PJkZ?6T>tiqP0eE`&;jR*aa;NM6UnX7!^2vA#3^^JQknJ} zFFz3(Z#6GcKBAwJ4{zPMf5*!uK29q+Iqj0TZzV^Q?sT=IeD;oAQ+jU^SwqC9?Dn&V zX@#a{V#0f8)Oor6r?*_cjPdXU?2tbE9R1Rv#PEEG@iM1$C)`_fR^rTzBb9WcN5LP#vS=~EFEn;sT<7^z z(cwpT=0a71{UjUi@?2Q?Hh6^>soVQWMj4MX1^CxieJyOcnpq!~6SBN8{&`beCcG?`|?}6CGKV~h08l_ zJBmf;J`_HvtL=v;(y)W<~mY)yFln#dxnctX$w3 znaS@QNhrK?u3+kdLHw8Faqjz2LN-W1!X;|Th}g`YBflOi2fUn2J+#v1vtVKP^5)eY z$Z7EeyZo6Z5g&q#Mp~ztyz`Sy=~w#-?ksp|aB*n_G@$)yN*ew*yz*WveGO1#Z&7)m z6mtCO^y4R&W3PxkThVSu+1c?we)u|1oVdZS2UEN)zq!wC`zg%GZtlEq-@bp&lgRc8 zU(+VdM-!uc(Qi!4rtVyM_;4uit_JgH;m7t}G0eNs2wnP4h^4XkP967K9emFXYE~Em zl3vpMpL4I~oPT>q1SN3ZUYFMjfrz4~O1IQVitU>})o%T5xxXa#p~acQJY49G?A>z* zLQ@YMShLlQipmbsj}aNnxf?OG4Z|1~?wleZ1x*&QDX|eHrW!gE)99$u@w>nGo%+an zpM9c1e?*`2J=%JJi+hQIL+Zq7wb1XJ=X?ncWo>(>-b`_D_|4CRP(zT$-P+XI^B#S@ zl0OHy_)Hf*@16M>ZaZxuFuV|aamo8PI;yd(ws&R4@A%l?Gw3hJ>Sf_wCq|0JUZkWek@sjc0Qrc3#ow72-AmGxe@vNf@JO{erIvMF z3)wlIu#^e1RXX3#7#ytrkBJRa5A-8`jckt2`L$2ynl`fw(cHZ6It zF*=DVk-kdAA6#gt|M1U&CZZpe6&0n6ME;b2{=D^5!IIQpvf2)RWi~dx(IZwRqWLqC z3tPn{#8CEG>=)m@ee#qMJ3G0cl=pp6!k$mDPg@s=AK7#X^ms`n+s6lBN)h%(Id7lOjNu1{Z=W`=G@-)2^GS4)5V zV9$Bj*f$!L^kn36Lu13i_m8={y_LA?&5*K5*rvOMo114+5AugH@l*MIZ-((Z%N{yw zYM~)(j+W4p)jyBw!-vVK$T~dhsKC*!9<%q4W<(3-#E|mUg{)p8jdP#;mbiIDG#7Zg7m4U%x|CL(3Z8$-xT(wiWl@^vTUK9xC=dGpHZept}YFu-ouVG`e%5 zyi@O9-iJk%I$wdA#9#NFouxxV7uJvZjNjcGETWGp&(d^93U~7zz>0%hFZPu-TKxE7 z@u@MvXR}2vqY#@3y6tYR+}&hldV5vbB;moS(v(ua*T$-g`yIcru?1Re4@L16LY|(U zC+zQ;#;I2;#PwA{bY_3m$-O0~O|FLC`p1nPvm}pRGwJgd7G{6h*cwPlJAJLePbtnc z&R-YREOSJnvS%HO-#<)rzAo-D}})DJYHFNJHsCS<}A>U7gcw#jRD_pM);OC8PyWgTkYO!qbC-hJK*Y zxXjR?nxF^yj`Kf~5|eVH!*f{*7itRWf7%=8ctguje&|R-enWon`-QUSZ{uE-MTQmu zh+}26tZFLsL1hpG$rC;daaELru2eA82&^PDM!qUcZfr`Zp9-&!2@VZSMvb4t-~5_b zi3}ZQ{rjfqb7SK#!b&AWWnE-XWLrlhp&nJ#vMq=6&xtF9m;L77(#}m-<>=z*Kc+hZ zOu$AsA0IDywe3pEVQJeBA*A^%4^M`mN?D3#jw^Lba?;#%B;2Qi-);$C;=zp1Q5 z1^)p8b}dg32)QLeV;LRwrPHrER{-{uQvc=cP%rDq=c3idj>tw>`Yi4Jmmbthjdt`4 zAAc}dTagl3(-`#NLuh++IP>z)iB?oSTTxQ;?hg&Jc<|=S+Jn%F656Md?r$|8H_|I9 zK_w}*bVB&9P27#4q3fN|#pU(C+un4R6Ml+{a{qigpRl9b4ik%i!|uL|c@*w@=w z7{2FMU?n0YKPoEfuy(X*>2G5~Pt3~FP-jVJN$~Rrp}|Qo@v921t_e=6o=8OXPhP~A z)z=km3dULze|+UFc{$H8HpaVY=1pkm+V4(g6iMd~4h_ysjSQWj24{Q)KTr5Q71F2#uVC*@~1rLgV)pMCg3=q`haNWp8mdXk>k{USJ9TvgVNEn5g4w$-aR!2z?4o7EXCJWFHVMi|G40HlUI-Z5+lJamSoR!Q#XL>{54PkKPekM00M?jHBz-98C#?>=-mW$PA-Ma2W|E0{ND zBrNRa4-Ye4A{{AdwdO2t`L+{%`}R$H#Dj@R(ZWKJJ1|+Guxe)W)vKLq{l9*hnm&7` z!MsgM%Jj8AIPzpfMna_TIvr0$Rjz7kuIpscI|3?petyczC-wptPTlW^ZE4^AkKIbP zXsN~}Xtr`+!t%0YD&s)P$I)vrf#BV4d$R8Wtrr+toKfIRIHJ{9sc&~1>cJefxaj4+ zf@9l?id@dI&5G*jeOoIipq)w*mQ;VUz8(~Ghm0Y(;^da$eVZg1WSQS~_cJXjrmZYDn`ve35pXjIpTI#XT}GIzWf9qQU_>BgU`uDMQrNOl|k;87lz+ zMa5$4RvRA@5Zg8Ky)i5}IQW4@aY~@`VO?GQXZM~5+!zZI!dywDBorH)5y#=^NaX0$ zCX*v0w{0^tF#*@7iVFAzQ@b$JjH;#bi znk4B)iwrox2K)pLZH_>JuufOexBTkrF!gS0Q>t-s30h8F20p{dl)gu!yplP67kZg@ zFi&^&j*PgtXlaQ;>kA5&mVW&BF%n!liIR|(JB~4mtw>u!p;+xUpqe!-R}69R*0C2`CCmBDL5Hva&WY zG!;gkxPWaX0hKHN54fBI5LB)xE-Pz)OU1a``1trXY+N1WoZ*W)9em~P+_@|RO~YEf zhb>Qt<#H;x9)s)r8NQLevu9;B4>{>mAj~Oc8)TUZK|6ULgIsEq?C$s3sxMnwihlq8 zHZtk<@XY5D%WPqg>D&6}OK zZgqCH(9oE;C0xUD2pF+b=g6S9>YL6zdq$#=Fl-a(Ek)%5l$8iuoUj}AJo!2G{>jfL zPoBKdZztcr&HZkBmXTk0MTH;IWIGxpjz+{RMCw+a7Q%w7wxpz|sq4AEU(Z{51({|#32 z&UVhvchZ7*I=s1O$z%b6cyaK)Lf^@l%B8~T9d~1|Z>KzDpEJE@2;K%VbKlDzomL0& z-C9dsXms`0dR<$~`0e%U*D+=eoi~42wLt59>+SVuF!+z&e$pAe$`c%H2C*`oH(Okm z?Df~zo0=lY7=kO!!zhDuW|N+o6T` zu57DbE!G5iVGz>X&|5AB#M60ko+5WB?(g5h!Qbz9`u~JV`A=)F)YXAUycqIkOOViv ziP2ShH}#~3XkL1py13W^NTsKrk)vZ8G8$lIou9vcJ)pQaIM`%l1b#uzBp5r3i|>Lo z+H6&RVAFNsBlPeH=%`kd! zrElH(#pbozw<|*eDZ;e!W@q<1m!zcy1?8D-rIL~;NgH~*Zmh=(B9=IH{l#xem)2{T znLl%=kZ30C&l2X2XJuV?8g#i(N5gVJZ;OgpL2rSC_2^Qgjtd<+dZFLCIB0mmyJV~G5}PeS+PH) z#L7(w3po?t$?Kc zbY*3wF7k#LDwc`Vg!dA=nwX%6c^W}&Vc0O=I4`cCAm#5jmkRRisDv zZyt=%d6}7coSd9PD-m!zW*!^cDJiK9?Hvz7Whf$o4kg-9Ff`vKz{5H=_K=;OpWnkH z@5M?uCuiENTbC}ST)T!$Lll^ow6*J#ZAlo*0;esBbj_FqUizT5#-7$V#zi=Cq?>0_ zoD`oZLrRo9#i-zvIgBGDM^GJaZ7e8go#vd=TU{smTKmcHRw=_O=%lxI$J_!kR9?*_ zMwz2wpcx(Ul~hn5R#g#^wIsLRpiBMeD1oP7uB4>9``Wemf{2Lj1tJMULdM2=dU_}0 zd3kvUpYZZN$*d=!<^bN_UOzvQH#|ELe{YD3X)dpK&5>qr?ec&KOAOo`Ao{PFR62 z-NIJWS+hs!ptOHRHhVLR^x`LZWn>^q!*{)d6HsmS z^-fE2iKJY@sIm(!HdY*|s+K4FnwpmQS|_uy#l{*NHv>(vc@+3_L?TdRd>O^zGl=4O zX3v_wRO@sc!xBMhd0`LMqUQ8;UIpN6*z*=tZG^di6bSXL`Vw3?te+4_lKlL@z915h zXqT4<_|Ex)($X?rm@QMChMSK`7I+s-ib|H33kj8%6F_NOTbGvLAp*V6P?8O45kP5e zQSK3FymBE^7pOP}w@Krg^wH6~Bo|OxKmNW{C#Ou$SYA-ti1G1p-VLR#nftRK>O z(h|R6`fx;JlxM-hlT@Kb?J7Mdr)$^7nVTmI3P=STN_)GzcL~Rk!rhuoh9j|-q6G!q zGAExnZCxGCbn`6m_P%|R_oQKRDwYjOo1VT3N((?r1-(Q|OKa2p=m-c_?l>ygP}<1? zIw&oXXgX~QEk&AJsA-)uZ6#qm2>BMJmpA|U(;CmJEdB@QXUL3#EH{`S;=KXo+DF`Y zb6xnW{QL+(LBqzz3*Fr=z1$r{Ht0Qg~#m1Hg1o-*2wkquBt!HCX zH8kw&tJ2WOGaUA3Nm^QRlKl=hVwT^$-?wjn89@3^dm9TjEuOx8daI!-84QQ z3J<||%)9KSr}u_+ZVl*;{Q4CZrYbK_{7$<6@|;nX%jf5?I{Ekdbp!QL>&-2ywh=n4 zd1cXy081`OV}+px5>nay)j%` za`0T*TGGYQq{6uLs-$Gb0r)?y?;>X{w{^!r>o_nlWbYdSwUw0>a`Jme6BFBBqonj= zt3Kc4^9gtN>S|*$hTsa5&K{}eP_2Z7dw!xv73PJTZjuv@A2%7_10C>}^{-k$oj}!& zN@!u8^P_>so4DzqwgKE5YAeGMPFo`dHi%1CadNh{MyR&D@qtRcaro;}RRz0*zD)9} zT-SI%HRYxq)->Pc;BhJQkcEXldiU;z#mvOSiy4o=y1Ko4nV8suf)o{X?%#)B*ilG> zkt-;u0fcdsGaBvxMTAisHVkT;TUu&Zf33Qj@DP}I2NFv2ZomO2P+KpanJsZgJ=SO^ zlVf6vi;E%7>eBuDx^9VXptfo;Ih(XfJV+{xuXtN{Pba);Z50yQwTlL7yJwG*5(JrP zf!e;f0BOO@g3kIW zEE8dIRn=ej{v}^;i-gNDmK`0mmoMLFX^AT;N;`3iwX#9ynQSd7Np=9q!KxbcqhR2H zu4ecbFQ2e{{P#25J*v1zz`SL`{T?Dd;^1KeLqP`GE_l|$Zg+vZ0>7J^+vN#=dNn>i z-kn!i{3mkD&n=*~rJiMLq(~E6dl!LF-Fx<$1w(sJ z*weNZ7T(v@?dUKw6E1SuGdpXx^nbDUCV*6STjTh1%=0`?=Lng~JY~o+PYD@QawH=2 zJX9P>X%J36pOIQTfO^K8qZ<9%g#-a^ z3k!jxe~hrKtZcKstgQZ##Bnz3%C?@Knws_oU}?i8dqVH(HXV_14~}WOJT&RM(?7GN zI}czG!>3>-y-$H!oZG-@LiR90mfW zwUM_ysw@aGloJ3HW_;7|HvCFF;1`%;JWgy+-$@?V-zh8N(R(F5ePAGdshWE$s1$c) zu-MED2tZ~&{HnFCBFECwUzAU;(Uf6Gd&hiD%@H^q-P=(idir9aOGU3=&&<4gxBJc= zc6J^{M(`g@<5m!-+_`fUaO0HEzHQPf<>{b>0qZe-kBn3Z|KQ{l%LP7(Dx8kYehUEP z2#B6IRwyjQ_mcj2j*_G#a7Yw;O?G)`XtIU11&FOC=we*S@O56aoB-4i1r#&qzst zYc{7cPHxGzo-PI~Mj;YbB6s#?DdxqC@_yRwDLr)BCbrx@M~{;E=qM*Aet>d;-16|K z0=X3y7Is%`9~huKbt%RibaOc=G0Hf({iY1sfn;T6MtlZP>XdMFsp9z=Cm^>mG3gfz z%2Cgs$6pb!XZHjq_A8tOvfjIQ?VEcDRS>?Kdv9iRZM zcjIYqZ`mNIJ<_lv@hl(6rR_4bLt=igC-?}c?92l{^Wi%}4|0HC@Eq{7SYBR+jb{V7 z1!lNxwyc7dmiki{-v{7BQo*Mw{oRhj9Kt|uNlR~LCWaRjgo6lmY+|;7p!kuI>I)Zw z0m0({S4~V$3*CCprB@ErjQWXH?T;vFu#C=NWo|1-yMTO@#wptqG1QPa>OAqr}082F>5r3c&s zqH+QdErZlch)U`;86y+;WSW@;a2Nvv1qBFm$O=9kW@F=E=LA8Lxw*M{`A`r!Fpm%j z{w)Fsh#Gv8E(Erx77*a!0YX9~EG&WsV-YY(AOgw&B_Vj3nVA?E=twv?fF(oD$0svJ z#LmtS7!BkS6%SuAWg{0A1!`PUQVL)#4agwM2fhf|CX9lJg!n*0QZ`mr9zI?$g$P6N z0{J8ci~CH7I!Z=bDk@4!a&i)43`7irtt_J;*mpn{zWI3&e|!$m3q%^9Sz02cprB-+ z1Rp_zUkVBm5`-iKX2zf;6bRQgGlRRFMMESIG4V8xbd*Sl0R|%@CHek+@jIaO;v9^O z5?NkYfKz{rIAoEm&Vz{bYK4fHD?9Pktg0#JmXU!E9HgAb4u)DD5*;9x>R z#O&;hjQs2n9TI{uaAiTn+`POX12OmlTNqS|aw|VCFApat&lX_>1QQhl8N|T{F@QjT zvO;1&5{bpPOGreRK{5ahSs)d1V9i)TQE3M#0}N+XKrT5IAcc@B8iIgH4edlj3K($K z0Dqc*o~o)KScQ^U_=-sz06!@;qIhKP}ZUA<9Bh@a0poIG9uR(heT;Q4n z>Vw*Fz*J~}Bm`W@g1uPOBAP|+fObY;SwPt$SPof)HOdHKk0munFks0T!DGaBEHM&63;7{Hj5q`}aAAV7II`ea zG6=~BxDdG=Ns0m`P$FPBB#5Z6bf6?n@QZLnfMo-O4`wgQ0Tv0xgP9YKPIkTM98ELtongxrlKV**@> z5y5gvVad#q_t2yma)c9t1VM)e@`>dHN*2TgB*tt*P=bO{2rL;hmKA&;j08h8ln8

    k* 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 z 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 literal 0 HcmV?d00001 From d9b339ee183f2dd276d5153d39b92ccd033e0f1f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Jul 2020 20:09:59 +0200 Subject: [PATCH 0323/1197] Add some more tests for hdf5 --- tests/data/test_history.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 003e90fbb..2e703c72b 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -713,6 +713,41 @@ def test_hdf5datahandler_trades_get_pairs(testdatadir): assert set(pairs) == {'XRP/ETH'} +def test_hdf5datahandler_trades_load(mocker, testdatadir, caplog): + dh = HDF5DataHandler(testdatadir) + trades = dh.trades_load('XRP/ETH') + assert isinstance(trades, list) + + +def test_hdf5datahandler_trades_store(mocker, testdatadir, caplog): + dh = HDF5DataHandler(testdatadir) + trades = dh.trades_load('XRP/ETH') + + dh.trades_store('XRP/NEW', trades) + file = testdatadir / 'XRP_NEW-trades.h5' + assert file.is_file() + # Load trades back + trades_new = dh.trades_load('XRP/NEW') + + assert len(trades_new) == len(trades) + assert trades[0][0] == trades_new[0][0] + assert trades[0][1] == trades_new[0][1] + # assert trades[0][2] == trades_new[0][2] # This is nan - so comparison does not make sense + assert trades[0][3] == trades_new[0][3] + assert trades[0][4] == trades_new[0][4] + assert trades[0][5] == trades_new[0][5] + assert trades[0][6] == trades_new[0][6] + assert trades[-1][0] == trades_new[-1][0] + assert trades[-1][1] == trades_new[-1][1] + # assert trades[-1][2] == trades_new[-1][2] # This is nan - so comparison does not make sense + assert trades[-1][3] == trades_new[-1][3] + assert trades[-1][4] == trades_new[-1][4] + assert trades[-1][5] == trades_new[-1][5] + assert trades[-1][6] == trades_new[-1][6] + + _clean_test_file(file) + + def test_gethandlerclass(): cl = get_datahandlerclass('json') assert cl == JsonDataHandler From ed33d4781d9bce146e85ee23ddc46e840e9c20c8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Jul 2020 20:19:34 +0200 Subject: [PATCH 0324/1197] Add more hdf5 tests --- tests/data/test_history.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 2e703c72b..3b20bd61d 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -670,7 +670,7 @@ def test_jsondatahandler_ohlcv_purge(mocker, testdatadir): assert unlinkmock.call_count == 1 -def test_jsondatahandler_trades_load(mocker, testdatadir, caplog): +def test_jsondatahandler_trades_load(testdatadir, caplog): dh = JsonGzDataHandler(testdatadir) logmsg = "Old trades format detected - converting" dh.trades_load('XRP/ETH') @@ -713,13 +713,13 @@ def test_hdf5datahandler_trades_get_pairs(testdatadir): assert set(pairs) == {'XRP/ETH'} -def test_hdf5datahandler_trades_load(mocker, testdatadir, caplog): +def test_hdf5datahandler_trades_load(testdatadir): dh = HDF5DataHandler(testdatadir) trades = dh.trades_load('XRP/ETH') assert isinstance(trades, list) -def test_hdf5datahandler_trades_store(mocker, testdatadir, caplog): +def test_hdf5datahandler_trades_store(testdatadir): dh = HDF5DataHandler(testdatadir) trades = dh.trades_load('XRP/ETH') @@ -748,6 +748,26 @@ def test_hdf5datahandler_trades_store(mocker, testdatadir, caplog): _clean_test_file(file) +def test_hdf5datahandler_ohlcv_load_and_resave(testdatadir): + dh = HDF5DataHandler(testdatadir) + ohlcv = dh.ohlcv_load('UNITTEST/BTC', '5m') + assert isinstance(ohlcv, DataFrame) + assert len(ohlcv) > 0 + + file = testdatadir / 'UNITTEST_NEW-5m.h5' + assert not file.is_file() + + dh.ohlcv_store('UNITTEST/NEW', '5m', ohlcv) + assert file.is_file() + + ohlcv1 = dh.ohlcv_load('UNITTEST/NEW', '5m') + # Account for the automatically dropped last candle + assert len(ohlcv) - 1 == len(ohlcv1) + assert ohlcv.iloc[:-1].equals(ohlcv1) + + _clean_test_file(file) + + def test_gethandlerclass(): cl = get_datahandlerclass('json') assert cl == JsonDataHandler From ae1c99bdd010c7f4e1bc122eb3e68a539a4f9912 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Jul 2020 20:36:30 +0200 Subject: [PATCH 0325/1197] more tests --- tests/data/test_history.py | 37 +++++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 3b20bd61d..41b20e35b 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -718,6 +718,23 @@ def test_hdf5datahandler_trades_load(testdatadir): trades = dh.trades_load('XRP/ETH') assert isinstance(trades, list) + trades1 = dh.trades_load('UNITTEST/NONEXIST') + assert trades1 == [] + # data goes from 2019-10-11 - 2019-10-13 + timerange = TimeRange.parse_timerange('20191011-20191012') + + trades2 = dh._trades_load('XRP/ETH', timerange) + assert len(trades) > len(trades2) + + # unfiltered load has trades before starttime + assert len([t for t in trades if t[0] < timerange.startts * 1000]) >= 0 + # filtered list does not have trades before starttime + assert len([t for t in trades2 if t[0] < timerange.startts * 1000]) == 0 + # unfiltered load has trades after endtime + assert len([t for t in trades if t[0] > timerange.stopts * 1000]) > 0 + # filtered list does not have trades after endtime + assert len([t for t in trades2 if t[0] > timerange.stopts * 1000]) == 0 + def test_hdf5datahandler_trades_store(testdatadir): dh = HDF5DataHandler(testdatadir) @@ -760,13 +777,25 @@ def test_hdf5datahandler_ohlcv_load_and_resave(testdatadir): dh.ohlcv_store('UNITTEST/NEW', '5m', ohlcv) assert file.is_file() - ohlcv1 = dh.ohlcv_load('UNITTEST/NEW', '5m') - # Account for the automatically dropped last candle - assert len(ohlcv) - 1 == len(ohlcv1) - assert ohlcv.iloc[:-1].equals(ohlcv1) + assert not ohlcv[ohlcv['date'] < '2018-01-15'].empty + + # Data gores from 2018-01-10 - 2018-01-30 + timerange = TimeRange.parse_timerange('20180115-20180119') + + # Call private function to ensure timerange is filtered in hdf5 + ohlcv = dh._ohlcv_load('UNITTEST/BTC', '5m', timerange) + ohlcv1 = dh._ohlcv_load('UNITTEST/NEW', '5m', timerange) + assert len(ohlcv) == len(ohlcv1) + assert ohlcv.equals(ohlcv1) + assert ohlcv[ohlcv['date'] < '2018-01-15'].empty + assert ohlcv[ohlcv['date'] > '2018-01-19'].empty _clean_test_file(file) + # Try loading inexisting file + ohlcv = dh.ohlcv_load('UNITTEST/NONEXIST', '5m') + assert ohlcv.empty + def test_gethandlerclass(): cl = get_datahandlerclass('json') From edb582e5229b18052dd82ee930d0af2fa859732d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 24 Jul 2020 20:40:07 +0200 Subject: [PATCH 0326/1197] Add more tests --- tests/data/test_history.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 41b20e35b..c89156f4c 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -765,6 +765,18 @@ def test_hdf5datahandler_trades_store(testdatadir): _clean_test_file(file) +def test_hdf5datahandler_trades_purge(mocker, testdatadir): + mocker.patch.object(Path, "exists", MagicMock(return_value=False)) + unlinkmock = mocker.patch.object(Path, "unlink", MagicMock()) + dh = HDF5DataHandler(testdatadir) + assert not dh.trades_purge('UNITTEST/NONEXIST') + assert unlinkmock.call_count == 0 + + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + assert dh.trades_purge('UNITTEST/NONEXIST') + assert unlinkmock.call_count == 1 + + def test_hdf5datahandler_ohlcv_load_and_resave(testdatadir): dh = HDF5DataHandler(testdatadir) ohlcv = dh.ohlcv_load('UNITTEST/BTC', '5m') @@ -797,6 +809,18 @@ def test_hdf5datahandler_ohlcv_load_and_resave(testdatadir): assert ohlcv.empty +def test_hdf5datahandler_ohlcv_purge(mocker, testdatadir): + mocker.patch.object(Path, "exists", MagicMock(return_value=False)) + unlinkmock = mocker.patch.object(Path, "unlink", MagicMock()) + dh = HDF5DataHandler(testdatadir) + assert not dh.ohlcv_purge('UNITTEST/NONEXIST', '5m') + assert unlinkmock.call_count == 0 + + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + assert dh.ohlcv_purge('UNITTEST/NONEXIST', '5m') + assert unlinkmock.call_count == 1 + + def test_gethandlerclass(): cl = get_datahandlerclass('json') assert cl == JsonDataHandler From 119bf2a8ea66ecafc900677f998be2317c458f5b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Jul 2020 17:06:58 +0200 Subject: [PATCH 0327/1197] Document hdf5 dataformat --- docs/data-download.md | 118 ++++++++++++++-------- freqtrade/data/history/hdf5datahandler.py | 2 + freqtrade/data/history/history_utils.py | 8 +- 3 files changed, 84 insertions(+), 44 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index a2bbec837..0b22ec9ce 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -15,61 +15,91 @@ Otherwise `--exchange` becomes mandatory. ### Usage ``` -usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-p PAIRS [PAIRS ...]] - [--pairs-file FILE] [--days INT] [--dl-trades] [--exchange EXCHANGE] +usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] + [-d PATH] [--userdir PATH] + [-p PAIRS [PAIRS ...]] [--pairs-file FILE] + [--days INT] [--dl-trades] + [--exchange EXCHANGE] [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} ...]] - [--erase] [--data-format-ohlcv {json,jsongz}] [--data-format-trades {json,jsongz}] + [--erase] + [--data-format-ohlcv {json,jsongz,hdf5}] + [--data-format-trades {json,jsongz,hdf5}] optional arguments: -h, --help show this help message and exit -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] - Show profits for only these pairs. Pairs are space-separated. + Show profits for only these pairs. Pairs are space- + separated. --pairs-file FILE File containing a list of pairs to download. --days INT Download data for given number of days. - --dl-trades Download trades instead of OHLCV data. The bot will resample trades to the desired timeframe as specified as - --timeframes/-t. - --exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no config is provided. + --dl-trades Download trades instead of OHLCV data. The bot will + resample trades to the desired timeframe as specified + as --timeframes/-t. + --exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no + config is provided. -t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} ...] - Specify which tickers to download. Space-separated list. Default: `1m 5m`. - --erase Clean all existing data for the selected exchange/pairs/timeframes. - --data-format-ohlcv {json,jsongz} - Storage format for downloaded candle (OHLCV) data. (default: `json`). - --data-format-trades {json,jsongz} - Storage format for downloaded trades data. (default: `jsongz`). + Specify which tickers to download. Space-separated + list. Default: `1m 5m`. + --erase Clean all existing data for the selected + exchange/pairs/timeframes. + --data-format-ohlcv {json,jsongz,hdf5} + Storage format for downloaded candle (OHLCV) data. + (default: `json`). + --data-format-trades {json,jsongz,hdf5} + Storage format for downloaded trades data. (default: + `jsongz`). Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). - --logfile FILE Log to the file specified. Special values are: 'syslog', 'journald'. See the documentation for more details. + --logfile FILE Log to the file specified. Special values are: + 'syslog', 'journald'. See the documentation for more + details. -V, --version show program's version number and exit -c PATH, --config PATH - Specify configuration file (default: `config.json`). Multiple --config options may be used. Can be set to `-` - to read config from stdin. + Specify configuration file (default: + `userdir/config.json` or `config.json` whichever + exists). Multiple --config options may be used. Can be + set to `-` to read config from stdin. -d PATH, --datadir PATH Path to directory with historical backtesting data. --userdir PATH, --user-data-dir PATH Path to userdata directory. + ``` ### Data format -Freqtrade currently supports 2 dataformats, `json` (plain "text" json files) and `jsongz` (a gzipped version of json files). +Freqtrade currently supports 3 data-formats for both OHLCV and trades data: + +* `json` (plain "text" json files) +* `jsongz` (a gzip-zipped version of json files) +* `hdf5` (a high performance datastore) + By default, OHLCV data is stored as `json` data, while trades data is stored as `jsongz` data. -This can be changed via the `--data-format-ohlcv` and `--data-format-trades` parameters respectivly. +This can be changed via the `--data-format-ohlcv` and `--data-format-trades` command line arguments respectively. +To persist this change, you can should also add the following snippet to your configuration, so you don't have to insert the above arguments each time: -If the default dataformat has been changed during download, then the keys `dataformat_ohlcv` and `dataformat_trades` in the configuration file need to be adjusted to the selected dataformat as well. +``` jsonc + // ... + "dataformat_ohlcv": "hdf5", + "dataformat_trades": "hdf5", + // ... +``` + +If the default data-format has been changed during download, then the keys `dataformat_ohlcv` and `dataformat_trades` in the configuration file need to be adjusted to the selected dataformat as well. !!! Note - You can convert between data-formats using the [convert-data](#subcommand-convert-data) and [convert-trade-data](#subcommand-convert-trade-data) methods. + You can convert between data-formats using the [convert-data](#sub-command-convert-data) and [convert-trade-data](#sub-command-convert-trade-data) methods. -#### Subcommand convert data +#### Sub-command convert data ``` usage: freqtrade convert-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-p PAIRS [PAIRS ...]] --format-from - {json,jsongz} --format-to {json,jsongz} - [--erase] + {json,jsongz,hdf5} --format-to + {json,jsongz,hdf5} [--erase] [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} ...]] optional arguments: @@ -77,9 +107,9 @@ optional arguments: -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] Show profits for only these pairs. Pairs are space- separated. - --format-from {json,jsongz} + --format-from {json,jsongz,hdf5} Source format for data conversion. - --format-to {json,jsongz} + --format-to {json,jsongz,hdf5} Destination format for data conversion. --erase Clean all existing data for the selected exchange/pairs/timeframes. @@ -94,9 +124,10 @@ Common arguments: details. -V, --version show program's version number and exit -c PATH, --config PATH - Specify configuration file (default: `config.json`). - Multiple --config options may be used. Can be set to - `-` to read config from stdin. + Specify configuration file (default: + `userdir/config.json` or `config.json` whichever + exists). Multiple --config options may be used. Can be + set to `-` to read config from stdin. -d PATH, --datadir PATH Path to directory with historical backtesting data. --userdir PATH, --user-data-dir PATH @@ -112,23 +143,23 @@ It'll also remove original json data files (`--erase` parameter). freqtrade convert-data --format-from json --format-to jsongz --datadir ~/.freqtrade/data/binance -t 5m 15m --erase ``` -#### Subcommand convert-trade data +#### Sub-command convert trade data ``` usage: freqtrade convert-trade-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-p PAIRS [PAIRS ...]] --format-from - {json,jsongz} --format-to {json,jsongz} - [--erase] + {json,jsongz,hdf5} --format-to + {json,jsongz,hdf5} [--erase] optional arguments: -h, --help show this help message and exit -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] Show profits for only these pairs. Pairs are space- separated. - --format-from {json,jsongz} + --format-from {json,jsongz,hdf5} Source format for data conversion. - --format-to {json,jsongz} + --format-to {json,jsongz,hdf5} Destination format for data conversion. --erase Clean all existing data for the selected exchange/pairs/timeframes. @@ -140,13 +171,15 @@ Common arguments: details. -V, --version show program's version number and exit -c PATH, --config PATH - Specify configuration file (default: `config.json`). - Multiple --config options may be used. Can be set to - `-` to read config from stdin. + Specify configuration file (default: + `userdir/config.json` or `config.json` whichever + exists). Multiple --config options may be used. Can be + set to `-` to read config from stdin. -d PATH, --datadir PATH Path to directory with historical backtesting data. --userdir PATH, --user-data-dir PATH Path to userdata directory. + ``` ##### Example converting trades @@ -158,21 +191,21 @@ It'll also remove original jsongz data files (`--erase` parameter). freqtrade convert-trade-data --format-from jsongz --format-to json --datadir ~/.freqtrade/data/kraken --erase ``` -### Subcommand list-data +### Sub-command list-data -You can get a list of downloaded data using the `list-data` subcommand. +You can get a list of downloaded data using the `list-data` sub-command. ``` usage: freqtrade list-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [--exchange EXCHANGE] - [--data-format-ohlcv {json,jsongz}] + [--data-format-ohlcv {json,jsongz,hdf5}] [-p PAIRS [PAIRS ...]] optional arguments: -h, --help show this help message and exit --exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no config is provided. - --data-format-ohlcv {json,jsongz} + --data-format-ohlcv {json,jsongz,hdf5} Storage format for downloaded candle (OHLCV) data. (default: `json`). -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] @@ -194,6 +227,7 @@ Common arguments: Path to directory with historical backtesting data. --userdir PATH, --user-data-dir PATH Path to userdata directory. + ``` #### Example list-data @@ -249,7 +283,7 @@ This will download historical candle (OHLCV) data for all the currency pairs you ### Other Notes - To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`. -- To change the exchange used to download the historical data from, please use a different configuration file (you'll probably need to adjust ratelimits etc.) +- To change the exchange used to download the historical data from, please use a different configuration file (you'll probably need to adjust rate limits etc.) - To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`. - To download historical candle (OHLCV) data for only 10 days, use `--days 10` (defaults to 30 days). - Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data. @@ -257,7 +291,7 @@ This will download historical candle (OHLCV) data for all the currency pairs you ### Trades (tick) data -By default, `download-data` subcommand downloads Candles (OHLCV) data. Some exchanges also provide historic trade-data via their API. +By default, `download-data` sub-command downloads Candles (OHLCV) data. Some exchanges also provide historic trade-data via their API. This data can be useful if you need many different timeframes, since it is only downloaded once, and then resampled locally to the desired timeframes. Since this data is large by default, the files use gzip by default. They are stored in your data-directory with the naming convention of `-trades.json.gz` (`ETH_BTC-trades.json.gz`). Incremental mode is also supported, as for historic OHLCV data, so downloading the data once per week with `--days 8` will create an incremental data-repository. diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index 6a4f45fa9..c55c0c1e5 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -59,6 +59,7 @@ class HDF5DataHandler(IDataHandler): _data = data.copy() filename = self._pair_data_filename(self._datadir, pair, timeframe) + ds = pd.HDFStore(filename, mode='a', complevel=9, complib='blosc') ds.put(key, _data.loc[:, self._columns], format='table', data_columns=['date']) @@ -139,6 +140,7 @@ class HDF5DataHandler(IDataHandler): column sequence as in DEFAULT_TRADES_COLUMNS """ key = self._pair_trades_key(pair) + ds = pd.HDFStore(self._pair_trades_filename(self._datadir, pair), mode='a', complevel=9, complib='blosc') ds.put(key, pd.DataFrame(data, columns=DEFAULT_TRADES_COLUMNS), diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 58bd752ea..dd09c4c05 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -9,7 +9,8 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS -from freqtrade.data.converter import (ohlcv_to_dataframe, +from freqtrade.data.converter import (clean_ohlcv_dataframe, + ohlcv_to_dataframe, trades_remove_duplicates, trades_to_ohlcv) from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler @@ -202,7 +203,10 @@ def _download_pair_history(datadir: Path, if data.empty: data = new_dataframe else: - data = data.append(new_dataframe) + # Run cleaning again to ensure there were no duplicate candles + # Especially between existing and new data. + data = clean_ohlcv_dataframe(data.append(new_dataframe), timeframe, pair, + fill_missing=False, drop_incomplete=False) logger.debug("New Start: %s", f"{data.iloc[0]['date']:%Y-%m-%d %H:%M:%S}" if not data.empty else 'None') From bad89307dd752195100855a1c4f9cc942b9ef59d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Jul 2020 17:19:41 +0200 Subject: [PATCH 0328/1197] Fix mypy error --- freqtrade/data/history/hdf5datahandler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index c55c0c1e5..594a1598a 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -191,11 +191,11 @@ class HDF5DataHandler(IDataHandler): return False @classmethod - def _pair_ohlcv_key(cls, pair: str, timeframe: str) -> Path: + def _pair_ohlcv_key(cls, pair: str, timeframe: str) -> str: return f"{pair}/ohlcv/tf_{timeframe}" @classmethod - def _pair_trades_key(cls, pair: str) -> Path: + def _pair_trades_key(cls, pair: str) -> str: return f"{pair}/trades" @classmethod From 6ce4fd7aff2c9cd78d86b4830e172526eb26877d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 26 Jul 2020 08:37:10 +0000 Subject: [PATCH 0329/1197] Bump arrow from 0.15.7 to 0.15.8 Bumps [arrow](https://github.com/arrow-py/arrow) from 0.15.7 to 0.15.8. - [Release notes](https://github.com/arrow-py/arrow/releases) - [Changelog](https://github.com/arrow-py/arrow/blob/master/CHANGELOG.rst) - [Commits](https://github.com/arrow-py/arrow/compare/0.15.7...0.15.8) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index d5c5fd832..63b2c0441 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -3,7 +3,7 @@ ccxt==1.31.37 SQLAlchemy==1.3.18 python-telegram-bot==12.8 -arrow==0.15.7 +arrow==0.15.8 cachetools==4.1.1 requests==2.24.0 urllib3==1.25.9 From d1d6f69e43b31491edfc01efb74a12da1349e57e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 26 Jul 2020 08:37:13 +0000 Subject: [PATCH 0330/1197] Bump scipy from 1.5.1 to 1.5.2 Bumps [scipy](https://github.com/scipy/scipy) from 1.5.1 to 1.5.2. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.5.1...v1.5.2) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 4773d9877..ce08f08e0 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.5.1 +scipy==1.5.2 scikit-learn==0.23.1 scikit-optimize==0.7.4 filelock==3.0.12 From 2ff03e173d3634128f47666a3777e0f838ad9374 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 26 Jul 2020 08:37:17 +0000 Subject: [PATCH 0331/1197] Bump numpy from 1.19.0 to 1.19.1 Bumps [numpy](https://github.com/numpy/numpy) from 1.19.0 to 1.19.1. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/master/doc/HOWTO_RELEASE.rst.txt) - [Commits](https://github.com/numpy/numpy/compare/v1.19.0...v1.19.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1e61d165f..2392d4cb2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Load common requirements -r requirements-common.txt -numpy==1.19.0 +numpy==1.19.1 pandas==1.0.5 From 838743bf01590b885e5bb1ddf5cacf009d02f673 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 26 Jul 2020 08:37:25 +0000 Subject: [PATCH 0332/1197] Bump mkdocs-material from 5.4.0 to 5.5.0 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 5.4.0 to 5.5.0. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/5.4.0...5.5.0) Signed-off-by: dependabot[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 3a236ee87..2a2405f8e 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==5.4.0 +mkdocs-material==5.5.0 mdx_truly_sane_lists==1.2 From 63e7490a55baae19beed1652c0bb0ba7c15e0cc9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 26 Jul 2020 08:37:45 +0000 Subject: [PATCH 0333/1197] Bump plotly from 4.8.2 to 4.9.0 Bumps [plotly](https://github.com/plotly/plotly.py) from 4.8.2 to 4.9.0. - [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.8.2...v4.9.0) Signed-off-by: dependabot[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index ec5af3dbf..51d14d636 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.8.2 +plotly==4.9.0 From b4d22f10001f8ad2c977bf1e47a79e39fc5094b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 26 Jul 2020 08:53:36 +0000 Subject: [PATCH 0334/1197] Bump urllib3 from 1.25.9 to 1.25.10 Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.25.9 to 1.25.10. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/master/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.25.9...1.25.10) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 63b2c0441..e0a9af77e 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -6,7 +6,7 @@ python-telegram-bot==12.8 arrow==0.15.8 cachetools==4.1.1 requests==2.24.0 -urllib3==1.25.9 +urllib3==1.25.10 wrapt==1.12.1 jsonschema==3.2.0 TA-Lib==0.4.18 From dbcccac6cd08bac45cec087f691300b77e962a2b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 26 Jul 2020 08:53:51 +0000 Subject: [PATCH 0335/1197] Bump ccxt from 1.31.37 to 1.32.3 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.31.37 to 1.32.3. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.31.37...1.32.3) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 63b2c0441..db06fee98 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.31.37 +ccxt==1.32.3 SQLAlchemy==1.3.18 python-telegram-bot==12.8 arrow==0.15.8 From 902e8fa62fce59c77bc809f4edad640e727c6f15 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Jul 2020 14:39:00 +0200 Subject: [PATCH 0336/1197] Fix wrong spelling in one subcomponent --- freqtrade/optimize/optimize_reports.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 3a42ba4a9..7e0a60566 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -142,7 +142,7 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List 'profit_sum': profit_sum, 'profit_sum_pct': round(profit_sum * 100, 2), 'profit_total_abs': result['profit_abs'].sum(), - 'profit_pct_total': profit_percent_tot, + 'profit_total_pct': profit_percent_tot, } ) return tabular_data @@ -338,7 +338,7 @@ def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_curren output = [[ t['sell_reason'], t['trades'], t['wins'], t['draws'], t['losses'], - t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], t['profit_pct_total'], + t['profit_mean_pct'], t['profit_sum_pct'], t['profit_total_abs'], t['profit_total_pct'], ] for t in sell_reason_stats] return tabulate(output, headers=headers, tablefmt="orgtbl", stralign="right") From 9ed5fed88738214c5b22acfe0fb0c8b7d72cc2ac Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Jul 2020 15:17:54 +0200 Subject: [PATCH 0337/1197] Fix output format to be of an identical type --- freqtrade/optimize/optimize_reports.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 7e0a60566..2db941db4 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -208,9 +208,9 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: 'draw_days': draw_days, 'losing_days': losing_days, 'winner_holding_avg': (timedelta(minutes=round(winning_trades['trade_duration'].mean())) - if not winning_trades.empty else '0:00'), + if not winning_trades.empty else timedelta()), 'loser_holding_avg': (timedelta(minutes=round(losing_trades['trade_duration'].mean())) - if not losing_trades.empty else '0:00'), + if not losing_trades.empty else timedelta()), } From 8d0f338bf2eed48e5ec9d61a9a98ddf0ec575502 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Jul 2020 15:23:21 +0200 Subject: [PATCH 0338/1197] Timestamps should be in ms --- freqtrade/optimize/optimize_reports.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 2db941db4..c0e1347cc 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -253,9 +253,9 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], 'left_open_trades': left_open_results, 'total_trades': len(results), 'backtest_start': min_date.datetime, - 'backtest_start_ts': min_date.timestamp, + 'backtest_start_ts': min_date.timestamp * 1000, 'backtest_end': max_date.datetime, - 'backtest_end_ts': max_date.timestamp, + 'backtest_end_ts': max_date.timestamp * 1000, 'backtest_days': backtest_days, 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else None, @@ -272,9 +272,9 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], strat_stats.update({ 'max_drawdown': max_drawdown, 'drawdown_start': drawdown_start, - 'drawdown_start_ts': drawdown_start.timestamp(), + 'drawdown_start_ts': drawdown_start.timestamp() * 1000, 'drawdown_end': drawdown_end, - 'drawdown_end_ts': drawdown_end.timestamp(), + 'drawdown_end_ts': drawdown_end.timestamp() * 1000, }) except ValueError: strat_stats.update({ From 454046f74596123f7b0b3ba3f2a96f3d8dce32ef Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Jul 2020 15:55:54 +0200 Subject: [PATCH 0339/1197] Add stake_currency and max_opeN_trades to backtest result --- freqtrade/optimize/optimize_reports.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index c0e1347cc..587ed303d 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -262,6 +262,8 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], 'market_change': market_change, 'pairlist': list(btdata.keys()), 'stake_amount': config['stake_amount'], + 'stake_currency': config['stake_currency'], + 'max_open_trades': config['max_open_trades'], **daily_stats, } result['strategy'][strategy] = strat_stats From 977a6d4e9cd8388eaf9a926aa7beaeb688573b3d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Jul 2020 16:10:48 +0200 Subject: [PATCH 0340/1197] Add profit_total to results line --- freqtrade/optimize/optimize_reports.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 587ed303d..6e0f9acea 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -72,6 +72,7 @@ def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: 'profit_sum': result['profit_percent'].sum(), 'profit_sum_pct': result['profit_percent'].sum() * 100.0, 'profit_total_abs': result['profit_abs'].sum(), + 'profit_total': result['profit_percent'].sum() / max_open_trades, 'profit_total_pct': result['profit_percent'].sum() * 100.0 / max_open_trades, 'duration_avg': str(timedelta( minutes=round(result['trade_duration'].mean())) From aab5596fa6ed145a5bf3afb9a700272704bffd9b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 Jul 2020 07:20:40 +0200 Subject: [PATCH 0341/1197] Convert trade open / close to timestamp (to allow uniform analysis of backtest and real trade data - while giving control of date-formatting to the endsystem. --- freqtrade/optimize/optimize_reports.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 6e0f9acea..f917e7cab 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -5,6 +5,7 @@ from typing import Any, Dict, List from arrow import Arrow from pandas import DataFrame +from numpy import int64 from tabulate import tabulate from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN @@ -246,6 +247,9 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], skip_nan=True) daily_stats = generate_daily_stats(results) + results['open_timestamp'] = results['open_date'].astype(int64) // 1e6 + results['close_timestamp'] = results['close_date'].astype(int64) // 1e6 + backtest_days = (max_date - min_date).days strat_stats = { 'trades': results.to_dict(orient='records'), From 7318d02ebc1331adba985d4c2525344c262437b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2020 07:05:17 +0000 Subject: [PATCH 0342/1197] Bump ccxt from 1.32.3 to 1.32.7 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.32.3 to 1.32.7. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.32.3...1.32.7) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index ba41d3ac2..942fe3792 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.32.3 +ccxt==1.32.7 SQLAlchemy==1.3.18 python-telegram-bot==12.8 arrow==0.15.8 From 14cb29aae1d5fba8fe7d735ddfff7f0aa451434a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Jul 2020 08:16:55 +0200 Subject: [PATCH 0343/1197] Add test for remove_pumps in edge --- tests/edge/test_edge.py | 95 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index cf9cb6fe1..7373778ad 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -409,3 +409,98 @@ def test_process_expectancy(mocker, edge_conf, fee, risk_reward_ratio, expectanc final = edge._process_expectancy(trades_df) assert len(final) == 0 assert isinstance(final, dict) + + +def test_process_expectancy_remove_pumps(mocker, edge_conf, fee,): + edge_conf['edge']['min_trade_number'] = 2 + edge_conf['edge']['remove_pumps'] = True + freqtrade = get_patched_freqtradebot(mocker, edge_conf) + + freqtrade.exchange.get_fee = fee + edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) + + trades = [ + {'pair': 'TEST/BTC', + 'stoploss': -0.9, + 'profit_percent': '', + 'profit_abs': '', + 'open_time': np.datetime64('2018-10-03T00:05:00.000000000'), + 'close_time': np.datetime64('2018-10-03T00:10:00.000000000'), + 'open_index': 1, + 'close_index': 1, + 'trade_duration': '', + 'open_rate': 17, + 'close_rate': 15, + 'exit_type': 'sell_signal'}, + + {'pair': 'TEST/BTC', + 'stoploss': -0.9, + 'profit_percent': '', + 'profit_abs': '', + 'open_time': np.datetime64('2018-10-03T00:20:00.000000000'), + 'close_time': np.datetime64('2018-10-03T00:25:00.000000000'), + 'open_index': 4, + 'close_index': 4, + 'trade_duration': '', + 'open_rate': 20, + 'close_rate': 10, + 'exit_type': 'sell_signal'}, + {'pair': 'TEST/BTC', + 'stoploss': -0.9, + 'profit_percent': '', + 'profit_abs': '', + 'open_time': np.datetime64('2018-10-03T00:20:00.000000000'), + 'close_time': np.datetime64('2018-10-03T00:25:00.000000000'), + 'open_index': 4, + 'close_index': 4, + 'trade_duration': '', + 'open_rate': 20, + 'close_rate': 10, + 'exit_type': 'sell_signal'}, + {'pair': 'TEST/BTC', + 'stoploss': -0.9, + 'profit_percent': '', + 'profit_abs': '', + 'open_time': np.datetime64('2018-10-03T00:20:00.000000000'), + 'close_time': np.datetime64('2018-10-03T00:25:00.000000000'), + 'open_index': 4, + 'close_index': 4, + 'trade_duration': '', + 'open_rate': 20, + 'close_rate': 10, + 'exit_type': 'sell_signal'}, + {'pair': 'TEST/BTC', + 'stoploss': -0.9, + 'profit_percent': '', + 'profit_abs': '', + 'open_time': np.datetime64('2018-10-03T00:20:00.000000000'), + 'close_time': np.datetime64('2018-10-03T00:25:00.000000000'), + 'open_index': 4, + 'close_index': 4, + 'trade_duration': '', + 'open_rate': 20, + 'close_rate': 10, + 'exit_type': 'sell_signal'}, + + {'pair': 'TEST/BTC', + 'stoploss': -0.9, + 'profit_percent': '', + 'profit_abs': '', + 'open_time': np.datetime64('2018-10-03T00:30:00.000000000'), + 'close_time': np.datetime64('2018-10-03T00:40:00.000000000'), + 'open_index': 6, + 'close_index': 7, + 'trade_duration': '', + 'open_rate': 26, + 'close_rate': 134, + 'exit_type': 'sell_signal'} + ] + + trades_df = DataFrame(trades) + trades_df = edge._fill_calculable_fields(trades_df) + final = edge._process_expectancy(trades_df) + + assert 'TEST/BTC' in final + assert final['TEST/BTC'].stoploss == -0.9 + assert final['TEST/BTC'].nb_trades == len(trades_df) - 1 + assert round(final['TEST/BTC'].winrate, 10) == 0.0 From d1cbc567e4895111f877827c9007030a8cf843c1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Jul 2020 13:41:09 +0200 Subject: [PATCH 0344/1197] Fix filtering for bumped pairs --- freqtrade/edge/edge_positioning.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 41252ee51..1993eded3 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -281,8 +281,8 @@ class Edge: # # Removing Pumps if self.edge_config.get('remove_pumps', False): - results = results.groupby(['pair', 'stoploss']).apply( - lambda x: x[x['profit_abs'] < 2 * x['profit_abs'].std() + x['profit_abs'].mean()]) + results = results[results['profit_abs'] < 2 * results['profit_abs'].std() + + results['profit_abs'].mean()] ########################################################################## # Removing trades having a duration more than X minutes (set in config) From 071e82043aabb4e63aa4fdbe8426ceadb449c015 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Aug 2020 15:59:50 +0200 Subject: [PATCH 0345/1197] Better handle cancelled buy orders --- freqtrade/exchange/exchange.py | 2 +- freqtrade/freqtradebot.py | 6 ++++++ tests/exchange/test_exchange.py | 2 +- tests/test_freqtradebot.py | 29 +++++++++++++++++++++++------ 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 04ad10a68..c3779ff8e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -999,7 +999,7 @@ class Exchange: if self.is_cancel_order_result_suitable(corder): return corder except InvalidOrderException: - logger.warning(f"Could not cancel order {order_id}.") + logger.warning(f"Could not cancel order {order_id} for {pair}.") try: order = self.fetch_order(order_id, pair) except InvalidOrderException: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a6d96ef77..b866842b7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -976,6 +976,12 @@ class FreqtradeBot: reason = constants.CANCEL_REASON['TIMEOUT'] corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, trade.amount) + # Avoid race condition where the order could not be cancelled coz its already filled. + # Simply bailing here is the only safe way - as this order will then be + # handled in the next iteration. + if corder.get('status') not in ('canceled', 'closed'): + logger.warning(f"Order {trade.open_order_id} for {trade.pair} not cancelled.") + return False else: # Order was cancelled already, so we can reuse the existing dict corder = order diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 60c4847f6..4f5cfa9e1 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1818,7 +1818,7 @@ def test_cancel_order_with_result_error(default_conf, mocker, exchange_name, cap res = exchange.cancel_order_with_result('1234', 'ETH/BTC', 1541) assert isinstance(res, dict) - assert log_has("Could not cancel order 1234.", caplog) + assert log_has("Could not cancel order 1234 for ETH/BTC.", caplog) assert log_has("Could not fetch cancelled order 1234.", caplog) assert res['amount'] == 1541 diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index fd57eae6f..d93672ea7 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2023,11 +2023,16 @@ def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_or rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock(return_value=limit_buy_order_old) + cancel_buy_order = deepcopy(limit_buy_order_old) + cancel_buy_order['status'] = 'canceled' + cancel_order_wr_mock = MagicMock(return_value=cancel_buy_order) + patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, fetch_order=MagicMock(return_value=limit_buy_order_old), + cancel_order_with_result=cancel_order_wr_mock, cancel_order=cancel_order_mock, get_fee=fee ) @@ -2060,7 +2065,7 @@ def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_or freqtrade.strategy.check_buy_timeout = MagicMock(return_value=True) # Trade should be closed since the function returns true freqtrade.check_handle_timedout() - assert cancel_order_mock.call_count == 1 + assert cancel_order_wr_mock.call_count == 1 assert rpc_mock.call_count == 1 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() nb_trades = len(trades) @@ -2071,7 +2076,9 @@ def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_or def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, open_trade, fee, mocker) -> None: rpc_mock = patch_RPCManager(mocker) - cancel_order_mock = MagicMock(return_value=limit_buy_order_old) + limit_buy_cancel = deepcopy(limit_buy_order_old) + limit_buy_cancel['status'] = 'canceled' + cancel_order_mock = MagicMock(return_value=limit_buy_cancel) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -2259,7 +2266,10 @@ def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old, def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial, open_trade, mocker) -> None: rpc_mock = patch_RPCManager(mocker) - cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial) + limit_buy_canceled = deepcopy(limit_buy_order_old_partial) + limit_buy_canceled['status'] = 'canceled' + + cancel_order_mock = MagicMock(return_value=limit_buy_canceled) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -2392,7 +2402,11 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> None: patch_RPCManager(mocker) patch_exchange(mocker) - cancel_order_mock = MagicMock(return_value=limit_buy_order) + cancel_buy_order = deepcopy(limit_buy_order) + cancel_buy_order['status'] = 'canceled' + del cancel_buy_order['filled'] + + cancel_order_mock = MagicMock(return_value=cancel_buy_order) mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock) freqtrade = FreqtradeBot(default_conf) @@ -2412,9 +2426,12 @@ def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> Non assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) assert cancel_order_mock.call_count == 1 - limit_buy_order['filled'] = 2 - mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException) + # Order remained open for some reason (cancel failed) + cancel_buy_order['status'] = 'open' + cancel_order_mock = MagicMock(return_value=cancel_buy_order) + mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock) assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) + assert log_has_re(r"Order .* for .* not cancelled.", caplog) @pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'], From 99bfa839ebcc6673ec435c53250b27b0ca9d437b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Aug 2020 10:12:15 +0200 Subject: [PATCH 0346/1197] Improve logging for sell exception --- freqtrade/freqtradebot.py | 2 +- tests/test_freqtradebot.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b866842b7..967f68b90 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -660,7 +660,7 @@ class FreqtradeBot: trades_closed += 1 except DependencyException as exception: - logger.warning('Unable to sell trade: %s', exception) + logger.warning('Unable to sell trade %s: %s', trade.pair, exception) # Updating wallets if any trade occured if trades_closed: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index d93672ea7..15dd5d4ee 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1660,6 +1660,7 @@ def test_exit_positions_exception(mocker, default_conf, limit_buy_order, caplog) trade = MagicMock() trade.open_order_id = None trade.open_fee = 0.001 + trade.pair = 'ETH/BTC' trades = [trade] # Test raise of DependencyException exception @@ -1669,7 +1670,7 @@ def test_exit_positions_exception(mocker, default_conf, limit_buy_order, caplog) ) n = freqtrade.exit_positions(trades) assert n == 0 - assert log_has('Unable to sell trade: ', caplog) + assert log_has('Unable to sell trade ETH/BTC: ', caplog) def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> None: From 6c77feee8501a5290a4e6941ae873a31da92edcf Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Aug 2020 10:18:19 +0200 Subject: [PATCH 0347/1197] Improve some exchange logs --- freqtrade/exchange/exchange.py | 4 ++-- tests/exchange/test_exchange.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c3779ff8e..dcdb36c84 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1022,10 +1022,10 @@ class Exchange: return self._api.fetch_order(order_id, pair) except ccxt.OrderNotFound as e: raise RetryableOrderError( - f'Order not found (id: {order_id}). Message: {e}') from e + f'Order not found (pair: {pair} id: {order_id}). Message: {e}') from e except ccxt.InvalidOrder as e: raise InvalidOrderException( - f'Tried to get an invalid order (id: {order_id}). Message: {e}') from e + f'Tried to get an invalid order (pair: {pair} id: {order_id}). Message: {e}') from e except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 4f5cfa9e1..673399fa6 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2315,6 +2315,18 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None: (3, 3, 1), (0, 1, 2), (1, 1, 1), + (0, 4, 17), + (1, 4, 10), + (2, 4, 5), + (3, 4, 2), + (4, 4, 1), + (0, 5, 26), + (1, 5, 17), + (2, 5, 10), + (3, 5, 5), + (4, 5, 2), + (5, 5, 1), + ]) def test_calculate_backoff(retrycount, max_retries, expected): assert calculate_backoff(retrycount, max_retries) == expected From 3915101d2d8d0a7c4d3f0bfcc547cddbefa27205 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 2 Aug 2020 10:24:10 +0200 Subject: [PATCH 0348/1197] Add more backoff to fetch_order endpoint --- freqtrade/exchange/exchange.py | 2 +- freqtrade/exchange/ftx.py | 2 +- tests/exchange/test_exchange.py | 5 +++-- tests/exchange/test_ftx.py | 1 + 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index dcdb36c84..ec787ca3a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1008,7 +1008,7 @@ class Exchange: return order - @retrier + @retrier(retries=5) def fetch_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: try: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index b75f77ca4..01e8267ad 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -78,7 +78,7 @@ class Ftx(Exchange): except ccxt.BaseError as e: raise OperationalException(e) from e - @retrier + @retrier(retries=5) def fetch_stoploss_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: try: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 673399fa6..350c2d3cb 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1896,10 +1896,10 @@ def test_fetch_order(default_conf, mocker, exchange_name): assert tm.call_args_list[1][0][0] == 2 assert tm.call_args_list[2][0][0] == 5 assert tm.call_args_list[3][0][0] == 10 - assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1 + assert api_mock.fetch_order.call_count == 6 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, - 'fetch_order', 'fetch_order', + 'fetch_order', 'fetch_order', retries=6, order_id='_', pair='TKN/BTC') @@ -1932,6 +1932,7 @@ def test_fetch_stoploss_order(default_conf, mocker, exchange_name): ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, 'fetch_stoploss_order', 'fetch_order', + retries=6, order_id='_', pair='TKN/BTC') diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index eb7d83be3..bed92d276 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -154,4 +154,5 @@ def test_fetch_stoploss_order(default_conf, mocker): ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'ftx', 'fetch_stoploss_order', 'fetch_orders', + retries=6, order_id='_', pair='TKN/BTC') From 5ff09a06c7a74875184aea4cfc159d32825b4566 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2020 07:17:30 +0000 Subject: [PATCH 0349/1197] Bump pytest from 5.4.3 to 6.0.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.4.3 to 6.0.1. - [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.4.3...6.0.1) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 9f9be638d..c02a439d3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.8.3 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 mypy==0.782 -pytest==5.4.3 +pytest==6.0.1 pytest-asyncio==0.14.0 pytest-cov==2.10.0 pytest-mock==3.2.0 From 809b3ddafc5863caab228cd75c10f5ae8e496e5a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2020 07:17:31 +0000 Subject: [PATCH 0350/1197] Bump mkdocs-material from 5.5.0 to 5.5.1 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 5.5.0 to 5.5.1. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/5.5.0...5.5.1) Signed-off-by: dependabot[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 2a2405f8e..c30661b6a 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==5.5.0 +mkdocs-material==5.5.1 mdx_truly_sane_lists==1.2 From 1855a444fa29f574aa29f5f6e936c9bfd1d879e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2020 07:17:32 +0000 Subject: [PATCH 0351/1197] Bump pandas from 1.0.5 to 1.1.0 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.0.5 to 1.1.0. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.0.5...v1.1.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2392d4cb2..d65f90325 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ -r requirements-common.txt numpy==1.19.1 -pandas==1.0.5 +pandas==1.1.0 From b3f04d89d259c49f8db954aababe9e812f6f4ca6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Aug 2020 07:17:50 +0000 Subject: [PATCH 0352/1197] Bump ccxt from 1.32.7 to 1.32.45 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.32.7 to 1.32.45. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.32.7...1.32.45) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 942fe3792..62cde9dbc 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.32.7 +ccxt==1.32.45 SQLAlchemy==1.3.18 python-telegram-bot==12.8 arrow==0.15.8 From a33346c6b660c518a70bdde9a56dc805046d08b8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 Aug 2020 19:21:46 +0200 Subject: [PATCH 0353/1197] Fix testing errors - which surfaced with pytest 6.0.1 --- freqtrade/exchange/exchange.py | 4 ++-- tests/test_freqtradebot.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 04ad10a68..c3fe3e601 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -258,8 +258,8 @@ class Exchange: api.urls['api'] = api.urls['test'] logger.info("Enabled Sandbox API on %s", name) else: - logger.warning(name, "No Sandbox URL in CCXT, exiting. " - "Please check your config.json") + logger.warning(f"No Sandbox URL in CCXT for {name}, exiting. " + "Please check your config.json") raise OperationalException(f'Exchange {name} does not provide a sandbox api') def _load_async_markets(self, reload: bool = False) -> None: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index fd57eae6f..dcddf34e3 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1726,6 +1726,7 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_ amount=amount, exchange='binance', open_rate=0.245441, + open_date=arrow.utcnow().datetime, fee_open=fee.return_value, fee_close=fee.return_value, open_order_id="123456", @@ -1816,6 +1817,7 @@ def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_orde open_rate=0.245441, fee_open=0.0025, fee_close=0.0025, + open_date=arrow.utcnow().datetime, open_order_id="123456", is_open=True, ) From a3688b159fcf703a5cc390d0ae70d884db717762 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 3 Aug 2020 19:28:57 +0200 Subject: [PATCH 0354/1197] Improve formatting --- freqtrade/exchange/exchange.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c3fe3e601..c4ed75878 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -258,8 +258,8 @@ class Exchange: api.urls['api'] = api.urls['test'] logger.info("Enabled Sandbox API on %s", name) else: - logger.warning(f"No Sandbox URL in CCXT for {name}, exiting. " - "Please check your config.json") + logger.warning( + f"No Sandbox URL in CCXT for {name}, exiting. Please check your config.json") raise OperationalException(f'Exchange {name} does not provide a sandbox api') def _load_async_markets(self, reload: bool = False) -> None: From 215972c68f3efa6397a6d5a6b17fa2c4ea1a3bea Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 Aug 2020 14:41:22 +0200 Subject: [PATCH 0355/1197] Implement /delete for api-server --- freqtrade/rpc/api_server.py | 11 +++++++++++ freqtrade/rpc/rpc.py | 4 ++-- freqtrade/rpc/telegram.py | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 351842e10..f7481fee4 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -200,6 +200,8 @@ class ApiServer(RPC): view_func=self._ping, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/trades', 'trades', view_func=self._trades, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/trades/', 'trades_delete', + view_func=self._trades_delete, methods=['DELETE']) # Combined actions and infos self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist, methods=['GET', 'POST']) @@ -424,6 +426,15 @@ class ApiServer(RPC): results = self._rpc_trade_history(limit) return self.rest_dump(results) + @require_login + @rpc_catch_errors + def _trades_delete(self, tradeid): + """ + Handler for DELETE /trades/ endpoint. + """ + result = self._rpc_delete(tradeid) + return self.rest_dump(result) + @require_login @rpc_catch_errors def _whitelist(self): diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index f6627ed16..b79cac243 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -537,7 +537,7 @@ class RPC: return trade else: return None - + def _rpc_delete(self, trade_id: str) -> Dict[str, str]: """ Handler for delete . @@ -558,7 +558,7 @@ class RPC: _exec_delete(trade) Trade.session.flush() self._freqtrade.wallets.update() - return {'result': f'Deleted trade {trade_id}.'} + return {'result_msg': f'Deleted trade {trade_id}.'} def _rpc_performance(self) -> List[Dict[str, Any]]: """ diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 20c1cc9dc..293e3a686 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -547,7 +547,7 @@ class Telegram(RPC): trade_id = context.args[0] if len(context.args) > 0 else None try: msg = self._rpc_delete(trade_id) - self._send_msg('Delete Result: `{result}`'.format(**msg)) + self._send_msg('Delete Result: `{result_msg}`'.format(**msg)) except RPCException as e: self._send_msg(str(e)) From 26c7341b7d712b4f1bb671722ee0bf0f6e55a42b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 Aug 2020 14:41:38 +0200 Subject: [PATCH 0356/1197] Add test for api-server DELETE trade --- tests/rpc/test_rpc_apiserver.py | 38 ++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index f4d7b8ca3..99f17383f 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -50,6 +50,12 @@ def client_get(client, url): 'Origin': 'http://example.com'}) +def client_delete(client, url): + # Add fake Origin to ensure CORS kicks in + return client.delete(url, headers={'Authorization': _basic_auth_str(_TEST_USER, _TEST_PASS), + 'Origin': 'http://example.com'}) + + def assert_response(response, expected_code=200, needs_cors=True): assert response.status_code == expected_code assert response.content_type == "application/json" @@ -352,7 +358,7 @@ def test_api_daily(botclient, mocker, ticker, fee, markets): assert rc.json['data'][0]['date'] == str(datetime.utcnow().date()) -def test_api_trades(botclient, mocker, ticker, fee, markets): +def test_api_trades(botclient, mocker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) mocker.patch.multiple( @@ -376,6 +382,36 @@ def test_api_trades(botclient, mocker, ticker, fee, markets): assert rc.json['trades_count'] == 1 +def test_api_delete_trade(botclient, mocker, fee, markets): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets) + ) + rc = client_delete(client, f"{BASE_URI}/trades/1") + # Error - trade won't exist yet. + assert_response(rc, 502) + + create_mock_trades(fee) + trades = Trade.query.all() + assert len(trades) > 2 + + rc = client_delete(client, f"{BASE_URI}/trades/1") + assert_response(rc) + assert rc.json['result_msg'] == 'Deleted trade 1.' + assert len(trades) - 1 == len(Trade.query.all()) + + rc = client_delete(client, f"{BASE_URI}/trades/1") + # Trade is gone now. + assert_response(rc, 502) + assert len(trades) - 1 == len(Trade.query.all()) + rc = client_delete(client, f"{BASE_URI}/trades/2") + assert_response(rc) + assert rc.json['result_msg'] == 'Deleted trade 2.' + assert len(trades) - 2 == len(Trade.query.all()) + + def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) From 4b0164770c50d3efdef5fb979bdcf71452f01ef6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 4 Aug 2020 14:49:59 +0200 Subject: [PATCH 0357/1197] Add test for /delete --- freqtrade/rpc/telegram.py | 4 ++-- tests/rpc/test_rpc_telegram.py | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 293e3a686..19d3e1bd9 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -94,7 +94,7 @@ class Telegram(RPC): CommandHandler('forcesell', self._forcesell), CommandHandler('forcebuy', self._forcebuy), CommandHandler('trades', self._trades), - CommandHandler('delete', self._delete), + CommandHandler('delete', self._delete_trade), CommandHandler('performance', self._performance), CommandHandler('daily', self._daily), CommandHandler('count', self._count), @@ -535,7 +535,7 @@ class Telegram(RPC): self._send_msg(str(e)) @authorized_only - def _delete(self, update: Update, context: CallbackContext) -> None: + def _delete_trade(self, update: Update, context: CallbackContext) -> None: """ Handler for /delete . Delete the given trade diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 62a4f49a1..c8ac05d0d 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1177,6 +1177,33 @@ def test_telegram_trades(mocker, update, default_conf, fee): assert "

    " in msg_mock.call_args_list[0][0][0]
     
     
    +def test_telegram_delete_trade(mocker, update, default_conf, fee):
    +    msg_mock = MagicMock()
    +    mocker.patch.multiple(
    +        'freqtrade.rpc.telegram.Telegram',
    +        _init=MagicMock(),
    +        _send_msg=msg_mock
    +    )
    +
    +    freqtradebot = get_patched_freqtradebot(mocker, default_conf)
    +    telegram = Telegram(freqtradebot)
    +    context = MagicMock()
    +    context.args = []
    +
    +    telegram._delete_trade(update=update, context=context)
    +    assert "invalid argument" in msg_mock.call_args_list[0][0][0]
    +
    +    msg_mock.reset_mock()
    +    create_mock_trades(fee)
    +
    +    context = MagicMock()
    +    context.args = [1]
    +    telegram._delete_trade(update=update, context=context)
    +    msg_mock.call_count == 1
    +    assert "Delete Result" in msg_mock.call_args_list[0][0][0]
    +    assert "Deleted trade 1." in msg_mock.call_args_list[0][0][0]
    +
    +
     def test_help_handle(default_conf, update, mocker) -> None:
         msg_mock = MagicMock()
         mocker.patch.multiple(
    
    From b954af33cfa06387684297c3f6cce35813603414 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Tue, 4 Aug 2020 16:01:41 +0200
    Subject: [PATCH 0358/1197] Fix type erorr in callable
    
    ---
     freqtrade/rpc/api_server.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py
    index f7481fee4..f28a35ff0 100644
    --- a/freqtrade/rpc/api_server.py
    +++ b/freqtrade/rpc/api_server.py
    @@ -56,7 +56,7 @@ def require_login(func: Callable[[Any, Any], Any]):
     
     
     # Type should really be Callable[[ApiServer], Any], but that will create a circular dependency
    -def rpc_catch_errors(func: Callable[[Any], Any]):
    +def rpc_catch_errors(func: Callable[..., Any]):
     
         def func_wrapper(obj, *args, **kwargs):
     
    
    From 9163c7f3d3bb0854bcaceea5cef2980ea8ef6a19 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Tue, 4 Aug 2020 19:43:05 +0200
    Subject: [PATCH 0359/1197] Improve api response
    
    ---
     freqtrade/rpc/api_server.py     |  4 ++++
     tests/rpc/test_rpc_apiserver.py | 17 ++++++++++++++---
     2 files changed, 18 insertions(+), 3 deletions(-)
    
    diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py
    index f28a35ff0..06926ac35 100644
    --- a/freqtrade/rpc/api_server.py
    +++ b/freqtrade/rpc/api_server.py
    @@ -431,6 +431,10 @@ class ApiServer(RPC):
         def _trades_delete(self, tradeid):
             """
             Handler for DELETE /trades/ endpoint.
    +        Removes the trade from the database (tries to cancel open orders first!)
    +        get:
    +          param:
    +            tradeid: Numeric trade-id assigned to the trade.
             """
             result = self._rpc_delete(tradeid)
             return self.rest_dump(result)
    diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py
    index 99f17383f..ccdec6fdc 100644
    --- a/tests/rpc/test_rpc_apiserver.py
    +++ b/tests/rpc/test_rpc_apiserver.py
    @@ -385,31 +385,42 @@ def test_api_trades(botclient, mocker, fee, markets):
     def test_api_delete_trade(botclient, mocker, fee, markets):
         ftbot, client = botclient
         patch_get_signal(ftbot, (True, False))
    +    stoploss_mock = MagicMock()
    +    cancel_mock = MagicMock()
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
    -        markets=PropertyMock(return_value=markets)
    +        markets=PropertyMock(return_value=markets),
    +        cancel_order=cancel_mock,
    +        cancel_stoploss_order=stoploss_mock,
         )
         rc = client_delete(client, f"{BASE_URI}/trades/1")
         # Error - trade won't exist yet.
         assert_response(rc, 502)
     
         create_mock_trades(fee)
    +    ftbot.strategy.order_types['stoploss_on_exchange'] = True
         trades = Trade.query.all()
    +    trades[1].stoploss_order_id = '1234'
         assert len(trades) > 2
     
         rc = client_delete(client, f"{BASE_URI}/trades/1")
         assert_response(rc)
    -    assert rc.json['result_msg'] == 'Deleted trade 1.'
    +    assert rc.json['result_msg'] == 'Deleted trade 1. Closed 1 open orders.'
         assert len(trades) - 1 == len(Trade.query.all())
    +    assert cancel_mock.call_count == 1
     
    +    cancel_mock.reset_mock()
         rc = client_delete(client, f"{BASE_URI}/trades/1")
         # Trade is gone now.
         assert_response(rc, 502)
    +    assert cancel_mock.call_count == 0
    +
         assert len(trades) - 1 == len(Trade.query.all())
         rc = client_delete(client, f"{BASE_URI}/trades/2")
         assert_response(rc)
    -    assert rc.json['result_msg'] == 'Deleted trade 2.'
    +    assert rc.json['result_msg'] == 'Deleted trade 2. Closed 2 open orders.'
         assert len(trades) - 2 == len(Trade.query.all())
    +    assert stoploss_mock.call_count == 1
     
     
     def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
    
    From 817f5289db94895a6db6e0b045fcf40aa75047b1 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Tue, 4 Aug 2020 19:43:22 +0200
    Subject: [PATCH 0360/1197] /delete should Cancel open orders (and stoploss
     orders)
    
    ---
     freqtrade/rpc/rpc.py  | 46 +++++++++++++++++++++++-----------
     tests/rpc/test_rpc.py | 57 ++++++++++++++++++++++++++++++++++++++++++-
     2 files changed, 88 insertions(+), 15 deletions(-)
    
    diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
    index b79cac243..58fdebe85 100644
    --- a/freqtrade/rpc/rpc.py
    +++ b/freqtrade/rpc/rpc.py
    @@ -11,9 +11,9 @@ from typing import Any, Dict, List, Optional, Tuple
     import arrow
     from numpy import NAN, mean
     
    -from freqtrade.exceptions import ExchangeError, PricingError
    -
    -from freqtrade.exchange import timeframe_to_msecs, timeframe_to_minutes
    +from freqtrade.exceptions import (ExchangeError, InvalidOrderException,
    +                                  PricingError)
    +from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
     from freqtrade.misc import shorten_date
     from freqtrade.persistence import Trade
     from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
    @@ -541,24 +541,42 @@ class RPC:
         def _rpc_delete(self, trade_id: str) -> Dict[str, str]:
             """
             Handler for delete .
    -        Delete the given trade
    +        Delete the given trade and close eventually existing open orders.
             """
    -        def _exec_delete(trade: Trade) -> None:
    -            Trade.session.delete(trade)
    -            Trade.session.flush()
    -
             with self._freqtrade._sell_lock:
    -            trade = Trade.get_trades(
    -                trade_filter=[Trade.id == trade_id, ]
    -            ).first()
    +            c_count = 0
    +            trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first()
                 if not trade:
    -                logger.warning('delete: Invalid argument received')
    +                logger.warning('delete trade: Invalid argument received')
                     raise RPCException('invalid argument')
     
    -            _exec_delete(trade)
    +            # Try cancelling regular order if that exists
    +            if trade.open_order_id:
    +                try:
    +                    self._freqtrade.exchange.cancel_order(trade.open_order_id, trade.pair)
    +                    c_count += 1
    +                except (ExchangeError, InvalidOrderException):
    +                    pass
    +
    +            # cancel stoploss on exchange ...
    +            if (self._freqtrade.strategy.order_types.get('stoploss_on_exchange')
    +                    and trade.stoploss_order_id):
    +                try:
    +                    self._freqtrade.exchange.cancel_stoploss_order(trade.stoploss_order_id,
    +                                                                   trade.pair)
    +                    c_count += 1
    +                except (ExchangeError, InvalidOrderException):
    +                    pass
    +
    +            Trade.session.delete(trade)
                 Trade.session.flush()
                 self._freqtrade.wallets.update()
    -            return {'result_msg': f'Deleted trade {trade_id}.'}
    +            return {
    +                'result': 'success',
    +                'trade_id': trade_id,
    +                'result_msg': f'Deleted trade {trade_id}. Closed {c_count} open orders.',
    +                'cancel_order_count': c_count,
    +            }
     
         def _rpc_performance(self) -> List[Dict[str, Any]]:
             """
    diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py
    index e5859fcd9..b4a781459 100644
    --- a/tests/rpc/test_rpc.py
    +++ b/tests/rpc/test_rpc.py
    @@ -8,7 +8,7 @@ import pytest
     from numpy import isnan
     
     from freqtrade.edge import PairInfo
    -from freqtrade.exceptions import ExchangeError, TemporaryError
    +from freqtrade.exceptions import ExchangeError, InvalidOrderException, TemporaryError
     from freqtrade.persistence import Trade
     from freqtrade.rpc import RPC, RPCException
     from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
    @@ -291,6 +291,61 @@ def test_rpc_trade_history(mocker, default_conf, markets, fee):
         assert trades['trades'][0]['pair'] == 'XRP/BTC'
     
     
    +def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog):
    +    mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
    +    stoploss_mock = MagicMock()
    +    cancel_mock = MagicMock()
    +    mocker.patch.multiple(
    +        'freqtrade.exchange.Exchange',
    +        markets=PropertyMock(return_value=markets),
    +        cancel_order=cancel_mock,
    +        cancel_stoploss_order=stoploss_mock,
    +    )
    +
    +    freqtradebot = get_patched_freqtradebot(mocker, default_conf)
    +    freqtradebot.strategy.order_types['stoploss_on_exchange'] = True
    +    create_mock_trades(fee)
    +    rpc = RPC(freqtradebot)
    +    with pytest.raises(RPCException, match='invalid argument'):
    +        rpc._rpc_delete('200')
    +
    +    create_mock_trades(fee)
    +    trades = Trade.query.all()
    +    trades[1].stoploss_order_id = '1234'
    +    trades[2].stoploss_order_id = '1234'
    +    assert len(trades) > 2
    +
    +    res = rpc._rpc_delete('1')
    +    assert isinstance(res, dict)
    +    assert res['result'] == 'success'
    +    assert res['trade_id'] == '1'
    +    assert res['cancel_order_count'] == 1
    +    assert cancel_mock.call_count == 1
    +    assert stoploss_mock.call_count == 0
    +    cancel_mock.reset_mock()
    +    stoploss_mock.reset_mock()
    +
    +    res = rpc._rpc_delete('2')
    +    assert isinstance(res, dict)
    +    assert cancel_mock.call_count == 1
    +    assert stoploss_mock.call_count == 1
    +    assert res['cancel_order_count'] == 2
    +
    +    stoploss_mock = mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order',
    +                                 side_effect=InvalidOrderException)
    +
    +    res = rpc._rpc_delete('3')
    +    assert stoploss_mock.call_count == 1
    +    stoploss_mock.reset_mock()
    +
    +    cancel_mock = mocker.patch('freqtrade.exchange.Exchange.cancel_order',
    +                               side_effect=InvalidOrderException)
    +
    +    res = rpc._rpc_delete('4')
    +    assert cancel_mock.call_count == 1
    +    assert stoploss_mock.call_count == 0
    +
    +
     def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
                                   limit_buy_order, limit_sell_order, mocker) -> None:
         mocker.patch.multiple(
    
    From 075c73b9e38e7f71ce5eb32e2bc8930b1c947fa0 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Tue, 4 Aug 2020 19:56:49 +0200
    Subject: [PATCH 0361/1197] Improve formatting of telegram message
    
    ---
     freqtrade/rpc/rpc.py           | 4 ++--
     freqtrade/rpc/telegram.py      | 5 ++++-
     tests/rpc/test_rpc_telegram.py | 2 +-
     3 files changed, 7 insertions(+), 4 deletions(-)
    
    diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
    index 58fdebe85..8a1ff7e96 100644
    --- a/freqtrade/rpc/rpc.py
    +++ b/freqtrade/rpc/rpc.py
    @@ -6,7 +6,7 @@ from abc import abstractmethod
     from datetime import date, datetime, timedelta
     from enum import Enum
     from math import isnan
    -from typing import Any, Dict, List, Optional, Tuple
    +from typing import Any, Dict, List, Optional, Tuple, Union
     
     import arrow
     from numpy import NAN, mean
    @@ -538,7 +538,7 @@ class RPC:
             else:
                 return None
     
    -    def _rpc_delete(self, trade_id: str) -> Dict[str, str]:
    +    def _rpc_delete(self, trade_id: str) -> Dict[str, Union[str, int]]:
             """
             Handler for delete .
             Delete the given trade and close eventually existing open orders.
    diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
    index 19d3e1bd9..dde19fddb 100644
    --- a/freqtrade/rpc/telegram.py
    +++ b/freqtrade/rpc/telegram.py
    @@ -547,7 +547,10 @@ class Telegram(RPC):
             trade_id = context.args[0] if len(context.args) > 0 else None
             try:
                 msg = self._rpc_delete(trade_id)
    -            self._send_msg('Delete Result: `{result_msg}`'.format(**msg))
    +            self._send_msg((
    +                'Delete Result: `{result_msg}`'
    +                'Please make sure to take care of this asset on the exchange manually.'
    +            ).format(**msg))
     
             except RPCException as e:
                 self._send_msg(str(e))
    diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
    index c8ac05d0d..ac8dc62c6 100644
    --- a/tests/rpc/test_rpc_telegram.py
    +++ b/tests/rpc/test_rpc_telegram.py
    @@ -1200,8 +1200,8 @@ def test_telegram_delete_trade(mocker, update, default_conf, fee):
         context.args = [1]
         telegram._delete_trade(update=update, context=context)
         msg_mock.call_count == 1
    -    assert "Delete Result" in msg_mock.call_args_list[0][0][0]
         assert "Deleted trade 1." in msg_mock.call_args_list[0][0][0]
    +    assert "Please make sure to take care of this asset" in msg_mock.call_args_list[0][0][0]
     
     
     def test_help_handle(default_conf, update, mocker) -> None:
    
    From 8ed3b81c618293106ad821585ad3fbe89d8685e5 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Tue, 4 Aug 2020 19:57:28 +0200
    Subject: [PATCH 0362/1197] Implement /delete in rest client
    
    ---
     docs/rest-api.md          | 43 +++++++++++++++++++++------------------
     docs/telegram-usage.md    |  1 +
     freqtrade/rpc/telegram.py |  2 +-
     scripts/rest_client.py    | 12 +++++++++++
     4 files changed, 37 insertions(+), 21 deletions(-)
    
    diff --git a/docs/rest-api.md b/docs/rest-api.md
    index a8d902b53..2887a9ffc 100644
    --- a/docs/rest-api.md
    +++ b/docs/rest-api.md
    @@ -106,26 +106,29 @@ python3 scripts/rest_client.py --config rest_config.json  [optional par
     
     ## Available commands
     
    -|  Command | Default | Description |
    -|----------|---------|-------------|
    -| `start` | | Starts the trader
    -| `stop` | | Stops the trader
    -| `stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
    -| `reload_config` | | Reloads the configuration file
    -| `show_config` | | Shows part of the current configuration with relevant settings to operation
    -| `status` | | Lists all open trades
    -| `count` | | Displays number of trades used and available
    -| `profit` | | Display a summary of your profit/loss from close trades and some stats about your performance
    -| `forcesell ` | | Instantly sells the given trade  (Ignoring `minimum_roi`).
    -| `forcesell all` | | Instantly sells all open trades (Ignoring `minimum_roi`).
    -| `forcebuy  [rate]` | | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True)
    -| `performance` | | Show performance of each finished trade grouped by pair
    -| `balance` | | Show account balance per currency
    -| `daily ` | 7 | Shows profit or loss per day, over the last n days
    -| `whitelist` | | Show the current whitelist
    -| `blacklist [pair]` | | Show the current blacklist, or adds a pair to the blacklist.
    -| `edge` | | Show validated pairs by Edge if it is enabled.
    -| `version` | | Show version
    +|  Command | Description |
    +|----------|-------------|
    +| `ping` | Simple command testing the API Readiness - requires no authentication.
    +| `start` | Starts the trader
    +| `stop` | Stops the trader
    +| `stopbuy` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
    +| `reload_config` | Reloads the configuration file
    +| `trades` | List last trades.
    +| `delete_trade ` | Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange.
    +| `show_config` | Shows part of the current configuration with relevant settings to operation
    +| `status` | Lists all open trades
    +| `count` | Displays number of trades used and available
    +| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance
    +| `forcesell ` | Instantly sells the given trade  (Ignoring `minimum_roi`).
    +| `forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).
    +| `forcebuy  [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True)
    +| `performance` | Show performance of each finished trade grouped by pair
    +| `balance` | Show account balance per currency
    +| `daily ` | Shows profit or loss per day, over the last n days (n defaults to 7)
    +| `whitelist` | Show the current whitelist
    +| `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
    +| `edge` | Show validated pairs by Edge if it is enabled.
    +| `version` | Show version
     
     Possible commands can be listed from the rest-client script using the `help` command.
     
    diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md
    index 250293d25..b81ef012b 100644
    --- a/docs/telegram-usage.md
    +++ b/docs/telegram-usage.md
    @@ -57,6 +57,7 @@ official commands. You can ask at any moment for help with `/help`.
     | `/status` | | Lists all open trades
     | `/status table` | | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
     | `/trades [limit]` | | List all recently closed trades in a table format.
    +| `/delete ` | | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange.
     | `/count` | | Displays number of trades used and available
     | `/profit` | | Display a summary of your profit/loss from close trades and some stats about your performance
     | `/forcesell ` | | Instantly sells the given trade  (Ignoring `minimum_roi`).
    diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
    index dde19fddb..f1d3cde21 100644
    --- a/freqtrade/rpc/telegram.py
    +++ b/freqtrade/rpc/telegram.py
    @@ -548,7 +548,7 @@ class Telegram(RPC):
             try:
                 msg = self._rpc_delete(trade_id)
                 self._send_msg((
    -                'Delete Result: `{result_msg}`'
    +                '`{result_msg}`\n'
                     'Please make sure to take care of this asset on the exchange manually.'
                 ).format(**msg))
     
    diff --git a/scripts/rest_client.py b/scripts/rest_client.py
    index 1f96bcb69..51ea596f6 100755
    --- a/scripts/rest_client.py
    +++ b/scripts/rest_client.py
    @@ -62,6 +62,9 @@ class FtRestClient():
         def _get(self, apipath, params: dict = None):
             return self._call("GET", apipath, params=params)
     
    +    def _delete(self, apipath, params: dict = None):
    +        return self._call("DELETE", apipath, params=params)
    +
         def _post(self, apipath, params: dict = None, data: dict = None):
             return self._call("POST", apipath, params=params, data=data)
     
    @@ -164,6 +167,15 @@ class FtRestClient():
             """
             return self._get("trades", params={"limit": limit} if limit else 0)
     
    +    def delete_trade(self, trade_id):
    +        """Delete trade from the database.
    +        Tries to close open orders. Requires manual handling of this asset on the exchange.
    +
    +        :param trade_id: Deletes the trade with this ID from the database.
    +        :return: json object
    +        """
    +        return self._delete("trades/{}".format(trade_id))
    +
         def whitelist(self):
             """Show the current whitelist.
     
    
    From 5082acc33f1220f95490c56c4fa1d8571ae6faf4 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 6 Aug 2020 07:54:54 +0200
    Subject: [PATCH 0363/1197] Fix typos in documentation
    
    ---
     docs/rest-api.md       | 2 +-
     docs/telegram-usage.md | 8 +++++---
     2 files changed, 6 insertions(+), 4 deletions(-)
    
    diff --git a/docs/rest-api.md b/docs/rest-api.md
    index 2887a9ffc..68754f79a 100644
    --- a/docs/rest-api.md
    +++ b/docs/rest-api.md
    @@ -46,7 +46,7 @@ secrets.token_hex()
     
     ### Configuration with docker
     
    -If you run your bot using docker, you'll need to have the bot listen to incomming connections. The security is then handled by docker.
    +If you run your bot using docker, you'll need to have the bot listen to incoming connections. The security is then handled by docker.
     
     ``` json
         "api_server": {
    diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md
    index b81ef012b..b050a7a60 100644
    --- a/docs/telegram-usage.md
    +++ b/docs/telegram-usage.md
    @@ -9,7 +9,7 @@ Telegram user id.
     
     Start a chat with the [Telegram BotFather](https://telegram.me/BotFather)
     
    -Send the message `/newbot`. 
    +Send the message `/newbot`.
     
     *BotFather response:*
     
    @@ -115,6 +115,7 @@ For each open trade, the bot will send you the following message.
     ### /status table
     
     Return the status of all open trades in a table format.
    +
     ```
        ID  Pair      Since    Profit
     ----  --------  -------  --------
    @@ -125,6 +126,7 @@ Return the status of all open trades in a table format.
     ### /count
     
     Return the number of trades used and available.
    +
     ```
     current    max
     ---------  -----
    @@ -210,7 +212,7 @@ Shows the current whitelist
     
     Shows the current blacklist.
     If Pair is set, then this pair will be added to the pairlist.
    -Also supports multiple pairs, seperated by a space.
    +Also supports multiple pairs, separated by a space.
     Use `/reload_config` to reset the blacklist.
     
     > Using blacklist `StaticPairList` with 2 pairs  
    @@ -218,7 +220,7 @@ Use `/reload_config` to reset the blacklist.
     
     ### /edge
     
    -Shows pairs validated by Edge along with their corresponding winrate, expectancy and stoploss values.
    +Shows pairs validated by Edge along with their corresponding win-rate, expectancy and stoploss values.
     
     > **Edge only validated following pairs:**
     ```
    
    From 8b6d10daf1cf3b384eb4732597e5835b97ced143 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 6 Aug 2020 08:50:41 +0200
    Subject: [PATCH 0364/1197] Move DefaultHyperopt to test folder (aligned to
     strategy)
    
    ---
     .../optimize => tests/optimize/hyperopts}/default_hyperopt.py     | 0
     1 file changed, 0 insertions(+), 0 deletions(-)
     rename {freqtrade/optimize => tests/optimize/hyperopts}/default_hyperopt.py (100%)
    
    diff --git a/freqtrade/optimize/default_hyperopt.py b/tests/optimize/hyperopts/default_hyperopt.py
    similarity index 100%
    rename from freqtrade/optimize/default_hyperopt.py
    rename to tests/optimize/hyperopts/default_hyperopt.py
    
    From 081625c5dcf14206683daa7c73f9a1848a157d9a Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 6 Aug 2020 08:51:01 +0200
    Subject: [PATCH 0365/1197] Have hyperopt tests use new hyperopt location
    
    ---
     tests/optimize/test_hyperopt.py | 235 ++++++++++++++------------------
     1 file changed, 106 insertions(+), 129 deletions(-)
    
    diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py
    index 564725709..2f9f9bc56 100644
    --- a/tests/optimize/test_hyperopt.py
    +++ b/tests/optimize/test_hyperopt.py
    @@ -3,6 +3,7 @@ import locale
     import logging
     from datetime import datetime
     from pathlib import Path
    +from copy import deepcopy
     from typing import Dict, List
     from unittest.mock import MagicMock, PropertyMock
     
    @@ -16,7 +17,6 @@ from freqtrade.commands.optimize_commands import (setup_optimize_configuration,
                                                       start_hyperopt)
     from freqtrade.data.history import load_data
     from freqtrade.exceptions import DependencyException, OperationalException
    -from freqtrade.optimize.default_hyperopt import DefaultHyperOpt
     from freqtrade.optimize.default_hyperopt_loss import DefaultHyperOptLoss
     from freqtrade.optimize.hyperopt import Hyperopt
     from freqtrade.resolvers.hyperopt_resolver import (HyperOptLossResolver,
    @@ -26,15 +26,28 @@ from freqtrade.strategy.interface import SellType
     from tests.conftest import (get_args, log_has, log_has_re, patch_exchange,
                                 patched_configuration_load_config_file)
     
    +from .hyperopts.default_hyperopt import DefaultHyperOpt
    +
     
     @pytest.fixture(scope='function')
    -def hyperopt(default_conf, mocker):
    -    default_conf.update({
    -        'spaces': ['default'],
    -        'hyperopt': 'DefaultHyperOpt',
    -    })
    +def hyperopt_conf(default_conf):
    +    hyperconf = deepcopy(default_conf)
    +    hyperconf.update({
    +                         'hyperopt': 'DefaultHyperOpt',
    +                         'hyperopt_path': str(Path(__file__).parent / 'hyperopts'),
    +                         'epochs': 1,
    +                         'timerange': None,
    +                         'spaces': ['default'],
    +                         'hyperopt_jobs': 1,
    +                         })
    +    return hyperconf
    +
    +
    +@pytest.fixture(scope='function')
    +def hyperopt(hyperopt_conf, mocker):
    +
         patch_exchange(mocker)
    -    return Hyperopt(default_conf)
    +    return Hyperopt(hyperopt_conf)
     
     
     @pytest.fixture(scope='function')
    @@ -160,7 +173,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo
         assert log_has('Parameter --print-all detected ...', caplog)
     
     
    -def test_setup_hyperopt_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None:
    +def test_setup_hyperopt_configuration_unlimited_stake_amount(mocker, default_conf) -> None:
         default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
     
         patched_configuration_load_config_file(mocker, default_conf)
    @@ -201,7 +214,7 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
         assert hasattr(x, "timeframe")
     
     
    -def test_hyperoptresolver_wrongname(mocker, default_conf, caplog) -> None:
    +def test_hyperoptresolver_wrongname(default_conf) -> None:
         default_conf.update({'hyperopt': "NonExistingHyperoptClass"})
     
         with pytest.raises(OperationalException, match=r'Impossible to load Hyperopt.*'):
    @@ -216,7 +229,7 @@ def test_hyperoptresolver_noname(default_conf):
             HyperOptResolver.load_hyperopt(default_conf)
     
     
    -def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None:
    +def test_hyperoptlossresolver(mocker, default_conf) -> None:
     
         hl = DefaultHyperOptLoss
         mocker.patch(
    @@ -227,14 +240,14 @@ def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None:
         assert hasattr(x, "hyperopt_loss_function")
     
     
    -def test_hyperoptlossresolver_wrongname(mocker, default_conf, caplog) -> None:
    +def test_hyperoptlossresolver_wrongname(default_conf) -> None:
         default_conf.update({'hyperopt_loss': "NonExistingLossClass"})
     
         with pytest.raises(OperationalException, match=r'Impossible to load HyperoptLoss.*'):
             HyperOptLossResolver.load_hyperoptloss(default_conf)
     
     
    -def test_start_not_installed(mocker, default_conf, caplog, import_fails) -> None:
    +def test_start_not_installed(mocker, default_conf) -> None:
         start_mock = MagicMock()
         patched_configuration_load_config_file(mocker, default_conf)
     
    @@ -253,9 +266,9 @@ def test_start_not_installed(mocker, default_conf, caplog, import_fails) -> None
             start_hyperopt(pargs)
     
     
    -def test_start(mocker, default_conf, caplog) -> None:
    +def test_start(mocker, hyperopt_conf, caplog) -> None:
         start_mock = MagicMock()
    -    patched_configuration_load_config_file(mocker, default_conf)
    +    patched_configuration_load_config_file(mocker, hyperopt_conf)
         mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
         patch_exchange(mocker)
     
    @@ -272,8 +285,8 @@ def test_start(mocker, default_conf, caplog) -> None:
         assert start_mock.call_count == 1
     
     
    -def test_start_no_data(mocker, default_conf, caplog) -> None:
    -    patched_configuration_load_config_file(mocker, default_conf)
    +def test_start_no_data(mocker, hyperopt_conf) -> None:
    +    patched_configuration_load_config_file(mocker, hyperopt_conf)
         mocker.patch('freqtrade.data.history.load_pair_history', MagicMock(return_value=pd.DataFrame))
         mocker.patch(
             'freqtrade.optimize.hyperopt.get_timerange',
    @@ -293,9 +306,9 @@ def test_start_no_data(mocker, default_conf, caplog) -> None:
             start_hyperopt(pargs)
     
     
    -def test_start_filelock(mocker, default_conf, caplog) -> None:
    -    start_mock = MagicMock(side_effect=Timeout(Hyperopt.get_lock_filename(default_conf)))
    -    patched_configuration_load_config_file(mocker, default_conf)
    +def test_start_filelock(mocker, hyperopt_conf, caplog) -> None:
    +    start_mock = MagicMock(side_effect=Timeout(Hyperopt.get_lock_filename(hyperopt_conf)))
    +    patched_configuration_load_config_file(mocker, hyperopt_conf)
         mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
         patch_exchange(mocker)
     
    @@ -519,7 +532,7 @@ def test_roi_table_generation(hyperopt) -> None:
         assert hyperopt.custom_hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
     
     
    -def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
    +def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None:
         dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
         mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
                      MagicMock(return_value=(MagicMock(), None)))
    @@ -545,15 +558,9 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
         )
         patch_exchange(mocker)
         # Co-test loading timeframe from strategy
    -    del default_conf['timeframe']
    -    default_conf.update({'config': 'config.json.example',
    -                         'hyperopt': 'DefaultHyperOpt',
    -                         'epochs': 1,
    -                         'timerange': None,
    -                         'spaces': 'default',
    -                         'hyperopt_jobs': 1, })
    +    del hyperopt_conf['timeframe']
     
    -    hyperopt = Hyperopt(default_conf)
    +    hyperopt = Hyperopt(hyperopt_conf)
         hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
         hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
     
    @@ -569,7 +576,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
         assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
         assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
         assert hasattr(hyperopt, "max_open_trades")
    -    assert hyperopt.max_open_trades == default_conf['max_open_trades']
    +    assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
         assert hasattr(hyperopt, "position_stacking")
     
     
    @@ -686,13 +693,36 @@ def test_buy_strategy_generator(hyperopt, testdatadir) -> None:
         assert 1 in result['buy']
     
     
    -def test_generate_optimizer(mocker, default_conf) -> None:
    -    default_conf.update({'config': 'config.json.example',
    -                         'hyperopt': 'DefaultHyperOpt',
    -                         'timerange': None,
    -                         'spaces': 'all',
    -                         'hyperopt_min_trades': 1,
    -                         })
    +def test_sell_strategy_generator(hyperopt, testdatadir) -> None:
    +    data = load_data(testdatadir, '1m', ['UNITTEST/BTC'], fill_up_missing=True)
    +    dataframes = hyperopt.backtesting.strategy.ohlcvdata_to_dataframe(data)
    +    dataframe = hyperopt.custom_hyperopt.populate_indicators(dataframes['UNITTEST/BTC'],
    +                                                             {'pair': 'UNITTEST/BTC'})
    +
    +    populate_sell_trend = hyperopt.custom_hyperopt.sell_strategy_generator(
    +        {
    +            'sell-adx-value': 20,
    +            'sell-fastd-value': 75,
    +            'sell-mfi-value': 80,
    +            'sell-rsi-value': 20,
    +            'sell-adx-enabled': True,
    +            'sell-fastd-enabled': True,
    +            'sell-mfi-enabled': True,
    +            'sell-rsi-enabled': True,
    +            'sell-trigger': 'sell-bb_upper'
    +        }
    +    )
    +    result = populate_sell_trend(dataframe, {'pair': 'UNITTEST/BTC'})
    +    # Check if some indicators are generated. We will not test all of them
    +    print(result)
    +    assert 'sell' in result
    +    assert 1 in result['sell']
    +
    +
    +def test_generate_optimizer(mocker, hyperopt_conf) -> None:
    +    hyperopt_conf.update({'spaces': 'all',
    +                          'hyperopt_min_trades': 1,
    +                          })
     
         trades = [
             ('TRX/BTC', 0.023117, 0.000233, 100)
    @@ -783,48 +813,35 @@ def test_generate_optimizer(mocker, default_conf) -> None:
             'total_profit': 0.00023300
         }
     
    -    hyperopt = Hyperopt(default_conf)
    +    hyperopt = Hyperopt(hyperopt_conf)
         hyperopt.dimensions = hyperopt.hyperopt_space()
         generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values()))
         assert generate_optimizer_value == response_expected
     
     
    -def test_clean_hyperopt(mocker, default_conf, caplog):
    +def test_clean_hyperopt(mocker, hyperopt_conf, caplog):
         patch_exchange(mocker)
    -    default_conf.update({'config': 'config.json.example',
    -                         'hyperopt': 'DefaultHyperOpt',
    -                         'epochs': 1,
    -                         'timerange': None,
    -                         'spaces': 'default',
    -                         'hyperopt_jobs': 1,
    -                         })
    +
         mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True))
         unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.Path.unlink", MagicMock())
    -    h = Hyperopt(default_conf)
    +    h = Hyperopt(hyperopt_conf)
     
         assert unlinkmock.call_count == 2
         assert log_has(f"Removing `{h.data_pickle_file}`.", caplog)
     
     
    -def test_continue_hyperopt(mocker, default_conf, caplog):
    +def test_continue_hyperopt(mocker, hyperopt_conf, caplog):
         patch_exchange(mocker)
    -    default_conf.update({'config': 'config.json.example',
    -                         'hyperopt': 'DefaultHyperOpt',
    -                         'epochs': 1,
    -                         'timerange': None,
    -                         'spaces': 'default',
    -                         'hyperopt_jobs': 1,
    -                         'hyperopt_continue': True
    -                         })
    +    hyperopt_conf.update({'hyperopt_continue': True})
         mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True))
         unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.Path.unlink", MagicMock())
    -    Hyperopt(default_conf)
    +    Hyperopt(hyperopt_conf)
     
         assert unlinkmock.call_count == 0
         assert log_has("Continuing on previous hyperopt results.", caplog)
     
     
    -def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None:
    +def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None:
         dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
         mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
                      MagicMock(return_value=(MagicMock(), None)))
    @@ -855,16 +872,12 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None:
         )
         patch_exchange(mocker)
     
    -    default_conf.update({'config': 'config.json.example',
    -                         'hyperopt': 'DefaultHyperOpt',
    -                         'epochs': 1,
    -                         'timerange': None,
    -                         'spaces': 'all',
    -                         'hyperopt_jobs': 1,
    -                         'print_json': True,
    -                         })
    +    hyperopt_conf.update({'spaces': 'all',
    +                          'hyperopt_jobs': 1,
    +                          'print_json': True,
    +                          })
     
    -    hyperopt = Hyperopt(default_conf)
    +    hyperopt = Hyperopt(hyperopt_conf)
         hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
         hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
     
    @@ -883,7 +896,7 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None:
         assert dumper.call_count == 2
     
     
    -def test_print_json_spaces_default(mocker, default_conf, caplog, capsys) -> None:
    +def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None:
         dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
         mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
                      MagicMock(return_value=(MagicMock(), None)))
    @@ -913,16 +926,9 @@ def test_print_json_spaces_default(mocker, default_conf, caplog, capsys) -> None
         )
         patch_exchange(mocker)
     
    -    default_conf.update({'config': 'config.json.example',
    -                         'hyperopt': 'DefaultHyperOpt',
    -                         'epochs': 1,
    -                         'timerange': None,
    -                         'spaces': 'default',
    -                         'hyperopt_jobs': 1,
    -                         'print_json': True,
    -                         })
    +    hyperopt_conf.update({'print_json': True})
     
    -    hyperopt = Hyperopt(default_conf)
    +    hyperopt = Hyperopt(hyperopt_conf)
         hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
         hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
     
    @@ -937,7 +943,7 @@ def test_print_json_spaces_default(mocker, default_conf, caplog, capsys) -> None
         assert dumper.call_count == 2
     
     
    -def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) -> None:
    +def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None:
         dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
         mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
                      MagicMock(return_value=(MagicMock(), None)))
    @@ -963,16 +969,12 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) ->
         )
         patch_exchange(mocker)
     
    -    default_conf.update({'config': 'config.json.example',
    -                         'hyperopt': 'DefaultHyperOpt',
    -                         'epochs': 1,
    -                         'timerange': None,
    -                         'spaces': 'roi stoploss',
    -                         'hyperopt_jobs': 1,
    -                         'print_json': True,
    -                         })
    +    hyperopt_conf.update({'spaces': 'roi stoploss',
    +                          'hyperopt_jobs': 1,
    +                          'print_json': True,
    +                          })
     
    -    hyperopt = Hyperopt(default_conf)
    +    hyperopt = Hyperopt(hyperopt_conf)
         hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
         hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
     
    @@ -987,7 +989,7 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) ->
         assert dumper.call_count == 2
     
     
    -def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys) -> None:
    +def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> None:
         dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
         mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
                      MagicMock(return_value=(MagicMock(), None)))
    @@ -1012,14 +1014,9 @@ def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys)
         )
         patch_exchange(mocker)
     
    -    default_conf.update({'config': 'config.json.example',
    -                         'hyperopt': 'DefaultHyperOpt',
    -                         'epochs': 1,
    -                         'timerange': None,
    -                         'spaces': 'roi stoploss',
    -                         'hyperopt_jobs': 1, })
    +    hyperopt_conf.update({'spaces': 'roi stoploss'})
     
    -    hyperopt = Hyperopt(default_conf)
    +    hyperopt = Hyperopt(hyperopt_conf)
         hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
         hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
     
    @@ -1040,11 +1037,11 @@ def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys)
         assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
         assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
         assert hasattr(hyperopt, "max_open_trades")
    -    assert hyperopt.max_open_trades == default_conf['max_open_trades']
    +    assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
         assert hasattr(hyperopt, "position_stacking")
     
     
    -def test_simplified_interface_all_failed(mocker, default_conf, caplog, capsys) -> None:
    +def test_simplified_interface_all_failed(mocker, hyperopt_conf) -> None:
         mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
         mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
                      MagicMock(return_value=(MagicMock(), None)))
    @@ -1055,14 +1052,9 @@ def test_simplified_interface_all_failed(mocker, default_conf, caplog, capsys) -
     
         patch_exchange(mocker)
     
    -    default_conf.update({'config': 'config.json.example',
    -                         'hyperopt': 'DefaultHyperOpt',
    -                         'epochs': 1,
    -                         'timerange': None,
    -                         'spaces': 'all',
    -                         'hyperopt_jobs': 1, })
    +    hyperopt_conf.update({'spaces': 'all', })
     
    -    hyperopt = Hyperopt(default_conf)
    +    hyperopt = Hyperopt(hyperopt_conf)
         hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
         hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
     
    @@ -1075,7 +1067,7 @@ def test_simplified_interface_all_failed(mocker, default_conf, caplog, capsys) -
             hyperopt.start()
     
     
    -def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None:
    +def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
         dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
         mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
                      MagicMock(return_value=(MagicMock(), None)))
    @@ -1100,14 +1092,9 @@ def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None:
         )
         patch_exchange(mocker)
     
    -    default_conf.update({'config': 'config.json.example',
    -                         'hyperopt': 'DefaultHyperOpt',
    -                         'epochs': 1,
    -                         'timerange': None,
    -                         'spaces': 'buy',
    -                         'hyperopt_jobs': 1, })
    +    hyperopt_conf.update({'spaces': 'buy'})
     
    -    hyperopt = Hyperopt(default_conf)
    +    hyperopt = Hyperopt(hyperopt_conf)
         hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
         hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
     
    @@ -1128,11 +1115,11 @@ def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None:
         assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
         assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
         assert hasattr(hyperopt, "max_open_trades")
    -    assert hyperopt.max_open_trades == default_conf['max_open_trades']
    +    assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
         assert hasattr(hyperopt, "position_stacking")
     
     
    -def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None:
    +def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None:
         dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
         mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
                      MagicMock(return_value=(MagicMock(), None)))
    @@ -1157,14 +1144,9 @@ def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None
         )
         patch_exchange(mocker)
     
    -    default_conf.update({'config': 'config.json.example',
    -                         'hyperopt': 'DefaultHyperOpt',
    -                         'epochs': 1,
    -                         'timerange': None,
    -                         'spaces': 'sell',
    -                         'hyperopt_jobs': 1, })
    +    hyperopt_conf.update({'spaces': 'sell', })
     
    -    hyperopt = Hyperopt(default_conf)
    +    hyperopt = Hyperopt(hyperopt_conf)
         hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
         hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
     
    @@ -1185,7 +1167,7 @@ def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None
         assert hasattr(hyperopt.backtesting.strategy, "advise_sell")
         assert hasattr(hyperopt.backtesting.strategy, "advise_buy")
         assert hasattr(hyperopt, "max_open_trades")
    -    assert hyperopt.max_open_trades == default_conf['max_open_trades']
    +    assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades']
         assert hasattr(hyperopt, "position_stacking")
     
     
    @@ -1195,7 +1177,7 @@ def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None
         ('sell_strategy_generator', 'sell'),
         ('sell_indicator_space', 'sell'),
     ])
    -def test_simplified_interface_failed(mocker, default_conf, caplog, capsys, method, space) -> None:
    +def test_simplified_interface_failed(mocker, hyperopt_conf, method, space) -> None:
         mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
         mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
                      MagicMock(return_value=(MagicMock(), None)))
    @@ -1206,14 +1188,9 @@ def test_simplified_interface_failed(mocker, default_conf, caplog, capsys, metho
     
         patch_exchange(mocker)
     
    -    default_conf.update({'config': 'config.json.example',
    -                         'hyperopt': 'DefaultHyperOpt',
    -                         'epochs': 1,
    -                         'timerange': None,
    -                         'spaces': space,
    -                         'hyperopt_jobs': 1, })
    +    hyperopt_conf.update({'spaces': space})
     
    -    hyperopt = Hyperopt(default_conf)
    +    hyperopt = Hyperopt(hyperopt_conf)
         hyperopt.backtesting.strategy.ohlcvdata_to_dataframe = MagicMock()
         hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
     
    
    From 59370672b811aba5bcee1f3597f59375c5df2994 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 6 Aug 2020 09:00:28 +0200
    Subject: [PATCH 0366/1197] Fix more tests
    
    ---
     tests/commands/test_commands.py | 5 ++---
     tests/optimize/test_hyperopt.py | 4 +++-
     2 files changed, 5 insertions(+), 4 deletions(-)
    
    diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py
    index 3ec7e4798..6837ebe98 100644
    --- a/tests/commands/test_commands.py
    +++ b/tests/commands/test_commands.py
    @@ -667,7 +667,7 @@ def test_start_list_hyperopts(mocker, caplog, capsys):
         args = [
             "list-hyperopts",
             "--hyperopt-path",
    -        str(Path(__file__).parent.parent / "optimize"),
    +        str(Path(__file__).parent.parent / "optimize" / "hyperopts"),
             "-1"
         ]
         pargs = get_args(args)
    @@ -683,7 +683,7 @@ def test_start_list_hyperopts(mocker, caplog, capsys):
         args = [
             "list-hyperopts",
             "--hyperopt-path",
    -        str(Path(__file__).parent.parent / "optimize"),
    +        str(Path(__file__).parent.parent / "optimize" / "hyperopts"),
         ]
         pargs = get_args(args)
         # pargs['config'] = None
    @@ -692,7 +692,6 @@ def test_start_list_hyperopts(mocker, caplog, capsys):
         assert "TestHyperoptLegacy" not in captured.out
         assert "legacy_hyperopt.py" not in captured.out
         assert "DefaultHyperOpt" in captured.out
    -    assert "test_hyperopt.py" in captured.out
     
     
     def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
    diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py
    index 2f9f9bc56..0d2ebf213 100644
    --- a/tests/optimize/test_hyperopt.py
    +++ b/tests/optimize/test_hyperopt.py
    @@ -247,7 +247,7 @@ def test_hyperoptlossresolver_wrongname(default_conf) -> None:
             HyperOptLossResolver.load_hyperoptloss(default_conf)
     
     
    -def test_start_not_installed(mocker, default_conf) -> None:
    +def test_start_not_installed(mocker, default_conf, import_fails) -> None:
         start_mock = MagicMock()
         patched_configuration_load_config_file(mocker, default_conf)
     
    @@ -258,6 +258,8 @@ def test_start_not_installed(mocker, default_conf) -> None:
             'hyperopt',
             '--config', 'config.json',
             '--hyperopt', 'DefaultHyperOpt',
    +        '--hyperopt-path',
    +        str(Path(__file__).parent / "hyperopts"),
             '--epochs', '5'
         ]
         pargs = get_args(args)
    
    From 995d3e1ed5ee8548e55e5f5075b4533b6ff5d907 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 6 Aug 2020 09:07:48 +0200
    Subject: [PATCH 0367/1197] Don't search internal path for Hyperopt files
    
    ---
     freqtrade/resolvers/hyperopt_resolver.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py
    index abbfee6ed..5dcf73d67 100644
    --- a/freqtrade/resolvers/hyperopt_resolver.py
    +++ b/freqtrade/resolvers/hyperopt_resolver.py
    @@ -23,7 +23,7 @@ class HyperOptResolver(IResolver):
         object_type = IHyperOpt
         object_type_str = "Hyperopt"
         user_subdir = USERPATH_HYPEROPTS
    -    initial_search_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
    +    initial_search_path = None
     
         @staticmethod
         def load_hyperopt(config: Dict) -> IHyperOpt:
    
    From d01070dba81b19e3e3f9fc715e4d22022259e4b4 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 6 Aug 2020 09:22:41 +0200
    Subject: [PATCH 0368/1197] Increase coverage of edge_cli
    
    ---
     tests/optimize/test_edge_cli.py | 14 ++++++++++++++
     1 file changed, 14 insertions(+)
    
    diff --git a/tests/optimize/test_edge_cli.py b/tests/optimize/test_edge_cli.py
    index acec51f66..188b4aa5f 100644
    --- a/tests/optimize/test_edge_cli.py
    +++ b/tests/optimize/test_edge_cli.py
    @@ -105,3 +105,17 @@ def test_edge_init_fee(mocker, edge_conf) -> None:
         edge_cli = EdgeCli(edge_conf)
         assert edge_cli.edge.fee == 0.1234
         assert fee_mock.call_count == 0
    +
    +
    +def test_edge_start(mocker, edge_conf) -> None:
    +    mock_calculate = mocker.patch('freqtrade.edge.edge_positioning.Edge.calculate',
    +                                  return_value=True)
    +    table_mock = mocker.patch('freqtrade.optimize.edge_cli.generate_edge_table')
    +
    +    patch_exchange(mocker)
    +    edge_conf['stake_amount'] = 20
    +
    +    edge_cli = EdgeCli(edge_conf)
    +    edge_cli.start()
    +    assert mock_calculate.call_count == 1
    +    assert table_mock.call_count == 1
    
    From eba73307e42ce6b94d5bed393c40c0bfbbafd05c Mon Sep 17 00:00:00 2001
    From: Fredrik81 
    Date: Fri, 7 Aug 2020 01:13:36 +0200
    Subject: [PATCH 0369/1197] Update strategy_methods_advanced.j2
    
    Fix def confirm_trade_exit arguments
    ---
     freqtrade/templates/subtemplates/strategy_methods_advanced.j2 | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2
    index c7ce41bb7..5ca6e6971 100644
    --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2
    +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2
    @@ -34,7 +34,7 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f
         """
         return True
     
    -def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
    +def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float,
                            rate: float, time_in_force: str, sell_reason: str, **kwargs) -> bool:
         """
         Called right before placing a regular sell order.
    
    From f3ce54150e92f66c70b82f91276f10fab7e801a4 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Sat, 8 Aug 2020 15:06:13 +0200
    Subject: [PATCH 0370/1197] Simplify Telegram table
    
    ---
     docs/telegram-usage.md | 48 +++++++++++++++++++++---------------------
     1 file changed, 24 insertions(+), 24 deletions(-)
    
    diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md
    index b050a7a60..9776b26ba 100644
    --- a/docs/telegram-usage.md
    +++ b/docs/telegram-usage.md
    @@ -47,30 +47,30 @@ Per default, the Telegram bot shows predefined commands. Some commands
     are only available by sending them to the bot. The table below list the
     official commands. You can ask at any moment for help with `/help`.
     
    -|  Command | Default | Description |
    -|----------|---------|-------------|
    -| `/start` | | Starts the trader
    -| `/stop` | | Stops the trader
    -| `/stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
    -| `/reload_config` | | Reloads the configuration file
    -| `/show_config` | | Shows part of the current configuration with relevant settings to operation
    -| `/status` | | Lists all open trades
    -| `/status table` | | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
    -| `/trades [limit]` | | List all recently closed trades in a table format.
    -| `/delete ` | | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange.
    -| `/count` | | Displays number of trades used and available
    -| `/profit` | | Display a summary of your profit/loss from close trades and some stats about your performance
    -| `/forcesell ` | | Instantly sells the given trade  (Ignoring `minimum_roi`).
    -| `/forcesell all` | | Instantly sells all open trades (Ignoring `minimum_roi`).
    -| `/forcebuy  [rate]` | | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True)
    -| `/performance` | | Show performance of each finished trade grouped by pair
    -| `/balance` | | Show account balance per currency
    -| `/daily ` | 7 | Shows profit or loss per day, over the last n days
    -| `/whitelist` | | Show the current whitelist
    -| `/blacklist [pair]` | | Show the current blacklist, or adds a pair to the blacklist.
    -| `/edge` | | Show validated pairs by Edge if it is enabled.
    -| `/help` | | Show help message
    -| `/version` | | Show version
    +|  Command | Description |
    +|----------|-------------|
    +| `/start` | Starts the trader
    +| `/stop` | Stops the trader
    +| `/stopbuy` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
    +| `/reload_config` | Reloads the configuration file
    +| `/show_config` | Shows part of the current configuration with relevant settings to operation
    +| `/status` | Lists all open trades
    +| `/status table` | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**)
    +| `/trades [limit]` | List all recently closed trades in a table format.
    +| `/delete ` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange.
    +| `/count` | Displays number of trades used and available
    +| `/profit` | Display a summary of your profit/loss from close trades and some stats about your performance
    +| `/forcesell ` | Instantly sells the given trade  (Ignoring `minimum_roi`).
    +| `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).
    +| `/forcebuy  [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True)
    +| `/performance` | Show performance of each finished trade grouped by pair
    +| `/balance` | Show account balance per currency
    +| `/daily ` | Shows profit or loss per day, over the last n days (n defaults to 7)
    +| `/whitelist` | Show the current whitelist
    +| `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist.
    +| `/edge` | Show validated pairs by Edge if it is enabled.
    +| `/help` | Show help message
    +| `/version` | Show version
     
     ## Telegram commands in action
     
    
    From dd430455e411bdfae6bef3162d03e3c893b2e883 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Sat, 8 Aug 2020 17:04:32 +0200
    Subject: [PATCH 0371/1197] Enable dataprovier for hyperopt
    
    ---
     freqtrade/optimize/backtesting.py |  5 ++---
     freqtrade/optimize/hyperopt.py    | 15 +++++++++------
     2 files changed, 11 insertions(+), 9 deletions(-)
    
    diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py
    index 214c92e0e..d058493cf 100644
    --- a/freqtrade/optimize/backtesting.py
    +++ b/freqtrade/optimize/backtesting.py
    @@ -65,9 +65,8 @@ class Backtesting:
             self.strategylist: List[IStrategy] = []
             self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
     
    -        if self.config.get('runmode') != RunMode.HYPEROPT:
    -            self.dataprovider = DataProvider(self.config, self.exchange)
    -            IStrategy.dp = self.dataprovider
    +        dataprovider = DataProvider(self.config, self.exchange)
    +        IStrategy.dp = dataprovider
     
             if self.config.get('strategy_list', None):
                 for strat in list(self.config['strategy_list']):
    diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
    index 153ae3861..9bc0dadc0 100644
    --- a/freqtrade/optimize/hyperopt.py
    +++ b/freqtrade/optimize/hyperopt.py
    @@ -4,26 +4,26 @@
     This module contains the hyperopt logic
     """
     
    +import io
     import locale
     import logging
     import random
     import warnings
    -from math import ceil
     from collections import OrderedDict
    +from math import ceil
     from operator import itemgetter
    +from os import path
     from pathlib import Path
     from pprint import pformat
     from typing import Any, Dict, List, Optional
     
    +import progressbar
     import rapidjson
    +import tabulate
     from colorama import Fore, Style
     from joblib import (Parallel, cpu_count, delayed, dump, load,
                         wrap_non_picklable_objects)
    -from pandas import DataFrame, json_normalize, isna
    -import progressbar
    -import tabulate
    -from os import path
    -import io
    +from pandas import DataFrame, isna, json_normalize
     
     from freqtrade.data.converter import trim_dataframe
     from freqtrade.data.history import get_timerange
    @@ -35,6 +35,7 @@ from freqtrade.optimize.hyperopt_interface import IHyperOpt  # noqa: F401
     from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss  # noqa: F401
     from freqtrade.resolvers.hyperopt_resolver import (HyperOptLossResolver,
                                                        HyperOptResolver)
    +from freqtrade.strategy import IStrategy
     
     # Suppress scikit-learn FutureWarnings from skopt
     with warnings.catch_warnings():
    @@ -634,6 +635,8 @@ class Hyperopt:
             # We don't need exchange instance anymore while running hyperopt
             self.backtesting.exchange = None  # type: ignore
             self.backtesting.pairlists = None  # type: ignore
    +        self.backtesting.strategy.dp = None  # type: ignore
    +        IStrategy.dp = None  # type: ignore
     
             self.epochs = self.load_previous_results(self.results_file)
     
    
    From 5e1032c4af168aa744dccc626a17ee339219450f Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Sat, 8 Aug 2020 17:08:38 +0200
    Subject: [PATCH 0372/1197] Simplify strategy documentation, move
     "substrategies" to advanced page
    
    ---
     docs/strategy-advanced.md      | 21 +++++++++
     docs/strategy-customization.md | 78 +++++++++-------------------------
     2 files changed, 42 insertions(+), 57 deletions(-)
    
    diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md
    index e4bab303e..359280694 100644
    --- a/docs/strategy-advanced.md
    +++ b/docs/strategy-advanced.md
    @@ -199,3 +199,24 @@ class Awesomestrategy(IStrategy):
             return True
     
     ```
    +
    +## Derived strategies
    +
    +The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched:
    +
    +``` python
    +class MyAwesomeStrategy(IStrategy):
    +    ...
    +    stoploss = 0.13
    +    trailing_stop = False
    +    # All other attributes and methods are here as they
    +    # should be in any custom strategy...
    +    ...
    +
    +class MyAwesomeStrategy2(MyAwesomeStrategy):
    +    # Override something
    +    stoploss = 0.08
    +    trailing_stop = True
    +```
    +
    +Both attributes and methods may be overriden, altering behavior of the original strategy in a way you need.
    diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md
    index 98c71b4b2..ec521470a 100644
    --- a/docs/strategy-customization.md
    +++ b/docs/strategy-customization.md
    @@ -355,7 +355,7 @@ def informative_pairs(self):
     
     ***
     
    -### Additional data (DataProvider)
    +## Additional data (DataProvider)
     
     The strategy provides access to the `DataProvider`. This allows you to get additional data to use in your strategy.
     
    @@ -363,7 +363,7 @@ All methods return `None` in case of failure (do not raise an exception).
     
     Please always check the mode of operation to select the correct method to get data (samples see below).
     
    -#### Possible options for DataProvider
    +### Possible options for DataProvider
     
     - [`available_pairs`](#available_pairs) - Property with tuples listing cached pairs with their intervals (pair, interval).
     - [`current_whitelist()`](#current_whitelist) - Returns a current list of whitelisted pairs. Useful for accessing dynamic whitelists (ie. VolumePairlist)
    @@ -376,9 +376,9 @@ Please always check the mode of operation to select the correct method to get da
     - [`ticker(pair)`](#tickerpair) - Returns current ticker data for the pair. See [ccxt documentation](https://github.com/ccxt/ccxt/wiki/Manual#price-tickers) for more details on the Ticker data structure.
     - `runmode` - Property containing the current runmode.
     
    -#### Example Usages:
    +### Example Usages
     
    -#### *available_pairs*
    +### *available_pairs*
     
     ``` python
     if self.dp:
    @@ -386,7 +386,7 @@ if self.dp:
             print(f"available {pair}, {timeframe}")
     ```
     
    -#### *current_whitelist()*
    +### *current_whitelist()*
     
     Imagine you've developed a strategy that trades the `5m` timeframe using signals generated from a `1d` timeframe on the top 10 volume pairs by volume. 
     
    @@ -420,7 +420,7 @@ class SampleStrategy(IStrategy):
     
             inf_tf = '1d'
             # Get the informative pair
    -        informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe='1d')
    +        informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=inf_tf)
             # Get the 14 day rsi
             informative['rsi'] = ta.RSI(informative, timeperiod=14)
     
    @@ -455,7 +455,7 @@ class SampleStrategy(IStrategy):
     
     ```
     
    -#### *get_pair_dataframe(pair, timeframe)*
    +### *get_pair_dataframe(pair, timeframe)*
     
     ``` python
     # fetch live / historical candle (OHLCV) data for the first informative pair
    @@ -468,12 +468,9 @@ if self.dp:
     !!! Warning "Warning about backtesting"
         Be careful when using dataprovider in backtesting. `historic_ohlcv()` (and `get_pair_dataframe()`
         for the backtesting runmode) provides the full time-range in one go,
    -    so please be aware of it and make sure to not "look into the future" to avoid surprises when running in dry/live mode).
    +    so please be aware of it and make sure to not "look into the future" to avoid surprises when running in dry/live mode.
     
    -!!! Warning "Warning in hyperopt"
    -    This option cannot currently be used during hyperopt.
    -
    -#### *get_analyzed_dataframe(pair, timeframe)*
    +### *get_analyzed_dataframe(pair, timeframe)*
     
     This method is used by freqtrade internally to determine the last signal.
     It can also be used in specific callbacks to get the signal that caused the action (see [Advanced Strategy Documentation](strategy-advanced.md) for more details on available callbacks).
    @@ -489,10 +486,7 @@ if self.dp:
         Returns an empty dataframe if the requested pair was not cached.
         This should not happen when using whitelisted pairs.
     
    -!!! Warning "Warning in hyperopt"
    -    This option cannot currently be used during hyperopt.
    -
    -#### *orderbook(pair, maximum)*
    +### *orderbook(pair, maximum)*
     
     ``` python
     if self.dp:
    @@ -503,10 +497,9 @@ if self.dp:
     ```
     
     !!! Warning
    -    The order book is not part of the historic data which means backtesting and hyperopt will not work if this
    -    method is used.
    +    The order book is not part of the historic data which means backtesting and hyperopt will not work correctly if this method is used.
     
    -#### *ticker(pair)*
    +### *ticker(pair)*
     
     ``` python
     if self.dp:
    @@ -525,7 +518,7 @@ if self.dp:
     
     ***
     
    -### Additional data (Wallets)
    +## Additional data (Wallets)
     
     The strategy provides access to the `Wallets` object. This contains the current balances on the exchange.
     
    @@ -541,7 +534,7 @@ if self.wallets:
         total_eth = self.wallets.get_total('ETH')
     ```
     
    -#### Possible options for Wallets
    +### Possible options for Wallets
     
     - `get_free(asset)` - currently available balance to trade
     - `get_used(asset)` - currently tied up balance (open orders)
    @@ -549,7 +542,7 @@ if self.wallets:
     
     ***
     
    -### Additional data (Trades)
    +## Additional data (Trades)
     
     A history of Trades can be retrieved in the strategy by querying the database.
     
    @@ -595,13 +588,13 @@ Sample return value: ETH/BTC had 5 trades, with a total profit of 1.5% (ratio of
     !!! Warning
         Trade history is not available during backtesting or hyperopt.
     
    -### Prevent trades from happening for a specific pair
    +## Prevent trades from happening for a specific pair
     
     Freqtrade locks pairs automatically for the current candle (until that candle is over) when a pair is sold, preventing an immediate re-buy of that pair.
     
     Locked pairs will show the message `Pair  is currently locked.`.
     
    -#### Locking pairs from within the strategy
    +### Locking pairs from within the strategy
     
     Sometimes it may be desired to lock a pair after certain events happen (e.g. multiple losing trades in a row).
     
    @@ -618,7 +611,7 @@ To verify if a pair is currently locked, use `self.is_pair_locked(pair)`.
     !!! Warning
         Locking pairs is not functioning during backtesting.
     
    -##### Pair locking example
    +#### Pair locking example
     
     ``` python
     from freqtrade.persistence import Trade
    @@ -640,7 +633,7 @@ if self.config['runmode'].value in ('live', 'dry_run'):
             self.lock_pair(metadata['pair'], until=datetime.now(timezone.utc) + timedelta(hours=12))
     ```
     
    -### Print created dataframe
    +## Print created dataframe
     
     To inspect the created dataframe, you can issue a print-statement in either `populate_buy_trend()` or `populate_sell_trend()`.
     You may also want to print the pair so it's clear what data is currently shown.
    @@ -664,36 +657,7 @@ def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
     
     Printing more than a few rows is also possible (simply use  `print(dataframe)` instead of `print(dataframe.tail())`), however not recommended, as that will be very verbose (~500 lines per pair every 5 seconds).
     
    -### Specify custom strategy location
    -
    -If you want to use a strategy from a different directory you can pass `--strategy-path`
    -
    -```bash
    -freqtrade trade --strategy AwesomeStrategy --strategy-path /some/directory
    -```
    -
    -### Derived strategies
    -
    -The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched:
    -
    -``` python
    -class MyAwesomeStrategy(IStrategy):
    -    ...
    -    stoploss = 0.13
    -    trailing_stop = False
    -    # All other attributes and methods are here as they
    -    # should be in any custom strategy...
    -    ...
    -
    -class MyAwesomeStrategy2(MyAwesomeStrategy):
    -    # Override something
    -    stoploss = 0.08
    -    trailing_stop = True
    -```
    -
    -Both attributes and methods may be overriden, altering behavior of the original strategy in a way you need.
    -
    -### Common mistakes when developing strategies
    +## Common mistakes when developing strategies
     
     Backtesting analyzes the whole time-range at once for performance reasons. Because of this, strategy authors need to make sure that strategies do not look-ahead into the future.
     This is a common pain-point, which can cause huge differences between backtesting and dry/live run methods, since they all use data which is not available during dry/live runs, so these strategies will perform well during backtesting, but will fail / perform badly in real conditions.
    @@ -705,7 +669,7 @@ The following lists some common patterns which should be avoided to prevent frus
     - don't use `dataframe['volume'].mean()`. This uses the full DataFrame for backtesting, including data from the future. Use `dataframe['volume'].rolling().mean()` instead
     - don't use `.resample('1h')`. This uses the left border of the interval, so moves data from an hour to the start of the hour. Use `.resample('1h', label='right')` instead.
     
    -### Further strategy ideas
    +## Further strategy ideas
     
     To get additional Ideas for strategies, head over to our [strategy repository](https://github.com/freqtrade/freqtrade-strategies). Feel free to use them as they are - but results will depend on the current market situation, pairs used etc. - therefore please backtest the strategy for your exchange/desired pairs first, evaluate carefully, use at your own risk.
     Feel free to use any of them as inspiration for your own strategies.
    
    From 09aa954b68f5ca64ffec9508ff94cfb0998a97df Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Sat, 8 Aug 2020 17:24:19 +0200
    Subject: [PATCH 0373/1197] Update strategy-customization documentation
    
    ---
     docs/strategy-customization.md | 141 ++++++++++++++++++++-------------
     1 file changed, 84 insertions(+), 57 deletions(-)
    
    diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md
    index ec521470a..633b82385 100644
    --- a/docs/strategy-customization.md
    +++ b/docs/strategy-customization.md
    @@ -58,12 +58,12 @@ file as reference.**
     
     !!! Note "Strategies and Backtesting"
         To avoid problems and unexpected differences between Backtesting and dry/live modes, please be aware
    -    that during backtesting the full time-interval is passed to the `populate_*()` methods at once.
    +    that during backtesting the full time range is passed to the `populate_*()` methods at once.
         It is therefore best to use vectorized operations (across the whole dataframe, not loops) and
         avoid index referencing (`df.iloc[-1]`), but instead use `df.shift()` to get to the previous candle.
     
     !!! Warning "Warning: Using future data"
    -    Since backtesting passes the full time interval to the `populate_*()` methods, the strategy author
    +    Since backtesting passes the full time range to the `populate_*()` methods, the strategy author
         needs to take care to avoid having the strategy utilize data from the future.
         Some common patterns for this are listed in the [Common Mistakes](#common-mistakes-when-developing-strategies) section of this document.
     
    @@ -251,7 +251,7 @@ minimal_roi = {
     While technically not completely disabled, this would sell once the trade reaches 10000% Profit.
     
     To use times based on candle duration (timeframe), the following snippet can be handy.
    -This will allow you to change the ticket_interval for the strategy, and ROI times will still be set as candles (e.g. after 3 candles ...)
    +This will allow you to change the timeframe for the strategy, and ROI times will still be set as candles (e.g. after 3 candles ...)
     
     ``` python
     from freqtrade.exchange import timeframe_to_minutes
    @@ -285,7 +285,7 @@ If your exchange supports it, it's recommended to also set `"stoploss_on_exchang
     
     For more information on order_types please look [here](configuration.md#understand-order_types).
     
    -### Timeframe (ticker interval)
    +### Timeframe (formerly ticker interval)
     
     This is the set of candles the bot should download and use for the analysis.
     Common values are `"1m"`, `"5m"`, `"15m"`, `"1h"`, however all values supported by your exchange should work.
    @@ -333,10 +333,10 @@ class Awesomestrategy(IStrategy):
     #### Get data for non-tradeable pairs
     
     Data for additional, informative pairs (reference pairs) can be beneficial for some strategies.
    -Ohlcv data for these pairs will be downloaded as part of the regular whitelist refresh process and is available via `DataProvider` just as other pairs (see below).
    +OHLCV data for these pairs will be downloaded as part of the regular whitelist refresh process and is available via `DataProvider` just as other pairs (see below).
     These parts will **not** be traded unless they are also specified in the pair whitelist, or have been selected by Dynamic Whitelisting.
     
    -The pairs need to be specified as tuples in the format `("pair", "interval")`, with pair as the first and time interval as the second argument.
    +The pairs need to be specified as tuples in the format `("pair", "timeframe")`, with pair as the first and timeframe as the second argument.
     
     Sample:
     
    @@ -349,8 +349,8 @@ def informative_pairs(self):
     
     !!! Warning
         As these pairs will be refreshed as part of the regular whitelist refresh, it's best to keep this list short.
    -    All intervals and all pairs can be specified as long as they are available (and active) on the used exchange.
    -    It is however better to use resampling to longer time-intervals when possible
    +    All timeframes and all pairs can be specified as long as they are available (and active) on the used exchange.
    +    It is however better to use resampling to longer timeframes whenever possible
         to avoid hammering the exchange with too many requests and risk being blocked.
     
     ***
    @@ -363,10 +363,14 @@ All methods return `None` in case of failure (do not raise an exception).
     
     Please always check the mode of operation to select the correct method to get data (samples see below).
     
    +!!! Warning "Hyperopt"
    +    Dataprovider is available during hyperopt, however it can only be used in `populate_indicators()`.
    +    It is not available in `populate_buy()` and `populate_sell()` methods.
    +
     ### Possible options for DataProvider
     
    -- [`available_pairs`](#available_pairs) - Property with tuples listing cached pairs with their intervals (pair, interval).
    -- [`current_whitelist()`](#current_whitelist) - Returns a current list of whitelisted pairs. Useful for accessing dynamic whitelists (ie. VolumePairlist)
    +- [`available_pairs`](#available_pairs) - Property with tuples listing cached pairs with their timeframe (pair, timeframe).
    +- [`current_whitelist()`](#current_whitelist) - Returns a current list of whitelisted pairs. Useful for accessing dynamic whitelists (i.e. VolumePairlist)
     - [`get_pair_dataframe(pair, timeframe)`](#get_pair_dataframepair-timeframe) - This is a universal method, which returns either historical data (for backtesting) or cached live data (for the Dry-Run and Live-Run modes).
     - [`get_analyzed_dataframe(pair, timeframe)`](#get_analyzed_dataframepair-timeframe) - Returns the analyzed dataframe (after calling `populate_indicators()`, `populate_buy()`, `populate_sell()`) and the time of the latest analysis.
     - `historic_ohlcv(pair, timeframe)` - Returns historical data stored on disk.
    @@ -401,58 +405,13 @@ Since we can't resample our data we will have to use an informative pair; and si
     This is where calling `self.dp.current_whitelist()` comes in handy.
     
     ```python
    -class SampleStrategy(IStrategy):
    -    # strategy init stuff...
    -
    -    timeframe = '5m'
    -
    -    # more strategy init stuff..
    -
         def informative_pairs(self):
     
             # get access to all pairs available in whitelist.
             pairs = self.dp.current_whitelist()
             # Assign tf to each pair so they can be downloaded and cached for strategy.
             informative_pairs = [(pair, '1d') for pair in pairs]
    -        return informative_pairs
    -
    -    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    -
    -        inf_tf = '1d'
    -        # Get the informative pair
    -        informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=inf_tf)
    -        # Get the 14 day rsi
    -        informative['rsi'] = ta.RSI(informative, timeperiod=14)
    -
    -        # Rename columns to be unique
    -        informative.columns = [f"{col}_{inf_tf}" for col in informative.columns]
    -        # Assuming inf_tf = '1d' - then the columns will now be:
    -        # date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d
    -
    -        # Combine the 2 dataframes
    -        # all indicators on the informative sample MUST be calculated before this point
    -        dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_{inf_tf}', how='left')
    -        # FFill to have the 1d value available in every row throughout the day.
    -        # Without this, comparisons would only work once per day.
    -        dataframe = dataframe.ffill()
    -        # Calculate rsi of the original dataframe (5m timeframe)
    -        dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
    -
    -        # Do other stuff
    -        # ...
    -
    -        return dataframe
    -
    -    def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    -
    -        dataframe.loc[
    -            (
    -                (qtpylib.crossed_above(dataframe['rsi'], 30)) &  # Signal: RSI crosses above 30
    -                (dataframe['rsi_1d'] < 30) &                     # Ensure daily RSI is < 30
    -                (dataframe['volume'] > 0)                        # Ensure this candle had volume (important for backtesting)
    -            ),
    -            'buy'] = 1
    -
    +        return informative_pairs   
     ```
     
     ### *get_pair_dataframe(pair, timeframe)*
    @@ -479,7 +438,7 @@ It can also be used in specific callbacks to get the signal that caused the acti
     # fetch current dataframe
     if self.dp:
         dataframe, last_updated = self.dp.get_analyzed_dataframe(pair=metadata['pair'],
    -                                                             timeframe=self.ticker_interval)
    +                                                             timeframe=self.timeframe)
     ```
     
     !!! Note "No data available"
    @@ -516,6 +475,74 @@ if self.dp:
         does not always fills in the `last` field (so it can be None), etc. So you need to carefully verify the ticker
         data returned from the exchange and add appropriate error handling / defaults.
     
    +!!! Warning "Warning about backtesting"
    +    This method will always return up-to-date values - so usage during backtesting / hyperopt will lead to wrong results.
    +
    +### Complete Data-provider sample
    +
    +```python
    +class SampleStrategy(IStrategy):
    +    # strategy init stuff...
    +
    +    timeframe = '5m'
    +
    +    # more strategy init stuff..
    +
    +    def informative_pairs(self):
    +
    +        # get access to all pairs available in whitelist.
    +        pairs = self.dp.current_whitelist()
    +        # Assign tf to each pair so they can be downloaded and cached for strategy.
    +        informative_pairs = [(pair, '1d') for pair in pairs]
    +        # Optionally Add additional "static" pairs
    +        informative_pairs += [("ETH/USDT", "5m"),
    +                              ("BTC/TUSD", "15m"),
    +                            ]
    +        return informative_pairs
    +
    +    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    +        if not self.dp:
    +            # Don't do anything if DataProvider is not available.
    +            return dataframe
    +
    +        inf_tf = '1d'
    +        # Get the informative pair
    +        informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=inf_tf)
    +        # Get the 14 day rsi
    +        informative['rsi'] = ta.RSI(informative, timeperiod=14)
    +
    +        # Rename columns to be unique
    +        informative.columns = [f"{col}_{inf_tf}" for col in informative.columns]
    +        # Assuming inf_tf = '1d' - then the columns will now be:
    +        # date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d
    +
    +        # Combine the 2 dataframes
    +        # all indicators on the informative sample MUST be calculated before this point
    +        dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_{inf_tf}', how='left')
    +        # FFill to have the 1d value available in every row throughout the day.
    +        # Without this, comparisons would only work once per day.
    +        dataframe = dataframe.ffill()
    +
    +        # Calculate rsi of the original dataframe (5m timeframe)
    +        dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
    +
    +        # Do other stuff
    +        # ...
    +
    +        return dataframe
    +
    +    def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
    +
    +        dataframe.loc[
    +            (
    +                (qtpylib.crossed_above(dataframe['rsi'], 30)) &  # Signal: RSI crosses above 30
    +                (dataframe['rsi_1d'] < 30) &                     # Ensure daily RSI is < 30
    +                (dataframe['volume'] > 0)                        # Ensure this candle had volume (important for backtesting)
    +            ),
    +            'buy'] = 1
    +
    +```
    +
     ***
     
     ## Additional data (Wallets)
    
    From 2afe1d5b11b1e63ee403b147fcd0572a9723a716 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Sat, 8 Aug 2020 17:27:22 +0200
    Subject: [PATCH 0374/1197] Add link to full sample
    
    ---
     docs/strategy-customization.md    | 6 ++++--
     freqtrade/optimize/backtesting.py | 1 -
     2 files changed, 4 insertions(+), 3 deletions(-)
    
    diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md
    index 633b82385..73d085abd 100644
    --- a/docs/strategy-customization.md
    +++ b/docs/strategy-customization.md
    @@ -328,9 +328,9 @@ class Awesomestrategy(IStrategy):
     
     ***
     
    -### Additional data (informative_pairs)
    +## Additional data (informative_pairs)
     
    -#### Get data for non-tradeable pairs
    +### Get data for non-tradeable pairs
     
     Data for additional, informative pairs (reference pairs) can be beneficial for some strategies.
     OHLCV data for these pairs will be downloaded as part of the regular whitelist refresh process and is available via `DataProvider` just as other pairs (see below).
    @@ -347,6 +347,8 @@ def informative_pairs(self):
                 ]
     ```
     
    +A full sample can be found [in the DataProvider section](#complete-data-provider-sample).
    +
     !!! Warning
         As these pairs will be refreshed as part of the regular whitelist refresh, it's best to keep this list short.
         All timeframes and all pairs can be specified as long as they are available (and active) on the used exchange.
    diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py
    index d058493cf..f3070f10f 100644
    --- a/freqtrade/optimize/backtesting.py
    +++ b/freqtrade/optimize/backtesting.py
    @@ -24,7 +24,6 @@ from freqtrade.optimize.optimize_reports import (generate_backtest_stats,
     from freqtrade.pairlist.pairlistmanager import PairListManager
     from freqtrade.persistence import Trade
     from freqtrade.resolvers import ExchangeResolver, StrategyResolver
    -from freqtrade.state import RunMode
     from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType
     
     logger = logging.getLogger(__name__)
    
    From fca41a44bb278b7544f6024ebff9f0fb2bd8d359 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Sat, 8 Aug 2020 20:20:58 +0200
    Subject: [PATCH 0375/1197] Also logg timeframe
    
    ---
     freqtrade/optimize/optimize_reports.py | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py
    index f917e7cab..8e25d9d89 100644
    --- a/freqtrade/optimize/optimize_reports.py
    +++ b/freqtrade/optimize/optimize_reports.py
    @@ -269,6 +269,7 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame],
                 'stake_amount': config['stake_amount'],
                 'stake_currency': config['stake_currency'],
                 'max_open_trades': config['max_open_trades'],
    +            'timeframe': config['timeframe'],
                 **daily_stats,
             }
             result['strategy'][strategy] = strat_stats
    
    From 2663aede24af7e5b3eab2608f9e5be6fef3c00d5 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Sun, 9 Aug 2020 10:28:11 +0200
    Subject: [PATCH 0376/1197] Update test to reflect new column naming
    
    ---
     tests/edge/test_edge.py | 24 ++++++++++++------------
     1 file changed, 12 insertions(+), 12 deletions(-)
    
    diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py
    index 969b0c44b..d35f7fcf6 100644
    --- a/tests/edge/test_edge.py
    +++ b/tests/edge/test_edge.py
    @@ -418,8 +418,8 @@ def test_process_expectancy_remove_pumps(mocker, edge_conf, fee,):
              'stoploss': -0.9,
              'profit_percent': '',
              'profit_abs': '',
    -         'open_time': np.datetime64('2018-10-03T00:05:00.000000000'),
    -         'close_time': np.datetime64('2018-10-03T00:10:00.000000000'),
    +         'open_date': np.datetime64('2018-10-03T00:05:00.000000000'),
    +         'close_date': np.datetime64('2018-10-03T00:10:00.000000000'),
              'open_index': 1,
              'close_index': 1,
              'trade_duration': '',
    @@ -431,8 +431,8 @@ def test_process_expectancy_remove_pumps(mocker, edge_conf, fee,):
              'stoploss': -0.9,
              'profit_percent': '',
              'profit_abs': '',
    -         'open_time': np.datetime64('2018-10-03T00:20:00.000000000'),
    -         'close_time': np.datetime64('2018-10-03T00:25:00.000000000'),
    +         'open_date': np.datetime64('2018-10-03T00:20:00.000000000'),
    +         'close_date': np.datetime64('2018-10-03T00:25:00.000000000'),
              'open_index': 4,
              'close_index': 4,
              'trade_duration': '',
    @@ -443,8 +443,8 @@ def test_process_expectancy_remove_pumps(mocker, edge_conf, fee,):
              'stoploss': -0.9,
              'profit_percent': '',
              'profit_abs': '',
    -         'open_time': np.datetime64('2018-10-03T00:20:00.000000000'),
    -         'close_time': np.datetime64('2018-10-03T00:25:00.000000000'),
    +         'open_date': np.datetime64('2018-10-03T00:20:00.000000000'),
    +         'close_date': np.datetime64('2018-10-03T00:25:00.000000000'),
              'open_index': 4,
              'close_index': 4,
              'trade_duration': '',
    @@ -455,8 +455,8 @@ def test_process_expectancy_remove_pumps(mocker, edge_conf, fee,):
              'stoploss': -0.9,
              'profit_percent': '',
              'profit_abs': '',
    -         'open_time': np.datetime64('2018-10-03T00:20:00.000000000'),
    -         'close_time': np.datetime64('2018-10-03T00:25:00.000000000'),
    +         'open_date': np.datetime64('2018-10-03T00:20:00.000000000'),
    +         'close_date': np.datetime64('2018-10-03T00:25:00.000000000'),
              'open_index': 4,
              'close_index': 4,
              'trade_duration': '',
    @@ -467,8 +467,8 @@ def test_process_expectancy_remove_pumps(mocker, edge_conf, fee,):
              'stoploss': -0.9,
              'profit_percent': '',
              'profit_abs': '',
    -         'open_time': np.datetime64('2018-10-03T00:20:00.000000000'),
    -         'close_time': np.datetime64('2018-10-03T00:25:00.000000000'),
    +         'open_date': np.datetime64('2018-10-03T00:20:00.000000000'),
    +         'close_date': np.datetime64('2018-10-03T00:25:00.000000000'),
              'open_index': 4,
              'close_index': 4,
              'trade_duration': '',
    @@ -480,8 +480,8 @@ def test_process_expectancy_remove_pumps(mocker, edge_conf, fee,):
              'stoploss': -0.9,
              'profit_percent': '',
              'profit_abs': '',
    -         'open_time': np.datetime64('2018-10-03T00:30:00.000000000'),
    -         'close_time': np.datetime64('2018-10-03T00:40:00.000000000'),
    +         'open_date': np.datetime64('2018-10-03T00:30:00.000000000'),
    +         'close_date': np.datetime64('2018-10-03T00:40:00.000000000'),
              'open_index': 6,
              'close_index': 7,
              'trade_duration': '',
    
    From 17613f203a2f898b8388a7eb85c82ac1acf5c5e8 Mon Sep 17 00:00:00 2001
    From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
    Date: Mon, 10 Aug 2020 06:17:04 +0000
    Subject: [PATCH 0377/1197] Bump mkdocs-material from 5.5.1 to 5.5.3
    
    Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 5.5.1 to 5.5.3.
    - [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
    - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md)
    - [Commits](https://github.com/squidfunk/mkdocs-material/compare/5.5.1...5.5.3)
    
    Signed-off-by: dependabot[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 c30661b6a..4068e364b 100644
    --- a/docs/requirements-docs.txt
    +++ b/docs/requirements-docs.txt
    @@ -1,2 +1,2 @@
    -mkdocs-material==5.5.1
    +mkdocs-material==5.5.3
     mdx_truly_sane_lists==1.2
    
    From 1afe4df7be5234630475ebdffc6d875c2467ddf1 Mon Sep 17 00:00:00 2001
    From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
    Date: Mon, 10 Aug 2020 06:17:36 +0000
    Subject: [PATCH 0378/1197] Bump ccxt from 1.32.45 to 1.32.88
    
    Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.32.45 to 1.32.88.
    - [Release notes](https://github.com/ccxt/ccxt/releases)
    - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst)
    - [Commits](https://github.com/ccxt/ccxt/compare/1.32.45...1.32.88)
    
    Signed-off-by: dependabot[bot] 
    ---
     requirements-common.txt | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/requirements-common.txt b/requirements-common.txt
    index 62cde9dbc..b7e71eada 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.32.45
    +ccxt==1.32.88
     SQLAlchemy==1.3.18
     python-telegram-bot==12.8
     arrow==0.15.8
    
    From c9c43d2f0b6adc59811a292efe95448d411f1499 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Tue, 11 Aug 2020 15:27:41 +0200
    Subject: [PATCH 0379/1197] Move log-message of retrying before decrementing
     count
    
    Otherwise the message is always one round "late".
    ---
     freqtrade/exchange/common.py | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py
    index 0610e8447..cbab742b7 100644
    --- a/freqtrade/exchange/common.py
    +++ b/freqtrade/exchange/common.py
    @@ -107,9 +107,9 @@ def retrier_async(f):
             except TemporaryError as ex:
                 logger.warning('%s() returned exception: "%s"', f.__name__, ex)
                 if count > 0:
    +                logger.warning('retrying %s() still for %s times', f.__name__, count)
                     count -= 1
                     kwargs.update({'count': count})
    -                logger.warning('retrying %s() still for %s times', f.__name__, count)
                     if isinstance(ex, DDosProtection):
                         backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT)
                         logger.debug(f"Applying DDosProtection backoff delay: {backoff_delay}")
    @@ -131,9 +131,9 @@ def retrier(_func=None, retries=API_RETRY_COUNT):
                 except (TemporaryError, RetryableOrderError) as ex:
                     logger.warning('%s() returned exception: "%s"', f.__name__, ex)
                     if count > 0:
    +                    logger.warning('retrying %s() still for %s times', f.__name__, count)
                         count -= 1
                         kwargs.update({'count': count})
    -                    logger.warning('retrying %s() still for %s times', f.__name__, count)
                         if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError):
                             # increasing backoff
                             backoff_delay = calculate_backoff(count + 1, retries)
    
    From d77c53960dfc789a5ae013ac89a04b26f4f4456f Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Tue, 11 Aug 2020 19:27:25 +0200
    Subject: [PATCH 0380/1197] Show API backoff in logs to better investigate
     eventual problems)
    
    ---
     freqtrade/exchange/common.py | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py
    index cbab742b7..3bba9be72 100644
    --- a/freqtrade/exchange/common.py
    +++ b/freqtrade/exchange/common.py
    @@ -112,7 +112,7 @@ def retrier_async(f):
                     kwargs.update({'count': count})
                     if isinstance(ex, DDosProtection):
                         backoff_delay = calculate_backoff(count + 1, API_RETRY_COUNT)
    -                    logger.debug(f"Applying DDosProtection backoff delay: {backoff_delay}")
    +                    logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}")
                         await asyncio.sleep(backoff_delay)
                     return await wrapper(*args, **kwargs)
                 else:
    @@ -137,7 +137,7 @@ def retrier(_func=None, retries=API_RETRY_COUNT):
                         if isinstance(ex, DDosProtection) or isinstance(ex, RetryableOrderError):
                             # increasing backoff
                             backoff_delay = calculate_backoff(count + 1, retries)
    -                        logger.debug(f"Applying DDosProtection backoff delay: {backoff_delay}")
    +                        logger.info(f"Applying DDosProtection backoff delay: {backoff_delay}")
                             time.sleep(backoff_delay)
                         return wrapper(*args, **kwargs)
                     else:
    
    From 77541935a8d686b0b1feb1ce345261a2c40436e8 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Tue, 11 Aug 2020 20:10:43 +0200
    Subject: [PATCH 0381/1197] Fix small merge mistake
    
    ---
     freqtrade/commands/hyperopt_commands.py | 16 ++++-----
     tests/commands/test_commands.py         | 44 ++++++++++++++++---------
     2 files changed, 37 insertions(+), 23 deletions(-)
    
    diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py
    index 7b079bdfe..b769100be 100755
    --- a/freqtrade/commands/hyperopt_commands.py
    +++ b/freqtrade/commands/hyperopt_commands.py
    @@ -183,17 +183,17 @@ def _hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
                 if x['results_metrics']['profit'] < filteroptions['filter_max_total_profit']
             ]
         if filteroptions['filter_min_objective'] is not None:
    -        trials = [x for x in trials if x['results_metrics']['trade_count'] > 0]
    -        # trials = [x for x in trials if x['loss'] != 20]
    -        trials = [
    -            x for x in trials
    +        epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
    +        # epochs = [x for x in epochs if x['loss'] != 20]
    +        epochs = [
    +            x for x in epochs
                 if x['loss'] < filteroptions['filter_min_objective']
             ]
         if filteroptions['filter_max_objective'] is not None:
    -        trials = [x for x in trials if x['results_metrics']['trade_count'] > 0]
    -        # trials = [x for x in trials if x['loss'] != 20]
    -        trials = [
    -            x for x in trials
    +        epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
    +        # epochs = [x for x in epochs if x['loss'] != 20]
    +        epochs = [
    +            x for x in epochs
                 if x['loss'] > filteroptions['filter_max_objective']
             ]
     
    diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py
    index 4ef4ec6c5..1eb017465 100644
    --- a/tests/commands/test_commands.py
    +++ b/tests/commands/test_commands.py
    @@ -736,7 +736,8 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
     
         args = [
             "hyperopt-list",
    -        "--no-details"
    +        "--no-details",
    +        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -749,7 +750,8 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
         args = [
             "hyperopt-list",
             "--best",
    -        "--no-details"
    +        "--no-details",
    +        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -763,7 +765,8 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
         args = [
             "hyperopt-list",
             "--profitable",
    -        "--no-details"
    +        "--no-details",
    +        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -776,7 +779,8 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
                              " 11/12", " 12/12"])
         args = [
             "hyperopt-list",
    -        "--profitable"
    +        "--profitable",
    +        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -792,7 +796,8 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
             "hyperopt-list",
             "--no-details",
             "--no-color",
    -        "--min-trades", "20"
    +        "--min-trades", "20",
    +        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -806,7 +811,8 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
             "hyperopt-list",
             "--profitable",
             "--no-details",
    -        "--max-trades", "20"
    +        "--max-trades", "20",
    +        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -821,7 +827,8 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
             "hyperopt-list",
             "--profitable",
             "--no-details",
    -        "--min-avg-profit", "0.11"
    +        "--min-avg-profit", "0.11",
    +        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -835,7 +842,8 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
         args = [
             "hyperopt-list",
             "--no-details",
    -        "--max-avg-profit", "0.10"
    +        "--max-avg-profit", "0.10",
    +        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -849,7 +857,8 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
         args = [
             "hyperopt-list",
             "--no-details",
    -        "--min-total-profit", "0.4"
    +        "--min-total-profit", "0.4",
    +        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -863,7 +872,8 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
         args = [
             "hyperopt-list",
             "--no-details",
    -        "--max-total-profit", "0.4"
    +        "--max-total-profit", "0.4",
    +        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -877,7 +887,8 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
         args = [
             "hyperopt-list",
             "--no-details",
    -        "--min-objective", "0.1"
    +        "--min-objective", "0.1",
    +        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -891,7 +902,8 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
         args = [
             "hyperopt-list",
             "--no-details",
    -        "--max-objective", "0.1"
    +        "--max-objective", "0.1",
    +        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -906,7 +918,8 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
             "hyperopt-list",
             "--profitable",
             "--no-details",
    -        "--min-avg-time", "2000"
    +        "--min-avg-time", "2000",
    +        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -920,7 +933,8 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
         args = [
             "hyperopt-list",
             "--no-details",
    -        "--max-avg-time", "1500"
    +        "--max-avg-time", "1500",
    +        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -934,7 +948,7 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
         args = [
             "hyperopt-list",
             "--no-details",
    -        "--export-csv", "test_file.csv"
    +        "--export-csv", "test_file.csv",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    
    From f51c03aa864e4489bb55886cf86b3be0bc413216 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Tue, 11 Aug 2020 20:29:47 +0200
    Subject: [PATCH 0382/1197] Revert changes to color using --no-color
    
    ---
     tests/commands/test_commands.py | 14 --------------
     1 file changed, 14 deletions(-)
    
    diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py
    index 1eb017465..69d80d2cd 100644
    --- a/tests/commands/test_commands.py
    +++ b/tests/commands/test_commands.py
    @@ -737,7 +737,6 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
         args = [
             "hyperopt-list",
             "--no-details",
    -        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -751,7 +750,6 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
             "hyperopt-list",
             "--best",
             "--no-details",
    -        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -766,7 +764,6 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
             "hyperopt-list",
             "--profitable",
             "--no-details",
    -        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -780,7 +777,6 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
         args = [
             "hyperopt-list",
             "--profitable",
    -        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -797,7 +793,6 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
             "--no-details",
             "--no-color",
             "--min-trades", "20",
    -        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -812,7 +807,6 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
             "--profitable",
             "--no-details",
             "--max-trades", "20",
    -        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -828,7 +822,6 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
             "--profitable",
             "--no-details",
             "--min-avg-profit", "0.11",
    -        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -843,7 +836,6 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
             "hyperopt-list",
             "--no-details",
             "--max-avg-profit", "0.10",
    -        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -858,7 +850,6 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
             "hyperopt-list",
             "--no-details",
             "--min-total-profit", "0.4",
    -        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -873,7 +864,6 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
             "hyperopt-list",
             "--no-details",
             "--max-total-profit", "0.4",
    -        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -888,7 +878,6 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
             "hyperopt-list",
             "--no-details",
             "--min-objective", "0.1",
    -        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -903,7 +892,6 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
             "hyperopt-list",
             "--no-details",
             "--max-objective", "0.1",
    -        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -919,7 +907,6 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
             "--profitable",
             "--no-details",
             "--min-avg-time", "2000",
    -        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    @@ -934,7 +921,6 @@ def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results):
             "hyperopt-list",
             "--no-details",
             "--max-avg-time", "1500",
    -        "--no-color",
         ]
         pargs = get_args(args)
         pargs['config'] = None
    
    From 56655b97cfc52116c7f990405dee6d5ebaea2ce7 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Tue, 11 Aug 2020 20:37:01 +0200
    Subject: [PATCH 0383/1197] Refactor hyperopt_filter method
    
    ---
     freqtrade/commands/hyperopt_commands.py | 38 ++++++++++++++++++++++---
     1 file changed, 34 insertions(+), 4 deletions(-)
    
    diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py
    index b769100be..a724443cf 100755
    --- a/freqtrade/commands/hyperopt_commands.py
    +++ b/freqtrade/commands/hyperopt_commands.py
    @@ -10,7 +10,7 @@ from freqtrade.state import RunMode
     
     logger = logging.getLogger(__name__)
     
    -# flake8: noqa C901
    +
     def start_hyperopt_list(args: Dict[str, Any]) -> None:
         """
         List hyperopt epochs previously evaluated
    @@ -47,7 +47,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
         epochs = Hyperopt.load_previous_results(results_file)
         total_epochs = len(epochs)
     
    -    epochs = _hyperopt_filter_epochs(epochs, filteroptions)
    +    epochs = hyperopt_filter_epochs(epochs, filteroptions)
     
         if print_colorized:
             colorama_init(autoreset=True)
    @@ -108,7 +108,7 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
         epochs = Hyperopt.load_previous_results(results_file)
         total_epochs = len(epochs)
     
    -    epochs = _hyperopt_filter_epochs(epochs, filteroptions)
    +    epochs = hyperopt_filter_epochs(epochs, filteroptions)
         filtered_epochs = len(epochs)
     
         if n > filtered_epochs:
    @@ -128,7 +128,7 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
                                          header_str="Epoch details")
     
     
    -def _hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
    +def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
         """
         Filter our items from the list of hyperopt results
         """
    @@ -136,6 +136,20 @@ def _hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
             epochs = [x for x in epochs if x['is_best']]
         if filteroptions['only_profitable']:
             epochs = [x for x in epochs if x['results_metrics']['profit'] > 0]
    +
    +    epochs = _hyperopt_filter_epochs_trade_count(epochs, filteroptions)
    +
    +    epochs = _hyperopt_filter_epochs_duration(epochs, filteroptions)
    +
    +    epochs = _hyperopt_filter_epochs_profit(epochs, filteroptions)
    +
    +    epochs = _hyperopt_filter_epochs_objective(epochs, filteroptions)
    +
    +    return epochs
    +
    +
    +def _hyperopt_filter_epochs_trade_count(epochs: List, filteroptions: dict) -> List:
    +
         if filteroptions['filter_min_trades'] > 0:
             epochs = [
                 x for x in epochs
    @@ -146,6 +160,11 @@ def _hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
                 x for x in epochs
                 if x['results_metrics']['trade_count'] < filteroptions['filter_max_trades']
             ]
    +    return epochs
    +
    +
    +def _hyperopt_filter_epochs_duration(epochs: List, filteroptions: dict) -> List:
    +
         if filteroptions['filter_min_avg_time'] is not None:
             epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
             epochs = [
    @@ -158,6 +177,12 @@ def _hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
                 x for x in epochs
                 if x['results_metrics']['duration'] < filteroptions['filter_max_avg_time']
             ]
    +
    +    return epochs
    +
    +
    +def _hyperopt_filter_epochs_profit(epochs: List, filteroptions: dict) -> List:
    +
         if filteroptions['filter_min_avg_profit'] is not None:
             epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
             epochs = [
    @@ -182,6 +207,11 @@ def _hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
                 x for x in epochs
                 if x['results_metrics']['profit'] < filteroptions['filter_max_total_profit']
             ]
    +    return epochs
    +
    +
    +def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List:
    +
         if filteroptions['filter_min_objective'] is not None:
             epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
             # epochs = [x for x in epochs if x['loss'] != 20]
    
    From 2dc36bb79eece79fd20c7ffac92fb1c12ba4177e Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Tue, 11 Aug 2020 20:52:18 +0200
    Subject: [PATCH 0384/1197] Remove inversion of min/max objective selection
    
    ---
     freqtrade/commands/hyperopt_commands.py | 5 -----
     1 file changed, 5 deletions(-)
    
    diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py
    index a724443cf..d37c1f13b 100755
    --- a/freqtrade/commands/hyperopt_commands.py
    +++ b/freqtrade/commands/hyperopt_commands.py
    @@ -99,11 +99,6 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
             'filter_max_objective': config.get('hyperopt_list_max_objective', None)
         }
     
    -    if filteroptions['filter_min_objective'] is not None:
    -        filteroptions['filter_min_objective'] = -filteroptions['filter_min_objective']
    -    if filteroptions['filter_max_objective'] is not None:
    -        filteroptions['filter_max_objective'] = -filteroptions['filter_max_objective']
    -
         # Previous evaluations
         epochs = Hyperopt.load_previous_results(results_file)
         total_epochs = len(epochs)
    
    From 2fed066e767bb3c3015af5d4a9f31ad588619fad Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Wed, 12 Aug 2020 10:39:53 +0200
    Subject: [PATCH 0385/1197] Simplify objective code formatting
    
    ---
     freqtrade/commands/hyperopt_commands.py | 21 +++++++--------------
     1 file changed, 7 insertions(+), 14 deletions(-)
    
    diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py
    index d37c1f13b..4fae51e28 100755
    --- a/freqtrade/commands/hyperopt_commands.py
    +++ b/freqtrade/commands/hyperopt_commands.py
    @@ -140,6 +140,10 @@ def hyperopt_filter_epochs(epochs: List, filteroptions: dict) -> List:
     
         epochs = _hyperopt_filter_epochs_objective(epochs, filteroptions)
     
    +    logger.info(f"{len(epochs)} " +
    +                ("best " if filteroptions['only_best'] else "") +
    +                ("profitable " if filteroptions['only_profitable'] else "") +
    +                "epochs found.")
         return epochs
     
     
    @@ -209,22 +213,11 @@ def _hyperopt_filter_epochs_objective(epochs: List, filteroptions: dict) -> List
     
         if filteroptions['filter_min_objective'] is not None:
             epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
    -        # epochs = [x for x in epochs if x['loss'] != 20]
    -        epochs = [
    -            x for x in epochs
    -            if x['loss'] < filteroptions['filter_min_objective']
    -        ]
    +
    +        epochs = [x for x in epochs if x['loss'] < filteroptions['filter_min_objective']]
         if filteroptions['filter_max_objective'] is not None:
             epochs = [x for x in epochs if x['results_metrics']['trade_count'] > 0]
    -        # epochs = [x for x in epochs if x['loss'] != 20]
    -        epochs = [
    -            x for x in epochs
    -            if x['loss'] > filteroptions['filter_max_objective']
    -        ]
     
    -    logger.info(f"{len(epochs)} " +
    -                ("best " if filteroptions['only_best'] else "") +
    -                ("profitable " if filteroptions['only_profitable'] else "") +
    -                "epochs found.")
    +        epochs = [x for x in epochs if x['loss'] > filteroptions['filter_max_objective']]
     
         return epochs
    
    From 1f1a819b292ecf927c2d332a1fd788ac5b7359a6 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Wed, 12 Aug 2020 11:21:00 +0200
    Subject: [PATCH 0386/1197] Remove unused 3rd argument to create_stoploss call
    
    ---
     freqtrade/freqtradebot.py  | 10 ++++------
     tests/test_freqtradebot.py |  2 +-
     2 files changed, 5 insertions(+), 7 deletions(-)
    
    diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
    index 967f68b90..3168976e2 100644
    --- a/freqtrade/freqtradebot.py
    +++ b/freqtrade/freqtradebot.py
    @@ -768,7 +768,7 @@ class FreqtradeBot:
             logger.debug('Found no sell signal for %s.', trade)
             return False
     
    -    def create_stoploss_order(self, trade: Trade, stop_price: float, rate: float) -> bool:
    +    def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool:
             """
             Abstracts creating stoploss orders from the logic.
             Handles errors and updates the trade database object.
    @@ -831,14 +831,13 @@ class FreqtradeBot:
                 stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss
                 stop_price = trade.open_rate * (1 + stoploss)
     
    -            if self.create_stoploss_order(trade=trade, stop_price=stop_price, rate=stop_price):
    +            if self.create_stoploss_order(trade=trade, stop_price=stop_price):
                     trade.stoploss_last_update = datetime.now()
                     return False
     
             # If stoploss order is canceled for some reason we add it
             if stoploss_order and stoploss_order['status'] in ('canceled', 'cancelled'):
    -            if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss,
    -                                          rate=trade.stop_loss):
    +            if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss):
                     return False
                 else:
                     trade.stoploss_order_id = None
    @@ -875,8 +874,7 @@ class FreqtradeBot:
                                          f"for pair {trade.pair}")
     
                     # Create new stoploss order
    -                if not self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss,
    -                                                  rate=trade.stop_loss):
    +                if not self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss):
                         logger.warning(f"Could not create trailing stoploss order "
                                        f"for pair {trade.pair}.")
     
    diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
    index 5c225bbc0..87071be3e 100644
    --- a/tests/test_freqtradebot.py
    +++ b/tests/test_freqtradebot.py
    @@ -1301,7 +1301,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee,
         freqtrade.enter_positions()
         trade = Trade.query.first()
         caplog.clear()
    -    freqtrade.create_stoploss_order(trade, 200, 199)
    +    freqtrade.create_stoploss_order(trade, 200)
         assert trade.stoploss_order_id is None
         assert trade.sell_reason == SellType.EMERGENCY_SELL.value
         assert log_has("Unable to place a stoploss order on exchange. ", caplog)
    
    From 6dfa159a914968d6a8838020521bfb1da5f1a905 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Wed, 12 Aug 2020 14:11:19 +0200
    Subject: [PATCH 0387/1197] Small comment adjustments in exchange class
    
    ---
     freqtrade/exchange/exchange.py | 10 +++++-----
     1 file changed, 5 insertions(+), 5 deletions(-)
    
    diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
    index 8438941f7..f8bac3a8d 100644
    --- a/freqtrade/exchange/exchange.py
    +++ b/freqtrade/exchange/exchange.py
    @@ -974,7 +974,7 @@ class Exchange:
             except ccxt.BaseError as e:
                 raise OperationalException(e) from e
     
    -    # Assign method to fetch_stoploss_order to allow easy overriding in other classes
    +    # Assign method to cancel_stoploss_order to allow easy overriding in other classes
         cancel_stoploss_order = cancel_order
     
         def is_cancel_order_result_suitable(self, corder) -> bool:
    @@ -1040,10 +1040,10 @@ class Exchange:
         @retrier
         def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
             """
    -        get order book level 2 from exchange
    -
    -        Notes:
    -        20180619: bittrex doesnt support limits -.-
    +        Get L2 order book from exchange.
    +        Can be limited to a certain amount (if supported).
    +        Returns a dict in the format
    +        {'asks': [price, volume], 'bids': [price, volume]}
             """
             try:
     
    
    From faa2bbb5553a19b21341c031e9eef46508551254 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Wed, 12 Aug 2020 14:25:50 +0200
    Subject: [PATCH 0388/1197] Document exception hierarchy
    
    ---
     docs/developer.md          | 29 +++++++++++++++++++++++++++++
     freqtrade/exceptions.py    | 16 ++++++++--------
     freqtrade/freqtradebot.py  |  4 ++--
     freqtrade/rpc/rpc.py       |  6 +++---
     tests/test_freqtradebot.py |  3 ++-
     5 files changed, 44 insertions(+), 14 deletions(-)
    
    diff --git a/docs/developer.md b/docs/developer.md
    index 036109d5b..ce454cec2 100644
    --- a/docs/developer.md
    +++ b/docs/developer.md
    @@ -85,6 +85,35 @@ docker-compose exec freqtrade_develop /bin/bash
     
     ![image](https://user-images.githubusercontent.com/419355/65456522-ba671a80-de06-11e9-9598-df9ca0d8dcac.png)
     
    +## ErrorHandling
    +
    +Freqtrade Exceptions all inherit from `FreqtradeException`.
    +This general class of error should however not be used directly, instead, multiple specialized sub-Exceptions exist.
    +
    +Below is an outline of exception inheritance hierarchy:
    +
    +```
    ++ FreqtradeException
    +|
    ++---+ OperationalException
    +|
    ++---+ DependencyException
    +|   |
    +|   +---+ PricingError
    +|   |
    +|   +---+ ExchangeError
    +|       |
    +|       +---+ TemporaryError
    +|       |
    +|       +---+ DDosProtection
    +|       |
    +|       +---+ InvalidOrderException
    +|           |
    +|           +---+ RetryableOrderError
    +|
    ++---+ StrategyError
    +```
    +
     ## Modules
     
     ### Dynamic Pairlist
    diff --git a/freqtrade/exceptions.py b/freqtrade/exceptions.py
    index c85fccc4b..e2bc969a9 100644
    --- a/freqtrade/exceptions.py
    +++ b/freqtrade/exceptions.py
    @@ -29,7 +29,14 @@ class PricingError(DependencyException):
         """
     
     
    -class InvalidOrderException(FreqtradeException):
    +class ExchangeError(DependencyException):
    +    """
    +    Error raised out of the exchange.
    +    Has multiple Errors to determine the appropriate error.
    +    """
    +
    +
    +class InvalidOrderException(ExchangeError):
         """
         This is returned when the order is not valid. Example:
         If stoploss on exchange order is hit, then trying to cancel the order
    @@ -44,13 +51,6 @@ class RetryableOrderError(InvalidOrderException):
         """
     
     
    -class ExchangeError(DependencyException):
    -    """
    -    Error raised out of the exchange.
    -    Has multiple Errors to determine the appropriate error.
    -    """
    -
    -
     class TemporaryError(ExchangeError):
         """
         Temporary network or exchange related error.
    diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
    index 3168976e2..557aefe94 100644
    --- a/freqtrade/freqtradebot.py
    +++ b/freqtrade/freqtradebot.py
    @@ -919,7 +919,7 @@ class FreqtradeBot:
                     if not trade.open_order_id:
                         continue
                     order = self.exchange.fetch_order(trade.open_order_id, trade.pair)
    -            except (ExchangeError, InvalidOrderException):
    +            except (ExchangeError):
                     logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
                     continue
     
    @@ -952,7 +952,7 @@ class FreqtradeBot:
             for trade in Trade.get_open_order_trades():
                 try:
                     order = self.exchange.fetch_order(trade.open_order_id, trade.pair)
    -            except (DependencyException, InvalidOrderException):
    +            except (ExchangeError):
                     logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
                     continue
     
    diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
    index 8a1ff7e96..f4e20c16f 100644
    --- a/freqtrade/rpc/rpc.py
    +++ b/freqtrade/rpc/rpc.py
    @@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union
     import arrow
     from numpy import NAN, mean
     
    -from freqtrade.exceptions import (ExchangeError, InvalidOrderException,
    +from freqtrade.exceptions import (ExchangeError,
                                       PricingError)
     from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
     from freqtrade.misc import shorten_date
    @@ -555,7 +555,7 @@ class RPC:
                     try:
                         self._freqtrade.exchange.cancel_order(trade.open_order_id, trade.pair)
                         c_count += 1
    -                except (ExchangeError, InvalidOrderException):
    +                except (ExchangeError):
                         pass
     
                 # cancel stoploss on exchange ...
    @@ -565,7 +565,7 @@ class RPC:
                         self._freqtrade.exchange.cancel_stoploss_order(trade.stoploss_order_id,
                                                                        trade.pair)
                         c_count += 1
    -                except (ExchangeError, InvalidOrderException):
    +                except (ExchangeError):
                         pass
     
                 Trade.session.delete(trade)
    diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
    index 87071be3e..2c6d2314c 100644
    --- a/tests/test_freqtradebot.py
    +++ b/tests/test_freqtradebot.py
    @@ -1,6 +1,7 @@
     # pragma pylint: disable=missing-docstring, C0103
     # pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments
     
    +from freqtrade.exchange.exchange import Exchange
     import logging
     import time
     from copy import deepcopy
    @@ -4107,7 +4108,7 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order,
     def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limit_sell_order):
         default_conf['cancel_open_orders_on_exit'] = True
         mocker.patch('freqtrade.exchange.Exchange.fetch_order',
    -                 side_effect=[DependencyException(), limit_sell_order, limit_buy_order])
    +                 side_effect=[ExchangeError(), limit_sell_order, limit_buy_order])
         buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_buy')
         sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_sell')
     
    
    From 815d88fd4a4f2b133653df7fa75d6fa2c40c69ed Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Wed, 12 Aug 2020 15:32:56 +0200
    Subject: [PATCH 0389/1197] Fix test after merge, fix forgotten 'amount'
    
    ---
     freqtrade/freqtradebot.py | 2 +-
     freqtrade/persistence.py  | 2 +-
     2 files changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
    index b4ef2b086..816d24e18 100644
    --- a/freqtrade/freqtradebot.py
    +++ b/freqtrade/freqtradebot.py
    @@ -553,7 +553,7 @@ class FreqtradeBot:
                                    order['filled'], order['amount'], order['remaining']
                                    )
                     stake_amount = order['cost']
    -                amount = order['filled']
    +                amount = safe_value_fallback(order, 'filled', 'amount')
                     buy_limit_filled_price = safe_value_fallback(order, 'average', 'price')
                     order_id = None
     
    diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py
    index fdb816eab..28753ed48 100644
    --- a/freqtrade/persistence.py
    +++ b/freqtrade/persistence.py
    @@ -259,7 +259,7 @@ class Trade(_DECL_BASE):
                 'is_open': self.is_open,
                 'exchange': self.exchange,
                 'amount': round(self.amount, 8),
    -            'amount_requested': round(self.amount_requested, 8),
    +            'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None,
                 'stake_amount': round(self.stake_amount, 8),
                 'strategy': self.strategy,
                 'ticker_interval': self.timeframe,  # DEPRECATED
    
    From 3afd5b631e39a85d3c0536979f8e529c6c82a917 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Wed, 12 Aug 2020 15:34:29 +0200
    Subject: [PATCH 0390/1197] Remove erroneous import
    
    ---
     tests/test_freqtradebot.py | 1 -
     1 file changed, 1 deletion(-)
    
    diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
    index 2c6d2314c..ec59ca5b0 100644
    --- a/tests/test_freqtradebot.py
    +++ b/tests/test_freqtradebot.py
    @@ -1,7 +1,6 @@
     # pragma pylint: disable=missing-docstring, C0103
     # pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments
     
    -from freqtrade.exchange.exchange import Exchange
     import logging
     import time
     from copy import deepcopy
    
    From 827c31d4bc4e7511acdf5d0f37313a1343251c8a Mon Sep 17 00:00:00 2001
    From: Blackhawke 
    Date: Wed, 12 Aug 2020 09:42:16 -0700
    Subject: [PATCH 0391/1197] Re-arranged the introduction to better explain the
     theory of operation and the limitations of Edge. Added paragraphs at the
     bottom of "running edge independently" to better explain Edge's order of
     operations processing and potential differences between historical output and
     live/dry-run operation.
    
    ---
     docs/edge.md | 14 ++++++++++----
     1 file changed, 10 insertions(+), 4 deletions(-)
    
    diff --git a/docs/edge.md b/docs/edge.md
    index c91e72a3a..ccbae1cb1 100644
    --- a/docs/edge.md
    +++ b/docs/edge.md
    @@ -2,11 +2,13 @@
     
     This page explains how to use Edge Positioning module in your bot in order to enter into a trade only if the trade has a reasonable win rate and risk reward ratio, and consequently adjust your position size and stoploss.
     
    -!!! Warning
    +  !!! Warning
         Edge positioning is not compatible with dynamic (volume-based) whitelist.
     
    -!!! Note
    -    Edge does not consider anything else than buy/sell/stoploss signals. So trailing stoploss, ROI, and everything else are ignored in its calculation.
    +  !!! Note
    + 1. Edge does not consider anything other than *its own* buy/sell/stoploss signals. It ignores the stoploss, trailing stoploss, and ROI settings in the strategy configuration file.
    +
    + 2. Therefore, it is important to understand that Edge can improve the performance of some trading strategies but *decrease* the performance of others.
     
     ## Introduction
     
    @@ -89,7 +91,7 @@ You can also use this value to evaluate the effectiveness of modifications to th
     
     ## How does it work?
     
    -If enabled in config, Edge will go through historical data with a range of stoplosses in order to find buy and sell/stoploss signals. It then calculates win rate and expectancy over *N* trades for each stoploss. Here is an example:
    +Edge combines dynamic stoploss, dynamic positions, and whitelist generation into one isolated module which is then applied to the trading strategy. If enabled in config, Edge will go through historical data with a range of stoplosses in order to find buy and sell/stoploss signals. It then calculates win rate and expectancy over *N* trades for each stoploss. Here is an example:
     
     | Pair   |      Stoploss      |  Win Rate | Risk Reward Ratio | Expectancy |
     |----------|:-------------:|-------------:|------------------:|-----------:|
    @@ -186,6 +188,10 @@ An example of its output:
     | APPC/BTC  |      -0.02 |       0.44 |                2.28 |                   1.27 |         0.44 |                       25 |                       43 |
     | NEBL/BTC  |      -0.03 |       0.63 |                1.29 |                   0.58 |         0.44 |                       19 |                       59 |
     
    +Edge produced the above table by comparing ``calculate_since_number_of_days`` to ``minimum_expectancy`` to find ``min_trade_number``. Historical information based on the config file. The time frame Edge uses for its comparisons can be further limited by using the ``--timeframe`` switch.
    +
    +In live and dry-run modes, after the ``process_throttle_secs`` has passed, Edge will again process ``calculate_since_number_of_days`` against ``minimum_expectancy`` to find ``min_trade_number``. If no ``min_trade_number`` is found, the bot will return "whitelist empty". Depending on the trade strategy being deployed, "whitelist empty" may be return much of the time---or *all* of the time. The use of Edge may also cause trading to occur in bursts, though this is rare.
    +
     ### Update cached pairs with the latest data
     
     Edge requires historic data the same way as backtesting does.
    
    From 1dabade883f13dcdff990b6069fe9d8a1aab498c Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 13 Aug 2020 08:02:36 +0200
    Subject: [PATCH 0392/1197] small rewording of FAQ documentation
    
    ---
     docs/faq.md | 24 +++++++++++++++---------
     1 file changed, 15 insertions(+), 9 deletions(-)
    
    diff --git a/docs/faq.md b/docs/faq.md
    index cc43e326d..514b01085 100644
    --- a/docs/faq.md
    +++ b/docs/faq.md
    @@ -19,11 +19,11 @@ This could have the following reasons:
     
     ### I have waited 5 minutes, why hasn't the bot made any trades yet?!
     
    -#1 Depending on the buy strategy, the amount of whitelisted coins, the
    +* Depending on the buy strategy, the amount of whitelisted coins, the
     situation of the market etc, it can take up to hours to find good entry
     position for a trade. Be patient!
     
    -#2 Or it may because you made an human error? Like writing --dry-run when you wanted to trade live?. Maybe an error with the exchange API? Or something else. You will have to do the hard work of finding out the root cause of the problem :) 
    +* Or it may because of a configuration error? Best check the logs, it's usually telling you if the bot is simply not getting buy signals (only heartbeat messages), or if there is something wrong (errors / exceptions in the log).
     
     ### I have made 12 trades already, why is my total profit negative?!
     
    @@ -135,7 +135,9 @@ to find a great result (unless if you are very lucky), so you probably
     have to run it for 10.000 or more. But it will take an eternity to
     compute.
     
    -We recommend you to run between 500-1000 epochs over and over untill you hit at least 10.000 epocs in total. You can best judge by looking at the results - if the bot keep discovering more profitable strategies or not. 
    +Since hyperopt uses Bayesian search, running for too many epochs may not produce greater results.
    +
    +It's therefore recommended to run between 500-1000 epochs over and over until you hit at least 10.000 epocs in total (or are satisfied with the result). You can best judge by looking at the results - if the bot keeps discovering better strategies, it's best to keep on going. 
     
     ```bash
     freqtrade hyperopt -e 1000
    @@ -147,11 +149,11 @@ or if you want intermediate result to see
     for i in {1..100}; do freqtrade hyperopt -e 1000; done
     ```
     
    -### Why does it take so long time to run hyperopt?
    +### Why does it take a long time to run hyperopt?
     
    -#1 Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Github page, join the Freqtrade Discord - or something totally else. While you patiently wait for the most advanced, public known, crypto bot, in the world, to hand you a possible golden strategy specially designed just for you =) 
    +* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) - or the Freqtrade [discord community](https://discord.gg/X89cVG). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you.
     
    -#2 If you wonder why it can take from 20 minutes to days to do 1000 epocs here are some answers:
    +* If you wonder why it can take from 20 minutes to days to do 1000 epocs here are some answers:
     
     This answer was written during the release 0.15.1, when we had:
     
    @@ -163,10 +165,14 @@ The following calculation is still very rough and not very precise
     but it will give the idea. With only these triggers and guards there is
     already 8\*10^9\*10 evaluations. A roughly total of 80 billion evals.
     Did you run 100 000 evals? Congrats, you've done roughly 1 / 100 000 th
    -of the search space. If we assume that the bot never test the same strategy more than once.
    +of the search space, assuming that the bot never tests the same parameters more than once.
     
    -#3 The time it takes to run 1000 hyperopt epocs depends on things like: The cpu, harddisk, ram, motherboard, indicator settings, indicator count, amount of coins that hyperopt test strategies on, trade count - can be 650 trades in a year or 10.0000 trades depending on if the strategy aims for a high profit rarely or a low profit many many many times. Example: 4% profit 650 times vs 0,3% profit a trade 10.000 times in a year. If we assume you set the --timerange to 365 days. 
    -Example: freqtrade --config config_mcd_1.json --strategy mcd_1 --hyperopt mcd_hyperopt_1 -e 1000 --timerange 20190601-20200601 
    +* The time it takes to run 1000 hyperopt epocs depends on things like: The available cpu, harddisk, ram, timeframe, timerange, indicator settings, indicator count, amount of coins that hyperopt test strategies on and the resulting trade count - which can be 650 trades in a year or 10.0000 trades depending if the strategy aims for big profits by trading rarely or for many low profit trades. 
    +
    +Example: 4% profit 650 times vs 0,3% profit a trade 10.000 times in a year. If we assume you set the --timerange to 365 days. 
    +
    +Example: 
    +`freqtrade --config config.json --strategy SampleStrategy --hyperopt SampleHyperopt -e 1000 --timerange 20190601-20200601`
     
     ## Edge module
     
    
    From e45e41adb457d90db1b4c281275779f44f9d6157 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 13 Aug 2020 08:05:05 +0200
    Subject: [PATCH 0393/1197] Improve docs test to catch !!! errors
    
    ---
     tests/test_docs.sh | 3 ++-
     1 file changed, 2 insertions(+), 1 deletion(-)
    
    diff --git a/tests/test_docs.sh b/tests/test_docs.sh
    index 09e142b99..8a354daad 100755
    --- a/tests/test_docs.sh
    +++ b/tests/test_docs.sh
    @@ -2,7 +2,8 @@
     # Test Documentation boxes -
     # !!! : is not allowed!
     # !!!  "title" - Title needs to be quoted!
    -grep -Er '^!{3}\s\S+:|^!{3}\s\S+\s[^"]' docs/*
    +# !!!  Spaces at the beginning are not allowed
    +grep -Er '^!{3}\s\S+:|^!{3}\s\S+\s[^"]|^\s+!{3}\s\S+' docs/*
     
     if  [ $? -ne 0 ]; then
         echo "Docs test success."
    
    From 6b85b1a34d65a42a6e24347d9137a6b4f604aef9 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 13 Aug 2020 08:06:57 +0200
    Subject: [PATCH 0394/1197] Don't only recommend pycharm, but keep it open to
     other editors too.
    
    ---
     docs/faq.md | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/docs/faq.md b/docs/faq.md
    index 514b01085..48f52a566 100644
    --- a/docs/faq.md
    +++ b/docs/faq.md
    @@ -2,7 +2,7 @@
     
     ## Beginner Tips & Tricks
     
    -#1 When you work with your strategy & hyperopt file you should use a real programmer software like Pycharm. If you by accident moved some code and freqtrade says error and you cant find the place where you moved something, or you cant find line 180 where you messed something up. Then a program like Pycharm shows you where line 180 is in your strategy file so you can fix the problem, or Pycharm shows you with some color marking that "here is a line of code that does not belong here" and you found your error in no time! This will save you many hours of problemsolving when working with the bot. Pycharm also got a usefull "Debug" feature that can tell you exactly what command on that line is making the error :) 
    +* When you work with your strategy & hyperopt file you should use a proper code editor like vscode or Pycharm. A good code editor will provide syntax highlighting as well as line numbers, making it easy to find syntax errors (most likely, pointed out by Freqtrade during startup).
     
     ## Freqtrade common issues
     
    
    From 4109b31dac08b607603590c798b41fad745f6bdf Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Fri, 14 Aug 2020 06:46:34 +0200
    Subject: [PATCH 0395/1197] Update wording in documentation
    
    ---
     docs/developer.md | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/docs/developer.md b/docs/developer.md
    index ce454cec2..f09ae2c76 100644
    --- a/docs/developer.md
    +++ b/docs/developer.md
    @@ -88,7 +88,7 @@ docker-compose exec freqtrade_develop /bin/bash
     ## ErrorHandling
     
     Freqtrade Exceptions all inherit from `FreqtradeException`.
    -This general class of error should however not be used directly, instead, multiple specialized sub-Exceptions exist.
    +This general class of error should however not be used directly. Instead, multiple specialized sub-Exceptions exist.
     
     Below is an outline of exception inheritance hierarchy:
     
    
    From d76ee432461a438126439b01e69d00eea3a045fc Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Fri, 14 Aug 2020 07:12:57 +0200
    Subject: [PATCH 0396/1197] Show wins / draws / losses in hyperopt table
    
    ---
     freqtrade/optimize/hyperopt.py  | 17 +++++++++++++----
     tests/optimize/test_hyperopt.py |  1 +
     2 files changed, 14 insertions(+), 4 deletions(-)
    
    diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
    index 522b217f7..fbd523904 100644
    --- a/freqtrade/optimize/hyperopt.py
    +++ b/freqtrade/optimize/hyperopt.py
    @@ -312,11 +312,16 @@ class Hyperopt:
     
             trials = json_normalize(results, max_level=1)
             trials['Best'] = ''
    +        if 'results_metrics.winsdrawslosses' not in trials.columns:
    +            # Ensure compatibility with older versions of hyperopt results
    +            trials['results_metrics.winsdrawslosses'] = 'N/A'
    +
             trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count',
    +                         'results_metrics.winsdrawslosses',
                              'results_metrics.avg_profit', 'results_metrics.total_profit',
                              'results_metrics.profit', 'results_metrics.duration',
                              'loss', 'is_initial_point', 'is_best']]
    -        trials.columns = ['Best', 'Epoch', 'Trades', 'Avg profit', 'Total profit',
    +        trials.columns = ['Best', 'Epoch', 'Trades', 'W/D/L', 'Avg profit', 'Total profit',
                               'Profit', 'Avg duration', 'Objective', 'is_initial_point', 'is_best']
             trials['is_profit'] = False
             trials.loc[trials['is_initial_point'], 'Best'] = '*     '
    @@ -558,11 +563,15 @@ class Hyperopt:
             }
     
         def _calculate_results_metrics(self, backtesting_results: DataFrame) -> Dict:
    +        wins = len(backtesting_results[backtesting_results.profit_percent > 0])
    +        draws = len(backtesting_results[backtesting_results.profit_percent == 0])
    +        losses = len(backtesting_results[backtesting_results.profit_percent < 0])
             return {
                 'trade_count': len(backtesting_results.index),
    -            'wins': len(backtesting_results[backtesting_results.profit_percent > 0]),
    -            'draws': len(backtesting_results[backtesting_results.profit_percent == 0]),
    -            'losses': len(backtesting_results[backtesting_results.profit_percent < 0]),
    +            'wins': wins,
    +            'draws': draws,
    +            'losses': losses,
    +            'winsdrawslosses': f"{wins}/{draws}/{losses}",
                 'avg_profit': backtesting_results.profit_percent.mean() * 100.0,
                 'median_profit': backtesting_results.profit_percent.median() * 100.0,
                 'total_profit': backtesting_results.profit_abs.sum(),
    diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py
    index 4b178ca11..bd86e315f 100644
    --- a/tests/optimize/test_hyperopt.py
    +++ b/tests/optimize/test_hyperopt.py
    @@ -781,6 +781,7 @@ def test_generate_optimizer(mocker, default_conf) -> None:
                                 'draws': 0,
                                 'duration': 100.0,
                                 'losses': 0,
    +                            'winsdrawslosses': '1/0/0',
                                 'median_profit': 2.3117,
                                 'profit': 2.3117,
                                 'total_profit': 0.000233,
    
    From b98107375edc44ff6d90bfb67c038d839a5f0d47 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Fri, 14 Aug 2020 07:31:14 +0200
    Subject: [PATCH 0397/1197] Improve formatting of result string to be a bit
     conciser
    
    ---
     freqtrade/optimize/hyperopt.py  | 5 ++---
     tests/optimize/test_hyperopt.py | 2 +-
     2 files changed, 3 insertions(+), 4 deletions(-)
    
    diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
    index fbd523904..6d11e543b 100644
    --- a/freqtrade/optimize/hyperopt.py
    +++ b/freqtrade/optimize/hyperopt.py
    @@ -585,9 +585,8 @@ class Hyperopt:
             """
             stake_cur = self.config['stake_currency']
             return (f"{results_metrics['trade_count']:6d} trades. "
    -                f"{results_metrics['wins']:6d} wins. "
    -                f"{results_metrics['draws']:6d} draws. "
    -                f"{results_metrics['losses']:6d} losses. "
    +                f"{results_metrics['wins']}/{results_metrics['draws']}"
    +                f"/{results_metrics['losses']} Wins/Draws/Losses. "
                     f"Avg profit {results_metrics['avg_profit']: 6.2f}%. "
                     f"Median profit {results_metrics['median_profit']: 6.2f}%. "
                     f"Total profit {results_metrics['total_profit']: 11.8f} {stake_cur} "
    diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py
    index bd86e315f..a6541f55b 100644
    --- a/tests/optimize/test_hyperopt.py
    +++ b/tests/optimize/test_hyperopt.py
    @@ -744,7 +744,7 @@ def test_generate_optimizer(mocker, default_conf) -> None:
         }
         response_expected = {
             'loss': 1.9840569076926293,
    -        'results_explanation': ('     1 trades.      1 wins.      0 draws.      0 losses. '
    +        'results_explanation': ('     1 trades. 1/0/0 Wins/Draws/Losses. '
                                     'Avg profit   2.31%. Median profit   2.31%. Total profit  '
                                     '0.00023300 BTC (   2.31\N{GREEK CAPITAL LETTER SIGMA}%). '
                                     'Avg duration 100.0 min.'
    
    From 044df880e68d513d5e3baf2d451e656631e9194d Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 13 Aug 2020 08:28:46 +0200
    Subject: [PATCH 0398/1197] Move persistence into it's own submodule
    
    ---
     freqtrade/persistence/__init__.py                   |  3 +++
     freqtrade/{persistence.py => persistence/models.py} |  0
     tests/test_persistence.py                           | 12 ++++++------
     3 files changed, 9 insertions(+), 6 deletions(-)
     create mode 100644 freqtrade/persistence/__init__.py
     rename freqtrade/{persistence.py => persistence/models.py} (100%)
    
    diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py
    new file mode 100644
    index 000000000..0973fab3f
    --- /dev/null
    +++ b/freqtrade/persistence/__init__.py
    @@ -0,0 +1,3 @@
    +# flake8: noqa: F401
    +
    +from freqtrade.persistence.models import Trade, clean_dry_run_db, cleanup, init
    diff --git a/freqtrade/persistence.py b/freqtrade/persistence/models.py
    similarity index 100%
    rename from freqtrade/persistence.py
    rename to freqtrade/persistence/models.py
    diff --git a/tests/test_persistence.py b/tests/test_persistence.py
    index 65c83e05b..dbb62e636 100644
    --- a/tests/test_persistence.py
    +++ b/tests/test_persistence.py
    @@ -22,7 +22,7 @@ def test_init_create_session(default_conf):
     def test_init_custom_db_url(default_conf, mocker):
         # Update path to a value other than default, but still in-memory
         default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'})
    -    create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
    +    create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock())
     
         init(default_conf['db_url'], default_conf['dry_run'])
         assert create_engine_mock.call_count == 1
    @@ -40,7 +40,7 @@ def test_init_prod_db(default_conf, mocker):
         default_conf.update({'dry_run': False})
         default_conf.update({'db_url': constants.DEFAULT_DB_PROD_URL})
     
    -    create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
    +    create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock())
     
         init(default_conf['db_url'], default_conf['dry_run'])
         assert create_engine_mock.call_count == 1
    @@ -51,7 +51,7 @@ def test_init_dryrun_db(default_conf, mocker):
         default_conf.update({'dry_run': True})
         default_conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL})
     
    -    create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
    +    create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock())
     
         init(default_conf['db_url'], default_conf['dry_run'])
         assert create_engine_mock.call_count == 1
    @@ -440,7 +440,7 @@ def test_migrate_old(mocker, default_conf, fee):
                                          amount=amount
                                          )
         engine = create_engine('sqlite://')
    -    mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine)
    +    mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine)
     
         # Create table using the old format
         engine.execute(create_table_old)
    @@ -524,7 +524,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
                                          amount=amount
                                          )
         engine = create_engine('sqlite://')
    -    mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine)
    +    mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine)
     
         # Create table using the old format
         engine.execute(create_table_old)
    @@ -601,7 +601,7 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog):
                                          amount=amount
                                          )
         engine = create_engine('sqlite://')
    -    mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine)
    +    mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine)
     
         # Create table using the old format
         engine.execute(create_table_old)
    
    From 7d03a067ee107a7f2cd3dc48653a61b82e1988da Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 13 Aug 2020 08:33:46 +0200
    Subject: [PATCH 0399/1197] Extract migrations ot seperate module
    
    ---
     freqtrade/persistence/migrations.py | 113 ++++++++++++++++++++++++++++
     freqtrade/persistence/models.py     | 112 +--------------------------
     2 files changed, 116 insertions(+), 109 deletions(-)
     create mode 100644 freqtrade/persistence/migrations.py
    
    diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py
    new file mode 100644
    index 000000000..bd7dd103f
    --- /dev/null
    +++ b/freqtrade/persistence/migrations.py
    @@ -0,0 +1,113 @@
    +import logging
    +from typing import List
    +
    +from sqlalchemy import inspect
    +
    +logger = logging.getLogger(__name__)
    +
    +
    +def has_column(columns: List, searchname: str) -> bool:
    +    return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1
    +
    +
    +def get_column_def(columns: List, column: str, default: str) -> str:
    +    return default if not has_column(columns, column) else column
    +
    +
    +def check_migrate(engine, decl_base) -> None:
    +    """
    +    Checks if migration is necessary and migrates if necessary
    +    """
    +    inspector = inspect(engine)
    +
    +    cols = inspector.get_columns('trades')
    +    tabs = inspector.get_table_names()
    +    table_back_name = 'trades_bak'
    +    for i, table_back_name in enumerate(tabs):
    +        table_back_name = f'trades_bak{i}'
    +        logger.debug(f'trying {table_back_name}')
    +
    +    # Check for latest column
    +    if not has_column(cols, 'amount_requested'):
    +        logger.info(f'Running database migration - backup available as {table_back_name}')
    +
    +        fee_open = get_column_def(cols, 'fee_open', 'fee')
    +        fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null')
    +        fee_open_currency = get_column_def(cols, 'fee_open_currency', 'null')
    +        fee_close = get_column_def(cols, 'fee_close', 'fee')
    +        fee_close_cost = get_column_def(cols, 'fee_close_cost', 'null')
    +        fee_close_currency = get_column_def(cols, 'fee_close_currency', 'null')
    +        open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null')
    +        close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null')
    +        stop_loss = get_column_def(cols, 'stop_loss', '0.0')
    +        stop_loss_pct = get_column_def(cols, 'stop_loss_pct', 'null')
    +        initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
    +        initial_stop_loss_pct = get_column_def(cols, 'initial_stop_loss_pct', 'null')
    +        stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null')
    +        stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null')
    +        max_rate = get_column_def(cols, 'max_rate', '0.0')
    +        min_rate = get_column_def(cols, 'min_rate', 'null')
    +        sell_reason = get_column_def(cols, 'sell_reason', 'null')
    +        strategy = get_column_def(cols, 'strategy', 'null')
    +        # If ticker-interval existed use that, else null.
    +        if has_column(cols, 'ticker_interval'):
    +            timeframe = get_column_def(cols, 'timeframe', 'ticker_interval')
    +        else:
    +            timeframe = get_column_def(cols, 'timeframe', 'null')
    +
    +        open_trade_price = get_column_def(cols, 'open_trade_price',
    +                                          f'amount * open_rate * (1 + {fee_open})')
    +        close_profit_abs = get_column_def(
    +            cols, 'close_profit_abs',
    +            f"(amount * close_rate * (1 - {fee_close})) - {open_trade_price}")
    +        sell_order_status = get_column_def(cols, 'sell_order_status', 'null')
    +        amount_requested = get_column_def(cols, 'amount_requested', 'amount')
    +
    +        # Schema migration necessary
    +        engine.execute(f"alter table trades rename to {table_back_name}")
    +        # drop indexes on backup table
    +        for index in inspector.get_indexes(table_back_name):
    +            engine.execute(f"drop index {index['name']}")
    +        # let SQLAlchemy create the schema as required
    +        decl_base.metadata.create_all(engine)
    +
    +        # Copy data back - following the correct schema
    +        engine.execute(f"""insert into trades
    +                (id, exchange, pair, is_open,
    +                fee_open, fee_open_cost, fee_open_currency,
    +                fee_close, fee_close_cost, fee_open_currency, open_rate,
    +                open_rate_requested, close_rate, close_rate_requested, close_profit,
    +                stake_amount, amount, amount_requested, open_date, close_date, open_order_id,
    +                stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
    +                stoploss_order_id, stoploss_last_update,
    +                max_rate, min_rate, sell_reason, sell_order_status, strategy,
    +                timeframe, open_trade_price, close_profit_abs
    +                )
    +            select id, lower(exchange),
    +                case
    +                    when instr(pair, '_') != 0 then
    +                    substr(pair,    instr(pair, '_') + 1) || '/' ||
    +                    substr(pair, 1, instr(pair, '_') - 1)
    +                    else pair
    +                    end
    +                pair,
    +                is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost,
    +                {fee_open_currency} fee_open_currency, {fee_close} fee_close,
    +                {fee_close_cost} fee_close_cost, {fee_close_currency} fee_close_currency,
    +                open_rate, {open_rate_requested} open_rate_requested, close_rate,
    +                {close_rate_requested} close_rate_requested, close_profit,
    +                stake_amount, amount, {amount_requested}, open_date, close_date, open_order_id,
    +                {stop_loss} stop_loss, {stop_loss_pct} stop_loss_pct,
    +                {initial_stop_loss} initial_stop_loss,
    +                {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,
    +                {sell_order_status} sell_order_status,
    +                {strategy} strategy, {timeframe} timeframe,
    +                {open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs
    +                from {table_back_name}
    +             """)
    +
    +        # Reread columns - the above recreated the table!
    +        inspector = inspect(engine)
    +        cols = inspector.get_columns('trades')
    diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py
    index 28753ed48..f56ff47fe 100644
    --- a/freqtrade/persistence/models.py
    +++ b/freqtrade/persistence/models.py
    @@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional
     
     import arrow
     from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String,
    -                        create_engine, desc, func, inspect)
    +                        create_engine, desc, func)
     from sqlalchemy.exc import NoSuchModuleError
     from sqlalchemy.ext.declarative import declarative_base
     from sqlalchemy.orm import Query
    @@ -18,6 +18,7 @@ from sqlalchemy.pool import StaticPool
     
     from freqtrade.exceptions import OperationalException
     from freqtrade.misc import safe_value_fallback
    +from freqtrade.persistence.migrations import check_migrate
     
     logger = logging.getLogger(__name__)
     
    @@ -58,120 +59,13 @@ def init(db_url: str, clean_open_orders: bool = False) -> None:
         Trade.session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
         Trade.query = Trade.session.query_property()
         _DECL_BASE.metadata.create_all(engine)
    -    check_migrate(engine)
    +    check_migrate(engine, decl_base=_DECL_BASE)
     
         # Clean dry_run DB if the db is not in-memory
         if clean_open_orders and db_url != 'sqlite://':
             clean_dry_run_db()
     
     
    -def has_column(columns: List, searchname: str) -> bool:
    -    return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1
    -
    -
    -def get_column_def(columns: List, column: str, default: str) -> str:
    -    return default if not has_column(columns, column) else column
    -
    -
    -def check_migrate(engine) -> None:
    -    """
    -    Checks if migration is necessary and migrates if necessary
    -    """
    -    inspector = inspect(engine)
    -
    -    cols = inspector.get_columns('trades')
    -    tabs = inspector.get_table_names()
    -    table_back_name = 'trades_bak'
    -    for i, table_back_name in enumerate(tabs):
    -        table_back_name = f'trades_bak{i}'
    -        logger.debug(f'trying {table_back_name}')
    -
    -    # Check for latest column
    -    if not has_column(cols, 'amount_requested'):
    -        logger.info(f'Running database migration - backup available as {table_back_name}')
    -
    -        fee_open = get_column_def(cols, 'fee_open', 'fee')
    -        fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null')
    -        fee_open_currency = get_column_def(cols, 'fee_open_currency', 'null')
    -        fee_close = get_column_def(cols, 'fee_close', 'fee')
    -        fee_close_cost = get_column_def(cols, 'fee_close_cost', 'null')
    -        fee_close_currency = get_column_def(cols, 'fee_close_currency', 'null')
    -        open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null')
    -        close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null')
    -        stop_loss = get_column_def(cols, 'stop_loss', '0.0')
    -        stop_loss_pct = get_column_def(cols, 'stop_loss_pct', 'null')
    -        initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
    -        initial_stop_loss_pct = get_column_def(cols, 'initial_stop_loss_pct', 'null')
    -        stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null')
    -        stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null')
    -        max_rate = get_column_def(cols, 'max_rate', '0.0')
    -        min_rate = get_column_def(cols, 'min_rate', 'null')
    -        sell_reason = get_column_def(cols, 'sell_reason', 'null')
    -        strategy = get_column_def(cols, 'strategy', 'null')
    -        # If ticker-interval existed use that, else null.
    -        if has_column(cols, 'ticker_interval'):
    -            timeframe = get_column_def(cols, 'timeframe', 'ticker_interval')
    -        else:
    -            timeframe = get_column_def(cols, 'timeframe', 'null')
    -
    -        open_trade_price = get_column_def(cols, 'open_trade_price',
    -                                          f'amount * open_rate * (1 + {fee_open})')
    -        close_profit_abs = get_column_def(
    -            cols, 'close_profit_abs',
    -            f"(amount * close_rate * (1 - {fee_close})) - {open_trade_price}")
    -        sell_order_status = get_column_def(cols, 'sell_order_status', 'null')
    -        amount_requested = get_column_def(cols, 'amount_requested', 'amount')
    -
    -        # Schema migration necessary
    -        engine.execute(f"alter table trades rename to {table_back_name}")
    -        # drop indexes on backup table
    -        for index in inspector.get_indexes(table_back_name):
    -            engine.execute(f"drop index {index['name']}")
    -        # let SQLAlchemy create the schema as required
    -        _DECL_BASE.metadata.create_all(engine)
    -
    -        # Copy data back - following the correct schema
    -        engine.execute(f"""insert into trades
    -                (id, exchange, pair, is_open,
    -                fee_open, fee_open_cost, fee_open_currency,
    -                fee_close, fee_close_cost, fee_open_currency, open_rate,
    -                open_rate_requested, close_rate, close_rate_requested, close_profit,
    -                stake_amount, amount, amount_requested, open_date, close_date, open_order_id,
    -                stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
    -                stoploss_order_id, stoploss_last_update,
    -                max_rate, min_rate, sell_reason, sell_order_status, strategy,
    -                timeframe, open_trade_price, close_profit_abs
    -                )
    -            select id, lower(exchange),
    -                case
    -                    when instr(pair, '_') != 0 then
    -                    substr(pair,    instr(pair, '_') + 1) || '/' ||
    -                    substr(pair, 1, instr(pair, '_') - 1)
    -                    else pair
    -                    end
    -                pair,
    -                is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost,
    -                {fee_open_currency} fee_open_currency, {fee_close} fee_close,
    -                {fee_close_cost} fee_close_cost, {fee_close_currency} fee_close_currency,
    -                open_rate, {open_rate_requested} open_rate_requested, close_rate,
    -                {close_rate_requested} close_rate_requested, close_profit,
    -                stake_amount, amount, {amount_requested}, open_date, close_date, open_order_id,
    -                {stop_loss} stop_loss, {stop_loss_pct} stop_loss_pct,
    -                {initial_stop_loss} initial_stop_loss,
    -                {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,
    -                {sell_order_status} sell_order_status,
    -                {strategy} strategy, {timeframe} timeframe,
    -                {open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs
    -                from {table_back_name}
    -             """)
    -
    -        # Reread columns - the above recreated the table!
    -        inspector = inspect(engine)
    -        cols = inspector.get_columns('trades')
    -
    -
     def cleanup() -> None:
         """
         Flushes all pending operations to disk.
    
    From 171a52b21a076f6c20e21408b891258e40738bd6 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 13 Aug 2020 09:34:53 +0200
    Subject: [PATCH 0400/1197] Introduce Order database model
    
    ---
     freqtrade/exchange/exchange.py    |  1 +
     freqtrade/freqtradebot.py         | 12 +++++-
     freqtrade/persistence/__init__.py |  3 +-
     freqtrade/persistence/models.py   | 64 +++++++++++++++++++++++++++++--
     4 files changed, 74 insertions(+), 6 deletions(-)
    
    diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
    index d32f79a3f..34d57ae4d 100644
    --- a/freqtrade/exchange/exchange.py
    +++ b/freqtrade/exchange/exchange.py
    @@ -487,6 +487,7 @@ class Exchange:
                 'side': side,
                 'remaining': _amount,
                 'datetime': arrow.utcnow().isoformat(),
    +            'timestamp': arrow.utcnow().timestamp,
                 'status': "closed" if ordertype == "market" else "open",
                 'fee': None,
                 'info': {}
    diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
    index 2a95f58fc..ff282aa77 100644
    --- a/freqtrade/freqtradebot.py
    +++ b/freqtrade/freqtradebot.py
    @@ -22,7 +22,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError,
     from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date
     from freqtrade.misc import safe_value_fallback, safe_value_fallback2
     from freqtrade.pairlist.pairlistmanager import PairListManager
    -from freqtrade.persistence import Trade
    +from freqtrade.persistence import Order, Trade
     from freqtrade.resolvers import ExchangeResolver, StrategyResolver
     from freqtrade.rpc import RPCManager, RPCMessageType
     from freqtrade.state import State
    @@ -527,6 +527,7 @@ class FreqtradeBot:
             order = self.exchange.buy(pair=pair, ordertype=order_type,
                                       amount=amount, rate=buy_limit_requested,
                                       time_in_force=time_in_force)
    +        order_obj = Order.parse_from_ccxt_object(order, pair)
             order_id = order['id']
             order_status = order.get('status', None)
     
    @@ -580,6 +581,7 @@ class FreqtradeBot:
                 strategy=self.strategy.get_strategy_name(),
                 timeframe=timeframe_to_minutes(self.config['timeframe'])
             )
    +        trade.orders.append(order_obj)
     
             # Update fees if order is closed
             if order_status == 'closed':
    @@ -781,6 +783,9 @@ class FreqtradeBot:
                 stoploss_order = self.exchange.stoploss(pair=trade.pair, amount=trade.amount,
                                                         stop_price=stop_price,
                                                         order_types=self.strategy.order_types)
    +
    +            order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair)
    +            trade.orders.append(order_obj)
                 trade.stoploss_order_id = str(stoploss_order['id'])
                 return True
             except InvalidOrderException as e:
    @@ -1123,12 +1128,15 @@ class FreqtradeBot:
                 return False
     
             # Execute sell and update trade record
    -        order = self.exchange.sell(pair=str(trade.pair),
    +        order = self.exchange.sell(pair=trade.pair,
                                        ordertype=order_type,
                                        amount=amount, rate=limit,
                                        time_in_force=time_in_force
                                        )
     
    +        order_obj = Order.parse_from_ccxt_object(order, trade.pair)
    +        trade.orders.append(order_obj)
    +
             trade.open_order_id = order['id']
             trade.close_rate_requested = limit
             trade.sell_reason = sell_reason.value
    diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py
    index 0973fab3f..764856f2b 100644
    --- a/freqtrade/persistence/__init__.py
    +++ b/freqtrade/persistence/__init__.py
    @@ -1,3 +1,4 @@
     # flake8: noqa: F401
     
    -from freqtrade.persistence.models import Trade, clean_dry_run_db, cleanup, init
    +from freqtrade.persistence.models import (Order, Trade, clean_dry_run_db,
    +                                          cleanup, init)
    diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py
    index f56ff47fe..3b77438ea 100644
    --- a/freqtrade/persistence/models.py
    +++ b/freqtrade/persistence/models.py
    @@ -7,11 +7,11 @@ from decimal import Decimal
     from typing import Any, Dict, List, Optional
     
     import arrow
    -from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String,
    +from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String, ForeignKey,
                             create_engine, desc, func)
     from sqlalchemy.exc import NoSuchModuleError
     from sqlalchemy.ext.declarative import declarative_base
    -from sqlalchemy.orm import Query
    +from sqlalchemy.orm import Query, relationship
     from sqlalchemy.orm.scoping import scoped_session
     from sqlalchemy.orm.session import sessionmaker
     from sqlalchemy.pool import StaticPool
    @@ -85,13 +85,71 @@ def clean_dry_run_db() -> None:
                 trade.open_order_id = None
     
     
    +class Order(_DECL_BASE):
    +    """
    +    Order database model
    +    Keeps a record of all orders placed on the exchange
    +
    +    One to many relationship with Trades:
    +      - One trade can have many orders
    +      - One Order can only be associated with one Trade
    +
    +    Mirrors CCXT Order structure
    +    """
    +    __tablename__ = 'orders'
    +
    +    id = Column(Integer, primary_key=True)
    +    trade_id = Column(Integer, ForeignKey('trades.id'), index=True)
    +
    +    order_id = Column(String, nullable=False, index=True)
    +    status = Column(String, nullable=False)
    +    symbol = Column(String, nullable=False)
    +    order_type = Column(String, nullable=False)
    +    side = Column(String, nullable=False)
    +    price = Column(Float, nullable=False)
    +    amount = Column(Float, nullable=False)
    +    filled = Column(Float, nullable=True)
    +    remaining = Column(Float, nullable=True)
    +    cost = Column(Float, nullable=True)
    +    order_date = Column(DateTime, nullable=False, default=datetime.utcnow)
    +    order_filled_date = Column(DateTime, nullable=True)
    +
    +    @staticmethod
    +    def parse_from_ccxt_object(order, pair) -> 'Order':
    +        """
    +        Parse an order from a ccxt object and return a new order Object.
    +        """
    +        o = Order(order_id=str(order['id']))
    +
    +        o.status = order['status']
    +        o.symbol = order.get('symbol', pair)
    +        o.order_type = order['type']
    +        o.side = order['side']
    +        o.price = order['price']
    +        o.amount = order['amount']
    +        o.filled = order.get('filled')
    +        o.remaining = order.get('remaining')
    +        o.cost = order.get('cost')
    +        o.order_date = datetime.fromtimestamp(order['timestamp'])
    +        return o
    +
    +    def __repr__(self):
    +
    +        return (f'Order(id={self.id}, trade_id={self.trade_id}, side={self.side}, '
    +                f'status={self.status})')
    +
    +
     class Trade(_DECL_BASE):
         """
    -    Class used to define a trade structure
    +    Trade database model.
    +    Also handles updating and querying trades
         """
         __tablename__ = 'trades'
     
         id = Column(Integer, primary_key=True)
    +
    +    orders = relationship("Order", order_by="Order.id")
    +
         exchange = Column(String, nullable=False)
         pair = Column(String, nullable=False, index=True)
         is_open = Column(Boolean, nullable=False, default=True, index=True)
    
    From ed87abd93abe991f127fa9e0f1c8e03972e871d4 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 13 Aug 2020 09:41:12 +0200
    Subject: [PATCH 0401/1197] Allow selecting only a certain table range in
     migration
    
    ---
     freqtrade/persistence/migrations.py | 5 ++++-
     1 file changed, 4 insertions(+), 1 deletion(-)
    
    diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py
    index bd7dd103f..1bce8fef2 100644
    --- a/freqtrade/persistence/migrations.py
    +++ b/freqtrade/persistence/migrations.py
    @@ -5,6 +5,9 @@ from sqlalchemy import inspect
     
     logger = logging.getLogger(__name__)
     
    +def get_table_names_for_table(inspector, tabletype):
    +    return [t for t in inspector.get_table_names() if t.startswith(tabletype)]
    +
     
     def has_column(columns: List, searchname: str) -> bool:
         return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1
    @@ -21,7 +24,7 @@ def check_migrate(engine, decl_base) -> None:
         inspector = inspect(engine)
     
         cols = inspector.get_columns('trades')
    -    tabs = inspector.get_table_names()
    +    tabs = get_table_names_for_table(inspector, 'trades')
         table_back_name = 'trades_bak'
         for i, table_back_name in enumerate(tabs):
             table_back_name = f'trades_bak{i}'
    
    From a66a3d047f0d65643c09d5fdd805307fc360bdc0 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 13 Aug 2020 09:43:48 +0200
    Subject: [PATCH 0402/1197] Remove unneeded mocks
    
    ---
     tests/rpc/test_rpc_telegram.py | 2 --
     1 file changed, 2 deletions(-)
    
    diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
    index bfa774856..113232add 100644
    --- a/tests/rpc/test_rpc_telegram.py
    +++ b/tests/rpc/test_rpc_telegram.py
    @@ -249,7 +249,6 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
             fetch_ticker=ticker,
    -        buy=MagicMock(return_value={'id': 'mocked_order_id'}),
             get_fee=fee,
         )
         msg_mock = MagicMock()
    @@ -1002,7 +1001,6 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
             fetch_ticker=ticker,
    -        buy=MagicMock(return_value={'id': 'mocked_order_id'}),
             get_fee=fee,
         )
         freqtradebot = get_patched_freqtradebot(mocker, default_conf)
    
    From 420a8c2b1c20d0b2f9d87363f68731daccfff172 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 13 Aug 2020 11:36:58 +0200
    Subject: [PATCH 0403/1197] Improve tests for rpc/forcebuy
    
    ---
     tests/conftest.py     | 1 +
     tests/rpc/test_rpc.py | 3 ++-
     2 files changed, 3 insertions(+), 1 deletion(-)
    
    diff --git a/tests/conftest.py b/tests/conftest.py
    index 1ac8256a8..b8367f36d 100644
    --- a/tests/conftest.py
    +++ b/tests/conftest.py
    @@ -827,6 +827,7 @@ def limit_buy_order():
             'side': 'buy',
             'symbol': 'mocked',
             'datetime': arrow.utcnow().isoformat(),
    +        'timestamp': arrow.utcnow().timestamp,
             'price': 0.00001099,
             'amount': 90.99181073,
             'filled': 90.99181073,
    diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py
    index 9bbd34672..5af6a42cb 100644
    --- a/tests/rpc/test_rpc.py
    +++ b/tests/rpc/test_rpc.py
    @@ -817,7 +817,8 @@ def test_rpc_count(mocker, default_conf, ticker, fee) -> None:
     def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order) -> None:
         default_conf['forcebuy_enable'] = True
         mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
    -    buy_mm = MagicMock(return_value={'id': limit_buy_order['id']})
    +    limit_buy_order['status'] = 'open'
    +    buy_mm = MagicMock(return_value=limit_buy_order)
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
             get_balances=MagicMock(return_value=ticker),
    
    From ee7b235cdc0f3b782baaffd8fff185a18a86aeee Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 13 Aug 2020 13:33:45 +0200
    Subject: [PATCH 0404/1197] Improve tests to use open_order mock where
     applicable
    
    ---
     tests/conftest.py          |  35 +++++++--
     tests/rpc/test_rpc.py      |   5 +-
     tests/test_freqtradebot.py | 150 ++++++++++++++++++-------------------
     3 files changed, 103 insertions(+), 87 deletions(-)
    
    diff --git a/tests/conftest.py b/tests/conftest.py
    index b8367f36d..68b499dda 100644
    --- a/tests/conftest.py
    +++ b/tests/conftest.py
    @@ -820,7 +820,7 @@ def markets_empty():
     
     
     @pytest.fixture(scope='function')
    -def limit_buy_order():
    +def limit_buy_order_open():
         return {
             'id': 'mocked_limit_buy',
             'type': 'limit',
    @@ -830,13 +830,22 @@ def limit_buy_order():
             'timestamp': arrow.utcnow().timestamp,
             'price': 0.00001099,
             'amount': 90.99181073,
    -        'filled': 90.99181073,
    +        'filled': 0.0,
             'cost': 0.0009999,
    -        'remaining': 0.0,
    -        'status': 'closed'
    +        'remaining': 90.99181073,
    +        'status': 'open'
         }
     
     
    +@pytest.fixture(scope='function')
    +def limit_buy_order(limit_buy_order_open):
    +    order = deepcopy(limit_buy_order_open)
    +    order['status'] = 'closed'
    +    order['filled'] = order['amount']
    +    order['remaining'] = 0.0
    +    return order
    +
    +
     @pytest.fixture(scope='function')
     def market_buy_order():
         return {
    @@ -1019,21 +1028,31 @@ def limit_buy_order_canceled_empty(request):
     
     
     @pytest.fixture
    -def limit_sell_order():
    +def limit_sell_order_open():
         return {
             'id': 'mocked_limit_sell',
             'type': 'limit',
             'side': 'sell',
             'pair': 'mocked',
             'datetime': arrow.utcnow().isoformat(),
    +        'timestamp': arrow.utcnow().timestamp,
             'price': 0.00001173,
             'amount': 90.99181073,
    -        'filled': 90.99181073,
    -        'remaining': 0.0,
    -        'status': 'closed'
    +        'filled': 0.0,
    +        'remaining': 90.99181073,
    +        'status': 'open'
         }
     
     
    +@pytest.fixture
    +def limit_sell_order(limit_sell_order_open):
    +    order = deepcopy(limit_sell_order_open)
    +    order['remaining'] = 0.0
    +    order['filled'] = order['amount']
    +    order['status'] = 'closed'
    +    return order
    +
    +
     @pytest.fixture
     def order_book_l2():
         return MagicMock(return_value={
    diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py
    index 5af6a42cb..9a3e50a9e 100644
    --- a/tests/rpc/test_rpc.py
    +++ b/tests/rpc/test_rpc.py
    @@ -814,11 +814,10 @@ def test_rpc_count(mocker, default_conf, ticker, fee) -> None:
         assert counts["current"] == 1
     
     
    -def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order) -> None:
    +def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) -> None:
         default_conf['forcebuy_enable'] = True
         mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
    -    limit_buy_order['status'] = 'open'
    -    buy_mm = MagicMock(return_value=limit_buy_order)
    +    buy_mm = MagicMock(return_value=limit_buy_order_open)
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
             get_balances=MagicMock(return_value=ticker),
    diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
    index 3f42aa889..40340fcd0 100644
    --- a/tests/test_freqtradebot.py
    +++ b/tests/test_freqtradebot.py
    @@ -170,7 +170,7 @@ def test_get_trade_stake_amount(default_conf, ticker, mocker) -> None:
                             (True, 0.0027, 3, 0.5, [0.001, 0.001, 0.000673]),
                             (True, 0.0022, 3, 1, [0.001, 0.001, 0.0]),
                             ])
    -def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_buy_order,
    +def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_buy_order_open,
                                           amend_last, wallet, max_open, lsamr, expected) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
    @@ -178,7 +178,7 @@ def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_b
             'freqtrade.exchange.Exchange',
             fetch_ticker=ticker,
             get_balance=MagicMock(return_value=default_conf['stake_amount'] * 2),
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             get_fee=fee
         )
         default_conf['dry_run_wallet'] = wallet
    @@ -216,13 +216,13 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None:
                             (0.50, 0.0025),
                             ])
     def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1,
    -                                                 limit_buy_order, fee, mocker) -> None:
    +                                                 limit_buy_order_open, fee, mocker) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
             fetch_ticker=ticker,
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             get_fee=fee
         )
     
    @@ -303,7 +303,6 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf
                 'ask': buy_price * 0.79,
                 'last': buy_price * 0.79
             }),
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
             get_fee=fee,
         )
         #############################################
    @@ -343,7 +342,6 @@ def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee,
                 'ask': buy_price * 0.85,
                 'last': buy_price * 0.85
             }),
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
             get_fee=fee,
         )
         #############################################
    @@ -362,8 +360,7 @@ def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee,
         assert freqtrade.handle_trade(trade) is False
     
     
    -def test_total_open_trades_stakes(mocker, default_conf, ticker,
    -                                  limit_buy_order, fee) -> None:
    +def test_total_open_trades_stakes(mocker, default_conf, ticker, fee) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
         default_conf['stake_amount'] = 0.00098751
    @@ -371,7 +368,6 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker,
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
             fetch_ticker=ticker,
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
             get_fee=fee,
         )
         freqtrade = FreqtradeBot(default_conf)
    @@ -534,7 +530,6 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> Non
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
             fetch_ticker=ticker,
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
             get_fee=fee,
         )
     
    @@ -568,7 +563,6 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order,
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
             fetch_ticker=ticker,
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
             get_fee=fee,
         )
         freqtrade = FreqtradeBot(default_conf)
    @@ -578,11 +572,11 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order,
             freqtrade.create_trade('ETH/BTC')
     
     
    -def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order,
    +def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order_open,
                                          fee, mocker) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
    -    buy_mock = MagicMock(return_value={'id': limit_buy_order['id']})
    +    buy_mock = MagicMock(return_value=limit_buy_order_open)
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
             fetch_ticker=ticker,
    @@ -598,11 +592,11 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order,
         assert rate * amount <= default_conf['stake_amount']
     
     
    -def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order,
    +def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order_open,
                                                  fee, mocker) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
    -    buy_mock = MagicMock(return_value={'id': limit_buy_order['id']})
    +    buy_mock = MagicMock(return_value=limit_buy_order_open)
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
             fetch_ticker=ticker,
    @@ -618,14 +612,14 @@ def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_ord
         assert not freqtrade.create_trade('ETH/BTC')
     
     
    -def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order,
    -                                    fee, markets, mocker) -> None:
    +def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order_open,
    +                                    fee, mocker) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
             fetch_ticker=ticker,
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             get_balance=MagicMock(return_value=default_conf['stake_amount']),
             get_fee=fee,
         )
    @@ -639,14 +633,14 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order,
         assert freqtrade.get_trade_stake_amount('ETH/BTC') == 0
     
     
    -def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order, fee,
    +def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee,
                                            mocker, caplog) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
             fetch_ticker=ticker,
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             get_fee=fee,
         )
     
    @@ -702,7 +696,7 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None:
     
     @pytest.mark.parametrize("max_open", range(0, 5))
     @pytest.mark.parametrize("tradable_balance_ratio,modifier", [(1.0, 1), (0.99, 0.8), (0.5, 0.5)])
    -def test_create_trades_multiple_trades(default_conf, ticker, fee, mocker,
    +def test_create_trades_multiple_trades(default_conf, ticker, fee, mocker, limit_buy_order_open,
                                            max_open, tradable_balance_ratio, modifier) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
    @@ -713,7 +707,7 @@ def test_create_trades_multiple_trades(default_conf, ticker, fee, mocker,
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
             fetch_ticker=ticker,
    -        buy=MagicMock(return_value={'id': "12355555"}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             get_fee=fee,
         )
         freqtrade = FreqtradeBot(default_conf)
    @@ -727,14 +721,14 @@ def test_create_trades_multiple_trades(default_conf, ticker, fee, mocker,
         assert len(trades) == max(int(max_open * modifier), 0)
     
     
    -def test_create_trades_preopen(default_conf, ticker, fee, mocker) -> None:
    +def test_create_trades_preopen(default_conf, ticker, fee, mocker, limit_buy_order_open) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
         default_conf['max_open_trades'] = 4
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
             fetch_ticker=ticker,
    -        buy=MagicMock(return_value={'id': "12355555"}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             get_fee=fee,
         )
         freqtrade = FreqtradeBot(default_conf)
    @@ -754,14 +748,14 @@ def test_create_trades_preopen(default_conf, ticker, fee, mocker) -> None:
         assert len(trades) == 4
     
     
    -def test_process_trade_creation(default_conf, ticker, limit_buy_order,
    +def test_process_trade_creation(default_conf, ticker, limit_buy_order, limit_buy_order_open,
                                     fee, mocker, caplog) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
             fetch_ticker=ticker,
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             fetch_order=MagicMock(return_value=limit_buy_order),
             get_fee=fee,
         )
    @@ -824,14 +818,14 @@ def test_process_operational_exception(default_conf, ticker, mocker) -> None:
         assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]['status']
     
     
    -def test_process_trade_handling(default_conf, ticker, limit_buy_order, fee, mocker) -> None:
    +def test_process_trade_handling(default_conf, ticker, limit_buy_order_open, fee, mocker) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
             fetch_ticker=ticker,
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    -        fetch_order=MagicMock(return_value=limit_buy_order),
    +        buy=MagicMock(return_value=limit_buy_order_open),
    +        fetch_order=MagicMock(return_value=limit_buy_order_open),
             get_fee=fee,
         )
         freqtrade = FreqtradeBot(default_conf)
    @@ -970,7 +964,7 @@ def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid,
         assert not log_has("Using cached buy rate for ETH/BTC.", caplog)
     
     
    -def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None:
    +def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order_open) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
         freqtrade = FreqtradeBot(default_conf)
    @@ -983,7 +977,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None:
             get_buy_rate=buy_rate_mock,
             _get_min_pair_stake_amount=MagicMock(return_value=1)
         )
    -    buy_mm = MagicMock(return_value={'id': limit_buy_order['id']})
    +    buy_mm = MagicMock(return_value=limit_buy_order_open)
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
             fetch_ticker=MagicMock(return_value={
    @@ -1277,7 +1271,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
     
     
     def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee,
    -                                             limit_buy_order, limit_sell_order):
    +                                             limit_buy_order_open, limit_sell_order):
         rpc_mock = patch_RPCManager(mocker)
         patch_exchange(mocker)
         sell_mock = MagicMock(return_value={'id': limit_sell_order['id']})
    @@ -1288,7 +1282,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee,
                 'ask': 0.00001173,
                 'last': 0.00001172
             }),
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             sell=sell_mock,
             get_fee=fee,
             fetch_order=MagicMock(return_value={'status': 'canceled'}),
    @@ -1829,7 +1823,7 @@ def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_orde
         assert not trade.is_open
     
     
    -def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mocker) -> None:
    +def test_handle_trade(default_conf, limit_buy_order, limit_sell_order_open, limit_sell_order, fee, mocker) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
         mocker.patch.multiple(
    @@ -1839,8 +1833,8 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mock
                 'ask': 0.00001173,
                 'last': 0.00001172
             }),
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    -        sell=MagicMock(return_value={'id': limit_sell_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order),
    +        sell=MagicMock(return_value=limit_sell_order_open),
             get_fee=fee,
         )
         freqtrade = FreqtradeBot(default_conf)
    @@ -1869,13 +1863,13 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mock
         assert trade.close_date is not None
     
     
    -def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order, fee, mocker) -> None:
    +def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, fee, mocker) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
             fetch_ticker=ticker,
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             get_fee=fee,
         )
     
    @@ -1920,7 +1914,7 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order, fee,
         assert freqtrade.handle_trade(trades[0]) is True
     
     
    -def test_handle_trade_roi(default_conf, ticker, limit_buy_order,
    +def test_handle_trade_roi(default_conf, ticker, limit_buy_order_open,
                               fee, mocker, caplog) -> None:
         caplog.set_level(logging.DEBUG)
     
    @@ -1928,7 +1922,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order,
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
             fetch_ticker=ticker,
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             get_fee=fee,
         )
     
    @@ -1953,14 +1947,14 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order,
     
     
     def test_handle_trade_use_sell_signal(
    -        default_conf, ticker, limit_buy_order, fee, mocker, caplog) -> None:
    +        default_conf, ticker, limit_buy_order_open, fee, mocker, caplog) -> None:
         # use_sell_signal is True buy default
         caplog.set_level(logging.DEBUG)
         patch_RPCManager(mocker)
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
             fetch_ticker=ticker,
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             get_fee=fee,
         )
     
    @@ -1981,14 +1975,14 @@ def test_handle_trade_use_sell_signal(
                        caplog)
     
     
    -def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order,
    +def test_close_trade(default_conf, ticker, limit_buy_order, limit_buy_order_open, limit_sell_order,
                          fee, mocker) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
             fetch_ticker=ticker,
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             get_fee=fee,
         )
         freqtrade = FreqtradeBot(default_conf)
    @@ -2804,7 +2798,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
     
     
     def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, fee,
    -                                                         limit_buy_order, mocker) -> None:
    +                                                         mocker) -> None:
         default_conf['exchange']['name'] = 'binance'
         rpc_mock = patch_RPCManager(mocker)
         patch_exchange(mocker)
    @@ -2926,7 +2920,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee,
         } == last_msg
     
     
    -def test_sell_profit_only_enable_profit(default_conf, limit_buy_order,
    +def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy_order_open,
                                             fee, mocker) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
    @@ -2937,7 +2931,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order,
                 'ask': 0.00002173,
                 'last': 0.00002172
             }),
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             get_fee=fee,
         )
         default_conf['ask_strategy'] = {
    @@ -2958,7 +2952,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order,
         assert trade.sell_reason == SellType.SELL_SIGNAL.value
     
     
    -def test_sell_profit_only_disable_profit(default_conf, limit_buy_order,
    +def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, limit_buy_order_open,
                                              fee, mocker) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
    @@ -2969,7 +2963,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order,
                 'ask': 0.00002173,
                 'last': 0.00002172
             }),
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             get_fee=fee,
         )
         default_conf['ask_strategy'] = {
    @@ -2989,7 +2983,8 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order,
         assert trade.sell_reason == SellType.SELL_SIGNAL.value
     
     
    -def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker) -> None:
    +def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_order_open,
    +                                      fee, mocker) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
         mocker.patch.multiple(
    @@ -2999,7 +2994,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker
                 'ask': 0.00000173,
                 'last': 0.00000172
             }),
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             get_fee=fee,
         )
         default_conf['ask_strategy'] = {
    @@ -3018,7 +3013,8 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker
         assert freqtrade.handle_trade(trade) is False
     
     
    -def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocker) -> None:
    +def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, limit_buy_order_open,
    +                                       fee, mocker) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
         mocker.patch.multiple(
    @@ -3028,7 +3024,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocke
                 'ask': 0.0000173,
                 'last': 0.0000172
             }),
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             get_fee=fee,
         )
         default_conf['ask_strategy'] = {
    @@ -3050,7 +3046,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocke
         assert trade.sell_reason == SellType.SELL_SIGNAL.value
     
     
    -def test_sell_not_enough_balance(default_conf, limit_buy_order,
    +def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_open,
                                      fee, mocker, caplog) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
    @@ -3061,7 +3057,7 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order,
                 'ask': 0.00002173,
                 'last': 0.00002172
             }),
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             get_fee=fee,
         )
     
    @@ -3169,7 +3165,8 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo
         assert log_has(f"Pair {trade.pair} is currently locked.", caplog)
     
     
    -def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, mocker) -> None:
    +def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_buy_order_open,
    +                                  fee, mocker) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
         mocker.patch.multiple(
    @@ -3179,7 +3176,7 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, mocker) ->
                 'ask': 0.0000173,
                 'last': 0.0000172
             }),
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             get_fee=fee,
         )
         default_conf['ask_strategy'] = {
    @@ -3203,7 +3200,8 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, mocker) ->
         assert trade.sell_reason == SellType.ROI.value
     
     
    -def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker) -> None:
    +def test_trailing_stop_loss(default_conf, limit_buy_order_open, limit_buy_order,
    +                            fee, caplog, mocker) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
         mocker.patch.multiple(
    @@ -3213,7 +3211,7 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker)
                 'ask': 0.00001099,
                 'last': 0.00001099
             }),
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             get_fee=fee,
         )
         default_conf['trailing_stop'] = True
    @@ -3253,7 +3251,7 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker)
         assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value
     
     
    -def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee,
    +def test_trailing_stop_loss_positive(default_conf, limit_buy_order, limit_buy_order_open, fee,
                                          caplog, mocker) -> None:
         buy_price = limit_buy_order['price']
         patch_RPCManager(mocker)
    @@ -3265,7 +3263,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee,
                 'ask': buy_price - 0.000001,
                 'last': buy_price - 0.000001
             }),
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             get_fee=fee,
         )
         default_conf['trailing_stop'] = True
    @@ -3310,7 +3308,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee,
             f"initial stoploss was at 0.000010, trade opened at 0.000011", caplog)
     
     
    -def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee,
    +def test_trailing_stop_loss_offset(default_conf, limit_buy_order, limit_buy_order_open, fee,
                                        caplog, mocker) -> None:
         buy_price = limit_buy_order['price']
         patch_RPCManager(mocker)
    @@ -3322,7 +3320,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee,
                 'ask': buy_price - 0.000001,
                 'last': buy_price - 0.000001
             }),
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             get_fee=fee,
         )
         patch_whitelist(mocker, default_conf)
    @@ -3368,7 +3366,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee,
         assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value
     
     
    -def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee,
    +def test_tsl_only_offset_reached(default_conf, limit_buy_order, limit_buy_order_open, fee,
                                      caplog, mocker) -> None:
         buy_price = limit_buy_order['price']
         # buy_price: 0.00001099
    @@ -3382,7 +3380,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee,
                 'ask': buy_price,
                 'last': buy_price
             }),
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             get_fee=fee,
         )
         patch_whitelist(mocker, default_conf)
    @@ -3431,7 +3429,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee,
         assert trade.stop_loss == 0.0000117705
     
     
    -def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order,
    +def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_buy_order_open,
                                               fee, mocker) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
    @@ -3442,7 +3440,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order,
                 'ask': 0.00000173,
                 'last': 0.00000172
             }),
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             get_fee=fee,
         )
         default_conf['ask_strategy'] = {
    @@ -3784,8 +3782,8 @@ def test_apply_fee_conditional(default_conf, fee, caplog, mocker,
         assert walletmock.call_count == 1
     
     
    -def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, mocker,
    -                                    order_book_l2):
    +def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order_open, limit_buy_order,
    +                                    fee, mocker, order_book_l2):
         default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True
         default_conf['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = 0.1
         patch_RPCManager(mocker)
    @@ -3794,7 +3792,7 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee,
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
             fetch_ticker=ticker,
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             get_fee=fee,
         )
     
    @@ -3909,8 +3907,8 @@ def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2) -> None:
         assert freqtrade._check_depth_of_market_buy('ETH/BTC', conf) is False
     
     
    -def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order,
    -                                 fee, mocker, order_book_l2, caplog) -> None:
    +def test_order_book_ask_strategy(default_conf, limit_buy_order_open, limit_buy_order, fee,
    +                                 limit_sell_order_open, mocker, order_book_l2, caplog) -> None:
         """
         test order book ask strategy
         """
    @@ -3929,8 +3927,8 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order
                 'ask': 0.00001173,
                 'last': 0.00001172
             }),
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    -        sell=MagicMock(return_value={'id': limit_sell_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
    +        sell=MagicMock(return_value=limit_sell_order_open),
             get_fee=fee,
         )
         freqtrade = FreqtradeBot(default_conf)
    @@ -4072,7 +4070,7 @@ def test_startup_trade_reinit(default_conf, edge_conf, mocker):
     
     
     @pytest.mark.usefixtures("init_persistence")
    -def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order, caplog):
    +def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order_open, caplog):
         default_conf['dry_run'] = True
         # Initialize to 2 times stake amount
         default_conf['dry_run_wallet'] = 0.002
    @@ -4082,7 +4080,7 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order,
         mocker.patch.multiple(
             'freqtrade.exchange.Exchange',
             fetch_ticker=ticker,
    -        buy=MagicMock(return_value={'id': limit_buy_order['id']}),
    +        buy=MagicMock(return_value=limit_buy_order_open),
             get_fee=fee,
         )
     
    
    From 4924d8487ee25f84e4d1e4b2dcce62603744f4ed Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 13 Aug 2020 13:39:36 +0200
    Subject: [PATCH 0405/1197] Extract "update order from ccxt" to it's onw
     function
    
    ---
     freqtrade/freqtradebot.py       |  6 ++--
     freqtrade/persistence/models.py | 49 +++++++++++++++++++++------------
     tests/test_freqtradebot.py      |  8 ++++--
     3 files changed, 39 insertions(+), 24 deletions(-)
    
    diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
    index ff282aa77..c1a898c30 100644
    --- a/freqtrade/freqtradebot.py
    +++ b/freqtrade/freqtradebot.py
    @@ -527,7 +527,7 @@ class FreqtradeBot:
             order = self.exchange.buy(pair=pair, ordertype=order_type,
                                       amount=amount, rate=buy_limit_requested,
                                       time_in_force=time_in_force)
    -        order_obj = Order.parse_from_ccxt_object(order, pair)
    +        order_obj = Order.parse_from_ccxt_object(order, 'buy')
             order_id = order['id']
             order_status = order.get('status', None)
     
    @@ -784,7 +784,7 @@ class FreqtradeBot:
                                                         stop_price=stop_price,
                                                         order_types=self.strategy.order_types)
     
    -            order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair)
    +            order_obj = Order.parse_from_ccxt_object(stoploss_order, 'stoploss')
                 trade.orders.append(order_obj)
                 trade.stoploss_order_id = str(stoploss_order['id'])
                 return True
    @@ -1134,7 +1134,7 @@ class FreqtradeBot:
                                        time_in_force=time_in_force
                                        )
     
    -        order_obj = Order.parse_from_ccxt_object(order, trade.pair)
    +        order_obj = Order.parse_from_ccxt_object(order, 'sell')
             trade.orders.append(order_obj)
     
             trade.open_order_id = order['id']
    diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py
    index 3b77438ea..c50fcbe88 100644
    --- a/freqtrade/persistence/models.py
    +++ b/freqtrade/persistence/models.py
    @@ -101,36 +101,49 @@ class Order(_DECL_BASE):
         id = Column(Integer, primary_key=True)
         trade_id = Column(Integer, ForeignKey('trades.id'), index=True)
     
    +    ft_order_side = Column(String, nullable=False)
    +
         order_id = Column(String, nullable=False, index=True)
    -    status = Column(String, nullable=False)
    -    symbol = Column(String, nullable=False)
    -    order_type = Column(String, nullable=False)
    -    side = Column(String, nullable=False)
    -    price = Column(Float, nullable=False)
    -    amount = Column(Float, nullable=False)
    +    status = Column(String, nullable=True)
    +    symbol = Column(String, nullable=True)
    +    order_type = Column(String, nullable=True)
    +    side = Column(String, nullable=True)
    +    price = Column(Float, nullable=True)
    +    amount = Column(Float, nullable=True)
         filled = Column(Float, nullable=True)
         remaining = Column(Float, nullable=True)
         cost = Column(Float, nullable=True)
         order_date = Column(DateTime, nullable=False, default=datetime.utcnow)
         order_filled_date = Column(DateTime, nullable=True)
     
    +    def update_from_ccxt_object(self, order):
    +        """
    +        Update Order from ccxt response
    +        Only updates if fields are available from ccxt -
    +        """
    +        if self.order_id != str(order['id']):
    +            return OperationalException("Order-id's don't match")
    +
    +        self.status = order.get('status', self.status)
    +        self.symbol = order.get('symbol', self.symbol)
    +        self.order_type = order.get('type', self.order_type)
    +        self.side = order.get('side', self.side)
    +        self.price = order.get('price', self.price)
    +        self.amount = order.get('amount', self.amount)
    +        self.filled = order.get('filled', self.filled)
    +        self.remaining = order.get('remaining', self.remaining)
    +        self.cost = order.get('cost', self.cost)
    +        if 'timestamp' in order and order['timestamp'] is not None:
    +            self.order_date = datetime.fromtimestamp(order['timestamp'])
    +
         @staticmethod
    -    def parse_from_ccxt_object(order, pair) -> 'Order':
    +    def parse_from_ccxt_object(order: Dict[str, Any], side: str) -> 'Order':
             """
             Parse an order from a ccxt object and return a new order Object.
             """
    -        o = Order(order_id=str(order['id']))
    +        o = Order(order_id=str(order['id']), ft_order_side=side)
     
    -        o.status = order['status']
    -        o.symbol = order.get('symbol', pair)
    -        o.order_type = order['type']
    -        o.side = order['side']
    -        o.price = order['price']
    -        o.amount = order['amount']
    -        o.filled = order.get('filled')
    -        o.remaining = order.get('remaining')
    -        o.cost = order.get('cost')
    -        o.order_date = datetime.fromtimestamp(order['timestamp'])
    +        o.update_from_ccxt_object(order)
             return o
     
         def __repr__(self):
    diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
    index 40340fcd0..1621be6e5 100644
    --- a/tests/test_freqtradebot.py
    +++ b/tests/test_freqtradebot.py
    @@ -1313,7 +1313,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee,
         assert rpc_mock.call_args_list[1][0][0]['order_type'] == 'market'
     
     
    -def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
    +def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee,
                                                   limit_buy_order, limit_sell_order) -> None:
         # When trailing stoploss is set
         stoploss = MagicMock(return_value={'id': 13434334})
    @@ -1823,7 +1823,8 @@ def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_orde
         assert not trade.is_open
     
     
    -def test_handle_trade(default_conf, limit_buy_order, limit_sell_order_open, limit_sell_order, fee, mocker) -> None:
    +def test_handle_trade(default_conf, limit_buy_order, limit_sell_order_open, limit_sell_order,
    +                      fee, mocker) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
         mocker.patch.multiple(
    @@ -1863,7 +1864,8 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order_open, limi
         assert trade.close_date is not None
     
     
    -def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, fee, mocker) -> None:
    +def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open,
    +                                    fee, mocker) -> None:
         patch_RPCManager(mocker)
         patch_exchange(mocker)
         mocker.patch.multiple(
    
    From 396e781bf43acc00776fde1545bfd91deac2dab2 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 13 Aug 2020 14:13:58 +0200
    Subject: [PATCH 0406/1197] Update orders
    
    ---
     freqtrade/exchange/exchange.py  |  7 +++----
     freqtrade/freqtradebot.py       |  1 +
     freqtrade/persistence/models.py | 25 +++++++++++++++++++------
     3 files changed, 23 insertions(+), 10 deletions(-)
    
    diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
    index 34d57ae4d..533377746 100644
    --- a/freqtrade/exchange/exchange.py
    +++ b/freqtrade/exchange/exchange.py
    @@ -8,7 +8,6 @@ import logging
     from copy import deepcopy
     from datetime import datetime, timezone
     from math import ceil
    -from random import randint
     from typing import Any, Dict, List, Optional, Tuple
     
     import arrow
    @@ -474,11 +473,11 @@ 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)}'
    +        order_id = f'dry_run_{side}_{datetime.now().timestamp()}'
             _amount = self.amount_to_precision(pair, amount)
             dry_order = {
    -            "id": order_id,
    -            'pair': pair,
    +            'id': order_id,
    +            'symbol': pair,
                 'price': rate,
                 'average': rate,
                 'amount': _amount,
    diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
    index c1a898c30..e2d504916 100644
    --- a/freqtrade/freqtradebot.py
    +++ b/freqtrade/freqtradebot.py
    @@ -1260,6 +1260,7 @@ class FreqtradeBot:
             except InvalidOrderException as exception:
                 logger.warning('Unable to fetch order %s: %s', order_id, exception)
                 return False
    +        Order.update_order(order)
             # Try update amount (binance-fix)
             try:
                 new_amount = self.get_real_amount(trade, order, order_amount)
    diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py
    index c50fcbe88..12bbd8adc 100644
    --- a/freqtrade/persistence/models.py
    +++ b/freqtrade/persistence/models.py
    @@ -58,6 +58,10 @@ def init(db_url: str, clean_open_orders: bool = False) -> None:
         # We should use the scoped_session object - not a seperately initialized version
         Trade.session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
         Trade.query = Trade.session.query_property()
    +    # Copy session attributes to order object too
    +    Order.session = Trade.session
    +    Order.query = Order.session.query_property()
    +
         _DECL_BASE.metadata.create_all(engine)
         check_migrate(engine, decl_base=_DECL_BASE)
     
    @@ -103,7 +107,7 @@ class Order(_DECL_BASE):
     
         ft_order_side = Column(String, nullable=False)
     
    -    order_id = Column(String, nullable=False, index=True)
    +    order_id = Column(String, nullable=False, unique=True, index=True)
         status = Column(String, nullable=True)
         symbol = Column(String, nullable=True)
         order_type = Column(String, nullable=True)
    @@ -115,6 +119,12 @@ class Order(_DECL_BASE):
         cost = Column(Float, nullable=True)
         order_date = Column(DateTime, nullable=False, default=datetime.utcnow)
         order_filled_date = Column(DateTime, nullable=True)
    +    order_update_date = Column(DateTime, nullable=True)
    +
    +    def __repr__(self):
    +
    +        return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.trade_id}, '
    +                f'side={self.side}, status={self.status})')
     
         def update_from_ccxt_object(self, order):
             """
    @@ -136,6 +146,14 @@ class Order(_DECL_BASE):
             if 'timestamp' in order and order['timestamp'] is not None:
                 self.order_date = datetime.fromtimestamp(order['timestamp'])
     
    +    @staticmethod
    +    def update_order(order: Dict[str, Any]):
    +        """
    +        """
    +        oobj = Order.query.filter(Order.order_id == order['id']).first()
    +        oobj.update_from_ccxt_object(order)
    +        oobj.order_update_date = datetime.now()
    +
         @staticmethod
         def parse_from_ccxt_object(order: Dict[str, Any], side: str) -> 'Order':
             """
    @@ -146,11 +164,6 @@ class Order(_DECL_BASE):
             o.update_from_ccxt_object(order)
             return o
     
    -    def __repr__(self):
    -
    -        return (f'Order(id={self.id}, trade_id={self.trade_id}, side={self.side}, '
    -                f'status={self.status})')
    -
     
     class Trade(_DECL_BASE):
         """
    
    From 73182bb2ddc41e05067bc317265ed445d5a2196a Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 13 Aug 2020 14:50:57 +0200
    Subject: [PATCH 0407/1197] Update migrations to populate Orders table for open
     orders
    
    ---
     freqtrade/persistence/migrations.py | 201 ++++++++++++++++------------
     freqtrade/persistence/models.py     |  10 +-
     tests/test_persistence.py           |  34 +++--
     3 files changed, 148 insertions(+), 97 deletions(-)
    
    diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py
    index 1bce8fef2..55825436d 100644
    --- a/freqtrade/persistence/migrations.py
    +++ b/freqtrade/persistence/migrations.py
    @@ -5,6 +5,7 @@ from sqlalchemy import inspect
     
     logger = logging.getLogger(__name__)
     
    +
     def get_table_names_for_table(inspector, tabletype):
         return [t for t in inspector.get_table_names() if t.startswith(tabletype)]
     
    @@ -17,7 +18,110 @@ def get_column_def(columns: List, column: str, default: str) -> str:
         return default if not has_column(columns, column) else column
     
     
    -def check_migrate(engine, decl_base) -> None:
    +def get_backup_name(tabs, backup_prefix: str):
    +    table_back_name = backup_prefix
    +    for i, table_back_name in enumerate(tabs):
    +        table_back_name = f'{backup_prefix}{i}'
    +        logger.debug(f'trying {table_back_name}')
    +
    +    return table_back_name
    +
    +
    +def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, cols: List):
    +    fee_open = get_column_def(cols, 'fee_open', 'fee')
    +    fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null')
    +    fee_open_currency = get_column_def(cols, 'fee_open_currency', 'null')
    +    fee_close = get_column_def(cols, 'fee_close', 'fee')
    +    fee_close_cost = get_column_def(cols, 'fee_close_cost', 'null')
    +    fee_close_currency = get_column_def(cols, 'fee_close_currency', 'null')
    +    open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null')
    +    close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null')
    +    stop_loss = get_column_def(cols, 'stop_loss', '0.0')
    +    stop_loss_pct = get_column_def(cols, 'stop_loss_pct', 'null')
    +    initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
    +    initial_stop_loss_pct = get_column_def(cols, 'initial_stop_loss_pct', 'null')
    +    stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null')
    +    stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null')
    +    max_rate = get_column_def(cols, 'max_rate', '0.0')
    +    min_rate = get_column_def(cols, 'min_rate', 'null')
    +    sell_reason = get_column_def(cols, 'sell_reason', 'null')
    +    strategy = get_column_def(cols, 'strategy', 'null')
    +    # If ticker-interval existed use that, else null.
    +    if has_column(cols, 'ticker_interval'):
    +        timeframe = get_column_def(cols, 'timeframe', 'ticker_interval')
    +    else:
    +        timeframe = get_column_def(cols, 'timeframe', 'null')
    +
    +    open_trade_price = get_column_def(cols, 'open_trade_price',
    +                                      f'amount * open_rate * (1 + {fee_open})')
    +    close_profit_abs = get_column_def(
    +        cols, 'close_profit_abs',
    +        f"(amount * close_rate * (1 - {fee_close})) - {open_trade_price}")
    +    sell_order_status = get_column_def(cols, 'sell_order_status', 'null')
    +    amount_requested = get_column_def(cols, 'amount_requested', 'amount')
    +
    +    # Schema migration necessary
    +    engine.execute(f"alter table trades rename to {table_back_name}")
    +    # drop indexes on backup table
    +    for index in inspector.get_indexes(table_back_name):
    +        engine.execute(f"drop index {index['name']}")
    +    # let SQLAlchemy create the schema as required
    +    decl_base.metadata.create_all(engine)
    +
    +    # Copy data back - following the correct schema
    +    engine.execute(f"""insert into trades
    +            (id, exchange, pair, is_open,
    +            fee_open, fee_open_cost, fee_open_currency,
    +            fee_close, fee_close_cost, fee_open_currency, open_rate,
    +            open_rate_requested, close_rate, close_rate_requested, close_profit,
    +            stake_amount, amount, amount_requested, open_date, close_date, open_order_id,
    +            stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
    +            stoploss_order_id, stoploss_last_update,
    +            max_rate, min_rate, sell_reason, sell_order_status, strategy,
    +            timeframe, open_trade_price, close_profit_abs
    +            )
    +        select id, lower(exchange),
    +            case
    +                when instr(pair, '_') != 0 then
    +                substr(pair,    instr(pair, '_') + 1) || '/' ||
    +                substr(pair, 1, instr(pair, '_') - 1)
    +                else pair
    +                end
    +            pair,
    +            is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost,
    +            {fee_open_currency} fee_open_currency, {fee_close} fee_close,
    +            {fee_close_cost} fee_close_cost, {fee_close_currency} fee_close_currency,
    +            open_rate, {open_rate_requested} open_rate_requested, close_rate,
    +            {close_rate_requested} close_rate_requested, close_profit,
    +            stake_amount, amount, {amount_requested}, open_date, close_date, open_order_id,
    +            {stop_loss} stop_loss, {stop_loss_pct} stop_loss_pct,
    +            {initial_stop_loss} initial_stop_loss,
    +            {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,
    +            {sell_order_status} sell_order_status,
    +            {strategy} strategy, {timeframe} timeframe,
    +            {open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs
    +            from {table_back_name}
    +            """)
    +
    +
    +def migrate_open_orders_to_trades(engine):
    +    engine.execute("""
    +    insert into orders (trade_id, order_id, ft_order_side)
    +    select id, open_order_id,
    +        case when close_rate_requested is null then 'buy'
    +        else 'sell' end ft_order_side
    +    from trades
    +    where open_order_id is not null
    +    union all
    +    select id, stoploss_order_id, 'stoploss'
    +    from trades
    +    where stoploss_order_id is not null
    +    """)
    +
    +
    +def check_migrate(engine, decl_base, previous_tables) -> None:
         """
         Checks if migration is necessary and migrates if necessary
         """
    @@ -25,92 +129,21 @@ def check_migrate(engine, decl_base) -> None:
     
         cols = inspector.get_columns('trades')
         tabs = get_table_names_for_table(inspector, 'trades')
    -    table_back_name = 'trades_bak'
    -    for i, table_back_name in enumerate(tabs):
    -        table_back_name = f'trades_bak{i}'
    -        logger.debug(f'trying {table_back_name}')
    +    table_back_name = get_backup_name(tabs, 'trades_bak')
     
         # Check for latest column
         if not has_column(cols, 'amount_requested'):
    -        logger.info(f'Running database migration - backup available as {table_back_name}')
    -
    -        fee_open = get_column_def(cols, 'fee_open', 'fee')
    -        fee_open_cost = get_column_def(cols, 'fee_open_cost', 'null')
    -        fee_open_currency = get_column_def(cols, 'fee_open_currency', 'null')
    -        fee_close = get_column_def(cols, 'fee_close', 'fee')
    -        fee_close_cost = get_column_def(cols, 'fee_close_cost', 'null')
    -        fee_close_currency = get_column_def(cols, 'fee_close_currency', 'null')
    -        open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null')
    -        close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null')
    -        stop_loss = get_column_def(cols, 'stop_loss', '0.0')
    -        stop_loss_pct = get_column_def(cols, 'stop_loss_pct', 'null')
    -        initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
    -        initial_stop_loss_pct = get_column_def(cols, 'initial_stop_loss_pct', 'null')
    -        stoploss_order_id = get_column_def(cols, 'stoploss_order_id', 'null')
    -        stoploss_last_update = get_column_def(cols, 'stoploss_last_update', 'null')
    -        max_rate = get_column_def(cols, 'max_rate', '0.0')
    -        min_rate = get_column_def(cols, 'min_rate', 'null')
    -        sell_reason = get_column_def(cols, 'sell_reason', 'null')
    -        strategy = get_column_def(cols, 'strategy', 'null')
    -        # If ticker-interval existed use that, else null.
    -        if has_column(cols, 'ticker_interval'):
    -            timeframe = get_column_def(cols, 'timeframe', 'ticker_interval')
    -        else:
    -            timeframe = get_column_def(cols, 'timeframe', 'null')
    -
    -        open_trade_price = get_column_def(cols, 'open_trade_price',
    -                                          f'amount * open_rate * (1 + {fee_open})')
    -        close_profit_abs = get_column_def(
    -            cols, 'close_profit_abs',
    -            f"(amount * close_rate * (1 - {fee_close})) - {open_trade_price}")
    -        sell_order_status = get_column_def(cols, 'sell_order_status', 'null')
    -        amount_requested = get_column_def(cols, 'amount_requested', 'amount')
    -
    -        # Schema migration necessary
    -        engine.execute(f"alter table trades rename to {table_back_name}")
    -        # drop indexes on backup table
    -        for index in inspector.get_indexes(table_back_name):
    -            engine.execute(f"drop index {index['name']}")
    -        # let SQLAlchemy create the schema as required
    -        decl_base.metadata.create_all(engine)
    -
    -        # Copy data back - following the correct schema
    -        engine.execute(f"""insert into trades
    -                (id, exchange, pair, is_open,
    -                fee_open, fee_open_cost, fee_open_currency,
    -                fee_close, fee_close_cost, fee_open_currency, open_rate,
    -                open_rate_requested, close_rate, close_rate_requested, close_profit,
    -                stake_amount, amount, amount_requested, open_date, close_date, open_order_id,
    -                stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
    -                stoploss_order_id, stoploss_last_update,
    -                max_rate, min_rate, sell_reason, sell_order_status, strategy,
    -                timeframe, open_trade_price, close_profit_abs
    -                )
    -            select id, lower(exchange),
    -                case
    -                    when instr(pair, '_') != 0 then
    -                    substr(pair,    instr(pair, '_') + 1) || '/' ||
    -                    substr(pair, 1, instr(pair, '_') - 1)
    -                    else pair
    -                    end
    -                pair,
    -                is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost,
    -                {fee_open_currency} fee_open_currency, {fee_close} fee_close,
    -                {fee_close_cost} fee_close_cost, {fee_close_currency} fee_close_currency,
    -                open_rate, {open_rate_requested} open_rate_requested, close_rate,
    -                {close_rate_requested} close_rate_requested, close_profit,
    -                stake_amount, amount, {amount_requested}, open_date, close_date, open_order_id,
    -                {stop_loss} stop_loss, {stop_loss_pct} stop_loss_pct,
    -                {initial_stop_loss} initial_stop_loss,
    -                {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,
    -                {sell_order_status} sell_order_status,
    -                {strategy} strategy, {timeframe} timeframe,
    -                {open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs
    -                from {table_back_name}
    -             """)
    -
    +        logger.info(f'Running database migration for trades - backup: {table_back_name}')
    +        migrate_trades_table(decl_base, inspector, engine, table_back_name, cols)
             # Reread columns - the above recreated the table!
             inspector = inspect(engine)
             cols = inspector.get_columns('trades')
    +
    +    if 'orders' not in previous_tables:
    +        logger.info('Moving open orders to Orders table.')
    +        migrate_open_orders_to_trades(engine)
    +    else:
    +        logger.info(f'Running database migration for orders - backup: {table_back_name}')
    +        pass
    +        # Empty for now - as there is only one iteration of the orders table so far.
    +        # table_back_name = get_backup_name(tabs, 'orders_bak')
    diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py
    index 12bbd8adc..b0bde02fe 100644
    --- a/freqtrade/persistence/models.py
    +++ b/freqtrade/persistence/models.py
    @@ -7,8 +7,8 @@ from decimal import Decimal
     from typing import Any, Dict, List, Optional
     
     import arrow
    -from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String, ForeignKey,
    -                        create_engine, desc, func)
    +from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer,
    +                        String, create_engine, desc, func, inspect)
     from sqlalchemy.exc import NoSuchModuleError
     from sqlalchemy.ext.declarative import declarative_base
     from sqlalchemy.orm import Query, relationship
    @@ -61,9 +61,9 @@ def init(db_url: str, clean_open_orders: bool = False) -> None:
         # Copy session attributes to order object too
         Order.session = Trade.session
         Order.query = Order.session.query_property()
    -
    +    previous_tables = inspect(engine).get_table_names()
         _DECL_BASE.metadata.create_all(engine)
    -    check_migrate(engine, decl_base=_DECL_BASE)
    +    check_migrate(engine, decl_base=_DECL_BASE, previous_tables=previous_tables)
     
         # Clean dry_run DB if the db is not in-memory
         if clean_open_orders and db_url != 'sqlite://':
    @@ -117,7 +117,7 @@ class Order(_DECL_BASE):
         filled = Column(Float, nullable=True)
         remaining = Column(Float, nullable=True)
         cost = Column(Float, nullable=True)
    -    order_date = Column(DateTime, nullable=False, default=datetime.utcnow)
    +    order_date = Column(DateTime, nullable=True, default=datetime.utcnow)
         order_filled_date = Column(DateTime, nullable=True)
         order_update_date = Column(DateTime, nullable=True)
     
    diff --git a/tests/test_persistence.py b/tests/test_persistence.py
    index dbb62e636..c812c496f 100644
    --- a/tests/test_persistence.py
    +++ b/tests/test_persistence.py
    @@ -8,7 +8,7 @@ from sqlalchemy import create_engine
     
     from freqtrade import constants
     from freqtrade.exceptions import OperationalException
    -from freqtrade.persistence import Trade, clean_dry_run_db, init
    +from freqtrade.persistence import Trade, Order, clean_dry_run_db, init
     from tests.conftest import log_has, create_mock_trades
     
     
    @@ -421,9 +421,9 @@ def test_migrate_old(mocker, default_conf, fee):
                                     PRIMARY KEY (id),
                                     CHECK (is_open IN (0, 1))
                                     );"""
    -    insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee,
    +    insert_table_old = """INSERT INTO trades (exchange, pair, is_open, open_order_id, fee,
                               open_rate, stake_amount, amount, open_date)
    -                          VALUES ('BITTREX', 'BTC_ETC', 1, {fee},
    +                          VALUES ('BITTREX', 'BTC_ETC', 1, '123123', {fee},
                               0.00258580, {stake}, {amount},
                               '2017-11-28 12:44:24.000000')
                               """.format(fee=fee.return_value,
    @@ -481,6 +481,12 @@ def test_migrate_old(mocker, default_conf, fee):
         assert pytest.approx(trade.close_profit_abs) == trade.calc_profit()
         assert trade.sell_order_status is None
     
    +    # Should've created one order
    +    assert len(Order.query.all()) == 1
    +    order = Order.query.first()
    +    assert order.order_id == '123123'
    +    assert order.ft_order_side == 'buy'
    +
     
     def test_migrate_new(mocker, default_conf, fee, caplog):
         """
    @@ -509,16 +515,19 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
                                     sell_reason VARCHAR,
                                     strategy VARCHAR,
                                     ticker_interval INTEGER,
    +                                stoploss_order_id VARCHAR,
                                     PRIMARY KEY (id),
                                     CHECK (is_open IN (0, 1))
                                     );"""
         insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee,
                               open_rate, stake_amount, amount, open_date,
    -                          stop_loss, initial_stop_loss, max_rate, ticker_interval)
    +                          stop_loss, initial_stop_loss, max_rate, ticker_interval,
    +                          open_order_id, stoploss_order_id)
                               VALUES ('binance', 'ETC/BTC', 1, {fee},
                               0.00258580, {stake}, {amount},
                               '2019-11-28 12:44:24.000000',
    -                          0.0, 0.0, 0.0, '5m')
    +                          0.0, 0.0, 0.0, '5m',
    +                          'buy_order', 'stop_order_id222')
                               """.format(fee=fee.return_value,
                                          stake=default_conf.get("stake_amount"),
                                          amount=amount
    @@ -558,14 +567,23 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
         assert trade.sell_reason is None
         assert trade.strategy is None
         assert trade.timeframe == '5m'
    -    assert trade.stoploss_order_id is None
    +    assert trade.stoploss_order_id == 'stop_order_id222'
         assert trade.stoploss_last_update is None
         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 log_has("Running database migration for trades - backup: trades_bak2", caplog)
         assert trade.open_trade_price == trade._calc_open_trade_price()
         assert trade.close_profit_abs is None
     
    +    assert log_has("Moving open orders to Orders table.", caplog)
    +    orders = Order.query.all()
    +    assert len(orders) == 2
    +    assert orders[0].order_id == 'buy_order'
    +    assert orders[0].ft_order_side == 'buy'
    +
    +    assert orders[1].order_id == 'stop_order_id222'
    +    assert orders[1].ft_order_side == 'stoploss'
    +
     
     def test_migrate_mid_state(mocker, default_conf, fee, caplog):
         """
    @@ -626,7 +644,7 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog):
         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)
    +    assert log_has("Running database migration for trades - backup: trades_bak0", caplog)
     
     
     def test_adjust_stop_loss(fee):
    
    From 0af9e913d4a3bd0c66b387e9d79fdd949d9da1d2 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 13 Aug 2020 15:39:29 +0200
    Subject: [PATCH 0408/1197] Timestamps are in ms
    
    ---
     freqtrade/exchange/exchange.py  |  2 +-
     freqtrade/freqtradebot.py       |  5 ++++-
     freqtrade/persistence/models.py | 12 ++++++++----
     tests/exchange/test_exchange.py |  2 +-
     tests/test_freqtradebot.py      |  1 +
     5 files changed, 15 insertions(+), 7 deletions(-)
    
    diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
    index 533377746..bcb511010 100644
    --- a/freqtrade/exchange/exchange.py
    +++ b/freqtrade/exchange/exchange.py
    @@ -486,7 +486,7 @@ class Exchange:
                 'side': side,
                 'remaining': _amount,
                 'datetime': arrow.utcnow().isoformat(),
    -            'timestamp': arrow.utcnow().timestamp,
    +            'timestamp': int(arrow.utcnow().timestamp * 1000),
                 'status': "closed" if ordertype == "market" else "open",
                 'fee': None,
                 'info': {}
    diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
    index e2d504916..18d39e471 100644
    --- a/freqtrade/freqtradebot.py
    +++ b/freqtrade/freqtradebot.py
    @@ -817,6 +817,9 @@ class FreqtradeBot:
             except InvalidOrderException as exception:
                 logger.warning('Unable to fetch stoploss order: %s', exception)
     
    +        if stoploss_order:
    +            trade.update_order(stoploss_order)
    +
             # We check if stoploss order is fulfilled
             if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'):
                 trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
    @@ -1260,7 +1263,7 @@ class FreqtradeBot:
             except InvalidOrderException as exception:
                 logger.warning('Unable to fetch order %s: %s', order_id, exception)
                 return False
    -        Order.update_order(order)
    +        trade.update_order(order)
             # Try update amount (binance-fix)
             try:
                 new_amount = self.get_real_amount(trade, order, order_amount)
    diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py
    index b0bde02fe..5f85cdb4c 100644
    --- a/freqtrade/persistence/models.py
    +++ b/freqtrade/persistence/models.py
    @@ -107,7 +107,7 @@ class Order(_DECL_BASE):
     
         ft_order_side = Column(String, nullable=False)
     
    -    order_id = Column(String, nullable=False, unique=True, index=True)
    +    order_id = Column(String, nullable=False, index=True)
         status = Column(String, nullable=True)
         symbol = Column(String, nullable=True)
         order_type = Column(String, nullable=True)
    @@ -144,13 +144,14 @@ class Order(_DECL_BASE):
             self.remaining = order.get('remaining', self.remaining)
             self.cost = order.get('cost', self.cost)
             if 'timestamp' in order and order['timestamp'] is not None:
    -            self.order_date = datetime.fromtimestamp(order['timestamp'])
    +            self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000)
     
         @staticmethod
    -    def update_order(order: Dict[str, Any]):
    +    def update_orders(orders: List['Order'], order: Dict[str, Any]):
             """
             """
    -        oobj = Order.query.filter(Order.order_id == order['id']).first()
    +        filtered_orders = [o for o in orders if o.order_id == order['id']]
    +        oobj = filtered_orders[0] if filtered_orders else None
             oobj.update_from_ccxt_object(order)
             oobj.order_update_date = datetime.now()
     
    @@ -417,6 +418,9 @@ class Trade(_DECL_BASE):
             else:
                 return False
     
    +    def update_order(self, order: Dict) -> None:
    +        Order.update_orders(self.orders, order)
    +
         def _calc_open_trade_price(self) -> float:
             """
             Calculate the open_rate including open_fee.
    diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py
    index 350c2d3cb..d0e303f5f 100644
    --- a/tests/exchange/test_exchange.py
    +++ b/tests/exchange/test_exchange.py
    @@ -807,7 +807,7 @@ def test_dry_run_order(default_conf, mocker, side, exchange_name):
         assert f'dry_run_{side}_' in order["id"]
         assert order["side"] == side
         assert order["type"] == "limit"
    -    assert order["pair"] == "ETH/BTC"
    +    assert order["symbol"] == "ETH/BTC"
     
     
     @pytest.mark.parametrize("side", [
    diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
    index 1621be6e5..7257c73b9 100644
    --- a/tests/test_freqtradebot.py
    +++ b/tests/test_freqtradebot.py
    @@ -1194,6 +1194,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
         assert trade
     
         stoploss_order_hit = MagicMock(return_value={
    +        'id': 100,
             'status': 'closed',
             'type': 'stop_loss_limit',
             'price': 3,
    
    From ebd755e36a842f63a62044ebbd68d6779720bdd0 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 13 Aug 2020 15:54:36 +0200
    Subject: [PATCH 0409/1197] Improve order handling
    
    ---
     freqtrade/persistence/models.py |  9 ++++++---
     tests/test_freqtradebot.py      | 11 +++++++++--
     2 files changed, 15 insertions(+), 5 deletions(-)
    
    diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py
    index 5f85cdb4c..d930fbeb7 100644
    --- a/freqtrade/persistence/models.py
    +++ b/freqtrade/persistence/models.py
    @@ -151,9 +151,12 @@ class Order(_DECL_BASE):
             """
             """
             filtered_orders = [o for o in orders if o.order_id == order['id']]
    -        oobj = filtered_orders[0] if filtered_orders else None
    -        oobj.update_from_ccxt_object(order)
    -        oobj.order_update_date = datetime.now()
    +        if filtered_orders:
    +            oobj = filtered_orders[0]
    +            oobj.update_from_ccxt_object(order)
    +            oobj.order_update_date = datetime.now()
    +        else:
    +            logger.warning(f"Did not find order for {order['id']}.")
     
         @staticmethod
         def parse_from_ccxt_object(order: Dict[str, Any], side: str) -> 'Order':
    diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
    index 7257c73b9..1f7984a43 100644
    --- a/tests/test_freqtradebot.py
    +++ b/tests/test_freqtradebot.py
    @@ -1,6 +1,7 @@
     # pragma pylint: disable=missing-docstring, C0103
     # pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments
     
    +from freqtrade.persistence.models import Order
     import logging
     import time
     from copy import deepcopy
    @@ -1252,7 +1253,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
             buy=MagicMock(return_value={'id': limit_buy_order['id']}),
             sell=MagicMock(return_value={'id': limit_sell_order['id']}),
             get_fee=fee,
    -        fetch_stoploss_order=MagicMock(return_value={'status': 'canceled'}),
    +        fetch_stoploss_order=MagicMock(return_value={'status': 'canceled', 'id': 100}),
             stoploss=MagicMock(side_effect=ExchangeError()),
         )
         freqtrade = FreqtradeBot(default_conf)
    @@ -1794,7 +1795,8 @@ def test_update_trade_state_orderexception(mocker, default_conf, caplog) -> None
         assert log_has(f'Unable to fetch order {trade.open_order_id}: ', caplog)
     
     
    -def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_order, mocker):
    +def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_order_open,
    +                                 limit_sell_order, mocker):
         mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
         # fetch_order should not be called!!
         mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError))
    @@ -1817,11 +1819,16 @@ def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_orde
             open_order_id="123456",
             is_open=True,
         )
    +    order = Order.parse_from_ccxt_object(limit_sell_order_open, 'sell')
    +    trade.orders.append(order)
    +    assert order.status == 'open'
         freqtrade.update_trade_state(trade, limit_sell_order)
         assert trade.amount == limit_sell_order['amount']
         # Wallet needs to be updated after closing a limit-sell order to reenable buying
         assert wallet_mock.call_count == 1
         assert not trade.is_open
    +    # Order is updated by update_trade_state
    +    assert order.status == 'closed'
     
     
     def test_handle_trade(default_conf, limit_buy_order, limit_sell_order_open, limit_sell_order,
    
    From 4434a54d598b1a1ec3ea1fa2122d90834dfa91cf Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 13 Aug 2020 16:14:28 +0200
    Subject: [PATCH 0410/1197] Add unique key to order-Model
    
    ---
     freqtrade/freqtradebot.py       | 6 +++---
     freqtrade/persistence/models.py | 9 +++++++--
     2 files changed, 10 insertions(+), 5 deletions(-)
    
    diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
    index 18d39e471..038bf1f4f 100644
    --- a/freqtrade/freqtradebot.py
    +++ b/freqtrade/freqtradebot.py
    @@ -527,7 +527,7 @@ class FreqtradeBot:
             order = self.exchange.buy(pair=pair, ordertype=order_type,
                                       amount=amount, rate=buy_limit_requested,
                                       time_in_force=time_in_force)
    -        order_obj = Order.parse_from_ccxt_object(order, 'buy')
    +        order_obj = Order.parse_from_ccxt_object(order, pair, 'buy')
             order_id = order['id']
             order_status = order.get('status', None)
     
    @@ -784,7 +784,7 @@ class FreqtradeBot:
                                                         stop_price=stop_price,
                                                         order_types=self.strategy.order_types)
     
    -            order_obj = Order.parse_from_ccxt_object(stoploss_order, 'stoploss')
    +            order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss')
                 trade.orders.append(order_obj)
                 trade.stoploss_order_id = str(stoploss_order['id'])
                 return True
    @@ -1137,7 +1137,7 @@ class FreqtradeBot:
                                        time_in_force=time_in_force
                                        )
     
    -        order_obj = Order.parse_from_ccxt_object(order, 'sell')
    +        order_obj = Order.parse_from_ccxt_object(order, trade.pair, 'sell')
             trade.orders.append(order_obj)
     
             trade.open_order_id = order['id']
    diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py
    index d930fbeb7..14e2b0da8 100644
    --- a/freqtrade/persistence/models.py
    +++ b/freqtrade/persistence/models.py
    @@ -15,6 +15,7 @@ from sqlalchemy.orm import Query, relationship
     from sqlalchemy.orm.scoping import scoped_session
     from sqlalchemy.orm.session import sessionmaker
     from sqlalchemy.pool import StaticPool
    +from sqlalchemy.sql.schema import UniqueConstraint
     
     from freqtrade.exceptions import OperationalException
     from freqtrade.misc import safe_value_fallback
    @@ -101,11 +102,15 @@ class Order(_DECL_BASE):
         Mirrors CCXT Order structure
         """
         __tablename__ = 'orders'
    +    # Uniqueness should be ensured over pair, order_id
    +    # its likely that order_id is unique per Pair on some exchanges.
    +    __table_args__ = (UniqueConstraint('ft_pair', 'order_id'),)
     
         id = Column(Integer, primary_key=True)
         trade_id = Column(Integer, ForeignKey('trades.id'), index=True)
     
         ft_order_side = Column(String, nullable=False)
    +    ft_pair = Column(String, nullable=False)
     
         order_id = Column(String, nullable=False, index=True)
         status = Column(String, nullable=True)
    @@ -159,11 +164,11 @@ class Order(_DECL_BASE):
                 logger.warning(f"Did not find order for {order['id']}.")
     
         @staticmethod
    -    def parse_from_ccxt_object(order: Dict[str, Any], side: str) -> 'Order':
    +    def parse_from_ccxt_object(order: Dict[str, Any], pair: str, side: str) -> 'Order':
             """
             Parse an order from a ccxt object and return a new order Object.
             """
    -        o = Order(order_id=str(order['id']), ft_order_side=side)
    +        o = Order(order_id=str(order['id']), ft_order_side=side, ft_pair=pair)
     
             o.update_from_ccxt_object(order)
             return o
    
    From 2ca6547bafb88026b45e3075fef33858b879f1a8 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 13 Aug 2020 16:14:41 +0200
    Subject: [PATCH 0411/1197] Update tests to have unique ordernumbers
    
    ---
     tests/test_freqtradebot.py | 16 ++++++++++++++--
     1 file changed, 14 insertions(+), 2 deletions(-)
    
    diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
    index 1f7984a43..17d1a7ea3 100644
    --- a/tests/test_freqtradebot.py
    +++ b/tests/test_freqtradebot.py
    @@ -192,6 +192,7 @@ def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_b
         for i in range(0, max_open):
     
             if expected[i] is not None:
    +            limit_buy_order_open['id'] = str(i)
                 result = freqtrade.get_trade_stake_amount('ETH/BTC')
                 assert pytest.approx(result) == expected[i]
                 freqtrade.execute_buy('ETH/BTC', result)
    @@ -740,6 +741,8 @@ def test_create_trades_preopen(default_conf, ticker, fee, mocker, limit_buy_orde
         freqtrade.execute_buy('NEO/BTC', default_conf['stake_amount'])
     
         assert len(Trade.get_open_trades()) == 2
    +    # Change order_id for new orders
    +    limit_buy_order_open['id'] = '123444'
     
         # Create 2 new trades using create_trades
         assert freqtrade.create_trade('ETH/BTC')
    @@ -997,6 +1000,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
         assert freqtrade.strategy.confirm_trade_entry.call_count == 1
         buy_rate_mock.reset_mock()
     
    +    limit_buy_order_open['id'] = '22'
         freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True)
         assert freqtrade.execute_buy(pair, stake_amount)
         assert buy_rate_mock.call_count == 1
    @@ -1012,9 +1016,10 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
         trade = Trade.query.first()
         assert trade
         assert trade.is_open is True
    -    assert trade.open_order_id == limit_buy_order['id']
    +    assert trade.open_order_id == '22'
     
         # Test calling with price
    +    limit_buy_order_open['id'] = '33'
         fix_price = 0.06
         assert freqtrade.execute_buy(pair, stake_amount, fix_price)
         # Make sure get_buy_rate wasn't called again
    @@ -1030,6 +1035,8 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
         limit_buy_order['status'] = 'closed'
         limit_buy_order['price'] = 10
         limit_buy_order['cost'] = 100
    +    limit_buy_order['id'] = '444'
    +
         mocker.patch('freqtrade.exchange.Exchange.buy', MagicMock(return_value=limit_buy_order))
         assert freqtrade.execute_buy(pair, stake_amount)
         trade = Trade.query.all()[2]
    @@ -1045,6 +1052,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
         limit_buy_order['remaining'] = 10.00
         limit_buy_order['price'] = 0.5
         limit_buy_order['cost'] = 40.495905365
    +    limit_buy_order['id'] = '555'
         mocker.patch('freqtrade.exchange.Exchange.buy', MagicMock(return_value=limit_buy_order))
         assert freqtrade.execute_buy(pair, stake_amount)
         trade = Trade.query.all()[3]
    @@ -1060,6 +1068,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
         limit_buy_order['remaining'] = 90.99181073
         limit_buy_order['price'] = 0.5
         limit_buy_order['cost'] = 0.0
    +    limit_buy_order['id'] = '66'
         mocker.patch('freqtrade.exchange.Exchange.buy', MagicMock(return_value=limit_buy_order))
         assert not freqtrade.execute_buy(pair, stake_amount)
     
    @@ -1087,9 +1096,11 @@ def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) -
         freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=ValueError)
         assert freqtrade.execute_buy(pair, stake_amount)
     
    +    limit_buy_order['id'] = '222'
         freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=Exception)
         assert freqtrade.execute_buy(pair, stake_amount)
     
    +    limit_buy_order['id'] = '2223'
         freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True)
         assert freqtrade.execute_buy(pair, stake_amount)
     
    @@ -1315,6 +1326,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee,
         assert rpc_mock.call_args_list[1][0][0]['order_type'] == 'market'
     
     
    +@pytest.mark.usefixtures("init_persistence")
     def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee,
                                                   limit_buy_order, limit_sell_order) -> None:
         # When trailing stoploss is set
    @@ -1819,7 +1831,7 @@ def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_orde
             open_order_id="123456",
             is_open=True,
         )
    -    order = Order.parse_from_ccxt_object(limit_sell_order_open, 'sell')
    +    order = Order.parse_from_ccxt_object(limit_sell_order_open, 'LTC/ETH', 'sell')
         trade.orders.append(order)
         assert order.status == 'open'
         freqtrade.update_trade_state(trade, limit_sell_order)
    
    From 1a305ea8b0f20376faf0f7f650fa22d4ca505be4 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 13 Aug 2020 16:18:03 +0200
    Subject: [PATCH 0412/1197] Fix migrations to use unique key
    
    ---
     freqtrade/freqtradebot.py           | 2 ++
     freqtrade/persistence/migrations.py | 6 +++---
     tests/test_freqtradebot.py          | 2 +-
     3 files changed, 6 insertions(+), 4 deletions(-)
    
    diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
    index 038bf1f4f..dac3b7ce1 100644
    --- a/freqtrade/freqtradebot.py
    +++ b/freqtrade/freqtradebot.py
    @@ -1263,7 +1263,9 @@ class FreqtradeBot:
             except InvalidOrderException as exception:
                 logger.warning('Unable to fetch order %s: %s', order_id, exception)
                 return False
    +
             trade.update_order(order)
    +
             # Try update amount (binance-fix)
             try:
                 new_amount = self.get_real_amount(trade, order, order_amount)
    diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py
    index 55825436d..15e5f56bf 100644
    --- a/freqtrade/persistence/migrations.py
    +++ b/freqtrade/persistence/migrations.py
    @@ -108,14 +108,14 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
     
     def migrate_open_orders_to_trades(engine):
         engine.execute("""
    -    insert into orders (trade_id, order_id, ft_order_side)
    -    select id, open_order_id,
    +    insert into orders (trade_id, ft_pair, order_id, ft_order_side)
    +    select id trade_id, pair ft_pair, open_order_id,
             case when close_rate_requested is null then 'buy'
             else 'sell' end ft_order_side
         from trades
         where open_order_id is not null
         union all
    -    select id, stoploss_order_id, 'stoploss'
    +    select id trade_id, pair ft_pair, stoploss_order_id order_id, 'stoploss' ft_order_side
         from trades
         where stoploss_order_id is not null
         """)
    diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
    index 17d1a7ea3..a907ce5c2 100644
    --- a/tests/test_freqtradebot.py
    +++ b/tests/test_freqtradebot.py
    @@ -1396,7 +1396,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee,
         }))
     
         cancel_order_mock = MagicMock()
    -    stoploss_order_mock = MagicMock()
    +    stoploss_order_mock = MagicMock(return_value={'id': 13434334})
         mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock)
         mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock)
     
    
    From da2a515d0be4597e5282dd58a3eedf28db281bf6 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 13 Aug 2020 16:55:43 +0200
    Subject: [PATCH 0413/1197] Add delete cascade to alchemy model
    
    ---
     freqtrade/persistence/models.py | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py
    index 14e2b0da8..4efdacef8 100644
    --- a/freqtrade/persistence/models.py
    +++ b/freqtrade/persistence/models.py
    @@ -183,7 +183,7 @@ class Trade(_DECL_BASE):
     
         id = Column(Integer, primary_key=True)
     
    -    orders = relationship("Order", order_by="Order.id")
    +    orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan")
     
         exchange = Column(String, nullable=False)
         pair = Column(String, nullable=False, index=True)
    
    From c4d7aff5c33b8e069ff9d6f910c262f3c1a26ad4 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 13 Aug 2020 17:17:52 +0200
    Subject: [PATCH 0414/1197] Order should have a "is_open" flag
    
    ---
     freqtrade/persistence/migrations.py |  9 +++++----
     freqtrade/persistence/models.py     | 12 ++++++++----
     2 files changed, 13 insertions(+), 8 deletions(-)
    
    diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py
    index 15e5f56bf..96b3a0db6 100644
    --- a/freqtrade/persistence/migrations.py
    +++ b/freqtrade/persistence/migrations.py
    @@ -108,14 +108,15 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
     
     def migrate_open_orders_to_trades(engine):
         engine.execute("""
    -    insert into orders (trade_id, ft_pair, order_id, ft_order_side)
    -    select id trade_id, pair ft_pair, open_order_id,
    +    insert into orders (ft_trade_id, ft_pair, order_id, ft_order_side, ft_is_open)
    +    select id ft_trade_id, pair ft_pair, open_order_id,
             case when close_rate_requested is null then 'buy'
    -        else 'sell' end ft_order_side
    +        else 'sell' end ft_order_side, true ft_is_open
         from trades
         where open_order_id is not null
         union all
    -    select id trade_id, pair ft_pair, stoploss_order_id order_id, 'stoploss' ft_order_side
    +    select id ft_trade_id, pair ft_pair, stoploss_order_id order_id,
    +        'stoploss' ft_order_side, true ft_is_open
         from trades
         where stoploss_order_id is not null
         """)
    diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py
    index 4efdacef8..eb4d42ef5 100644
    --- a/freqtrade/persistence/models.py
    +++ b/freqtrade/persistence/models.py
    @@ -104,13 +104,14 @@ class Order(_DECL_BASE):
         __tablename__ = 'orders'
         # Uniqueness should be ensured over pair, order_id
         # its likely that order_id is unique per Pair on some exchanges.
    -    __table_args__ = (UniqueConstraint('ft_pair', 'order_id'),)
    +    __table_args__ = (UniqueConstraint('ft_pair', 'order_id', name="_order_pair_order_id"),)
     
         id = Column(Integer, primary_key=True)
    -    trade_id = Column(Integer, ForeignKey('trades.id'), index=True)
    +    ft_trade_id = Column(Integer, ForeignKey('trades.id'), index=True)
     
         ft_order_side = Column(String, nullable=False)
         ft_pair = Column(String, nullable=False)
    +    ft_is_open = Column(Boolean, nullable=False, default=True, index=True)
     
         order_id = Column(String, nullable=False, index=True)
         status = Column(String, nullable=True)
    @@ -128,7 +129,7 @@ class Order(_DECL_BASE):
     
         def __repr__(self):
     
    -        return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.trade_id}, '
    +        return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, '
                     f'side={self.side}, status={self.status})')
     
         def update_from_ccxt_object(self, order):
    @@ -151,6 +152,10 @@ class Order(_DECL_BASE):
             if 'timestamp' in order and order['timestamp'] is not None:
                 self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000)
     
    +        if self.status in ('closed', 'canceled', 'cancelled'):
    +            self.ft_is_open = False
    +        self.order_update_date = datetime.now()
    +
         @staticmethod
         def update_orders(orders: List['Order'], order: Dict[str, Any]):
             """
    @@ -159,7 +164,6 @@ class Order(_DECL_BASE):
             if filtered_orders:
                 oobj = filtered_orders[0]
                 oobj.update_from_ccxt_object(order)
    -            oobj.order_update_date = datetime.now()
             else:
                 logger.warning(f"Did not find order for {order['id']}.")
     
    
    From 95efc0d688556d80b869ae0c4533d9870414b58b Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 13 Aug 2020 17:18:56 +0200
    Subject: [PATCH 0415/1197] Add open_order_updater
    
    ---
     freqtrade/freqtradebot.py       | 16 ++++++++++++++++
     freqtrade/persistence/models.py |  7 +++++++
     2 files changed, 23 insertions(+)
    
    diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
    index dac3b7ce1..db0c852fe 100644
    --- a/freqtrade/freqtradebot.py
    +++ b/freqtrade/freqtradebot.py
    @@ -227,6 +227,22 @@ class FreqtradeBot:
             open_trades = len(Trade.get_open_trades())
             return max(0, self.config['max_open_trades'] - open_trades)
     
    +    def update_open_orders(self):
    +        """
    +        Updates open orders based on order list kept in the database
    +        """
    +        orders = Order.get_open_orders()
    +        logger.info(f"Updating {len(orders)} open orders.")
    +        for order in orders:
    +            try:
    +                if order.ft_order_side == 'stoposs':
    +                    fo = self.exchange.fetch_stoploss_order(order.order_id, order.ft_pair)
    +                else:
    +                    fo = self.exchange.fetch_order(order.order_id, order.ft_pair)
    +                order.update_from_ccxt_object(fo)
    +            except ExchangeError:
    +                logger.warning(f"Error updating {order.order_id}")
    +
     #
     # BUY / enter positions / open trades logic and methods
     #
    diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py
    index eb4d42ef5..b6a96b8f3 100644
    --- a/freqtrade/persistence/models.py
    +++ b/freqtrade/persistence/models.py
    @@ -159,6 +159,7 @@ class Order(_DECL_BASE):
         @staticmethod
         def update_orders(orders: List['Order'], order: Dict[str, Any]):
             """
    +        Get all non-closed orders - useful when trying to batch-update orders
             """
             filtered_orders = [o for o in orders if o.order_id == order['id']]
             if filtered_orders:
    @@ -177,6 +178,12 @@ class Order(_DECL_BASE):
             o.update_from_ccxt_object(order)
             return o
     
    +    @staticmethod
    +    def get_open_orders():
    +        """
    +        """
    +        return Order.query.filter(Order.ft_is_open.is_(True)).all()
    +
     
     class Trade(_DECL_BASE):
         """
    
    From 8458a380b8d8ffc0eb5cda78d317c9de17d247ed Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Thu, 13 Aug 2020 19:37:41 +0200
    Subject: [PATCH 0416/1197] Improve order catchup
    
    ---
     freqtrade/freqtradebot.py           |  8 ++++++--
     freqtrade/persistence/migrations.py | 25 ++++++++++++-------------
     freqtrade/persistence/models.py     |  4 +++-
     3 files changed, 21 insertions(+), 16 deletions(-)
    
    diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
    index db0c852fe..490169790 100644
    --- a/freqtrade/freqtradebot.py
    +++ b/freqtrade/freqtradebot.py
    @@ -134,6 +134,8 @@ class FreqtradeBot:
                 # Adjust stoploss if it was changed
                 Trade.stoploss_reinitialization(self.strategy.stoploss)
     
    +        self.update_open_orders()
    +
         def process(self) -> None:
             """
             Queries the persistence layer for open trades and handles them,
    @@ -235,11 +237,13 @@ class FreqtradeBot:
             logger.info(f"Updating {len(orders)} open orders.")
             for order in orders:
                 try:
    -                if order.ft_order_side == 'stoposs':
    +                if order.ft_order_side == 'stoploss':
                         fo = self.exchange.fetch_stoploss_order(order.order_id, order.ft_pair)
                     else:
                         fo = self.exchange.fetch_order(order.order_id, order.ft_pair)
    -                order.update_from_ccxt_object(fo)
    +
    +                self.update_trade_state(order.trade, fo, sl_order=order.ft_order_side == 'stoploss')
    +
                 except ExchangeError:
                     logger.warning(f"Error updating {order.order_id}")
     
    diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py
    index 96b3a0db6..b6fa8f9ae 100644
    --- a/freqtrade/persistence/migrations.py
    +++ b/freqtrade/persistence/migrations.py
    @@ -108,18 +108,18 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col
     
     def migrate_open_orders_to_trades(engine):
         engine.execute("""
    -    insert into orders (ft_trade_id, ft_pair, order_id, ft_order_side, ft_is_open)
    -    select id ft_trade_id, pair ft_pair, open_order_id,
    -        case when close_rate_requested is null then 'buy'
    -        else 'sell' end ft_order_side, true ft_is_open
    -    from trades
    -    where open_order_id is not null
    -    union all
    -    select id ft_trade_id, pair ft_pair, stoploss_order_id order_id,
    -        'stoploss' ft_order_side, true ft_is_open
    -    from trades
    -    where stoploss_order_id is not null
    -    """)
    +        insert into orders (ft_trade_id, ft_pair, order_id, ft_order_side, ft_is_open)
    +        select id ft_trade_id, pair ft_pair, open_order_id,
    +            case when close_rate_requested is null then 'buy'
    +            else 'sell' end ft_order_side, true ft_is_open
    +        from trades
    +        where open_order_id is not null
    +        union all
    +        select id ft_trade_id, pair ft_pair, stoploss_order_id order_id,
    +            'stoploss' ft_order_side, true ft_is_open
    +        from trades
    +        where stoploss_order_id is not null
    +        """)
     
     
     def check_migrate(engine, decl_base, previous_tables) -> None:
    @@ -144,7 +144,6 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
             logger.info('Moving open orders to Orders table.')
             migrate_open_orders_to_trades(engine)
         else:
    -        logger.info(f'Running database migration for orders - backup: {table_back_name}')
             pass
             # Empty for now - as there is only one iteration of the orders table so far.
             # table_back_name = get_backup_name(tabs, 'orders_bak')
    diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py
    index b6a96b8f3..3f918670a 100644
    --- a/freqtrade/persistence/models.py
    +++ b/freqtrade/persistence/models.py
    @@ -109,6 +109,8 @@ class Order(_DECL_BASE):
         id = Column(Integer, primary_key=True)
         ft_trade_id = Column(Integer, ForeignKey('trades.id'), index=True)
     
    +    trade = relationship("Trade", back_populates="orders")
    +
         ft_order_side = Column(String, nullable=False)
         ft_pair = Column(String, nullable=False)
         ft_is_open = Column(Boolean, nullable=False, default=True, index=True)
    @@ -179,7 +181,7 @@ class Order(_DECL_BASE):
             return o
     
         @staticmethod
    -    def get_open_orders():
    +    def get_open_orders() -> List['Order']:
             """
             """
             return Order.query.filter(Order.ft_is_open.is_(True)).all()
    
    From a6fc922f2830481c1d4b18b40a79acf9204c2b85 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Fri, 14 Aug 2020 09:56:48 +0200
    Subject: [PATCH 0417/1197] Introduce insufficientFunds Exception
    
    ---
     docs/developer.md       | 2 ++
     freqtrade/exceptions.py | 7 +++++++
     2 files changed, 9 insertions(+)
    
    diff --git a/docs/developer.md b/docs/developer.md
    index f09ae2c76..b79930061 100644
    --- a/docs/developer.md
    +++ b/docs/developer.md
    @@ -110,6 +110,8 @@ Below is an outline of exception inheritance hierarchy:
     |       +---+ InvalidOrderException
     |           |
     |           +---+ RetryableOrderError
    +|           |
    +|           +---+ InsufficientFundsError
     |
     +---+ StrategyError
     ```
    diff --git a/freqtrade/exceptions.py b/freqtrade/exceptions.py
    index e2bc969a9..caf970606 100644
    --- a/freqtrade/exceptions.py
    +++ b/freqtrade/exceptions.py
    @@ -51,6 +51,13 @@ class RetryableOrderError(InvalidOrderException):
         """
     
     
    +class InsufficientFundsError(InvalidOrderException):
    +    """
    +    This error is used when there are not enough funds available on the exchange
    +    to create an order.
    +    """
    +
    +
     class TemporaryError(ExchangeError):
         """
         Temporary network or exchange related error.
    
    From 22af82631a912b0e07c0b35138f2395d9db71d09 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Fri, 14 Aug 2020 09:57:13 +0200
    Subject: [PATCH 0418/1197] Introduce InsufficientFundsError exception
    
    ---
     freqtrade/exchange/binance.py  | 4 ++--
     freqtrade/exchange/exchange.py | 3 ++-
     freqtrade/exchange/ftx.py      | 3 ++-
     freqtrade/exchange/kraken.py   | 4 ++--
     4 files changed, 8 insertions(+), 6 deletions(-)
    
    diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py
    index f2fe1d6ad..d7da34482 100644
    --- a/freqtrade/exchange/binance.py
    +++ b/freqtrade/exchange/binance.py
    @@ -4,7 +4,7 @@ from typing import Dict
     
     import ccxt
     
    -from freqtrade.exceptions import (DDosProtection, ExchangeError,
    +from freqtrade.exceptions import (DDosProtection, InsufficientFundsError,
                                       InvalidOrderException, OperationalException,
                                       TemporaryError)
     from freqtrade.exchange import Exchange
    @@ -80,7 +80,7 @@ class Binance(Exchange):
                             'stop price: %s. limit: %s', pair, stop_price, rate)
                 return order
             except ccxt.InsufficientFunds as e:
    -            raise ExchangeError(
    +            raise InsufficientFundsError(
                     f'Insufficient funds to create {ordertype} sell order on market {pair}. '
                     f'Tried to sell amount {amount} at rate {rate}. '
                     f'Message: {e}') from e
    diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
    index bcb511010..578d753a4 100644
    --- a/freqtrade/exchange/exchange.py
    +++ b/freqtrade/exchange/exchange.py
    @@ -20,6 +20,7 @@ from pandas import DataFrame
     from freqtrade.constants import ListPairsWithTimeframes
     from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list
     from freqtrade.exceptions import (DDosProtection, ExchangeError,
    +                                  InsufficientFundsError,
                                       InvalidOrderException, OperationalException,
                                       RetryableOrderError, TemporaryError)
     from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async
    @@ -525,7 +526,7 @@ class Exchange:
                                               amount, rate_for_order, params)
     
             except ccxt.InsufficientFunds as e:
    -            raise ExchangeError(
    +            raise InsufficientFundsError(
                     f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
                     f'Tried to {side} amount {amount} at rate {rate}.'
                     f'Message: {e}') from e
    diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py
    index 01e8267ad..9c506c88e 100644
    --- a/freqtrade/exchange/ftx.py
    +++ b/freqtrade/exchange/ftx.py
    @@ -5,6 +5,7 @@ from typing import Dict
     import ccxt
     
     from freqtrade.exceptions import (DDosProtection, ExchangeError,
    +                                  InsufficientFundsError,
                                       InvalidOrderException, OperationalException,
                                       TemporaryError)
     from freqtrade.exchange import Exchange
    @@ -61,7 +62,7 @@ class Ftx(Exchange):
                             'stop price: %s.', pair, stop_price)
                 return order
             except ccxt.InsufficientFunds as e:
    -            raise ExchangeError(
    +            raise InsufficientFundsError(
                     f'Insufficient funds to create {ordertype} sell order on market {pair}. '
                     f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
                     f'Message: {e}') from e
    diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py
    index 7b9d0f09b..17e181527 100644
    --- a/freqtrade/exchange/kraken.py
    +++ b/freqtrade/exchange/kraken.py
    @@ -4,7 +4,7 @@ from typing import Dict
     
     import ccxt
     
    -from freqtrade.exceptions import (DDosProtection, ExchangeError,
    +from freqtrade.exceptions import (DDosProtection, InsufficientFundsError,
                                       InvalidOrderException, OperationalException,
                                       TemporaryError)
     from freqtrade.exchange import Exchange
    @@ -88,7 +88,7 @@ class Kraken(Exchange):
                             'stop price: %s.', pair, stop_price)
                 return order
             except ccxt.InsufficientFunds as e:
    -            raise ExchangeError(
    +            raise InsufficientFundsError(
                     f'Insufficient funds to create {ordertype} sell order on market {pair}. '
                     f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
                     f'Message: {e}') from e
    
    From 552aaf79459fbc66ad02e4c517bfd5d69ff2d090 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Fri, 14 Aug 2020 10:59:55 +0200
    Subject: [PATCH 0419/1197] add refind order logic
    
    ---
     freqtrade/freqtradebot.py | 55 ++++++++++++++++++++++++++++++++++-----
     1 file changed, 48 insertions(+), 7 deletions(-)
    
    diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
    index 490169790..91adbc598 100644
    --- a/freqtrade/freqtradebot.py
    +++ b/freqtrade/freqtradebot.py
    @@ -17,7 +17,7 @@ from freqtrade.configuration import validate_config_consistency
     from freqtrade.data.converter import order_book_to_dataframe
     from freqtrade.data.dataprovider import DataProvider
     from freqtrade.edge import Edge
    -from freqtrade.exceptions import (DependencyException, ExchangeError,
    +from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
                                       InvalidOrderException, PricingError)
     from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date
     from freqtrade.misc import safe_value_fallback, safe_value_fallback2
    @@ -247,6 +247,36 @@ class FreqtradeBot:
                 except ExchangeError:
                     logger.warning(f"Error updating {order.order_id}")
     
    +    def refind_lost_order(self, trade):
    +        """
    +        Try refinding a lost trade.
    +        Only used when InsufficientFunds appears on sell orders (stoploss or sell).
    +        Tries to walk the stored orders and sell them off eventually.
    +        """
    +        logger.info(f"Trying to refind lost order for {trade}")
    +        for order in trade.orders:
    +            logger.info(f"Trying to refind {order}")
    +            fo = None
    +            try:
    +                if order.ft_order_side == 'stoploss':
    +                    fo = self.exchange.fetch_stoploss_order(order.order_id, order.ft_pair)
    +                    if fo and fo['status'] == 'open':
    +                        # Assume this as the open stoploss order
    +                        trade.stoploss_order_id = order.order_id
    +                elif order.ft_order_side == 'sell':
    +                    fo = self.exchange.fetch_order(order.order_id, order.ft_pair)
    +                    if fo and fo['status'] == 'open':
    +                        # Assume this as the open order
    +                        trade.open_order_id = order.order_id
    +                else:
    +                    # No action for buy orders ...
    +                    continue
    +                if fo:
    +                    self.update_trade_state(trade, fo, sl_order=order.ft_order_side == 'stoploss')
    +
    +            except ExchangeError:
    +                logger.warning(f"Error updating {order.order_id}")
    +
     #
     # BUY / enter positions / open trades logic and methods
     #
    @@ -808,6 +838,11 @@ class FreqtradeBot:
                 trade.orders.append(order_obj)
                 trade.stoploss_order_id = str(stoploss_order['id'])
                 return True
    +        except InsufficientFundsError as e:
    +            logger.warning(f"Unable to place stoploss order {e}.")
    +            # Try refinding stoploss order
    +            self.refind_lost_order(trade)
    +
             except InvalidOrderException as e:
                 trade.stoploss_order_id = None
                 logger.error(f'Unable to place a stoploss order on exchange. {e}')
    @@ -1150,12 +1185,18 @@ class FreqtradeBot:
                 logger.info(f"User requested abortion of selling {trade.pair}")
                 return False
     
    -        # Execute sell and update trade record
    -        order = self.exchange.sell(pair=trade.pair,
    -                                   ordertype=order_type,
    -                                   amount=amount, rate=limit,
    -                                   time_in_force=time_in_force
    -                                   )
    +        try:
    +            # Execute sell and update trade record
    +            order = self.exchange.sell(pair=trade.pair,
    +                                       ordertype=order_type,
    +                                       amount=amount, rate=limit,
    +                                       time_in_force=time_in_force
    +                                       )
    +        except InsufficientFundsError as e:
    +            logger.warning(f"Unable to place order {e}.")
    +            # Try refinding "lost" orders
    +            self.refind_lost_order(trade)
    +            return False
     
             order_obj = Order.parse_from_ccxt_object(order, trade.pair, 'sell')
             trade.orders.append(order_obj)
    
    From b25267ad3d78d1bf815d69ac0bc68a468e31ece4 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Fri, 14 Aug 2020 11:13:55 +0200
    Subject: [PATCH 0420/1197] Build docker image for db_keep_orders branch
    
    ---
     .github/workflows/ci.yml | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
    index 239576c61..54f0e4444 100644
    --- a/.github/workflows/ci.yml
    +++ b/.github/workflows/ci.yml
    @@ -6,6 +6,7 @@ on:
           - master
           - develop
           - github_actions_tests
    +      - db_keep_orders
         tags:
         release:
           types: [published]
    
    From cfa352ecf2c275ce46defeac53460ef26d5b5c23 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Fri, 14 Aug 2020 11:25:20 +0200
    Subject: [PATCH 0421/1197] Disable refind_lost_order for now
    
    ---
     freqtrade/freqtradebot.py | 9 ++++++---
     1 file changed, 6 insertions(+), 3 deletions(-)
    
    diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
    index 91adbc598..0dac4d888 100644
    --- a/freqtrade/freqtradebot.py
    +++ b/freqtrade/freqtradebot.py
    @@ -841,7 +841,8 @@ class FreqtradeBot:
             except InsufficientFundsError as e:
                 logger.warning(f"Unable to place stoploss order {e}.")
                 # Try refinding stoploss order
    -            self.refind_lost_order(trade)
    +            # TODO: Currently disabled to allow testing without this first
    +            # self.refind_lost_order(trade)
     
             except InvalidOrderException as e:
                 trade.stoploss_order_id = None
    @@ -933,7 +934,8 @@ class FreqtradeBot:
                     logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s}) '
                                 'in order to add another one ...', order['id'])
                     try:
    -                    self.exchange.cancel_stoploss_order(order['id'], trade.pair)
    +                    co = self.exchange.cancel_stoploss_order(order['id'], trade.pair)
    +                    trade.update_order(co)
                     except InvalidOrderException:
                         logger.exception(f"Could not cancel stoploss order {order['id']} "
                                          f"for pair {trade.pair}")
    @@ -1195,7 +1197,8 @@ class FreqtradeBot:
             except InsufficientFundsError as e:
                 logger.warning(f"Unable to place order {e}.")
                 # Try refinding "lost" orders
    -            self.refind_lost_order(trade)
    +            # TODO: Currently disabled to allow testing without this first
    +            # self.refind_lost_order(trade)
                 return False
     
             order_obj = Order.parse_from_ccxt_object(order, trade.pair, 'sell')
    
    From d8fdd32b548222e3852adc401e14e1019258ee1c Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Fri, 14 Aug 2020 11:25:40 +0200
    Subject: [PATCH 0422/1197] FIx migrations
    
    ---
     freqtrade/persistence/migrations.py | 4 ++--
     1 file changed, 2 insertions(+), 2 deletions(-)
    
    diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py
    index b6fa8f9ae..5089953b2 100644
    --- a/freqtrade/persistence/migrations.py
    +++ b/freqtrade/persistence/migrations.py
    @@ -111,12 +111,12 @@ def migrate_open_orders_to_trades(engine):
             insert into orders (ft_trade_id, ft_pair, order_id, ft_order_side, ft_is_open)
             select id ft_trade_id, pair ft_pair, open_order_id,
                 case when close_rate_requested is null then 'buy'
    -            else 'sell' end ft_order_side, true ft_is_open
    +            else 'sell' end ft_order_side, 1 ft_is_open
             from trades
             where open_order_id is not null
             union all
             select id ft_trade_id, pair ft_pair, stoploss_order_id order_id,
    -            'stoploss' ft_order_side, true ft_is_open
    +            'stoploss' ft_order_side, 1 ft_is_open
             from trades
             where stoploss_order_id is not null
             """)
    
    From 06125df10c5b9f041ad4d113e2fa673385895bf0 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Fri, 14 Aug 2020 11:31:02 +0200
    Subject: [PATCH 0423/1197] Remove unused import
    
    ---
     freqtrade/exchange/ftx.py | 3 +--
     1 file changed, 1 insertion(+), 2 deletions(-)
    
    diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py
    index 9c506c88e..27051a945 100644
    --- a/freqtrade/exchange/ftx.py
    +++ b/freqtrade/exchange/ftx.py
    @@ -4,8 +4,7 @@ from typing import Dict
     
     import ccxt
     
    -from freqtrade.exceptions import (DDosProtection, ExchangeError,
    -                                  InsufficientFundsError,
    +from freqtrade.exceptions import (DDosProtection, InsufficientFundsError,
                                       InvalidOrderException, OperationalException,
                                       TemporaryError)
     from freqtrade.exchange import Exchange
    
    From 48944fd4cb441604703a338336a2786bcbca7a04 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Fri, 14 Aug 2020 14:41:46 +0200
    Subject: [PATCH 0424/1197] Logging with queueHandler
    
    ---
     freqtrade/loggers.py | 38 +++++++++++++++++++++++++++++---------
     freqtrade/main.py    | 13 +++++++------
     2 files changed, 36 insertions(+), 15 deletions(-)
    
    diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py
    index aa08ee8a7..eed480164 100644
    --- a/freqtrade/loggers.py
    +++ b/freqtrade/loggers.py
    @@ -1,14 +1,15 @@
     import logging
    +import queue
     import sys
    -
     from logging import Formatter
    -from logging.handlers import RotatingFileHandler, SysLogHandler
    +from logging.handlers import RotatingFileHandler, SysLogHandler, QueueHandler, QueueListener
     from typing import Any, Dict, List
     
     from freqtrade.exceptions import OperationalException
     
    -
     logger = logging.getLogger(__name__)
    +log_queue = queue.Queue(-1)
    +LOGFORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
     
     
     def _set_loggers(verbosity: int = 0, api_verbosity: str = 'info') -> None:
    @@ -33,6 +34,25 @@ def _set_loggers(verbosity: int = 0, api_verbosity: str = 'info') -> None:
         )
     
     
    +def setup_logging_pre() -> None:
    +    """
    +    Setup early logging.
    +    This uses a queuehandler, which delays logging.
    +    # TODO: How does QueueHandler work if no listenerhandler is attached??
    +    """
    +    logging.root.setLevel(logging.INFO)
    +    fmt = logging.Formatter(LOGFORMAT)
    +
    +    queue_handler = QueueHandler(log_queue)
    +    queue_handler.setFormatter(fmt)
    +    logger.root.addHandler(queue_handler)
    +
    +    # Add streamhandler here to capture Errors before QueueListener is started
    +    sth = logging.StreamHandler(sys.stderr)
    +    sth.setFormatter(fmt)
    +    logger.root.addHandler(sth)
    +
    +
     def setup_logging(config: Dict[str, Any]) -> None:
         """
         Process -v/--verbose, --logfile options
    @@ -41,7 +61,7 @@ def setup_logging(config: Dict[str, Any]) -> None:
         verbosity = config['verbosity']
     
         # Log to stderr
    -    log_handlers: List[logging.Handler] = [logging.StreamHandler(sys.stderr)]
    +    log_handlers: List[logging.Handler] = []
     
         logfile = config.get('logfile')
         if logfile:
    @@ -76,10 +96,10 @@ def setup_logging(config: Dict[str, Any]) -> None:
                                                         maxBytes=1024 * 1024,  # 1Mb
                                                         backupCount=10))
     
    -    logging.basicConfig(
    -        level=logging.INFO if verbosity < 1 else logging.DEBUG,
    -        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    -        handlers=log_handlers
    -    )
    +    listener = QueueListener(log_queue, *log_handlers)
    +
    +    # logging.root.setFormatter(logging.Formatter(LOGFORMAT))
    +    logging.root.setLevel(logging.INFO if verbosity < 1 else logging.DEBUG)
    +    listener.start()
         _set_loggers(verbosity, config.get('api_server', {}).get('verbosity', 'info'))
         logger.info('Verbosity set to %s', verbosity)
    diff --git a/freqtrade/main.py b/freqtrade/main.py
    index 08bdc5e32..eeb975953 100755
    --- a/freqtrade/main.py
    +++ b/freqtrade/main.py
    @@ -3,18 +3,18 @@
     Main Freqtrade bot script.
     Read the documentation to know what cli arguments you need.
     """
    -
    -from freqtrade.exceptions import FreqtradeException, OperationalException
    +# flake8: noqa E402
    +import logging
     import sys
    +from typing import Any, List
    +
     # check min. python version
     if sys.version_info < (3, 6):
         sys.exit("Freqtrade requires Python version >= 3.6")
     
    -# flake8: noqa E402
    -import logging
    -from typing import Any, List
    -
     from freqtrade.commands import Arguments
    +from freqtrade.exceptions import FreqtradeException, OperationalException
    +from freqtrade.loggers import setup_logging_pre
     
     
     logger = logging.getLogger('freqtrade')
    @@ -28,6 +28,7 @@ def main(sysargv: List[str] = None) -> None:
     
         return_code: Any = 1
         try:
    +        setup_logging_pre()
             arguments = Arguments(sysargv)
             args = arguments.get_parsed_arg()
     
    
    From b989ba0f82c610b06ca8dbb2db4033a8e9b567a6 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Fri, 14 Aug 2020 14:53:21 +0200
    Subject: [PATCH 0425/1197] Simplify setup of handlers
    
    ---
     freqtrade/loggers.py | 40 ++++++++++++++--------------------------
     1 file changed, 14 insertions(+), 26 deletions(-)
    
    diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py
    index eed480164..f5f383da7 100644
    --- a/freqtrade/loggers.py
    +++ b/freqtrade/loggers.py
    @@ -1,9 +1,8 @@
     import logging
     import queue
    -import sys
     from logging import Formatter
    -from logging.handlers import RotatingFileHandler, SysLogHandler, QueueHandler, QueueListener
    -from typing import Any, Dict, List
    +from logging.handlers import RotatingFileHandler, SysLogHandler
    +from typing import Any, Dict
     
     from freqtrade.exceptions import OperationalException
     
    @@ -40,17 +39,10 @@ def setup_logging_pre() -> None:
         This uses a queuehandler, which delays logging.
         # TODO: How does QueueHandler work if no listenerhandler is attached??
         """
    -    logging.root.setLevel(logging.INFO)
    -    fmt = logging.Formatter(LOGFORMAT)
    -
    -    queue_handler = QueueHandler(log_queue)
    -    queue_handler.setFormatter(fmt)
    -    logger.root.addHandler(queue_handler)
    -
    -    # Add streamhandler here to capture Errors before QueueListener is started
    -    sth = logging.StreamHandler(sys.stderr)
    -    sth.setFormatter(fmt)
    -    logger.root.addHandler(sth)
    +    logging.basicConfig(
    +        level=logging.INFO,
    +        format=LOGFORMAT,
    +    )
     
     
     def setup_logging(config: Dict[str, Any]) -> None:
    @@ -60,9 +52,6 @@ def setup_logging(config: Dict[str, Any]) -> None:
         # Log level
         verbosity = config['verbosity']
     
    -    # Log to stderr
    -    log_handlers: List[logging.Handler] = []
    -
         logfile = config.get('logfile')
         if logfile:
             s = logfile.split(':')
    @@ -78,7 +67,7 @@ def setup_logging(config: Dict[str, Any]) -> None:
                 # to perform reduction of repeating messages if this is set in the
                 # syslog config. The messages should be equal for this.
                 handler.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s'))
    -            log_handlers.append(handler)
    +            logging.root.addHandler(handler)
             elif s[0] == 'journald':
                 try:
                     from systemd.journal import JournaldLogHandler
    @@ -90,16 +79,15 @@ def setup_logging(config: Dict[str, Any]) -> None:
                 # to perform reduction of repeating messages if this is set in the
                 # syslog config. The messages should be equal for this.
                 handler.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s'))
    -            log_handlers.append(handler)
    +            logging.root.addHandler(handler)
             else:
    -            log_handlers.append(RotatingFileHandler(logfile,
    -                                                    maxBytes=1024 * 1024,  # 1Mb
    -                                                    backupCount=10))
    +            handler = RotatingFileHandler(logfile,
    +                                          maxBytes=1024 * 1024,  # 1Mb
    +                                          backupCount=10)
    +            handler.setFormatter(Formatter(LOGFORMAT))
    +            logging.root.addHandler(handler)
     
    -    listener = QueueListener(log_queue, *log_handlers)
    -
    -    # logging.root.setFormatter(logging.Formatter(LOGFORMAT))
         logging.root.setLevel(logging.INFO if verbosity < 1 else logging.DEBUG)
    -    listener.start()
         _set_loggers(verbosity, config.get('api_server', {}).get('verbosity', 'info'))
    +
         logger.info('Verbosity set to %s', verbosity)
    
    From 5f79caa307bcb24e88bd14bba7c691b1ddc238b4 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Fri, 14 Aug 2020 15:44:36 +0200
    Subject: [PATCH 0426/1197] Implement /logs endpoints in telegram and restAPI
    
    ---
     freqtrade/loggers.py           |  7 ++++++-
     freqtrade/rpc/api_server.py    | 13 +++++++++++++
     freqtrade/rpc/rpc.py           | 22 ++++++++++++++++++++--
     freqtrade/rpc/telegram.py      | 30 ++++++++++++++++++++++++++++++
     tests/rpc/test_rpc_telegram.py |  2 +-
     5 files changed, 70 insertions(+), 4 deletions(-)
    
    diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py
    index f5f383da7..0b1337b2c 100644
    --- a/freqtrade/loggers.py
    +++ b/freqtrade/loggers.py
    @@ -1,7 +1,7 @@
     import logging
     import queue
     from logging import Formatter
    -from logging.handlers import RotatingFileHandler, SysLogHandler
    +from logging.handlers import RotatingFileHandler, SysLogHandler, BufferingHandler
     from typing import Any, Dict
     
     from freqtrade.exceptions import OperationalException
    @@ -10,6 +10,10 @@ logger = logging.getLogger(__name__)
     log_queue = queue.Queue(-1)
     LOGFORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
     
    +# Initialize bufferhandler - will be used for /log endpoints
    +bufferHandler = BufferingHandler(1000)
    +bufferHandler.setFormatter(Formatter(LOGFORMAT))
    +
     
     def _set_loggers(verbosity: int = 0, api_verbosity: str = 'info') -> None:
         """
    @@ -51,6 +55,7 @@ def setup_logging(config: Dict[str, Any]) -> None:
         """
         # Log level
         verbosity = config['verbosity']
    +    logging.root.addHandler(bufferHandler)
     
         logfile = config.get('logfile')
         if logfile:
    diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py
    index 06926ac35..cb2236878 100644
    --- a/freqtrade/rpc/api_server.py
    +++ b/freqtrade/rpc/api_server.py
    @@ -186,6 +186,7 @@ class ApiServer(RPC):
             self.app.add_url_rule(f'{BASE_URI}/count', 'count', view_func=self._count, methods=['GET'])
             self.app.add_url_rule(f'{BASE_URI}/daily', 'daily', view_func=self._daily, methods=['GET'])
             self.app.add_url_rule(f'{BASE_URI}/edge', 'edge', view_func=self._edge, methods=['GET'])
    +        self.app.add_url_rule(f'{BASE_URI}/logs', 'log', view_func=self._get_logs, methods=['GET'])
             self.app.add_url_rule(f'{BASE_URI}/profit', 'profit',
                                   view_func=self._profit, methods=['GET'])
             self.app.add_url_rule(f'{BASE_URI}/performance', 'performance',
    @@ -348,6 +349,18 @@ class ApiServer(RPC):
     
             return self.rest_dump(stats)
     
    +    @require_login
    +    @rpc_catch_errors
    +    def _get_logs(self):
    +        """
    +        Returns latest logs
    +         get:
    +          param:
    +            limit: Only get a certain number of records
    +        """
    +        limit = int(request.args.get('limit', 0)) or None
    +        return self.rest_dump(self._rpc_get_logs(limit))
    +
         @require_login
         @rpc_catch_errors
         def _edge(self):
    diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
    index f4e20c16f..5da428a9c 100644
    --- a/freqtrade/rpc/rpc.py
    +++ b/freqtrade/rpc/rpc.py
    @@ -11,9 +11,9 @@ from typing import Any, Dict, List, Optional, Tuple, Union
     import arrow
     from numpy import NAN, mean
     
    -from freqtrade.exceptions import (ExchangeError,
    -                                  PricingError)
    +from freqtrade.exceptions import ExchangeError, PricingError
     from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
    +from freqtrade.loggers import bufferHandler
     from freqtrade.misc import shorten_date
     from freqtrade.persistence import Trade
     from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
    @@ -633,6 +633,24 @@ class RPC:
                    }
             return res
     
    +    def _rpc_get_logs(self, limit: Optional[int]) -> Dict[str, List]:
    +        """Returns the last X logs"""
    +        if limit:
    +            buffer = bufferHandler.buffer[-limit:]
    +        else:
    +            buffer = bufferHandler.buffer
    +        records = [[r.asctime, r.created, r.name, r.levelname, r.message] for r in buffer]
    +
    +        return {'log_count': len(records), 'logs': records}
    +
    +    def _rpc_get_logs_as_string(self, limit: Optional[int]) -> Dict[str, List]:
    +        """Returns the last X logs"""
    +        if limit:
    +            buffer = bufferHandler.buffer[-limit:]
    +        else:
    +            buffer = bufferHandler.buffer
    +        return [bufferHandler.format(r) for r in buffer]
    +
         def _rpc_edge(self) -> List[Dict[str, Any]]:
             """ Returns information related to Edge """
             if not self._freqtrade.edge:
    diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
    index f1d3cde21..da93604d1 100644
    --- a/freqtrade/rpc/telegram.py
    +++ b/freqtrade/rpc/telegram.py
    @@ -103,6 +103,7 @@ class Telegram(RPC):
                 CommandHandler('stopbuy', self._stopbuy),
                 CommandHandler('whitelist', self._whitelist),
                 CommandHandler('blacklist', self._blacklist),
    +            CommandHandler('logs', self._logs),
                 CommandHandler('edge', self._edge),
                 CommandHandler('help', self._help),
                 CommandHandler('version', self._version),
    @@ -637,6 +638,34 @@ class Telegram(RPC):
             except RPCException as e:
                 self._send_msg(str(e))
     
    +    @authorized_only
    +    def _logs(self, update: Update, context: CallbackContext) -> None:
    +        """
    +        Handler for /logs
    +        Shows the latest logs
    +        """
    +        try:
    +            try:
    +                limit = int(context.args[0])
    +            except (TypeError, ValueError, IndexError):
    +                limit = 10
    +            logs = self._rpc_get_logs_as_string(limit)
    +            msg = ''
    +            message_container = "
    {}
    " + for logrec in logs: + if len(msg + logrec) + 10 >= MAX_TELEGRAM_MESSAGE_LENGTH: + # Send message immediately if it would become too long + self._send_msg(message_container.format(msg), parse_mode=ParseMode.HTML) + msg = logrec + '\n' + else: + # Append message to messages to send + msg += logrec + '\n' + + if msg: + self._send_msg(message_container.format(msg), parse_mode=ParseMode.HTML) + except RPCException as e: + self._send_msg(str(e)) + @authorized_only def _edge(self, update: Update, context: CallbackContext) -> None: """ @@ -682,6 +711,7 @@ class Telegram(RPC): "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" "*/reload_config:* `Reload configuration file` \n" "*/show_config:* `Show running configuration` \n" + "*/logs [limit]:* `Show latest logs - defaults to 10` \n" "*/whitelist:* `Show current whitelist` \n" "*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs " "to the blacklist.` \n" diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index bfa774856..8651b0613 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -76,7 +76,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], " "['delete'], ['performance'], ['daily'], ['count'], ['reload_config', " "'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], " - "['whitelist'], ['blacklist'], ['edge'], ['help'], ['version']]") + "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']]") assert log_has(message_str, caplog) From 904c4ecc23f14dd3f549ae9d98e773616206ed61 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 14 Aug 2020 15:44:52 +0200 Subject: [PATCH 0427/1197] Document /logs endpoints --- docs/rest-api.md | 74 +++++++++++++++++++++++------------------- docs/telegram-usage.md | 1 + scripts/rest_client.py | 8 +++++ 3 files changed, 49 insertions(+), 34 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 68754f79a..075bd7e64 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -116,6 +116,7 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `trades` | List last trades. | `delete_trade ` | Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange. | `show_config` | Shows part of the current configuration with relevant settings to operation +| `logs` | Shows last log messages | `status` | Lists all open trades | `count` | Displays number of trades used and available | `profit` | Display a summary of your profit/loss from close trades and some stats about your performance @@ -138,78 +139,83 @@ python3 scripts/rest_client.py help ``` output Possible commands: + balance - Get the account balance - :returns: json object + Get the account balance. blacklist - Show the current blacklist + Show the current blacklist. + :param add: List of coins to add (example: "BNB/BTC") - :returns: json object count - Returns the amount of open trades - :returns: json object + Return the amount of open trades. daily - Returns the amount of open trades - :returns: json object + Return the amount of open trades. + +delete_trade + Delete trade from the database. + Tries to close open orders. Requires manual handling of this asset on the exchange. + + :param trade_id: Deletes the trade with this ID from the database. edge - Returns information about edge - :returns: json object + Return information about edge. forcebuy - Buy an asset + Buy an asset. + :param pair: Pair to buy (ETH/BTC) :param price: Optional - price to buy - :returns: json object of the trade forcesell - Force-sell a trade + Force-sell a trade. + :param tradeid: Id of the trade (can be received via status command) - :returns: json object + +logs + Show latest logs. + + :param limit: Limits log messages to the last logs. No limit to get all the trades. performance - Returns the performance of the different coins - :returns: json object + Return the performance of the different coins. profit - Returns the profit summary - :returns: json object + Return the profit summary. reload_config - Reload configuration - :returns: json object + Reload configuration. show_config + Returns part of the configuration, relevant for trading operations. - :return: json object containing the version start - Start the bot if it's in stopped state. - :returns: json object + Start the bot if it's in the stopped state. status - Get the status of open trades - :returns: json object + Get the status of open trades. stop - Stop the bot. Use start to restart - :returns: json object + Stop the bot. Use `start` to restart. stopbuy - Stop buying (but handle sells gracefully). - use reload_config to reset - :returns: json object + Stop buying (but handle sells gracefully). Use `reload_config` to reset. + +trades + Return trades history. + + :param limit: Limits trades to the X last trades. No limit to get all the trades. version - Returns the version of the bot - :returns: json object containing the version + Return the version of the bot. whitelist - Show the current whitelist - :returns: json object + Show the current whitelist. + + ``` ## Advanced API usage using JWT tokens diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 9776b26ba..5f804386d 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -54,6 +54,7 @@ official commands. You can ask at any moment for help with `/help`. | `/stopbuy` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `/reload_config` | Reloads the configuration file | `/show_config` | Shows part of the current configuration with relevant settings to operation +| `/logs [limit]` | Show last log messages. | `/status` | Lists all open trades | `/status table` | List all open trades in a table format. Pending buy orders are marked with an asterisk (*) Pending sell orders are marked with a double asterisk (**) | `/trades [limit]` | List all recently closed trades in a table format. diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 51ea596f6..b100999a3 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -159,6 +159,14 @@ class FtRestClient(): """ return self._get("show_config") + def logs(self, limit=None): + """Show latest logs. + + :param limit: Limits log messages to the last logs. No limit to get all the trades. + :return: json object + """ + return self._get("logs", params={"limit": limit} if limit else 0) + def trades(self, limit=None): """Return trades history. From 47b215fe0aa23f6222b0b1e107e017d3009cf4e8 Mon Sep 17 00:00:00 2001 From: Blackhawke Date: Fri, 14 Aug 2020 09:25:53 -0700 Subject: [PATCH 0428/1197] Update docs/edge.md Co-authored-by: Matthias --- docs/edge.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/edge.md b/docs/edge.md index ccbae1cb1..1695732af 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -190,7 +190,9 @@ An example of its output: Edge produced the above table by comparing ``calculate_since_number_of_days`` to ``minimum_expectancy`` to find ``min_trade_number``. Historical information based on the config file. The time frame Edge uses for its comparisons can be further limited by using the ``--timeframe`` switch. -In live and dry-run modes, after the ``process_throttle_secs`` has passed, Edge will again process ``calculate_since_number_of_days`` against ``minimum_expectancy`` to find ``min_trade_number``. If no ``min_trade_number`` is found, the bot will return "whitelist empty". Depending on the trade strategy being deployed, "whitelist empty" may be return much of the time---or *all* of the time. The use of Edge may also cause trading to occur in bursts, though this is rare. +In live and dry-run modes, after the `process_throttle_secs` has passed, Edge will again process `calculate_since_number_of_days` against `minimum_expectancy` to find `min_trade_number`. If no `min_trade_number` is found, the bot will return "whitelist empty". Depending on the trade strategy being deployed, "whitelist empty" may be return much of the time - or *all* of the time. The use of Edge may also cause trading to occur in bursts, though this is rare. + +If you encounter "whitelist empty" a lot, condsider tuning `calculate_since_number_of_days`, `minimum_expectancy` and `min_trade_number` to align to the trading frequency of your strategy. ### Update cached pairs with the latest data From a14ce9d7d9df45bf5ba9ce8e02c559ae8477c064 Mon Sep 17 00:00:00 2001 From: Blackhawke Date: Fri, 14 Aug 2020 09:26:28 -0700 Subject: [PATCH 0429/1197] Update docs/edge.md Co-authored-by: Matthias --- docs/edge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/edge.md b/docs/edge.md index 1695732af..dad23bfd6 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -188,7 +188,7 @@ An example of its output: | APPC/BTC | -0.02 | 0.44 | 2.28 | 1.27 | 0.44 | 25 | 43 | | NEBL/BTC | -0.03 | 0.63 | 1.29 | 0.58 | 0.44 | 19 | 59 | -Edge produced the above table by comparing ``calculate_since_number_of_days`` to ``minimum_expectancy`` to find ``min_trade_number``. Historical information based on the config file. The time frame Edge uses for its comparisons can be further limited by using the ``--timeframe`` switch. +Edge produced the above table by comparing `calculate_since_number_of_days` to `minimum_expectancy` to find `min_trade_number` historical information based on the config file. The timerange Edge uses for its comparisons can be further limited by using the `--timerange` switch. In live and dry-run modes, after the `process_throttle_secs` has passed, Edge will again process `calculate_since_number_of_days` against `minimum_expectancy` to find `min_trade_number`. If no `min_trade_number` is found, the bot will return "whitelist empty". Depending on the trade strategy being deployed, "whitelist empty" may be return much of the time - or *all* of the time. The use of Edge may also cause trading to occur in bursts, though this is rare. From f3cedc849aa223407890d4c216cfec17cdfda02d Mon Sep 17 00:00:00 2001 From: Blackhawke Date: Fri, 14 Aug 2020 09:27:04 -0700 Subject: [PATCH 0430/1197] Update docs/edge.md Co-authored-by: Matthias --- docs/edge.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/edge.md b/docs/edge.md index dad23bfd6..2bc43a4e8 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -5,10 +5,9 @@ This page explains how to use Edge Positioning module in your bot in order to en !!! Warning Edge positioning is not compatible with dynamic (volume-based) whitelist. - !!! Note - 1. Edge does not consider anything other than *its own* buy/sell/stoploss signals. It ignores the stoploss, trailing stoploss, and ROI settings in the strategy configuration file. - - 2. Therefore, it is important to understand that Edge can improve the performance of some trading strategies but *decrease* the performance of others. +!!! Note + Edge does not consider anything other than *its own* buy/sell/stoploss signals. It ignores the stoploss, trailing stoploss, and ROI settings in the strategy configuration file. + Therefore, it is important to understand that Edge can improve the performance of some trading strategies but *decrease* the performance of others. ## Introduction From 5d691b5ee3872129c3cd6a7bdfd70836e79e72fd Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 14 Aug 2020 19:34:22 +0200 Subject: [PATCH 0431/1197] Fix warning box typo --- docs/edge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/edge.md b/docs/edge.md index 2bc43a4e8..dcb559f96 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -2,7 +2,7 @@ This page explains how to use Edge Positioning module in your bot in order to enter into a trade only if the trade has a reasonable win rate and risk reward ratio, and consequently adjust your position size and stoploss. - !!! Warning +!!! Warning Edge positioning is not compatible with dynamic (volume-based) whitelist. !!! Note From 9ad8e74247ed1fb1bb50b39309e22528567e372f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 14 Aug 2020 19:36:12 +0200 Subject: [PATCH 0432/1197] Add tests for log-endpoints --- freqtrade/main.py | 1 - freqtrade/rpc/rpc.py | 5 +++-- tests/rpc/test_rpc_apiserver.py | 27 ++++++++++++++++++++++++++- tests/rpc/test_rpc_telegram.py | 21 +++++++++++++++++++++ 4 files changed, 50 insertions(+), 4 deletions(-) diff --git a/freqtrade/main.py b/freqtrade/main.py index eeb975953..dc26c2a46 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -3,7 +3,6 @@ Main Freqtrade bot script. Read the documentation to know what cli arguments you need. """ -# flake8: noqa E402 import logging import sys from typing import Any, List diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 5da428a9c..dd35b9613 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -639,12 +639,13 @@ class RPC: buffer = bufferHandler.buffer[-limit:] else: buffer = bufferHandler.buffer - records = [[r.asctime, r.created, r.name, r.levelname, r.message] for r in buffer] + records = [[datetime.fromtimestamp(r.created), r.created, r.name, r.levelname, r.message] + for r in buffer] return {'log_count': len(records), 'logs': records} def _rpc_get_logs_as_string(self, limit: Optional[int]) -> Dict[str, List]: - """Returns the last X logs""" + """Returns the last X logs as formatted string (Using the default log format)""" if limit: buffer = bufferHandler.buffer[-limit:] else: diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 408f7e537..2fb1e3ec1 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -10,10 +10,12 @@ from flask import Flask from requests.auth import _basic_auth_str from freqtrade.__init__ import __version__ +from freqtrade.loggers import setup_logging, setup_logging_pre from freqtrade.persistence import Trade from freqtrade.rpc.api_server import BASE_URI, ApiServer from freqtrade.state import State -from tests.conftest import get_patched_freqtradebot, log_has, patch_get_signal, create_mock_trades +from tests.conftest import (create_mock_trades, get_patched_freqtradebot, + log_has, patch_get_signal) _TEST_USER = "FreqTrader" _TEST_PASS = "SuperSecurePassword1!" @@ -21,6 +23,9 @@ _TEST_PASS = "SuperSecurePassword1!" @pytest.fixture def botclient(default_conf, mocker): + setup_logging_pre() + setup_logging(default_conf) + default_conf.update({"api_server": {"enabled": True, "listen_ip_address": "127.0.0.1", "listen_port": 8080, @@ -423,6 +428,26 @@ def test_api_delete_trade(botclient, mocker, fee, markets): assert stoploss_mock.call_count == 1 +def test_api_logs(botclient): + ftbot, client = botclient + rc = client_get(client, f"{BASE_URI}/logs") + assert_response(rc) + assert len(rc.json) == 2 + assert 'logs' in rc.json + # Using a fixed comparison here would make this test fail! + assert rc.json['log_count'] > 10 + assert len(rc.json['logs']) == rc.json['log_count'] + + assert isinstance(rc.json['logs'][0], list) + # date + assert isinstance(rc.json['logs'][0][0], str) + # created_timestamp + assert isinstance(rc.json['logs'][0][1], float) + assert isinstance(rc.json['logs'][0][2], str) + assert isinstance(rc.json['logs'][0][3], str) + assert isinstance(rc.json['logs'][0][4], str) + + def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 8651b0613..1144d8279 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -16,6 +16,7 @@ from telegram.error import NetworkError from freqtrade import __version__ from freqtrade.edge import PairInfo from freqtrade.freqtradebot import FreqtradeBot +from freqtrade.loggers import setup_logging from freqtrade.persistence import Trade from freqtrade.rpc import RPCMessageType from freqtrade.rpc.telegram import Telegram, authorized_only @@ -1107,6 +1108,26 @@ def test_blacklist_static(default_conf, update, mocker) -> None: assert freqtradebot.pairlists.blacklist == ["DOGE/BTC", "HOT/BTC", "ETH/BTC"] +def test_telegram_logs(default_conf, update, mocker) -> None: + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + setup_logging(default_conf) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + + telegram = Telegram(freqtradebot) + context = MagicMock() + context.args = [] + telegram._logs(update=update, context=context) + assert msg_mock.call_count == 1 + assert "freqtrade.rpc.telegram" in msg_mock.call_args_list[0][0][0] + assert "freqtrade.resolvers.iresolver" in msg_mock.call_args_list[0][0][0] + + def test_edge_disabled(default_conf, update, mocker) -> None: msg_mock = MagicMock() mocker.patch.multiple( From 122c0e8ddc1771c977b4e42e0785621ea73080d4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 14 Aug 2020 19:50:56 +0200 Subject: [PATCH 0433/1197] Readd accidentally dropped StreamHandler --- freqtrade/loggers.py | 7 ++++++- tests/test_configuration.py | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py index 0b1337b2c..cb83f9144 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers.py @@ -1,7 +1,9 @@ import logging import queue +import sys from logging import Formatter -from logging.handlers import RotatingFileHandler, SysLogHandler, BufferingHandler +from logging.handlers import (BufferingHandler, RotatingFileHandler, + SysLogHandler) from typing import Any, Dict from freqtrade.exceptions import OperationalException @@ -58,6 +60,9 @@ def setup_logging(config: Dict[str, Any]) -> None: logging.root.addHandler(bufferHandler) logfile = config.get('logfile') + + logging.root.addHandler(logging.StreamHandler(sys.stderr)) + if logfile: s = logfile.split(':') if s[0] == 'syslog': diff --git a/tests/test_configuration.py b/tests/test_configuration.py index ca5d6eadc..dd96f9d73 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -675,7 +675,7 @@ def test_set_loggers_syslog(mocker): } setup_logging(config) - assert len(logger.handlers) == 2 + assert len(logger.handlers) == 3 assert [x for x in logger.handlers if type(x) == logging.handlers.SysLogHandler] assert [x for x in logger.handlers if type(x) == logging.StreamHandler] # reset handlers to not break pytest From 251eb5aa96cbfc385d3eb18347742fd1a10e5db9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 14 Aug 2020 19:51:50 +0200 Subject: [PATCH 0434/1197] Test for bufferingHandler too --- tests/test_configuration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index dd96f9d73..8c3a47f87 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -678,6 +678,7 @@ def test_set_loggers_syslog(mocker): assert len(logger.handlers) == 3 assert [x for x in logger.handlers if type(x) == logging.handlers.SysLogHandler] assert [x for x in logger.handlers if type(x) == logging.StreamHandler] + assert [x for x in logger.handlers if type(x) == logging.handlers.BufferingHandler] # reset handlers to not break pytest logger.handlers = orig_handlers From cdfcdb86c96fcb0f9b53236aafe1140964206350 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 14 Aug 2020 20:00:09 +0200 Subject: [PATCH 0435/1197] Increase logfile size --- freqtrade/loggers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py index cb83f9144..80759e202 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers.py @@ -92,7 +92,7 @@ def setup_logging(config: Dict[str, Any]) -> None: logging.root.addHandler(handler) else: handler = RotatingFileHandler(logfile, - maxBytes=1024 * 1024, # 1Mb + maxBytes=1024 * 1024 * 10, # 10Mb backupCount=10) handler.setFormatter(Formatter(LOGFORMAT)) logging.root.addHandler(handler) From c4f78203ab7875c3b23fb0a39787a2e96a469dc4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 14 Aug 2020 20:08:55 +0200 Subject: [PATCH 0436/1197] Initialize streamhandler early to have it apply to all logs --- freqtrade/loggers.py | 3 +-- tests/test_configuration.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py index 80759e202..c4a1af4f3 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers.py @@ -48,6 +48,7 @@ def setup_logging_pre() -> None: logging.basicConfig( level=logging.INFO, format=LOGFORMAT, + handlers=[logging.StreamHandler(sys.stderr)] ) @@ -61,8 +62,6 @@ def setup_logging(config: Dict[str, Any]) -> None: logfile = config.get('logfile') - logging.root.addHandler(logging.StreamHandler(sys.stderr)) - if logfile: s = logfile.split(':') if s[0] == 'syslog': diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 8c3a47f87..30e0718f7 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -21,7 +21,7 @@ from freqtrade.configuration.deprecated_settings import ( from freqtrade.configuration.load_config import load_config_file, log_config_error_range from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.exceptions import OperationalException -from freqtrade.loggers import _set_loggers, setup_logging +from freqtrade.loggers import _set_loggers, setup_logging, setup_logging_pre from freqtrade.state import RunMode from tests.conftest import (log_has, log_has_re, patched_configuration_load_config_file) @@ -674,6 +674,7 @@ def test_set_loggers_syslog(mocker): 'logfile': 'syslog:/dev/log', } + setup_logging_pre() setup_logging(config) assert len(logger.handlers) == 3 assert [x for x in logger.handlers if type(x) == logging.handlers.SysLogHandler] From 9659e516c8c8298a376b131e24f281cfea537c0e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 14 Aug 2020 20:12:59 +0200 Subject: [PATCH 0437/1197] Remove queue import Improve tests --- freqtrade/loggers.py | 9 ++++----- tests/rpc/test_rpc_apiserver.py | 8 ++++++++ tests/rpc/test_rpc_telegram.py | 15 +++++++++++++++ 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py index c4a1af4f3..6e66abeeb 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers.py @@ -1,5 +1,4 @@ import logging -import queue import sys from logging import Formatter from logging.handlers import (BufferingHandler, RotatingFileHandler, @@ -9,7 +8,6 @@ from typing import Any, Dict from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) -log_queue = queue.Queue(-1) LOGFORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' # Initialize bufferhandler - will be used for /log endpoints @@ -41,9 +39,10 @@ def _set_loggers(verbosity: int = 0, api_verbosity: str = 'info') -> None: def setup_logging_pre() -> None: """ - Setup early logging. - This uses a queuehandler, which delays logging. - # TODO: How does QueueHandler work if no listenerhandler is attached?? + Early setup for logging. + Uses INFO loglevel and only the Streamhandler. + Early messages (before proper logging setup) will therefore only be available + after the proper logging setup. """ logging.basicConfig( level=logging.INFO, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 2fb1e3ec1..1dd0ecff8 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -447,6 +447,14 @@ def test_api_logs(botclient): assert isinstance(rc.json['logs'][0][3], str) assert isinstance(rc.json['logs'][0][4], str) + rc = client_get(client, f"{BASE_URI}/logs?limit=5") + assert_response(rc) + assert len(rc.json) == 2 + assert 'logs' in rc.json + # Using a fixed comparison here would make this test fail! + assert rc.json['log_count'] == 5 + assert len(rc.json['logs']) == rc.json['log_count'] + def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): ftbot, client = botclient diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 1144d8279..e0df31437 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1127,6 +1127,21 @@ def test_telegram_logs(default_conf, update, mocker) -> None: assert "freqtrade.rpc.telegram" in msg_mock.call_args_list[0][0][0] assert "freqtrade.resolvers.iresolver" in msg_mock.call_args_list[0][0][0] + msg_mock.reset_mock() + context.args = ["1"] + telegram._logs(update=update, context=context) + assert msg_mock.call_count == 1 + + msg_mock.reset_mock() + # Test with changed MaxMessageLength + mocker.patch('freqtrade.rpc.telegram.MAX_TELEGRAM_MESSAGE_LENGTH', 200) + context = MagicMock() + context.args = [] + telegram._logs(update=update, context=context) + # Called at least 3 times. Exact times will change with unrelated changes to setup messages + # Therefore we don't test for this explicitly. + assert msg_mock.call_count > 3 + def test_edge_disabled(default_conf, update, mocker) -> None: msg_mock = MagicMock() From f5863a1c6fd3d8b29cde23561fb8b4dbfc86bf3d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Aug 2020 08:15:18 +0200 Subject: [PATCH 0438/1197] Fix mypy errors --- freqtrade/loggers.py | 16 ++++++++-------- freqtrade/rpc/rpc.py | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py index 6e66abeeb..263f97ce1 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers.py @@ -82,18 +82,18 @@ def setup_logging(config: Dict[str, Any]) -> None: except ImportError: raise OperationalException("You need the systemd python package be installed in " "order to use logging to journald.") - handler = JournaldLogHandler() + handler_jd = JournaldLogHandler() # No datetime field for logging into journald, to allow syslog # to perform reduction of repeating messages if this is set in the # syslog config. The messages should be equal for this. - handler.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s')) - logging.root.addHandler(handler) + handler_jd.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s')) + logging.root.addHandler(handler_jd) else: - handler = RotatingFileHandler(logfile, - maxBytes=1024 * 1024 * 10, # 10Mb - backupCount=10) - handler.setFormatter(Formatter(LOGFORMAT)) - logging.root.addHandler(handler) + handler_rf = RotatingFileHandler(logfile, + maxBytes=1024 * 1024 * 10, # 10Mb + backupCount=10) + handler_rf.setFormatter(Formatter(LOGFORMAT)) + logging.root.addHandler(handler_rf) logging.root.setLevel(logging.INFO if verbosity < 1 else logging.DEBUG) _set_loggers(verbosity, config.get('api_server', {}).get('verbosity', 'info')) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index dd35b9613..37538499d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -633,7 +633,7 @@ class RPC: } return res - def _rpc_get_logs(self, limit: Optional[int]) -> Dict[str, List]: + def _rpc_get_logs(self, limit: Optional[int]) -> Dict[str, Any]: """Returns the last X logs""" if limit: buffer = bufferHandler.buffer[-limit:] @@ -644,7 +644,7 @@ class RPC: return {'log_count': len(records), 'logs': records} - def _rpc_get_logs_as_string(self, limit: Optional[int]) -> Dict[str, List]: + def _rpc_get_logs_as_string(self, limit: Optional[int]) -> List[str]: """Returns the last X logs as formatted string (Using the default log format)""" if limit: buffer = bufferHandler.buffer[-limit:] From 1ffa3d1ae0e4b12f082bc7628500d2940ae99291 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Aug 2020 08:31:36 +0200 Subject: [PATCH 0439/1197] Improve telegram message formatting --- freqtrade/rpc/rpc.py | 11 ++--------- freqtrade/rpc/telegram.py | 20 +++++++++++--------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 37538499d..59c6acafa 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -639,19 +639,12 @@ class RPC: buffer = bufferHandler.buffer[-limit:] else: buffer = bufferHandler.buffer - records = [[datetime.fromtimestamp(r.created), r.created, r.name, r.levelname, r.message] + records = [[datetime.fromtimestamp(r.created).strftime("%Y-%m-%d %H:%M:%S"), + r.created, r.name, r.levelname, r.message] for r in buffer] return {'log_count': len(records), 'logs': records} - def _rpc_get_logs_as_string(self, limit: Optional[int]) -> List[str]: - """Returns the last X logs as formatted string (Using the default log format)""" - if limit: - buffer = bufferHandler.buffer[-limit:] - else: - buffer = bufferHandler.buffer - return [bufferHandler.format(r) for r in buffer] - def _rpc_edge(self) -> List[Dict[str, Any]]: """ Returns information related to Edge """ if not self._freqtrade.edge: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index da93604d1..23c3e3689 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -649,20 +649,22 @@ class Telegram(RPC): limit = int(context.args[0]) except (TypeError, ValueError, IndexError): limit = 10 - logs = self._rpc_get_logs_as_string(limit) - msg = '' - message_container = "
    {}
    " + logs = self._rpc_get_logs(limit)['logs'] + msgs = '' + msg_template = "*{}* {}: {} - `{}`" for logrec in logs: - if len(msg + logrec) + 10 >= MAX_TELEGRAM_MESSAGE_LENGTH: + msg = msg_template.format(logrec[0], logrec[2], logrec[3], logrec[4]) + + if len(msgs + msg) + 10 >= MAX_TELEGRAM_MESSAGE_LENGTH: # Send message immediately if it would become too long - self._send_msg(message_container.format(msg), parse_mode=ParseMode.HTML) - msg = logrec + '\n' + self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN) + msgs = msg + '\n' else: # Append message to messages to send - msg += logrec + '\n' + msgs += msg + '\n' - if msg: - self._send_msg(message_container.format(msg), parse_mode=ParseMode.HTML) + if msgs: + self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN) except RPCException as e: self._send_msg(str(e)) From f3d4b114bbcb335b7225c0a984c0bdd8018814cd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Aug 2020 08:47:09 +0200 Subject: [PATCH 0440/1197] Skip windows test failure --- tests/test_configuration.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 30e0718f7..686f06057 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -729,7 +729,10 @@ def test_set_logfile(default_conf, mocker): assert validated_conf['logfile'] == "test_file.log" f = Path("test_file.log") assert f.is_file() - f.unlink() + try: + f.unlink() + except Exception: + pass def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None: From 9dd2800b980d044a07febcc00e471b4691db6a6f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Aug 2020 09:08:50 +0200 Subject: [PATCH 0441/1197] Apply some review changes --- docs/configuration.md | 6 +++--- freqtrade/pairlist/AgeFilter.py | 4 ++-- freqtrade/pairlist/PriceFilter.py | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index f39a3c62d..5fc28f3bf 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -669,13 +669,13 @@ The `PriceFilter` allows filtering of pairs by price. Currently the following pr * `low_price_ratio` The `min_price` setting removes pairs where the price is below the specified price. This is useful if you wish to avoid trading very low-priced pairs. -This option is disabled by default (or set to 0), and will only apply if set to > 0. +This option is disabled by default, and will only apply if set to > 0. The `max_price` setting removes pairs where the price is above the specified price. This is useful if you wish to trade only low-priced pairs. -This option is disabled by default (or set to 0), and will only apply if set to > 0. +This option is disabled by default, and will only apply if set to > 0. The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio. -This option is disabled by default (or set to 0), and will only apply if set to > 0. +This option is disabled by default, and will only apply if set to > 0. For `PriceFiler` at least one of its `min_price`, `max_price` or `low_price_ratio` settings must be applied. diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index 56e56ceeb..64f01cb61 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -26,9 +26,9 @@ class AgeFilter(IPairList): self._min_days_listed = pairlistconfig.get('min_days_listed', 10) if self._min_days_listed < 1: - raise OperationalException("AgeFilter requires min_days_listed be >= 1") + raise OperationalException("AgeFilter requires min_days_listed to be >= 1") if self._min_days_listed > exchange.ohlcv_candle_limit: - raise OperationalException("AgeFilter requires min_days_listed be not exceeding " + raise OperationalException("AgeFilter requires min_days_listed to not exceed " "exchange max request size " f"({exchange.ohlcv_candle_limit})") diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/pairlist/PriceFilter.py index ae3ab9230..8cd57ee1d 100644 --- a/freqtrade/pairlist/PriceFilter.py +++ b/freqtrade/pairlist/PriceFilter.py @@ -20,13 +20,13 @@ class PriceFilter(IPairList): self._low_price_ratio = pairlistconfig.get('low_price_ratio', 0) if self._low_price_ratio < 0: - raise OperationalException("PriceFilter requires low_price_ratio be >= 0") + raise OperationalException("PriceFilter requires low_price_ratio to be >= 0") self._min_price = pairlistconfig.get('min_price', 0) if self._min_price < 0: - raise OperationalException("PriceFilter requires min_price be >= 0") + raise OperationalException("PriceFilter requires min_price to be >= 0") self._max_price = pairlistconfig.get('max_price', 0) if self._max_price < 0: - raise OperationalException("PriceFilter requires max_price be >= 0") + raise OperationalException("PriceFilter requires max_price to be >= 0") self._enabled = ((self._low_price_ratio > 0) or (self._min_price > 0) or (self._max_price > 0)) From 142f87b68ce4bbe61d8f52581a3a44aa67ebdfae Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Aug 2020 09:11:46 +0200 Subject: [PATCH 0442/1197] Adjust tests to new wordings --- tests/pairlist/test_pairlist.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index ad664abd2..9217abc46 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -549,7 +549,7 @@ def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tick ) with pytest.raises(OperationalException, - match=r'AgeFilter requires min_days_listed be >= 1'): + match=r'AgeFilter requires min_days_listed to be >= 1'): get_patched_freqtradebot(mocker, default_conf) @@ -564,7 +564,7 @@ def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tick ) with pytest.raises(OperationalException, - match=r'AgeFilter requires min_days_listed be not exceeding ' + match=r'AgeFilter requires min_days_listed to not exceed ' r'exchange max request size \([0-9]+\)'): get_patched_freqtradebot(mocker, default_conf) @@ -617,15 +617,15 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_his ), ({"method": "PriceFilter", "low_price_ratio": -0.001}, None, - "PriceFilter requires low_price_ratio be >= 0" + "PriceFilter requires low_price_ratio to be >= 0" ), # OperationalException expected ({"method": "PriceFilter", "min_price": -0.00000010}, None, - "PriceFilter requires min_price be >= 0" + "PriceFilter requires min_price to be >= 0" ), # OperationalException expected ({"method": "PriceFilter", "max_price": -1.00010000}, None, - "PriceFilter requires max_price be >= 0" + "PriceFilter requires max_price to be >= 0" ), # OperationalException expected ]) def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, From cc91d5138910ce1d355a7eedb801274f05d0f7a0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Aug 2020 09:18:00 +0200 Subject: [PATCH 0443/1197] Fix wording in configuration.md --- docs/configuration.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 5fc28f3bf..a366cde12 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -683,7 +683,8 @@ Calculation example: Min price precision for SHITCOIN/BTC is 8 decimals. If its price is 0.00000011 - one price step above would be 0.00000012, which is ~9% higher than the previous price value. You may filter out this pair by using PriceFilter with `low_price_ratio` set to 0.09 (9%) or with `min_price` set to 0.00000011, correspondingly. -Low priced pairs are dangerous since they are often illiquid and it may also be impossible to place the desired stoploss, which can often result in high losses. Consider using PriceFilter with `low_price_ratio` set to a value which is less than the absolute value of your stoploss (for example, if your stoploss is -5% (-0.05), then the value for `low_price_ratio` can be 0.04 or even 0.02). +!!! Warning "Low priced pairs" + Low priced pairs with high "1 pip movements" are dangerous since they are often illiquid and it may also be impossible to place the desired stoploss, which can often result in high losses since price needs to be rounded to the next tradable price - so instead of having a stoploss of -5%, you could end up with a stoploss of -9% simply due to price rounding. #### ShuffleFilter From c2573c998b98fb295c2c29f6bc092a89c829b7c2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Aug 2020 16:26:47 +0200 Subject: [PATCH 0444/1197] Remove Hyperopt note about windows --- docs/hyperopt.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 6330a1a5e..9acb606c3 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -370,9 +370,6 @@ By default, hyperopt prints colorized results -- epochs with positive profit are You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. When `--print-all` is used, current best results are also colorized by default -- they are printed in bold (bright) style. This can also be switched off with the `--no-color` command line option. -!!! Note "Windows and color output" - Windows does not support color-output nativly, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL. - ### Understand Hyperopt ROI results If you are optimizing ROI (i.e. if optimization search-space contains 'all', 'default' or 'roi'), your result will look as follows and include a ROI table: From 56ca37fd8bb95e27baca7f396a4ea385e67d3638 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 15 Aug 2020 20:15:02 +0200 Subject: [PATCH 0445/1197] Also provide stacktrace via log endpoints --- freqtrade/rpc/rpc.py | 3 ++- freqtrade/rpc/telegram.py | 13 ++++++++----- tests/rpc/test_rpc_telegram.py | 4 ++-- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 59c6acafa..b7a4f4f8c 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -640,7 +640,8 @@ class RPC: else: buffer = bufferHandler.buffer records = [[datetime.fromtimestamp(r.created).strftime("%Y-%m-%d %H:%M:%S"), - r.created, r.name, r.levelname, r.message] + r.created, r.name, r.levelname, + r.message + ('\n' + r.exc_text if r.exc_text else '')] for r in buffer] return {'log_count': len(records), 'logs': records} diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 23c3e3689..a095714a7 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -12,6 +12,7 @@ from tabulate import tabulate from telegram import ParseMode, ReplyKeyboardMarkup, Update from telegram.error import NetworkError, TelegramError from telegram.ext import CallbackContext, CommandHandler, Updater +from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ from freqtrade.rpc import RPC, RPCException, RPCMessageType @@ -651,20 +652,22 @@ class Telegram(RPC): limit = 10 logs = self._rpc_get_logs(limit)['logs'] msgs = '' - msg_template = "*{}* {}: {} - `{}`" + msg_template = "*{}* {}: {} \\- `{}`" for logrec in logs: - msg = msg_template.format(logrec[0], logrec[2], logrec[3], logrec[4]) - + msg = msg_template.format(escape_markdown(logrec[0], version=2), + escape_markdown(logrec[2], version=2), + escape_markdown(logrec[3], version=2), + escape_markdown(logrec[4], version=2)) if len(msgs + msg) + 10 >= MAX_TELEGRAM_MESSAGE_LENGTH: # Send message immediately if it would become too long - self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN) + self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) msgs = msg + '\n' else: # Append message to messages to send msgs += msg + '\n' if msgs: - self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN) + self._send_msg(msgs, parse_mode=ParseMode.MARKDOWN_V2) except RPCException as e: self._send_msg(str(e)) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index e0df31437..026a81ff8 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1124,8 +1124,8 @@ def test_telegram_logs(default_conf, update, mocker) -> None: context.args = [] telegram._logs(update=update, context=context) assert msg_mock.call_count == 1 - assert "freqtrade.rpc.telegram" in msg_mock.call_args_list[0][0][0] - assert "freqtrade.resolvers.iresolver" in msg_mock.call_args_list[0][0][0] + assert "freqtrade\\.rpc\\.telegram" in msg_mock.call_args_list[0][0][0] + assert "freqtrade\\.resolvers\\.iresolver" in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() context.args = ["1"] From b9e46a3c5a0d2f2ad4f8b18af9d31385fa73e325 Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Sun, 16 Aug 2020 03:02:10 +0200 Subject: [PATCH 0446/1197] Update stoploss.md Updated documentation to simplify examples --- docs/stoploss.md | 117 +++++++++++++++++++++++++++++++++-------------- 1 file changed, 83 insertions(+), 34 deletions(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index bf7270dff..2945c18db 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -6,16 +6,10 @@ For example, value `-0.10` will cause immediate sell if the profit dips below -1 Most of the strategy files already include the optimal `stoploss` value. !!! Info - All stoploss properties mentioned in this file can be set in the Strategy, or in the configuration. Configuration values will override the strategy values. + All stoploss properties mentioned in this file can be set in the Strategy, or in the configuration. + Configuration values will override the strategy values. -## Stop Loss Types - -At this stage the bot contains the following stoploss support modes: - -1. Static stop loss. -2. Trailing stop loss. -3. Trailing stop loss, custom positive loss. -4. Trailing stop loss only once the trade has reached a certain offset. +## Stop Loss On-Exchange/Freqtrade Those stoploss modes can be *on exchange* or *off exchange*. If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfully. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled. @@ -27,19 +21,58 @@ So this parameter will tell the bot how often it should update the stoploss orde This same logic will reapply a stoploss order on the exchange should you cancel it accidentally. !!! Note - Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market) and FTX (stop limit and stop-market) as of now. + Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market) and FTX (stop limit and stop-market) as of now. + Do not set too low stoploss value if using stop loss on exhange! -## Static Stop Loss +Stop loss on exchange is controlled with this value (default False): + +``` python + 'stoploss_on_exchange': False +``` + +Example from strategy file: + +``` python +order_types = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'market', + 'stoploss_on_exchange': True +} +``` + +## Stop Loss Types + +At this stage the bot contains the following stoploss support modes: + +1. Static stop loss. +2. Trailing stop loss. +3. Trailing stop loss, custom positive loss. +4. Trailing stop loss only once the trade has reached a certain offset. + +### Static Stop Loss This is very simple, you define a stop loss of x (as a ratio of price, i.e. x * 100% of price). This will try to sell the asset once the loss exceeds the defined loss. -## Trailing Stop Loss +Example of stop loss: + +``` python + stoploss = -0.10 +``` + +For example, simplified math: +* the bot buys an asset at a price of 100$ +* the stop loss is defined at -10% +* the stop loss would get triggered once the asset dropps below 90$ + +### Trailing Stop Loss The initial value for this is `stoploss`, just as you would define your static Stop loss. To enable trailing stoploss: ``` python -trailing_stop = True + stoploss = -0.10 + trailing_stop = True ``` This will now activate an algorithm, which automatically moves the stop loss up every time the price of your asset increases. @@ -47,35 +80,40 @@ This will now activate an algorithm, which automatically moves the stop loss up For example, simplified math: * the bot buys an asset at a price of 100$ -* the stop loss is defined at 2% -* the stop loss would get triggered once the asset dropps below 98$ +* the stop loss is defined at -10% +* the stop loss would get triggered once the asset dropps below 90$ * assuming the asset now increases to 102$ -* the stop loss will now be 2% of 102$ or 99.96$ -* now the asset drops in value to 101$, the stop loss will still be 99.96$ and would trigger at 99.96$. +* the stop loss will now be -10% of 102$ = 91,8$ +* now the asset drops in value to 101$, the stop loss will still be 91,8$ and would trigger at 91,8$. In summary: The stoploss will be adjusted to be always be 2% of the highest observed price. -### Custom positive stoploss +### Trailing stop loss, custom positive loss -It is also possible to have a default stop loss, when you are in the red with your buy, but once your profit surpasses a certain percentage, the system will utilize a new stop loss, which can have a different value. -For example your default stop loss is 5%, but once you have 1.1% profit, it will be changed to be only a 1% stop loss, which trails the green candles until it goes below them. +It is also possible to have a default stop loss, when you are in the red with your buy (buy - fee), but once you hit possitive result the system will utilize a new stop loss, which can have a different value. +For example your default stop loss is -10%, but once you have more than 0% profit (example 0.1%) a different trailing stoploss will be used. -Both values require `trailing_stop` to be set to true. +Both values require `trailing_stop` to be set to true and trailing_stop_positive with a value. ``` python - trailing_stop_positive = 0.01 - trailing_stop_positive_offset = 0.011 + stoploss = -0.10 + trailing_stop = True + trailing_stop_positive = 0.02 ``` -The 0.01 would translate to a 1% stop loss, once you hit 1.1% profit. +For example, simplified math: + +* the bot buys an asset at a price of 100$ +* the stop loss is defined at -10% +* the stop loss would get triggered once the asset dropps below 90$ +* assuming the asset now increases to 102$ +* the stop loss will now be -2% of 102$ = 99,96$ +* now the asset drops in value to 101$, the stop loss will still be 99,96$ and would trigger at 99,96$ + +The 0.02 would translate to a -2% stop loss. Before this, `stoploss` is used for the trailing stoploss. -Read the [next section](#trailing-only-once-offset-is-reached) to keep stoploss at 5% of the entry point. - -!!! Tip - Make sure to have this value (`trailing_stop_positive_offset`) lower than minimal ROI, otherwise minimal ROI will apply first and sell the trade. - -### Trailing only once offset is reached +### Trailing stop loss only once the trade has reached a certain offset. It is also possible to use a static stoploss until the offset is reached, and then trail the trade to take profits once the market turns. @@ -87,17 +125,28 @@ This option can be used with or without `trailing_stop_positive`, but uses `trai trailing_only_offset_is_reached = True ``` -Simplified example: +Configuration (offset is buyprice + 3%): ``` python - stoploss = 0.05 + stoploss = -0.10 + trailing_stop = True + trailing_stop_positive = 0.02 trailing_stop_positive_offset = 0.03 trailing_only_offset_is_reached = True ``` +For example, simplified math: + * the bot buys an asset at a price of 100$ -* the stop loss is defined at 5% -* the stop loss will remain at 95% until profit reaches +3% +* the stop loss is defined at -10% +* the stop loss would get triggered once the asset dropps below 90$ +* stoploss will remain at 90$ unless asset increases to or above our configured offset +* assuming the asset now increases to 103$ (where we have the offset configured) +* the stop loss will now be -2% of 103$ = 100,94$ +* now the asset drops in value to 101$, the stop loss will still be 100,94$ and would trigger at 100,94$ + +!!! Tip + Make sure to have this value (`trailing_stop_positive_offset`) lower than minimal ROI, otherwise minimal ROI will apply first and sell the trade. ## Changing stoploss on open trades From bae8e5ed1a535435a580a2f39776929adc0046cd Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Sun, 16 Aug 2020 13:03:56 +0200 Subject: [PATCH 0447/1197] Update docs/stoploss.md Co-authored-by: Matthias --- docs/stoploss.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index 2945c18db..32c304e60 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -63,7 +63,7 @@ Example of stop loss: For example, simplified math: * the bot buys an asset at a price of 100$ * the stop loss is defined at -10% -* the stop loss would get triggered once the asset dropps below 90$ +* the stop loss would get triggered once the asset drops below 90$ ### Trailing Stop Loss From c60192e4bdd5440c9f1c5ff2d039c269935a71f4 Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Sun, 16 Aug 2020 13:05:07 +0200 Subject: [PATCH 0448/1197] Update docs/stoploss.md Co-authored-by: Matthias --- docs/stoploss.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index 32c304e60..8bb68eb1d 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -81,7 +81,7 @@ For example, simplified math: * the bot buys an asset at a price of 100$ * the stop loss is defined at -10% -* the stop loss would get triggered once the asset dropps below 90$ +* the stop loss would get triggered once the asset drops below 90$ * assuming the asset now increases to 102$ * the stop loss will now be -10% of 102$ = 91,8$ * now the asset drops in value to 101$, the stop loss will still be 91,8$ and would trigger at 91,8$. From 1ce392f65208b9a1426d414eae4712ac72ed6ce1 Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Sun, 16 Aug 2020 13:05:38 +0200 Subject: [PATCH 0449/1197] Update docs/stoploss.md Co-authored-by: Matthias --- docs/stoploss.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index 8bb68eb1d..dbe839cab 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -37,7 +37,9 @@ order_types = { 'buy': 'limit', 'sell': 'limit', 'stoploss': 'market', - 'stoploss_on_exchange': True + 'stoploss_on_exchange': True, + 'stoploss_on_exchange_interval': 60, + 'stoploss_on_exchange_limit_ratio': 0.99 } ``` From 4ade3daa1ef1fd25e523314acd6b209a494e2c8f Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Sun, 16 Aug 2020 13:09:19 +0200 Subject: [PATCH 0450/1197] Update docs/stoploss.md Co-authored-by: Matthias --- docs/stoploss.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index dbe839cab..57d6c160e 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -85,8 +85,8 @@ For example, simplified math: * the stop loss is defined at -10% * the stop loss would get triggered once the asset drops below 90$ * assuming the asset now increases to 102$ -* the stop loss will now be -10% of 102$ = 91,8$ -* now the asset drops in value to 101$, the stop loss will still be 91,8$ and would trigger at 91,8$. +* the stop loss will now be -10% of 102$ = 91.8$ +* now the asset drops in value to 101$, the stop loss will still be 91.8$ and would trigger at 91,8$. In summary: The stoploss will be adjusted to be always be 2% of the highest observed price. From 902d40a32a7a9ed9992a4743e9a16cc7226cf28d Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Sun, 16 Aug 2020 13:13:27 +0200 Subject: [PATCH 0451/1197] Update docs/stoploss.md Co-authored-by: Matthias --- docs/stoploss.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index 57d6c160e..7942b4c68 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -95,7 +95,7 @@ In summary: The stoploss will be adjusted to be always be 2% of the highest obse It is also possible to have a default stop loss, when you are in the red with your buy (buy - fee), but once you hit possitive result the system will utilize a new stop loss, which can have a different value. For example your default stop loss is -10%, but once you have more than 0% profit (example 0.1%) a different trailing stoploss will be used. -Both values require `trailing_stop` to be set to true and trailing_stop_positive with a value. +Both values require `trailing_stop` to be set to true and `trailing_stop_positive` with a value. ``` python stoploss = -0.10 From e30a38932f47406dde2cef8ec956358ff95bbf48 Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Sun, 16 Aug 2020 13:13:40 +0200 Subject: [PATCH 0452/1197] Update docs/stoploss.md Co-authored-by: Matthias --- docs/stoploss.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index 7942b4c68..e6cc59c83 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -107,7 +107,7 @@ For example, simplified math: * the bot buys an asset at a price of 100$ * the stop loss is defined at -10% -* the stop loss would get triggered once the asset dropps below 90$ +* the stop loss would get triggered once the asset drops below 90$ * assuming the asset now increases to 102$ * the stop loss will now be -2% of 102$ = 99,96$ * now the asset drops in value to 101$, the stop loss will still be 99,96$ and would trigger at 99,96$ From 4a0c988b678509cfe3fc61d87a015197b3d063a2 Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Sun, 16 Aug 2020 13:13:54 +0200 Subject: [PATCH 0453/1197] Update docs/stoploss.md Co-authored-by: Matthias --- docs/stoploss.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index e6cc59c83..142dfd2c9 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -109,8 +109,8 @@ For example, simplified math: * the stop loss is defined at -10% * the stop loss would get triggered once the asset drops below 90$ * assuming the asset now increases to 102$ -* the stop loss will now be -2% of 102$ = 99,96$ -* now the asset drops in value to 101$, the stop loss will still be 99,96$ and would trigger at 99,96$ +* the stop loss will now be -2% of 102$ = 99.96$ +* now the asset drops in value to 101$, the stop loss will still be 99.96$ and would trigger at 99.96$ The 0.02 would translate to a -2% stop loss. Before this, `stoploss` is used for the trailing stoploss. From 67e9721274aa2bd1e23654b0befadce93ade8ce0 Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Sun, 16 Aug 2020 13:14:50 +0200 Subject: [PATCH 0454/1197] Update docs/stoploss.md Co-authored-by: Matthias --- docs/stoploss.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index 142dfd2c9..b866a3efa 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -115,7 +115,7 @@ For example, simplified math: The 0.02 would translate to a -2% stop loss. Before this, `stoploss` is used for the trailing stoploss. -### Trailing stop loss only once the trade has reached a certain offset. +### Trailing stop loss only once the trade has reached a certain offset It is also possible to use a static stoploss until the offset is reached, and then trail the trade to take profits once the market turns. From 8b348fc247416b04149d62a378c43bb6aa4dea89 Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Sun, 16 Aug 2020 13:16:35 +0200 Subject: [PATCH 0455/1197] Update docs/stoploss.md Co-authored-by: Matthias --- docs/stoploss.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index b866a3efa..30d866fff 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -92,7 +92,7 @@ In summary: The stoploss will be adjusted to be always be 2% of the highest obse ### Trailing stop loss, custom positive loss -It is also possible to have a default stop loss, when you are in the red with your buy (buy - fee), but once you hit possitive result the system will utilize a new stop loss, which can have a different value. +It is also possible to have a default stop loss, when you are in the red with your buy (buy - fee), but once you hit positive result the system will utilize a new stop loss, which can have a different value. For example your default stop loss is -10%, but once you have more than 0% profit (example 0.1%) a different trailing stoploss will be used. Both values require `trailing_stop` to be set to true and `trailing_stop_positive` with a value. From 5091767276073528930d58b1f9d96b4d31da8133 Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Sun, 16 Aug 2020 13:17:01 +0200 Subject: [PATCH 0456/1197] Update docs/stoploss.md Co-authored-by: Matthias --- docs/stoploss.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index 30d866fff..ab3297ae9 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -141,7 +141,7 @@ For example, simplified math: * the bot buys an asset at a price of 100$ * the stop loss is defined at -10% -* the stop loss would get triggered once the asset dropps below 90$ +* the stop loss would get triggered once the asset drops below 90$ * stoploss will remain at 90$ unless asset increases to or above our configured offset * assuming the asset now increases to 103$ (where we have the offset configured) * the stop loss will now be -2% of 103$ = 100,94$ From 81a75c97cf6a17e7ff2683d9003696059993ba97 Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Sun, 16 Aug 2020 13:17:11 +0200 Subject: [PATCH 0457/1197] Update docs/stoploss.md Co-authored-by: Matthias --- docs/stoploss.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index ab3297ae9..2cee88748 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -144,8 +144,8 @@ For example, simplified math: * the stop loss would get triggered once the asset drops below 90$ * stoploss will remain at 90$ unless asset increases to or above our configured offset * assuming the asset now increases to 103$ (where we have the offset configured) -* the stop loss will now be -2% of 103$ = 100,94$ -* now the asset drops in value to 101$, the stop loss will still be 100,94$ and would trigger at 100,94$ +* the stop loss will now be -2% of 103$ = 100.94$ +* now the asset drops in value to 101$, the stop loss will still be 100.94$ and would trigger at 100.94$ !!! Tip Make sure to have this value (`trailing_stop_positive_offset`) lower than minimal ROI, otherwise minimal ROI will apply first and sell the trade. From ddba999fe2a25befa5ad6705956a6c9bc1776eb8 Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Sun, 16 Aug 2020 13:44:32 +0200 Subject: [PATCH 0458/1197] Update stoploss.md --- docs/stoploss.md | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index 2cee88748..283826708 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -16,13 +16,14 @@ Those stoploss modes can be *on exchange* or *off exchange*. If the stoploss is In case of stoploss on exchange there is another parameter called `stoploss_on_exchange_interval`. This configures the interval in seconds at which the bot will check the stoploss and update it if necessary. For example, assuming the stoploss is on exchange, and trailing stoploss is enabled, and the market is going up, then the bot automatically cancels the previous stoploss order and puts a new one with a stop value higher than the previous stoploss order. -The bot cannot do this every 5 seconds (at each iteration), otherwise it would get banned by the exchange. +The bot cannot do these every 5 seconds (at each iteration), otherwise it would get banned by the exchange. So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute). This same logic will reapply a stoploss order on the exchange should you cancel it accidentally. !!! Note Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market) and FTX (stop limit and stop-market) as of now. - Do not set too low stoploss value if using stop loss on exhange! + Do not set too low stoploss value if using stop loss on exhange! +* If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work Stop loss on exchange is controlled with this value (default False): @@ -88,12 +89,15 @@ For example, simplified math: * the stop loss will now be -10% of 102$ = 91.8$ * now the asset drops in value to 101$, the stop loss will still be 91.8$ and would trigger at 91,8$. -In summary: The stoploss will be adjusted to be always be 2% of the highest observed price. +In summary: The stoploss will be adjusted to be always be -10% of the highest observed price. ### Trailing stop loss, custom positive loss It is also possible to have a default stop loss, when you are in the red with your buy (buy - fee), but once you hit positive result the system will utilize a new stop loss, which can have a different value. -For example your default stop loss is -10%, but once you have more than 0% profit (example 0.1%) a different trailing stoploss will be used. +For example, your default stop loss is -10%, but once you have more than 0% profit (example 0.1%) a different trailing stoploss will be used. + +!!! Note + If you want the stoploss to only be changed when you break even of making a profit (what most users want) please refere to next section with [offset enabled](#Trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). Both values require `trailing_stop` to be set to true and `trailing_stop_positive` with a value. @@ -109,7 +113,7 @@ For example, simplified math: * the stop loss is defined at -10% * the stop loss would get triggered once the asset drops below 90$ * assuming the asset now increases to 102$ -* the stop loss will now be -2% of 102$ = 99.96$ +* the stop loss will now be -2% of 102$ = 99.96$ (99.96$ stop loss will be locked in and will follow asset price increasements with -2%) * now the asset drops in value to 101$, the stop loss will still be 99.96$ and would trigger at 99.96$ The 0.02 would translate to a -2% stop loss. From f8efb87a676d872e950a496e52ef0de88d32216c Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Sun, 16 Aug 2020 14:57:53 +0200 Subject: [PATCH 0459/1197] Update docs/stoploss.md Co-authored-by: Matthias --- docs/stoploss.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index 283826708..5a8a553e7 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -87,7 +87,7 @@ For example, simplified math: * the stop loss would get triggered once the asset drops below 90$ * assuming the asset now increases to 102$ * the stop loss will now be -10% of 102$ = 91.8$ -* now the asset drops in value to 101$, the stop loss will still be 91.8$ and would trigger at 91,8$. +* now the asset drops in value to 101$, the stop loss will still be 91.8$ and would trigger at 91.8$. In summary: The stoploss will be adjusted to be always be -10% of the highest observed price. From bd308889fcb49ba4decb7dc969d6b3886e3e49fc Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Sun, 16 Aug 2020 14:58:06 +0200 Subject: [PATCH 0460/1197] Update docs/stoploss.md Co-authored-by: Matthias --- docs/stoploss.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index 5a8a553e7..d0b7f654d 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -97,7 +97,7 @@ It is also possible to have a default stop loss, when you are in the red with yo For example, your default stop loss is -10%, but once you have more than 0% profit (example 0.1%) a different trailing stoploss will be used. !!! Note - If you want the stoploss to only be changed when you break even of making a profit (what most users want) please refere to next section with [offset enabled](#Trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). + If you want the stoploss to only be changed when you break even of making a profit (what most users want) please refer to next section with [offset enabled](#Trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). Both values require `trailing_stop` to be set to true and `trailing_stop_positive` with a value. From 4619a50097074ff5686ecbff47594092fb169b5a Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Mon, 17 Aug 2020 02:07:25 +0200 Subject: [PATCH 0461/1197] Update configuration.md --- docs/configuration.md | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index a200d6411..ad18ac713 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -55,9 +55,9 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `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 as ratio 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 as ratio of the stoploss used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
    **Datatype:** Float (as ratio) -| `trailing_stop` | Enables trailing stoploss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
    **Datatype:** Boolean -| `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
    **Datatype:** Float -| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `0.0` (no offset).*
    **Datatype:** Float +| `trailing_stop` | Enables trailing stoploss (based on `stoploss` in either configuration or strategy file). More details in the [stoploss documentation](stoploss.md#trailing-stop-loss). [Strategy Override](#parameters-in-the-strategy).
    **Datatype:** Boolean +| `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-custom-positive-loss). [Strategy Override](#parameters-in-the-strategy).
    **Datatype:** Float +| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `0.0` (no offset).*
    **Datatype:** Float | `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `false`.*
    **Datatype:** Boolean | `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).
    **Datatype:** Integer | `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).
    **Datatype:** Integer @@ -278,24 +278,13 @@ This allows to buy using limit orders, sell using limit-orders, and create stoplosses using market orders. It also allows to set the stoploss "on exchange" which means stoploss order would be placed immediately once the buy order is fulfilled. -If `stoploss_on_exchange` and `trailing_stop` are both set, then the bot will use `stoploss_on_exchange_interval` to check and update the stoploss on exchange periodically. -`order_types` can be set in the configuration file or in the strategy. + `order_types` set in the configuration file overwrites values set in the strategy as a whole, so you need to configure the whole `order_types` dictionary in one place. If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and `stoploss_on_exchange`) need to be present, otherwise the bot will fail to start. -`emergencysell` is an optional value, which defaults to `market` and is used when creating stoploss on exchange orders fails. -The below is the default which is used if this is not configured in either strategy or configuration file. - -Not all Exchanges support `stoploss_on_exchange`. If an exchange supports both limit and market stoploss orders, then the value of `stoploss` will be used to determine the stoploss type. - -If `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price. -`stoploss` defines the stop-price - and limit should be slightly below this. - -This defaults to 0.99 / 1% (configurable via `stoploss_on_exchange_limit_ratio`). -Calculation example: we bought the asset at 100$. -Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the stoploss will happen between 95$ and 94.05$. +For information on (`emergencysell`,`stoploss_on_exchange`,`stoploss_on_exchange_interval`,`stoploss_on_exchange_limit_ratio`) please see stop loss documentation [stop loss on exchange](stoploss.md) Syntax for Strategy: From 2a6faaae64f6e5e3a5d85a7cf60d4574031e7189 Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Mon, 17 Aug 2020 02:07:32 +0200 Subject: [PATCH 0462/1197] Update stoploss.md --- docs/stoploss.md | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index d0b7f654d..ffa125fa5 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -11,25 +11,47 @@ Most of the strategy files already include the optimal `stoploss` value. ## Stop Loss On-Exchange/Freqtrade -Those stoploss modes can be *on exchange* or *off exchange*. If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfully. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled. +Those stoploss modes can be *on exchange* or *off exchange*. -In case of stoploss on exchange there is another parameter called `stoploss_on_exchange_interval`. This configures the interval in seconds at which the bot will check the stoploss and update it if necessary. +These modes can be configured with these values: + +``` python + 'emergencysell': 'market', + 'stoploss_on_exchange': False + 'stoploss_on_exchange_interval': 60, + 'stoploss_on_exchange_limit_ratio': 0.99 +``` + +### stoploss_on_exchange +Enable or Disable stop loss on exchange. +If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfully. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled. + +If `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price. +`stoploss` defines the stop-price - and limit should be slightly below this. +If an exchange supports both limit and market stoploss orders, then the value of `stoploss` will be used to determine the stoploss type. For example, assuming the stoploss is on exchange, and trailing stoploss is enabled, and the market is going up, then the bot automatically cancels the previous stoploss order and puts a new one with a stop value higher than the previous stoploss order. + +### stoploss_on_exchange_interval +In case of stoploss on exchange there is another parameter called `stoploss_on_exchange_interval`. This configures the interval in seconds at which the bot will check the stoploss and update it if necessary. The bot cannot do these every 5 seconds (at each iteration), otherwise it would get banned by the exchange. So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute). This same logic will reapply a stoploss order on the exchange should you cancel it accidentally. +### emergencysell and stoploss_on_exchange_limit_ratio +`emergencysell` is an optional value, which defaults to `market` and is used when creating stop loss on exchange orders fails. +The below is the default which is used if this is not configured in either strategy or configuration file. + +This defaults to 0.99 / 1% (configurable via `stoploss_on_exchange_limit_ratio`). +Calculation example: we bought the asset at 100$. +Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the stoploss will happen between 95$ and 94.05$. + + !!! Note Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market) and FTX (stop limit and stop-market) as of now. Do not set too low stoploss value if using stop loss on exhange! * If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work -Stop loss on exchange is controlled with this value (default False): - -``` python - 'stoploss_on_exchange': False -``` Example from strategy file: @@ -37,6 +59,7 @@ Example from strategy file: order_types = { 'buy': 'limit', 'sell': 'limit', + 'emergencysell': 'market', 'stoploss': 'market', 'stoploss_on_exchange': True, 'stoploss_on_exchange_interval': 60, From d6ea442588a92493acaf9c9720e2f910028cc957 Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Mon, 17 Aug 2020 02:10:56 +0200 Subject: [PATCH 0463/1197] Update stoploss.md --- docs/stoploss.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index ffa125fa5..bbb0be566 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -49,7 +49,7 @@ Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the stoploss wi !!! Note Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market) and FTX (stop limit and stop-market) as of now. - Do not set too low stoploss value if using stop loss on exhange! + Do not set too low stoploss value if using stop loss on exchange! * If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work From da6672841a1a5e5c118718e1165c9ed7e9ac1765 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2020 06:24:49 +0000 Subject: [PATCH 0464/1197] Bump mkdocs-material from 5.5.3 to 5.5.7 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 5.5.3 to 5.5.7. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/5.5.3...5.5.7) Signed-off-by: dependabot[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 4068e364b..ab5aebb79 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==5.5.3 +mkdocs-material==5.5.7 mdx_truly_sane_lists==1.2 From 7af7fb261b015522ac92b51f2eea5eb468e15675 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2020 06:24:50 +0000 Subject: [PATCH 0465/1197] Bump pytest-cov from 2.10.0 to 2.10.1 Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.10.0 to 2.10.1. - [Release notes](https://github.com/pytest-dev/pytest-cov/releases) - [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.10.0...v2.10.1) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c02a439d3..e51713ceb 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,7 +10,7 @@ flake8-tidy-imports==4.1.0 mypy==0.782 pytest==6.0.1 pytest-asyncio==0.14.0 -pytest-cov==2.10.0 +pytest-cov==2.10.1 pytest-mock==3.2.0 pytest-random-order==1.0.4 From 988bff9eaec92c3553d4997be3e8b79a02471164 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2020 06:24:54 +0000 Subject: [PATCH 0466/1197] Bump ccxt from 1.32.88 to 1.33.18 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.32.88 to 1.33.18. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.32.88...1.33.18) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index b7e71eada..df11ea3de 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.32.88 +ccxt==1.33.18 SQLAlchemy==1.3.18 python-telegram-bot==12.8 arrow==0.15.8 From c8ddd5654adee8d69ee9243855c3cf483e0f6039 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2020 06:25:04 +0000 Subject: [PATCH 0467/1197] Bump prompt-toolkit from 3.0.5 to 3.0.6 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.5 to 3.0.6. - [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases) - [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG) - [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/compare/3.0.5...3.0.6) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index b7e71eada..2d7e9be3e 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -32,4 +32,4 @@ flask-cors==3.0.8 colorama==0.4.3 # Building config files interactively questionary==1.5.2 -prompt-toolkit==3.0.5 +prompt-toolkit==3.0.6 From 30a2df14cbb09e30fb12059467e951b73ce888e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2020 06:25:10 +0000 Subject: [PATCH 0468/1197] Bump coveralls from 2.1.1 to 2.1.2 Bumps [coveralls](https://github.com/coveralls-clients/coveralls-python) from 2.1.1 to 2.1.2. - [Release notes](https://github.com/coveralls-clients/coveralls-python/releases) - [Changelog](https://github.com/coveralls-clients/coveralls-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/coveralls-clients/coveralls-python/compare/2.1.1...2.1.2) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c02a439d3..41500764c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,7 +3,7 @@ -r requirements-plot.txt -r requirements-hyperopt.txt -coveralls==2.1.1 +coveralls==2.1.2 flake8==3.8.3 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 From ce15c55185f4c1e78ab084f26a3991c286062cd8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 17 Aug 2020 20:24:30 +0200 Subject: [PATCH 0469/1197] Add libffi-dev to rpi image --- Dockerfile.armhf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 45ed2dac9..5c5e4a885 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,7 +1,7 @@ FROM --platform=linux/arm/v7 python:3.7.7-slim-buster RUN apt-get update \ - && apt-get -y install curl build-essential libssl-dev libatlas3-base libgfortran5 sqlite3 \ + && apt-get -y install curl build-essential libssl-dev libffi-dev libatlas3-base libgfortran5 sqlite3 \ && apt-get clean \ && pip install --upgrade pip \ && echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf From aa866294cd7fb93ff25c89007b288a9c4d5e767b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 Aug 2020 14:02:22 +0200 Subject: [PATCH 0470/1197] Reformulate documentation --- docs/backtesting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index b1dcd5dba..149d1c104 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -213,7 +213,7 @@ Hence, keep in mind that your performance is an integral mix of all different el ### Sell reasons table The 2nd table contains a recap of sell reasons. -This table can tell you which area needs some additional work (e,g. all or many of the `sell_signal` trades are losses, so we should disable the sell-signal or work on improving that). +This table can tell you which area needs some additional work (e.g. all or many of the `sell_signal` trades are losses, so you should work on improving the sell signal, or consider disabling it). ### Left open trades table From 4eb17b4daf692041b7504a980edf84e0fd5b769a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 Aug 2020 15:20:37 +0200 Subject: [PATCH 0471/1197] Remove unneeded function --- freqtrade/data/btanalysis.py | 2 +- freqtrade/optimize/optimize_reports.py | 14 -------------- tests/optimize/test_optimize_reports.py | 19 +++++++++++++++++++ 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index cf6e18e64..972961b36 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -179,7 +179,7 @@ def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataF filters = [] if strategy: - filters = Trade.strategy == strategy + filters.append(Trade.strategy == strategy) trades = pd.DataFrame([(t.pair, t.open_date.replace(tzinfo=timezone.utc), diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 8e25d9d89..c69442d46 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -31,20 +31,6 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N file_dump_json(latest_filename, {'latest_backtest': str(filename.name)}) -def backtest_result_to_list(results: DataFrame) -> List[List]: - """ - Converts a list of Backtest-results to list - :param results: Dataframe containing results for one strategy - :return: List of Lists containing the trades - """ - # Return 0 as "index" for compatibility reasons (for now) - # TODO: Evaluate if we can remove this - return [[t.pair, t.profit_percent, t.open_date.timestamp(), - t.close_date.timestamp(), 0, t.trade_duration, - t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value] - for index, t in results.iterrows()] - - def _get_line_floatfmt() -> List[str]: """ Generate floatformat (goes in line with _generate_result_line()) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 2fab4578c..e5d98ca43 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -146,6 +146,25 @@ def test_generate_backtest_stats(default_conf, testdatadir): filename1.unlink() +def test_store_backtest_stats(testdatadir, mocker): + + dump_mock = mocker.patch('freqtrade.optimize.optimize_reports.file_dump_json') + + store_backtest_stats(testdatadir, {}) + + assert dump_mock.call_count == 2 + assert isinstance(dump_mock.call_args_list[0][0][0], Path) + assert str(dump_mock.call_args_list[0][0][0]).startswith(str(testdatadir/'backtest-result')) + + dump_mock.reset_mock() + filename = testdatadir / 'testresult.json' + store_backtest_stats(filename, {}) + assert dump_mock.call_count == 2 + assert isinstance(dump_mock.call_args_list[0][0][0], Path) + # result will be testdatadir / testresult-.json + assert str(dump_mock.call_args_list[0][0][0]).startswith(str(testdatadir / 'testresult')) + + def test_generate_pair_metrics(default_conf, mocker): results = pd.DataFrame( From 668d167adcb103309c44e8d87be97133fc7fbe87 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 Aug 2020 16:15:24 +0200 Subject: [PATCH 0472/1197] Add docstring to store_backtest_stats --- freqtrade/optimize/optimize_reports.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index c69442d46..bf4e518ba 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -16,7 +16,13 @@ logger = logging.getLogger(__name__) def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> None: - + """ + Stores backtest results + :param recordfilename: Path object, which can either be a filename or a directory. + Filenames will be appended with a timestamp right before the suffix + while for diectories, /backtest-result-.json will be used as filename + :param stats: Dataframe containing the backtesting statistics + """ if recordfilename.is_dir(): filename = (recordfilename / f'backtest-result-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}.json') From 9982ad2f365b715f8bf2dd225bdcff509c6d8030 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 Aug 2020 16:59:24 +0200 Subject: [PATCH 0473/1197] Add profit to backtest summary output --- docs/backtesting.md | 1 + freqtrade/optimize/optimize_reports.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/docs/backtesting.md b/docs/backtesting.md index 149d1c104..20e69f52f 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -165,6 +165,7 @@ A backtesting result will look like that: | Total trades | 429 | | First trade | 2019-01-01 18:30:00 | | First trade Pair | EOS/USDT | +| Total Profit % | 152.41% | | Trades per day | 3.575 | | Best day | 25.27% | | Worst day | -30.67% | diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index bf4e518ba..0799ebb23 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -249,6 +249,9 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], 'sell_reason_summary': sell_reason_stats, 'left_open_trades': left_open_results, 'total_trades': len(results), + 'profit_mean': results['profit_percent'].mean(), + 'profit_total': results['profit_percent'].sum(), + 'profit_total_abs': results['profit_abs'].sum(), 'backtest_start': min_date.datetime, 'backtest_start_ts': min_date.timestamp * 1000, 'backtest_end': max_date.datetime, @@ -372,6 +375,7 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Total trades', strat_results['total_trades']), ('First trade', min_trade['open_date'].strftime(DATETIME_PRINT_FORMAT)), ('First trade Pair', min_trade['pair']), + ('Total Profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"), ('Trades per day', strat_results['trades_per_day']), ('Best day', f"{round(strat_results['backtest_best_day'] * 100, 2)}%"), ('Worst day', f"{round(strat_results['backtest_worst_day'] * 100, 2)}%"), From d8e1f97465922f71128a4cffe37c582ea7721c00 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 Aug 2020 19:44:44 +0200 Subject: [PATCH 0474/1197] Fix documentation typo --- docs/backtesting.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 20e69f52f..84911568b 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -219,7 +219,7 @@ This table can tell you which area needs some additional work (e.g. all or many ### Left open trades table The 3rd table contains all trades the bot had to `forcesell` at the end of the backtesting period to present you the full picture. -This is necessary to simulate realistic behaviour, since the backtest period has to end at some point, while realistically, you could leave the bot running forever. +This is necessary to simulate realistic behavior, since the backtest period has to end at some point, while realistically, you could leave the bot running forever. These trades are also included in the first table, but are also shown separately in this table for clarity. ### Summary metrics @@ -236,6 +236,7 @@ It contains some useful key metrics about performance of your strategy on backte | Total trades | 429 | | First trade | 2019-01-01 18:30:00 | | First trade Pair | EOS/USDT | +| Total Profit % | 152.41% | | Trades per day | 3.575 | | Best day | 25.27% | | Worst day | -30.67% | @@ -254,11 +255,12 @@ It contains some useful key metrics about performance of your strategy on backte - `First trade`: First trade entered. - `First trade pair`: Which pair was part of the first trade. - `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option). +- `Total Profit %`: Total profit per stake amount. Aligned to the TOTAL column of the first table. - `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). - `Best day` / `Worst day`: Best and worst day based on daily profit. - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. - `Max Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced). -- `Drawdown Start` / `Drawdown End`: Start and end datetimes for this largest drawdown (can also be visualized via the `plot-dataframe` subcommand). +- `Drawdown Start` / `Drawdown End`: Start and end datetimes for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command). - `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column. ### Assumptions made by backtesting From 375e671aafc508e91c0ab4fd9b33f74d2eba7400 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 Aug 2020 20:12:14 +0200 Subject: [PATCH 0475/1197] Move formatting of /daily to telegram so /daily can return numbers in the API --- freqtrade/rpc/rpc.py | 10 ++++------ freqtrade/rpc/telegram.py | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index f4e20c16f..7be49b948 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -224,22 +224,20 @@ class RPC: ]).order_by(Trade.close_date).all() curdayprofit = sum(trade.close_profit_abs for trade in trades) profit_days[profitday] = { - 'amount': f'{curdayprofit:.8f}', + 'amount': curdayprofit, 'trades': len(trades) } data = [ { 'date': key, - 'abs_profit': f'{float(value["amount"]):.8f}', - 'fiat_value': '{value:.3f}'.format( - value=self._fiat_converter.convert_amount( + 'abs_profit': value["amount"], + 'fiat_value': self._fiat_converter.convert_amount( value['amount'], stake_currency, fiat_display_currency ) if self._fiat_converter else 0, - ), - 'trade_count': f'{value["trades"]}', + 'trade_count': value["trades"], } for key, value in profit_days.items() ] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index f1d3cde21..809deb765 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -305,8 +305,8 @@ class Telegram(RPC): ) stats_tab = tabulate( [[day['date'], - f"{day['abs_profit']} {stats['stake_currency']}", - f"{day['fiat_value']} {stats['fiat_display_currency']}", + f"{day['abs_profit']:.8f} {stats['stake_currency']}", + f"{day['fiat_value']:.3f} {stats['fiat_display_currency']}", f"{day['trade_count']} trades"] for day in stats['data']], headers=[ 'Day', From e206cc9c216bbf7f140797c001692f41c0d7fd48 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 18 Aug 2020 20:15:41 +0200 Subject: [PATCH 0476/1197] Adjust tests --- tests/rpc/test_rpc.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 9bbd34672..164c825ba 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -255,11 +255,11 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, assert days['fiat_display_currency'] == default_conf['fiat_display_currency'] for day in days['data']: # [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD'] - assert (day['abs_profit'] == '0.00000000' or - day['abs_profit'] == '0.00006217') + assert (day['abs_profit'] == 0.0 or + day['abs_profit'] == 0.00006217) - assert (day['fiat_value'] == '0.000' or - day['fiat_value'] == '0.767') + assert (day['fiat_value'] == 0.0 or + day['fiat_value'] == 0.76748865) # ensure first day is current date assert str(days['data'][0]['date']) == str(datetime.utcnow().date()) From 55c6e56762dc3d2523415641d1d25d903919de0c Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Wed, 19 Aug 2020 23:07:03 +0200 Subject: [PATCH 0477/1197] Update stoploss.md --- docs/stoploss.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index bbb0be566..2595a3f55 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -22,7 +22,7 @@ These modes can be configured with these values: 'stoploss_on_exchange_limit_ratio': 0.99 ``` -### stoploss_on_exchange +### stoploss_on_exchange and stoploss_on_exchange_limit_ratio Enable or Disable stop loss on exchange. If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfully. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled. @@ -30,6 +30,10 @@ If `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the st `stoploss` defines the stop-price - and limit should be slightly below this. If an exchange supports both limit and market stoploss orders, then the value of `stoploss` will be used to determine the stoploss type. +If `stoploss_on_exchange` fails to fill we can use `stoploss_on_exchange_limit_ratio` that defaults to 0.99 / 1% to perform an [emergency sell](#emergencysell). +Calculation example: we bought the asset at 100$. +Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the stoploss(emergency sell) will happen between 95$ and 94.05$. + For example, assuming the stoploss is on exchange, and trailing stoploss is enabled, and the market is going up, then the bot automatically cancels the previous stoploss order and puts a new one with a stop value higher than the previous stoploss order. ### stoploss_on_exchange_interval @@ -38,15 +42,10 @@ The bot cannot do these every 5 seconds (at each iteration), otherwise it would So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute). This same logic will reapply a stoploss order on the exchange should you cancel it accidentally. -### emergencysell and stoploss_on_exchange_limit_ratio +### emergencysell `emergencysell` is an optional value, which defaults to `market` and is used when creating stop loss on exchange orders fails. The below is the default which is used if this is not configured in either strategy or configuration file. -This defaults to 0.99 / 1% (configurable via `stoploss_on_exchange_limit_ratio`). -Calculation example: we bought the asset at 100$. -Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the stoploss will happen between 95$ and 94.05$. - - !!! Note Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market) and FTX (stop limit and stop-market) as of now. Do not set too low stoploss value if using stop loss on exchange! From bca24c8b6b584e012e573b8aeb393a1e8e54ab93 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Aug 2020 19:35:40 +0200 Subject: [PATCH 0478/1197] Clarify hyperopt dataprovider usage --- docs/strategy-customization.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 73d085abd..be08faa2d 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -366,8 +366,8 @@ All methods return `None` in case of failure (do not raise an exception). Please always check the mode of operation to select the correct method to get data (samples see below). !!! Warning "Hyperopt" - Dataprovider is available during hyperopt, however it can only be used in `populate_indicators()`. - It is not available in `populate_buy()` and `populate_sell()` methods. + Dataprovider is available during hyperopt, however it can only be used in `populate_indicators()` within a strategy. + It is not available in `populate_buy()` and `populate_sell()` methods, nor in `populate_indicators()`, if this method located in the hyperopt file. ### Possible options for DataProvider From f5a9001dc05effd378f07de663a30a88fa6201ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Aug 2020 19:51:36 +0200 Subject: [PATCH 0479/1197] Handle backtest results without any trades --- freqtrade/optimize/optimize_reports.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 0799ebb23..0681eed7b 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -31,6 +31,7 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N recordfilename.parent, f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}' ).with_suffix(recordfilename.suffix) + print(stats) file_dump_json(filename, stats) latest_filename = Path.joinpath(filename.parent, LAST_BT_RESULT_FN) @@ -185,6 +186,16 @@ def generate_edge_table(results: dict) -> str: def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: + if len(results) == 0: + return { + 'backtest_best_day': 0, + 'backtest_worst_day': 0, + 'winning_days': 0, + 'draw_days': 0, + 'losing_days': 0, + 'winner_holding_avg': timedelta(), + 'loser_holding_avg': timedelta(), + } daily_profit = results.resample('1d', on='close_date')['profit_percent'].sum() worst = min(daily_profit) best = max(daily_profit) @@ -249,7 +260,7 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], 'sell_reason_summary': sell_reason_stats, 'left_open_trades': left_open_results, 'total_trades': len(results), - 'profit_mean': results['profit_percent'].mean(), + 'profit_mean': results['profit_percent'].mean() if len(results) > 0 else 0, 'profit_total': results['profit_percent'].sum(), 'profit_total_abs': results['profit_abs'].sum(), 'backtest_start': min_date.datetime, @@ -258,7 +269,7 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], 'backtest_end_ts': max_date.timestamp * 1000, 'backtest_days': backtest_days, - 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else None, + 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0, 'market_change': market_change, 'pairlist': list(btdata.keys()), 'stake_amount': config['stake_amount'], From 4f1179d85c3d3aa11a76768c8aaa6af922157e96 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 20 Aug 2020 19:56:41 +0200 Subject: [PATCH 0480/1197] Test for empty case --- freqtrade/optimize/optimize_reports.py | 1 - tests/optimize/test_optimize_reports.py | 8 ++++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 0681eed7b..94729b6a5 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -31,7 +31,6 @@ def store_backtest_stats(recordfilename: Path, stats: Dict[str, DataFrame]) -> N recordfilename.parent, f'{recordfilename.stem}-{datetime.now().strftime("%Y-%m-%d_%H-%M-%S")}' ).with_suffix(recordfilename.suffix) - print(stats) file_dump_json(filename, stats) latest_filename = Path.joinpath(filename.parent, LAST_BT_RESULT_FN) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index e5d98ca43..4f62e2e23 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -204,6 +204,14 @@ def test_generate_daily_stats(testdatadir): assert res['winner_holding_avg'] == timedelta(seconds=1440) assert res['loser_holding_avg'] == timedelta(days=1, seconds=21420) + # Select empty dataframe! + res = generate_daily_stats(bt_data.loc[bt_data['open_date'] == '2000-01-01', :]) + assert isinstance(res, dict) + assert round(res['backtest_best_day'], 4) == 0.0 + assert res['winning_days'] == 0 + assert res['draw_days'] == 0 + assert res['losing_days'] == 0 + def test_text_table_sell_reason(default_conf): From 838985f6a06d7f1851282c30ef202eb8eeb214c3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Aug 2020 07:13:13 +0200 Subject: [PATCH 0481/1197] Don't reset open-order-id just yet it's needed to get the fees --- freqtrade/freqtradebot.py | 1 - tests/test_freqtradebot.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0dac4d888..fd5847a94 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -606,7 +606,6 @@ class FreqtradeBot: stake_amount = order['cost'] amount = safe_value_fallback(order, 'filled', 'amount') buy_limit_filled_price = safe_value_fallback(order, 'average', 'price') - order_id = None # in case of FOK the order may be filled immediately and fully elif order_status == 'closed': diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index a907ce5c2..9a3dcbd78 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1057,7 +1057,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order assert freqtrade.execute_buy(pair, stake_amount) trade = Trade.query.all()[3] assert trade - assert trade.open_order_id is None + assert trade.open_order_id == '555' assert trade.open_rate == 0.5 assert trade.stake_amount == 40.495905365 From 0b6014fae3aea892a85bc5e85a6c52f41796a652 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Aug 2020 07:17:52 +0200 Subject: [PATCH 0482/1197] update_trade_state should take the order id directly - not from the trade object --- freqtrade/freqtradebot.py | 29 +++++++++++++---------------- tests/test_freqtradebot.py | 18 +++++++++--------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index fd5847a94..929066c18 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -242,7 +242,7 @@ class FreqtradeBot: else: fo = self.exchange.fetch_order(order.order_id, order.ft_pair) - self.update_trade_state(order.trade, fo, sl_order=order.ft_order_side == 'stoploss') + self.update_trade_state(order.trade, order.order_id, fo) except ExchangeError: logger.warning(f"Error updating {order.order_id}") @@ -272,7 +272,7 @@ class FreqtradeBot: # No action for buy orders ... continue if fo: - self.update_trade_state(trade, fo, sl_order=order.ft_order_side == 'stoploss') + self.update_trade_state(trade, order.order_id, fo) except ExchangeError: logger.warning(f"Error updating {order.order_id}") @@ -634,7 +634,7 @@ class FreqtradeBot: # Update fees if order is closed if order_status == 'closed': - self.update_trade_state(trade, order) + self.update_trade_state(trade, order_id, order) Trade.session.add(trade) Trade.session.flush() @@ -878,7 +878,7 @@ class FreqtradeBot: # We check if stoploss order is fulfilled if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value - self.update_trade_state(trade, stoploss_order, sl_order=True) + self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order) # Lock pair for one candle to prevent immediate rebuys self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe'])) @@ -989,7 +989,7 @@ class FreqtradeBot: logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) continue - fully_cancelled = self.update_trade_state(trade, order) + fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order) if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and ( fully_cancelled @@ -1070,7 +1070,7 @@ class FreqtradeBot: # we need to fall back to the values from order if corder does not contain these keys. trade.amount = filled_amount trade.stake_amount = trade.amount * trade.open_rate - self.update_trade_state(trade, corder, trade.amount) + self.update_trade_state(trade, trade.open_order_id, corder, trade.amount) trade.open_order_id = None logger.info('Partial buy order timeout for %s.', trade) @@ -1208,7 +1208,7 @@ class FreqtradeBot: trade.sell_reason = sell_reason.value # In case of market sell orders the order can be closed immediately if order.get('status', 'unknown') == 'closed': - self.update_trade_state(trade, order) + self.update_trade_state(trade, trade.open_order_id, order) Trade.session.flush() # Lock pair for one candle to prevent immediate rebuys @@ -1305,20 +1305,17 @@ class FreqtradeBot: # Common update trade state methods # - def update_trade_state(self, trade: Trade, action_order: dict = None, - order_amount: float = None, sl_order: bool = False) -> bool: + def update_trade_state(self, trade: Trade, order_id: str, action_order: dict = None, + order_amount: float = None) -> bool: """ Checks trades with open orders and updates the amount if necessary Handles closing both buy and sell orders. :return: True if order has been cancelled without being filled partially, False otherwise """ - # Get order details for actual price per unit - if trade.open_order_id: - order_id = trade.open_order_id - elif trade.stoploss_order_id and sl_order: - order_id = trade.stoploss_order_id - else: - return False + if not order_id: + logger.warning(f'Orderid for trade {trade} is empty.') + False + # Update trade with order values logger.info('Found open order for %s', trade) try: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 9a3dcbd78..93b71f176 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1699,7 +1699,7 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No amount=11, ) # Add datetime explicitly since sqlalchemy defaults apply only once written to database - freqtrade.update_trade_state(trade) + freqtrade.update_trade_state(trade, '123') # Test amount not modified by fee-logic assert not log_has_re(r'Applying fee to .*', caplog) assert trade.open_order_id is None @@ -1709,14 +1709,14 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81) assert trade.amount != 90.81 # test amount modified by fee-logic - freqtrade.update_trade_state(trade) + freqtrade.update_trade_state(trade, '123') assert trade.amount == 90.81 assert trade.open_order_id is None trade.is_open = True trade.open_order_id = None # Assert we call handle_trade() if trade is feasible for execution - freqtrade.update_trade_state(trade) + freqtrade.update_trade_state(trade, '123') assert log_has_re('Found open order for.*', caplog) @@ -1741,7 +1741,7 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_ open_order_id="123456", is_open=True, ) - freqtrade.update_trade_state(trade, limit_buy_order) + freqtrade.update_trade_state(trade, '123456', limit_buy_order) assert trade.amount != amount assert trade.amount == limit_buy_order['amount'] @@ -1763,11 +1763,11 @@ def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_ open_rate=0.245441, fee_open=fee.return_value, fee_close=fee.return_value, - open_order_id="123456", + open_order_id='123456', is_open=True, open_date=arrow.utcnow().datetime, ) - freqtrade.update_trade_state(trade, limit_buy_order) + freqtrade.update_trade_state(trade, '123456', limit_buy_order) assert trade.amount != amount assert trade.amount == limit_buy_order['amount'] assert log_has_re(r'Applying fee on amount for .*', caplog) @@ -1787,7 +1787,7 @@ def test_update_trade_state_exception(mocker, default_conf, 'freqtrade.freqtradebot.FreqtradeBot.get_real_amount', side_effect=DependencyException() ) - freqtrade.update_trade_state(trade) + freqtrade.update_trade_state(trade, trade.open_order_id) assert log_has('Could not update trade amount: ', caplog) @@ -1802,7 +1802,7 @@ def test_update_trade_state_orderexception(mocker, default_conf, caplog) -> None # Test raise of OperationalException exception grm_mock = mocker.patch("freqtrade.freqtradebot.FreqtradeBot.get_real_amount", MagicMock()) - freqtrade.update_trade_state(trade) + freqtrade.update_trade_state(trade, trade.open_order_id) assert grm_mock.call_count == 0 assert log_has(f'Unable to fetch order {trade.open_order_id}: ', caplog) @@ -1834,7 +1834,7 @@ def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_orde order = Order.parse_from_ccxt_object(limit_sell_order_open, 'LTC/ETH', 'sell') trade.orders.append(order) assert order.status == 'open' - freqtrade.update_trade_state(trade, limit_sell_order) + freqtrade.update_trade_state(trade, trade.open_order_id, limit_sell_order) assert trade.amount == limit_sell_order['amount'] # Wallet needs to be updated after closing a limit-sell order to reenable buying assert wallet_mock.call_count == 1 From 3be14933d4cc048096177b3274d78dde47f9ed87 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Aug 2020 07:24:49 +0200 Subject: [PATCH 0483/1197] Add comment explaining update_open_orders --- freqtrade/freqtradebot.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 929066c18..3305bb584 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -134,6 +134,8 @@ class FreqtradeBot: # Adjust stoploss if it was changed Trade.stoploss_reinitialization(self.strategy.stoploss) + # Only update open orders on startup + # This will update the database after the initial migration self.update_open_orders() def process(self) -> None: From 357d7714ec9fa6da153c3ca1d87599ea2adc7419 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Aug 2020 07:31:22 +0200 Subject: [PATCH 0484/1197] Add docstring to update_trade_state --- freqtrade/freqtradebot.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 3305bb584..4a660d867 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1312,6 +1312,11 @@ class FreqtradeBot: """ Checks trades with open orders and updates the amount if necessary Handles closing both buy and sell orders. + :param trade: Trade object of the trade we're analyzing + :param order_id: Order-id of the order we're analyzing + :param action_order: Already aquired order object + :param order_amount: Order-amount - only used in case of partially cancelled buy order + TODO: Investigate if this is really needed, or covered by getting filled in here again. :return: True if order has been cancelled without being filled partially, False otherwise """ if not order_id: From fa0c8fa0b332e144556851cde5de218bca77f56d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Aug 2020 14:26:23 +0200 Subject: [PATCH 0485/1197] Readd note about windows hyperopt color output --- docs/hyperopt.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 9acb606c3..6330a1a5e 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -370,6 +370,9 @@ By default, hyperopt prints colorized results -- epochs with positive profit are You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. When `--print-all` is used, current best results are also colorized by default -- they are printed in bold (bright) style. This can also be switched off with the `--no-color` command line option. +!!! Note "Windows and color output" + Windows does not support color-output nativly, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL. + ### Understand Hyperopt ROI results If you are optimizing ROI (i.e. if optimization search-space contains 'all', 'default' or 'roi'), your result will look as follows and include a ROI table: From 3d93236709f93e0380659e074e2b44c518a1cc11 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Aug 2020 14:55:47 +0200 Subject: [PATCH 0486/1197] Remove unused import --- freqtrade/optimize/hyperopt.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 3e7e2ca57..b9db3c09a 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -12,7 +12,6 @@ import warnings from collections import OrderedDict from math import ceil from operator import itemgetter -from os import path from pathlib import Path from pprint import pformat from typing import Any, Dict, List, Optional From 0e368b16aba83de72328b52f1e867d6ee5c9bcd1 Mon Sep 17 00:00:00 2001 From: Fredrik81 Date: Fri, 21 Aug 2020 18:25:45 +0200 Subject: [PATCH 0487/1197] Update stoploss.md --- docs/stoploss.md | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index 2595a3f55..2518df846 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -6,8 +6,8 @@ For example, value `-0.10` will cause immediate sell if the profit dips below -1 Most of the strategy files already include the optimal `stoploss` value. !!! Info - All stoploss properties mentioned in this file can be set in the Strategy, or in the configuration. - Configuration values will override the strategy values. +* All stoploss properties mentioned in this file can be set in the Strategy, or in the configuration. +* Configuration values will override the strategy values. ## Stop Loss On-Exchange/Freqtrade @@ -22,35 +22,33 @@ These modes can be configured with these values: 'stoploss_on_exchange_limit_ratio': 0.99 ``` +!!! Note +* Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market) and FTX (stop limit and stop-market) as of now. +* Do not set too low stoploss value if using stop loss on exchange! +* If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work + ### stoploss_on_exchange and stoploss_on_exchange_limit_ratio Enable or Disable stop loss on exchange. If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfully. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled. -If `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price. -`stoploss` defines the stop-price - and limit should be slightly below this. -If an exchange supports both limit and market stoploss orders, then the value of `stoploss` will be used to determine the stoploss type. +If `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price. +`stoploss` defines the stop-price where the limit order is placed - and limit should be slightly below this. +If an exchange supports both limit and market stoploss orders, then the value of `stoploss` will be used to determine the stoploss type. -If `stoploss_on_exchange` fails to fill we can use `stoploss_on_exchange_limit_ratio` that defaults to 0.99 / 1% to perform an [emergency sell](#emergencysell). -Calculation example: we bought the asset at 100$. -Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the stoploss(emergency sell) will happen between 95$ and 94.05$. +Calculation example: we bought the asset at 100$. +Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the limit order fill can happen between 95$ and 94.05$. For example, assuming the stoploss is on exchange, and trailing stoploss is enabled, and the market is going up, then the bot automatically cancels the previous stoploss order and puts a new one with a stop value higher than the previous stoploss order. ### stoploss_on_exchange_interval -In case of stoploss on exchange there is another parameter called `stoploss_on_exchange_interval`. This configures the interval in seconds at which the bot will check the stoploss and update it if necessary. +In case of stoploss on exchange there is another parameter called `stoploss_on_exchange_interval`. This configures the interval in seconds at which the bot will check the stoploss and update it if necessary. The bot cannot do these every 5 seconds (at each iteration), otherwise it would get banned by the exchange. So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute). This same logic will reapply a stoploss order on the exchange should you cancel it accidentally. ### emergencysell `emergencysell` is an optional value, which defaults to `market` and is used when creating stop loss on exchange orders fails. -The below is the default which is used if this is not configured in either strategy or configuration file. - -!!! Note - Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market) and FTX (stop limit and stop-market) as of now. - Do not set too low stoploss value if using stop loss on exchange! -* If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work - +The below is the default which is used if not changed in strategy or configuration file. Example from strategy file: @@ -119,7 +117,7 @@ It is also possible to have a default stop loss, when you are in the red with yo For example, your default stop loss is -10%, but once you have more than 0% profit (example 0.1%) a different trailing stoploss will be used. !!! Note - If you want the stoploss to only be changed when you break even of making a profit (what most users want) please refer to next section with [offset enabled](#Trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). +* If you want the stoploss to only be changed when you break even of making a profit (what most users want) please refer to next section with [offset enabled](#Trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). Both values require `trailing_stop` to be set to true and `trailing_stop_positive` with a value. @@ -174,7 +172,7 @@ For example, simplified math: * now the asset drops in value to 101$, the stop loss will still be 100.94$ and would trigger at 100.94$ !!! Tip - Make sure to have this value (`trailing_stop_positive_offset`) lower than minimal ROI, otherwise minimal ROI will apply first and sell the trade. +* Make sure to have this value (`trailing_stop_positive_offset`) lower than minimal ROI, otherwise minimal ROI will apply first and sell the trade. ## Changing stoploss on open trades From 2d6bcbb454fe2c485148bfcf02a067cba2e57077 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Aug 2020 19:51:31 +0200 Subject: [PATCH 0488/1197] Fix small error in trades updating --- freqtrade/freqtradebot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4a660d867..c4147e7e4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -254,6 +254,8 @@ class FreqtradeBot: Try refinding a lost trade. Only used when InsufficientFunds appears on sell orders (stoploss or sell). Tries to walk the stored orders and sell them off eventually. + + TODO: maybe remove this method again. """ logger.info(f"Trying to refind lost order for {trade}") for order in trade.orders: @@ -1407,7 +1409,7 @@ class FreqtradeBot: """ fee-detection fallback to Trades. Parses result of fetch_my_trades to get correct fee. """ - trades = self.exchange.get_trades_for_order(trade.open_order_id, trade.pair, + trades = self.exchange.get_trades_for_order(order['id'], trade.pair, trade.open_date) if len(trades) == 0: From fc42d552ab63f0d6bfb0c7646e088561e6168cfc Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 21 Aug 2020 20:13:06 +0200 Subject: [PATCH 0489/1197] Convert logs to fstrings --- freqtrade/persistence/models.py | 6 +++--- tests/test_freqtradebot.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 3f918670a..31f40f713 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -379,15 +379,15 @@ class Trade(_DECL_BASE): self.open_rate = Decimal(safe_value_fallback(order, 'average', 'price')) self.amount = Decimal(safe_value_fallback(order, 'filled', 'amount')) self.recalc_open_trade_price() - logger.info('%s_BUY has been fulfilled for %s.', order_type.upper(), self) + logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.') self.open_order_id = None elif order_type in ('market', 'limit') and order['side'] == 'sell': self.close(safe_value_fallback(order, 'average', 'price')) - logger.info('%s_SELL has been fulfilled for %s.', order_type.upper(), self) + logger.info(f'{order_type.upper()}_SELL has been fulfilled for {self}.') elif order_type in ('stop_loss_limit', 'stop-loss', 'stop'): self.stoploss_order_id = None self.close_rate_requested = self.stop_loss - logger.info('%s is hit for %s.', order_type.upper(), self) + logger.info(f'{order_type.upper()} is hit for {self}.') self.close(order['average']) else: raise ValueError(f'Unknown order type: {order_type}') diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 93b71f176..aefaebad5 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1215,7 +1215,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, }) mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hit) assert freqtrade.handle_stoploss_on_exchange(trade) is True - assert log_has('STOP_LOSS_LIMIT is hit for {}.'.format(trade), caplog) + assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog) assert trade.stoploss_order_id is None assert trade.is_open is False From 39beb5c8375c8abe0fbce95c13f2346e7fe16bee Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Aug 2020 08:39:10 +0200 Subject: [PATCH 0490/1197] Add method to update fees on closed trades --- freqtrade/freqtradebot.py | 22 +++++++++++++++++++++- freqtrade/persistence/models.py | 24 +++++++++++++++++++++++- 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c4147e7e4..3783908a2 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -138,6 +138,8 @@ class FreqtradeBot: # This will update the database after the initial migration self.update_open_orders() + self.update_closed_trades_without_assigned_fees() + def process(self) -> None: """ Queries the persistence layer for open trades and handles them, @@ -148,6 +150,8 @@ class FreqtradeBot: # Check whether markets have to be reloaded and reload them when it's needed self.exchange.reload_markets() + self.update_closed_trades_without_assigned_fees() + # Query trades from persistence layer trades = Trade.get_open_trades() @@ -233,7 +237,8 @@ class FreqtradeBot: def update_open_orders(self): """ - Updates open orders based on order list kept in the database + Updates open orders based on order list kept in the database. + Mainly updates the state of orders - but may also close trades """ orders = Order.get_open_orders() logger.info(f"Updating {len(orders)} open orders.") @@ -249,6 +254,21 @@ class FreqtradeBot: except ExchangeError: logger.warning(f"Error updating {order.order_id}") + def update_closed_trades_without_assigned_fees(self): + """ + Update closed trades without close fees assigned. + Only works when Orders are in the database, otherwise the last orderid is unknown. + """ + trades: List[Trade] = Trade.get_sold_trades_without_assigned_fees() + for trade in trades: + + if not trade.is_open and not trade.fee_updated('sell'): + # Get sell fee + order = trade.select_order('sell', 'closed') + if order: + logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.") + self.update_trade_state(trade, order.order_id) + def refind_lost_order(self, trade): """ Try refinding a lost trade. diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 31f40f713..01d2286f9 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional import arrow from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, - String, create_engine, desc, func, inspect) + String, create_engine, desc, func, inspect, or_) from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import Query, relationship @@ -508,6 +508,17 @@ class Trade(_DECL_BASE): profit_ratio = (close_trade_price / self.open_trade_price) - 1 return float(f"{profit_ratio:.8f}") + def select_order(self, order_side: str, status: str): + """ + Returns latest order for this orderside and status + Returns None if nothing is found + """ + orders = [o for o in self.orders if o.side == order_side and o.status == status] + if len(orders) > 0: + return orders[-1] + else: + return None + @staticmethod def get_trades(trade_filter=None) -> Query: """ @@ -539,6 +550,17 @@ class Trade(_DECL_BASE): """ return Trade.get_trades(Trade.open_order_id.isnot(None)).all() + @staticmethod + def get_sold_trades_without_assigned_fees(): + """ + Returns all closed trades which don't have fees set correctly + """ + return Trade.get_trades([Trade.fee_close_currency.is_(None), + Trade.id == 100, + Trade.orders.any(), + Trade.is_open.is_(False), + ]).all() + @staticmethod def total_open_trades_stakes() -> float: """ From fc2104bfad5715d3a9db6e9a5644071dafce6c8f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Aug 2020 09:12:09 +0200 Subject: [PATCH 0491/1197] Fix bug with time when updating order_date --- freqtrade/persistence/models.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 01d2286f9..a5c047cf4 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -2,7 +2,7 @@ This module contains the class to persist trades into SQLite """ import logging -from datetime import datetime +from datetime import datetime, timezone from decimal import Decimal from typing import Any, Dict, List, Optional @@ -152,11 +152,13 @@ class Order(_DECL_BASE): self.remaining = order.get('remaining', self.remaining) self.cost = order.get('cost', self.cost) if 'timestamp' in order and order['timestamp'] is not None: - self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000) + self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc) if self.status in ('closed', 'canceled', 'cancelled'): self.ft_is_open = False - self.order_update_date = datetime.now() + if order.get('filled', 0) > 0: + self.order_filled_date = arrow.utcnow().datetime + self.order_update_date = arrow.utcnow().datetime @staticmethod def update_orders(orders: List['Order'], order: Dict[str, Any]): From f2b390a27113be687d488df18bd73c014380073d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Aug 2020 09:24:14 +0200 Subject: [PATCH 0492/1197] Add fetch_order_or_stoploss wrapper --- freqtrade/exchange/exchange.py | 11 +++++++++++ tests/exchange/test_exchange.py | 25 +++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 578d753a4..64d1a75de 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1039,6 +1039,17 @@ class Exchange: # Assign method to fetch_stoploss_order to allow easy overriding in other classes fetch_stoploss_order = fetch_order + def fetch_order_or_stoploss_order(self, order_id: str, pair: str, + stoploss_order: bool = False) -> Dict: + """ + Simple wrapper calling either fetch_order or fetch_stoploss_order depending on + the stoploss_order parameter + :param stoploss_order: If true, uses fetch_stoploss_order, otherwise fetch_order. + """ + if stoploss_order: + return self.fetch_stoploss_order(order_id, pair) + return self.fetch_order(order_id, pair) + @retrier def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: """ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index d0e303f5f..e68629d3d 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1936,6 +1936,31 @@ def test_fetch_stoploss_order(default_conf, mocker, exchange_name): order_id='_', pair='TKN/BTC') +def test_fetch_order_or_stoploss_order(default_conf, mocker): + exchange = get_patched_exchange(mocker, default_conf, id='binance') + fetch_order_mock = MagicMock() + fetch_stoploss_order_mock = MagicMock() + mocker.patch.multiple('freqtrade.exchange.Exchange', + fetch_order=fetch_order_mock, + fetch_stoploss_order=fetch_stoploss_order_mock, + ) + + exchange.fetch_order_or_stoploss_order('1234', 'ETH/BTC', False) + assert fetch_order_mock.call_count == 1 + assert fetch_order_mock.call_args_list[0][0][0] == '1234' + assert fetch_order_mock.call_args_list[0][0][1] == 'ETH/BTC' + assert fetch_stoploss_order_mock.call_count == 0 + + fetch_order_mock.reset_mock() + fetch_stoploss_order_mock.reset_mock() + + exchange.fetch_order_or_stoploss_order('1234', 'ETH/BTC', True) + assert fetch_order_mock.call_count == 0 + assert fetch_stoploss_order_mock.call_count == 1 + assert fetch_stoploss_order_mock.call_args_list[0][0][0] == '1234' + assert fetch_stoploss_order_mock.call_args_list[0][0][1] == 'ETH/BTC' + + @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_name(default_conf, mocker, exchange_name): exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) From 3b4446339e9df77044a99c55d5df9cb2a15ac227 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Aug 2020 09:30:25 +0200 Subject: [PATCH 0493/1197] Use fetch_order_or_stoploss order --- freqtrade/freqtradebot.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 3783908a2..ea1cb5322 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -138,6 +138,7 @@ class FreqtradeBot: # This will update the database after the initial migration self.update_open_orders() + # TODO: remove next call once testing is done - this is called on every iteration. self.update_closed_trades_without_assigned_fees() def process(self) -> None: @@ -244,10 +245,8 @@ class FreqtradeBot: logger.info(f"Updating {len(orders)} open orders.") for order in orders: try: - if order.ft_order_side == 'stoploss': - fo = self.exchange.fetch_stoploss_order(order.order_id, order.ft_pair) - else: - fo = self.exchange.fetch_order(order.order_id, order.ft_pair) + fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, + order.ft_order_side == 'stoploss') self.update_trade_state(order.trade, order.order_id, fo) @@ -257,7 +256,7 @@ class FreqtradeBot: def update_closed_trades_without_assigned_fees(self): """ Update closed trades without close fees assigned. - Only works when Orders are in the database, otherwise the last orderid is unknown. + Only acts when Orders are in the database, otherwise the last orderid is unknown. """ trades: List[Trade] = Trade.get_sold_trades_without_assigned_fees() for trade in trades: @@ -267,7 +266,8 @@ class FreqtradeBot: order = trade.select_order('sell', 'closed') if order: logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.") - self.update_trade_state(trade, order.order_id) + self.update_trade_state(trade, order.order_id, + order.ft_order_side == 'stoploss') def refind_lost_order(self, trade): """ @@ -282,13 +282,13 @@ class FreqtradeBot: logger.info(f"Trying to refind {order}") fo = None try: + fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, + order.ft_order_side == 'stoploss') if order.ft_order_side == 'stoploss': - fo = self.exchange.fetch_stoploss_order(order.order_id, order.ft_pair) if fo and fo['status'] == 'open': # Assume this as the open stoploss order trade.stoploss_order_id = order.order_id elif order.ft_order_side == 'sell': - fo = self.exchange.fetch_order(order.order_id, order.ft_pair) if fo and fo['status'] == 'open': # Assume this as the open order trade.open_order_id = order.order_id @@ -296,7 +296,8 @@ class FreqtradeBot: # No action for buy orders ... continue if fo: - self.update_trade_state(trade, order.order_id, fo) + self.update_trade_state(trade, order.order_id, fo, + order.ft_order_side == 'stoploss') except ExchangeError: logger.warning(f"Error updating {order.order_id}") @@ -902,7 +903,8 @@ class FreqtradeBot: # We check if stoploss order is fulfilled if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value - self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order) + self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, + stoploss_order=True) # Lock pair for one candle to prevent immediate rebuys self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe'])) @@ -1330,7 +1332,7 @@ class FreqtradeBot: # def update_trade_state(self, trade: Trade, order_id: str, action_order: dict = None, - order_amount: float = None) -> bool: + order_amount: float = None, stoploss_order: bool = False) -> bool: """ Checks trades with open orders and updates the amount if necessary Handles closing both buy and sell orders. @@ -1348,7 +1350,9 @@ class FreqtradeBot: # Update trade with order values logger.info('Found open order for %s', trade) try: - order = action_order or self.exchange.fetch_order(order_id, trade.pair) + order = action_order or self.exchange.fetch_order_or_stoploss_order(order_id, + trade.pair, + stoploss_order) except InvalidOrderException as exception: logger.warning('Unable to fetch order %s: %s', order_id, exception) return False From 637147f89c59a5fe431cd6d0f91f54345922c875 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Aug 2020 09:33:35 +0200 Subject: [PATCH 0494/1197] Update sql cheatsheet parentheses --- docs/sql_cheatsheet.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index 748b16928..cf785ced6 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -110,7 +110,7 @@ SET is_open=0, close_date=, close_rate=, close_profit = close_rate / open_rate - 1, - close_profit_abs = (amount * * (1 - fee_close) - (amount * (open_rate * 1 - fee_open))), + close_profit_abs = (amount * * (1 - fee_close) - (amount * (open_rate * (1 - fee_open)))), sell_reason= WHERE id=; ``` @@ -123,7 +123,7 @@ SET is_open=0, close_date='2020-06-20 03:08:45.103418', close_rate=0.19638016, close_profit=0.0496, - close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * open_rate * (1 - fee_open))), + close_profit_abs = (amount * 0.19638016 * (1 - fee_close) - (amount * (open_rate * (1 - fee_open)))), sell_reason='force_sell' WHERE id=31; ``` From fd33282eb1ecbd954c2d757b3e4efef9f174365a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Aug 2020 15:48:00 +0200 Subject: [PATCH 0495/1197] Add handle_insufficient exception --- freqtrade/freqtradebot.py | 31 +++++++++++++++++++++++++------ freqtrade/persistence/models.py | 7 ++++--- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ea1cb5322..2656d3d3a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -269,6 +269,27 @@ class FreqtradeBot: self.update_trade_state(trade, order.order_id, order.ft_order_side == 'stoploss') + def handle_insufficient_funds(self, trade: Trade): + """ + """ + sell_order = trade.select_order('sell', None) + if sell_order: + self.refind_lost_order(trade) + else: + self.reupdate_buy_order_fees(trade) + + # See if we ever opened a sell order for this + # If not, try update buy fees + + def reupdate_buy_order_fees(self, trade: Trade): + """ + """ + logger.info(f"Trying to reupdate buy fees for {trade}") + order = trade.select_order('buy', 'closed') + if order: + logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") + self.update_trade_state(trade, order.order_id) + def refind_lost_order(self, trade): """ Try refinding a lost trade. @@ -864,9 +885,8 @@ class FreqtradeBot: return True except InsufficientFundsError as e: logger.warning(f"Unable to place stoploss order {e}.") - # Try refinding stoploss order - # TODO: Currently disabled to allow testing without this first - # self.refind_lost_order(trade) + # Try to figure out what went wrong + self.handle_insufficient_funds(trade) except InvalidOrderException as e: trade.stoploss_order_id = None @@ -1221,9 +1241,8 @@ class FreqtradeBot: ) except InsufficientFundsError as e: logger.warning(f"Unable to place order {e}.") - # Try refinding "lost" orders - # TODO: Currently disabled to allow testing without this first - # self.refind_lost_order(trade) + # Try to figure out what went wrong + self.handle_insufficient_funds(trade) return False order_obj = Order.parse_from_ccxt_object(order, trade.pair, 'sell') diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a5c047cf4..d66108fce 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -510,12 +510,14 @@ class Trade(_DECL_BASE): profit_ratio = (close_trade_price / self.open_trade_price) - 1 return float(f"{profit_ratio:.8f}") - def select_order(self, order_side: str, status: str): + def select_order(self, order_side: str, status: Optional[str]): """ Returns latest order for this orderside and status Returns None if nothing is found """ - orders = [o for o in self.orders if o.side == order_side and o.status == status] + orders = [o for o in self.orders if o.side == order_side] + if status: + orders = [o for o in orders if o.status == status] if len(orders) > 0: return orders[-1] else: @@ -558,7 +560,6 @@ class Trade(_DECL_BASE): Returns all closed trades which don't have fees set correctly """ return Trade.get_trades([Trade.fee_close_currency.is_(None), - Trade.id == 100, Trade.orders.any(), Trade.is_open.is_(False), ]).all() From 11e69bdd65b5951fbf4bfb33678cb3c90e7649e5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Aug 2020 15:48:42 +0200 Subject: [PATCH 0496/1197] Update open trades too --- freqtrade/freqtradebot.py | 21 ++++++++++++++------- freqtrade/persistence/models.py | 12 +++++++++++- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2656d3d3a..5e782a353 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -267,10 +267,20 @@ class FreqtradeBot: if order: logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.") self.update_trade_state(trade, order.order_id, - order.ft_order_side == 'stoploss') + stoploss_order=order.ft_order_side == 'stoploss') + + trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() + for trade in trades: + if trade.is_open and not trade.fee_updated('buy'): + order = trade.select_order('buy', 'closed') + if order: + logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") + self.update_trade_state(trade, order.order_id) def handle_insufficient_funds(self, trade: Trade): """ + Determine if we ever opened a sell order for this trade. + If not, try update buy fees - otherwise "refind" the open order we obviously lost. """ sell_order = trade.select_order('sell', None) if sell_order: @@ -278,11 +288,10 @@ class FreqtradeBot: else: self.reupdate_buy_order_fees(trade) - # See if we ever opened a sell order for this - # If not, try update buy fees - def reupdate_buy_order_fees(self, trade: Trade): """ + Get buy order from database, and try to reupdate. + Handles trades where the initial fee-update did not work. """ logger.info(f"Trying to reupdate buy fees for {trade}") order = trade.select_order('buy', 'closed') @@ -295,8 +304,6 @@ class FreqtradeBot: Try refinding a lost trade. Only used when InsufficientFunds appears on sell orders (stoploss or sell). Tries to walk the stored orders and sell them off eventually. - - TODO: maybe remove this method again. """ logger.info(f"Trying to refind lost order for {trade}") for order in trade.orders: @@ -318,7 +325,7 @@ class FreqtradeBot: continue if fo: self.update_trade_state(trade, order.order_id, fo, - order.ft_order_side == 'stoploss') + stoploss_order=order.ft_order_side == 'stoploss') except ExchangeError: logger.warning(f"Error updating {order.order_id}") diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index d66108fce..f9eeedd4b 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -8,7 +8,7 @@ from typing import Any, Dict, List, Optional import arrow from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, - String, create_engine, desc, func, inspect, or_) + String, create_engine, desc, func, inspect) from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import Query, relationship @@ -554,6 +554,16 @@ class Trade(_DECL_BASE): """ return Trade.get_trades(Trade.open_order_id.isnot(None)).all() + @staticmethod + def get_open_trades_without_assigned_fees(): + """ + Returns all open trades which don't have open fees set correctly + """ + return Trade.get_trades([Trade.fee_open_currency.is_(None), + Trade.orders.any(), + Trade.is_open.is_(True), + ]).all() + @staticmethod def get_sold_trades_without_assigned_fees(): """ From 3d7e800ff2e00c2e8e580d2868b07d6cf21fbb88 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Aug 2020 16:08:54 +0200 Subject: [PATCH 0497/1197] Remove test code --- freqtrade/freqtradebot.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5e782a353..6d2f9ddcc 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -138,9 +138,6 @@ class FreqtradeBot: # This will update the database after the initial migration self.update_open_orders() - # TODO: remove next call once testing is done - this is called on every iteration. - self.update_closed_trades_without_assigned_fees() - def process(self) -> None: """ Queries the persistence layer for open trades and handles them, From 674b510d2345ebcb7a8207588b44a1a6b25d9465 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 22 Aug 2020 17:35:42 +0200 Subject: [PATCH 0498/1197] Parametrize fetch_order retry counts --- freqtrade/exchange/common.py | 4 ++++ freqtrade/exchange/exchange.py | 5 +++-- freqtrade/exchange/ftx.py | 4 ++-- tests/exchange/test_exchange.py | 22 ++++++++++++---------- tests/exchange/test_ftx.py | 5 ++--- tests/exchange/test_kraken.py | 2 -- 6 files changed, 23 insertions(+), 19 deletions(-) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 3bba9be72..539bcef22 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -9,7 +9,11 @@ from freqtrade.exceptions import (DDosProtection, RetryableOrderError, logger = logging.getLogger(__name__) +# Maximum default retry count. +# Functions are always called RETRY_COUNT + 1 times (for the original call) API_RETRY_COUNT = 4 +API_FETCH_ORDER_RETRY_COUNT = 3 + BAD_EXCHANGES = { "bitmex": "Various reasons.", "bitstamp": "Does not provide history. " diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 64d1a75de..3b2b56d13 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -23,7 +23,8 @@ from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, RetryableOrderError, TemporaryError) -from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async +from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, + BAD_EXCHANGES, retrier, retrier_async) from freqtrade.misc import deep_merge_dicts, safe_value_fallback2 CcxtModuleType = Any @@ -1010,7 +1011,7 @@ class Exchange: return order - @retrier(retries=5) + @retrier(retries=API_FETCH_ORDER_RETRY_COUNT) def fetch_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: try: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 27051a945..39100d0b7 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -8,7 +8,7 @@ from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange -from freqtrade.exchange.common import retrier +from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT, retrier logger = logging.getLogger(__name__) @@ -78,7 +78,7 @@ class Ftx(Exchange): except ccxt.BaseError as e: raise OperationalException(e) from e - @retrier(retries=5) + @retrier(retries=API_FETCH_ORDER_RETRY_COUNT) def fetch_stoploss_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: try: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index e68629d3d..a05377702 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1,5 +1,3 @@ -# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement -# pragma pylint: disable=protected-access import copy import logging from datetime import datetime, timezone @@ -11,10 +9,12 @@ import ccxt import pytest from pandas import DataFrame -from freqtrade.exceptions import (DependencyException, InvalidOrderException, DDosProtection, - OperationalException, TemporaryError) +from freqtrade.exceptions import (DDosProtection, DependencyException, + InvalidOrderException, OperationalException, + TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken -from freqtrade.exchange.common import API_RETRY_COUNT, calculate_backoff +from freqtrade.exchange.common import (API_RETRY_COUNT, API_FETCH_ORDER_RETRY_COUNT, + calculate_backoff) from freqtrade.exchange.exchange import (market_is_active, symbol_is_pair, timeframe_to_minutes, timeframe_to_msecs, @@ -1894,12 +1894,14 @@ def test_fetch_order(default_conf, mocker, exchange_name): # Ensure backoff is called assert tm.call_args_list[0][0][0] == 1 assert tm.call_args_list[1][0][0] == 2 - assert tm.call_args_list[2][0][0] == 5 - assert tm.call_args_list[3][0][0] == 10 - assert api_mock.fetch_order.call_count == 6 + if API_FETCH_ORDER_RETRY_COUNT > 2: + assert tm.call_args_list[2][0][0] == 5 + if API_FETCH_ORDER_RETRY_COUNT > 3: + assert tm.call_args_list[3][0][0] == 10 + assert api_mock.fetch_order.call_count == API_FETCH_ORDER_RETRY_COUNT + 1 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, - 'fetch_order', 'fetch_order', retries=6, + 'fetch_order', 'fetch_order', retries=API_FETCH_ORDER_RETRY_COUNT + 1, order_id='_', pair='TKN/BTC') @@ -1932,7 +1934,7 @@ def test_fetch_stoploss_order(default_conf, mocker, exchange_name): ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, 'fetch_stoploss_order', 'fetch_order', - retries=6, + retries=API_FETCH_ORDER_RETRY_COUNT + 1, order_id='_', pair='TKN/BTC') diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index bed92d276..16789af2c 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -1,5 +1,3 @@ -# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement -# pragma pylint: disable=protected-access from random import randint from unittest.mock import MagicMock @@ -7,6 +5,7 @@ import ccxt import pytest from freqtrade.exceptions import DependencyException, InvalidOrderException +from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT from tests.conftest import get_patched_exchange from .test_exchange import ccxt_exceptionhandlers @@ -154,5 +153,5 @@ def test_fetch_stoploss_order(default_conf, mocker): ccxt_exceptionhandlers(mocker, default_conf, api_mock, 'ftx', 'fetch_stoploss_order', 'fetch_orders', - retries=6, + retries=API_FETCH_ORDER_RETRY_COUNT + 1, order_id='_', pair='TKN/BTC') diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 9451c0b9e..8f774a7ec 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -1,5 +1,3 @@ -# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement -# pragma pylint: disable=protected-access from random import randint from unittest.mock import MagicMock From d8a6410fd145d38a01f40b5cbc3757f76db2ea2c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Aug 2020 09:00:57 +0200 Subject: [PATCH 0499/1197] Fix small bug when using max-open-trades -1 in backtesting --- freqtrade/optimize/optimize_reports.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 94729b6a5..b5e5da4af 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -273,7 +273,8 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], 'pairlist': list(btdata.keys()), 'stake_amount': config['stake_amount'], 'stake_currency': config['stake_currency'], - 'max_open_trades': config['max_open_trades'], + 'max_open_trades': (config['max_open_trades'] + if config['max_open_trades'] != float('inf') else -1), 'timeframe': config['timeframe'], **daily_stats, } From 2701a7cb12ce57769ad79cd0b52a38cb27ad87df Mon Sep 17 00:00:00 2001 From: Martin Schultheiss Date: Sun, 23 Aug 2020 09:11:34 +0200 Subject: [PATCH 0500/1197] update bad exchanges --- freqtrade/exchange/common.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 3bba9be72..7f6dfe0eb 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -16,6 +16,7 @@ BAD_EXCHANGES = { "Details in https://github.com/freqtrade/freqtrade/issues/1983", "hitbtc": "This API cannot be used with Freqtrade. " "Use `hitbtc2` exchange id to access this exchange.", + "phemex": "Does not provide history. ", **dict.fromkeys([ 'adara', 'anxpro', From 73417f11f1db843244ac321d91beaaf88ae30c83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Aug 2020 09:11:52 +0200 Subject: [PATCH 0501/1197] Fix rendering issue on readthedocs --- docs/stoploss.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index 2518df846..fa888cd47 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -6,8 +6,8 @@ For example, value `-0.10` will cause immediate sell if the profit dips below -1 Most of the strategy files already include the optimal `stoploss` value. !!! Info -* All stoploss properties mentioned in this file can be set in the Strategy, or in the configuration. -* Configuration values will override the strategy values. + All stoploss properties mentioned in this file can be set in the Strategy, or in the configuration. + Configuration values will override the strategy values. ## Stop Loss On-Exchange/Freqtrade @@ -23,9 +23,9 @@ These modes can be configured with these values: ``` !!! Note -* Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market) and FTX (stop limit and stop-market) as of now. -* Do not set too low stoploss value if using stop loss on exchange! -* If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work + Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market) and FTX (stop limit and stop-market) as of now. + Do not set too low stoploss value if using stop loss on exchange! + If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work ### stoploss_on_exchange and stoploss_on_exchange_limit_ratio Enable or Disable stop loss on exchange. @@ -117,7 +117,7 @@ It is also possible to have a default stop loss, when you are in the red with yo For example, your default stop loss is -10%, but once you have more than 0% profit (example 0.1%) a different trailing stoploss will be used. !!! Note -* If you want the stoploss to only be changed when you break even of making a profit (what most users want) please refer to next section with [offset enabled](#Trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). + If you want the stoploss to only be changed when you break even of making a profit (what most users want) please refer to next section with [offset enabled](#Trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). Both values require `trailing_stop` to be set to true and `trailing_stop_positive` with a value. @@ -172,7 +172,7 @@ For example, simplified math: * now the asset drops in value to 101$, the stop loss will still be 100.94$ and would trigger at 100.94$ !!! Tip -* Make sure to have this value (`trailing_stop_positive_offset`) lower than minimal ROI, otherwise minimal ROI will apply first and sell the trade. + Make sure to have this value (`trailing_stop_positive_offset`) lower than minimal ROI, otherwise minimal ROI will apply first and sell the trade. ## Changing stoploss on open trades From 05ec56d906b025f15032f513a1b68b4c2fec44fb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Aug 2020 10:16:28 +0200 Subject: [PATCH 0502/1197] Dates should be changed to UTC to provide the correct timestamp --- freqtrade/persistence.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 28753ed48..9eebadd8d 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -2,7 +2,7 @@ This module contains the class to persist trades into SQLite """ import logging -from datetime import datetime +from datetime import datetime, timezone from decimal import Decimal from typing import Any, Dict, List, Optional @@ -274,7 +274,7 @@ class Trade(_DECL_BASE): 'open_date_hum': arrow.get(self.open_date).humanize(), 'open_date': self.open_date.strftime("%Y-%m-%d %H:%M:%S"), - 'open_timestamp': int(self.open_date.timestamp() * 1000), + 'open_timestamp': int(self.open_date.replace(tzinfo=timezone.utc).timestamp() * 1000), 'open_rate': self.open_rate, 'open_rate_requested': self.open_rate_requested, 'open_trade_price': round(self.open_trade_price, 8), @@ -283,7 +283,8 @@ class Trade(_DECL_BASE): if self.close_date else None), 'close_date': (self.close_date.strftime("%Y-%m-%d %H:%M:%S") if self.close_date else None), - 'close_timestamp': int(self.close_date.timestamp() * 1000) if self.close_date else None, + 'close_timestamp': int(self.close_date.replace( + tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None, 'close_rate': self.close_rate, 'close_rate_requested': self.close_rate_requested, 'close_profit': self.close_profit, @@ -298,8 +299,8 @@ class Trade(_DECL_BASE): 'stoploss_order_id': self.stoploss_order_id, 'stoploss_last_update': (self.stoploss_last_update.strftime("%Y-%m-%d %H:%M:%S") if self.stoploss_last_update else None), - 'stoploss_last_update_timestamp': (int(self.stoploss_last_update.timestamp() * 1000) - if self.stoploss_last_update else None), + 'stoploss_last_update_timestamp': int(self.stoploss_last_update.replace( + tzinfo=timezone.utc).timestamp() * 1000) if self.stoploss_last_update else None, 'initial_stop_loss': self.initial_stop_loss, # Deprecated - should not be used 'initial_stop_loss_abs': self.initial_stop_loss, 'initial_stop_loss_ratio': (self.initial_stop_loss_pct From 9ba9f73706361e8f5d8cd93b778645d636dead15 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Aug 2020 16:04:32 +0200 Subject: [PATCH 0503/1197] Improve logging, don't search for buy orders in refind_lost_order --- freqtrade/freqtradebot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6d2f9ddcc..9db60be43 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -306,6 +306,9 @@ class FreqtradeBot: for order in trade.orders: logger.info(f"Trying to refind {order}") fo = None + if order.ft_order_side == 'buy': + # Skip buy side - this is handled by reupdate_buy_order_fees + continue try: fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, order.ft_order_side == 'stoploss') @@ -321,6 +324,7 @@ class FreqtradeBot: # No action for buy orders ... continue if fo: + logger.info(f"Found {order} for trade {trade}.jj") self.update_trade_state(trade, order.order_id, fo, stoploss_order=order.ft_order_side == 'stoploss') From ec949614374925d076febb3c140fc22aa4cedea9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Aug 2020 19:14:28 +0200 Subject: [PATCH 0504/1197] Reduce loglevel of "using cached rate" --- freqtrade/freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2a95f58fc..2fcb9f3f9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -275,7 +275,7 @@ class FreqtradeBot: rate = self._buy_rate_cache.get(pair) # Check if cache has been invalidated if rate: - logger.info(f"Using cached buy rate for {pair}.") + logger.debug(f"Using cached buy rate for {pair}.") return rate bid_strategy = self.config.get('bid_strategy', {}) @@ -693,7 +693,7 @@ class FreqtradeBot: rate = self._sell_rate_cache.get(pair) # Check if cache has been invalidated if rate: - logger.info(f"Using cached sell rate for {pair}.") + logger.debug(f"Using cached sell rate for {pair}.") return rate ask_strategy = self.config.get('ask_strategy', {}) From a55dd8444df6c07fae14d467efc8006ae869b341 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Aug 2020 19:31:35 +0200 Subject: [PATCH 0505/1197] Fix loglevel of using_cached-rate --- tests/test_freqtradebot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c6413cb5d..0d7968e26 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -953,6 +953,7 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None: ]) def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid, last, last_ab, expected) -> None: + caplog.set_level(logging.DEBUG) default_conf['bid_strategy']['ask_last_balance'] = last_ab default_conf['bid_strategy']['price_side'] = side freqtrade = get_patched_freqtradebot(mocker, default_conf) @@ -3969,6 +3970,8 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order ('ask', 0.006, 1.0, 0.006), ]) def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask, expected) -> None: + caplog.set_level(logging.DEBUG) + default_conf['ask_strategy']['price_side'] = side mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'ask': ask, 'bid': bid}) pair = "ETH/BTC" @@ -3990,6 +3993,7 @@ def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask, expected) - ('ask', 0.043949), # Value from order_book_l2 fiture - asks side ]) def test_get_sell_rate_orderbook(default_conf, mocker, caplog, side, expected, order_book_l2): + caplog.set_level(logging.DEBUG) # Test orderbook mode default_conf['ask_strategy']['price_side'] = side default_conf['ask_strategy']['use_order_book'] = True From 8940ba828f621963528578defe7c34f7cf48eb3c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Aug 2020 21:12:08 +0200 Subject: [PATCH 0506/1197] Update sandbox documentation --- docs/sandbox-testing.md | 150 ++++++++++++++-------------------------- 1 file changed, 51 insertions(+), 99 deletions(-) diff --git a/docs/sandbox-testing.md b/docs/sandbox-testing.md index 7f3457d15..bb075217f 100644 --- a/docs/sandbox-testing.md +++ b/docs/sandbox-testing.md @@ -1,104 +1,59 @@ # Sandbox API testing -Where an exchange provides a sandbox for risk-free integration, or end-to-end, testing CCXT provides access to these. +Some exchanges provide sandboxes or testbeds for risk-free testing, while running the bot against a real exchange. +With some configuration, freqtrade (in combination with ccxt) provides access to these. -This document is a *light overview of configuring Freqtrade and GDAX sandbox. -This can be useful to developers and trader alike as Freqtrade is quite customisable. +This document is a light overview of configuring Freqtrade to be used with sandboxes. +This can be useful to developers and trader alike. + +## Exchanges known to have a sandbox / testnet + +* [binance](https://testnet.binance.vision/) +* [coinbasepro](https://public.sandbox.pro.coinbase.com) +* [gemini](https://exchange.sandbox.gemini.com/) +* [huobipro](https://www.testnet.huobi.pro/) +* [kucoin](https://sandbox.kucoin.com/) +* [phemex](https://testnet.phemex.com/) + +!!! Note + We did not test correct functioning of all of the above testnets. Please report your experiences with each sandbox. + +--- + +## Configure a Sandbox account When testing your API connectivity, make sure to use the following URLs. -***Website** -https://public.sandbox.gdax.com -***REST API** -https://api-public.sandbox.gdax.com ---- +In general, you should follow these steps to enable an exchange's sandbox: -# Configure a Sandbox account on Gdax +- Figure out if an exchange has a sandbox (most likely by using google or the exchange's support documents) +- Create a sandbox account (often the sandbox-account requires separate registration) +- [Add some test assets to account](#add-test-funds) +- Create API keys -Aim of this document section +### Add test funds -- An sanbox account -- create 2FA (needed to create an API) -- Add test 50BTC to account -- Create : -- - API-KEY -- - API-Secret -- - API Password +Usually, sandbox exchanges allow depositing funds directly via web-interface. +You should make sure to have a realistic amount of funds available to your test-account, so results are representable of your real account funds. -## Acccount +!!! Warning + Test exchanges will NEVER require your real credit card or banking details! -This link will redirect to the sandbox main page to login / create account dialogues: -https://public.sandbox.pro.coinbase.com/orders/ +## Configure freqtrade to use a exchange's sandbox -After registration and Email confimation you wil be redirected into your sanbox account. It is easy to verify you're in sandbox by checking the URL bar. -> https://public.sandbox.pro.coinbase.com/ - -## Enable 2Fa (a prerequisite to creating sandbox API Keys) - -From within sand box site select your profile, top right. ->Or as a direct link: https://public.sandbox.pro.coinbase.com/profile - -From the menu panel to the left of the screen select - -> Security: "*View or Update*" - -In the new site select "enable authenticator" as typical google Authenticator. - -- open Google Authenticator on your phone -- scan barcode -- enter your generated 2fa - -## Enable API Access - -From within sandbox select profile>api>create api-keys ->or as a direct link: https://public.sandbox.pro.coinbase.com/profile/api - -Click on "create one" and ensure **view** and **trade** are "checked" and sumbit your 2FA - -- **Copy and paste the Passphase** into a notepade this will be needed later -- **Copy and paste the API Secret** popup into a notepad this will needed later -- **Copy and paste the API Key** into a notepad this will needed later - -## Add 50 BTC test funds - -To add funds, use the web interface deposit and withdraw buttons. - -To begin select 'Wallets' from the top menu. -> Or as a direct link: https://public.sandbox.pro.coinbase.com/wallets - -- Deposits (bottom left of screen) -- - Deposit Funds Bitcoin -- - - Coinbase BTC Wallet -- - - - Max (50 BTC) -- - - - - Deposit - -*This process may be repeated for other currencies, ETH as example* - ---- - -# Configure Freqtrade to use Gax Sandbox - -The aim of this document section - -- Enable sandbox URLs in Freqtrade -- Configure API -- - secret -- - key -- - passphrase - -## Sandbox URLs +### Sandbox URLs Freqtrade makes use of CCXT which in turn provides a list of URLs to Freqtrade. These include `['test']` and `['api']`. -- `[Test]` if available will point to an Exchanges sandbox. -- `[Api]` normally used, and resolves to live API target on the exchange +- `[Test]` if available will point to an Exchanges sandbox. +- `[Api]` normally used, and resolves to live API target on the exchange. To make use of sandbox / test add "sandbox": true, to your config.json ```json "exchange": { - "name": "gdax", + "name": "coinbasepro", "sandbox": true, "key": "5wowfxemogxeowo;heiohgmd", "secret": "/ZMH1P62rCVmwefewrgcewX8nh4gob+lywxfwfxwwfxwfNsH1ySgvWCUR/w==", @@ -106,36 +61,33 @@ To make use of sandbox / test add "sandbox": true, to your config.json "outdated_offset": 5 "pair_whitelist": [ "BTC/USD" + ] + }, + "datadir": "user_data/data/coinbasepro_sandbox" ``` -Also insert your +Also the following information: -- api-key (noted earlier) +- api-key (created for the sandbox webpage) - api-secret (noted earlier) - password (the passphrase - noted earlier) +!!! Tip "Different data directory" + We also recommend to set `datadir` to something identifying downloaded data as sandbox data, to avoid having sandbox data mixed with data from the real exchange. + This can be done by adding the `"datadir"` key to the configuration. + Now, whenever you use this configuration, your data directory will be set to this directory. + --- ## You should now be ready to test your sandbox -Ensure Freqtrade logs show the sandbox URL, and trades made are shown in sandbox. -** Typically the BTC/USD has the most activity in sandbox to test against. +Ensure Freqtrade logs show the sandbox URL, and trades made are shown in sandbox. Also make sure to select a pair which shows at least some decent value (which very often is BTC/). -## GDAX - Old Candles problem +## Common problems with sandbox exchanges -It is my experience that GDAX sandbox candles may be 20+- minutes out of date. This can cause trades to fail as one of Freqtrades safety checks. +Sandbox exchange instances often have very low volume, which can cause some problems which usually are not seen on a real exchange instance. -To disable this check, add / change the `"outdated_offset"` parameter in the exchange section of your configuration to adjust for this delay. -Example based on the above configuration: +### Old Candles problem -```json - "exchange": { - "name": "gdax", - "sandbox": true, - "key": "5wowfxemogxeowo;heiohgmd", - "secret": "/ZMH1P62rCVmwefewrgcewX8nh4gob+lywxfwfxwwfxwfNsH1ySgvWCUR/w==", - "password": "1bkjfkhfhfu6sr", - "outdated_offset": 30 - "pair_whitelist": [ - "BTC/USD" -``` +Since Sandboxes often have low volume, candles can be quite old and show no volume. +To disable the error "Outdated history for pair ...", best increase the parameter `"outdated_offset"` to a number that seems realistic for the sandbox you're using. From 8478e083dc6e3e0ec40721c71c580500ef2484fb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 23 Aug 2020 21:16:44 +0200 Subject: [PATCH 0507/1197] Improve wording of sandbox documentation --- docs/sandbox-testing.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/sandbox-testing.md b/docs/sandbox-testing.md index bb075217f..9556dd014 100644 --- a/docs/sandbox-testing.md +++ b/docs/sandbox-testing.md @@ -3,7 +3,7 @@ Some exchanges provide sandboxes or testbeds for risk-free testing, while running the bot against a real exchange. With some configuration, freqtrade (in combination with ccxt) provides access to these. -This document is a light overview of configuring Freqtrade to be used with sandboxes. +This document is an overview to configure Freqtrade to be used with sandboxes. This can be useful to developers and trader alike. ## Exchanges known to have a sandbox / testnet @@ -22,14 +22,14 @@ This can be useful to developers and trader alike. ## Configure a Sandbox account -When testing your API connectivity, make sure to use the following URLs. +When testing your API connectivity, make sure to use the appropriate sandbox / testnet URL. In general, you should follow these steps to enable an exchange's sandbox: -- Figure out if an exchange has a sandbox (most likely by using google or the exchange's support documents) -- Create a sandbox account (often the sandbox-account requires separate registration) -- [Add some test assets to account](#add-test-funds) -- Create API keys +* Figure out if an exchange has a sandbox (most likely by using google or the exchange's support documents) +* Create a sandbox account (often the sandbox-account requires separate registration) +* [Add some test assets to account](#add-test-funds) +* Create API keys ### Add test funds @@ -37,7 +37,7 @@ Usually, sandbox exchanges allow depositing funds directly via web-interface. You should make sure to have a realistic amount of funds available to your test-account, so results are representable of your real account funds. !!! Warning - Test exchanges will NEVER require your real credit card or banking details! + Test exchanges will **NEVER** require your real credit card or banking details! ## Configure freqtrade to use a exchange's sandbox @@ -46,8 +46,8 @@ You should make sure to have a realistic amount of funds available to your test- Freqtrade makes use of CCXT which in turn provides a list of URLs to Freqtrade. These include `['test']` and `['api']`. -- `[Test]` if available will point to an Exchanges sandbox. -- `[Api]` normally used, and resolves to live API target on the exchange. +* `[Test]` if available will point to an Exchanges sandbox. +* `[Api]` normally used, and resolves to live API target on the exchange. To make use of sandbox / test add "sandbox": true, to your config.json @@ -68,9 +68,9 @@ To make use of sandbox / test add "sandbox": true, to your config.json Also the following information: -- api-key (created for the sandbox webpage) -- api-secret (noted earlier) -- password (the passphrase - noted earlier) +* api-key (created for the sandbox webpage) +* api-secret (noted earlier) +* password (the passphrase - noted earlier) !!! Tip "Different data directory" We also recommend to set `datadir` to something identifying downloaded data as sandbox data, to avoid having sandbox data mixed with data from the real exchange. From 38809acde817c27262c119b7bab157b19ad9e34a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Aug 2020 06:50:43 +0200 Subject: [PATCH 0508/1197] Don't rerun for known closed orders --- freqtrade/freqtradebot.py | 4 ++++ freqtrade/persistence/models.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index cb44165f4..498b3eea6 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -306,6 +306,10 @@ class FreqtradeBot: for order in trade.orders: logger.info(f"Trying to refind {order}") fo = None + if not order.ft_is_open: + # TODO: Does this need to be info level? + logger.info(f"Order {order} is no longer open.") + continue if order.ft_order_side == 'buy': # Skip buy side - this is handled by reupdate_buy_order_fees continue diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index c3a112828..e0b9624dd 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -132,7 +132,7 @@ class Order(_DECL_BASE): def __repr__(self): return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' - f'side={self.side}, status={self.status})') + f'side={self.side}, order_type={self.order_type}, status={self.status})') def update_from_ccxt_object(self, order): """ From 26f45c83234900dd8c2f6407138c2aa454d32f81 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Aug 2020 06:56:56 +0200 Subject: [PATCH 0509/1197] Improve logmessage for trailing stoploss --- freqtrade/freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 498b3eea6..faa67f504 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -988,8 +988,8 @@ class FreqtradeBot: update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat: # cancelling the current stoploss on exchange first - logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s}) ' - 'in order to add another one ...', order['id']) + logger.info(f"Cancelling current stoploss on exchange for pair {trade.pair} " + f"(orderid:{order['id']}) in order to add another one ...") try: co = self.exchange.cancel_stoploss_order(order['id'], trade.pair) trade.update_order(co) From 5799cc51f28bf06f9b7f208250b9169d454d3162 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2020 06:44:50 +0000 Subject: [PATCH 0510/1197] Bump mkdocs-material from 5.5.7 to 5.5.8 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 5.5.7 to 5.5.8. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/5.5.7...5.5.8) Signed-off-by: dependabot[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 ab5aebb79..5226db750 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==5.5.7 +mkdocs-material==5.5.8 mdx_truly_sane_lists==1.2 From 4c48fe96edbc1e2528f980e1a0cc8c3e73b0c24d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2020 06:44:52 +0000 Subject: [PATCH 0511/1197] Bump pytest-mock from 3.2.0 to 3.3.0 Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.2.0 to 3.3.0. - [Release notes](https://github.com/pytest-dev/pytest-mock/releases) - [Changelog](https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.2.0...v3.3.0) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 3c10fe445..1f5b68a73 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,7 +11,7 @@ mypy==0.782 pytest==6.0.1 pytest-asyncio==0.14.0 pytest-cov==2.10.1 -pytest-mock==3.2.0 +pytest-mock==3.3.0 pytest-random-order==1.0.4 # Convert jupyter notebooks to markdown documents From 74c97369d9f6a57e577672ab6a6ba6803432b6f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2020 06:44:54 +0000 Subject: [PATCH 0512/1197] Bump arrow from 0.15.8 to 0.16.0 Bumps [arrow](https://github.com/arrow-py/arrow) from 0.15.8 to 0.16.0. - [Release notes](https://github.com/arrow-py/arrow/releases) - [Changelog](https://github.com/arrow-py/arrow/blob/master/CHANGELOG.rst) - [Commits](https://github.com/arrow-py/arrow/compare/0.15.8...0.16.0) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 712cf820d..7f81d6265 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -3,7 +3,7 @@ ccxt==1.33.18 SQLAlchemy==1.3.18 python-telegram-bot==12.8 -arrow==0.15.8 +arrow==0.16.0 cachetools==4.1.1 requests==2.24.0 urllib3==1.25.10 From 0e20b8f530f3b93cfb2eb0e70a969aaf52b5dbec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2020 06:45:12 +0000 Subject: [PATCH 0513/1197] Bump ccxt from 1.33.18 to 1.33.52 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.33.18 to 1.33.52. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.33.18...1.33.52) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 712cf820d..3cd764693 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.33.18 +ccxt==1.33.52 SQLAlchemy==1.3.18 python-telegram-bot==12.8 arrow==0.15.8 From f22fc8ef3ee05e39c00aec8d84914fa2f487aa5d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2020 06:45:20 +0000 Subject: [PATCH 0514/1197] Bump pandas from 1.1.0 to 1.1.1 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.1.0 to 1.1.1. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.1.0...v1.1.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d65f90325..66f4cbc5f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ -r requirements-common.txt numpy==1.19.1 -pandas==1.1.0 +pandas==1.1.1 From 7ece7294b2a0d7431cea91a674b785bc3bd0e7c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2020 07:18:00 +0000 Subject: [PATCH 0515/1197] Bump sqlalchemy from 1.3.18 to 1.3.19 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.18 to 1.3.19. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 27a233604..b6e2d329f 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,7 +1,7 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs ccxt==1.33.52 -SQLAlchemy==1.3.18 +SQLAlchemy==1.3.19 python-telegram-bot==12.8 arrow==0.16.0 cachetools==4.1.1 From c272944834b73f6705ab92d6eef1e956e3666cb0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Aug 2020 11:09:09 +0200 Subject: [PATCH 0516/1197] Lock pair until a new candle arrives --- freqtrade/freqtradebot.py | 5 ++-- freqtrade/strategy/interface.py | 20 +++++++++------ tests/strategy/test_interface.py | 43 ++++++++++++++++++++------------ 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2fcb9f3f9..eee60cc22 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -433,7 +433,9 @@ class FreqtradeBot: """ logger.debug(f"create_trade for pair {pair}") - if self.strategy.is_pair_locked(pair): + analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, self.strategy.timeframe) + if self.strategy.is_pair_locked( + pair, analyzed_df.iloc[-1]['date'] if len(analyzed_df) > 0 else None): logger.info(f"Pair {pair} is currently locked.") return False @@ -444,7 +446,6 @@ class FreqtradeBot: return False # running get_signal on historical data fetched - analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, self.strategy.timeframe) (buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df) if buy and not sell: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index efc4b8430..4a3a78c8f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -2,6 +2,7 @@ IStrategy interface This module defines the interface to apply for strategies """ +from freqtrade.exchange.exchange import timeframe_to_next_date import logging import warnings from abc import ABC, abstractmethod @@ -297,13 +298,22 @@ class IStrategy(ABC): if pair in self._pair_locked_until: del self._pair_locked_until[pair] - def is_pair_locked(self, pair: str) -> bool: + def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool: """ Checks if a pair is currently locked """ if pair not in self._pair_locked_until: return False - return self._pair_locked_until[pair] >= datetime.now(timezone.utc) + if not candle_date: + return self._pair_locked_until[pair] >= datetime.now(timezone.utc) + else: + # Locking should happen until a new candle arrives + lock_time = timeframe_to_next_date(self.timeframe, candle_date) + # lock_time = candle_date + timedelta(minutes=timeframe_to_minutes(self.timeframe)) + res = self._pair_locked_until[pair] > lock_time + logger.debug(f"pair time = {lock_time} - pair_lock = {self._pair_locked_until[pair]} " + f"- res: {res}") + return res def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ @@ -438,12 +448,6 @@ class IStrategy(ABC): ) return False, False - # Check if dataframe has new candle - if (arrow.utcnow() - latest_date).total_seconds() // 60 >= timeframe_minutes: - logger.warning('Old candle for pair %s. Last candle is %s minutes old', - pair, int((arrow.utcnow() - latest_date).total_seconds() // 60)) - return False, False - (buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1 logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', latest['date'], pair, str(buy), str(sell)) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index bca7cc0d9..f1b5d0244 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -1,6 +1,7 @@ # pragma pylint: disable=missing-docstring, C0103 import logging +from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock import arrow @@ -8,12 +9,12 @@ import pytest from pandas import DataFrame from freqtrade.configuration import TimeRange +from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data from freqtrade.exceptions import StrategyError from freqtrade.persistence import Trade from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper -from freqtrade.data.dataprovider import DataProvider from tests.conftest import log_has, log_has_re from .strats.default_strategy import DefaultStrategy @@ -87,21 +88,6 @@ def test_get_signal_exception_valueerror(default_conf, mocker, caplog, ohlcv_his assert log_has_re(r'Strategy caused the following exception: xyz.*', caplog) -def test_get_signal_old_candle(default_conf, mocker, caplog, ohlcv_history): - caplog.set_level(logging.INFO) - # default_conf defines a 5m interval. we check interval of previous candle - # this is necessary as the last candle is removed (partial candles) by default - oldtime = arrow.utcnow().shift(minutes=-10) - ticks = DataFrame([{'buy': 1, 'date': oldtime}]) - mocker.patch.object( - _STRATEGY, '_analyze_ticker_internal', - return_value=DataFrame(ticks) - ) - assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe'], - ohlcv_history) - assert log_has('Old candle for pair xyz. Last candle is 10 minutes old', caplog) - - def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): # default_conf defines a 5m interval. we check interval * 2 + 5m # this is necessary as the last candle is removed (partial candles) by default @@ -402,6 +388,31 @@ def test_is_pair_locked(default_conf): strategy.unlock_pair(pair) assert not strategy.is_pair_locked(pair) + pair = 'BTC/USDT' + # Lock until 14:30 + lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc) + strategy.lock_pair(pair, lock_time) + # Lock is in the past ... + assert not strategy.is_pair_locked(pair) + # latest candle is from 14:20, lock goes to 14:30 + assert strategy.is_pair_locked(pair, lock_time + timedelta(minutes=-10)) + assert strategy.is_pair_locked(pair, lock_time + timedelta(minutes=-50)) + + # latest candle is from 14:25 (lock should be lifted) + # Since this is the "new candle" available at 14:30 + assert not strategy.is_pair_locked(pair, lock_time + timedelta(minutes=-4)) + + # Should not be locked after time expired + assert not strategy.is_pair_locked(pair, lock_time + timedelta(minutes=10)) + + # Change timeframe to 15m + strategy.timeframe = '15m' + # Candle from 14:14 - lock goes until 14:30 + assert strategy.is_pair_locked(pair, lock_time + timedelta(minutes=-16)) + assert strategy.is_pair_locked(pair, lock_time + timedelta(minutes=-15, seconds=-2)) + # Candle from 14:15 - lock goes until 14:30 + assert not strategy.is_pair_locked(pair, lock_time + timedelta(minutes=-15)) + def test_is_informative_pairs_callback(default_conf): default_conf.update({'strategy': 'TestStrategyLegacy'}) From 502e21b2cb6da649c97a00b0a484a2ab1b1ba408 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Aug 2020 11:17:27 +0200 Subject: [PATCH 0517/1197] Add unfilled explanation for sandboxes --- docs/sandbox-testing.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/sandbox-testing.md b/docs/sandbox-testing.md index 9556dd014..9c14412de 100644 --- a/docs/sandbox-testing.md +++ b/docs/sandbox-testing.md @@ -91,3 +91,27 @@ Sandbox exchange instances often have very low volume, which can cause some prob Since Sandboxes often have low volume, candles can be quite old and show no volume. To disable the error "Outdated history for pair ...", best increase the parameter `"outdated_offset"` to a number that seems realistic for the sandbox you're using. + +### Unfilled orders + +Sandboxes often have very low volumes - which means that many trades can go unfilled, or can go unfilled for a very long time. + +To mitigate this, you can try to match the first order on the opposite orderbook side using the following configuration: + +``` jsonc + "order_types": { + "buy": "limit", + "sell": "limit" + // ... + }, + "bid_strategy": { + "price_side": "ask", + // ... + }, + "ask_strategy":{ + "price_side": "bid", + // ... + }, + ``` + + The configuration is similar to the suggested configuration for market orders - however by using limit-orders you can avoid moving the price too much, and you can set the worst price you might get. From 354a40624866645b191dde458c857588c17a211b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Aug 2020 11:44:32 +0200 Subject: [PATCH 0518/1197] Sort imports in interface.py --- freqtrade/strategy/interface.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 4a3a78c8f..9673b0c68 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -2,7 +2,6 @@ IStrategy interface This module defines the interface to apply for strategies """ -from freqtrade.exchange.exchange import timeframe_to_next_date import logging import warnings from abc import ABC, abstractmethod @@ -15,8 +14,9 @@ from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.exceptions import StrategyError, OperationalException +from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes +from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.persistence import Trade from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -310,10 +310,7 @@ class IStrategy(ABC): # Locking should happen until a new candle arrives lock_time = timeframe_to_next_date(self.timeframe, candle_date) # lock_time = candle_date + timedelta(minutes=timeframe_to_minutes(self.timeframe)) - res = self._pair_locked_until[pair] > lock_time - logger.debug(f"pair time = {lock_time} - pair_lock = {self._pair_locked_until[pair]} " - f"- res: {res}") - return res + return self._pair_locked_until[pair] > lock_time def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ From fca11160e443470aea767bb4e7d43c021fd5d149 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Aug 2020 17:18:57 +0200 Subject: [PATCH 0519/1197] Improve docstring of is_pair_locked --- freqtrade/strategy/interface.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 9673b0c68..69d9333e2 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -301,6 +301,11 @@ class IStrategy(ABC): def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool: """ Checks if a pair is currently locked + The 2nd, optional parameter ensures that locks are applied until the new candle arrives, + and not stop at 14:00:00 - while the next candle arrives at 14:00:02 leaving a gap + of 2 seconds for a buy to happen on an old signal. + :param: pair: "Pair to check" + :param candle_date: Date of the last candle. Optional, defaults to current date """ if pair not in self._pair_locked_until: return False From 3bb69bc1bd5b78498a9c2d236a7f32a9779dc322 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Aug 2020 17:31:00 +0200 Subject: [PATCH 0520/1197] Add returns statement to docstring --- freqtrade/strategy/interface.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 69d9333e2..92d9f6c48 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -306,6 +306,7 @@ class IStrategy(ABC): of 2 seconds for a buy to happen on an old signal. :param: pair: "Pair to check" :param candle_date: Date of the last candle. Optional, defaults to current date + :returns: locking state of the pair in question. """ if pair not in self._pair_locked_until: return False From 9d4ecb625a004a45dc58c0f942394f0f4b80d277 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Aug 2020 07:16:29 +0200 Subject: [PATCH 0521/1197] Allow numpy numbers as comparisons, too --- freqtrade/vendor/qtpylib/indicators.py | 2 +- tests/test_indicators.py | 18 ++++++++++++++++++ tests/test_talib.py | 2 -- 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 tests/test_indicators.py diff --git a/freqtrade/vendor/qtpylib/indicators.py b/freqtrade/vendor/qtpylib/indicators.py index bef140396..e5a404862 100644 --- a/freqtrade/vendor/qtpylib/indicators.py +++ b/freqtrade/vendor/qtpylib/indicators.py @@ -222,7 +222,7 @@ def crossed(series1, series2, direction=None): if isinstance(series1, np.ndarray): series1 = pd.Series(series1) - if isinstance(series2, (float, int, np.ndarray)): + if isinstance(series2, (float, int, np.ndarray, np.integer, np.floating)): series2 = pd.Series(index=series1.index, data=series2) if direction is None or direction == "above": diff --git a/tests/test_indicators.py b/tests/test_indicators.py new file mode 100644 index 000000000..2f9bdc0f9 --- /dev/null +++ b/tests/test_indicators.py @@ -0,0 +1,18 @@ +import freqtrade.vendor.qtpylib.indicators as qtpylib +import numpy as np +import pandas as pd + + +def test_crossed_numpy_types(): + """ + This test is only present since this method currently diverges from the qtpylib implementation. + And we must ensure to not break this again once we update from the original source. + """ + series = pd.Series([56, 97, 19, 76, 65, 25, 87, 91, 79, 79]) + expected_result = pd.Series([False, True, False, True, False, False, True, False, False, False]) + + assert qtpylib.crossed_above(series, 60).equals(expected_result) + assert qtpylib.crossed_above(series, 60.0).equals(expected_result) + assert qtpylib.crossed_above(series, np.int32(60)).equals(expected_result) + assert qtpylib.crossed_above(series, np.int64(60)).equals(expected_result) + assert qtpylib.crossed_above(series, np.float64(60.0)).equals(expected_result) diff --git a/tests/test_talib.py b/tests/test_talib.py index 2c7f73eb1..4effc129b 100644 --- a/tests/test_talib.py +++ b/tests/test_talib.py @@ -1,5 +1,3 @@ - - import talib.abstract as ta import pandas as pd From 309ea1246aab34310aa9cf8aef32170bb5b3ccac Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Aug 2020 20:52:09 +0200 Subject: [PATCH 0522/1197] Update config to use single quotes --- freqtrade/commands/data_commands.py | 16 +-- freqtrade/commands/deploy_commands.py | 8 +- freqtrade/configuration/configuration.py | 40 +++---- freqtrade/data/btanalysis.py | 2 +- freqtrade/exchange/exchange.py | 4 +- freqtrade/plot/plotting.py | 22 ++-- scripts/rest_client.py | 10 +- tests/conftest.py | 2 +- tests/rpc/test_rpc_apiserver.py | 10 +- tests/test_arguments.py | 132 +++++++++++------------ tests/test_configuration.py | 2 +- tests/test_main.py | 14 +-- tests/test_plotting.py | 12 +-- 13 files changed, 137 insertions(+), 137 deletions(-) diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index aa0b826b5..da1eb0cf5 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -35,8 +35,8 @@ def start_download_data(args: Dict[str, Any]) -> None: "Downloading data requires a list of pairs. " "Please check the documentation on how to configure this.") - logger.info(f'About to download pairs: {config["pairs"]}, ' - f'intervals: {config["timeframes"]} to {config["datadir"]}') + logger.info(f"About to download pairs: {config['pairs']}, " + f"intervals: {config['timeframes']} to {config['datadir']}") pairs_not_available: List[str] = [] @@ -51,21 +51,21 @@ def start_download_data(args: Dict[str, Any]) -> None: if config.get('download_trades'): pairs_not_available = refresh_backtest_trades_data( - exchange, pairs=config["pairs"], datadir=config['datadir'], - timerange=timerange, erase=bool(config.get("erase")), + exchange, pairs=config['pairs'], datadir=config['datadir'], + timerange=timerange, erase=bool(config.get('erase')), data_format=config['dataformat_trades']) # Convert downloaded trade data to different timeframes convert_trades_to_ohlcv( - pairs=config["pairs"], timeframes=config["timeframes"], - datadir=config['datadir'], timerange=timerange, erase=bool(config.get("erase")), + pairs=config['pairs'], timeframes=config['timeframes'], + datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')), data_format_ohlcv=config['dataformat_ohlcv'], data_format_trades=config['dataformat_trades'], ) else: pairs_not_available = refresh_backtest_ohlcv_data( - exchange, pairs=config["pairs"], timeframes=config["timeframes"], - datadir=config['datadir'], timerange=timerange, erase=bool(config.get("erase")), + exchange, pairs=config['pairs'], timeframes=config['timeframes'], + datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')), data_format=config['dataformat_ohlcv']) except KeyboardInterrupt: diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 86562fa7c..bfd68cb9b 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -75,7 +75,7 @@ def start_new_strategy(args: Dict[str, Any]) -> None: if args["strategy"] == "DefaultStrategy": raise OperationalException("DefaultStrategy is not allowed as name.") - new_path = config['user_data_dir'] / USERPATH_STRATEGIES / (args["strategy"] + ".py") + new_path = config['user_data_dir'] / USERPATH_STRATEGIES / (args['strategy'] + '.py') if new_path.exists(): raise OperationalException(f"`{new_path}` already exists. " @@ -125,11 +125,11 @@ def start_new_hyperopt(args: Dict[str, Any]) -> None: config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) - if "hyperopt" in args and args["hyperopt"]: - if args["hyperopt"] == "DefaultHyperopt": + if 'hyperopt' in args and args['hyperopt']: + if args['hyperopt'] == 'DefaultHyperopt': raise OperationalException("DefaultHyperopt is not allowed as name.") - new_path = config['user_data_dir'] / USERPATH_HYPEROPTS / (args["hyperopt"] + ".py") + new_path = config['user_data_dir'] / USERPATH_HYPEROPTS / (args['hyperopt'] + '.py') if new_path.exists(): raise OperationalException(f"`{new_path}` already exists. " diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 01e42144a..930917fae 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -54,7 +54,7 @@ class Configuration: :param files: List of file paths :return: configuration dictionary """ - c = Configuration({"config": files}, RunMode.OTHER) + c = Configuration({'config': files}, RunMode.OTHER) return c.get_config() def load_from_files(self, files: List[str]) -> Dict[str, Any]: @@ -123,10 +123,10 @@ class Configuration: the -v/--verbose, --logfile options """ # Log level - config.update({'verbosity': self.args.get("verbosity", 0)}) + config.update({'verbosity': self.args.get('verbosity', 0)}) - if 'logfile' in self.args and self.args["logfile"]: - config.update({'logfile': self.args["logfile"]}) + if 'logfile' in self.args and self.args['logfile']: + config.update({'logfile': self.args['logfile']}) setup_logging(config) @@ -149,22 +149,22 @@ class Configuration: def _process_common_options(self, config: Dict[str, Any]) -> None: # Set strategy if not specified in config and or if it's non default - if self.args.get("strategy") or not config.get('strategy'): - config.update({'strategy': self.args.get("strategy")}) + if self.args.get('strategy') or not config.get('strategy'): + config.update({'strategy': self.args.get('strategy')}) self._args_to_config(config, argname='strategy_path', logstring='Using additional Strategy lookup path: {}') - if ('db_url' in self.args and self.args["db_url"] and - self.args["db_url"] != constants.DEFAULT_DB_PROD_URL): - config.update({'db_url': self.args["db_url"]}) + if ('db_url' in self.args and self.args['db_url'] and + self.args['db_url'] != constants.DEFAULT_DB_PROD_URL): + config.update({'db_url': self.args['db_url']}) logger.info('Parameter --db-url detected ...') if config.get('forcebuy_enable', False): logger.warning('`forcebuy` RPC message enabled.') # Support for sd_notify - if 'sd_notify' in self.args and self.args["sd_notify"]: + if 'sd_notify' in self.args and self.args['sd_notify']: config['internals'].update({'sd_notify': True}) def _process_datadir_options(self, config: Dict[str, Any]) -> None: @@ -173,24 +173,24 @@ class Configuration: --user-data, --datadir """ # Check exchange parameter here - otherwise `datadir` might be wrong. - if "exchange" in self.args and self.args["exchange"]: - config['exchange']['name'] = self.args["exchange"] + if 'exchange' in self.args and self.args['exchange']: + config['exchange']['name'] = self.args['exchange'] logger.info(f"Using exchange {config['exchange']['name']}") if 'pair_whitelist' not in config['exchange']: config['exchange']['pair_whitelist'] = [] - if 'user_data_dir' in self.args and self.args["user_data_dir"]: - config.update({'user_data_dir': self.args["user_data_dir"]}) + if 'user_data_dir' in self.args and self.args['user_data_dir']: + config.update({'user_data_dir': self.args['user_data_dir']}) elif 'user_data_dir' not in config: # Default to cwd/user_data (legacy option ...) - config.update({'user_data_dir': str(Path.cwd() / "user_data")}) + config.update({'user_data_dir': str(Path.cwd() / 'user_data')}) # reset to user_data_dir so this contains the absolute path. config['user_data_dir'] = create_userdata_dir(config['user_data_dir'], create_dir=False) logger.info('Using user-data directory: %s ...', config['user_data_dir']) - config.update({'datadir': create_datadir(config, self.args.get("datadir", None))}) + config.update({'datadir': create_datadir(config, self.args.get('datadir', None))}) logger.info('Using data directory: %s ...', config.get('datadir')) if self.args.get('exportfilename'): @@ -219,8 +219,8 @@ class Configuration: config.update({'use_max_market_positions': False}) logger.info('Parameter --disable-max-market-positions detected ...') logger.info('max_open_trades set to unlimited ...') - elif 'max_open_trades' in self.args and self.args["max_open_trades"]: - config.update({'max_open_trades': self.args["max_open_trades"]}) + elif 'max_open_trades' in self.args and self.args['max_open_trades']: + config.update({'max_open_trades': self.args['max_open_trades']}) logger.info('Parameter --max-open-trades detected, ' 'overriding max_open_trades to: %s ...', config.get('max_open_trades')) elif config['runmode'] in NON_UTIL_MODES: @@ -447,12 +447,12 @@ class Configuration: config['pairs'].sort() return - if "config" in self.args and self.args["config"]: + if 'config' in self.args and self.args['config']: logger.info("Using pairlist from configuration.") config['pairs'] = config.get('exchange', {}).get('pair_whitelist') else: # Fall back to /dl_path/pairs.json - pairs_file = config['datadir'] / "pairs.json" + pairs_file = config['datadir'] / 'pairs.json' if pairs_file.exists(): with pairs_file.open('r') as f: config['pairs'] = json_load(f) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 972961b36..2d45a7222 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -208,7 +208,7 @@ def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataF def load_trades(source: str, db_url: str, exportfilename: Path, no_trades: bool = False, strategy: Optional[str] = None) -> pd.DataFrame: """ - Based on configuration option "trade_source": + Based on configuration option 'trade_source': * loads data from DB (using `db_url`) * loads data from backtestfile (using `exportfilename`) :param source: "DB" or "file" - specify source to load from diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c9c5a0027..d84fe7b82 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -85,8 +85,8 @@ class Exchange: # Deep merge ft_has with default ft_has options self._ft_has = deep_merge_dicts(self._ft_has, deepcopy(self._ft_has_default)) - if exchange_config.get("_ft_has_params"): - self._ft_has = deep_merge_dicts(exchange_config.get("_ft_has_params"), + if exchange_config.get('_ft_has_params'): + self._ft_has = deep_merge_dicts(exchange_config.get('_ft_has_params'), self._ft_has) logger.info("Overriding exchange._ft_has with config params, result: %s", self._ft_has) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index b420db770..270fe615b 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -38,15 +38,15 @@ def init_plotscript(config): """ if "pairs" in config: - pairs = config["pairs"] + pairs = config['pairs'] else: - pairs = config["exchange"]["pair_whitelist"] + pairs = config['exchange']['pair_whitelist'] # Set timerange to use - timerange = TimeRange.parse_timerange(config.get("timerange")) + timerange = TimeRange.parse_timerange(config.get('timerange')) data = load_data( - datadir=config.get("datadir"), + datadir=config.get('datadir'), pairs=pairs, timeframe=config.get('timeframe', '5m'), timerange=timerange, @@ -67,7 +67,7 @@ def init_plotscript(config): db_url=config.get('db_url'), exportfilename=filename, no_trades=no_trades, - strategy=config.get("strategy"), + strategy=config.get('strategy'), ) trades = trim_dataframe(trades, timerange, 'open_date') @@ -491,13 +491,13 @@ def load_and_plot_trades(config: Dict[str, Any]): pair=pair, data=df_analyzed, trades=trades_pair, - indicators1=config.get("indicators1", []), - indicators2=config.get("indicators2", []), + indicators1=config.get('indicators1', []), + indicators2=config.get('indicators2', []), plot_config=strategy.plot_config if hasattr(strategy, 'plot_config') else {} ) store_plot_file(fig, filename=generate_plot_filename(pair, config['timeframe']), - directory=config['user_data_dir'] / "plot") + directory=config['user_data_dir'] / 'plot') logger.info('End of plotting process. %s plots generated', pair_counter) @@ -514,7 +514,7 @@ def plot_profit(config: Dict[str, Any]) -> None: # Filter trades to relevant pairs # Remove open pairs - we don't know the profit yet so can't calculate profit for these. # Also, If only one open pair is left, then the profit-generation would fail. - trades = trades[(trades['pair'].isin(plot_elements["pairs"])) + trades = trades[(trades['pair'].isin(plot_elements['pairs'])) & (~trades['close_date'].isnull()) ] if len(trades) == 0: @@ -523,7 +523,7 @@ def plot_profit(config: Dict[str, Any]) -> None: # Create an average close price of all the pairs that were involved. # this could be useful to gauge the overall market trend - fig = generate_profit_graph(plot_elements["pairs"], plot_elements["ohlcv"], + fig = generate_profit_graph(plot_elements['pairs'], plot_elements['ohlcv'], trades, config.get('timeframe', '5m')) store_plot_file(fig, filename='freqtrade-profit-plot.html', - directory=config['user_data_dir'] / "plot", auto_open=True) + directory=config['user_data_dir'] / 'plot', auto_open=True) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 51ea596f6..598a82040 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -276,11 +276,11 @@ def main(args): print_commands() sys.exit() - config = load_config(args["config"]) - url = config.get("api_server", {}).get("server_url", "127.0.0.1") - port = config.get("api_server", {}).get("listen_port", "8080") - username = config.get("api_server", {}).get("username") - password = config.get("api_server", {}).get("password") + config = load_config(args['config']) + url = config.get('api_server', {}).get('server_url', '127.0.0.1') + port = config.get('api_server', {}).get('listen_port', '8080') + username = config.get('api_server', {}).get('username') + password = config.get('api_server', {}).get('password') server_url = f"http://{url}:{port}" client = FtRestClient(server_url, username, password) diff --git a/tests/conftest.py b/tests/conftest.py index a40bfbc6c..dbed08ec5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -78,7 +78,7 @@ def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> No def get_patched_exchange(mocker, config, api_mock=None, id='bittrex', mock_markets=True) -> Exchange: patch_exchange(mocker, api_mock, id, mock_markets) - config["exchange"]["name"] = id + config['exchange']['name'] = id try: exchange = ExchangeResolver.load_exchange(id, config) except ImportError: diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index a3a2c9a1f..2513f751b 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -87,20 +87,20 @@ def test_api_unauthorized(botclient): assert rc.json == {'error': 'Unauthorized'} # Change only username - ftbot.config['api_server']['username'] = "Ftrader" + ftbot.config['api_server']['username'] = 'Ftrader' rc = client_get(client, f"{BASE_URI}/version") assert_response(rc, 401) assert rc.json == {'error': 'Unauthorized'} # Change only password ftbot.config['api_server']['username'] = _TEST_USER - ftbot.config['api_server']['password'] = "WrongPassword" + ftbot.config['api_server']['password'] = 'WrongPassword' rc = client_get(client, f"{BASE_URI}/version") assert_response(rc, 401) assert rc.json == {'error': 'Unauthorized'} - ftbot.config['api_server']['username'] = "Ftrader" - ftbot.config['api_server']['password'] = "WrongPassword" + ftbot.config['api_server']['username'] = 'Ftrader' + ftbot.config['api_server']['password'] = 'WrongPassword' rc = client_get(client, f"{BASE_URI}/version") assert_response(rc, 401) @@ -677,7 +677,7 @@ def test_api_forcebuy(botclient, mocker, fee): assert rc.json == {"error": "Error querying _forcebuy: Forcebuy not enabled."} # enable forcebuy - ftbot.config["forcebuy_enable"] = True + ftbot.config['forcebuy_enable'] = True fbuy_mock = MagicMock(return_value=None) mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) diff --git a/tests/test_arguments.py b/tests/test_arguments.py index 457683598..2af36277b 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -19,64 +19,64 @@ def test_parse_args_none() -> None: def test_parse_args_defaults(mocker) -> None: - mocker.patch.object(Path, "is_file", MagicMock(side_effect=[False, True])) + mocker.patch.object(Path, 'is_file', MagicMock(side_effect=[False, True])) args = Arguments(['trade']).get_parsed_arg() - assert args["config"] == ['config.json'] - assert args["strategy_path"] is None - assert args["datadir"] is None - assert args["verbosity"] == 0 + assert args['config'] == ['config.json'] + assert args['strategy_path'] is None + assert args['datadir'] is None + assert args['verbosity'] == 0 def test_parse_args_default_userdatadir(mocker) -> None: - mocker.patch.object(Path, "is_file", MagicMock(return_value=True)) + mocker.patch.object(Path, 'is_file', MagicMock(return_value=True)) args = Arguments(['trade']).get_parsed_arg() # configuration defaults to user_data if that is available. - assert args["config"] == [str(Path('user_data/config.json'))] - assert args["strategy_path"] is None - assert args["datadir"] is None - assert args["verbosity"] == 0 + assert args['config'] == [str(Path('user_data/config.json'))] + assert args['strategy_path'] is None + assert args['datadir'] is None + assert args['verbosity'] == 0 def test_parse_args_userdatadir(mocker) -> None: - mocker.patch.object(Path, "is_file", MagicMock(return_value=True)) + mocker.patch.object(Path, 'is_file', MagicMock(return_value=True)) args = Arguments(['trade', '--user-data-dir', 'user_data']).get_parsed_arg() # configuration defaults to user_data if that is available. - assert args["config"] == [str(Path('user_data/config.json'))] - assert args["strategy_path"] is None - assert args["datadir"] is None - assert args["verbosity"] == 0 + assert args['config'] == [str(Path('user_data/config.json'))] + assert args['strategy_path'] is None + assert args['datadir'] is None + assert args['verbosity'] == 0 def test_parse_args_config() -> None: args = Arguments(['trade', '-c', '/dev/null']).get_parsed_arg() - assert args["config"] == ['/dev/null'] + assert args['config'] == ['/dev/null'] args = Arguments(['trade', '--config', '/dev/null']).get_parsed_arg() - assert args["config"] == ['/dev/null'] + assert args['config'] == ['/dev/null'] args = Arguments(['trade', '--config', '/dev/null', '--config', '/dev/zero'],).get_parsed_arg() - assert args["config"] == ['/dev/null', '/dev/zero'] + assert args['config'] == ['/dev/null', '/dev/zero'] def test_parse_args_db_url() -> None: args = Arguments(['trade', '--db-url', 'sqlite:///test.sqlite']).get_parsed_arg() - assert args["db_url"] == 'sqlite:///test.sqlite' + assert args['db_url'] == 'sqlite:///test.sqlite' def test_parse_args_verbose() -> None: args = Arguments(['trade', '-v']).get_parsed_arg() - assert args["verbosity"] == 1 + assert args['verbosity'] == 1 args = Arguments(['trade', '--verbose']).get_parsed_arg() - assert args["verbosity"] == 1 + assert args['verbosity'] == 1 def test_common_scripts_options() -> None: args = Arguments(['download-data', '-p', 'ETH/BTC', 'XRP/BTC']).get_parsed_arg() - assert args["pairs"] == ['ETH/BTC', 'XRP/BTC'] - assert "func" in args + assert args['pairs'] == ['ETH/BTC', 'XRP/BTC'] + assert 'func' in args def test_parse_args_version() -> None: @@ -91,7 +91,7 @@ def test_parse_args_invalid() -> None: def test_parse_args_strategy() -> None: args = Arguments(['trade', '--strategy', 'SomeStrategy']).get_parsed_arg() - assert args["strategy"] == 'SomeStrategy' + assert args['strategy'] == 'SomeStrategy' def test_parse_args_strategy_invalid() -> None: @@ -101,7 +101,7 @@ def test_parse_args_strategy_invalid() -> None: def test_parse_args_strategy_path() -> None: args = Arguments(['trade', '--strategy-path', '/some/path']).get_parsed_arg() - assert args["strategy_path"] == '/some/path' + assert args['strategy_path'] == '/some/path' def test_parse_args_strategy_path_invalid() -> None: @@ -127,13 +127,13 @@ def test_parse_args_backtesting_custom() -> None: 'SampleStrategy' ] call_args = Arguments(args).get_parsed_arg() - assert call_args["config"] == ['test_conf.json'] - assert call_args["verbosity"] == 0 - assert call_args["command"] == 'backtesting' - assert call_args["func"] is not None - assert call_args["timeframe"] == '1m' - assert type(call_args["strategy_list"]) is list - assert len(call_args["strategy_list"]) == 2 + assert call_args['config'] == ['test_conf.json'] + assert call_args['verbosity'] == 0 + assert call_args['command'] == 'backtesting' + assert call_args['func'] is not None + assert call_args['timeframe'] == '1m' + assert type(call_args['strategy_list']) is list + assert len(call_args['strategy_list']) == 2 def test_parse_args_hyperopt_custom() -> None: @@ -144,13 +144,13 @@ def test_parse_args_hyperopt_custom() -> None: '--spaces', 'buy' ] call_args = Arguments(args).get_parsed_arg() - assert call_args["config"] == ['test_conf.json'] - assert call_args["epochs"] == 20 - assert call_args["verbosity"] == 0 - assert call_args["command"] == 'hyperopt' - assert call_args["spaces"] == ['buy'] - assert call_args["func"] is not None - assert callable(call_args["func"]) + assert call_args['config'] == ['test_conf.json'] + assert call_args['epochs'] == 20 + assert call_args['verbosity'] == 0 + assert call_args['command'] == 'hyperopt' + assert call_args['spaces'] == ['buy'] + assert call_args['func'] is not None + assert callable(call_args['func']) def test_download_data_options() -> None: @@ -163,10 +163,10 @@ def test_download_data_options() -> None: ] pargs = Arguments(args).get_parsed_arg() - assert pargs["pairs_file"] == 'file_with_pairs' - assert pargs["datadir"] == 'datadir/directory' - assert pargs["days"] == 30 - assert pargs["exchange"] == 'binance' + assert pargs['pairs_file'] == 'file_with_pairs' + assert pargs['datadir'] == 'datadir/directory' + assert pargs['days'] == 30 + assert pargs['exchange'] == 'binance' def test_plot_dataframe_options() -> None: @@ -180,10 +180,10 @@ def test_plot_dataframe_options() -> None: ] pargs = Arguments(args).get_parsed_arg() - assert pargs["indicators1"] == ["sma10", "sma100"] - assert pargs["indicators2"] == ["macd", "fastd", "fastk"] - assert pargs["plot_limit"] == 30 - assert pargs["pairs"] == ["UNITTEST/BTC"] + assert pargs['indicators1'] == ['sma10', 'sma100'] + assert pargs['indicators2'] == ['macd', 'fastd', 'fastk'] + assert pargs['plot_limit'] == 30 + assert pargs['pairs'] == ['UNITTEST/BTC'] def test_plot_profit_options() -> None: @@ -191,66 +191,66 @@ def test_plot_profit_options() -> None: 'plot-profit', '-p', 'UNITTEST/BTC', '--trade-source', 'DB', - "--db-url", "sqlite:///whatever.sqlite", + '--db-url', 'sqlite:///whatever.sqlite', ] pargs = Arguments(args).get_parsed_arg() - assert pargs["trade_source"] == "DB" - assert pargs["pairs"] == ["UNITTEST/BTC"] - assert pargs["db_url"] == "sqlite:///whatever.sqlite" + assert pargs['trade_source'] == 'DB' + assert pargs['pairs'] == ['UNITTEST/BTC'] + assert pargs['db_url'] == 'sqlite:///whatever.sqlite' def test_config_notallowed(mocker) -> None: - mocker.patch.object(Path, "is_file", MagicMock(return_value=False)) + mocker.patch.object(Path, 'is_file', MagicMock(return_value=False)) args = [ 'create-userdir', ] pargs = Arguments(args).get_parsed_arg() - assert "config" not in pargs + assert 'config' not in pargs # When file exists: - mocker.patch.object(Path, "is_file", MagicMock(return_value=True)) + mocker.patch.object(Path, 'is_file', MagicMock(return_value=True)) args = [ 'create-userdir', ] pargs = Arguments(args).get_parsed_arg() # config is not added even if it exists, since create-userdir is in the notallowed list - assert "config" not in pargs + assert 'config' not in pargs def test_config_notrequired(mocker) -> None: - mocker.patch.object(Path, "is_file", MagicMock(return_value=False)) + mocker.patch.object(Path, 'is_file', MagicMock(return_value=False)) args = [ 'download-data', ] pargs = Arguments(args).get_parsed_arg() - assert pargs["config"] is None + assert pargs['config'] is None # When file exists: - mocker.patch.object(Path, "is_file", MagicMock(side_effect=[False, True])) + mocker.patch.object(Path, 'is_file', MagicMock(side_effect=[False, True])) args = [ 'download-data', ] pargs = Arguments(args).get_parsed_arg() # config is added if it exists - assert pargs["config"] == ['config.json'] + assert pargs['config'] == ['config.json'] def test_check_int_positive() -> None: - assert check_int_positive("3") == 3 - assert check_int_positive("1") == 1 - assert check_int_positive("100") == 100 + assert check_int_positive('3') == 3 + assert check_int_positive('1') == 1 + assert check_int_positive('100') == 100 with pytest.raises(argparse.ArgumentTypeError): - check_int_positive("-2") + check_int_positive('-2') with pytest.raises(argparse.ArgumentTypeError): - check_int_positive("0") + check_int_positive('0') with pytest.raises(argparse.ArgumentTypeError): - check_int_positive("3.5") + check_int_positive('3.5') with pytest.raises(argparse.ArgumentTypeError): - check_int_positive("DeadBeef") + check_int_positive('DeadBeef') diff --git a/tests/test_configuration.py b/tests/test_configuration.py index ca5d6eadc..8549f00c9 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1005,7 +1005,7 @@ def test_pairlist_resolving_fallback(mocker): args = Arguments(arglist).get_parsed_arg() # Fix flaky tests if config.json exists - args["config"] = None + args['config'] = None configuration = Configuration(args, RunMode.OTHER) config = configuration.get_config() diff --git a/tests/test_main.py b/tests/test_main.py index d5309ae3f..dd0c877e8 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -44,19 +44,19 @@ def test_parse_args_backtesting(mocker) -> None: def test_main_start_hyperopt(mocker) -> None: - mocker.patch.object(Path, "is_file", MagicMock(side_effect=[False, True])) + mocker.patch.object(Path, 'is_file', MagicMock(side_effect=[False, True])) hyperopt_mock = mocker.patch('freqtrade.commands.start_hyperopt', MagicMock()) - hyperopt_mock.__name__ = PropertyMock("start_hyperopt") + hyperopt_mock.__name__ = PropertyMock('start_hyperopt') # it's sys.exit(0) at the end of hyperopt with pytest.raises(SystemExit): main(['hyperopt']) assert hyperopt_mock.call_count == 1 call_args = hyperopt_mock.call_args[0][0] - assert call_args["config"] == ['config.json'] - assert call_args["verbosity"] == 0 - assert call_args["command"] == 'hyperopt' - assert call_args["func"] is not None - assert callable(call_args["func"]) + assert call_args['config'] == ['config.json'] + assert call_args['verbosity'] == 0 + assert call_args['command'] == 'hyperopt' + assert call_args['func'] is not None + assert callable(call_args['func']) def test_main_fatal_exception(mocker, default_conf, caplog) -> None: diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 28c486877..bcababbf1 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -362,22 +362,22 @@ def test_start_plot_profit(mocker): def test_start_plot_profit_error(mocker): args = [ - "plot-profit", - "--pairs", "ETH/BTC" + 'plot-profit', + '--pairs', 'ETH/BTC' ] argsp = get_args(args) # Make sure we use no config. Details: #2241 # not resetting config causes random failures if config.json exists - argsp["config"] = [] + argsp['config'] = [] with pytest.raises(OperationalException): start_plot_profit(argsp) def test_plot_profit(default_conf, mocker, testdatadir, caplog): default_conf['trade_source'] = 'file' - default_conf["datadir"] = testdatadir - default_conf['exportfilename'] = testdatadir / "backtest-result_test_nofile.json" - default_conf['pairs'] = ["ETH/BTC", "LTC/BTC"] + default_conf['datadir'] = testdatadir + default_conf['exportfilename'] = testdatadir / 'backtest-result_test_nofile.json' + default_conf['pairs'] = ['ETH/BTC', 'LTC/BTC'] profit_mock = MagicMock() store_mock = MagicMock() From d161b94d7241e9f5c5bc873a8e8f1282aba1738e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Aug 2020 21:22:36 +0200 Subject: [PATCH 0523/1197] Allow simulating cancelled orders in dry-run --- freqtrade/exchange/exchange.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index d84fe7b82..b89da14eb 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -973,7 +973,12 @@ class Exchange: @retrier def cancel_order(self, order_id: str, pair: str) -> Dict: if self._config['dry_run']: - return {} + order = self._dry_run_open_orders.get(order_id) + if order: + order.update({'status': 'canceled', 'filled': 0.0, 'remaining': order['amount']}) + return order + else: + return {} try: return self._api.cancel_order(order_id, pair) From add78414e4c9aa1d1b3a9847d3ae0a20fb332e1d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Aug 2020 21:24:47 +0200 Subject: [PATCH 0524/1197] Don't overwrite cancel_reason --- freqtrade/constants.py | 1 + freqtrade/freqtradebot.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 1f8cebd0d..f44be220e 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -341,6 +341,7 @@ CANCEL_REASON = { "PARTIALLY_FILLED": "partially filled - keeping order open", "ALL_CANCELLED": "cancelled (all unfilled and partially filled open orders cancelled)", "CANCELLED_ON_EXCHANGE": "cancelled on exchange", + "FORCE_SELL": "forcesold", } # List of pairs with their timeframes diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index eee60cc22..917bb356f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -974,7 +974,8 @@ class FreqtradeBot: # Cancelled orders may have the status of 'canceled' or 'closed' if order['status'] not in ('canceled', 'closed'): - reason = constants.CANCEL_REASON['TIMEOUT'] + # TODO: this reason will overwrite the input in all cases + # reason = constants.CANCEL_REASON['TIMEOUT'] corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, trade.amount) # Avoid race condition where the order could not be cancelled coz its already filled. From 85e71275d3c555b04a7ab8bb15269c0a6711830f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Aug 2020 21:27:09 +0200 Subject: [PATCH 0525/1197] Simplify forcesell method by using freqtrade methods --- freqtrade/rpc/rpc.py | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 12e79d35b..25a85ac02 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -11,6 +11,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union import arrow from numpy import NAN, mean +from freqtrade.constants import CANCEL_REASON from freqtrade.exceptions import (ExchangeError, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs @@ -453,29 +454,22 @@ class RPC: """ def _exec_forcesell(trade: Trade) -> None: # Check if there is there is an open order + fully_canceled = False if trade.open_order_id: order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) - # Cancel open LIMIT_BUY orders and close trade - if order and order['status'] == 'open' \ - and order['type'] == 'limit' \ - and order['side'] == 'buy': - self._freqtrade.exchange.cancel_order(trade.open_order_id, trade.pair) - trade.close(order.get('price') or trade.open_rate) - # Do the best effort, if we don't know 'filled' amount, don't try selling - if order['filled'] is None: - return - trade.amount = order['filled'] + if order['side'] == 'buy': + fully_canceled = self._freqtrade.handle_cancel_buy( + trade, order, CANCEL_REASON['FORCE_SELL']) - # Ignore trades with an attached LIMIT_SELL order - if order and order['status'] == 'open' \ - and order['type'] == 'limit' \ - and order['side'] == 'sell': - return + if order['side'] == 'sell': + # Cancel order - so it is placed anew with a fresh price. + self._freqtrade.handle_cancel_sell(trade, order, CANCEL_REASON['FORCE_SELL']) - # Get current rate and execute sell - current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL) + if not fully_canceled: + # Get current rate and execute sell + current_rate = self._freqtrade.get_sell_rate(trade.pair, False) + self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL) # ---- EOF def _exec_forcesell ---- if self._freqtrade.state != State.RUNNING: From 5e75caa91778908abd14bbd8bcfc4c59e2d0c2ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Aug 2020 21:37:52 +0200 Subject: [PATCH 0526/1197] Adjust tests to new forcesell --- tests/rpc/test_rpc.py | 33 +++++++++++++++++++++++++++------ tests/rpc/test_rpc_telegram.py | 10 ++++++---- 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index c370dce8f..102ed12fe 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -669,7 +669,8 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: return_value={ 'status': 'closed', 'type': 'limit', - 'side': 'buy' + 'side': 'buy', + 'filled': 0.0, } ), get_fee=fee, @@ -695,6 +696,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: msg = rpc._rpc_forcesell('all') assert msg == {'result': 'Created sell orders for all open trades.'} + freqtradebot.enter_positions() msg = rpc._rpc_forcesell('1') assert msg == {'result': 'Created sell order for trade 1.'} @@ -707,17 +709,24 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: freqtradebot.state = State.RUNNING assert cancel_order_mock.call_count == 0 + freqtradebot.enter_positions() # make an limit-buy open trade trade = Trade.query.filter(Trade.id == '1').first() filled_amount = trade.amount / 2 + # Fetch order - it's open first, and closed after cancel_order is called. mocker.patch( 'freqtrade.exchange.Exchange.fetch_order', - return_value={ + side_effect=[{ 'status': 'open', 'type': 'limit', 'side': 'buy', 'filled': filled_amount - } + }, { + 'status': 'closed', + 'type': 'limit', + 'side': 'buy', + 'filled': filled_amount + }] ) # check that the trade is called, which is done by ensuring exchange.cancel_order is called # and trade amount is updated @@ -725,6 +734,16 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: assert cancel_order_mock.call_count == 1 assert trade.amount == filled_amount + mocker.patch( + 'freqtrade.exchange.Exchange.fetch_order', + return_value={ + 'status': 'open', + 'type': 'limit', + 'side': 'buy', + 'filled': filled_amount + }) + + freqtradebot.config['max_open_trades'] = 3 freqtradebot.enter_positions() trade = Trade.query.filter(Trade.id == '2').first() amount = trade.amount @@ -744,20 +763,22 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: assert cancel_order_mock.call_count == 2 assert trade.amount == amount - freqtradebot.enter_positions() # make an limit-sell open trade mocker.patch( 'freqtrade.exchange.Exchange.fetch_order', return_value={ 'status': 'open', 'type': 'limit', - 'side': 'sell' + 'side': 'sell', + 'amount': amount, + 'remaining': amount, + 'filled': 0.0 } ) msg = rpc._rpc_forcesell('3') assert msg == {'result': 'Created sell order for trade 3.'} # status quo, no exchange calls - assert cancel_order_mock.call_count == 2 + assert cancel_order_mock.call_count == 3 def test_performance_handle(default_conf, ticker, limit_buy_order, fee, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 10738ada3..b11409767 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -724,7 +724,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, context.args = ["1"] telegram._forcesell(update=update, context=context) - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 4 last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, @@ -783,7 +783,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, context.args = ["1"] telegram._forcesell(update=update, context=context) - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 4 last_msg = rpc_mock.call_args_list[-1][0][0] assert { @@ -833,8 +833,10 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None context.args = ["all"] telegram._forcesell(update=update, context=context) - assert rpc_mock.call_count == 4 - msg = rpc_mock.call_args_list[0][0][0] + # Called for all trades 3 times + # cancel notification (wtf??), sell notification, buy_cancel + assert rpc_mock.call_count == 12 + msg = rpc_mock.call_args_list[2][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, From 9c0a3fffd733dc22f0498ea9cb72758d61ff0738 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Aug 2020 22:17:43 +0200 Subject: [PATCH 0527/1197] Avoid double notifications in case of partially filled buy orders --- freqtrade/constants.py | 4 +++- freqtrade/freqtradebot.py | 15 +++++++-------- freqtrade/rpc/telegram.py | 2 +- tests/rpc/test_rpc_telegram.py | 6 ++++-- tests/test_freqtradebot.py | 6 +++--- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index f44be220e..b92ab3eeb 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -338,7 +338,9 @@ SCHEMA_MINIMAL_REQUIRED = [ CANCEL_REASON = { "TIMEOUT": "cancelled due to timeout", - "PARTIALLY_FILLED": "partially filled - keeping order open", + "PARTIALLY_FILLED_KEEP_OPEN": "partially filled - keeping order open", + "PARTIALLY_FILLED": "partially filled", + "FULLY_CANCELLED": "fully cancelled", "ALL_CANCELLED": "cancelled (all unfilled and partially filled open orders cancelled)", "CANCELLED_ON_EXCHANGE": "cancelled on exchange", "FORCE_SELL": "forcesold", diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 917bb356f..d53902633 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -618,7 +618,7 @@ class FreqtradeBot: # Send the message self.rpc.send_msg(msg) - def _notify_buy_cancel(self, trade: Trade, order_type: str) -> None: + def _notify_buy_cancel(self, trade: Trade, order_type: str, reason: str) -> None: """ Sends rpc notification when a buy cancel occured. """ @@ -637,6 +637,7 @@ class FreqtradeBot: 'amount': trade.amount, 'open_date': trade.open_date, 'current_rate': current_rate, + 'reason': reason, } # Send the message @@ -993,13 +994,13 @@ class FreqtradeBot: # Using filled to determine the filled amount filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled') - if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC): logger.info('Buy order fully cancelled. Removing %s from database.', trade) # if trade is not partially completed, just delete the trade Trade.session.delete(trade) Trade.session.flush() was_trade_fully_canceled = True + reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}" else: # if trade is partially complete, edit the stake details for the trade # and close the order @@ -1012,13 +1013,11 @@ class FreqtradeBot: trade.open_order_id = None logger.info('Partial buy order timeout for %s.', trade) - self.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': f'Remaining buy order for {trade.pair} cancelled due to timeout' - }) + reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}" self.wallets.update() - self._notify_buy_cancel(trade, order_type=self.strategy.order_types['buy']) + self._notify_buy_cancel(trade, order_type=self.strategy.order_types['buy'], + reason=reason) return was_trade_fully_canceled def handle_cancel_sell(self, trade: Trade, order: Dict, reason: str) -> str: @@ -1049,7 +1048,7 @@ class FreqtradeBot: trade.open_order_id = None else: # TODO: figure out how to handle partially complete sell orders - reason = constants.CANCEL_REASON['PARTIALLY_FILLED'] + reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] self.wallets.update() self._notify_sell_cancel( diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 458007c04..ecf907f54 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -149,7 +149,7 @@ class Telegram(RPC): elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: message = ("\N{WARNING SIGN} *{exchange}:* " - "Cancelling Open Buy Order for {pair}".format(**msg)) + "Cancelling open buy Order for {pair}. Reason: {reason}.".format(**msg)) elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: msg['amount'] = round(msg['amount'], 8) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index b11409767..145df9ed7 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -14,6 +14,7 @@ from telegram import Chat, Message, Update from telegram.error import NetworkError from freqtrade import __version__ +from freqtrade.constants import CANCEL_REASON from freqtrade.edge import PairInfo from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade @@ -1310,9 +1311,10 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', + 'reason': CANCEL_REASON['TIMEOUT'] }) - assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Bittrex:* Cancelling Open Buy Order for ETH/BTC') + assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Bittrex:* ' + 'Cancelling open buy Order for ETH/BTC. Reason: cancelled due to timeout.') def test_send_msg_sell_notification(default_conf, mocker) -> None: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 0d7968e26..7b4ed47f1 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2527,13 +2527,13 @@ def test_handle_cancel_sell_limit(mocker, default_conf, fee) -> None: send_msg_mock.reset_mock() order['amount'] = 2 - assert freqtrade.handle_cancel_sell(trade, order, reason) == CANCEL_REASON['PARTIALLY_FILLED'] + assert freqtrade.handle_cancel_sell(trade, order, reason) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] # Assert cancel_order was not called (callcount remains unchanged) assert cancel_order_mock.call_count == 1 assert send_msg_mock.call_count == 1 - assert freqtrade.handle_cancel_sell(trade, order, reason) == CANCEL_REASON['PARTIALLY_FILLED'] + assert freqtrade.handle_cancel_sell(trade, order, reason) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] # Message should not be iterated again - assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED'] + assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] assert send_msg_mock.call_count == 1 From b2373fccfd8a5441c4639f8934d344a40a921e2b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 26 Aug 2020 22:24:45 +0200 Subject: [PATCH 0528/1197] Adjust tests as send_msg is only called once --- freqtrade/freqtradebot.py | 2 -- tests/rpc/test_rpc_telegram.py | 10 +++++----- tests/test_freqtradebot.py | 12 +++++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d53902633..66d687536 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -975,8 +975,6 @@ class FreqtradeBot: # Cancelled orders may have the status of 'canceled' or 'closed' if order['status'] not in ('canceled', 'closed'): - # TODO: this reason will overwrite the input in all cases - # reason = constants.CANCEL_REASON['TIMEOUT'] corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, trade.amount) # Avoid race condition where the order could not be cancelled coz its already filled. diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 145df9ed7..a5e501390 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -725,7 +725,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, context.args = ["1"] telegram._forcesell(update=update, context=context) - assert rpc_mock.call_count == 4 + assert rpc_mock.call_count == 3 last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, @@ -784,7 +784,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, context.args = ["1"] telegram._forcesell(update=update, context=context) - assert rpc_mock.call_count == 4 + assert rpc_mock.call_count == 3 last_msg = rpc_mock.call_args_list[-1][0][0] assert { @@ -836,8 +836,8 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None # Called for all trades 3 times # cancel notification (wtf??), sell notification, buy_cancel - assert rpc_mock.call_count == 12 - msg = rpc_mock.call_args_list[2][0][0] + assert rpc_mock.call_count == 8 + msg = rpc_mock.call_args_list[1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, @@ -1314,7 +1314,7 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: 'reason': CANCEL_REASON['TIMEOUT'] }) assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Bittrex:* ' - 'Cancelling open buy Order for ETH/BTC. Reason: cancelled due to timeout.') + 'Cancelling open buy Order for ETH/BTC. Reason: cancelled due to timeout.') def test_send_msg_sell_notification(default_conf, mocker) -> None: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 7b4ed47f1..ac6d3791a 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2289,7 +2289,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old # note this is for a partially-complete buy order freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 1 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() assert len(trades) == 1 assert trades[0].amount == 23.0 @@ -2324,7 +2324,7 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap assert log_has_re(r"Applying fee on amount for Trade.*", caplog) assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 1 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() assert len(trades) == 1 # Verify that trade has been updated @@ -2364,7 +2364,7 @@ def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade, assert log_has_re(r"Could not update trade amount: .*", caplog) assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 1 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() assert len(trades) == 1 # Verify that trade has been updated @@ -2527,11 +2527,13 @@ def test_handle_cancel_sell_limit(mocker, default_conf, fee) -> None: send_msg_mock.reset_mock() order['amount'] = 2 - assert freqtrade.handle_cancel_sell(trade, order, reason) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] + assert freqtrade.handle_cancel_sell(trade, order, reason + ) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] # Assert cancel_order was not called (callcount remains unchanged) assert cancel_order_mock.call_count == 1 assert send_msg_mock.call_count == 1 - assert freqtrade.handle_cancel_sell(trade, order, reason) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] + assert freqtrade.handle_cancel_sell(trade, order, reason + ) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] # Message should not be iterated again assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] assert send_msg_mock.call_count == 1 From bf5a082358f93adfd2bbe771b76d7ab7928fc3cf Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 Aug 2020 11:37:20 +0200 Subject: [PATCH 0529/1197] bufferhandler should log right from the beginning --- freqtrade/loggers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py index 263f97ce1..8f5da9bee 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers.py @@ -41,13 +41,14 @@ def setup_logging_pre() -> None: """ Early setup for logging. Uses INFO loglevel and only the Streamhandler. - Early messages (before proper logging setup) will therefore only be available - after the proper logging setup. + Early messages (before proper logging setup) will therefore only be sent to additional + logging handlers after the real initialization, because we don't know which + ones the user desires beforehand. """ logging.basicConfig( level=logging.INFO, format=LOGFORMAT, - handlers=[logging.StreamHandler(sys.stderr)] + handlers=[logging.StreamHandler(sys.stderr), bufferHandler] ) From cf719bc5d32a53aea394e8a2473812dff1412340 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 Aug 2020 12:04:55 +0200 Subject: [PATCH 0530/1197] Fix logformat to use epoch timestamp in ms --- freqtrade/rpc/rpc.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b7a4f4f8c..fed170001 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -640,10 +640,15 @@ class RPC: else: buffer = bufferHandler.buffer records = [[datetime.fromtimestamp(r.created).strftime("%Y-%m-%d %H:%M:%S"), - r.created, r.name, r.levelname, + r.created * 1000, r.name, r.levelname, r.message + ('\n' + r.exc_text if r.exc_text else '')] for r in buffer] + # Logs format: + # [logtime-formatted, logepoch, logger-name, loglevel, message \n + exception] + # e.g. ["2020-08-27 11:35:01", 1598520901097.9397, + # "freqtrade.worker", "INFO", "Starting worker develop"] + return {'log_count': len(records), 'logs': records} def _rpc_edge(self) -> List[Dict[str, Any]]: From dc6d71f651cc087ba943efe2e9e73d399f75bce5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 27 Aug 2020 14:41:31 +0200 Subject: [PATCH 0531/1197] Improve comment formatting --- freqtrade/rpc/rpc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index fed170001..13a799ef2 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -644,10 +644,10 @@ class RPC: r.message + ('\n' + r.exc_text if r.exc_text else '')] for r in buffer] - # Logs format: + # Log format: # [logtime-formatted, logepoch, logger-name, loglevel, message \n + exception] # e.g. ["2020-08-27 11:35:01", 1598520901097.9397, - # "freqtrade.worker", "INFO", "Starting worker develop"] + # "freqtrade.worker", "INFO", "Starting worker develop"] return {'log_count': len(records), 'logs': records} From 289425a434e5e7b3f010beca8639623a5e6982a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Aug 2020 10:07:02 +0200 Subject: [PATCH 0532/1197] Add test for dry-run-cancel order --- tests/exchange/test_exchange.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 571053b44..c254d6a09 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1761,6 +1761,14 @@ def test_cancel_order_dry_run(default_conf, mocker, exchange_name): assert exchange.cancel_order(order_id='123', pair='TKN/BTC') == {} assert exchange.cancel_stoploss_order(order_id='123', pair='TKN/BTC') == {} + order = exchange.buy('ETH/BTC', 'limit', 5, 0.55, 'gtc') + + cancel_order = exchange.cancel_order(order_id=order['id'], pair='ETH/BTC') + assert order['id'] == cancel_order['id'] + assert order['amount'] == cancel_order['amount'] + assert order['pair'] == cancel_order['pair'] + assert cancel_order['status'] == 'canceled' + @pytest.mark.parametrize("exchange_name", EXCHANGES) @pytest.mark.parametrize("order,result", [ From a595d23bf16b7488ac5c5c0e3d16ac7c2fa96411 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Aug 2020 10:14:49 +0200 Subject: [PATCH 0533/1197] Improve comment in test --- tests/rpc/test_rpc_telegram.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index bce20a043..51298d8f3 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -835,8 +835,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None context.args = ["all"] telegram._forcesell(update=update, context=context) - # Called for all trades 3 times - # cancel notification (wtf??), sell notification, buy_cancel + # Called for each trade 3 times assert rpc_mock.call_count == 8 msg = rpc_mock.call_args_list[1][0][0] assert { From 2ae04af6946097b555d28addecd771bb44ca707d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 29 Aug 2020 10:26:26 +0200 Subject: [PATCH 0534/1197] Improve some doc wording --- README.md | 1 - docs/deprecated.md | 11 +++++------ tests/rpc/test_rpc_telegram.py | 1 - 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7e0acde46..90f303c6d 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,6 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor - `/help`: Show help message - `/version`: Show version - ## Development branches The project is currently setup in two main branches: diff --git a/docs/deprecated.md b/docs/deprecated.md index a7b57b10e..44f0b686a 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -9,21 +9,20 @@ and are no longer supported. Please avoid their usage in your configuration. ### the `--refresh-pairs-cached` command line option `--refresh-pairs-cached` in the context of backtesting, hyperopt and edge allows to refresh candle data for backtesting. -Since this leads to much confusion, and slows down backtesting (while not being part of backtesting) this has been singled out -as a seperate freqtrade subcommand `freqtrade download-data`. +Since this leads to much confusion, and slows down backtesting (while not being part of backtesting) this has been singled out as a separate freqtrade sub-command `freqtrade download-data`. -This command line option was deprecated in 2019.7-dev (develop branch) and removed in 2019.9 (master branch). +This command line option was deprecated in 2019.7-dev (develop branch) and removed in 2019.9. ### The **--dynamic-whitelist** command line option This command line option was deprecated in 2018 and removed freqtrade 2019.6-dev (develop branch) -and in freqtrade 2019.7 (master branch). +and in freqtrade 2019.7. ### the `--live` command line option `--live` in the context of backtesting allowed to download the latest tick data for backtesting. Did only download the latest 500 candles, so was ineffective in getting good backtest data. -Removed in 2019-7-dev (develop branch) and in freqtrade 2019-8 (master branch) +Removed in 2019-7-dev (develop branch) and in freqtrade 2019.8. ### Allow running multiple pairlists in sequence @@ -31,6 +30,6 @@ The former `"pairlist"` section in the configuration has been removed, and is re The old section of configuration parameters (`"pairlist"`) has been deprecated in 2019.11 and has been removed in 2020.4. -### deprecation of bidVolume and askVolume from volumepairlist +### deprecation of bidVolume and askVolume from volume-pairlist Since only quoteVolume can be compared between assets, the other options (bidVolume, askVolume) have been deprecated in 2020.4. diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index bb63f283a..c962f68db 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1127,7 +1127,6 @@ def test_telegram_logs(default_conf, update, mocker) -> None: telegram._logs(update=update, context=context) assert msg_mock.call_count == 1 assert "freqtrade\\.rpc\\.telegram" in msg_mock.call_args_list[0][0][0] - assert "freqtrade\\.resolvers\\.iresolver" in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() context.args = ["1"] From 284d39930fc6b94912cdb4d45bc8e8df7307feb0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Aug 2020 10:07:28 +0200 Subject: [PATCH 0535/1197] Allow using pairlists through dataprovider in backtesting --- freqtrade/data/dataprovider.py | 6 ++++++ freqtrade/optimize/backtesting.py | 1 + 2 files changed, 7 insertions(+) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 3b4de823f..ccb6cbf56 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -39,6 +39,12 @@ class DataProvider: """ self.__cached_pairs[(pair, timeframe)] = (dataframe, Arrow.utcnow().datetime) + def add_pairlisthandler(self, pairlists) -> None: + """ + Allow adding pairlisthandler after initialization + """ + self._pairlists = pairlists + def refresh(self, pairlist: ListPairsWithTimeframes, helping_pairs: ListPairsWithTimeframes = None) -> None: diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3bd75f61a..005ec9fb8 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -96,6 +96,7 @@ class Backtesting: "PrecisionFilter not allowed for backtesting multiple strategies." ) + dataprovider.add_pairlisthandler(self.pairlists) self.pairlists.refresh_pairlist() if len(self.pairlists.whitelist) == 0: From 842eff95eba81ddaa34e71ac1d4082ed163d6630 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Aug 2020 10:07:58 +0200 Subject: [PATCH 0536/1197] Add simple verification to ensure pairlists is iitialized --- tests/optimize/test_backtesting.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 52d8f217c..f5c313520 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -359,6 +359,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: ] for line in exists: assert log_has(line, caplog) + assert backtesting.strategy.dp._pairlists is not None def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None: From 3d39f05c8fabe2d237a9207aab026dcbc6878fbe Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Aug 2020 10:23:14 +0200 Subject: [PATCH 0537/1197] Improve release documetation --- docs/developer.md | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index f09ae2c76..8bee1fd8e 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -52,6 +52,7 @@ The fastest and easiest way to start up is to use docker-compose.develop which g * [docker-compose](https://docs.docker.com/compose/install/) #### Starting the bot + ##### Use the develop dockerfile ``` bash @@ -74,7 +75,7 @@ docker-compose up docker-compose build ``` -##### Execing (effectively SSH into the container) +##### Executing (effectively SSH into the container) The `exec` command requires that the container already be running, if you want to start it that can be effected by `docker-compose up` or `docker-compose run freqtrade_develop` @@ -127,7 +128,7 @@ First of all, have a look at the [VolumePairList](https://github.com/freqtrade/f This is a simple Handler, which however serves as a good example on how to start developing. -Next, modify the classname of the Handler (ideally align this with the module filename). +Next, modify the class-name of the Handler (ideally align this with the module filename). The base-class provides an instance of the exchange (`self._exchange`) the pairlist manager (`self._pairlistmanager`), as well as the main configuration (`self._config`), the pairlist dedicated configuration (`self._pairlistconfig`) and the absolute position within the list of pairlists. @@ -147,7 +148,7 @@ Configuration for the chain of Pairlist Handlers is done in the bot configuratio By convention, `"number_assets"` is used to specify the maximum number of pairs to keep in the pairlist. Please follow this to ensure a consistent user experience. -Additional parameters can be configured as needed. For instance, `VolumePairList` uses `"sort_key"` to specify the sorting value - however feel free to specify whatever is necessary for your great algorithm to be successfull and dynamic. +Additional parameters can be configured as needed. For instance, `VolumePairList` uses `"sort_key"` to specify the sorting value - however feel free to specify whatever is necessary for your great algorithm to be successful and dynamic. #### short_desc @@ -163,7 +164,7 @@ This is called with each iteration of the bot (only if the Pairlist Handler is a It must return the resulting pairlist (which may then be passed into the chain of Pairlist Handlers). -Validations are optional, the parent class exposes a `_verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filtering. Use this if you limit your result to a certain number of pairs - so the endresult is not shorter than expected. +Validations are optional, the parent class exposes a `_verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filtering. Use this if you limit your result to a certain number of pairs - so the end-result is not shorter than expected. #### filter_pairlist @@ -171,13 +172,13 @@ This method is called for each Pairlist Handler in the chain by the pairlist man This is called with each iteration of the bot - so consider implementing caching for compute/network heavy calculations. -It get's passed a pairlist (which can be the result of previous pairlists) as well as `tickers`, a pre-fetched version of `get_tickers()`. +It gets passed a pairlist (which can be the result of previous pairlists) as well as `tickers`, a pre-fetched version of `get_tickers()`. The default implementation in the base class simply calls the `_validate_pair()` method for each pair in the pairlist, but you may override it. So you should either implement the `_validate_pair()` in your Pairlist Handler or override `filter_pairlist()` to do something else. If overridden, it must return the resulting pairlist (which may then be passed into the next Pairlist Handler in the chain). -Validations are optional, the parent class exposes a `_verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filters. Use this if you limit your result to a certain number of pairs - so the endresult is not shorter than expected. +Validations are optional, the parent class exposes a `_verify_blacklist(pairlist)` and `_whitelist_for_active_markets(pairlist)` to do default filters. Use this if you limit your result to a certain number of pairs - so the end result is not shorter than expected. In `VolumePairList`, this implements different methods of sorting, does early validation so only the expected number of pairs is returned. @@ -201,7 +202,7 @@ Most exchanges supported by CCXT should work out of the box. Check if the new exchange supports Stoploss on Exchange orders through their API. -Since CCXT does not provide unification for Stoploss On Exchange yet, we'll need to implement the exchange-specific parameters ourselfs. Best look at `binance.py` for an example implementation of this. You'll need to dig through the documentation of the Exchange's API on how exactly this can be done. [CCXT Issues](https://github.com/ccxt/ccxt/issues) may also provide great help, since others may have implemented something similar for their projects. +Since CCXT does not provide unification for Stoploss On Exchange yet, we'll need to implement the exchange-specific parameters ourselves. Best look at `binance.py` for an example implementation of this. You'll need to dig through the documentation of the Exchange's API on how exactly this can be done. [CCXT Issues](https://github.com/ccxt/ccxt/issues) may also provide great help, since others may have implemented something similar for their projects. ### Incomplete candles @@ -274,6 +275,7 @@ git checkout -b new_release Determine if crucial bugfixes have been made between this commit and the current state, and eventually cherry-pick these. +* Merge the release branch (master) into this branch. * Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for July 2019). Minor versions can be `2019.7.1` should we need to do a second release that month. Version numbers must follow allowed versions from PEP0440 to avoid failures pushing to pypi. * Commit this part * push that branch to the remote and create a PR against the master branch @@ -281,14 +283,14 @@ Determine if crucial bugfixes have been made between this commit and the current ### Create changelog from git commits !!! Note - Make sure that the master branch is uptodate! + Make sure that the master branch is up-to-date! ``` bash # Needs to be done before merging / pulling that branch. 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. +To keep the release-log short, best wrap the full git changelog into a collapsible details section. ```markdown
    @@ -312,6 +314,9 @@ Once the PR against master is merged (best right after merging): ### pypi +!!! Note + This process is now automated as part of Github Actions. + To create a pypi release, please run the following commands: Additional requirement: `wheel`, `twine` (for uploading), account on pypi with proper permissions. From 7f74ff53b12c5baff8c497227b8dd49ddcc4d499 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 31 Aug 2020 07:34:43 +0200 Subject: [PATCH 0538/1197] Move clock warning to installation pages --- docs/bot-usage.md | 3 +++ docs/docker.md | 3 +++ docs/index.md | 6 +----- docs/installation.md | 3 +++ 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 40ff3d82b..4a4496bbc 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -5,6 +5,9 @@ This page explains the different parameters of the bot and how to run it. !!! Note If you've used `setup.sh`, don't forget to activate your virtual environment (`source .env/bin/activate`) before running freqtrade commands. +!!! Warning "Up-to-date clock" + The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges. + ## Bot commands ``` diff --git a/docs/docker.md b/docs/docker.md index 92478088a..b9508648b 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -12,6 +12,9 @@ Optionally, [docker-compose](https://docs.docker.com/compose/install/) should be Once you have Docker installed, simply prepare the config file (e.g. `config.json`) and run the image for `freqtrade` as explained below. +!!! Warning "Up-to-date clock" + The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges. + ## Freqtrade with docker-compose Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) ready for usage. diff --git a/docs/index.md b/docs/index.md index adc661300..397c549aa 100644 --- a/docs/index.md +++ b/docs/index.md @@ -37,13 +37,9 @@ Freqtrade is a crypto-currency algorithmic trading software developed in python ## Requirements -### Up to date clock - -The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges. - ### Hardware requirements -To run this bot we recommend you a cloud instance with a minimum of: +To run this bot we recommend you a linux cloud instance with a minimum of: - 2GB RAM - 1GB disk space diff --git a/docs/installation.md b/docs/installation.md index c03be55d1..ec5e40965 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -18,6 +18,9 @@ Click each one for install guide: We also recommend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot), which is optional but recommended. +!!! Warning "Up-to-date clock" + The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges. + ## Quick start Freqtrade provides the Linux/MacOS Easy Installation script to install all dependencies and help you configure the bot. From f83633ff4e9ded4689ccc70a0af9b7efe0ea26fc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2020 06:38:24 +0000 Subject: [PATCH 0539/1197] Bump prompt-toolkit from 3.0.6 to 3.0.7 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.6 to 3.0.7. - [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases) - [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG) - [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/compare/3.0.6...3.0.7) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index b6e2d329f..9e1dbf86f 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -32,4 +32,4 @@ flask-cors==3.0.8 colorama==0.4.3 # Building config files interactively questionary==1.5.2 -prompt-toolkit==3.0.6 +prompt-toolkit==3.0.7 From 4adf012ee68f200ab185a49a0b9c437e22ee2f8d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2020 06:38:35 +0000 Subject: [PATCH 0540/1197] Bump flask-cors from 3.0.8 to 3.0.9 Bumps [flask-cors](https://github.com/corydolphin/flask-cors) from 3.0.8 to 3.0.9. - [Release notes](https://github.com/corydolphin/flask-cors/releases) - [Changelog](https://github.com/corydolphin/flask-cors/blob/master/CHANGELOG.md) - [Commits](https://github.com/corydolphin/flask-cors/compare/3.0.8...3.0.9) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index b6e2d329f..d543f206d 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -26,7 +26,7 @@ sdnotify==0.3.2 # Api server flask==1.1.2 flask-jwt-extended==3.24.1 -flask-cors==3.0.8 +flask-cors==3.0.9 # Support for colorized terminal output colorama==0.4.3 From 821af9be9e36ff5a5c4f632c309409034467c8a0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2020 06:38:36 +0000 Subject: [PATCH 0541/1197] Bump mkdocs-material from 5.5.8 to 5.5.11 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 5.5.8 to 5.5.11. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/5.5.8...5.5.11) Signed-off-by: dependabot[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 5226db750..c8f08d12a 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==5.5.8 +mkdocs-material==5.5.11 mdx_truly_sane_lists==1.2 From 55a49bfc5325364a0c5da7190e723b2a3c261089 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2020 06:38:38 +0000 Subject: [PATCH 0542/1197] Bump progressbar2 from 3.51.4 to 3.52.1 Bumps [progressbar2](https://github.com/WoLpH/python-progressbar) from 3.51.4 to 3.52.1. - [Release notes](https://github.com/WoLpH/python-progressbar/releases) - [Changelog](https://github.com/WoLpH/python-progressbar/blob/develop/CHANGES.rst) - [Commits](https://github.com/WoLpH/python-progressbar/compare/v3.51.4...v3.52.1) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index ce08f08e0..fbc679eaa 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -7,4 +7,4 @@ scikit-learn==0.23.1 scikit-optimize==0.7.4 filelock==3.0.12 joblib==0.16.0 -progressbar2==3.51.4 +progressbar2==3.52.1 From 8969ab4aa3026f3536980a34d6612bdb0a9dde38 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2020 06:38:55 +0000 Subject: [PATCH 0543/1197] Bump pytest-mock from 3.3.0 to 3.3.1 Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.3.0 to 3.3.1. - [Release notes](https://github.com/pytest-dev/pytest-mock/releases) - [Changelog](https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.3.0...v3.3.1) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1f5b68a73..44f0c7265 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,7 +11,7 @@ mypy==0.782 pytest==6.0.1 pytest-asyncio==0.14.0 pytest-cov==2.10.1 -pytest-mock==3.3.0 +pytest-mock==3.3.1 pytest-random-order==1.0.4 # Convert jupyter notebooks to markdown documents From c5b8993e9d061e7f839be9c40bc54769ec997991 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 31 Aug 2020 06:38:56 +0000 Subject: [PATCH 0544/1197] Bump ccxt from 1.33.52 to 1.33.72 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.33.52 to 1.33.72. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.33.52...1.33.72) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index b6e2d329f..6f4ae45b3 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.33.52 +ccxt==1.33.72 SQLAlchemy==1.3.19 python-telegram-bot==12.8 arrow==0.16.0 From 24df8d6bf5c81c3bfc9127b0d3cd1c01b4a99780 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 31 Aug 2020 15:46:31 +0200 Subject: [PATCH 0545/1197] Sort imports --- tests/data/test_history.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index c89156f4c..2d6aed8f6 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -1,6 +1,5 @@ # pragma pylint: disable=missing-docstring, protected-access, C0103 -from freqtrade.data.history.hdf5datahandler import HDF5DataHandler import json import uuid from pathlib import Path @@ -15,6 +14,7 @@ from pandas.testing import assert_frame_equal from freqtrade.configuration import TimeRange from freqtrade.constants import AVAILABLE_DATAHANDLERS from freqtrade.data.converter import ohlcv_to_dataframe +from freqtrade.data.history.hdf5datahandler import HDF5DataHandler from freqtrade.data.history.history_utils import ( _download_pair_history, _download_trades_history, _load_cached_data_for_updating, convert_trades_to_ohlcv, get_timerange, From a4e3edbcc597fc05f9be1eec2aa827593e1d33e7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Sep 2020 07:10:48 +0200 Subject: [PATCH 0546/1197] Fix stoploss_last_update beein updated with date object in wrong timezone --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index eee60cc22..768f283ab 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -835,7 +835,7 @@ class FreqtradeBot: stop_price = trade.open_rate * (1 + stoploss) if self.create_stoploss_order(trade=trade, stop_price=stop_price): - trade.stoploss_last_update = datetime.now() + trade.stoploss_last_update = datetime.utcnow() return False # If stoploss order is canceled for some reason we add it From 3bc6cb36c60155a8de4e25cfd1879d1b260a4348 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Sep 2020 08:00:20 +0200 Subject: [PATCH 0547/1197] Remove deprectead volumepairlist options --- docs/deprecated.md | 2 +- freqtrade/pairlist/VolumePairList.py | 7 +------ tests/pairlist/test_pairlist.py | 13 ------------- 3 files changed, 2 insertions(+), 20 deletions(-) diff --git a/docs/deprecated.md b/docs/deprecated.md index 44f0b686a..312f2c74f 100644 --- a/docs/deprecated.md +++ b/docs/deprecated.md @@ -32,4 +32,4 @@ The old section of configuration parameters (`"pairlist"`) has been deprecated i ### deprecation of bidVolume and askVolume from volume-pairlist -Since only quoteVolume can be compared between assets, the other options (bidVolume, askVolume) have been deprecated in 2020.4. +Since only quoteVolume can be compared between assets, the other options (bidVolume, askVolume) have been deprecated in 2020.4, and have been removed in 2020.9. diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 35dce93eb..44e5c52d7 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -14,7 +14,7 @@ from freqtrade.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) -SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume'] +SORT_VALUES = ['quoteVolume'] class VolumePairList(IPairList): @@ -45,11 +45,6 @@ class VolumePairList(IPairList): raise OperationalException( f'key {self._sort_key} not in {SORT_VALUES}') - if self._sort_key != 'quoteVolume': - logger.warning( - "DEPRECATED: using any key other than quoteVolume for VolumePairList is deprecated." - ) - @property def needstickers(self) -> bool: """ diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 9217abc46..a5e479912 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -231,9 +231,6 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # VolumePairList only ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC', 'HOT/BTC']), - # Different sorting depending on quote or bid volume - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}], - "BTC", ['HOT/BTC', 'FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']), ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}], "USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT', 'ADADOUBLE/USDT']), # No pair for ETH, VolumePairList @@ -263,10 +260,6 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "PrecisionFilter"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']), - # Precisionfilter bid - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}, - {"method": "PrecisionFilter"}], - "BTC", ['FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']), # PriceFilter and VolumePairList ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "PriceFilter", "low_price_ratio": 0.03}], @@ -293,9 +286,6 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "StaticPairList"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), # Static Pairlist before VolumePairList - sorting changes - ([{"method": "StaticPairList"}, - {"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}], - "BTC", ['HOT/BTC', 'TKN/BTC', 'ETH/BTC']), # SpreadFilter ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}], @@ -344,9 +334,6 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "SpreadFilter", "max_spread_ratio": 0.005}], "BTC", 'filter_at_the_beginning'), # OperationalException expected # Static Pairlist after VolumePairList, on a non-first position - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}, - {"method": "StaticPairList"}], - "BTC", 'static_in_the_middle'), ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, {"method": "PriceFilter", "low_price_ratio": 0.02}], "USDT", ['ETH/USDT', 'NANO/USDT']), From d44418282935c97eb17181fa4e564bc2abf10d61 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Sep 2020 10:31:11 +0200 Subject: [PATCH 0548/1197] Reinstate wrongly removed pairlist test --- tests/pairlist/test_pairlist.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index a5e479912..1f05bef1e 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -334,6 +334,9 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "SpreadFilter", "max_spread_ratio": 0.005}], "BTC", 'filter_at_the_beginning'), # OperationalException expected # Static Pairlist after VolumePairList, on a non-first position + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "StaticPairList"}], + "BTC", 'static_in_the_middle'), ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, {"method": "PriceFilter", "low_price_ratio": 0.02}], "USDT", ['ETH/USDT', 'NANO/USDT']), From dff0ac276803e480c03ba6fb33620b7179307824 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Sep 2020 19:16:11 +0200 Subject: [PATCH 0549/1197] Remove trailing_stop from default config example - it'll be misleading --- config.json.example | 1 - config_binance.json.example | 1 - config_kraken.json.example | 1 - 3 files changed, 3 deletions(-) diff --git a/config.json.example b/config.json.example index 77a147d0c..ab517b77c 100644 --- a/config.json.example +++ b/config.json.example @@ -7,7 +7,6 @@ "timeframe": "5m", "dry_run": false, "cancel_open_orders_on_exit": false, - "trailing_stop": false, "unfilledtimeout": { "buy": 10, "sell": 30 diff --git a/config_binance.json.example b/config_binance.json.example index 82943749d..f3f8eb659 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -7,7 +7,6 @@ "timeframe": "5m", "dry_run": true, "cancel_open_orders_on_exit": false, - "trailing_stop": false, "unfilledtimeout": { "buy": 10, "sell": 30 diff --git a/config_kraken.json.example b/config_kraken.json.example index fb983a4a3..fd0b2b95d 100644 --- a/config_kraken.json.example +++ b/config_kraken.json.example @@ -7,7 +7,6 @@ "timeframe": "5m", "dry_run": true, "cancel_open_orders_on_exit": false, - "trailing_stop": false, "unfilledtimeout": { "buy": 10, "sell": 30 From f54fecaebaaa009dd4a5da2c83dcb85516be8a6a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Sep 2020 19:58:26 +0200 Subject: [PATCH 0550/1197] Expose helpermethods thorugh freqtrade.strategy --- freqtrade/strategy/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 40a4a0bea..91ea0e075 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -1 +1,4 @@ -from freqtrade.strategy.interface import IStrategy # noqa: F401 +# flake8: noqa: F401 +from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_prev_date, + timeframe_to_seconds, timeframe_to_next_date, timeframe_to_msecs) +from freqtrade.strategy.interface import IStrategy From e268bd192e85868cb4da9e74dceb652d249ffbe6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Sep 2020 19:59:04 +0200 Subject: [PATCH 0551/1197] Fix informative sample documentation --- docs/strategy-customization.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index be08faa2d..4362c251f 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -483,6 +483,10 @@ if self.dp: ### Complete Data-provider sample ```python +from freqtrade.strategy import IStrategy, timeframe_to_minutes +from pandas import DataFrame +import pandas as pd + class SampleStrategy(IStrategy): # strategy init stuff... @@ -518,9 +522,15 @@ class SampleStrategy(IStrategy): # Assuming inf_tf = '1d' - then the columns will now be: # date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d + # Shift date by 1 Frequency unit + # This is necessary since the data is always the "open date" + # and a 15m candle starting at 12:15 should not know the close of the 1h candle from 12:00 to 13:00 + minutes = timeframe_to_minutes(inf_tf) + informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes, 'm') + # Combine the 2 dataframes # all indicators on the informative sample MUST be calculated before this point - dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_{inf_tf}', how='left') + dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_merge_{inf_tf}', how='left') # FFill to have the 1d value available in every row throughout the day. # Without this, comparisons would only work once per day. dataframe = dataframe.ffill() From 79ea8cf7719879e942d4c49dddd63cafd2d38cfe Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Sep 2020 20:02:41 +0200 Subject: [PATCH 0552/1197] Improve wording --- docs/strategy-customization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 4362c251f..e2548e510 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -522,7 +522,7 @@ class SampleStrategy(IStrategy): # Assuming inf_tf = '1d' - then the columns will now be: # date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d - # Shift date by 1 Frequency unit + # Shift date by 1 candle # This is necessary since the data is always the "open date" # and a 15m candle starting at 12:15 should not know the close of the 1h candle from 12:00 to 13:00 minutes = timeframe_to_minutes(inf_tf) From 295ecaa9b20e688a4735a7a9fe1cd9bbbea2f7d0 Mon Sep 17 00:00:00 2001 From: silvavn <37382997+silvavn@users.noreply.github.com> Date: Wed, 2 Sep 2020 16:58:54 -0600 Subject: [PATCH 0553/1197] Updating Edge Positioning Doc. Integrated MathJax Included worked out examples Changed Language to achieve a middle ground. Minor formatting improvements --- docs/edge.md | 204 +++++++++++++++++++++++-------------- docs/javascripts/config.js | 12 +++ mkdocs.yml | 8 +- 3 files changed, 149 insertions(+), 75 deletions(-) create mode 100644 docs/javascripts/config.js diff --git a/docs/edge.md b/docs/edge.md index dcb559f96..182c47651 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -1,92 +1,141 @@ # Edge positioning -This page explains how to use Edge Positioning module in your bot in order to enter into a trade only if the trade has a reasonable win rate and risk reward ratio, and consequently adjust your position size and stoploss. +The `Edge Positioning` module uses probability to calculate your win rate and risk reward ration. It will use these statistics to control your strategy trade entry points, position side and, stoploss. !!! Warning - Edge positioning is not compatible with dynamic (volume-based) whitelist. + `Edge positioning` is not compatible with dynamic (volume-based) whitelist. !!! Note - Edge does not consider anything other than *its own* buy/sell/stoploss signals. It ignores the stoploss, trailing stoploss, and ROI settings in the strategy configuration file. - Therefore, it is important to understand that Edge can improve the performance of some trading strategies but *decrease* the performance of others. + `Edge Positioning` only considers *its own* buy/sell/stoploss signals. It ignores the stoploss, trailing stoploss, and ROI settings in the strategy configuration file. + `Edge Positioning` improves the performance of some trading strategies and *decreases* the performance of others. ## Introduction -Trading is all about probability. No one can claim that he has a strategy working all the time. You have to assume that sometimes you lose. +Trading strategies are not perfect. They are frameworks that are susceptible to the market and its indicators. Because the market is not at all predictable, sometimes a strategy will win and sometimes the same strategy will lose. -But it doesn't mean there is no rule, it only means rules should work "most of the time". Let's play a game: we toss a coin, heads: I give you 10$, tails: you give me 10$. Is it an interesting game? No, it's quite boring, isn't it? +To obtain an edge in the market, a strategy has to make more money than it loses. Marking money in trading is not only about *how often* the strategy makes or loses money. -But let's say the probability that we have heads is 80% (because our coin has the displaced distribution of mass or other defect), and the probability that we have tails is 20%. Now it is becoming interesting... +!!! tip "It doesn't matter how often, but how much!" + A bad strategy might make 1 penny in *ten* transactions but lose 1 dollar in *one* transaction. If one only checks the number of winning trades, it would be misleading to think that the strategy is actually making a profit. -That means 10$ X 80% versus 10$ X 20%. 8$ versus 2$. That means over time you will win 8$ risking only 2$ on each toss of coin. +The Edge Positioning module seeks to improve a strategy's winning probability and the money that the strategy will make *on the long run*. -Let's complicate it more: you win 80% of the time but only 2$, I win 20% of the time but 8$. The calculation is: 80% X 2$ versus 20% X 8$. It is becoming boring again because overtime you win $1.6$ (80% X 2$) and me $1.6 (20% X 8$) too. +We raise the following question[^1]: -The question is: How do you calculate that? How do you know if you wanna play? +!!! Question "Which trade is a better option?" + a) A trade with 80% of chance of losing $100 and 20% chance of winning $200
    + b) A trade with 100% of chance of losing $30 -The answer comes to two factors: +??? Info "Answer" + The expected value of *a)* is smaller than the expected value of *b)*.
    + Hence, *b*) represents a smaller loss in the long run.
    + However, the answer is: *it depends* -- Win Rate -- Risk Reward Ratio +Another way to look at it is to ask a similar question: -### Win Rate +!!! Question "Which trade is a better option?" + a) A trade with 80% of chance of winning 100 and 20% chance of losing $200
    + b) A trade with 100% of chance of winning $30 -Win Rate (*W*) is is the mean over some amount of trades (*N*) what is the percentage of winning trades to total number of trades (note that we don't consider how much you gained but only if you won or not). +Edge positioning tries to answer the hard questions about risk/reward and position size automatically, seeking to minimizes the chances of losing of a given strategy. -``` -W = (Number of winning trades) / (Total number of trades) = (Number of winning trades) / N -``` +### Trading, winning and losing -Complementary Loss Rate (*L*) is defined as +Let's call $o$ the return of a single transaction $o$ where $o \in \mathbb{R}$. The collection $O = \{o_1, o_2, ..., o_N\}$ is the set of all returns of transactions made during a trading session. We say that $N$ is the cardinality of $O$, or, in lay terms, it is the number of transactions made in a trading session. -``` -L = (Number of losing trades) / (Total number of trades) = (Number of losing trades) / N -``` +!!! Example + In a session where a strategy made three transactions we can say that $O = \{3.5, -1, 15\}$. That means that $N = 3$ and $o_1 = 3.5$, $o_2 = -1$, $o = 15$. -or, which is the same, as +A winning trade is a trade where a strategy *made* money. Making money means that the strategy closed the position in a value that returned a profit, after all deducted fees. Formally, a winning trade will have a return $o_i > 0$. Similarly, a losing trade will have a return $o_j \leq 0$. With that, we can discover the set of all winning trades, $T_{win}$, as follows: -``` -L = 1 – W -``` +$$ T_{win} = \{ o \in O | o > 0 \} $$ + +Similarly, we can discover the set of losing trades $T_{lose}$ as follows: + +$$ T_{lose} = \{o \in O | o \leq 0\} $$ + +!!! Example + In a section where a strategy made three transactions $O = \{3.5, -1, 15, 0\}$:
    + $T_{win} = \{3.5, 15\}$
    + $T_{lose} = \{-1, 0\}$
    + +### Win Rate and Lose Rate + +The win rate $W$ is the proportion of winning trades with respect to all the trades made by a strategy. We use the following function to compute the win rate: + +$$W = \frac{\sum^{o \in T_{win}} o}{N}$$ + +Where $W$ is the win rate, $N$ is the number of trades and, $T_{win}$ is the set of all trades where the strategy made money. + +Similarly, we can compute the rate of losing trades: + +$$ + L = \frac{\sum^{o \in T_{lose}} o}{N} +$$ + +Where $L$ is the lose rate, $N$ is the amount of trades made and, $T_{lose}$ is the set of all trades where the strategy lost money. Note that the above formula is the same as calculating $L = 1 – W$ or $W = 1 – L$ ### Risk Reward Ratio -Risk Reward Ratio (*R*) is a formula used to measure the expected gains of a given investment against the risk of loss. It is basically what you potentially win divided by what you potentially lose: +Risk Reward Ratio (*R*) is a formula used to measure the expected gains of a given investment against the risk of loss. It is basically what you potentially win divided by what you potentially lose. Formally: -``` -R = Profit / Loss -``` +$$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ -Over time, on many trades, you can calculate your risk reward by dividing your average profit on winning trades by your average loss on losing trades: +??? Example "Worked example of $R$ calculation" + Let's say that you think that the price of *stonecoin* today is $10.0. You believe that, because they will start mining stonecoin it will go up to $15.0 tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to $0 tomorrow. You are planning to invest $100.
    + Your potential profit is calculated as:
    + $\begin{aligned} + \text{potential_profit} &= (\text{potential_price} - \text{cost_per_unit}) * \frac{\text{investment}}{\text{cost_per_unit}} \\ + &= (15 - 10) * \frac{100}{15}\\ + &= 33.33 + \end{aligned}$
    + Since the price might go to $0, the $100 dolars invested could turn into 0. We can compute the Risk Reward Ratio as follows:
    + $\begin{aligned} + R &= \frac{\text{potential_profit}}{\text{potential_loss}}\\ + &= \frac{33.33}{100}\\ + &= 0.333... + \end{aligned}$
    + What it effectivelly means is that the strategy have the potential to make $0.33 for each $1 invested. -``` -Average profit = (Sum of profits) / (Number of winning trades) +On a long horizonte, that is, on many trades, we can calculate the risk reward by dividing the strategy' average profit on winning trades by the strategy' average loss on losing trades. We can calculate the average profit, $\mu_{win}$, as follows: -Average loss = (Sum of losses) / (Number of losing trades) +$$ \text{average_profit} = \mu_{win} = \frac{\text{sum_of_profits}}{\text{count_winning_trades}} = \frac{\sum^{o \in T_{win}} o}{|T_{win}|} $$ -R = (Average profit) / (Average loss) -``` +Similarly, we can calculate the average loss, $\mu_{lose}$, as follows: + +$$ \text{average_loss} = \mu_{lose} = \frac{\text{sum_of_losses}}{\text{count_losing_trades}} = \frac{\sum^{o \in T_{lose}} o}{|T_{lose}|} $$ + +Finally, we can calculate the Risk Reward ratio as follows: + +$$ R = \frac{\text{average_profit}}{\text{average_loss}} = \frac{\mu_{win}}{\mu_{lose}}\\ $$ + + +??? Example "Worked example of $R$ calculation using mean profit/loss" + Let's say the strategy that we are using makes an average win $\mu_{win} = 2.06$ and an average loss $\mu_{loss} = 4.11$.
    + We calculate the risk reward ratio as follows:
    + $R = \frac{\mu_{win}}{\mu_{loss}} = \frac{2.06}{4.11} = 0.5012...$ + ### Expectancy -At this point we can combine *W* and *R* to create an expectancy ratio. This is a simple process of multiplying the risk reward ratio by the percentage of winning trades and subtracting the percentage of losing trades, which is calculated as follows: +By combining the Win Rate $W$ and and the Risk Reward ratio $R$ to create an expectancy ratio $E$. A expectance ratio is the expected return of the investment made in a trade. We can compute the value of $E$ as follows: -``` -Expectancy Ratio = (Risk Reward Ratio X Win Rate) – Loss Rate = (R X W) – L -``` +$$E = R * W - L$$ -So lets say your Win rate is 28% and your Risk Reward Ratio is 5: +!!! Example "Calculating $E$" + Let's say that a strategy has a win rate $W = 0.28$ and a risk reward ratio $R = 5$. What this means is that the strategy is expected to make 5 times the investment around on 28% of the trades it makes. Working out the example:
    + $E = R * W - L = 5 * 0.28 - 0.72 = 0.68$ +
    -``` -Expectancy = (5 X 0.28) – 0.72 = 0.68 -``` +The expectancy worked out in the example above means that, on average, this strategy' trades will return 1.68 times the size of its losses. Said another way, the strategy makes $1.68 for every $1 it loses, on average. -Superficially, this means that on average you expect this strategy’s trades to return 1.68 times the size of your loses. Said another way, you can expect to win $1.68 for every $1 you lose. This is important for two reasons: First, it may seem obvious, but you know right away that you have a positive return. Second, you now have a number you can compare to other candidate systems to make decisions about which ones you employ. +You canThis is important for two reasons: First, it may seem obvious, but you know right away that you have a positive return. Second, you now have a number you can compare to other candidate systems to make decisions about which ones you employ. It is important to remember that any system with an expectancy greater than 0 is profitable using past data. The key is finding one that will be profitable in the future. You can also use this value to evaluate the effectiveness of modifications to this system. -**NOTICE:** It's important to keep in mind that Edge is testing your expectancy using historical data, there's no guarantee that you will have a similar edge in the future. It's still vital to do this testing in order to build confidence in your methodology, but be wary of "curve-fitting" your approach to the historical data as things are unlikely to play out the exact same way for future trades. +**NOTICE:** It's important to keep in mind that Edge is testing your expectancy using historical data, there's no guarantee that you will have a similar edge in the future. It's still vital to do this testing in order to build confidence in your methodology but be wary of "curve-fitting" your approach to the historical data as things are unlikely to play out the exact same way for future trades. ## How does it work? @@ -99,13 +148,13 @@ Edge combines dynamic stoploss, dynamic positions, and whitelist generation into | XZC/ETH | -0.03 | 0.52 |1.359670 | 0.228 | | XZC/ETH | -0.04 | 0.51 |1.234539 | 0.117 | -The goal here is to find the best stoploss for the strategy in order to have the maximum expectancy. In the above example stoploss at 3% leads to the maximum expectancy according to historical data. +The goal here is to find the best stoploss for the strategy in order to have the maximum expectancy. In the above example stoploss at $3% $leads to the maximum expectancy according to historical data. Edge module then forces stoploss value it evaluated to your strategy dynamically. ### Position size -Edge also dictates the stake amount for each trade to the bot according to the following factors: +Edge dictates the amount at stake for each trade to the bot according to the following factors: - Allowed capital at risk - Stoploss @@ -116,9 +165,9 @@ Allowed capital at risk is calculated as follows: Allowed capital at risk = (Capital available_percentage) X (Allowed risk per trade) ``` -Stoploss is calculated as described above against historical data. +Stoploss is calculated as described above with respect to historical data. -Your position size then will be: +The position size is calculated as follows: ``` Position size = (Allowed capital at risk) / Stoploss @@ -126,19 +175,23 @@ Position size = (Allowed capital at risk) / Stoploss Example: -Let's say the stake currency is ETH and you have 10 ETH on the exchange, your capital available percentage is 50% and you would allow 1% of risk for each trade. thus your available capital for trading is **10 x 0.5 = 5 ETH** and allowed capital at risk would be **5 x 0.01 = 0.05 ETH**. +Let's say the stake currency is **ETH** and there is $10$ **ETH** on the wallet. The capital available percentage is $50%$ and the allowed risk per trade is $1\%$. Thus, the available capital for trading is $10 * 0.5 = 5$ **ETH** and the allowed capital at risk would be $5 * 0.01 = 0.05$ **ETH**. -Let's assume Edge has calculated that for **XLM/ETH** market your stoploss should be at 2%. So your position size will be **0.05 / 0.02 = 2.5 ETH**. +- **Trade 1:** The strategy detects a new buy signal in the **XLM/ETH** market. `Edge Positioning` calculates a stoploss of $2\%$ and a position of $0.05 / 0.02 = 2.5$ **ETH**. The bot takes a position of $2.5$ **ETH** in the **XLM/ETH** market. -Bot takes a position of 2.5 ETH on XLM/ETH (call it trade 1). Up next, you receive another buy signal while trade 1 is still open. This time on **BTC/ETH** market. Edge calculated stoploss for this market at 4%. So your position size would be 0.05 / 0.04 = 1.25 ETH (call it trade 2). +- **Trade 2:** The strategy detects a buy signal on the **BTC/ETH** market while **Trade 1** is still open. `Edge Positioning` calculates the stoploss of $4\%$ on this market. Thus, **Trade 2** position size is $0.05 / 0.04 = 1.25$ **ETH**. -Note that available capital for trading didn’t change for trade 2 even if you had already trade 1. The available capital doesn’t mean the free amount on your wallet. +!!! Tip "Available Capital $\neq$ Available in wallet" + The available capital for trading didn't change in **Trade 2** even with **Trade 1** still open. The available capital **is not** the free amount in the wallet. -Now you have two trades open. The bot receives yet another buy signal for another market: **ADA/ETH**. This time the stoploss is calculated at 1%. So your position size is **0.05 / 0.01 = 5 ETH**. But there are already 3.75 ETH blocked in two previous trades. So the position size for this third trade would be **5 – 3.75 = 1.25 ETH**. +- **Trade 3:** The strategy detects a buy signal in the **ADA/ETH** market. `Edge Positioning` calculates a stoploss of $1\%$ and a position of $0.05 / 0.01 = 5$ **ETH**. Since **Trade 1** has $2.5$ **ETH** blocked and **Trade 2** has $1.25$ **ETH** blocked, there is only $5 - 1.25 - 2.5 = 1.25$ **ETH** available. Hence, the position size of **Trade 3** is $1.25$ **ETH**. -Available capital doesn’t change before a position is sold. Let’s assume that trade 1 receives a sell signal and it is sold with a profit of 1 ETH. Your total capital on exchange would be 11 ETH and the available capital for trading becomes 5.5 ETH. +!!! Tip "Available Capital Updates" + The available capital does not change before a position is sold. After a trade is closed the Available Capital goes up if the trade was profitable or goes down if the trade was a loss. -So the Bot receives another buy signal for trade 4 with a stoploss at 2% then your position size would be **0.055 / 0.02 = 2.75 ETH**. +- The strategy detects a sell signal in the **XLM/ETH** market. The bot exits **Trade 1** for a profit of $1$ **ETH**. The total capital in the wallet becomes $11$ **ETH** and the available capital for trading becomes $5.5$ **ETH**. + +- **Trade 4** The strategy detects a new buy signal int the **XLM/ETH** market. `Edge Positioning` calculates the stoploss of $2%$, and the position size of $0.055 / 0.02 = 2.75$ **ETH**. ## Configurations @@ -169,23 +222,23 @@ freqtrade edge An example of its output: -| pair | stoploss | win rate | risk reward ratio | required risk reward | expectancy | total number of trades | average duration (min) | -|:----------|-----------:|-----------:|--------------------:|-----------------------:|-------------:|-------------------------:|-------------------------:| -| AGI/BTC | -0.02 | 0.64 | 5.86 | 0.56 | 3.41 | 14 | 54 | -| NXS/BTC | -0.03 | 0.64 | 2.99 | 0.57 | 1.54 | 11 | 26 | -| LEND/BTC | -0.02 | 0.82 | 2.05 | 0.22 | 1.50 | 11 | 36 | -| VIA/BTC | -0.01 | 0.55 | 3.01 | 0.83 | 1.19 | 11 | 48 | -| MTH/BTC | -0.09 | 0.56 | 2.82 | 0.80 | 1.12 | 18 | 52 | -| ARDR/BTC | -0.04 | 0.42 | 3.14 | 1.40 | 0.73 | 12 | 42 | -| BCPT/BTC | -0.01 | 0.71 | 1.34 | 0.40 | 0.67 | 14 | 30 | -| WINGS/BTC | -0.02 | 0.56 | 1.97 | 0.80 | 0.65 | 27 | 42 | -| VIBE/BTC | -0.02 | 0.83 | 0.91 | 0.20 | 0.59 | 12 | 35 | -| MCO/BTC | -0.02 | 0.79 | 0.97 | 0.27 | 0.55 | 14 | 31 | -| GNT/BTC | -0.02 | 0.50 | 2.06 | 1.00 | 0.53 | 18 | 24 | -| HOT/BTC | -0.01 | 0.17 | 7.72 | 4.81 | 0.50 | 209 | 7 | -| SNM/BTC | -0.03 | 0.71 | 1.06 | 0.42 | 0.45 | 17 | 38 | -| APPC/BTC | -0.02 | 0.44 | 2.28 | 1.27 | 0.44 | 25 | 43 | -| NEBL/BTC | -0.03 | 0.63 | 1.29 | 0.58 | 0.44 | 19 | 59 | +| **pair** | **stoploss** | **win rate** | **risk reward ratio** | **required risk reward** | **expectancy** | **total number of trades** | **average duration (min)** | +|:----------|-----------:|-----------:|--------------------:|-----------------------:|-------------:|-----------------:|---------------:| +| **AGI/BTC** | -0.02 | 0.64 | 5.86 | 0.56 | 3.41 | 14 | 54 | +| **NXS/BTC** | -0.03 | 0.64 | 2.99 | 0.57 | 1.54 | 11 | 26 | +| **LEND/BTC** | -0.02 | 0.82 | 2.05 | 0.22 | 1.50 | 11 | 36 | +| **VIA/BTC** | -0.01 | 0.55 | 3.01 | 0.83 | 1.19 | 11 | 48 | +| **MTH/BTC** | -0.09 | 0.56 | 2.82 | 0.80 | 1.12 | 18 | 52 | +| **ARDR/BTC** | -0.04 | 0.42 | 3.14 | 1.40 | 0.73 | 12 | 42 | +| **BCPT/BTC** | -0.01 | 0.71 | 1.34 | 0.40 | 0.67 | 14 | 30 | +| **WINGS/BTC** | -0.02 | 0.56 | 1.97 | 0.80 | 0.65 | 27 | 42 | +| **VIBE/BTC** | -0.02 | 0.83 | 0.91 | 0.20 | 0.59 | 12 | 35 | +| **MCO/BTC** | -0.02 | 0.79 | 0.97 | 0.27 | 0.55 | 14 | 31 | +| **GNT/BTC** | -0.02 | 0.50 | 2.06 | 1.00 | 0.53 | 18 | 24 | +| **HOT/BTC** | -0.01 | 0.17 | 7.72 | 4.81 | 0.50 | 209 | 7 | +| **SNM/BTC** | -0.03 | 0.71 | 1.06 | 0.42 | 0.45 | 17 | 38 | +| **APPC/BTC** | -0.02 | 0.44 | 2.28 | 1.27 | 0.44 | 25 | 43 | +| **NEBL/BTC** | -0.03 | 0.63 | 1.29 | 0.58 | 0.44 | 19 | 59 | Edge produced the above table by comparing `calculate_since_number_of_days` to `minimum_expectancy` to find `min_trade_number` historical information based on the config file. The timerange Edge uses for its comparisons can be further limited by using the `--timerange` switch. @@ -218,3 +271,6 @@ The full timerange specification: * Use tickframes since 2018/01/31: `--timerange=20180131-` * Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301` * Use tickframes between POSIX timestamps 1527595200 1527618600: `--timerange=1527595200-1527618600` + + +[^1]: Question extracted from MIT Opencourseware S096 - Mathematics with applications in Finance: https://ocw.mit.edu/courses/mathematics/18-s096-topics-in-mathematics-with-applications-in-finance-fall-2013/ diff --git a/docs/javascripts/config.js b/docs/javascripts/config.js new file mode 100644 index 000000000..95d619efc --- /dev/null +++ b/docs/javascripts/config.js @@ -0,0 +1,12 @@ +window.MathJax = { + tex: { + inlineMath: [["\\(", "\\)"]], + displayMath: [["\\[", "\\]"]], + processEscapes: true, + processEnvironments: true + }, + options: { + ignoreHtmlClass: ".*|", + processHtmlClass: "arithmatex" + } +}; \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml index ebd32b3c1..324dc46db 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -39,13 +39,19 @@ theme: accent: 'tear' extra_css: - 'stylesheets/ft.extra.css' +extra_javascript: + - javascripts/config.js + - https://polyfill.io/v3/polyfill.min.js?features=es6 + - https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js markdown_extensions: - admonition + - footnotes - codehilite: guess_lang: false - toc: permalink: true - - pymdownx.arithmatex + - pymdownx.arithmatex: + generic: true - pymdownx.caret - pymdownx.critic - pymdownx.details From 47352e17215731b02748ee05aedfd08fd7fc9c71 Mon Sep 17 00:00:00 2001 From: silvavn <37382997+silvavn@users.noreply.github.com> Date: Wed, 2 Sep 2020 20:37:45 -0600 Subject: [PATCH 0554/1197] Address issue #2487 Breakdown insllation instructions Make installation instructions shorter Separate Windows from the remainder Use tabs for better navigation Minor language improvements --- docs/docker.md | 349 ++++++--------------------------- docs/docker_compose.md | 44 +++++ docs/installation.md | 146 ++++---------- docs/windows_installation.md | 49 +++++ docs/without_docker_compose.md | 201 +++++++++++++++++++ mkdocs.yml | 10 +- 6 files changed, 405 insertions(+), 394 deletions(-) create mode 100644 docs/docker_compose.md create mode 100644 docs/windows_installation.md create mode 100644 docs/without_docker_compose.md diff --git a/docs/docker.md b/docs/docker.md index 92478088a..83f2d06e3 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -8,7 +8,7 @@ Start by downloading and installing Docker CE for your platform: * [Windows](https://docs.docker.com/docker-for-windows/install/) * [Linux](https://docs.docker.com/install/) -Optionally, [docker-compose](https://docs.docker.com/compose/install/) should be installed and available to follow the [docker quick start guide](#docker-quick-start). +Optionally, [`docker-compose`](https://docs.docker.com/compose/install/) should be installed and available to follow the [docker quick start guide](#docker-quick-start). Once you have Docker installed, simply prepare the config file (e.g. `config.json`) and run the image for `freqtrade` as explained below. @@ -17,325 +17,96 @@ Once you have Docker installed, simply prepare the config file (e.g. `config.jso Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) ready for usage. !!! Note - The following section assumes that docker and docker-compose is installed and available to the logged in user. - -!!! Note - All below comands use relative directories and will have to be executed from the directory containing the `docker-compose.yml` file. - -!!! Note "Docker on Raspberry" - If you're running freqtrade on a Raspberry PI, you must change the image from `freqtradeorg/freqtrade:master` to `freqtradeorg/freqtrade:master_pi` or `freqtradeorg/freqtrade:develop_pi`, otherwise the image will not work. + - The following section assumes that `docker` and `docker-compose` are installed and available to the logged in user. + - All below comands use relative directories and will have to be executed from the directory containing the `docker-compose.yml` file. + ### Docker quick start Create a new directory and place the [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) in this directory. -``` bash -mkdir ft_userdata -cd ft_userdata/ -# Download the docker-compose file from the repository -curl https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docker-compose.yml -o docker-compose.yml +=== "PC/MAC/Linux" + ``` bash + mkdir ft_userdata + cd ft_userdata/ + # Download the docker-compose file from the repository + curl https://raw.githubusercontent.com/freqtrade/freqtrade/master/docker-compose.yml -o docker-compose.yml -# Pull the freqtrade image -docker-compose pull + # Pull the freqtrade image + docker-compose pull -# Create user directory structure -docker-compose run --rm freqtrade create-userdir --userdir user_data + # Create user directory structure + docker-compose run --rm freqtrade create-userdir --userdir user_data -# Create configuration - Requires answering interactive questions -docker-compose run --rm freqtrade new-config --config user_data/config.json -``` + # Create configuration - Requires answering interactive questions + docker-compose run --rm freqtrade new-config --config user_data/config.json + ``` -The above snippet creates a new directory called "ft_userdata", downloads the latest compose file and pulls the freqtrade image. -The last 2 steps in the snippet create the directory with user-data, as well as (interactively) the default configuration based on your selections. +=== "RaspberryPi" + ``` bash + mkdir ft_userdata + cd ft_userdata/ + # Download the docker-compose file from the repository + curl https://raw.githubusercontent.com/freqtrade/freqtrade/master_pi/docker-compose.yml -o docker-compose.yml -!!! Note + # Pull the freqtrade image + docker-compose pull + + # Create user directory structure + docker-compose run --rm freqtrade create-userdir --userdir user_data + + # Create configuration - Requires answering interactive questions + docker-compose run --rm freqtrade new-config --config user_data/config.json + ``` + +The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image. +The last 2 steps in the snippet create the directory with `user_data`, as well as (interactively) the default configuration based on your selections. + +!!! Question "How to edit the bot configuration?" You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. -#### Adding your strategy +#### Adding a custom strategy -The configuration is now available as `user_data/config.json`. -You should now copy your strategy to `user_data/strategies/` - and add the Strategy class name to the `docker-compose.yml` file, replacing `SampleStrategy`. If you wish to run the bot with the SampleStrategy, just leave it as it is. +1. The configuration is now available as `user_data/config.json` +2. Copy a custom strategy to the directory `user_data/strategies/` +3. add the Strategy' class name to the `docker-compose.yml` file -!!! Warning +The `SampleStrategy` is run by default. + +!!! Warning "`SampleStrategy` is just a demo!" The `SampleStrategy` is there for your reference and give you ideas for your own strategy. Please always backtest the strategy and use dry-run for some time before risking real money! Once this is done, you're ready to launch the bot in trading mode (Dry-run or Live-trading, depending on your answer to the corresponding question you made above). -``` bash -docker-compose up -d -``` +=== "Docker Compose" + ``` bash + docker-compose up -d + ``` #### Docker-compose logs -Logs will be written to `user_data/logs/freqtrade.log`. -Alternatively, you can check the latest logs using `docker-compose logs -f`. +Logs will be located at: `user_data/logs/freqtrade.log`. +You can check the latest log with the command `docker-compose logs -f`. #### Database -The database will be in the user_data directory as well, and will be called `user_data/tradesv3.sqlite`. +The database will be at: `user_data/tradesv3.sqlite` #### Updating freqtrade with docker-compose -To update freqtrade when using docker-compose is as simple as running the following 2 commands: +To update freqtrade when using `docker-compose` is as simple as running the following 2 commands: -``` bash -# Download the latest image -docker-compose pull -# Restart the image -docker-compose up -d -``` +=== "Docker Compose" + ``` bash + # Download the latest image + docker-compose pull + # Restart the image + docker-compose up -d + ``` This will first pull the latest image, and will then restart the container with the just pulled version. -!!! Note +!!! Warning "Check the Changelog" You should always check the changelog for breaking changes / manual interventions required and make sure the bot starts correctly after the update. -#### Going from here - -Advanced users may edit the docker-compose file further to include all possible options or arguments. - -All possible freqtrade arguments will be available by running `docker-compose run --rm freqtrade `. - -!!! Note "`docker-compose run --rm`" - Including `--rm` will clean up the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). - -##### Example: Download data with docker-compose - -Download backtesting data for 5 days for the pair ETH/BTC and 1h timeframe from Binance. The data will be stored in the directory `user_data/data/` on the host. - -``` bash -docker-compose run --rm freqtrade download-data --pairs ETH/BTC --exchange binance --days 5 -t 1h -``` - -Head over to the [Data Downloading Documentation](data-download.md) for more details on downloading data. - -##### Example: Backtest with docker-compose - -Run backtesting in docker-containers for SampleStrategy and specified timerange of historical data, on 5m timeframe: - -``` bash -docker-compose run --rm freqtrade backtesting --config user_data/config.json --strategy SampleStrategy --timerange 20190801-20191001 -i 5m -``` - -Head over to the [Backtesting Documentation](backtesting.md) to learn more. - -#### Additional dependencies with docker-compose - -If your strategy requires dependencies not included in the default image (like [technical](https://github.com/freqtrade/technical)) - it will be necessary to build the image on your host. -For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) for an example). - -You'll then also need to modify the `docker-compose.yml` file and uncomment the build step, as well as rename the image to avoid naming collisions. - -``` yaml - image: freqtrade_custom - build: - context: . - dockerfile: "./Dockerfile." -``` - -You can then run `docker-compose build` to build the docker image, and run it using the commands described above. - -## Freqtrade with docker without docker-compose - -!!! Warning - The below documentation is provided for completeness and assumes that you are somewhat familiar with running docker containers. If you're just starting out with docker, we recommend to follow the [Freqtrade with docker-compose](#freqtrade-with-docker-compose) instructions. - -### Download the official Freqtrade docker image - -Pull the image from docker hub. - -Branches / tags available can be checked out on [Dockerhub tags page](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/). - -```bash -docker pull freqtradeorg/freqtrade:develop -# Optionally tag the repository so the run-commands remain shorter -docker tag freqtradeorg/freqtrade:develop freqtrade -``` - -To update the image, simply run the above commands again and restart your running container. - -Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image). - -!!! Note "Docker image update frequency" - The official docker images with tags `master`, `develop` and `latest` are automatically rebuild once a week to keep the base image uptodate. - In addition to that, every merge to `develop` will trigger a rebuild for `develop` and `latest`. - -### Prepare the configuration files - -Even though you will use docker, you'll still need some files from the github repository. - -#### Clone the git repository - -Linux/Mac/Windows with WSL - -```bash -git clone https://github.com/freqtrade/freqtrade.git -``` - -Windows with docker - -```bash -git clone --config core.autocrlf=input https://github.com/freqtrade/freqtrade.git -``` - -#### Copy `config.json.example` to `config.json` - -```bash -cd freqtrade -cp -n config.json.example config.json -``` - -> To understand the configuration options, please refer to the [Bot Configuration](configuration.md) page. - -#### Create your database file - -Production - -```bash -touch tradesv3.sqlite -```` - -Dry-Run - -```bash -touch tradesv3.dryrun.sqlite -``` - -!!! Note - Make sure to use the path to this file when starting the bot in docker. - -### Build your own Docker image - -Best start by pulling the official docker image from dockerhub as explained [here](#download-the-official-docker-image) to speed up building. - -To add additional libraries to your docker image, best check out [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) which adds the [technical](https://github.com/freqtrade/technical) module to the image. - -```bash -docker build -t freqtrade -f Dockerfile.technical . -``` - -If you are developing using Docker, use `Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies: - -```bash -docker build -f Dockerfile.develop -t freqtrade-dev . -``` - -!!! Note - For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see the "5. Run a restartable docker image" section) to keep it between updates. - -#### Verify the Docker image - -After the build process you can verify that the image was created with: - -```bash -docker images -``` - -The output should contain the freqtrade image. - -### Run the Docker image - -You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory): - -```bash -docker run --rm -v `pwd`/config.json:/freqtrade/config.json -it freqtrade -``` - -!!! Warning - In this example, the database will be created inside the docker instance and will be lost when you will refresh your image. - -#### Adjust timezone - -By default, the container will use UTC timezone. -Should you find this irritating please add the following to your docker commands: - -##### Linux - -``` bash --v /etc/timezone:/etc/timezone:ro - -# Complete command: -docker run --rm -v /etc/timezone:/etc/timezone:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade -``` - -##### MacOS - -There is known issue in OSX Docker versions after 17.09.1, whereby `/etc/localtime` cannot be shared causing Docker to not start. A work-around for this is to start with the following cmd. - -```bash -docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade -``` - -More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396). - -### Run a restartable docker image - -To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem). - -#### Move your config file and database - -The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden directory in your home directory. Feel free to use a different directory and replace the directory in the upcomming commands. - -```bash -mkdir ~/.freqtrade -mv config.json ~/.freqtrade -mv tradesv3.sqlite ~/.freqtrade -``` - -#### Run the docker image - -```bash -docker run -d \ - --name freqtrade \ - -v ~/.freqtrade/config.json:/freqtrade/config.json \ - -v ~/.freqtrade/user_data/:/freqtrade/user_data \ - -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ - freqtrade trade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy -``` - -!!! Note - When using docker, it's best to specify `--db-url` explicitly to ensure that the database URL and the mounted database file match. - -!!! Note - All available bot command line parameters can be added to the end of the `docker run` command. - -!!! Note - You can define a [restart policy](https://docs.docker.com/config/containers/start-containers-automatically/) in docker. It can be useful in some cases to use the `--restart unless-stopped` flag (crash of freqtrade or reboot of your system). - -### Monitor your Docker instance - -You can use the following commands to monitor and manage your container: - -```bash -docker logs freqtrade -docker logs -f freqtrade -docker restart freqtrade -docker stop freqtrade -docker start freqtrade -``` - -For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/). - -!!! Note - You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container. - -### Backtest with docker - -The following assumes that the download/setup of the docker image have been completed successfully. -Also, backtest-data should be available at `~/.freqtrade/user_data/`. - -```bash -docker run -d \ - --name freqtrade \ - -v /etc/localtime:/etc/localtime:ro \ - -v ~/.freqtrade/config.json:/freqtrade/config.json \ - -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ - -v ~/.freqtrade/user_data/:/freqtrade/user_data/ \ - freqtrade backtesting --strategy AwsomelyProfitableStrategy -``` - -Head over to the [Backtesting Documentation](backtesting.md) for more details. - -!!! Note - Additional bot command line parameters can be appended after the image name (`freqtrade` in the above example). diff --git a/docs/docker_compose.md b/docs/docker_compose.md new file mode 100644 index 000000000..302d3b358 --- /dev/null +++ b/docs/docker_compose.md @@ -0,0 +1,44 @@ +#### Editing the docker-compose file + +Advanced users may edit the docker-compose file further to include all possible options or arguments. + +All possible freqtrade arguments will be available by running `docker-compose run --rm freqtrade `. + +!!! Note "`docker-compose run --rm`" + Including `--rm` will clean up the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). + +##### Example: Download data with docker-compose + +Download backtesting data for 5 days for the pair ETH/BTC and 1h timeframe from Binance. The data will be stored in the directory `user_data/data/` on the host. + +``` bash +docker-compose run --rm freqtrade download-data --pairs ETH/BTC --exchange binance --days 5 -t 1h +``` + +Head over to the [Data Downloading Documentation](data-download.md) for more details on downloading data. + +##### Example: Backtest with docker-compose + +Run backtesting in docker-containers for SampleStrategy and specified timerange of historical data, on 5m timeframe: + +``` bash +docker-compose run --rm freqtrade backtesting --config user_data/config.json --strategy SampleStrategy --timerange 20190801-20191001 -i 5m +``` + +Head over to the [Backtesting Documentation](backtesting.md) to learn more. + +#### Additional dependencies with docker-compose + +If your strategy requires dependencies not included in the default image (like [technical](https://github.com/freqtrade/technical)) - it will be necessary to build the image on your host. +For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) for an example). + +You'll then also need to modify the `docker-compose.yml` file and uncomment the build step, as well as rename the image to avoid naming collisions. + +``` yaml + image: freqtrade_custom + build: + context: . + dockerfile: "./Dockerfile." +``` + +You can then run `docker-compose build` to build the docker image, and run it using the commands described above. \ No newline at end of file diff --git a/docs/installation.md b/docs/installation.md index c03be55d1..979679c9f 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -79,6 +79,20 @@ This option will hard reset your branch (only if you are on either `master` or ` DEPRECATED - use `freqtrade new-config -c config.json` instead. +### MacOS installation error + +Newer versions of MacOS may have installation failed with errors like `error: command 'g++' failed with exit status 1`. + +This error will require explicit installation of the SDK Headers, which are not installed by default in this version of MacOS. +For MacOS 10.14, this can be accomplished with the below command. + +``` bash +open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg +``` + +If this file is inexistant, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details. + + ------ ## Custom Installation @@ -89,36 +103,44 @@ OS Specific steps are listed first, the [Common](#common) section below is neces !!! Note Python3.6 or higher and the corresponding pip are assumed to be available. -### Linux - Ubuntu 16.04 +=== "Ubuntu 16.04" + #### Install necessary dependencies -#### Install necessary dependencies + ```bash + sudo apt-get update + sudo apt-get install build-essential git + ``` -```bash -sudo apt-get update -sudo apt-get install build-essential git -``` +=== "RaspberryPi/Raspbian" + The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/) from at least September 2019. + This image comes with python3.7 preinstalled, making it easy to get freqtrade up and running. -### Raspberry Pi / Raspbian + Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied. -The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/) from at least September 2019. -This image comes with python3.7 preinstalled, making it easy to get freqtrade up and running. + ``` bash + sudo apt-get install python3-venv libatlas-base-dev + git clone https://github.com/freqtrade/freqtrade.git + cd freqtrade -Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied. + bash setup.sh -i + ``` -``` bash -sudo apt-get install python3-venv libatlas-base-dev -git clone https://github.com/freqtrade/freqtrade.git -cd freqtrade + !!! Note "Installation duration" + Depending on your internet speed and the Raspberry Pi version, installation can take multiple hours to complete. -bash setup.sh -i -``` + !!! Note + The above does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`. + We do not advise to run hyperopt on a Raspberry Pi, since this is a very resource-heavy operation, which should be done on powerful machine. -!!! Note "Installation duration" - Depending on your internet speed and the Raspberry Pi version, installation can take multiple hours to complete. +=== "Anaconda" + Freqtrade can also be installed using Anaconda (or Miniconda). -!!! Note - The above does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`. - We do not advise to run hyperopt on a Raspberry Pi, since this is a very resource-heavy operation, which should be done on powerful machine. + !!! Note + This requires the [ta-lib](#1-install-ta-lib) C-library to be installed first. See below. + + ``` bash + conda env create -f environment.yml + ``` ### Common @@ -169,11 +191,6 @@ Clone the git repository: ```bash git clone https://github.com/freqtrade/freqtrade.git cd freqtrade -``` - -Optionally checkout the master branch to get the latest stable release: - -```bash git checkout master ``` @@ -212,83 +229,6 @@ On Linux, as an optional post-installation task, you may wish to setup the bot t ------ -## Using Conda - -Freqtrade can also be installed using Anaconda (or Miniconda). - -``` bash -conda env create -f environment.yml -``` - -!!! Note - This requires the [ta-lib](#1-install-ta-lib) C-library to be installed first. - -## Windows - -We recommend that Windows users use [Docker](docker.md) as this will work much easier and smoother (also more secure). - -If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work. -If that is not available on your system, feel free to try the instructions below, which led to success for some. - -### Install freqtrade manually - -!!! Note - Make sure to use 64bit Windows and 64bit Python to avoid problems with backtesting or hyperopt due to the memory constraints 32bit applications have under Windows. - -!!! Hint - Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Conda section](#using-conda) in this document for more information. - -#### Clone the git repository - -```bash -git clone https://github.com/freqtrade/freqtrade.git -``` - -#### Install ta-lib - -Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows). - -As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial precompiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_Lib‑0.4.18‑cp38‑cp38‑win_amd64.whl` (make sure to use the version matching your python version) - -```cmd ->cd \path\freqtrade-develop ->python -m venv .env ->.env\Scripts\activate.bat -REM optionally install ta-lib from wheel -REM >pip install TA_Lib‑0.4.18‑cp38‑cp38‑win_amd64.whl ->pip install -r requirements.txt ->pip install -e . ->freqtrade -``` - -> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222) - -#### Error during installation under Windows - -``` bash -error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools -``` - -Unfortunately, many packages requiring compilation don't provide a pre-build wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use. - -The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first. - ---- - Now you have an environment ready, the next step is [Bot Configuration](configuration.md). -## Troubleshooting - -### MacOS installation error - -Newer versions of MacOS may have installation failed with errors like `error: command 'g++' failed with exit status 1`. - -This error will require explicit installation of the SDK Headers, which are not installed by default in this version of MacOS. -For MacOS 10.14, this can be accomplished with the below command. - -``` bash -open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg -``` - -If this file is inexistant, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details. diff --git a/docs/windows_installation.md b/docs/windows_installation.md new file mode 100644 index 000000000..1cdb3d613 --- /dev/null +++ b/docs/windows_installation.md @@ -0,0 +1,49 @@ +We **strongly** recommend that Windows users use [Docker](docker.md) as this will work much easier and smoother (also more secure). + +If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work. +Otherwise, try the instructions below. + +## Install freqtrade manually + +!!! Note + Make sure to use 64bit Windows and 64bit Python to avoid problems with backtesting or hyperopt due to the memory constraints 32bit applications have under Windows. + +!!! Hint + Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Conda section](#using-conda) in this document for more information. + +### 1. Clone the git repository + +```bash +git clone https://github.com/freqtrade/freqtrade.git +``` + +### 2. Install ta-lib + +Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows). + +As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial precompiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_Lib‑0.4.18‑cp38‑cp38‑win_amd64.whl` (make sure to use the version matching your python version) + +```cmd +>cd \path\freqtrade-develop +>python -m venv .env +>.env\Scripts\activate.bat +REM optionally install ta-lib from wheel +REM >pip install TA_Lib‑0.4.18‑cp38‑cp38‑win_amd64.whl +>pip install -r requirements.txt +>pip install -e . +>freqtrade +``` + +> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222) + +### Error during installation on Windows + +``` bash +error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools +``` + +Unfortunately, many packages requiring compilation don't provide a pre-build wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use. + +The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first. + +--- diff --git a/docs/without_docker_compose.md b/docs/without_docker_compose.md new file mode 100644 index 000000000..23994f38f --- /dev/null +++ b/docs/without_docker_compose.md @@ -0,0 +1,201 @@ +## Freqtrade with docker without docker-compose + +!!! Warning + The below documentation is provided for completeness and assumes that you are familiar with running docker containers. If you're just starting out with Docker, we recommend to follow the [Quickstart](docker.md) instructions. + +### Download the official Freqtrade docker image + +Pull the image from docker hub. + +Branches / tags available can be checked out on [Dockerhub tags page](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/). + +```bash +docker pull freqtradeorg/freqtrade:master +# Optionally tag the repository so the run-commands remain shorter +docker tag freqtradeorg/freqtrade:master freqtrade +``` + +To update the image, simply run the above commands again and restart your running container. + +Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image). + +!!! Note "Docker image update frequency" + The official docker images with tags `master`, `develop` and `latest` are automatically rebuild once a week to keep the base image uptodate. + In addition to that, every merge to `develop` will trigger a rebuild for `develop` and `latest`. + +### Prepare the configuration files + +Even though you will use docker, you'll still need some files from the github repository. + +#### Clone the git repository + +Linux/Mac/Windows with WSL + +```bash +git clone https://github.com/freqtrade/freqtrade.git +``` + +Windows with docker + +```bash +git clone --config core.autocrlf=input https://github.com/freqtrade/freqtrade.git +``` + +#### Copy `config.json.example` to `config.json` + +```bash +cd freqtrade +cp -n config.json.example config.json +``` + +> To understand the configuration options, please refer to the [Bot Configuration](configuration.md) page. + +#### Create your database file + +=== "Dry-Run" + ``` bash + touch tradesv3.dryrun.sqlite + ``` + +=== "Production" + ``` bash + touch tradesv3.sqlite + ``` + + +!!! Warning Database File Path + Make sure to use the path to the correct database file when starting the bot in Docker. + +### Build your own Docker image + +Best start by pulling the official docker image from dockerhub as explained [here](#download-the-official-docker-image) to speed up building. + +To add additional libraries to your docker image, best check out [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) which adds the [technical](https://github.com/freqtrade/technical) module to the image. + +```bash +docker build -t freqtrade -f Dockerfile.technical . +``` + +If you are developing using Docker, use `Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies: + +```bash +docker build -f Dockerfile.develop -t freqtrade-dev . +``` + +!!! Warning Include your config file manually + For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see [5. Run a restartable docker image](#run-a-restartable-docker-image)") to keep it between updates. + +#### Verify the Docker image + +After the build process you can verify that the image was created with: + +```bash +docker images +``` + +The output should contain the freqtrade image. + +### Run the Docker image + +You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory): + +```bash +docker run --rm -v `pwd`/config.json:/freqtrade/config.json -it freqtrade +``` + +!!! Warning + In this example, the database will be created inside the docker instance and will be lost when you refresh your image. + +#### Adjust timezone + +By default, the container will use UTC timezone. +If you would like to change the timezone use the following commands: + +=== "Linux" + ``` bash + -v /etc/timezone:/etc/timezone:ro + + # Complete command: + docker run --rm -v /etc/timezone:/etc/timezone:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade + ``` + +=== "MacOS" + ```bash + docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade + ``` + +!!! Note MacOS Issues + The OSX Docker versions after 17.09.1 have a known issue whereby `/etc/localtime` cannot be shared causing Docker to not start.
    + A work-around for this is to start with the MacOS command above + More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396). + +### Run a restartable docker image + +To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem). + +#### 1. Move your config file and database + +The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden directory in your home directory. Feel free to use a different directory and replace the directory in the upcomming commands. + +```bash +mkdir ~/.freqtrade +mv config.json ~/.freqtrade +mv tradesv3.sqlite ~/.freqtrade +``` + +#### 2. Run the docker image + +```bash +docker run -d \ + --name freqtrade \ + -v ~/.freqtrade/config.json:/freqtrade/config.json \ + -v ~/.freqtrade/user_data/:/freqtrade/user_data \ + -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ + freqtrade trade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy +``` + +!!! Note + When using docker, it's best to specify `--db-url` explicitly to ensure that the database URL and the mounted database file match. + +!!! Note + All available bot command line parameters can be added to the end of the `docker run` command. + +!!! Note + You can define a [restart policy](https://docs.docker.com/config/containers/start-containers-automatically/) in docker. It can be useful in some cases to use the `--restart unless-stopped` flag (crash of freqtrade or reboot of your system). + +### Monitor your Docker instance + +You can use the following commands to monitor and manage your container: + +```bash +docker logs freqtrade +docker logs -f freqtrade +docker restart freqtrade +docker stop freqtrade +docker start freqtrade +``` + +For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/). + +!!! Note + You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container. + +### Backtest with docker + +The following assumes that the download/setup of the docker image have been completed successfully. +Also, backtest-data should be available at `~/.freqtrade/user_data/`. + +```bash +docker run -d \ + --name freqtrade \ + -v /etc/localtime:/etc/localtime:ro \ + -v ~/.freqtrade/config.json:/freqtrade/config.json \ + -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ + -v ~/.freqtrade/user_data/:/freqtrade/user_data/ \ + freqtrade backtesting --strategy AwsomelyProfitableStrategy +``` + +Head over to the [Backtesting Documentation](backtesting.md) for more details. + +!!! Note + Additional bot command line parameters can be appended after the image name (`freqtrade` in the above example). diff --git a/mkdocs.yml b/mkdocs.yml index 324dc46db..2750ed3a5 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,8 +1,13 @@ site_name: Freqtrade nav: - Home: index.md - - Installation Docker: docker.md - - Installation: installation.md + - Installation Docker: + - Quickstart: docker.md + - Freqtrade with Docker Compose (Advanced): docker_compose.md + - Freqtrade without docker-compose: without_docker_compose.md + - Installation: + - Linux/MacOS/Raspberry: installation.md + - Windows: windows_installation.md - Freqtrade Basics: bot-basics.md - Configuration: configuration.md - Strategy Customization: strategy-customization.md @@ -59,6 +64,7 @@ markdown_extensions: - pymdownx.magiclink - pymdownx.mark - pymdownx.smartsymbols + - pymdownx.tabbed - pymdownx.superfences - pymdownx.tasklist: custom_checkbox: true From 5c5cf782f53597f27bf0c361f86268d1c07a8ebf Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 3 Sep 2020 19:29:48 +0200 Subject: [PATCH 0555/1197] Fix small bug with /daily if close_profit_abs is not yet filled --- freqtrade/rpc/rpc.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 802c8372b..b89a95ee8 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -224,7 +224,8 @@ class RPC: Trade.close_date >= profitday, Trade.close_date < (profitday + timedelta(days=1)) ]).order_by(Trade.close_date).all() - curdayprofit = sum(trade.close_profit_abs for trade in trades) + curdayprofit = sum( + trade.close_profit_abs for trade in trades if trade.close_profit_abs is not None) profit_days[profitday] = { 'amount': curdayprofit, 'trades': len(trades) From 27362046d474744da563d74bd370da70eb82cd3b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 3 Sep 2020 19:33:34 +0200 Subject: [PATCH 0556/1197] Add documentation section about running docs locally --- docs/developer.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/developer.md b/docs/developer.md index 8bee1fd8e..111c7a96f 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -10,6 +10,15 @@ Documentation is available at [https://freqtrade.io](https://www.freqtrade.io/) Special fields for the documentation (like Note boxes, ...) can be found [here](https://squidfunk.github.io/mkdocs-material/extensions/admonition/). +To test the documentation locally use the following commands. + +``` bash +pip install -r docs/requirements-docs.txt +mkdocs serve +``` + +This will spin up a local server (usually on port 8000) so you can see if everything looks as you'd like it to. + ## Developer setup To configure a development environment, best use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ". From ec9b51d60a49feda0ab4d8dbb477780ffe149bca Mon Sep 17 00:00:00 2001 From: Victor Silva <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 12:49:32 -0600 Subject: [PATCH 0557/1197] Update docs/edge.md Co-authored-by: Matthias --- docs/edge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/edge.md b/docs/edge.md index 182c47651..6d43b0ea9 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -13,7 +13,7 @@ The `Edge Positioning` module uses probability to calculate your win rate and ri Trading strategies are not perfect. They are frameworks that are susceptible to the market and its indicators. Because the market is not at all predictable, sometimes a strategy will win and sometimes the same strategy will lose. -To obtain an edge in the market, a strategy has to make more money than it loses. Marking money in trading is not only about *how often* the strategy makes or loses money. +To obtain an edge in the market, a strategy has to make more money than it loses. Making money in trading is not only about *how often* the strategy makes or loses money. !!! tip "It doesn't matter how often, but how much!" A bad strategy might make 1 penny in *ten* transactions but lose 1 dollar in *one* transaction. If one only checks the number of winning trades, it would be misleading to think that the strategy is actually making a profit. From 69349a9d8d047c762e567723637a68259a478bd2 Mon Sep 17 00:00:00 2001 From: Victor Silva <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 12:49:54 -0600 Subject: [PATCH 0558/1197] Update docs/edge.md Co-authored-by: Matthias --- docs/edge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/edge.md b/docs/edge.md index 6d43b0ea9..8fae683a5 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -82,7 +82,7 @@ Risk Reward Ratio (*R*) is a formula used to measure the expected gains of a giv $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ ??? Example "Worked example of $R$ calculation" - Let's say that you think that the price of *stonecoin* today is $10.0. You believe that, because they will start mining stonecoin it will go up to $15.0 tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to $0 tomorrow. You are planning to invest $100.
    + Let's say that you think that the price of *stonecoin* today is $10.0. You believe that, because they will start mining stonecoin, it will go up to $15.0 tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to $0 tomorrow. You are planning to invest $100.
    Your potential profit is calculated as:
    $\begin{aligned} \text{potential_profit} &= (\text{potential_price} - \text{cost_per_unit}) * \frac{\text{investment}}{\text{cost_per_unit}} \\ From 70eaf971cd55c45c9e27c8420f23e5c41a875ecc Mon Sep 17 00:00:00 2001 From: Victor Silva <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 12:50:23 -0600 Subject: [PATCH 0559/1197] Update docs/edge.md Co-authored-by: Matthias --- docs/edge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/edge.md b/docs/edge.md index 8fae683a5..7634718ae 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -77,7 +77,7 @@ Where $L$ is the lose rate, $N$ is the amount of trades made and, $T_{lose}$ is ### Risk Reward Ratio -Risk Reward Ratio (*R*) is a formula used to measure the expected gains of a given investment against the risk of loss. It is basically what you potentially win divided by what you potentially lose. Formally: +Risk Reward Ratio ($R$) is a formula used to measure the expected gains of a given investment against the risk of loss. It is basically what you potentially win divided by what you potentially lose. Formally: $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ From 5f9c449d8eecd8a1af79c2a1e7a3f5b897ea5eb6 Mon Sep 17 00:00:00 2001 From: Victor Silva <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 12:53:33 -0600 Subject: [PATCH 0560/1197] Update docs/edge.md Co-authored-by: Matthias --- docs/edge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/edge.md b/docs/edge.md index 7634718ae..e6b27a340 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -97,7 +97,7 @@ $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ \end{aligned}$
    What it effectivelly means is that the strategy have the potential to make $0.33 for each $1 invested. -On a long horizonte, that is, on many trades, we can calculate the risk reward by dividing the strategy' average profit on winning trades by the strategy' average loss on losing trades. We can calculate the average profit, $\mu_{win}$, as follows: +On a long horizon, that is, on many trades, we can calculate the risk reward by dividing the strategy' average profit on winning trades by the strategy' average loss on losing trades. We can calculate the average profit, $\mu_{win}$, as follows: $$ \text{average_profit} = \mu_{win} = \frac{\text{sum_of_profits}}{\text{count_winning_trades}} = \frac{\sum^{o \in T_{win}} o}{|T_{win}|} $$ From 08e35461209d47ab0b57b838da55add33d8a5575 Mon Sep 17 00:00:00 2001 From: Victor Silva <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 12:55:07 -0600 Subject: [PATCH 0561/1197] Update docs/edge.md Co-authored-by: Matthias --- docs/edge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/edge.md b/docs/edge.md index e6b27a340..8c8b230c9 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -105,7 +105,7 @@ Similarly, we can calculate the average loss, $\mu_{lose}$, as follows: $$ \text{average_loss} = \mu_{lose} = \frac{\text{sum_of_losses}}{\text{count_losing_trades}} = \frac{\sum^{o \in T_{lose}} o}{|T_{lose}|} $$ -Finally, we can calculate the Risk Reward ratio as follows: +Finally, we can calculate the Risk Reward ratio, $R$, as follows: $$ R = \frac{\text{average_profit}}{\text{average_loss}} = \frac{\mu_{win}}{\mu_{lose}}\\ $$ From 1f13a8b91d28d5f25579b2010e9b4246bece4a90 Mon Sep 17 00:00:00 2001 From: Victor Silva <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 12:55:49 -0600 Subject: [PATCH 0562/1197] Update docs/edge.md Co-authored-by: Matthias --- docs/edge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/edge.md b/docs/edge.md index 8c8b230c9..a5df45901 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -129,7 +129,7 @@ $$E = R * W - L$$ The expectancy worked out in the example above means that, on average, this strategy' trades will return 1.68 times the size of its losses. Said another way, the strategy makes $1.68 for every $1 it loses, on average. -You canThis is important for two reasons: First, it may seem obvious, but you know right away that you have a positive return. Second, you now have a number you can compare to other candidate systems to make decisions about which ones you employ. +This is important for two reasons: First, it may seem obvious, but you know right away that you have a positive return. Second, you now have a number you can compare to other candidate systems to make decisions about which ones you employ. It is important to remember that any system with an expectancy greater than 0 is profitable using past data. The key is finding one that will be profitable in the future. From 93d1ad5ed9569072a51678e3a3b2634be41bc8fd Mon Sep 17 00:00:00 2001 From: Victor Silva <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 12:56:54 -0600 Subject: [PATCH 0563/1197] Update docs/edge.md Co-authored-by: Matthias --- docs/edge.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/edge.md b/docs/edge.md index a5df45901..f7870ac1a 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -135,7 +135,8 @@ It is important to remember that any system with an expectancy greater than 0 is You can also use this value to evaluate the effectiveness of modifications to this system. -**NOTICE:** It's important to keep in mind that Edge is testing your expectancy using historical data, there's no guarantee that you will have a similar edge in the future. It's still vital to do this testing in order to build confidence in your methodology but be wary of "curve-fitting" your approach to the historical data as things are unlikely to play out the exact same way for future trades. +!!! Note + It's important to keep in mind that Edge is testing your expectancy using historical data, there's no guarantee that you will have a similar edge in the future. It's still vital to do this testing in order to build confidence in your methodology but be wary of "curve-fitting" your approach to the historical data as things are unlikely to play out the exact same way for future trades. ## How does it work? From 47f0e69072975469acabe808304f1a9491086db2 Mon Sep 17 00:00:00 2001 From: Victor Silva <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 12:57:15 -0600 Subject: [PATCH 0564/1197] Update docs/edge.md Co-authored-by: Matthias --- docs/edge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/edge.md b/docs/edge.md index f7870ac1a..593137c8d 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -149,7 +149,7 @@ Edge combines dynamic stoploss, dynamic positions, and whitelist generation into | XZC/ETH | -0.03 | 0.52 |1.359670 | 0.228 | | XZC/ETH | -0.04 | 0.51 |1.234539 | 0.117 | -The goal here is to find the best stoploss for the strategy in order to have the maximum expectancy. In the above example stoploss at $3% $leads to the maximum expectancy according to historical data. +The goal here is to find the best stoploss for the strategy in order to have the maximum expectancy. In the above example stoploss at $3%$ leads to the maximum expectancy according to historical data. Edge module then forces stoploss value it evaluated to your strategy dynamically. From 714264701c5bcc338ebee9b1dd8286e3d7cb90cd Mon Sep 17 00:00:00 2001 From: silvavn <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 13:11:04 -0600 Subject: [PATCH 0565/1197] Fixes typos --- docs/without_docker_compose.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/without_docker_compose.md b/docs/without_docker_compose.md index 23994f38f..3fe335cf0 100644 --- a/docs/without_docker_compose.md +++ b/docs/without_docker_compose.md @@ -63,7 +63,7 @@ cp -n config.json.example config.json ``` -!!! Warning Database File Path +!!! Warning "Database File Path" Make sure to use the path to the correct database file when starting the bot in Docker. ### Build your own Docker image @@ -82,7 +82,7 @@ If you are developing using Docker, use `Dockerfile.develop` to build a dev Dock docker build -f Dockerfile.develop -t freqtrade-dev . ``` -!!! Warning Include your config file manually +!!! Warning "Include your config file manually" For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see [5. Run a restartable docker image](#run-a-restartable-docker-image)") to keep it between updates. #### Verify the Docker image @@ -124,7 +124,7 @@ If you would like to change the timezone use the following commands: docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade ``` -!!! Note MacOS Issues +!!! Note "MacOS Issues" The OSX Docker versions after 17.09.1 have a known issue whereby `/etc/localtime` cannot be shared causing Docker to not start.
    A work-around for this is to start with the MacOS command above More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396). From f6a8dda8e5600ca8a95108013cb1903240085bec Mon Sep 17 00:00:00 2001 From: silvavn <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 13:12:43 -0600 Subject: [PATCH 0566/1197] Reorganize structure - Quickstart moved out of installation - Installation now contains only advanced modes. - Joined quickstart with Docker --- mkdocs.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/mkdocs.yml b/mkdocs.yml index 2750ed3a5..5d936687f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,11 +1,9 @@ site_name: Freqtrade nav: - Home: index.md - - Installation Docker: - - Quickstart: docker.md - - Freqtrade with Docker Compose (Advanced): docker_compose.md - - Freqtrade without docker-compose: without_docker_compose.md + - Quickstart with Docker: docker.md - Installation: + - Freqtrade without docker-compose: without_docker_compose.md - Linux/MacOS/Raspberry: installation.md - Windows: windows_installation.md - Freqtrade Basics: bot-basics.md From 66505bd9bf2f281417044b5259db345b4621b6c8 Mon Sep 17 00:00:00 2001 From: silvavn <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 13:18:15 -0600 Subject: [PATCH 0567/1197] Fixes Raspberri Pi Image config --- docs/docker.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/docker.md b/docs/docker.md index 83f2d06e3..9c8cc1683 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -47,7 +47,7 @@ Create a new directory and place the [docker-compose file](https://github.com/fr mkdir ft_userdata cd ft_userdata/ # Download the docker-compose file from the repository - curl https://raw.githubusercontent.com/freqtrade/freqtrade/master_pi/docker-compose.yml -o docker-compose.yml + curl https://raw.githubusercontent.com/freqtrade/freqtrade/master/docker-compose.yml -o docker-compose.yml # Pull the freqtrade image docker-compose pull @@ -59,6 +59,13 @@ Create a new directory and place the [docker-compose file](https://github.com/fr docker-compose run --rm freqtrade new-config --config user_data/config.json ``` + !!! Note "Change your docker Image" + You should change the docker image in your config file for your Raspeberry build to work properly. + ``` bash + image: freqtradeorg/freqtrade:master_pi + # image: freqtradeorg/freqtrade:develop_pi + ``` + The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image. The last 2 steps in the snippet create the directory with `user_data`, as well as (interactively) the default configuration based on your selections. From e6058b716b1be8711d54d9274b0509700fdda5dd Mon Sep 17 00:00:00 2001 From: silvavn <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 13:19:05 -0600 Subject: [PATCH 0568/1197] removes prolixity docker-compose --- docs/docker.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/docker.md b/docs/docker.md index 9c8cc1683..9f90502bf 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -104,13 +104,13 @@ The database will be at: `user_data/tradesv3.sqlite` To update freqtrade when using `docker-compose` is as simple as running the following 2 commands: -=== "Docker Compose" - ``` bash - # Download the latest image - docker-compose pull - # Restart the image - docker-compose up -d - ``` + +``` bash +# Download the latest image +docker-compose pull +# Restart the image +docker-compose up -d +``` This will first pull the latest image, and will then restart the container with the just pulled version. From 29fe2ffff74de8f1ee2f3ce1f53f1da4f6cbf960 Mon Sep 17 00:00:00 2001 From: silvavn <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 13:22:22 -0600 Subject: [PATCH 0569/1197] Added that the user can edit docker-compose.yml --- docs/docker.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/docker.md b/docs/docker.md index 9f90502bf..ae16fe922 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -60,7 +60,7 @@ Create a new directory and place the [docker-compose file](https://github.com/fr ``` !!! Note "Change your docker Image" - You should change the docker image in your config file for your Raspeberry build to work properly. + You should change the docker image in your config file for your Raspberry build to work properly. ``` bash image: freqtradeorg/freqtrade:master_pi # image: freqtradeorg/freqtrade:develop_pi @@ -72,6 +72,8 @@ The last 2 steps in the snippet create the directory with `user_data`, as well a !!! Question "How to edit the bot configuration?" You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. + You can also change the both Strategy and commands by editing the `docker-compose.yml` file. + #### Adding a custom strategy 1. The configuration is now available as `user_data/config.json` From 34b27d2f96808846e64430a9c19724519cfb5309 Mon Sep 17 00:00:00 2001 From: silvavn <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 13:32:07 -0600 Subject: [PATCH 0570/1197] Moving stuff around - Mac troubleshooting to the end - optional master checkout - Anaconda moved to the end --- docs/installation.md | 50 +++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 979679c9f..83dd2938c 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -38,7 +38,7 @@ This can be achieved with the following commands: ```bash git clone https://github.com/freqtrade/freqtrade.git cd freqtrade -git checkout master # Optional, see (1) +# git checkout master # Optional, see (1) ./setup.sh --install ``` @@ -79,18 +79,7 @@ This option will hard reset your branch (only if you are on either `master` or ` DEPRECATED - use `freqtrade new-config -c config.json` instead. -### MacOS installation error -Newer versions of MacOS may have installation failed with errors like `error: command 'g++' failed with exit status 1`. - -This error will require explicit installation of the SDK Headers, which are not installed by default in this version of MacOS. -For MacOS 10.14, this can be accomplished with the below command. - -``` bash -open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg -``` - -If this file is inexistant, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details. ------ @@ -132,16 +121,6 @@ OS Specific steps are listed first, the [Common](#common) section below is neces The above does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`. We do not advise to run hyperopt on a Raspberry Pi, since this is a very resource-heavy operation, which should be done on powerful machine. -=== "Anaconda" - Freqtrade can also be installed using Anaconda (or Miniconda). - - !!! Note - This requires the [ta-lib](#1-install-ta-lib) C-library to be installed first. See below. - - ``` bash - conda env create -f environment.yml - ``` - ### Common #### 1. Install TA-Lib @@ -229,6 +208,33 @@ On Linux, as an optional post-installation task, you may wish to setup the bot t ------ +### Anaconda + +Freqtrade can also be installed using Anaconda (or Miniconda). + +!!! Note + This requires the [ta-lib](#1-install-ta-lib) C-library to be installed first. See below. + +``` bash +conda env create -f environment.yml +``` + +----- + Now you have an environment ready, the next step is [Bot Configuration](configuration.md). +----- + +### MacOS installation error + +Newer versions of MacOS may have installation failed with errors like `error: command 'g++' failed with exit status 1`. + +This error will require explicit installation of the SDK Headers, which are not installed by default in this version of MacOS. +For MacOS 10.14, this can be accomplished with the below command. + +``` bash +open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg +``` + +If this file is inexistant, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details. \ No newline at end of file From 275d8534323129124a1a5ed369fe7a38576e7251 Mon Sep 17 00:00:00 2001 From: silvavn <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 13:38:46 -0600 Subject: [PATCH 0571/1197] Updated W, L Formulas --- docs/edge.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/edge.md b/docs/edge.md index 593137c8d..e147cc15e 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -26,7 +26,7 @@ We raise the following question[^1]: a) A trade with 80% of chance of losing $100 and 20% chance of winning $200
    b) A trade with 100% of chance of losing $30 -??? Info "Answer" +???+ Info "Answer" The expected value of *a)* is smaller than the expected value of *b)*.
    Hence, *b*) represents a smaller loss in the long run.
    However, the answer is: *it depends* @@ -63,14 +63,14 @@ $$ T_{lose} = \{o \in O | o \leq 0\} $$ The win rate $W$ is the proportion of winning trades with respect to all the trades made by a strategy. We use the following function to compute the win rate: -$$W = \frac{\sum^{o \in T_{win}} o}{N}$$ +$$W = \frac{|T_{win}|}{N}$$ Where $W$ is the win rate, $N$ is the number of trades and, $T_{win}$ is the set of all trades where the strategy made money. Similarly, we can compute the rate of losing trades: $$ - L = \frac{\sum^{o \in T_{lose}} o}{N} + L = \frac{|T_{lose}|}{N} $$ Where $L$ is the lose rate, $N$ is the amount of trades made and, $T_{lose}$ is the set of all trades where the strategy lost money. Note that the above formula is the same as calculating $L = 1 – W$ or $W = 1 – L$ @@ -81,7 +81,7 @@ Risk Reward Ratio ($R$) is a formula used to measure the expected gains of a giv $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ -??? Example "Worked example of $R$ calculation" +???+ Example "Worked example of $R$ calculation" Let's say that you think that the price of *stonecoin* today is $10.0. You believe that, because they will start mining stonecoin, it will go up to $15.0 tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to $0 tomorrow. You are planning to invest $100.
    Your potential profit is calculated as:
    $\begin{aligned} @@ -110,7 +110,7 @@ Finally, we can calculate the Risk Reward ratio, $R$, as follows: $$ R = \frac{\text{average_profit}}{\text{average_loss}} = \frac{\mu_{win}}{\mu_{lose}}\\ $$ -??? Example "Worked example of $R$ calculation using mean profit/loss" +???+ Example "Worked example of $R$ calculation using mean profit/loss" Let's say the strategy that we are using makes an average win $\mu_{win} = 2.06$ and an average loss $\mu_{loss} = 4.11$.
    We calculate the risk reward ratio as follows:
    $R = \frac{\mu_{win}}{\mu_{loss}} = \frac{2.06}{4.11} = 0.5012...$ From 32005b886a9cc5916e015cb1308ce3e78572db7b Mon Sep 17 00:00:00 2001 From: silvavn <37382997+silvavn@users.noreply.github.com> Date: Thu, 3 Sep 2020 13:39:38 -0600 Subject: [PATCH 0572/1197] small typo --- docs/edge.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/edge.md b/docs/edge.md index e147cc15e..500c3c833 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -44,7 +44,7 @@ Edge positioning tries to answer the hard questions about risk/reward and positi Let's call $o$ the return of a single transaction $o$ where $o \in \mathbb{R}$. The collection $O = \{o_1, o_2, ..., o_N\}$ is the set of all returns of transactions made during a trading session. We say that $N$ is the cardinality of $O$, or, in lay terms, it is the number of transactions made in a trading session. !!! Example - In a session where a strategy made three transactions we can say that $O = \{3.5, -1, 15\}$. That means that $N = 3$ and $o_1 = 3.5$, $o_2 = -1$, $o = 15$. + In a session where a strategy made three transactions we can say that $O = \{3.5, -1, 15\}$. That means that $N = 3$ and $o_1 = 3.5$, $o_2 = -1$, $o_3 = 15$. A winning trade is a trade where a strategy *made* money. Making money means that the strategy closed the position in a value that returned a profit, after all deducted fees. Formally, a winning trade will have a return $o_i > 0$. Similarly, a losing trade will have a return $o_j \leq 0$. With that, we can discover the set of all winning trades, $T_{win}$, as follows: From 1406691945511db6c1de8f69b89f03fd1a09af75 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 4 Sep 2020 07:12:08 +0200 Subject: [PATCH 0573/1197] Rename files to have clearer paths --- docs/docker.md | 304 +++++++++++++++++++++------------ docs/docker_compose.md | 44 ----- docs/docker_quickstart.md | 162 ++++++++++++++++++ docs/installation.md | 13 +- docs/without_docker_compose.md | 201 ---------------------- mkdocs.yml | 6 +- 6 files changed, 364 insertions(+), 366 deletions(-) delete mode 100644 docs/docker_compose.md create mode 100644 docs/docker_quickstart.md delete mode 100644 docs/without_docker_compose.md diff --git a/docs/docker.md b/docs/docker.md index ae16fe922..3fe335cf0 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -1,121 +1,201 @@ -# Using Freqtrade with Docker +## Freqtrade with docker without docker-compose -## Install Docker +!!! Warning + The below documentation is provided for completeness and assumes that you are familiar with running docker containers. If you're just starting out with Docker, we recommend to follow the [Quickstart](docker.md) instructions. -Start by downloading and installing Docker CE for your platform: +### Download the official Freqtrade docker image -* [Mac](https://docs.docker.com/docker-for-mac/install/) -* [Windows](https://docs.docker.com/docker-for-windows/install/) -* [Linux](https://docs.docker.com/install/) +Pull the image from docker hub. -Optionally, [`docker-compose`](https://docs.docker.com/compose/install/) should be installed and available to follow the [docker quick start guide](#docker-quick-start). +Branches / tags available can be checked out on [Dockerhub tags page](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/). -Once you have Docker installed, simply prepare the config file (e.g. `config.json`) and run the image for `freqtrade` as explained below. - -## Freqtrade with docker-compose - -Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) ready for usage. - -!!! Note - - The following section assumes that `docker` and `docker-compose` are installed and available to the logged in user. - - All below comands use relative directories and will have to be executed from the directory containing the `docker-compose.yml` file. - - -### Docker quick start - -Create a new directory and place the [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) in this directory. - -=== "PC/MAC/Linux" - ``` bash - mkdir ft_userdata - cd ft_userdata/ - # Download the docker-compose file from the repository - curl https://raw.githubusercontent.com/freqtrade/freqtrade/master/docker-compose.yml -o docker-compose.yml - - # Pull the freqtrade image - docker-compose pull - - # Create user directory structure - docker-compose run --rm freqtrade create-userdir --userdir user_data - - # Create configuration - Requires answering interactive questions - docker-compose run --rm freqtrade new-config --config user_data/config.json - ``` - -=== "RaspberryPi" - ``` bash - mkdir ft_userdata - cd ft_userdata/ - # Download the docker-compose file from the repository - curl https://raw.githubusercontent.com/freqtrade/freqtrade/master/docker-compose.yml -o docker-compose.yml - - # Pull the freqtrade image - docker-compose pull - - # Create user directory structure - docker-compose run --rm freqtrade create-userdir --userdir user_data - - # Create configuration - Requires answering interactive questions - docker-compose run --rm freqtrade new-config --config user_data/config.json - ``` - - !!! Note "Change your docker Image" - You should change the docker image in your config file for your Raspberry build to work properly. - ``` bash - image: freqtradeorg/freqtrade:master_pi - # image: freqtradeorg/freqtrade:develop_pi - ``` - -The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image. -The last 2 steps in the snippet create the directory with `user_data`, as well as (interactively) the default configuration based on your selections. - -!!! Question "How to edit the bot configuration?" - You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. - - You can also change the both Strategy and commands by editing the `docker-compose.yml` file. - -#### Adding a custom strategy - -1. The configuration is now available as `user_data/config.json` -2. Copy a custom strategy to the directory `user_data/strategies/` -3. add the Strategy' class name to the `docker-compose.yml` file - -The `SampleStrategy` is run by default. - -!!! Warning "`SampleStrategy` is just a demo!" - The `SampleStrategy` is there for your reference and give you ideas for your own strategy. - Please always backtest the strategy and use dry-run for some time before risking real money! - -Once this is done, you're ready to launch the bot in trading mode (Dry-run or Live-trading, depending on your answer to the corresponding question you made above). - -=== "Docker Compose" - ``` bash - docker-compose up -d - ``` - -#### Docker-compose logs - -Logs will be located at: `user_data/logs/freqtrade.log`. -You can check the latest log with the command `docker-compose logs -f`. - -#### Database - -The database will be at: `user_data/tradesv3.sqlite` - -#### Updating freqtrade with docker-compose - -To update freqtrade when using `docker-compose` is as simple as running the following 2 commands: - - -``` bash -# Download the latest image -docker-compose pull -# Restart the image -docker-compose up -d +```bash +docker pull freqtradeorg/freqtrade:master +# Optionally tag the repository so the run-commands remain shorter +docker tag freqtradeorg/freqtrade:master freqtrade ``` -This will first pull the latest image, and will then restart the container with the just pulled version. +To update the image, simply run the above commands again and restart your running container. -!!! Warning "Check the Changelog" - You should always check the changelog for breaking changes / manual interventions required and make sure the bot starts correctly after the update. +Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image). +!!! Note "Docker image update frequency" + The official docker images with tags `master`, `develop` and `latest` are automatically rebuild once a week to keep the base image uptodate. + In addition to that, every merge to `develop` will trigger a rebuild for `develop` and `latest`. + +### Prepare the configuration files + +Even though you will use docker, you'll still need some files from the github repository. + +#### Clone the git repository + +Linux/Mac/Windows with WSL + +```bash +git clone https://github.com/freqtrade/freqtrade.git +``` + +Windows with docker + +```bash +git clone --config core.autocrlf=input https://github.com/freqtrade/freqtrade.git +``` + +#### Copy `config.json.example` to `config.json` + +```bash +cd freqtrade +cp -n config.json.example config.json +``` + +> To understand the configuration options, please refer to the [Bot Configuration](configuration.md) page. + +#### Create your database file + +=== "Dry-Run" + ``` bash + touch tradesv3.dryrun.sqlite + ``` + +=== "Production" + ``` bash + touch tradesv3.sqlite + ``` + + +!!! Warning "Database File Path" + Make sure to use the path to the correct database file when starting the bot in Docker. + +### Build your own Docker image + +Best start by pulling the official docker image from dockerhub as explained [here](#download-the-official-docker-image) to speed up building. + +To add additional libraries to your docker image, best check out [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) which adds the [technical](https://github.com/freqtrade/technical) module to the image. + +```bash +docker build -t freqtrade -f Dockerfile.technical . +``` + +If you are developing using Docker, use `Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies: + +```bash +docker build -f Dockerfile.develop -t freqtrade-dev . +``` + +!!! Warning "Include your config file manually" + For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see [5. Run a restartable docker image](#run-a-restartable-docker-image)") to keep it between updates. + +#### Verify the Docker image + +After the build process you can verify that the image was created with: + +```bash +docker images +``` + +The output should contain the freqtrade image. + +### Run the Docker image + +You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory): + +```bash +docker run --rm -v `pwd`/config.json:/freqtrade/config.json -it freqtrade +``` + +!!! Warning + In this example, the database will be created inside the docker instance and will be lost when you refresh your image. + +#### Adjust timezone + +By default, the container will use UTC timezone. +If you would like to change the timezone use the following commands: + +=== "Linux" + ``` bash + -v /etc/timezone:/etc/timezone:ro + + # Complete command: + docker run --rm -v /etc/timezone:/etc/timezone:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade + ``` + +=== "MacOS" + ```bash + docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade + ``` + +!!! Note "MacOS Issues" + The OSX Docker versions after 17.09.1 have a known issue whereby `/etc/localtime` cannot be shared causing Docker to not start.
    + A work-around for this is to start with the MacOS command above + More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396). + +### Run a restartable docker image + +To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem). + +#### 1. Move your config file and database + +The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden directory in your home directory. Feel free to use a different directory and replace the directory in the upcomming commands. + +```bash +mkdir ~/.freqtrade +mv config.json ~/.freqtrade +mv tradesv3.sqlite ~/.freqtrade +``` + +#### 2. Run the docker image + +```bash +docker run -d \ + --name freqtrade \ + -v ~/.freqtrade/config.json:/freqtrade/config.json \ + -v ~/.freqtrade/user_data/:/freqtrade/user_data \ + -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ + freqtrade trade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy +``` + +!!! Note + When using docker, it's best to specify `--db-url` explicitly to ensure that the database URL and the mounted database file match. + +!!! Note + All available bot command line parameters can be added to the end of the `docker run` command. + +!!! Note + You can define a [restart policy](https://docs.docker.com/config/containers/start-containers-automatically/) in docker. It can be useful in some cases to use the `--restart unless-stopped` flag (crash of freqtrade or reboot of your system). + +### Monitor your Docker instance + +You can use the following commands to monitor and manage your container: + +```bash +docker logs freqtrade +docker logs -f freqtrade +docker restart freqtrade +docker stop freqtrade +docker start freqtrade +``` + +For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/). + +!!! Note + You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container. + +### Backtest with docker + +The following assumes that the download/setup of the docker image have been completed successfully. +Also, backtest-data should be available at `~/.freqtrade/user_data/`. + +```bash +docker run -d \ + --name freqtrade \ + -v /etc/localtime:/etc/localtime:ro \ + -v ~/.freqtrade/config.json:/freqtrade/config.json \ + -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ + -v ~/.freqtrade/user_data/:/freqtrade/user_data/ \ + freqtrade backtesting --strategy AwsomelyProfitableStrategy +``` + +Head over to the [Backtesting Documentation](backtesting.md) for more details. + +!!! Note + Additional bot command line parameters can be appended after the image name (`freqtrade` in the above example). diff --git a/docs/docker_compose.md b/docs/docker_compose.md deleted file mode 100644 index 302d3b358..000000000 --- a/docs/docker_compose.md +++ /dev/null @@ -1,44 +0,0 @@ -#### Editing the docker-compose file - -Advanced users may edit the docker-compose file further to include all possible options or arguments. - -All possible freqtrade arguments will be available by running `docker-compose run --rm freqtrade `. - -!!! Note "`docker-compose run --rm`" - Including `--rm` will clean up the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). - -##### Example: Download data with docker-compose - -Download backtesting data for 5 days for the pair ETH/BTC and 1h timeframe from Binance. The data will be stored in the directory `user_data/data/` on the host. - -``` bash -docker-compose run --rm freqtrade download-data --pairs ETH/BTC --exchange binance --days 5 -t 1h -``` - -Head over to the [Data Downloading Documentation](data-download.md) for more details on downloading data. - -##### Example: Backtest with docker-compose - -Run backtesting in docker-containers for SampleStrategy and specified timerange of historical data, on 5m timeframe: - -``` bash -docker-compose run --rm freqtrade backtesting --config user_data/config.json --strategy SampleStrategy --timerange 20190801-20191001 -i 5m -``` - -Head over to the [Backtesting Documentation](backtesting.md) to learn more. - -#### Additional dependencies with docker-compose - -If your strategy requires dependencies not included in the default image (like [technical](https://github.com/freqtrade/technical)) - it will be necessary to build the image on your host. -For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) for an example). - -You'll then also need to modify the `docker-compose.yml` file and uncomment the build step, as well as rename the image to avoid naming collisions. - -``` yaml - image: freqtrade_custom - build: - context: . - dockerfile: "./Dockerfile." -``` - -You can then run `docker-compose build` to build the docker image, and run it using the commands described above. \ No newline at end of file diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md new file mode 100644 index 000000000..c033e827b --- /dev/null +++ b/docs/docker_quickstart.md @@ -0,0 +1,162 @@ +# Using Freqtrade with Docker + +## Install Docker + +Start by downloading and installing Docker CE for your platform: + +* [Mac](https://docs.docker.com/docker-for-mac/install/) +* [Windows](https://docs.docker.com/docker-for-windows/install/) +* [Linux](https://docs.docker.com/install/) + +Optionally, [`docker-compose`](https://docs.docker.com/compose/install/) should be installed and available to follow the [docker quick start guide](#docker-quick-start). + +Once you have Docker installed, simply prepare the config file (e.g. `config.json`) and run the image for `freqtrade` as explained below. + +## Freqtrade with docker-compose + +Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) ready for usage. + +!!! Note + - The following section assumes that `docker` and `docker-compose` are installed and available to the logged in user. + - All below commands use relative directories and will have to be executed from the directory containing the `docker-compose.yml` file. + +### Docker quick start + +Create a new directory and place the [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) in this directory. + +=== "PC/MAC/Linux" + ``` bash + mkdir ft_userdata + cd ft_userdata/ + # Download the docker-compose file from the repository + curl https://raw.githubusercontent.com/freqtrade/freqtrade/master/docker-compose.yml -o docker-compose.yml + + # Pull the freqtrade image + docker-compose pull + + # Create user directory structure + docker-compose run --rm freqtrade create-userdir --userdir user_data + + # Create configuration - Requires answering interactive questions + docker-compose run --rm freqtrade new-config --config user_data/config.json + ``` + +=== "RaspberryPi" + ``` bash + mkdir ft_userdata + cd ft_userdata/ + # Download the docker-compose file from the repository + curl https://raw.githubusercontent.com/freqtrade/freqtrade/master/docker-compose.yml -o docker-compose.yml + + # Pull the freqtrade image + docker-compose pull + + # Create user directory structure + docker-compose run --rm freqtrade create-userdir --userdir user_data + + # Create configuration - Requires answering interactive questions + docker-compose run --rm freqtrade new-config --config user_data/config.json + ``` + + !!! Note "Change your docker Image" + You have to change the docker image in the docker-compose file for your Raspberry build to work properly. + ``` yml + image: freqtradeorg/freqtrade:master_pi + # image: freqtradeorg/freqtrade:develop_pi + ``` + +The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image. +The last 2 steps in the snippet create the directory with `user_data`, as well as (interactively) the default configuration based on your selections. + +!!! Question "How to edit the bot configuration?" + You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. + + You can also change the both Strategy and commands by editing the `docker-compose.yml` file. + +#### Adding a custom strategy + +1. The configuration is now available as `user_data/config.json` +2. Copy a custom strategy to the directory `user_data/strategies/` +3. add the Strategy' class name to the `docker-compose.yml` file + +The `SampleStrategy` is run by default. + +!!! Warning "`SampleStrategy` is just a demo!" + The `SampleStrategy` is there for your reference and give you ideas for your own strategy. + Please always backtest the strategy and use dry-run for some time before risking real money! + +Once this is done, you're ready to launch the bot in trading mode (Dry-run or Live-trading, depending on your answer to the corresponding question you made above). + +``` bash +docker-compose up -d +``` + +#### Docker-compose logs + +Logs will be located at: `user_data/logs/freqtrade.log`. +You can check the latest log with the command `docker-compose logs -f`. + +#### Database + +The database will be at: `user_data/tradesv3.sqlite` + +#### Updating freqtrade with docker-compose + +To update freqtrade when using `docker-compose` is as simple as running the following 2 commands: + +``` bash +# Download the latest image +docker-compose pull +# Restart the image +docker-compose up -d +``` + +This will first pull the latest image, and will then restart the container with the just pulled version. + +!!! Warning "Check the Changelog" + You should always check the changelog for breaking changes / manual interventions required and make sure the bot starts correctly after the update. + +### Editing the docker-compose file + +Advanced users may edit the docker-compose file further to include all possible options or arguments. + +All possible freqtrade arguments will be available by running `docker-compose run --rm freqtrade `. + +!!! Note "`docker-compose run --rm`" + Including `--rm` will clean up the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). + +#### Example: Download data with docker-compose + +Download backtesting data for 5 days for the pair ETH/BTC and 1h timeframe from Binance. The data will be stored in the directory `user_data/data/` on the host. + +``` bash +docker-compose run --rm freqtrade download-data --pairs ETH/BTC --exchange binance --days 5 -t 1h +``` + +Head over to the [Data Downloading Documentation](data-download.md) for more details on downloading data. + +#### Example: Backtest with docker-compose + +Run backtesting in docker-containers for SampleStrategy and specified timerange of historical data, on 5m timeframe: + +``` bash +docker-compose run --rm freqtrade backtesting --config user_data/config.json --strategy SampleStrategy --timerange 20190801-20191001 -i 5m +``` + +Head over to the [Backtesting Documentation](backtesting.md) to learn more. + +### Additional dependencies with docker-compose + +If your strategy requires dependencies not included in the default image (like [technical](https://github.com/freqtrade/technical)) - it will be necessary to build the image on your host. +For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) for an example). + +You'll then also need to modify the `docker-compose.yml` file and uncomment the build step, as well as rename the image to avoid naming collisions. + +``` yaml + image: freqtrade_custom + build: + context: . + dockerfile: "./Dockerfile." +``` + +You can then run `docker-compose build` to build the docker image, and run it using the commands described above. diff --git a/docs/installation.md b/docs/installation.md index 83dd2938c..baa4a64d7 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -220,11 +220,7 @@ conda env create -f environment.yml ``` ----- - -Now you have an environment ready, the next step is -[Bot Configuration](configuration.md). - ------ +## Troubleshooting ### MacOS installation error @@ -237,4 +233,9 @@ For MacOS 10.14, this can be accomplished with the below command. open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg ``` -If this file is inexistant, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details. \ No newline at end of file +If this file is inexistent, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details. + +----- + +Now you have an environment ready, the next step is +[Bot Configuration](configuration.md). diff --git a/docs/without_docker_compose.md b/docs/without_docker_compose.md deleted file mode 100644 index 3fe335cf0..000000000 --- a/docs/without_docker_compose.md +++ /dev/null @@ -1,201 +0,0 @@ -## Freqtrade with docker without docker-compose - -!!! Warning - The below documentation is provided for completeness and assumes that you are familiar with running docker containers. If you're just starting out with Docker, we recommend to follow the [Quickstart](docker.md) instructions. - -### Download the official Freqtrade docker image - -Pull the image from docker hub. - -Branches / tags available can be checked out on [Dockerhub tags page](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/). - -```bash -docker pull freqtradeorg/freqtrade:master -# Optionally tag the repository so the run-commands remain shorter -docker tag freqtradeorg/freqtrade:master freqtrade -``` - -To update the image, simply run the above commands again and restart your running container. - -Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image). - -!!! Note "Docker image update frequency" - The official docker images with tags `master`, `develop` and `latest` are automatically rebuild once a week to keep the base image uptodate. - In addition to that, every merge to `develop` will trigger a rebuild for `develop` and `latest`. - -### Prepare the configuration files - -Even though you will use docker, you'll still need some files from the github repository. - -#### Clone the git repository - -Linux/Mac/Windows with WSL - -```bash -git clone https://github.com/freqtrade/freqtrade.git -``` - -Windows with docker - -```bash -git clone --config core.autocrlf=input https://github.com/freqtrade/freqtrade.git -``` - -#### Copy `config.json.example` to `config.json` - -```bash -cd freqtrade -cp -n config.json.example config.json -``` - -> To understand the configuration options, please refer to the [Bot Configuration](configuration.md) page. - -#### Create your database file - -=== "Dry-Run" - ``` bash - touch tradesv3.dryrun.sqlite - ``` - -=== "Production" - ``` bash - touch tradesv3.sqlite - ``` - - -!!! Warning "Database File Path" - Make sure to use the path to the correct database file when starting the bot in Docker. - -### Build your own Docker image - -Best start by pulling the official docker image from dockerhub as explained [here](#download-the-official-docker-image) to speed up building. - -To add additional libraries to your docker image, best check out [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) which adds the [technical](https://github.com/freqtrade/technical) module to the image. - -```bash -docker build -t freqtrade -f Dockerfile.technical . -``` - -If you are developing using Docker, use `Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies: - -```bash -docker build -f Dockerfile.develop -t freqtrade-dev . -``` - -!!! Warning "Include your config file manually" - For security reasons, your configuration file will not be included in the image, you will need to bind mount it. It is also advised to bind mount an SQLite database file (see [5. Run a restartable docker image](#run-a-restartable-docker-image)") to keep it between updates. - -#### Verify the Docker image - -After the build process you can verify that the image was created with: - -```bash -docker images -``` - -The output should contain the freqtrade image. - -### Run the Docker image - -You can run a one-off container that is immediately deleted upon exiting with the following command (`config.json` must be in the current working directory): - -```bash -docker run --rm -v `pwd`/config.json:/freqtrade/config.json -it freqtrade -``` - -!!! Warning - In this example, the database will be created inside the docker instance and will be lost when you refresh your image. - -#### Adjust timezone - -By default, the container will use UTC timezone. -If you would like to change the timezone use the following commands: - -=== "Linux" - ``` bash - -v /etc/timezone:/etc/timezone:ro - - # Complete command: - docker run --rm -v /etc/timezone:/etc/timezone:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade - ``` - -=== "MacOS" - ```bash - docker run --rm -e TZ=`ls -la /etc/localtime | cut -d/ -f8-9` -v `pwd`/config.json:/freqtrade/config.json -it freqtrade - ``` - -!!! Note "MacOS Issues" - The OSX Docker versions after 17.09.1 have a known issue whereby `/etc/localtime` cannot be shared causing Docker to not start.
    - A work-around for this is to start with the MacOS command above - More information on this docker issue and work-around can be read [here](https://github.com/docker/for-mac/issues/2396). - -### Run a restartable docker image - -To run a restartable instance in the background (feel free to place your configuration and database files wherever it feels comfortable on your filesystem). - -#### 1. Move your config file and database - -The following will assume that you place your configuration / database files to `~/.freqtrade`, which is a hidden directory in your home directory. Feel free to use a different directory and replace the directory in the upcomming commands. - -```bash -mkdir ~/.freqtrade -mv config.json ~/.freqtrade -mv tradesv3.sqlite ~/.freqtrade -``` - -#### 2. Run the docker image - -```bash -docker run -d \ - --name freqtrade \ - -v ~/.freqtrade/config.json:/freqtrade/config.json \ - -v ~/.freqtrade/user_data/:/freqtrade/user_data \ - -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ - freqtrade trade --db-url sqlite:///tradesv3.sqlite --strategy MyAwesomeStrategy -``` - -!!! Note - When using docker, it's best to specify `--db-url` explicitly to ensure that the database URL and the mounted database file match. - -!!! Note - All available bot command line parameters can be added to the end of the `docker run` command. - -!!! Note - You can define a [restart policy](https://docs.docker.com/config/containers/start-containers-automatically/) in docker. It can be useful in some cases to use the `--restart unless-stopped` flag (crash of freqtrade or reboot of your system). - -### Monitor your Docker instance - -You can use the following commands to monitor and manage your container: - -```bash -docker logs freqtrade -docker logs -f freqtrade -docker restart freqtrade -docker stop freqtrade -docker start freqtrade -``` - -For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/). - -!!! Note - You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container. - -### Backtest with docker - -The following assumes that the download/setup of the docker image have been completed successfully. -Also, backtest-data should be available at `~/.freqtrade/user_data/`. - -```bash -docker run -d \ - --name freqtrade \ - -v /etc/localtime:/etc/localtime:ro \ - -v ~/.freqtrade/config.json:/freqtrade/config.json \ - -v ~/.freqtrade/tradesv3.sqlite:/freqtrade/tradesv3.sqlite \ - -v ~/.freqtrade/user_data/:/freqtrade/user_data/ \ - freqtrade backtesting --strategy AwsomelyProfitableStrategy -``` - -Head over to the [Backtesting Documentation](backtesting.md) for more details. - -!!! Note - Additional bot command line parameters can be appended after the image name (`freqtrade` in the above example). diff --git a/mkdocs.yml b/mkdocs.yml index 5d936687f..26494ae45 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,9 +1,9 @@ site_name: Freqtrade nav: - Home: index.md - - Quickstart with Docker: docker.md - - Installation: - - Freqtrade without docker-compose: without_docker_compose.md + - Quickstart with Docker: docker_quickstart.md + - Installation: + - Docker without docker-compose: docker.md - Linux/MacOS/Raspberry: installation.md - Windows: windows_installation.md - Freqtrade Basics: bot-basics.md From bc5cc48f67ddb494ad320e93817e7f8f7e44606b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 4 Sep 2020 07:28:21 +0200 Subject: [PATCH 0574/1197] Adjust windows docs, fix failing doc-test --- docs/windows_installation.md | 28 ++++++++++++++++++---------- tests/test_docs.sh | 3 +-- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/docs/windows_installation.md b/docs/windows_installation.md index 1cdb3d613..f7900d85a 100644 --- a/docs/windows_installation.md +++ b/docs/windows_installation.md @@ -9,7 +9,7 @@ Otherwise, try the instructions below. Make sure to use 64bit Windows and 64bit Python to avoid problems with backtesting or hyperopt due to the memory constraints 32bit applications have under Windows. !!! Hint - Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Conda section](#using-conda) in this document for more information. + Using the [Anaconda Distribution](https://www.anaconda.com/distribution/) under Windows can greatly help with installation problems. Check out the [Anaconda installation section](installation.md#Anaconda) in this document for more information. ### 1. Clone the git repository @@ -23,17 +23,25 @@ Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7 As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial precompiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_Lib‑0.4.18‑cp38‑cp38‑win_amd64.whl` (make sure to use the version matching your python version) -```cmd ->cd \path\freqtrade-develop ->python -m venv .env ->.env\Scripts\activate.bat -REM optionally install ta-lib from wheel -REM >pip install TA_Lib‑0.4.18‑cp38‑cp38‑win_amd64.whl ->pip install -r requirements.txt ->pip install -e . ->freqtrade +Freqtrade provides these dependencies for the latest 2 Python versions (3.7 and 3.8) and for 64bit Windows. +Other versions must be downloaded from the above link. + +``` powershell +cd \path\freqtrade +python -m venv .env +.env\Scripts\activate.ps1 +# optionally install ta-lib from wheel +# Eventually adjust the below filename to match the downloaded wheel +pip install build_helpes/TA_Lib‑0.4.18‑cp38‑cp38‑win_amd64.whl +pip install -r requirements.txt +pip install -e . +freqtrade ``` +!!! Note "Use Powershell" + The above installation script assumes you're using powershell on a 64bit windows. + Commands for the legacy CMD windows console may differ. + > Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222) ### Error during installation on Windows diff --git a/tests/test_docs.sh b/tests/test_docs.sh index 8a354daad..09e142b99 100755 --- a/tests/test_docs.sh +++ b/tests/test_docs.sh @@ -2,8 +2,7 @@ # Test Documentation boxes - # !!! : is not allowed! # !!! "title" - Title needs to be quoted! -# !!! Spaces at the beginning are not allowed -grep -Er '^!{3}\s\S+:|^!{3}\s\S+\s[^"]|^\s+!{3}\s\S+' docs/* +grep -Er '^!{3}\s\S+:|^!{3}\s\S+\s[^"]' docs/* if [ $? -ne 0 ]; then echo "Docs test success." From bd4f3d838ab959c3c8ff9f0e39daaeb62c3bddce Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 4 Sep 2020 19:44:35 +0200 Subject: [PATCH 0575/1197] Implement merge_informative_pairs helper --- freqtrade/strategy/__init__.py | 1 + freqtrade/strategy/strategy_helper.py | 39 ++++++++++++++++ tests/strategy/test_strategy_helpers.py | 61 +++++++++++++++++++++++++ 3 files changed, 101 insertions(+) create mode 100644 freqtrade/strategy/strategy_helper.py create mode 100644 tests/strategy/test_strategy_helpers.py diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 91ea0e075..5758bbbcc 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -2,3 +2,4 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_prev_date, timeframe_to_seconds, timeframe_to_next_date, timeframe_to_msecs) from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy.strategy_helper import merge_informative_pairs diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py new file mode 100644 index 000000000..ce98cccba --- /dev/null +++ b/freqtrade/strategy/strategy_helper.py @@ -0,0 +1,39 @@ +import pandas as pd +from freqtrade.exchange import timeframe_to_minutes + + +def merge_informative_pairs(dataframe: pd.DataFrame, informative: pd.DataFrame, + timeframe_inf: str, ffill: bool = True) -> pd.DataFrame: + """ + Correctly merge informative samples to the original dataframe, avoiding lookahead bias. + + Since dates are candle open dates, merging a 15m candle that starts at 15:00, and a + 1h candle that starts at 15:00 will result in all candles to know the close at 16:00 + which they should not know. + + Moves the date of the informative pair by 1 time interval forward. + This way, the 14:00 1h candle is merged to 15:00 15m candle, since the 14:00 1h candle is the + last candle that's closed at 15:00, 15:15, 15:30 or 15:45. + + :param dataframe: Original dataframe + :param informative: Informative pair, most likely loaded via dp.get_pair_dataframe + :param timeframe_inf: Timeframe of the informative pair sample. + :param ffill: Forwardfill missing values - optional but usually required + """ + # Rename columns to be unique + + minutes = timeframe_to_minutes(timeframe_inf) + informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes, 'm') + + informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns] + + # Combine the 2 dataframes + # all indicators on the informative sample MUST be calculated before this point + dataframe = pd.merge(dataframe, informative, left_on='date', + right_on=f'date_merge_{timeframe_inf}', how='left') + dataframe = dataframe.drop(f'date_merge_{timeframe_inf}', axis=1) + + if ffill: + dataframe = dataframe.ffill() + + return dataframe diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py new file mode 100644 index 000000000..89bbba2c1 --- /dev/null +++ b/tests/strategy/test_strategy_helpers.py @@ -0,0 +1,61 @@ +import pandas as pd +import numpy as np + +from freqtrade.strategy import merge_informative_pairs, timeframe_to_minutes + + +def generate_test_data(timeframe: str, size: int): + np.random.seed(42) + tf_mins = timeframe_to_minutes(timeframe) + + base = np.random.normal(20, 2, size=size) + + date = pd.period_range('2020-07-05', periods=size, freq=f'{tf_mins}min').to_timestamp() + df = pd.DataFrame({ + 'date': date, + 'open': base, + 'high': base + np.random.normal(2, 1, size=size), + 'low': base - np.random.normal(2, 1, size=size), + 'close': base + np.random.normal(0, 1, size=size), + 'volume': np.random.normal(200, size=size) + } + ) + df = df.dropna() + return df + + +def test_merge_informative_pairs(): + data = generate_test_data('15m', 40) + informative = generate_test_data('1h', 40) + + result = merge_informative_pairs(data, informative, '1h', ffill=True) + assert isinstance(result, pd.DataFrame) + assert len(result) == len(data) + assert 'date' in result.columns + assert result['date'].equals(data['date']) + assert 'date_1h' in result.columns + + assert 'open' in result.columns + assert 'open_1h' in result.columns + assert result['open'].equals(data['open']) + + assert 'close' in result.columns + assert 'close_1h' in result.columns + assert result['close'].equals(data['close']) + + assert 'volume' in result.columns + assert 'volume_1h' in result.columns + assert result['volume'].equals(data['volume']) + + # First 4 rows are empty + assert result.iloc[0]['date_1h'] is pd.NaT + assert result.iloc[1]['date_1h'] is pd.NaT + assert result.iloc[2]['date_1h'] is pd.NaT + assert result.iloc[3]['date_1h'] is pd.NaT + # Next 4 rows contain the starting date (0:00) + assert result.iloc[4]['date_1h'] == result.iloc[0]['date'] + assert result.iloc[5]['date_1h'] == result.iloc[0]['date'] + assert result.iloc[6]['date_1h'] == result.iloc[0]['date'] + assert result.iloc[7]['date_1h'] == result.iloc[0]['date'] + # Next 4 rows contain the next Hourly date original date row 4 + assert result.iloc[8]['date_1h'] == result.iloc[4]['date'] From 7bc89279148872e43d4f6f6233ffbc864bc1f436 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 4 Sep 2020 20:02:31 +0200 Subject: [PATCH 0576/1197] Add documentation for merge_informative_pair helper --- docs/strategy-customization.md | 79 +++++++++++++++++++------ freqtrade/strategy/__init__.py | 2 +- freqtrade/strategy/strategy_helper.py | 7 ++- tests/strategy/test_strategy_helpers.py | 6 +- 4 files changed, 71 insertions(+), 23 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index e2548e510..c791be615 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -483,9 +483,8 @@ if self.dp: ### Complete Data-provider sample ```python -from freqtrade.strategy import IStrategy, timeframe_to_minutes +from freqtrade.strategy import IStrategy, merge_informative_pairs from pandas import DataFrame -import pandas as pd class SampleStrategy(IStrategy): # strategy init stuff... @@ -517,23 +516,12 @@ class SampleStrategy(IStrategy): # Get the 14 day rsi informative['rsi'] = ta.RSI(informative, timeperiod=14) - # Rename columns to be unique - informative.columns = [f"{col}_{inf_tf}" for col in informative.columns] - # Assuming inf_tf = '1d' - then the columns will now be: - # date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d - - # Shift date by 1 candle - # This is necessary since the data is always the "open date" - # and a 15m candle starting at 12:15 should not know the close of the 1h candle from 12:00 to 13:00 - minutes = timeframe_to_minutes(inf_tf) - informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes, 'm') - - # Combine the 2 dataframes - # all indicators on the informative sample MUST be calculated before this point - dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_merge_{inf_tf}', how='left') + # Use the helper function merge_informative_pair to safely merge the pair + # Automatically renames the columns and merges a shorter timeframe dataframe and a longer timeframe informative pair # FFill to have the 1d value available in every row throughout the day. # Without this, comparisons would only work once per day. - dataframe = dataframe.ffill() + # Full documentation of this method, see below + dataframe = merge_informative_pair(dataframe, informative_pairs, inf_tf, ffill=True) # Calculate rsi of the original dataframe (5m timeframe) dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) @@ -557,6 +545,63 @@ class SampleStrategy(IStrategy): *** +## Helper functions + +### *merge_informative_pair()* + +This method helps you merge an informative pair to a regular dataframe without lookahead bias. +It's there to help you merge the dataframe in a safe and consistent way. + +Options: + +- Rename the columns for you to create unique columns +- Merge the dataframe without lookahead bias +- Forward-fill (optional) + +All columns of the informative dataframe will be available on the returning dataframe in a renamed fashion: + +!!! Example "Column renaming" + Assuming `inf_tf = '1d'` the resulting columns will be: + + ``` python + 'date', 'open', 'high', 'low', 'close', 'rsi' # from the original dataframe + 'date_1d', 'open_1d', 'high_1d', 'low_1d', 'close_1d', 'rsi_1d' # from the informative dataframe + ``` + +??? Example "Column renaming - 1h" + Assuming `inf_tf = '1h'` the resulting columns will be: + + ``` python + 'date', 'open', 'high', 'low', 'close', 'rsi' # from the original dataframe + 'date_1h', 'open_1h', 'high_1h', 'low_1h', 'close_1h', 'rsi_1h' # from the informative dataframe + ``` + +??? Example "Custom implementation" + A custom implementation for this is possible, and can be done as follows: + + ``` python + # Rename columns to be unique + informative.columns = [f"{col}_{inf_tf}" for col in informative.columns] + # Assuming inf_tf = '1d' - then the columns will now be: + # date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d + + # Shift date by 1 candle + # This is necessary since the data is always the "open date" + # and a 15m candle starting at 12:15 should not know the close of the 1h candle from 12:00 to 13:00 + minutes = timeframe_to_minutes(inf_tf) + informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes, 'm') + + # Combine the 2 dataframes + # all indicators on the informative sample MUST be calculated before this point + dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_merge_{inf_tf}', how='left') + # FFill to have the 1d value available in every row throughout the day. + # Without this, comparisons would only work once per day. + dataframe = dataframe.ffill() + + ``` + +*** + ## Additional data (Wallets) The strategy provides access to the `Wallets` object. This contains the current balances on the exchange. diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 5758bbbcc..d1510489e 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -2,4 +2,4 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_prev_date, timeframe_to_seconds, timeframe_to_next_date, timeframe_to_msecs) from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.strategy_helper import merge_informative_pairs +from freqtrade.strategy.strategy_helper import merge_informative_pair diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index ce98cccba..2684e7b03 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -2,8 +2,8 @@ import pandas as pd from freqtrade.exchange import timeframe_to_minutes -def merge_informative_pairs(dataframe: pd.DataFrame, informative: pd.DataFrame, - timeframe_inf: str, ffill: bool = True) -> pd.DataFrame: +def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, + timeframe_inf: str, ffill: bool = True) -> pd.DataFrame: """ Correctly merge informative samples to the original dataframe, avoiding lookahead bias. @@ -15,6 +15,9 @@ def merge_informative_pairs(dataframe: pd.DataFrame, informative: pd.DataFrame, This way, the 14:00 1h candle is merged to 15:00 15m candle, since the 14:00 1h candle is the last candle that's closed at 15:00, 15:15, 15:30 or 15:45. + Assuming inf_tf = '1d' - then the resulting columns will be: + date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d + :param dataframe: Original dataframe :param informative: Informative pair, most likely loaded via dp.get_pair_dataframe :param timeframe_inf: Timeframe of the informative pair sample. diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 89bbba2c1..9201d91e1 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -1,7 +1,7 @@ import pandas as pd import numpy as np -from freqtrade.strategy import merge_informative_pairs, timeframe_to_minutes +from freqtrade.strategy import merge_informative_pair, timeframe_to_minutes def generate_test_data(timeframe: str, size: int): @@ -24,11 +24,11 @@ def generate_test_data(timeframe: str, size: int): return df -def test_merge_informative_pairs(): +def test_merge_informative_pair(): data = generate_test_data('15m', 40) informative = generate_test_data('1h', 40) - result = merge_informative_pairs(data, informative, '1h', ffill=True) + result = merge_informative_pair(data, informative, '1h', ffill=True) assert isinstance(result, pd.DataFrame) assert len(result) == len(data) assert 'date' in result.columns From cc684c51415afd7e1b682242ecd8c76f594e8c1d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 4 Sep 2020 20:09:02 +0200 Subject: [PATCH 0577/1197] Correctly handle identical timerame merges --- docs/strategy-customization.md | 7 +++--- freqtrade/strategy/strategy_helper.py | 11 +++++++--- tests/strategy/test_strategy_helpers.py | 29 ++++++++++++++++++++++++- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index c791be615..7396f2a89 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -518,10 +518,10 @@ class SampleStrategy(IStrategy): # Use the helper function merge_informative_pair to safely merge the pair # Automatically renames the columns and merges a shorter timeframe dataframe and a longer timeframe informative pair - # FFill to have the 1d value available in every row throughout the day. - # Without this, comparisons would only work once per day. + # use ffill to have the 1d value available in every row throughout the day. + # Without this, comparisons between columns of the original and the informative pair would only work once per day. # Full documentation of this method, see below - dataframe = merge_informative_pair(dataframe, informative_pairs, inf_tf, ffill=True) + dataframe = merge_informative_pair(dataframe, informative_pairs, self.timeframe, inf_tf, ffill=True) # Calculate rsi of the original dataframe (5m timeframe) dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) @@ -589,6 +589,7 @@ All columns of the informative dataframe will be available on the returning data # This is necessary since the data is always the "open date" # and a 15m candle starting at 12:15 should not know the close of the 1h candle from 12:00 to 13:00 minutes = timeframe_to_minutes(inf_tf) + # Only do this if the timeframes are different: informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes, 'm') # Combine the 2 dataframes diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 2684e7b03..0fa7f4258 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -3,7 +3,7 @@ from freqtrade.exchange import timeframe_to_minutes def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, - timeframe_inf: str, ffill: bool = True) -> pd.DataFrame: + timeframe: str, timeframe_inf: str, ffill: bool = True) -> pd.DataFrame: """ Correctly merge informative samples to the original dataframe, avoiding lookahead bias. @@ -20,13 +20,18 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, :param dataframe: Original dataframe :param informative: Informative pair, most likely loaded via dp.get_pair_dataframe + :param timeframe: Timeframe of the original pair sample. :param timeframe_inf: Timeframe of the informative pair sample. :param ffill: Forwardfill missing values - optional but usually required """ # Rename columns to be unique - minutes = timeframe_to_minutes(timeframe_inf) - informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes, 'm') + minutes_inf = timeframe_to_minutes(timeframe_inf) + if timeframe == timeframe_inf: + # No need to forwardshift if the timeframes are identical + informative['date_merge'] = informative["date"] + else: + informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes_inf, 'm') informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns] diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 9201d91e1..4b29bf304 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -28,7 +28,7 @@ def test_merge_informative_pair(): data = generate_test_data('15m', 40) informative = generate_test_data('1h', 40) - result = merge_informative_pair(data, informative, '1h', ffill=True) + result = merge_informative_pair(data, informative, '15m', '1h', ffill=True) assert isinstance(result, pd.DataFrame) assert len(result) == len(data) assert 'date' in result.columns @@ -59,3 +59,30 @@ def test_merge_informative_pair(): assert result.iloc[7]['date_1h'] == result.iloc[0]['date'] # Next 4 rows contain the next Hourly date original date row 4 assert result.iloc[8]['date_1h'] == result.iloc[4]['date'] + + +def test_merge_informative_pair_same(): + data = generate_test_data('15m', 40) + informative = generate_test_data('15m', 40) + + result = merge_informative_pair(data, informative, '15m', '15m', ffill=True) + assert isinstance(result, pd.DataFrame) + assert len(result) == len(data) + assert 'date' in result.columns + assert result['date'].equals(data['date']) + assert 'date_15m' in result.columns + + assert 'open' in result.columns + assert 'open_15m' in result.columns + assert result['open'].equals(data['open']) + + assert 'close' in result.columns + assert 'close_15m' in result.columns + assert result['close'].equals(data['close']) + + assert 'volume' in result.columns + assert 'volume_15m' in result.columns + assert result['volume'].equals(data['volume']) + + # Dates match 1:1 + assert result['date_15m'].equals(result['date']) From 71af64af94ba07a19181618e58f0dbfa8480709f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 4 Sep 2020 20:10:43 +0200 Subject: [PATCH 0578/1197] Move comment to the right place --- freqtrade/strategy/strategy_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 0fa7f4258..1fbf618bd 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -24,7 +24,6 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, :param timeframe_inf: Timeframe of the informative pair sample. :param ffill: Forwardfill missing values - optional but usually required """ - # Rename columns to be unique minutes_inf = timeframe_to_minutes(timeframe_inf) if timeframe == timeframe_inf: @@ -33,6 +32,7 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, else: informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes_inf, 'm') + # Rename columns to be unique informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns] # Combine the 2 dataframes From c18441f36fe5fa25012f0b363153d41df93c4955 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Sep 2020 16:44:23 +0200 Subject: [PATCH 0579/1197] Fix typo in reloading_conf --- freqtrade/rpc/rpc.py | 2 +- tests/rpc/test_rpc_apiserver.py | 2 +- tests/rpc/test_rpc_telegram.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b89a95ee8..0b9196f2e 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -436,7 +436,7 @@ class RPC: def _rpc_reload_config(self) -> Dict[str, str]: """ Handler for reload_config. """ self._freqtrade.state = State.RELOAD_CONFIG - return {'status': 'reloading config ...'} + return {'status': 'Reloading config ...'} def _rpc_stopbuy(self) -> Dict[str, str]: """ diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index d2b69ee4f..d9f5bf781 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -266,7 +266,7 @@ def test_api_reloadconf(botclient): rc = client_post(client, f"{BASE_URI}/reload_config") assert_response(rc) - assert rc.json == {'status': 'reloading config ...'} + assert rc.json == {'status': 'Reloading config ...'} assert ftbot.state == State.RELOAD_CONFIG diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 71898db8c..762780111 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -692,7 +692,7 @@ def test_reload_config_handle(default_conf, update, mocker) -> None: telegram._reload_config(update=update, context=MagicMock()) assert freqtradebot.state == State.RELOAD_CONFIG assert msg_mock.call_count == 1 - assert 'reloading config' in msg_mock.call_args_list[0][0][0] + assert 'Reloading config' in msg_mock.call_args_list[0][0][0] def test_telegram_forcesell_handle(default_conf, update, ticker, fee, From 8c9297e1f0effeccd5490652f83b91a0a3105507 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Sep 2020 16:51:19 +0200 Subject: [PATCH 0580/1197] Don't crash if a strategy imports something wrongly --- freqtrade/resolvers/iresolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 52d944f2c..b7d25ef2c 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -59,7 +59,7 @@ class IResolver: module = importlib.util.module_from_spec(spec) try: spec.loader.exec_module(module) # type: ignore # importlib does not use typehints - except (ModuleNotFoundError, SyntaxError) as err: + except (ModuleNotFoundError, SyntaxError, ImportError) as err: # Catch errors in case a specific module is not installed logger.warning(f"Could not import {module_path} due to '{err}'") if enum_failed: From b4c35291358fa33613b1eeae72f3a6bbdabef15d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Sep 2020 14:05:15 +0200 Subject: [PATCH 0581/1197] Add orders to mock_trades fixture --- tests/conftest.py | 67 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 08061f647..591fd47bb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ # pragma pylint: disable=missing-docstring +from freqtrade.persistence.models import Order import json import logging import re @@ -184,6 +185,17 @@ def create_mock_trades(fee): open_order_id='dry_run_buy_12345', strategy='DefaultStrategy', ) + o = Order.parse_from_ccxt_object({ + 'id': '1234', + 'symbol': 'ETH/BTC', + 'status': 'closed', + 'side': 'buy', + 'price': 0.123, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'buy') + trade.orders.append(o) Trade.session.add(trade) trade = Trade( @@ -201,6 +213,28 @@ def create_mock_trades(fee): open_order_id='dry_run_sell_12345', strategy='DefaultStrategy', ) + o = Order.parse_from_ccxt_object({ + 'id': '1235', + 'symbol': 'ETC/BTC', + 'status': 'closed', + 'side': 'buy', + 'price': 0.123, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'buy') + trade.orders.append(o) + o = Order.parse_from_ccxt_object({ + 'id': '12366', + 'symbol': 'ETC/BTC', + 'status': 'closed', + 'side': 'sell', + 'price': 0.128, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'sell') + trade.orders.append(o) Trade.session.add(trade) trade = Trade( @@ -215,6 +249,28 @@ def create_mock_trades(fee): exchange='bittrex', is_open=False, ) + o = Order.parse_from_ccxt_object({ + 'id': '41231a12a', + 'symbol': 'XRP/BTC', + 'status': 'closed', + 'side': 'buy', + 'price': 0.05, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'buy') + trade.orders.append(o) + o = Order.parse_from_ccxt_object({ + 'id': '41231a666a', + 'symbol': 'XRP/BTC', + 'status': 'closed', + 'side': 'stop_loss', + 'price': 0.06, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'sell') + trade.orders.append(o) Trade.session.add(trade) # Simulate prod entry @@ -230,6 +286,17 @@ def create_mock_trades(fee): open_order_id='prod_buy_12345', strategy='DefaultStrategy', ) + o = Order.parse_from_ccxt_object({ + 'id': 'prod_buy_12345', + 'symbol': 'ETC/BTC', + 'status': 'open', + 'side': 'buy', + 'price': 0.123, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'buy') + trade.orders.append(o) Trade.session.add(trade) From b7662722bacb8635c4733abe450214c8d5297bb5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Sep 2020 14:17:45 +0200 Subject: [PATCH 0582/1197] Add tests for Order object parsing --- freqtrade/persistence/models.py | 1 + tests/test_persistence.py | 47 +++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index e0b9624dd..891822064 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -154,6 +154,7 @@ class Order(_DECL_BASE): if 'timestamp' in order and order['timestamp'] is not None: self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc) + self.ft_is_open = True if self.status in ('closed', 'canceled', 'cancelled'): self.ft_is_open = False if order.get('filled', 0) > 0: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index c812c496f..b6623d461 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1030,3 +1030,50 @@ def test_get_best_pair(fee): assert len(res) == 2 assert res[0] == 'XRP/BTC' assert res[1] == 0.01 + + +@pytest.mark.usefixtures("init_persistence") +def test_update_order_from_ccxt(): + # Most basic order return (only has orderid) + o = Order.parse_from_ccxt_object({'id': '1234'}, 'ETH/BTC', 'buy') + assert isinstance(o, Order) + assert o.ft_pair == 'ETH/BTC' + assert o.ft_order_side == 'buy' + assert o.order_id == '1234' + assert o.ft_is_open + ccxt_order = { + 'id': '1234', + 'side': 'buy', + 'symbol': 'ETH/BTC', + 'type': 'limit', + 'price': 1234.5, + 'amount': 20.0, + 'filled': 9, + 'remaining': 11, + 'status': 'open', + 'timestamp': 1599394315123 + } + o = Order.parse_from_ccxt_object(ccxt_order, 'ETH/BTC', 'buy') + assert isinstance(o, Order) + assert o.ft_pair == 'ETH/BTC' + assert o.ft_order_side == 'buy' + assert o.order_id == '1234' + assert o.order_type == 'limit' + assert o.price == 1234.5 + assert o.filled == 9 + assert o.remaining == 11 + assert o.order_date is not None + assert o.ft_is_open + assert o.order_filled_date is None + + # Order has been closed + ccxt_order.update({'filled': 20.0, 'remaining': 0.0, 'status': 'closed'}) + o.update_from_ccxt_object(ccxt_order) + + assert o.filled == 20.0 + assert o.remaining == 0.0 + assert not o.ft_is_open + assert o.order_filled_date is not None + + + From a78d61150c5ab7346fe9a3557de0cc3b282d2e67 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Sep 2020 14:27:36 +0200 Subject: [PATCH 0583/1197] Deleting must delete orders first --- freqtrade/freqtradebot.py | 3 +-- freqtrade/persistence/models.py | 8 ++++++++ freqtrade/rpc/rpc.py | 3 +-- tests/rpc/test_rpc.py | 1 - tests/test_persistence.py | 3 --- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 3e9f8c75c..f2cd4c5dd 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1118,8 +1118,7 @@ class FreqtradeBot: if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC): logger.info('Buy order fully cancelled. Removing %s from database.', trade) # if trade is not partially completed, just delete the trade - Trade.session.delete(trade) - Trade.session.flush() + trade.delete() was_trade_fully_canceled = True else: # if trade is partially complete, edit the stake details for the trade diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 891822064..9759ac830 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -446,6 +446,14 @@ class Trade(_DECL_BASE): def update_order(self, order: Dict) -> None: Order.update_orders(self.orders, order) + def delete(self) -> None: + + for order in self.orders: + Order.session.delete(order) + + Trade.session.delete(self) + Trade.session.flush() + def _calc_open_trade_price(self) -> float: """ Calculate the open_rate including open_fee. diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 9b5d79267..6ace0bb88 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -567,8 +567,7 @@ class RPC: except (ExchangeError): pass - Trade.session.delete(trade) - Trade.session.flush() + trade.delete() self._freqtrade.wallets.update() return { 'result': 'success', diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 42025b3a3..8c13bc00f 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -313,7 +313,6 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog): with pytest.raises(RPCException, match='invalid argument'): rpc._rpc_delete('200') - create_mock_trades(fee) trades = Trade.query.all() trades[1].stoploss_order_id = '1234' trades[2].stoploss_order_id = '1234' diff --git a/tests/test_persistence.py b/tests/test_persistence.py index b6623d461..707247e99 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1074,6 +1074,3 @@ def test_update_order_from_ccxt(): assert o.remaining == 0.0 assert not o.ft_is_open assert o.order_filled_date is not None - - - From 68d51a97878e8b31e8fe49013d3cced2136ffa51 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Sep 2020 14:33:45 +0200 Subject: [PATCH 0584/1197] Don't raise OperationalException when orderid's dont' match --- freqtrade/persistence/models.py | 4 ++-- tests/test_persistence.py | 10 +++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 9759ac830..096cf6209 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -17,7 +17,7 @@ from sqlalchemy.orm.session import sessionmaker from sqlalchemy.pool import StaticPool from sqlalchemy.sql.schema import UniqueConstraint -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.misc import safe_value_fallback from freqtrade.persistence.migrations import check_migrate @@ -140,7 +140,7 @@ class Order(_DECL_BASE): Only updates if fields are available from ccxt - """ if self.order_id != str(order['id']): - return OperationalException("Order-id's don't match") + raise DependencyException("Order-id's don't match") self.status = order.get('status', self.status) self.symbol = order.get('symbol', self.symbol) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 707247e99..788debace 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -7,9 +7,9 @@ import pytest from sqlalchemy import create_engine from freqtrade import constants -from freqtrade.exceptions import OperationalException -from freqtrade.persistence import Trade, Order, clean_dry_run_db, init -from tests.conftest import log_has, create_mock_trades +from freqtrade.exceptions import DependencyException, OperationalException +from freqtrade.persistence import Order, Trade, clean_dry_run_db, init +from tests.conftest import create_mock_trades, log_has def test_init_create_session(default_conf): @@ -1074,3 +1074,7 @@ def test_update_order_from_ccxt(): assert o.remaining == 0.0 assert not o.ft_is_open assert o.order_filled_date is not None + + ccxt_order.update({'id': 'somethingelse'}) + with pytest.raises(DependencyException, match=r"Order-id's don't match"): + o.update_from_ccxt_object(ccxt_order) From cec98ad407b2814b7266ae9d21a2ee4b1da066ab Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Sep 2020 14:51:11 +0200 Subject: [PATCH 0585/1197] Test stoploss insufficient funds handling --- tests/test_freqtradebot.py | 40 +++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index f89befc55..850ddaeeb 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -13,7 +13,7 @@ import pytest from freqtrade.constants import (CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT) -from freqtrade.exceptions import (DependencyException, ExchangeError, +from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, TemporaryError) from freqtrade.freqtradebot import FreqtradeBot @@ -1327,6 +1327,44 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, assert rpc_mock.call_args_list[1][0][0]['order_type'] == 'market' +def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog, fee, + limit_buy_order_open, limit_sell_order): + sell_mock = MagicMock(return_value={'id': limit_sell_order['id']}) + freqtrade = get_patched_freqtradebot(mocker, default_conf) + + mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds') + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=MagicMock(return_value={ + 'bid': 0.00001172, + 'ask': 0.00001173, + 'last': 0.00001172 + }), + buy=MagicMock(return_value=limit_buy_order_open), + sell=sell_mock, + get_fee=fee, + fetch_order=MagicMock(return_value={'status': 'canceled'}), + stoploss=MagicMock(side_effect=InsufficientFundsError()), + ) + patch_get_signal(freqtrade) + freqtrade.strategy.order_types['stoploss_on_exchange'] = True + + freqtrade.enter_positions() + trade = Trade.query.first() + caplog.clear() + freqtrade.create_stoploss_order(trade, 200) + # stoploss_orderid was empty before + assert trade.stoploss_order_id is None + assert mock_insuf.call_count == 1 + mock_insuf.reset_mock() + + trade.stoploss_order_id = 'stoploss_orderid' + freqtrade.create_stoploss_order(trade, 200) + # No change to stoploss-orderid + assert trade.stoploss_order_id == 'stoploss_orderid' + assert mock_insuf.call_count == 1 + + @pytest.mark.usefixtures("init_persistence") def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, limit_buy_order, limit_sell_order) -> None: From 7c1f111ddfd50eeb4cc770aaa8564497cf12ea64 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Sep 2020 14:59:30 +0200 Subject: [PATCH 0586/1197] Add insufficient_funds_test --- tests/test_freqtradebot.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 850ddaeeb..2f8e5aef6 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2981,6 +2981,35 @@ def test_execute_sell_market_order(default_conf, ticker, fee, } == last_msg +def test_execute_sell_insufficient_funds_error(default_conf, ticker, fee, + ticker_sell_up, mocker) -> None: + freqtrade = get_patched_freqtradebot(mocker, default_conf) + mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds') + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker, + get_fee=fee, + sell=MagicMock(side_effect=InsufficientFundsError()) + ) + patch_get_signal(freqtrade) + + # Create some test data + freqtrade.enter_positions() + + trade = Trade.query.first() + assert trade + + # Increase the price and sell it + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker_sell_up + ) + + assert not freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], + sell_reason=SellType.ROI) + assert mock_insuf.call_count == 1 + + def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy_order_open, fee, mocker) -> None: patch_RPCManager(mocker) From b4da36d6e9a94bd81b9da90a5e0350b931793f4c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Sep 2020 15:05:47 +0200 Subject: [PATCH 0587/1197] Fix small typo and add small testcase --- freqtrade/freqtradebot.py | 2 +- tests/test_freqtradebot.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f2cd4c5dd..44d1c31eb 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1376,7 +1376,7 @@ class FreqtradeBot: """ if not order_id: logger.warning(f'Orderid for trade {trade} is empty.') - False + return False # Update trade with order values logger.info('Found open order for %s', trade) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 2f8e5aef6..fe5b64d5b 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1737,6 +1737,8 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No open_date=arrow.utcnow().datetime, amount=11, ) + assert not freqtrade.update_trade_state(trade, None) + assert log_has_re(r'Orderid for trade .* is empty.', caplog) # Add datetime explicitly since sqlalchemy defaults apply only once written to database freqtrade.update_trade_state(trade, '123') # Test amount not modified by fee-logic From a0fd7f46445e32c7f92d339fd398de3a2151c528 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Sep 2020 15:27:16 +0200 Subject: [PATCH 0588/1197] Update tests to merged version --- tests/exchange/test_exchange.py | 2 +- tests/rpc/test_rpc.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 64de1f171..e0b97d157 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1765,7 +1765,7 @@ def test_cancel_order_dry_run(default_conf, mocker, exchange_name): cancel_order = exchange.cancel_order(order_id=order['id'], pair='ETH/BTC') assert order['id'] == cancel_order['id'] assert order['amount'] == cancel_order['amount'] - assert order['pair'] == cancel_order['pair'] + assert order['symbol'] == cancel_order['symbol'] assert cancel_order['status'] == 'canceled' diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 5f11532b0..c2dee6439 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -716,11 +716,13 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: mocker.patch( 'freqtrade.exchange.Exchange.fetch_order', side_effect=[{ + 'id': '1234', 'status': 'open', 'type': 'limit', 'side': 'buy', 'filled': filled_amount }, { + 'id': '1234', 'status': 'closed', 'type': 'limit', 'side': 'buy', From f6ebe51314875b42596b4e21ea4e9acf6761822f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Sep 2020 19:32:00 +0200 Subject: [PATCH 0589/1197] Add test for update_open_orders --- freqtrade/freqtradebot.py | 4 ++-- tests/test_freqtradebot.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4f68a1112..1af0e85b7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -247,8 +247,8 @@ class FreqtradeBot: self.update_trade_state(order.trade, order.order_id, fo) - except ExchangeError: - logger.warning(f"Error updating {order.order_id}") + except ExchangeError as e: + logger.warning(f"Error updating Order {order.order_id} due to {e}") def update_closed_trades_without_assigned_fees(self): """ diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c366b6777..0f340ae8f 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4216,7 +4216,7 @@ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limi @pytest.mark.usefixtures("init_persistence") -def test_check_for_open_trades(mocker, default_conf, fee, limit_buy_order, limit_sell_order): +def test_check_for_open_trades(mocker, default_conf, fee): freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade.check_for_open_trades() @@ -4229,3 +4229,30 @@ def test_check_for_open_trades(mocker, default_conf, fee, limit_buy_order, limit freqtrade.check_for_open_trades() assert freqtrade.rpc.send_msg.call_count == 1 assert 'Handle these trades manually' in freqtrade.rpc.send_msg.call_args[0][0]['status'] + + +@pytest.mark.usefixtures("init_persistence") +def test_update_open_orders(mocker, default_conf, fee, caplog): + freqtrade = get_patched_freqtradebot(mocker, default_conf) + create_mock_trades(fee) + + freqtrade.update_open_orders() + assert log_has_re(r"Error updating Order .*", caplog) + caplog.clear() + + assert len(Order.get_open_orders()) == 1 + + matching_buy_order = { + 'id': 'prod_buy_12345', + 'symbol': 'ETC/BTC', + 'status': 'closed', + 'side': 'buy', + 'type': 'limit', + 'price': 0.123, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + } + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=matching_buy_order) + freqtrade.update_open_orders() + assert len(Order.get_open_orders()) == 0 From cad0275b3282b30f50bd62d034a79add1a575f87 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Sep 2020 06:39:48 +0200 Subject: [PATCH 0590/1197] Extract mock_trade generation to sepearate file --- tests/conftest.py | 134 +------------------------------- tests/conftest_trades.py | 162 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+), 130 deletions(-) create mode 100644 tests/conftest_trades.py diff --git a/tests/conftest.py b/tests/conftest.py index 591fd47bb..2181388fa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ # pragma pylint: disable=missing-docstring +from tests.conftest_trades import create_mock_trades from freqtrade.persistence.models import Order import json import logging @@ -168,136 +169,9 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: freqtrade.exchange.refresh_latest_ohlcv = lambda p: None -def create_mock_trades(fee): - """ - Create some fake trades ... - """ - # Simulate dry_run entries - trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=123.0, - amount_requested=123.0, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_rate=0.123, - exchange='bittrex', - open_order_id='dry_run_buy_12345', - strategy='DefaultStrategy', - ) - o = Order.parse_from_ccxt_object({ - 'id': '1234', - 'symbol': 'ETH/BTC', - 'status': 'closed', - 'side': 'buy', - 'price': 0.123, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - }, 'ETH/BTC', 'buy') - trade.orders.append(o) - Trade.session.add(trade) - - trade = Trade( - pair='ETC/BTC', - stake_amount=0.001, - amount=123.0, - amount_requested=123.0, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_rate=0.123, - close_rate=0.128, - close_profit=0.005, - exchange='bittrex', - is_open=False, - open_order_id='dry_run_sell_12345', - strategy='DefaultStrategy', - ) - o = Order.parse_from_ccxt_object({ - 'id': '1235', - 'symbol': 'ETC/BTC', - 'status': 'closed', - 'side': 'buy', - 'price': 0.123, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - }, 'ETH/BTC', 'buy') - trade.orders.append(o) - o = Order.parse_from_ccxt_object({ - 'id': '12366', - 'symbol': 'ETC/BTC', - 'status': 'closed', - 'side': 'sell', - 'price': 0.128, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - }, 'ETH/BTC', 'sell') - trade.orders.append(o) - Trade.session.add(trade) - - trade = Trade( - pair='XRP/BTC', - stake_amount=0.001, - amount=123.0, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_rate=0.05, - close_rate=0.06, - close_profit=0.01, - exchange='bittrex', - is_open=False, - ) - o = Order.parse_from_ccxt_object({ - 'id': '41231a12a', - 'symbol': 'XRP/BTC', - 'status': 'closed', - 'side': 'buy', - 'price': 0.05, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - }, 'ETH/BTC', 'buy') - trade.orders.append(o) - o = Order.parse_from_ccxt_object({ - 'id': '41231a666a', - 'symbol': 'XRP/BTC', - 'status': 'closed', - 'side': 'stop_loss', - 'price': 0.06, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - }, 'ETH/BTC', 'sell') - trade.orders.append(o) - Trade.session.add(trade) - - # Simulate prod entry - trade = Trade( - pair='ETC/BTC', - stake_amount=0.001, - amount=123.0, - amount_requested=124.0, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_rate=0.123, - exchange='bittrex', - open_order_id='prod_buy_12345', - strategy='DefaultStrategy', - ) - o = Order.parse_from_ccxt_object({ - 'id': 'prod_buy_12345', - 'symbol': 'ETC/BTC', - 'status': 'open', - 'side': 'buy', - 'price': 0.123, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - }, 'ETH/BTC', 'buy') - trade.orders.append(o) - Trade.session.add(trade) +@pytest.fixture(scope='function') +def mock_trades(fee): + return create_mock_trades(fee) @pytest.fixture(autouse=True) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py new file mode 100644 index 000000000..6966eb68f --- /dev/null +++ b/tests/conftest_trades.py @@ -0,0 +1,162 @@ +import pytest + +from freqtrade.persistence.models import Order, Trade + + +def mock_trade_1(fee): + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + amount=123.0, + amount_requested=123.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.123, + exchange='bittrex', + open_order_id='dry_run_buy_12345', + strategy='DefaultStrategy', + ) + o = Order.parse_from_ccxt_object({ + 'id': '1234', + 'symbol': 'ETH/BTC', + 'status': 'closed', + 'side': 'buy', + 'price': 0.123, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'buy') + trade.orders.append(o) + return trade + + +def mock_trade_2(fee): + """ + Closed trade... + """ + trade = Trade( + pair='ETC/BTC', + stake_amount=0.001, + amount=123.0, + amount_requested=123.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.123, + close_rate=0.128, + close_profit=0.005, + exchange='bittrex', + is_open=False, + open_order_id='dry_run_sell_12345', + strategy='DefaultStrategy', + ) + o = Order.parse_from_ccxt_object({ + 'id': '1235', + 'symbol': 'ETC/BTC', + 'status': 'closed', + 'side': 'buy', + 'price': 0.123, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'buy') + trade.orders.append(o) + o = Order.parse_from_ccxt_object({ + 'id': '12366', + 'symbol': 'ETC/BTC', + 'status': 'closed', + 'side': 'sell', + 'price': 0.128, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'sell') + trade.orders.append(o) + return trade + + +def mock_trade_3(fee): + """ + Closed trade + """ + trade = Trade( + pair='XRP/BTC', + stake_amount=0.001, + amount=123.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.05, + close_rate=0.06, + close_profit=0.01, + exchange='bittrex', + is_open=False, + ) + o = Order.parse_from_ccxt_object({ + 'id': '41231a12a', + 'symbol': 'XRP/BTC', + 'status': 'closed', + 'side': 'buy', + 'price': 0.05, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'buy') + trade.orders.append(o) + o = Order.parse_from_ccxt_object({ + 'id': '41231a666a', + 'symbol': 'XRP/BTC', + 'status': 'closed', + 'side': 'stop_loss', + 'price': 0.06, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'sell') + trade.orders.append(o) + return trade + + +def mock_trade_4(fee): + """ + Simulate prod entry + """ + trade = Trade( + pair='ETC/BTC', + stake_amount=0.001, + amount=123.0, + amount_requested=124.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.123, + exchange='bittrex', + open_order_id='prod_buy_12345', + strategy='DefaultStrategy', + ) + o = Order.parse_from_ccxt_object({ + 'id': 'prod_buy_12345', + 'symbol': 'ETC/BTC', + 'status': 'open', + 'side': 'buy', + 'price': 0.123, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + }, 'ETH/BTC', 'buy') + trade.orders.append(o) + + +def create_mock_trades(fee): + """ + Create some fake trades ... + """ + # Simulate dry_run entries + trade = mock_trade_1(fee) + Trade.session.add(trade) + + trade = mock_trade_2(fee) + Trade.session.add(trade) + + trade = mock_trade_3(fee) + Trade.session.add(trade) + + trade = mock_trade_4(fee) + Trade.session.add(trade) From da0ceb7d87314242b73edc400e4ea30d2e894874 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Sep 2020 06:48:34 +0200 Subject: [PATCH 0591/1197] Extract orders for mock trades --- tests/conftest.py | 2 +- tests/conftest_trades.py | 143 ++++++++++++++++++++++++--------------- 2 files changed, 88 insertions(+), 57 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2181388fa..b966cde5b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -170,7 +170,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: @pytest.fixture(scope='function') -def mock_trades(fee): +def mock_trades(init_persistence, fee): return create_mock_trades(fee) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 6966eb68f..b2aff2ee7 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -3,6 +3,20 @@ import pytest from freqtrade.persistence.models import Order, Trade +def mock_order_1(): + return { + 'id': '1234', + 'symbol': 'ETH/BTC', + 'status': 'closed', + 'side': 'buy', + 'type': 'limit', + 'price': 0.123, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + } + + def mock_trade_1(fee): trade = Trade( pair='ETH/BTC', @@ -16,18 +30,37 @@ def mock_trade_1(fee): open_order_id='dry_run_buy_12345', strategy='DefaultStrategy', ) - o = Order.parse_from_ccxt_object({ - 'id': '1234', - 'symbol': 'ETH/BTC', + o = Order.parse_from_ccxt_object(mock_order_1(), 'ETH/BTC', 'buy') + trade.orders.append(o) + return trade + + +def mock_order_2(): + return { + 'id': '1235', + 'symbol': 'ETC/BTC', 'status': 'closed', 'side': 'buy', + 'type': 'limit', 'price': 0.123, 'amount': 123.0, 'filled': 123.0, 'remaining': 0.0, - }, 'ETH/BTC', 'buy') - trade.orders.append(o) - return trade + } + + +def mock_order_2_sell(): + return { + 'id': '12366', + 'symbol': 'ETC/BTC', + 'status': 'closed', + 'side': 'sell', + 'type': 'limit', + 'price': 0.128, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + } def mock_trade_2(fee): @@ -49,31 +82,41 @@ def mock_trade_2(fee): open_order_id='dry_run_sell_12345', strategy='DefaultStrategy', ) - o = Order.parse_from_ccxt_object({ - 'id': '1235', - 'symbol': 'ETC/BTC', - 'status': 'closed', - 'side': 'buy', - 'price': 0.123, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - }, 'ETH/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_2(), 'ETH/BTC', 'buy') trade.orders.append(o) - o = Order.parse_from_ccxt_object({ - 'id': '12366', - 'symbol': 'ETC/BTC', - 'status': 'closed', - 'side': 'sell', - 'price': 0.128, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - }, 'ETH/BTC', 'sell') + o = Order.parse_from_ccxt_object(mock_order_2_sell(), 'ETH/BTC', 'sell') trade.orders.append(o) return trade +def mock_order_3(): + return { + 'id': '41231a12a', + 'symbol': 'XRP/BTC', + 'status': 'closed', + 'side': 'buy', + 'type': 'limit', + 'price': 0.05, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + } + + +def mock_order_3_sell(): + return { + 'id': '41231a666a', + 'symbol': 'XRP/BTC', + 'status': 'closed', + 'side': 'stop_loss', + 'type': 'limit', + 'price': 0.06, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + } + + def mock_trade_3(fee): """ Closed trade @@ -90,31 +133,27 @@ def mock_trade_3(fee): exchange='bittrex', is_open=False, ) - o = Order.parse_from_ccxt_object({ - 'id': '41231a12a', - 'symbol': 'XRP/BTC', - 'status': 'closed', - 'side': 'buy', - 'price': 0.05, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - }, 'ETH/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_3(), 'ETH/BTC', 'buy') trade.orders.append(o) - o = Order.parse_from_ccxt_object({ - 'id': '41231a666a', - 'symbol': 'XRP/BTC', - 'status': 'closed', - 'side': 'stop_loss', - 'price': 0.06, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - }, 'ETH/BTC', 'sell') + o = Order.parse_from_ccxt_object(mock_order_3_sell(), 'ETH/BTC', 'sell') trade.orders.append(o) return trade +def mock_order_4(): + return { + 'id': 'prod_buy_12345', + 'symbol': 'ETC/BTC', + 'status': 'open', + 'side': 'buy', + 'type': 'limit', + 'price': 0.123, + 'amount': 123.0, + 'filled': 0.0, + 'remaining': 123.0, + } + + def mock_trade_4(fee): """ Simulate prod entry @@ -131,17 +170,9 @@ def mock_trade_4(fee): open_order_id='prod_buy_12345', strategy='DefaultStrategy', ) - o = Order.parse_from_ccxt_object({ - 'id': 'prod_buy_12345', - 'symbol': 'ETC/BTC', - 'status': 'open', - 'side': 'buy', - 'price': 0.123, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - }, 'ETH/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_4(), 'ETH/BTC', 'buy') trade.orders.append(o) + return trade def create_mock_trades(fee): From f113b45036c39d216c6cfdca7702d816901b4f3b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Sep 2020 06:49:30 +0200 Subject: [PATCH 0592/1197] Refactor test to not duplicate order info --- tests/conftest.py | 23 ++++++++++++++++++----- tests/conftest_trades.py | 18 ------------------ tests/test_freqtradebot.py | 20 +++++++------------- 3 files changed, 25 insertions(+), 36 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index b966cde5b..a63b4e314 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,4 @@ # pragma pylint: disable=missing-docstring -from tests.conftest_trades import create_mock_trades -from freqtrade.persistence.models import Order import json import logging import re @@ -24,6 +22,8 @@ from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver from freqtrade.worker import Worker +from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, + mock_trade_4) logging.getLogger('').setLevel(logging.INFO) @@ -169,9 +169,22 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: freqtrade.exchange.refresh_latest_ohlcv = lambda p: None -@pytest.fixture(scope='function') -def mock_trades(init_persistence, fee): - return create_mock_trades(fee) +def create_mock_trades(fee): + """ + Create some fake trades ... + """ + # Simulate dry_run entries + trade = mock_trade_1(fee) + Trade.session.add(trade) + + trade = mock_trade_2(fee) + Trade.session.add(trade) + + trade = mock_trade_3(fee) + Trade.session.add(trade) + + trade = mock_trade_4(fee) + Trade.session.add(trade) @pytest.fixture(autouse=True) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index b2aff2ee7..1990725ee 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -173,21 +173,3 @@ def mock_trade_4(fee): o = Order.parse_from_ccxt_object(mock_order_4(), 'ETH/BTC', 'buy') trade.orders.append(o) return trade - - -def create_mock_trades(fee): - """ - Create some fake trades ... - """ - # Simulate dry_run entries - trade = mock_trade_1(fee) - Trade.session.add(trade) - - trade = mock_trade_2(fee) - Trade.session.add(trade) - - trade = mock_trade_3(fee) - Trade.session.add(trade) - - trade = mock_trade_4(fee) - Trade.session.add(trade) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 0f340ae8f..0d2e70f1e 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1,7 +1,6 @@ # pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments -from freqtrade.persistence.models import Order import logging import time from copy import deepcopy @@ -13,11 +12,13 @@ import pytest from freqtrade.constants import (CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT) -from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, +from freqtrade.exceptions import (DependencyException, ExchangeError, + InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, TemporaryError) from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade +from freqtrade.persistence.models import Order from freqtrade.rpc import RPCMessageType from freqtrade.state import RunMode, State from freqtrade.strategy.interface import SellCheckTuple, SellType @@ -26,6 +27,7 @@ from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal, patch_wallet, patch_whitelist) +from tests.conftest_trades import mock_order_4 def patch_RPCManager(mocker) -> MagicMock: @@ -4241,18 +4243,10 @@ def test_update_open_orders(mocker, default_conf, fee, caplog): caplog.clear() assert len(Order.get_open_orders()) == 1 - - matching_buy_order = { - 'id': 'prod_buy_12345', - 'symbol': 'ETC/BTC', + matching_buy_order = mock_order_4() + matching_buy_order.update({ 'status': 'closed', - 'side': 'buy', - 'type': 'limit', - 'price': 0.123, - 'amount': 123.0, - 'filled': 123.0, - 'remaining': 0.0, - } + }) mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=matching_buy_order) freqtrade.update_open_orders() assert len(Order.get_open_orders()) == 0 From 26a5cc5959de01f1a3c410409ddf12db461f0f9d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Sep 2020 07:41:58 +0200 Subject: [PATCH 0593/1197] Add return-type for select_order --- freqtrade/persistence/models.py | 2 +- tests/conftest_trades.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 096cf6209..79b3d491b 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -520,7 +520,7 @@ class Trade(_DECL_BASE): profit_ratio = (close_trade_price / self.open_trade_price) - 1 return float(f"{profit_ratio:.8f}") - def select_order(self, order_side: str, status: Optional[str]): + def select_order(self, order_side: str, status: Optional[str]) -> Optional[Order]: """ Returns latest order for this orderside and status Returns None if nothing is found diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 1990725ee..723a3fe58 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -108,9 +108,10 @@ def mock_order_3_sell(): 'id': '41231a666a', 'symbol': 'XRP/BTC', 'status': 'closed', - 'side': 'stop_loss', - 'type': 'limit', + 'side': 'sell', + 'type': 'stop_loss_limit', 'price': 0.06, + 'average': 0.06, 'amount': 123.0, 'filled': 123.0, 'remaining': 0.0, From 6518e7a7892e4b71771fe540e5a18ef8a5438fb4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Sep 2020 07:47:38 +0200 Subject: [PATCH 0594/1197] Add test for update_closed_trades_without_fees --- tests/test_freqtradebot.py | 51 +++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 0d2e70f1e..b6e5c80c8 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -27,7 +27,7 @@ from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal, patch_wallet, patch_whitelist) -from tests.conftest_trades import mock_order_4 +from tests.conftest_trades import mock_order_1, mock_order_2, mock_order_2_sell, mock_order_3, mock_order_3_sell, mock_order_4 def patch_RPCManager(mocker) -> MagicMock: @@ -4250,3 +4250,52 @@ def test_update_open_orders(mocker, default_conf, fee, caplog): mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=matching_buy_order) freqtrade.update_open_orders() assert len(Order.get_open_orders()) == 0 + + +@pytest.mark.usefixtures("init_persistence") +def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee): + freqtrade = get_patched_freqtradebot(mocker, default_conf) + + def patch_with_fee(order): + order.update({'fee': {'cost': 0.1, 'rate': 0.2, + 'currency': order['symbol'].split('/')[0]}}) + return order + + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + side_effect=[ + patch_with_fee(mock_order_2_sell()), + patch_with_fee(mock_order_3_sell()), + patch_with_fee(mock_order_1()), + patch_with_fee(mock_order_2()), + patch_with_fee(mock_order_3()), + patch_with_fee(mock_order_4()), + ] + ) + + create_mock_trades(fee) + trades = Trade.get_trades().all() + assert len(trades) == 4 + for trade in trades: + assert trade.fee_open_cost is None + assert trade.fee_open_currency is None + assert trade.fee_close_cost is None + assert trade.fee_close_currency is None + + freqtrade.update_closed_trades_without_assigned_fees() + + # trades = Trade.get_trades().all() + assert len(trades) == 4 + + for trade in trades: + if trade.is_open: + # Exclude Trade 4 - as the order is still open. + if trade.select_order('buy', 'closed'): + assert trade.fee_open_cost is not None + assert trade.fee_open_currency is not None + else: + assert trade.fee_open_cost is None + assert trade.fee_open_currency is None + + else: + assert trade.fee_close_cost is not None + assert trade.fee_close_currency is not None From f3e0370d4da308e40ecf5cf21b2844c4d1166de0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Sep 2020 07:54:55 +0200 Subject: [PATCH 0595/1197] Stylistic fixes --- tests/conftest_trades.py | 2 -- tests/test_freqtradebot.py | 4 +++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 723a3fe58..c990f6cdc 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -1,5 +1,3 @@ -import pytest - from freqtrade.persistence.models import Order, Trade diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index b6e5c80c8..f9bf37938 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -27,7 +27,9 @@ from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal, patch_wallet, patch_whitelist) -from tests.conftest_trades import mock_order_1, mock_order_2, mock_order_2_sell, mock_order_3, mock_order_3_sell, mock_order_4 +from tests.conftest_trades import (mock_order_1, mock_order_2, + mock_order_2_sell, mock_order_3, + mock_order_3_sell, mock_order_4) def patch_RPCManager(mocker) -> MagicMock: From 7852feab0566ecdda19e705cd88c7c7bd61cd52b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Sep 2020 09:06:43 +0200 Subject: [PATCH 0596/1197] support smaller timeframes --- docs/strategy-customization.md | 6 +++++- freqtrade/strategy/strategy_helper.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 7396f2a89..a1de1044c 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -521,7 +521,7 @@ class SampleStrategy(IStrategy): # use ffill to have the 1d value available in every row throughout the day. # Without this, comparisons between columns of the original and the informative pair would only work once per day. # Full documentation of this method, see below - dataframe = merge_informative_pair(dataframe, informative_pairs, self.timeframe, inf_tf, ffill=True) + dataframe = merge_informative_pair(dataframe, informative, self.timeframe, inf_tf, ffill=True) # Calculate rsi of the original dataframe (5m timeframe) dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) @@ -601,6 +601,10 @@ All columns of the informative dataframe will be available on the returning data ``` +!!! Warning "Informative timeframe < timeframe" + Using informative timeframes smaller than the dataframe timeframe is not recommended with this method, as it will not use any of the additional information this would provide. + To use the more detailed information properly, more advanced methods should be applied (which are out of scope for freqtrade documentation, as it'll depend on the respective need). + *** ## Additional data (Wallets) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 1fbf618bd..1a5b2d0f8 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -26,7 +26,8 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, """ minutes_inf = timeframe_to_minutes(timeframe_inf) - if timeframe == timeframe_inf: + minutes = timeframe_to_minutes(timeframe) + if minutes >= minutes_inf: # No need to forwardshift if the timeframes are identical informative['date_merge'] = informative["date"] else: From 014fcb36f4e6b3da535dcc7ebaa604ea47f9e2d6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2020 07:09:07 +0000 Subject: [PATCH 0597/1197] Bump mkdocs-material from 5.5.11 to 5.5.12 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 5.5.11 to 5.5.12. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/5.5.11...5.5.12) Signed-off-by: dependabot[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 c8f08d12a..6408616a0 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==5.5.11 +mkdocs-material==5.5.12 mdx_truly_sane_lists==1.2 From 534404c284a3de1b8bc9b6cb4f8e9603c32f81c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2020 07:09:22 +0000 Subject: [PATCH 0598/1197] Bump ccxt from 1.33.72 to 1.34.3 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.33.72 to 1.34.3. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.33.72...1.34.3) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index f305d8793..58912bc0c 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.33.72 +ccxt==1.34.3 SQLAlchemy==1.3.19 python-telegram-bot==12.8 arrow==0.16.0 From ff0e73a9e592e4c61eca8fce97b0c84d5b886844 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2020 07:09:23 +0000 Subject: [PATCH 0599/1197] Bump scikit-optimize from 0.7.4 to 0.8.1 Bumps [scikit-optimize](https://github.com/scikit-optimize/scikit-optimize) from 0.7.4 to 0.8.1. - [Release notes](https://github.com/scikit-optimize/scikit-optimize/releases) - [Changelog](https://github.com/scikit-optimize/scikit-optimize/blob/master/CHANGELOG.md) - [Commits](https://github.com/scikit-optimize/scikit-optimize/compare/v0.7.4...v0.8.1) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index fbc679eaa..ea24196b9 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -4,7 +4,7 @@ # Required for hyperopt scipy==1.5.2 scikit-learn==0.23.1 -scikit-optimize==0.7.4 +scikit-optimize==0.8.1 filelock==3.0.12 joblib==0.16.0 progressbar2==3.52.1 From f20318fad1b27039c8269984f4ea78cecb1aab90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2020 07:37:12 +0000 Subject: [PATCH 0600/1197] Bump scikit-learn from 0.23.1 to 0.23.2 Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 0.23.1 to 0.23.2. - [Release notes](https://github.com/scikit-learn/scikit-learn/releases) - [Commits](https://github.com/scikit-learn/scikit-learn/compare/0.23.1...0.23.2) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index ea24196b9..4d884b4fe 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -3,7 +3,7 @@ # Required for hyperopt scipy==1.5.2 -scikit-learn==0.23.1 +scikit-learn==0.23.2 scikit-optimize==0.8.1 filelock==3.0.12 joblib==0.16.0 From f63a3789672b7a5e58ce264688e26f895f26d747 Mon Sep 17 00:00:00 2001 From: Allen Day Date: Mon, 7 Sep 2020 23:26:55 +0800 Subject: [PATCH 0601/1197] Update hyperopt.py zero pad wins/draws/losses (W/D/L) column to preserve alignment in console pretty print --- 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 b9db3c09a..6d29be08e 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -574,7 +574,7 @@ class Hyperopt: 'wins': wins, 'draws': draws, 'losses': losses, - 'winsdrawslosses': f"{wins}/{draws}/{losses}", + 'winsdrawslosses': f"{wins:04}/{draws:04}/{losses:04}", 'avg_profit': backtesting_results.profit_percent.mean() * 100.0, 'median_profit': backtesting_results.profit_percent.median() * 100.0, 'total_profit': backtesting_results.profit_abs.sum(), From 3fe2ed0e189a2843c570e84395aea3c99e5b8af8 Mon Sep 17 00:00:00 2001 From: Allen Day Date: Mon, 7 Sep 2020 23:38:51 +0800 Subject: [PATCH 0602/1197] zero pad in test --- tests/optimize/test_hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index bb6f043e7..e7a26cd5f 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -813,7 +813,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'draws': 0, 'duration': 100.0, 'losses': 0, - 'winsdrawslosses': '1/0/0', + 'winsdrawslosses': '0001/0000/0000', 'median_profit': 2.3117, 'profit': 2.3117, 'total_profit': 0.000233, From 8af610b543a82c8ebef54aeeaf834d092dceeea1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Sep 2020 06:40:40 +0200 Subject: [PATCH 0603/1197] Add Test for reupdate_buy_order_fees --- tests/test_freqtradebot.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index f9bf37938..aef9b5f29 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4301,3 +4301,40 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee): else: assert trade.fee_close_cost is not None assert trade.fee_close_currency is not None + + +@pytest.mark.usefixtures("init_persistence") +def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog): + freqtrade = get_patched_freqtradebot(mocker, default_conf) + mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') + + create_mock_trades(fee) + trades = Trade.get_trades().all() + + freqtrade.reupdate_buy_order_fees(trades[0]) + assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) + assert mock_uts.call_count == 1 + assert mock_uts.call_args_list[0][0][0] == trades[0] + assert mock_uts.call_args_list[0][0][1] == '1234' + assert log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog) + mock_uts.reset_mock() + caplog.clear() + + # Test with trade without orders + trade = Trade( + pair='XRP/ETH', + stake_amount=0.001, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_date=arrow.utcnow().datetime, + is_open=True, + amount=20, + open_rate=0.01, + exchange='bittrex', + ) + Trade.session.add(trade) + + freqtrade.reupdate_buy_order_fees(trade) + assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) + assert mock_uts.call_count == 0 + assert not log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog) From caf0476717c90f1c98d07fb6875ee4a60eeeda0d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Sep 2020 06:46:38 +0200 Subject: [PATCH 0604/1197] Add test for handle_insufficient_funds --- tests/test_freqtradebot.py | 64 +++++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index aef9b5f29..c95a085a6 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4315,7 +4315,7 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog): assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) assert mock_uts.call_count == 1 assert mock_uts.call_args_list[0][0][0] == trades[0] - assert mock_uts.call_args_list[0][0][1] == '1234' + assert mock_uts.call_args_list[0][0][1] == mock_order_1()['id'] assert log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog) mock_uts.reset_mock() caplog.clear() @@ -4338,3 +4338,65 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog): assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) assert mock_uts.call_count == 0 assert not log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog) + + +@pytest.mark.usefixtures("init_persistence") +def test_handle_insufficient_funds(mocker, default_conf, fee): + freqtrade = get_patched_freqtradebot(mocker, default_conf) + mock_rlo = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.refind_lost_order') + mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_buy_order_fees') + create_mock_trades(fee) + trades = Trade.get_trades().all() + + # Trade 0 has only a open buy order, no closed order + freqtrade.handle_insufficient_funds(trades[0]) + assert mock_rlo.call_count == 0 + assert mock_bof.call_count == 1 + + mock_rlo.reset_mock() + mock_bof.reset_mock() + + # Trade 1 has closed buy and sell orders + freqtrade.handle_insufficient_funds(trades[1]) + assert mock_rlo.call_count == 1 + assert mock_bof.call_count == 0 + + mock_rlo.reset_mock() + mock_bof.reset_mock() + + # Trade 2 has closed buy and sell orders + freqtrade.handle_insufficient_funds(trades[2]) + assert mock_rlo.call_count == 1 + assert mock_bof.call_count == 0 + + mock_rlo.reset_mock() + mock_bof.reset_mock() + + # Trade 3 has an opne buy order + freqtrade.handle_insufficient_funds(trades[3]) + assert mock_rlo.call_count == 0 + assert mock_bof.call_count == 1 + + +@pytest.mark.usefixtures("init_persistence") +def test_refind_lost_order(mocker, default_conf, fee, caplog): + freqtrade = get_patched_freqtradebot(mocker, default_conf) + mock_rlo = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.refind_lost_order') + mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_buy_order_fees') + + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + side_effect=[ + mock_order_2_sell(), + mock_order_3_sell(), + mock_order_1(), + mock_order_2(), + mock_order_3(), + mock_order_4(), + ]) + + create_mock_trades(fee) + trades = Trade.get_trades().all() + + # freqtrade.refind_lost_order(trades[0]) + + # TODO: Implement test here. From 98840eef3cd9d7f1008b95f4dad8b36a9d3ab66b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Sep 2020 07:01:43 +0200 Subject: [PATCH 0605/1197] Add 5th mock trade --- tests/commands/test_commands.py | 2 +- tests/conftest.py | 5 +++- tests/conftest_trades.py | 51 +++++++++++++++++++++++++++++++++ tests/data/test_btanalysis.py | 2 +- tests/test_freqtradebot.py | 13 +++++---- tests/test_persistence.py | 4 +-- 6 files changed, 66 insertions(+), 11 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 26875fb7f..290a3fa77 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1116,7 +1116,7 @@ def test_show_trades(mocker, fee, capsys, caplog): pargs = get_args(args) pargs['config'] = None start_show_trades(pargs) - assert log_has("Printing 4 Trades: ", caplog) + assert log_has("Printing 5 Trades: ", caplog) captured = capsys.readouterr() assert "Trade(id=1" in captured.out assert "Trade(id=2" in captured.out diff --git a/tests/conftest.py b/tests/conftest.py index a63b4e314..1ef495301 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,7 +23,7 @@ from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver from freqtrade.worker import Worker from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, - mock_trade_4) + mock_trade_4, mock_trade_5) logging.getLogger('').setLevel(logging.INFO) @@ -186,6 +186,9 @@ def create_mock_trades(fee): trade = mock_trade_4(fee) Trade.session.add(trade) + trade = mock_trade_5(fee) + Trade.session.add(trade) + @pytest.fixture(autouse=True) def patch_coingekko(mocker) -> None: diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index c990f6cdc..d3e19603b 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -172,3 +172,54 @@ def mock_trade_4(fee): o = Order.parse_from_ccxt_object(mock_order_4(), 'ETH/BTC', 'buy') trade.orders.append(o) return trade + + +def mock_order_5(): + return { + 'id': 'prod_buy_3455', + 'symbol': 'XRP/BTC', + 'status': 'closed', + 'side': 'buy', + 'type': 'limit', + 'price': 0.123, + 'amount': 123.0, + 'filled': 123.0, + 'remaining': 0.0, + } + + +def mock_order_5_stoploss(): + return { + 'id': 'prod_stoploss_3455', + 'symbol': 'XRP/BTC', + 'status': 'open', + 'side': 'sell', + 'type': 'stop_loss_limit', + 'price': 0.123, + 'amount': 123.0, + 'filled': 0.0, + 'remaining': 123.0, + } + + +def mock_trade_5(fee): + """ + Simulate prod entry with stoploss + """ + trade = Trade( + pair='XRP/BTC', + stake_amount=0.001, + amount=123.0, + amount_requested=124.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.123, + exchange='bittrex', + strategy='SampleStrategy', + stoploss_order_id='prod_stoploss_3455' + ) + o = Order.parse_from_ccxt_object(mock_order_5(), 'ETH/BTC', 'buy') + trade.orders.append(o) + o = Order.parse_from_ccxt_object(mock_order_5_stoploss(), 'ETH/BTC', 'sell') + trade.orders.append(o) + return trade diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index e2ca66bd8..3b5672e65 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -110,7 +110,7 @@ def test_load_trades_from_db(default_conf, fee, mocker): trades = load_trades_from_db(db_url=default_conf['db_url']) assert init_mock.call_count == 1 - assert len(trades) == 4 + assert len(trades) == 5 assert isinstance(trades, DataFrame) assert "pair" in trades.columns assert "open_date" in trades.columns diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c95a085a6..883c63737 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4213,7 +4213,7 @@ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limi freqtrade = get_patched_freqtradebot(mocker, default_conf) create_mock_trades(fee) trades = Trade.query.all() - assert len(trades) == 4 + assert len(trades) == 5 freqtrade.cancel_all_open_orders() assert buy_mock.call_count == 1 assert sell_mock.call_count == 1 @@ -4244,14 +4244,15 @@ def test_update_open_orders(mocker, default_conf, fee, caplog): assert log_has_re(r"Error updating Order .*", caplog) caplog.clear() - assert len(Order.get_open_orders()) == 1 + assert len(Order.get_open_orders()) == 2 matching_buy_order = mock_order_4() matching_buy_order.update({ 'status': 'closed', }) mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=matching_buy_order) freqtrade.update_open_orders() - assert len(Order.get_open_orders()) == 0 + # Only stoploss order is kept open + assert len(Order.get_open_orders()) == 1 @pytest.mark.usefixtures("init_persistence") @@ -4276,7 +4277,7 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee): create_mock_trades(fee) trades = Trade.get_trades().all() - assert len(trades) == 4 + assert len(trades) == 5 for trade in trades: assert trade.fee_open_cost is None assert trade.fee_open_currency is None @@ -4285,8 +4286,8 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee): freqtrade.update_closed_trades_without_assigned_fees() - # trades = Trade.get_trades().all() - assert len(trades) == 4 + trades = Trade.get_trades().all() + assert len(trades) == 5 for trade in trades: if trade.is_open: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 788debace..48b918128 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -734,7 +734,7 @@ def test_adjust_min_max_rates(fee): def test_get_open(default_conf, fee): create_mock_trades(fee) - assert len(Trade.get_open_trades()) == 2 + assert len(Trade.get_open_trades()) == 3 @pytest.mark.usefixtures("init_persistence") @@ -1004,7 +1004,7 @@ def test_total_open_trades_stakes(fee): assert res == 0 create_mock_trades(fee) res = Trade.total_open_trades_stakes() - assert res == 0.002 + assert res == 0.003 @pytest.mark.usefixtures("init_persistence") From 25938efee665389eb8c013b942205dceaf1e23d0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Sep 2020 07:50:52 +0200 Subject: [PATCH 0606/1197] Add partial test for refind_order --- freqtrade/freqtradebot.py | 2 +- tests/conftest_trades.py | 2 +- tests/test_freqtradebot.py | 51 +++++++++++++++++++++++++++----------- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1af0e85b7..c20a57205 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -333,7 +333,7 @@ class FreqtradeBot: stoploss_order=order.ft_order_side == 'stoploss') except ExchangeError: - logger.warning(f"Error updating {order.order_id}") + logger.warning(f"Error updating {order.order_id}.") # # BUY / enter positions / open trades logic and methods diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index d3e19603b..03af7fa1d 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -220,6 +220,6 @@ def mock_trade_5(fee): ) o = Order.parse_from_ccxt_object(mock_order_5(), 'ETH/BTC', 'buy') trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_5_stoploss(), 'ETH/BTC', 'sell') + o = Order.parse_from_ccxt_object(mock_order_5_stoploss(), 'ETH/BTC', 'stoploss') trade.orders.append(o) return trade diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 883c63737..625c6ed7e 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -29,7 +29,7 @@ from tests.conftest import (create_mock_trades, get_patched_freqtradebot, patch_wallet, patch_whitelist) from tests.conftest_trades import (mock_order_1, mock_order_2, mock_order_2_sell, mock_order_3, - mock_order_3_sell, mock_order_4) + mock_order_3_sell, mock_order_4, mock_order_5_stoploss) def patch_RPCManager(mocker) -> MagicMock: @@ -4382,22 +4382,45 @@ def test_handle_insufficient_funds(mocker, default_conf, fee): @pytest.mark.usefixtures("init_persistence") def test_refind_lost_order(mocker, default_conf, fee, caplog): freqtrade = get_patched_freqtradebot(mocker, default_conf) - mock_rlo = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.refind_lost_order') - mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_buy_order_fees') + mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') - mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', - side_effect=[ - mock_order_2_sell(), - mock_order_3_sell(), - mock_order_1(), - mock_order_2(), - mock_order_3(), - mock_order_4(), - ]) + mock_fo = mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order') create_mock_trades(fee) trades = Trade.get_trades().all() - # freqtrade.refind_lost_order(trades[0]) + caplog.clear() - # TODO: Implement test here. + # No open order + freqtrade.refind_lost_order(trades[0]) + order = mock_order_1() + assert log_has_re(r"Order Order(.*order_id=" + order['id'] + ".*) is no longer open.", caplog) + assert mock_fo.call_count == 0 + assert mock_uts.call_count == 0 + + caplog.clear() + mock_fo.reset_mock() + + # Open buy order + freqtrade.refind_lost_order(trades[3]) + order = mock_order_4() + assert log_has_re(r"Trying to refind Order\(.*", caplog) + assert mock_fo.call_count == 0 + assert mock_uts.call_count == 0 + + caplog.clear() + mock_fo.reset_mock() + + # Open stoploss order + freqtrade.refind_lost_order(trades[4]) + order = mock_order_5_stoploss() + assert log_has_re(r"Trying to refind Order\(.*", caplog) + assert mock_fo.call_count == 1 + assert mock_uts.call_count == 1 + + # Test error case + mock_fo = mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + side_effect=ExchangeError()) + freqtrade.refind_lost_order(trades[4]) + caplog.clear() + assert log_has(f"Error updating {order['id']}.", caplog) From 083c358044d53b2303f2dbbeb260d2b1fad09dd9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Sep 2020 07:57:02 +0200 Subject: [PATCH 0607/1197] Fix wrong sequence in test --- tests/test_freqtradebot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 625c6ed7e..542206e62 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4418,9 +4418,10 @@ def test_refind_lost_order(mocker, default_conf, fee, caplog): assert mock_fo.call_count == 1 assert mock_uts.call_count == 1 + caplog.clear() + # Test error case mock_fo = mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', side_effect=ExchangeError()) freqtrade.refind_lost_order(trades[4]) - caplog.clear() assert log_has(f"Error updating {order['id']}.", caplog) From 4cf66e2fba76d3aab734bffe94063ca906c74bbc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2020 13:29:22 +0000 Subject: [PATCH 0608/1197] Bump progressbar2 from 3.52.1 to 3.53.1 Bumps [progressbar2](https://github.com/WoLpH/python-progressbar) from 3.52.1 to 3.53.1. - [Release notes](https://github.com/WoLpH/python-progressbar/releases) - [Changelog](https://github.com/WoLpH/python-progressbar/blob/develop/CHANGES.rst) - [Commits](https://github.com/WoLpH/python-progressbar/compare/v3.52.1...v3.53.1) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 4d884b4fe..b47331aa3 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -7,4 +7,4 @@ scikit-learn==0.23.2 scikit-optimize==0.8.1 filelock==3.0.12 joblib==0.16.0 -progressbar2==3.52.1 +progressbar2==3.53.1 From 986e767d6c4ec460cc35b95db178e19bbef9de2b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2020 13:29:27 +0000 Subject: [PATCH 0609/1197] Bump blosc from 1.9.1 to 1.9.2 Bumps [blosc](https://github.com/blosc/python-blosc) from 1.9.1 to 1.9.2. - [Release notes](https://github.com/blosc/python-blosc/releases) - [Changelog](https://github.com/Blosc/python-blosc/blob/master/RELEASE_NOTES.rst) - [Commits](https://github.com/blosc/python-blosc/compare/v1.9.1...v1.9.2) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 58912bc0c..5efc6a2ee 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -14,7 +14,7 @@ tabulate==0.8.7 pycoingecko==1.3.0 jinja2==2.11.2 tables==3.6.1 -blosc==1.9.1 +blosc==1.9.2 # find first, C search in arrays py_find_1st==1.1.4 From d8dae46544987a2f5d672d1128851d47d5f6d9bb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2020 13:29:31 +0000 Subject: [PATCH 0610/1197] Bump ccxt from 1.34.3 to 1.34.11 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.34.3 to 1.34.11. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.34.3...1.34.11) Signed-off-by: dependabot[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 58912bc0c..faa022c4c 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.34.3 +ccxt==1.34.11 SQLAlchemy==1.3.19 python-telegram-bot==12.8 arrow==0.16.0 From 8c97b83b8c44de9f7b921cba6bec3bdeb39e5527 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 9 Sep 2020 13:29:36 +0000 Subject: [PATCH 0611/1197] Bump pandas from 1.1.1 to 1.1.2 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.1.1 to 1.1.2. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.1.1...v1.1.2) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 66f4cbc5f..a1a1fb250 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ -r requirements-common.txt numpy==1.19.1 -pandas==1.1.1 +pandas==1.1.2 From 4480b3b39374439296407128b816a797749865c9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Sep 2020 15:39:35 +0200 Subject: [PATCH 0612/1197] Fix error in documentation (wrong sequence of steps) --- docs/strategy-customization.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index a1de1044c..615be0247 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -580,10 +580,6 @@ All columns of the informative dataframe will be available on the returning data A custom implementation for this is possible, and can be done as follows: ``` python - # Rename columns to be unique - informative.columns = [f"{col}_{inf_tf}" for col in informative.columns] - # Assuming inf_tf = '1d' - then the columns will now be: - # date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d # Shift date by 1 candle # This is necessary since the data is always the "open date" @@ -592,6 +588,11 @@ All columns of the informative dataframe will be available on the returning data # Only do this if the timeframes are different: informative['date_merge'] = informative["date"] + pd.to_timedelta(minutes, 'm') + # Rename columns to be unique + informative.columns = [f"{col}_{inf_tf}" for col in informative.columns] + # Assuming inf_tf = '1d' - then the columns will now be: + # date_1d, open_1d, high_1d, low_1d, close_1d, rsi_1d + # Combine the 2 dataframes # all indicators on the informative sample MUST be calculated before this point dataframe = pd.merge(dataframe, informative, left_on='date', right_on=f'date_merge_{inf_tf}', how='left') From c3e0397743a61227c741585fc5c97bb45a486cee Mon Sep 17 00:00:00 2001 From: Blackhawke Date: Wed, 9 Sep 2020 09:16:11 -0700 Subject: [PATCH 0613/1197] Added full "source" command to virtualenv in easy install --- docs/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index c03be55d1..da82d632f 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -63,7 +63,7 @@ With this option, the script will install the bot and most dependencies: You will need to have git and python3.6+ installed beforehand for this to work. * Mandatory software as: `ta-lib` -* Setup your virtualenv under `.env/` +* Setup your virtualenv: `source .env/bin/activate` This option is a combination of installation tasks, `--reset` and `--config`. From 3c521f55b234bd1f7109342c963d72cdf5fec9a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Sep 2020 07:40:19 +0200 Subject: [PATCH 0614/1197] Add 6th mock trade --- tests/commands/test_commands.py | 3 +- tests/conftest.py | 5 ++- tests/conftest_trades.py | 68 +++++++++++++++++++++++++++++---- tests/data/test_btanalysis.py | 3 +- tests/test_freqtradebot.py | 18 ++++----- tests/test_persistence.py | 6 +-- 6 files changed, 81 insertions(+), 22 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 290a3fa77..a35fc9fb8 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -18,6 +18,7 @@ from freqtrade.state import RunMode from tests.conftest import (create_mock_trades, get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) +from tests.conftest_trades import MOCK_TRADE_COUNT def test_setup_utils_configuration(): @@ -1116,7 +1117,7 @@ def test_show_trades(mocker, fee, capsys, caplog): pargs = get_args(args) pargs['config'] = None start_show_trades(pargs) - assert log_has("Printing 5 Trades: ", caplog) + assert log_has(f"Printing {MOCK_TRADE_COUNT} Trades: ", caplog) captured = capsys.readouterr() assert "Trade(id=1" in captured.out assert "Trade(id=2" in captured.out diff --git a/tests/conftest.py b/tests/conftest.py index 1ef495301..fe55c8784 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,7 +23,7 @@ from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver from freqtrade.worker import Worker from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, - mock_trade_4, mock_trade_5) + mock_trade_4, mock_trade_5, mock_trade_6) logging.getLogger('').setLevel(logging.INFO) @@ -189,6 +189,9 @@ def create_mock_trades(fee): trade = mock_trade_5(fee) Trade.session.add(trade) + trade = mock_trade_6(fee) + Trade.session.add(trade) + @pytest.fixture(autouse=True) def patch_coingekko(mocker) -> None: diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 03af7fa1d..43bf15e51 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -1,6 +1,9 @@ from freqtrade.persistence.models import Order, Trade +MOCK_TRADE_COUNT = 6 + + def mock_order_1(): return { 'id': '1234', @@ -80,9 +83,9 @@ def mock_trade_2(fee): open_order_id='dry_run_sell_12345', strategy='DefaultStrategy', ) - o = Order.parse_from_ccxt_object(mock_order_2(), 'ETH/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_2(), 'ETC/BTC', 'buy') trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_2_sell(), 'ETH/BTC', 'sell') + o = Order.parse_from_ccxt_object(mock_order_2_sell(), 'ETC/BTC', 'sell') trade.orders.append(o) return trade @@ -132,9 +135,9 @@ def mock_trade_3(fee): exchange='bittrex', is_open=False, ) - o = Order.parse_from_ccxt_object(mock_order_3(), 'ETH/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_3(), 'XRP/BTC', 'buy') trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_3_sell(), 'ETH/BTC', 'sell') + o = Order.parse_from_ccxt_object(mock_order_3_sell(), 'XRP/BTC', 'sell') trade.orders.append(o) return trade @@ -169,7 +172,7 @@ def mock_trade_4(fee): open_order_id='prod_buy_12345', strategy='DefaultStrategy', ) - o = Order.parse_from_ccxt_object(mock_order_4(), 'ETH/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_4(), 'ETC/BTC', 'buy') trade.orders.append(o) return trade @@ -218,8 +221,59 @@ def mock_trade_5(fee): strategy='SampleStrategy', stoploss_order_id='prod_stoploss_3455' ) - o = Order.parse_from_ccxt_object(mock_order_5(), 'ETH/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_5(), 'XRP/BTC', 'buy') trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_5_stoploss(), 'ETH/BTC', 'stoploss') + o = Order.parse_from_ccxt_object(mock_order_5_stoploss(), 'XRP/BTC', 'stoploss') + trade.orders.append(o) + return trade + + +def mock_order_6(): + return { + 'id': 'prod_buy_6', + 'symbol': 'LTC/BTC', + 'status': 'closed', + 'side': 'buy', + 'type': 'limit', + 'price': 0.15, + 'amount': 2.0, + 'filled': 2.0, + 'remaining': 0.0, + } + + +def mock_order_6_sell(): + return { + 'id': 'prod_sell_6', + 'symbol': 'LTC/BTC', + 'status': 'open', + 'side': 'sell', + 'type': 'limit', + 'price': 0.20, + 'amount': 2.0, + 'filled': 0.0, + 'remaining': 2.0, + } + + +def mock_trade_6(fee): + """ + Simulate prod entry with open sell order + """ + trade = Trade( + pair='LTC/BTC', + stake_amount=0.001, + amount=2.0, + amount_requested=2.0, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_rate=0.15, + exchange='bittrex', + strategy='SampleStrategy', + open_order_id="prod_sell_6", + ) + o = Order.parse_from_ccxt_object(mock_order_6(), 'LTC/BTC', 'buy') + trade.orders.append(o) + o = Order.parse_from_ccxt_object(mock_order_6_sell(), 'LTC/BTC', 'stoploss') trade.orders.append(o) return trade diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 3b5672e65..564dae0b1 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -20,6 +20,7 @@ from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, from freqtrade.data.history import load_data, load_pair_history from freqtrade.optimize.backtesting import BacktestResult from tests.conftest import create_mock_trades +from tests.conftest_trades import MOCK_TRADE_COUNT def test_get_latest_backtest_filename(testdatadir, mocker): @@ -110,7 +111,7 @@ def test_load_trades_from_db(default_conf, fee, mocker): trades = load_trades_from_db(db_url=default_conf['db_url']) assert init_mock.call_count == 1 - assert len(trades) == 5 + assert len(trades) == MOCK_TRADE_COUNT assert isinstance(trades, DataFrame) assert "pair" in trades.columns assert "open_date" in trades.columns diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 542206e62..ace44bfae 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -27,7 +27,7 @@ from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal, patch_wallet, patch_whitelist) -from tests.conftest_trades import (mock_order_1, mock_order_2, +from tests.conftest_trades import (MOCK_TRADE_COUNT, mock_order_1, mock_order_2, mock_order_2_sell, mock_order_3, mock_order_3_sell, mock_order_4, mock_order_5_stoploss) @@ -4206,17 +4206,17 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order_ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limit_sell_order): default_conf['cancel_open_orders_on_exit'] = True mocker.patch('freqtrade.exchange.Exchange.fetch_order', - side_effect=[ExchangeError(), limit_sell_order, limit_buy_order]) + side_effect=[ExchangeError(), limit_sell_order, limit_buy_order, limit_sell_order, ]) buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_buy') sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_sell') freqtrade = get_patched_freqtradebot(mocker, default_conf) create_mock_trades(fee) trades = Trade.query.all() - assert len(trades) == 5 + assert len(trades) == MOCK_TRADE_COUNT freqtrade.cancel_all_open_orders() assert buy_mock.call_count == 1 - assert sell_mock.call_count == 1 + assert sell_mock.call_count == 2 @pytest.mark.usefixtures("init_persistence") @@ -4244,15 +4244,15 @@ def test_update_open_orders(mocker, default_conf, fee, caplog): assert log_has_re(r"Error updating Order .*", caplog) caplog.clear() - assert len(Order.get_open_orders()) == 2 + assert len(Order.get_open_orders()) == 3 matching_buy_order = mock_order_4() matching_buy_order.update({ 'status': 'closed', }) mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=matching_buy_order) freqtrade.update_open_orders() - # Only stoploss order is kept open - assert len(Order.get_open_orders()) == 1 + # Only stoploss and sell orders are kept open + assert len(Order.get_open_orders()) == 2 @pytest.mark.usefixtures("init_persistence") @@ -4277,7 +4277,7 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee): create_mock_trades(fee) trades = Trade.get_trades().all() - assert len(trades) == 5 + assert len(trades) == MOCK_TRADE_COUNT for trade in trades: assert trade.fee_open_cost is None assert trade.fee_open_currency is None @@ -4287,7 +4287,7 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee): freqtrade.update_closed_trades_without_assigned_fees() trades = Trade.get_trades().all() - assert len(trades) == 5 + assert len(trades) == MOCK_TRADE_COUNT for trade in trades: if trade.is_open: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 48b918128..d2da1c6a2 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -731,10 +731,10 @@ def test_adjust_min_max_rates(fee): @pytest.mark.usefixtures("init_persistence") -def test_get_open(default_conf, fee): +def test_get_open(fee): create_mock_trades(fee) - assert len(Trade.get_open_trades()) == 3 + assert len(Trade.get_open_trades()) == 4 @pytest.mark.usefixtures("init_persistence") @@ -1004,7 +1004,7 @@ def test_total_open_trades_stakes(fee): assert res == 0 create_mock_trades(fee) res = Trade.total_open_trades_stakes() - assert res == 0.003 + assert res == 0.004 @pytest.mark.usefixtures("init_persistence") From 23f569ea38aded041e61ad5b3daa6f73d37b8247 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Sep 2020 08:03:26 +0200 Subject: [PATCH 0615/1197] Add test for sell order refind, improve overall test for this function --- tests/conftest_trades.py | 2 +- tests/test_freqtradebot.py | 60 ++++++++++++++++++++++++++++++++++---- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 43bf15e51..78388f022 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -274,6 +274,6 @@ def mock_trade_6(fee): ) o = Order.parse_from_ccxt_object(mock_order_6(), 'LTC/BTC', 'buy') trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_6_sell(), 'LTC/BTC', 'stoploss') + o = Order.parse_from_ccxt_object(mock_order_6_sell(), 'LTC/BTC', 'sell') trade.orders.append(o) return trade diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index ace44bfae..29792c248 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -29,7 +29,7 @@ from tests.conftest import (create_mock_trades, get_patched_freqtradebot, patch_wallet, patch_whitelist) from tests.conftest_trades import (MOCK_TRADE_COUNT, mock_order_1, mock_order_2, mock_order_2_sell, mock_order_3, - mock_order_3_sell, mock_order_4, mock_order_5_stoploss) + mock_order_3_sell, mock_order_4, mock_order_5_stoploss, mock_order_6_sell) def patch_RPCManager(mocker) -> MagicMock: @@ -4384,7 +4384,12 @@ def test_refind_lost_order(mocker, default_conf, fee, caplog): freqtrade = get_patched_freqtradebot(mocker, default_conf) mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') - mock_fo = mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order') + mock_fo = mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + return_value={'status': 'open'}) + + def reset_open_orders(trade): + trade.open_order_id = None + trade.stoploss_order_id = None create_mock_trades(fee) trades = Trade.get_trades().all() @@ -4392,36 +4397,81 @@ def test_refind_lost_order(mocker, default_conf, fee, caplog): caplog.clear() # No open order - freqtrade.refind_lost_order(trades[0]) + trade = trades[0] + reset_open_orders(trade) + assert trade.open_order_id is None + assert trade.stoploss_order_id is None + + freqtrade.refind_lost_order(trade) order = mock_order_1() assert log_has_re(r"Order Order(.*order_id=" + order['id'] + ".*) is no longer open.", caplog) assert mock_fo.call_count == 0 assert mock_uts.call_count == 0 + # No change to orderid - as update_trade_state is mocked + assert trade.open_order_id is None + assert trade.stoploss_order_id is None caplog.clear() mock_fo.reset_mock() # Open buy order - freqtrade.refind_lost_order(trades[3]) + trade = trades[3] + reset_open_orders(trade) + assert trade.open_order_id is None + assert trade.stoploss_order_id is None + + freqtrade.refind_lost_order(trade) order = mock_order_4() assert log_has_re(r"Trying to refind Order\(.*", caplog) assert mock_fo.call_count == 0 assert mock_uts.call_count == 0 + # No change to orderid - as update_trade_state is mocked + assert trade.open_order_id is None + assert trade.stoploss_order_id is None caplog.clear() mock_fo.reset_mock() # Open stoploss order - freqtrade.refind_lost_order(trades[4]) + trade = trades[4] + reset_open_orders(trade) + assert trade.open_order_id is None + assert trade.stoploss_order_id is None + + freqtrade.refind_lost_order(trade) order = mock_order_5_stoploss() assert log_has_re(r"Trying to refind Order\(.*", caplog) assert mock_fo.call_count == 1 assert mock_uts.call_count == 1 + # stoploss_order_id is "refound" and added to the trade + assert trade.open_order_id is None + assert trade.stoploss_order_id is not None + + caplog.clear() + mock_fo.reset_mock() + mock_uts.reset_mock() + + # Open sell order + trade = trades[5] + reset_open_orders(trade) + assert trade.open_order_id is None + assert trade.stoploss_order_id is None + + freqtrade.refind_lost_order(trade) + order = mock_order_6_sell() + assert log_has_re(r"Trying to refind Order\(.*", caplog) + assert mock_fo.call_count == 1 + assert mock_uts.call_count == 1 + # sell-orderid is "refound" and added to the trade + assert trade.open_order_id == order['id'] + assert trade.stoploss_order_id is None caplog.clear() # Test error case mock_fo = mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', side_effect=ExchangeError()) + order = mock_order_5_stoploss() + freqtrade.refind_lost_order(trades[4]) assert log_has(f"Error updating {order['id']}.", caplog) From 6a08fee25b88b8f4824bcc5f76f2dc3ee7c96529 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Sep 2020 08:04:04 +0200 Subject: [PATCH 0616/1197] Fix wrong import in documentation --- docs/strategy-customization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 615be0247..14d5fcd84 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -483,7 +483,7 @@ if self.dp: ### Complete Data-provider sample ```python -from freqtrade.strategy import IStrategy, merge_informative_pairs +from freqtrade.strategy import IStrategy, merge_informative_pair from pandas import DataFrame class SampleStrategy(IStrategy): From 4db8c779fc30eb5efb0ec6661e0cfa46a47ff007 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Sep 2020 08:19:40 +0200 Subject: [PATCH 0617/1197] Fix formatting issues --- tests/test_freqtradebot.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 29792c248..03b338bab 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -27,9 +27,11 @@ from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker, log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal, patch_wallet, patch_whitelist) -from tests.conftest_trades import (MOCK_TRADE_COUNT, mock_order_1, mock_order_2, - mock_order_2_sell, mock_order_3, - mock_order_3_sell, mock_order_4, mock_order_5_stoploss, mock_order_6_sell) +from tests.conftest_trades import (MOCK_TRADE_COUNT, mock_order_1, + mock_order_2, mock_order_2_sell, + mock_order_3, mock_order_3_sell, + mock_order_4, mock_order_5_stoploss, + mock_order_6_sell) def patch_RPCManager(mocker) -> MagicMock: @@ -4206,7 +4208,8 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order_ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limit_sell_order): default_conf['cancel_open_orders_on_exit'] = True mocker.patch('freqtrade.exchange.Exchange.fetch_order', - side_effect=[ExchangeError(), limit_sell_order, limit_buy_order, limit_sell_order, ]) + side_effect=[ + ExchangeError(), limit_sell_order, limit_buy_order, limit_sell_order]) buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_buy') sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_sell') From 85d90645c73a7fbef781c6afe998f8cfad84beca Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Sep 2020 15:42:34 +0200 Subject: [PATCH 0618/1197] Remove duplciate check for buy orders --- freqtrade/freqtradebot.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c20a57205..7d94b6cee 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -324,9 +324,6 @@ class FreqtradeBot: if fo and fo['status'] == 'open': # Assume this as the open order trade.open_order_id = order.order_id - else: - # No action for buy orders ... - continue if fo: logger.info(f"Found {order} for trade {trade}.jj") self.update_trade_state(trade, order.order_id, fo, From b8773de5b0d39163c9ded61e312578791ad9bb98 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Sep 2020 06:44:20 +0200 Subject: [PATCH 0619/1197] scoped sessions should be closed after requests --- freqtrade/rpc/api_server.py | 10 +++++++++- tests/rpc/test_rpc_apiserver.py | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 4bbc8a1dc..0ae0698cd 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -17,8 +17,9 @@ from werkzeug.serving import make_server from freqtrade.__init__ import __version__ from freqtrade.constants import DATETIME_PRINT_FORMAT -from freqtrade.rpc.rpc import RPC, RPCException +from freqtrade.persistence import Trade from freqtrade.rpc.fiat_convert import CryptoToFiatConverter +from freqtrade.rpc.rpc import RPC, RPCException logger = logging.getLogger(__name__) @@ -70,6 +71,11 @@ def rpc_catch_errors(func: Callable[..., Any]): return func_wrapper +def shutdown_session(exception=None): + # Remove scoped session + Trade.session.remove() + + class ApiServer(RPC): """ This class runs api server and provides rpc.rpc functionality to it @@ -104,6 +110,8 @@ class ApiServer(RPC): self.jwt = JWTManager(self.app) self.app.json_encoder = ArrowJSONEncoder + self.app.teardown_appcontext(shutdown_session) + # Register application handling self.register_rest_rpc_urls() diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index d9f5bf781..f8256f1ba 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -471,6 +471,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): assert rc.json == {"error": "Error querying _edge: Edge is not enabled."} +@pytest.mark.usefixtures("init_persistence") def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, limit_sell_order): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) @@ -498,6 +499,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li assert rc.json['best_pair'] == '' assert rc.json['best_rate'] == 0 + trade = Trade.query.first() trade.update(limit_sell_order) trade.close_date = datetime.utcnow() From 41942e3af1c93a7d1301724c4467863c124f3110 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Sep 2020 06:59:07 +0200 Subject: [PATCH 0620/1197] Update docstring for select_order --- freqtrade/persistence/models.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 79b3d491b..dc123a8d9 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -522,8 +522,10 @@ class Trade(_DECL_BASE): def select_order(self, order_side: str, status: Optional[str]) -> Optional[Order]: """ - Returns latest order for this orderside and status - Returns None if nothing is found + Finds latest order for this orderside and status + :param order_side: Side of the order (either 'buy' or 'sell') + :param status: Optionally filter on open / closed orders + :return: latest Order object if it exists, else None """ orders = [o for o in self.orders if o.side == order_side] if status: From aa8832f70e3b8fa8451664d7b802b2603213ebeb Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Sep 2020 07:12:10 +0200 Subject: [PATCH 0621/1197] Convert select_order to use ft_is_open flag --- freqtrade/freqtradebot.py | 6 ++--- freqtrade/persistence/models.py | 8 +++--- tests/test_freqtradebot.py | 2 +- tests/test_persistence.py | 45 +++++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7d94b6cee..71ced3212 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -260,7 +260,7 @@ class FreqtradeBot: if not trade.is_open and not trade.fee_updated('sell'): # Get sell fee - order = trade.select_order('sell', 'closed') + order = trade.select_order('sell', False) if order: logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.") self.update_trade_state(trade, order.order_id, @@ -269,7 +269,7 @@ class FreqtradeBot: trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() for trade in trades: if trade.is_open and not trade.fee_updated('buy'): - order = trade.select_order('buy', 'closed') + order = trade.select_order('buy', False) if order: logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") self.update_trade_state(trade, order.order_id) @@ -291,7 +291,7 @@ class FreqtradeBot: Handles trades where the initial fee-update did not work. """ logger.info(f"Trying to reupdate buy fees for {trade}") - order = trade.select_order('buy', 'closed') + order = trade.select_order('buy', False) if order: logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") self.update_trade_state(trade, order.order_id) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index dc123a8d9..5e7adfc74 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -520,16 +520,16 @@ class Trade(_DECL_BASE): profit_ratio = (close_trade_price / self.open_trade_price) - 1 return float(f"{profit_ratio:.8f}") - def select_order(self, order_side: str, status: Optional[str]) -> Optional[Order]: + def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]: """ Finds latest order for this orderside and status :param order_side: Side of the order (either 'buy' or 'sell') - :param status: Optionally filter on open / closed orders + :param is_open: Only search for open orders? :return: latest Order object if it exists, else None """ orders = [o for o in self.orders if o.side == order_side] - if status: - orders = [o for o in orders if o.status == status] + if is_open is not None: + orders = [o for o in orders if o.ft_is_open == is_open] if len(orders) > 0: return orders[-1] else: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 03b338bab..3c5621e7a 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4295,7 +4295,7 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee): for trade in trades: if trade.is_open: # Exclude Trade 4 - as the order is still open. - if trade.select_order('buy', 'closed'): + if trade.select_order('buy', False): assert trade.fee_open_cost is not None assert trade.fee_open_currency is not None else: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index d2da1c6a2..2bf2ce311 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1078,3 +1078,48 @@ def test_update_order_from_ccxt(): ccxt_order.update({'id': 'somethingelse'}) with pytest.raises(DependencyException, match=r"Order-id's don't match"): o.update_from_ccxt_object(ccxt_order) + + +@pytest.mark.usefixtures("init_persistence") +def test_select_order(fee): + create_mock_trades(fee) + + trades = Trade.get_trades().all() + + # Open buy order, no sell order + order = trades[0].select_order('buy', True) + assert order is None + order = trades[0].select_order('buy', False) + assert order is not None + order = trades[0].select_order('sell', None) + assert order is None + + # closed buy order, and open sell order + order = trades[1].select_order('buy', True) + assert order is None + order = trades[1].select_order('buy', False) + assert order is not None + order = trades[1].select_order('buy', None) + assert order is not None + order = trades[1].select_order('sell', True) + assert order is None + order = trades[1].select_order('sell', False) + assert order is not None + + # Has open buy order + order = trades[3].select_order('buy', True) + assert order is not None + order = trades[3].select_order('buy', False) + assert order is None + + # Open sell order + order = trades[4].select_order('buy', True) + assert order is None + order = trades[4].select_order('buy', False) + assert order is not None + + order = trades[4].select_order('sell', True) + assert order is not None + assert order.ft_order_side == 'stoploss' + order = trades[4].select_order('sell', False) + assert order is None From 0c9301e74a313a40e3829aed1ab701e04858d262 Mon Sep 17 00:00:00 2001 From: caudurodev Date: Fri, 11 Sep 2020 08:41:33 +0200 Subject: [PATCH 0622/1197] FIX: added missing ) for SQLite insert --- docs/sql_cheatsheet.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index cf785ced6..249e935ef 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -47,6 +47,7 @@ sqlite3 ```sql CREATE TABLE trades +( id INTEGER NOT NULL, exchange VARCHAR NOT NULL, pair VARCHAR NOT NULL, From 90d97c536d5b0a8ad7d863304023e418fe6f766e Mon Sep 17 00:00:00 2001 From: caudurodev Date: Fri, 11 Sep 2020 08:42:42 +0200 Subject: [PATCH 0623/1197] FIX: added missing ) for SQLite insert --- docs/sql_cheatsheet.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index 249e935ef..168d416ab 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -46,8 +46,7 @@ sqlite3 ### Trade table structure ```sql -CREATE TABLE trades -( +CREATE TABLE trades( id INTEGER NOT NULL, exchange VARCHAR NOT NULL, pair VARCHAR NOT NULL, From 50f0483d9aab4a0c73b6f7eb65d7babd3946817d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Sep 2020 20:00:36 +0200 Subject: [PATCH 0624/1197] FIx fluky test in test_api_logs --- tests/rpc/test_rpc_apiserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index f8256f1ba..626586a4a 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -435,7 +435,7 @@ def test_api_logs(botclient): assert len(rc.json) == 2 assert 'logs' in rc.json # Using a fixed comparison here would make this test fail! - assert rc.json['log_count'] > 10 + assert rc.json['log_count'] > 1 assert len(rc.json['logs']) == rc.json['log_count'] assert isinstance(rc.json['logs'][0], list) From 77c28187a6a63b250eecd7ed0a8151665efbff3e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Sep 2020 20:06:05 +0200 Subject: [PATCH 0625/1197] Don't run refind order on stoploss --- freqtrade/freqtradebot.py | 4 ++- tests/test_freqtradebot.py | 66 +++++++++++++++++++------------------- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 71ced3212..4ecafca67 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -897,7 +897,9 @@ class FreqtradeBot: except InsufficientFundsError as e: logger.warning(f"Unable to place stoploss order {e}.") # Try to figure out what went wrong - self.handle_insufficient_funds(trade) + # TODO: test without refinding order logic + # TODO: Also reenable the test test_create_stoploss_order_insufficient_funds + # self.handle_insufficient_funds(trade) except InvalidOrderException as e: trade.stoploss_order_id = None diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 3c5621e7a..90e398c91 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1333,42 +1333,42 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, assert rpc_mock.call_args_list[1][0][0]['order_type'] == 'market' -def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog, fee, - limit_buy_order_open, limit_sell_order): - sell_mock = MagicMock(return_value={'id': limit_sell_order['id']}) - freqtrade = get_patched_freqtradebot(mocker, default_conf) +# def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog, fee, +# limit_buy_order_open, limit_sell_order): +# sell_mock = MagicMock(return_value={'id': limit_sell_order['id']}) +# freqtrade = get_patched_freqtradebot(mocker, default_conf) - mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds') - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=MagicMock(return_value={ - 'bid': 0.00001172, - 'ask': 0.00001173, - 'last': 0.00001172 - }), - buy=MagicMock(return_value=limit_buy_order_open), - sell=sell_mock, - get_fee=fee, - fetch_order=MagicMock(return_value={'status': 'canceled'}), - stoploss=MagicMock(side_effect=InsufficientFundsError()), - ) - patch_get_signal(freqtrade) - freqtrade.strategy.order_types['stoploss_on_exchange'] = True +# mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds') +# mocker.patch.multiple( +# 'freqtrade.exchange.Exchange', +# fetch_ticker=MagicMock(return_value={ +# 'bid': 0.00001172, +# 'ask': 0.00001173, +# 'last': 0.00001172 +# }), +# buy=MagicMock(return_value=limit_buy_order_open), +# sell=sell_mock, +# get_fee=fee, +# fetch_order=MagicMock(return_value={'status': 'canceled'}), +# stoploss=MagicMock(side_effect=InsufficientFundsError()), +# ) +# patch_get_signal(freqtrade) +# freqtrade.strategy.order_types['stoploss_on_exchange'] = True - freqtrade.enter_positions() - trade = Trade.query.first() - caplog.clear() - freqtrade.create_stoploss_order(trade, 200) - # stoploss_orderid was empty before - assert trade.stoploss_order_id is None - assert mock_insuf.call_count == 1 - mock_insuf.reset_mock() +# freqtrade.enter_positions() +# trade = Trade.query.first() +# caplog.clear() +# freqtrade.create_stoploss_order(trade, 200) +# # stoploss_orderid was empty before +# assert trade.stoploss_order_id is None +# assert mock_insuf.call_count == 1 +# mock_insuf.reset_mock() - trade.stoploss_order_id = 'stoploss_orderid' - freqtrade.create_stoploss_order(trade, 200) - # No change to stoploss-orderid - assert trade.stoploss_order_id == 'stoploss_orderid' - assert mock_insuf.call_count == 1 +# trade.stoploss_order_id = 'stoploss_orderid' +# freqtrade.create_stoploss_order(trade, 200) +# # No change to stoploss-orderid +# assert trade.stoploss_order_id == 'stoploss_orderid' +# assert mock_insuf.call_count == 1 @pytest.mark.usefixtures("init_persistence") From 3f52b6d6d5115e7cfa22d8b1860104223e4a15e2 Mon Sep 17 00:00:00 2001 From: Blackhawke Date: Fri, 11 Sep 2020 12:01:45 -0700 Subject: [PATCH 0626/1197] Move "source" restored ".env/" --- docs/installation.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index da82d632f..153de7065 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -63,7 +63,7 @@ With this option, the script will install the bot and most dependencies: You will need to have git and python3.6+ installed beforehand for this to work. * Mandatory software as: `ta-lib` -* Setup your virtualenv: `source .env/bin/activate` +* Setup your virtualenv under `.env/` This option is a combination of installation tasks, `--reset` and `--config`. @@ -79,6 +79,12 @@ This option will hard reset your branch (only if you are on either `master` or ` DEPRECATED - use `freqtrade new-config -c config.json` instead. +** Setup your virtual environment ** + +Each time you open a new terminal, you must run `source .env/bin/activate` + + + ------ ## Custom Installation From ba8e93e2a11ce397dab9528b39a7529f7db82fcd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Sep 2020 16:43:22 +0200 Subject: [PATCH 0627/1197] Remove requirements-common.txt in an attempt to simplify installation --- Dockerfile | 2 +- Dockerfile.armhf | 2 +- requirements-common.txt | 37 ------------------------------------- requirements.txt | 39 ++++++++++++++++++++++++++++++++++++--- setup.py | 3 +-- 5 files changed, 39 insertions(+), 44 deletions(-) delete mode 100644 requirements-common.txt diff --git a/Dockerfile b/Dockerfile index e1220e3b8..22b0c43a7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib* ENV LD_LIBRARY_PATH /usr/local/lib # Install dependencies -COPY requirements.txt requirements-common.txt requirements-hyperopt.txt /freqtrade/ +COPY requirements.txt requirements-hyperopt.txt /freqtrade/ RUN pip install numpy --no-cache-dir \ && pip install -r requirements-hyperopt.txt --no-cache-dir diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 5c5e4a885..0633008ea 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -17,7 +17,7 @@ RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib* ENV LD_LIBRARY_PATH /usr/local/lib # Install dependencies -COPY requirements.txt requirements-common.txt /freqtrade/ +COPY requirements.txt /freqtrade/ RUN pip install numpy --no-cache-dir \ && pip install -r requirements.txt --no-cache-dir diff --git a/requirements-common.txt b/requirements-common.txt deleted file mode 100644 index d0f0ef370..000000000 --- a/requirements-common.txt +++ /dev/null @@ -1,37 +0,0 @@ -# requirements without requirements installable via conda -# mainly used for Raspberry pi installs -ccxt==1.34.11 -SQLAlchemy==1.3.19 -python-telegram-bot==12.8 -arrow==0.16.0 -cachetools==4.1.1 -requests==2.24.0 -urllib3==1.25.10 -wrapt==1.12.1 -jsonschema==3.2.0 -TA-Lib==0.4.18 -tabulate==0.8.7 -pycoingecko==1.3.0 -jinja2==2.11.2 -tables==3.6.1 -blosc==1.9.2 - -# find first, C search in arrays -py_find_1st==1.1.4 - -# Load ticker files 30% faster -python-rapidjson==0.9.1 - -# Notify systemd -sdnotify==0.3.2 - -# Api server -flask==1.1.2 -flask-jwt-extended==3.24.1 -flask-cors==3.0.9 - -# Support for colorized terminal output -colorama==0.4.3 -# Building config files interactively -questionary==1.5.2 -prompt-toolkit==3.0.7 diff --git a/requirements.txt b/requirements.txt index a1a1fb250..73ad7f4ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,38 @@ -# Load common requirements --r requirements-common.txt - numpy==1.19.1 pandas==1.1.2 + +ccxt==1.34.11 +SQLAlchemy==1.3.19 +python-telegram-bot==12.8 +arrow==0.16.0 +cachetools==4.1.1 +requests==2.24.0 +urllib3==1.25.10 +wrapt==1.12.1 +jsonschema==3.2.0 +TA-Lib==0.4.18 +tabulate==0.8.7 +pycoingecko==1.3.0 +jinja2==2.11.2 +tables==3.6.1 +blosc==1.9.2 + +# find first, C search in arrays +py_find_1st==1.1.4 + +# Load ticker files 30% faster +python-rapidjson==0.9.1 + +# Notify systemd +sdnotify==0.3.2 + +# Api server +flask==1.1.2 +flask-jwt-extended==3.24.1 +flask-cors==3.0.9 + +# Support for colorized terminal output +colorama==0.4.3 +# Building config files interactively +questionary==1.5.2 +prompt-toolkit==3.0.7 diff --git a/setup.py b/setup.py index 7213d3092..88d754668 100644 --- a/setup.py +++ b/setup.py @@ -62,7 +62,7 @@ setup(name='freqtrade', setup_requires=['pytest-runner', 'numpy'], tests_require=['pytest', 'pytest-asyncio', 'pytest-cov', 'pytest-mock', ], install_requires=[ - # from requirements-common.txt + # from requirements.txt 'ccxt>=1.24.96', 'SQLAlchemy', 'python-telegram-bot', @@ -82,7 +82,6 @@ setup(name='freqtrade', 'jinja2', 'questionary', 'prompt-toolkit', - # from requirements.txt 'numpy', 'pandas', 'tables', From d65d886d7de343d763c83f602fe9dc1d3da5ac39 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Sep 2020 16:54:21 +0200 Subject: [PATCH 0628/1197] add branch to CI - change branch-detection to support tags as well --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2c6141344..a50b0886e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,7 @@ on: - master - develop - github_actions_tests + - reduce_requirements_files tags: release: types: [published] @@ -226,7 +227,7 @@ jobs: - name: Extract branch name shell: bash - run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" + run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF##*/})" id: extract_branch - name: Build distribution From 07ccda9146ba8faf89ef509574b0183359e50255 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Sep 2020 16:59:45 +0200 Subject: [PATCH 0629/1197] Fix syntax for released CI --- .github/workflows/ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a50b0886e..16bf1959d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,10 +6,9 @@ on: - master - develop - github_actions_tests - - reduce_requirements_files tags: - release: - types: [published] + release: + types: [published] pull_request: schedule: - cron: '0 5 * * 4' From 60538368aca0c24bc03f5ac00c2daa67666ce04e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2020 06:35:00 +0000 Subject: [PATCH 0630/1197] Bump pytest from 6.0.1 to 6.0.2 Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.0.1 to 6.0.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/6.0.1...6.0.2) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 44f0c7265..aa7145053 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.8.3 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 mypy==0.782 -pytest==6.0.1 +pytest==6.0.2 pytest-asyncio==0.14.0 pytest-cov==2.10.1 pytest-mock==3.3.1 From 3c76945d5e5c0929fdae88f07b741a1ef6a2e2f9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2020 06:35:14 +0000 Subject: [PATCH 0631/1197] Bump numpy from 1.19.1 to 1.19.2 Bumps [numpy](https://github.com/numpy/numpy) from 1.19.1 to 1.19.2. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/master/doc/HOWTO_RELEASE.rst.txt) - [Commits](https://github.com/numpy/numpy/compare/v1.19.1...v1.19.2) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 73ad7f4ab..34b8381e5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.19.1 +numpy==1.19.2 pandas==1.1.2 ccxt==1.34.11 From 9d3caae9e3f302409e65dd75e151240bf5f3ea18 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2020 06:35:36 +0000 Subject: [PATCH 0632/1197] Bump plotly from 4.9.0 to 4.10.0 Bumps [plotly](https://github.com/plotly/plotly.py) from 4.9.0 to 4.10.0. - [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.9.0...v4.10.0) Signed-off-by: dependabot[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index 51d14d636..a91b3bd38 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.9.0 +plotly==4.10.0 From 23c1ae5d4af117d17da8bee6bee26d82cee80e82 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2020 06:35:41 +0000 Subject: [PATCH 0633/1197] Bump nbconvert from 5.6.1 to 6.0.2 Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 5.6.1 to 6.0.2. - [Release notes](https://github.com/jupyter/nbconvert/releases) - [Commits](https://github.com/jupyter/nbconvert/compare/5.6.1...6.0.2) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 44f0c7265..72d5a795d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -15,4 +15,4 @@ pytest-mock==3.3.1 pytest-random-order==1.0.4 # Convert jupyter notebooks to markdown documents -nbconvert==5.6.1 +nbconvert==6.0.2 From 6d30740b55f41d6d878e08ce120e2bfcc0b508d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Sep 2020 07:07:14 +0000 Subject: [PATCH 0634/1197] Bump ccxt from 1.34.11 to 1.34.25 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.34.11 to 1.34.25. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.34.11...1.34.25) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 34b8381e5..5da544a3c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.2 pandas==1.1.2 -ccxt==1.34.11 +ccxt==1.34.25 SQLAlchemy==1.3.19 python-telegram-bot==12.8 arrow==0.16.0 From 962fed24b090b76f4bac0468b949fea493b5d333 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Sep 2020 17:34:13 +0200 Subject: [PATCH 0635/1197] Readd refind_order logic --- freqtrade/freqtradebot.py | 4 +-- tests/test_freqtradebot.py | 66 +++++++++++++++++++------------------- 2 files changed, 34 insertions(+), 36 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4ecafca67..71ced3212 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -897,9 +897,7 @@ class FreqtradeBot: except InsufficientFundsError as e: logger.warning(f"Unable to place stoploss order {e}.") # Try to figure out what went wrong - # TODO: test without refinding order logic - # TODO: Also reenable the test test_create_stoploss_order_insufficient_funds - # self.handle_insufficient_funds(trade) + self.handle_insufficient_funds(trade) except InvalidOrderException as e: trade.stoploss_order_id = None diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 90e398c91..3c5621e7a 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1333,42 +1333,42 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, assert rpc_mock.call_args_list[1][0][0]['order_type'] == 'market' -# def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog, fee, -# limit_buy_order_open, limit_sell_order): -# sell_mock = MagicMock(return_value={'id': limit_sell_order['id']}) -# freqtrade = get_patched_freqtradebot(mocker, default_conf) +def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog, fee, + limit_buy_order_open, limit_sell_order): + sell_mock = MagicMock(return_value={'id': limit_sell_order['id']}) + freqtrade = get_patched_freqtradebot(mocker, default_conf) -# mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds') -# mocker.patch.multiple( -# 'freqtrade.exchange.Exchange', -# fetch_ticker=MagicMock(return_value={ -# 'bid': 0.00001172, -# 'ask': 0.00001173, -# 'last': 0.00001172 -# }), -# buy=MagicMock(return_value=limit_buy_order_open), -# sell=sell_mock, -# get_fee=fee, -# fetch_order=MagicMock(return_value={'status': 'canceled'}), -# stoploss=MagicMock(side_effect=InsufficientFundsError()), -# ) -# patch_get_signal(freqtrade) -# freqtrade.strategy.order_types['stoploss_on_exchange'] = True + mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds') + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=MagicMock(return_value={ + 'bid': 0.00001172, + 'ask': 0.00001173, + 'last': 0.00001172 + }), + buy=MagicMock(return_value=limit_buy_order_open), + sell=sell_mock, + get_fee=fee, + fetch_order=MagicMock(return_value={'status': 'canceled'}), + stoploss=MagicMock(side_effect=InsufficientFundsError()), + ) + patch_get_signal(freqtrade) + freqtrade.strategy.order_types['stoploss_on_exchange'] = True -# freqtrade.enter_positions() -# trade = Trade.query.first() -# caplog.clear() -# freqtrade.create_stoploss_order(trade, 200) -# # stoploss_orderid was empty before -# assert trade.stoploss_order_id is None -# assert mock_insuf.call_count == 1 -# mock_insuf.reset_mock() + freqtrade.enter_positions() + trade = Trade.query.first() + caplog.clear() + freqtrade.create_stoploss_order(trade, 200) + # stoploss_orderid was empty before + assert trade.stoploss_order_id is None + assert mock_insuf.call_count == 1 + mock_insuf.reset_mock() -# trade.stoploss_order_id = 'stoploss_orderid' -# freqtrade.create_stoploss_order(trade, 200) -# # No change to stoploss-orderid -# assert trade.stoploss_order_id == 'stoploss_orderid' -# assert mock_insuf.call_count == 1 + trade.stoploss_order_id = 'stoploss_orderid' + freqtrade.create_stoploss_order(trade, 200) + # No change to stoploss-orderid + assert trade.stoploss_order_id == 'stoploss_orderid' + assert mock_insuf.call_count == 1 @pytest.mark.usefixtures("init_persistence") From ec01f20bf85a8b960bf607ec5f224accc52194a1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Sep 2020 20:26:55 +0200 Subject: [PATCH 0636/1197] Add ratio to sell reason stats --- freqtrade/optimize/optimize_reports.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index b5e5da4af..771ac91fb 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -122,7 +122,7 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List profit_mean = result['profit_percent'].mean() profit_sum = result["profit_percent"].sum() - profit_percent_tot = round(result['profit_percent'].sum() * 100.0 / max_open_trades, 2) + profit_percent_tot = result['profit_percent'].sum() / max_open_trades tabular_data.append( { @@ -136,7 +136,8 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List 'profit_sum': profit_sum, 'profit_sum_pct': round(profit_sum * 100, 2), 'profit_total_abs': result['profit_abs'].sum(), - 'profit_total_pct': profit_percent_tot, + 'profit_total': profit_percent_tot, + 'profit_total_pct': round(profit_percent_tot * 100, 2), } ) return tabular_data From dd87938a5eb8ae78c371d2a0c95fce686c217dab Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 08:33:12 +0200 Subject: [PATCH 0637/1197] Fix bug causing close_date to be set again --- freqtrade/persistence/models.py | 11 +++++++---- tests/test_persistence.py | 31 ++++++++++++++++++------------- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 5e7adfc74..816e23fd3 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -383,15 +383,18 @@ class Trade(_DECL_BASE): self.open_rate = Decimal(safe_value_fallback(order, 'average', 'price')) self.amount = Decimal(safe_value_fallback(order, 'filled', 'amount')) self.recalc_open_trade_price() - logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.') + if self.is_open: + logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.') self.open_order_id = None elif order_type in ('market', 'limit') and order['side'] == 'sell': + if self.is_open: + logger.info(f'{order_type.upper()}_SELL has been fulfilled for {self}.') self.close(safe_value_fallback(order, 'average', 'price')) - logger.info(f'{order_type.upper()}_SELL has been fulfilled for {self}.') elif order_type in ('stop_loss_limit', 'stop-loss', 'stop'): self.stoploss_order_id = None self.close_rate_requested = self.stop_loss - logger.info(f'{order_type.upper()} is hit for {self}.') + if self.is_open: + logger.info(f'{order_type.upper()} is hit for {self}.') self.close(order['average']) else: raise ValueError(f'Unknown order type: {order_type}') @@ -405,7 +408,7 @@ class Trade(_DECL_BASE): self.close_rate = Decimal(rate) self.close_profit = self.calc_profit_ratio() self.close_profit_abs = self.calc_profit() - self.close_date = datetime.utcnow() + self.close_date = self.close_date or datetime.utcnow() self.is_open = False self.sell_order_status = 'closed' self.open_order_id = None diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 2bf2ce311..723143cfc 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -9,7 +9,7 @@ from sqlalchemy import create_engine from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.persistence import Order, Trade, clean_dry_run_db, init -from tests.conftest import create_mock_trades, log_has +from tests.conftest import create_mock_trades, log_has, log_has_re def test_init_create_session(default_conf): @@ -93,6 +93,8 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog): stake_amount=0.001, open_rate=0.01, amount=5, + is_open=True, + open_date=arrow.utcnow().datetime, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -107,9 +109,9 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog): assert trade.open_rate == 0.00001099 assert trade.close_profit is None assert trade.close_date is None - assert log_has("LIMIT_BUY has been fulfilled for Trade(id=2, " - "pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=closed).", - caplog) + assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=2, " + r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=.*\).", + caplog) caplog.clear() trade.open_order_id = 'something' @@ -118,9 +120,9 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog): assert trade.close_rate == 0.00001173 assert trade.close_profit == 0.06201058 assert trade.close_date is not None - assert log_has("LIMIT_SELL has been fulfilled for Trade(id=2, " - "pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=closed).", - caplog) + assert log_has_re(r"LIMIT_SELL has been fulfilled for Trade\(id=2, " + r"pair=ETH/BTC, amount=90.99181073, open_rate=0.00001099, open_since=.*\).", + caplog) @pytest.mark.usefixtures("init_persistence") @@ -131,8 +133,10 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog): stake_amount=0.001, amount=5, open_rate=0.01, + is_open=True, fee_open=fee.return_value, fee_close=fee.return_value, + open_date=arrow.utcnow().datetime, exchange='bittrex', ) @@ -142,20 +146,21 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog): assert trade.open_rate == 0.00004099 assert trade.close_profit is None assert trade.close_date is None - assert log_has("MARKET_BUY has been fulfilled for Trade(id=1, " - "pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=closed).", - caplog) + assert log_has_re(r"MARKET_BUY has been fulfilled for Trade\(id=1, " + r"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=.*\).", + caplog) caplog.clear() + trade.is_open = True trade.open_order_id = 'something' trade.update(market_sell_order) assert trade.open_order_id is None assert trade.close_rate == 0.00004173 assert trade.close_profit == 0.01297561 assert trade.close_date is not None - assert log_has("MARKET_SELL has been fulfilled for Trade(id=1, " - "pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=closed).", - caplog) + assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, " + r"pair=ETH/BTC, amount=91.99181073, open_rate=0.00004099, open_since=.*\).", + caplog) @pytest.mark.usefixtures("init_persistence") From 254875e6b362fd8acb2aff51289b5d6fad8adbd6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 08:42:15 +0200 Subject: [PATCH 0638/1197] Add test for new close functionality * Don't updates close_date if the trade was already closed --- tests/test_persistence.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 723143cfc..adfa18876 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -189,6 +189,36 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): assert trade.calc_profit_ratio() == 0.06201058 +@pytest.mark.usefixtures("init_persistence") +def test_trade_close(limit_buy_order, limit_sell_order, fee): + trade = Trade( + pair='ETH/BTC', + stake_amount=0.001, + open_rate=0.01, + amount=5, + is_open=True, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_date=arrow.Arrow(2020, 2, 1, 15, 5, 1).datetime, + exchange='bittrex', + ) + assert trade.close_profit is None + assert trade.close_date is None + assert trade.is_open is True + trade.close(0.02) + assert trade.is_open is False + assert trade.close_profit == 0.99002494 + assert trade.close_date is not None + + new_date = arrow.Arrow(2020, 2, 2, 15, 6, 1).datetime, + assert trade.close_date != new_date + # Close should NOT update close_date if the trade has been closed already + assert trade.is_open is False + trade.close_date = new_date + trade.close(0.02) + assert trade.close_date == new_date + + @pytest.mark.usefixtures("init_persistence") def test_calc_close_trade_price_exception(limit_buy_order, fee): trade = Trade( From bfd0e3553accf22775de52e4aec00b038de33116 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 08:42:37 +0200 Subject: [PATCH 0639/1197] Don't build this branch anymore in CI --- .github/workflows/ci.yml | 2 -- freqtrade/exchange/common.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81e450c0f..2bb7b195a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,8 +5,6 @@ on: branches: - master - develop - - github_actions_tests - - db_keep_orders tags: release: types: [published] diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 2abac9286..9abd42aa7 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -12,7 +12,7 @@ logger = logging.getLogger(__name__) # Maximum default retry count. # Functions are always called RETRY_COUNT + 1 times (for the original call) API_RETRY_COUNT = 4 -API_FETCH_ORDER_RETRY_COUNT = 3 +API_FETCH_ORDER_RETRY_COUNT = 5 BAD_EXCHANGES = { "bitmex": "Various reasons.", From 35857b3ddea9c4a73332a38677db533cacd8e55b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 09:10:34 +0200 Subject: [PATCH 0640/1197] Datetime should support --timerange too --- freqtrade/commands/arguments.py | 2 +- freqtrade/commands/data_commands.py | 6 ++++ tests/commands/test_commands.py | 45 +++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 7268c3c8f..498c73fb3 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -57,7 +57,7 @@ ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"] ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"] ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchange", - "timeframes", "erase", "dataformat_ohlcv", "dataformat_trades"] + "timeframes", "erase", "dataformat_ohlcv", "dataformat_trades", "timerange"] ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", "trade_source", "export", "exportfilename", diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index da1eb0cf5..956a8693e 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -25,11 +25,17 @@ def start_download_data(args: Dict[str, Any]) -> None: """ config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) + if 'days' in config and 'timerange' in config: + raise OperationalException("--days and --timerange are mutually exclusive. " + "You can only specify one or the other.") timerange = TimeRange() if 'days' in config: time_since = arrow.utcnow().shift(days=-config['days']).strftime("%Y%m%d") timerange = TimeRange.parse_timerange(f'{time_since}-') + if 'timerange' in config: + timerange = timerange.parse_timerange(config['timerange']) + if 'pairs' not in config: raise OperationalException( "Downloading data requires a list of pairs. " diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 26875fb7f..99b7139b0 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -2,6 +2,7 @@ import re from pathlib import Path from unittest.mock import MagicMock, PropertyMock +import arrow import pytest from freqtrade.commands import (start_convert_data, start_create_userdir, @@ -552,6 +553,50 @@ def test_download_data_keyboardInterrupt(mocker, caplog, markets): assert dl_mock.call_count == 1 +def test_download_data_timerange(mocker, caplog, markets): + dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', + MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) + patch_exchange(mocker) + mocker.patch( + 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) + ) + args = [ + "download-data", + "--exchange", "binance", + "--pairs", "ETH/BTC", "XRP/BTC", + "--days", "20", + "--timerange", "20200101-" + ] + with pytest.raises(OperationalException, + match=r"--days and --timerange are mutually.*"): + start_download_data(get_args(args)) + assert dl_mock.call_count == 0 + + args = [ + "download-data", + "--exchange", "binance", + "--pairs", "ETH/BTC", "XRP/BTC", + "--days", "20", + ] + start_download_data(get_args(args)) + assert dl_mock.call_count == 1 + # 20days ago + days_ago = arrow.get(arrow.utcnow().shift(days=-20).date()).timestamp + assert dl_mock.call_args_list[0][1]['timerange'].startts == days_ago + + dl_mock.reset_mock() + args = [ + "download-data", + "--exchange", "binance", + "--pairs", "ETH/BTC", "XRP/BTC", + "--timerange", "20200101-" + ] + start_download_data(get_args(args)) + assert dl_mock.call_count == 1 + + assert dl_mock.call_args_list[0][1]['timerange'].startts == arrow.Arrow(2020, 1, 1).timestamp + + def test_download_data_no_markets(mocker, caplog): dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) From 2f6b00555a09da306f321a2effef242e958a60da Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 09:13:43 +0200 Subject: [PATCH 0641/1197] Document support for --timerange in download-data --- docs/data-download.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/data-download.md b/docs/data-download.md index 0b22ec9ce..932b84557 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -8,9 +8,11 @@ If no additional parameter is specified, freqtrade will download data for `"1m"` Exchange and pairs will come from `config.json` (if specified using `-c/--config`). Otherwise `--exchange` becomes mandatory. +You can use a relative timerange (`--days 20`) or an absolute starting point (`--timerange 20200101`). For incremental downloads, the relative approach should be used. + !!! Tip "Tip: Updating existing data" If you already have backtesting data available in your data-directory and would like to refresh this data up to today, use `--days xx` with a number slightly higher than the missing number of days. Freqtrade will keep the available data and only download the missing data. - Be carefull though: If the number is too small (which would result in a few missing days), the whole dataset will be removed and only xx days will be downloaded. + Be careful though: If the number is too small (which would result in a few missing days), the whole dataset will be removed and only xx days will be downloaded. ### Usage @@ -24,6 +26,7 @@ usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [--erase] [--data-format-ohlcv {json,jsongz,hdf5}] [--data-format-trades {json,jsongz,hdf5}] + [--timerange TIMERANGE] optional arguments: -h, --help show this help message and exit @@ -48,6 +51,8 @@ optional arguments: --data-format-trades {json,jsongz,hdf5} Storage format for downloaded trades data. (default: `jsongz`). + --timerange TIMERANGE + Specify what timerange of data to use. Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). @@ -286,6 +291,7 @@ This will download historical candle (OHLCV) data for all the currency pairs you - To change the exchange used to download the historical data from, please use a different configuration file (you'll probably need to adjust rate limits etc.) - To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`. - To download historical candle (OHLCV) data for only 10 days, use `--days 10` (defaults to 30 days). +- To download historical candle (OHLCV) data from a fixed starting point, use `--timerange 20200101-` - which will download all data from January 1st, 2020. - Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data. - To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options. From 77d01896954c69cd10f8097713b3b39ce3dc7284 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 09:37:11 +0200 Subject: [PATCH 0642/1197] Remove not needed argument in update_trade_state --- freqtrade/data/history/history_utils.py | 1 - freqtrade/freqtradebot.py | 18 +++++++----------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index dd09c4c05..ac234a72e 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -136,7 +136,6 @@ def _load_cached_data_for_updating(pair: str, timeframe: str, timerange: Optiona start = None if timerange: if timerange.starttype == 'date': - # TODO: convert to date for conversion start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc) # Intentionally don't pass timerange in - since we need to load the full dataset. diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 71ced3212..eec09a17c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -307,8 +307,7 @@ class FreqtradeBot: logger.info(f"Trying to refind {order}") fo = None if not order.ft_is_open: - # TODO: Does this need to be info level? - logger.info(f"Order {order} is no longer open.") + logger.debug(f"Order {order} is no longer open.") continue if order.ft_order_side == 'buy': # Skip buy side - this is handled by reupdate_buy_order_fees @@ -1125,7 +1124,7 @@ class FreqtradeBot: # we need to fall back to the values from order if corder does not contain these keys. trade.amount = filled_amount trade.stake_amount = trade.amount * trade.open_rate - self.update_trade_state(trade, trade.open_order_id, corder, trade.amount) + self.update_trade_state(trade, trade.open_order_id, corder) trade.open_order_id = None logger.info('Partial buy order timeout for %s.', trade) @@ -1357,16 +1356,14 @@ class FreqtradeBot: # Common update trade state methods # - def update_trade_state(self, trade: Trade, order_id: str, action_order: dict = None, - order_amount: float = None, stoploss_order: bool = False) -> bool: + def update_trade_state(self, trade: Trade, order_id: str, action_order: Dict[str, Any] = None, + stoploss_order: bool = False) -> bool: """ Checks trades with open orders and updates the amount if necessary Handles closing both buy and sell orders. :param trade: Trade object of the trade we're analyzing :param order_id: Order-id of the order we're analyzing :param action_order: Already aquired order object - :param order_amount: Order-amount - only used in case of partially cancelled buy order - TODO: Investigate if this is really needed, or covered by getting filled in here again. :return: True if order has been cancelled without being filled partially, False otherwise """ if not order_id: @@ -1387,7 +1384,7 @@ class FreqtradeBot: # Try update amount (binance-fix) try: - new_amount = self.get_real_amount(trade, order, order_amount) + new_amount = self.get_real_amount(trade, order) if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount, abs_tol=constants.MATH_CLOSE_PREC): order['amount'] = new_amount @@ -1425,7 +1422,7 @@ class FreqtradeBot: return real_amount return amount - def get_real_amount(self, trade: Trade, order: Dict, order_amount: float = None) -> float: + def get_real_amount(self, trade: Trade, order: Dict) -> float: """ Detect and update trade fee. Calls trade.update_fee() uppon correct detection. @@ -1434,8 +1431,7 @@ class FreqtradeBot: :return: identical (or new) amount for the trade """ # Init variables - if order_amount is None: - order_amount = safe_value_fallback(order, 'filled', 'amount') + order_amount = safe_value_fallback(order, 'filled', 'amount') # Only run for closed orders if trade.fee_updated(order.get('side', '')) or order['status'] == 'open': return order_amount From 1f086e1466e80cb138227bf42fe75360c199504d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 09:46:32 +0200 Subject: [PATCH 0643/1197] Modify test loglevel --- tests/test_freqtradebot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 3c5621e7a..0c12c05bb 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4384,6 +4384,7 @@ def test_handle_insufficient_funds(mocker, default_conf, fee): @pytest.mark.usefixtures("init_persistence") def test_refind_lost_order(mocker, default_conf, fee, caplog): + caplog.set_level(logging.DEBUG) freqtrade = get_patched_freqtradebot(mocker, default_conf) mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') From 5daaed144949e8ffde666c2e7fc22b9e9db85ee9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 11:25:00 +0200 Subject: [PATCH 0644/1197] Help endpoint does not make sense for the rest api server. therefore, remove the TODO. --- freqtrade/rpc/api_server.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 0ae0698cd..db22ce453 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -222,9 +222,6 @@ class ApiServer(RPC): self.app.add_url_rule(f'{BASE_URI}/forcesell', 'forcesell', view_func=self._forcesell, methods=['POST']) - # TODO: Implement the following - # help (?) - @require_login def page_not_found(self, error): """ From bf95fe2e5cee112fddf83e8d9b09cfa5d3e9e916 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 11:33:55 +0200 Subject: [PATCH 0645/1197] have the 2 timerange arguments next to each other --- docs/data-download.md | 9 ++++----- freqtrade/commands/arguments.py | 4 ++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index 932b84557..9065bb050 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -20,13 +20,12 @@ You can use a relative timerange (`--days 20`) or an absolute starting point (`- usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-p PAIRS [PAIRS ...]] [--pairs-file FILE] - [--days INT] [--dl-trades] - [--exchange EXCHANGE] + [--days INT] [--timerange TIMERANGE] + [--dl-trades] [--exchange EXCHANGE] [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} ...]] [--erase] [--data-format-ohlcv {json,jsongz,hdf5}] [--data-format-trades {json,jsongz,hdf5}] - [--timerange TIMERANGE] optional arguments: -h, --help show this help message and exit @@ -35,6 +34,8 @@ optional arguments: separated. --pairs-file FILE File containing a list of pairs to download. --days INT Download data for given number of days. + --timerange TIMERANGE + Specify what timerange of data to use. --dl-trades Download trades instead of OHLCV data. The bot will resample trades to the desired timeframe as specified as --timeframes/-t. @@ -51,8 +52,6 @@ optional arguments: --data-format-trades {json,jsongz,hdf5} Storage format for downloaded trades data. (default: `jsongz`). - --timerange TIMERANGE - Specify what timerange of data to use. Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 498c73fb3..b61a4933e 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -56,8 +56,8 @@ ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"] ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"] -ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchange", - "timeframes", "erase", "dataformat_ohlcv", "dataformat_trades", "timerange"] +ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "timerange", "download_trades", "exchange", + "timeframes", "erase", "dataformat_ohlcv", "dataformat_trades"] ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", "trade_source", "export", "exportfilename", From 476319da45b017bdc09e7691a93102ab4481583d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 17:21:56 +0200 Subject: [PATCH 0646/1197] Clarify --timerange documentation --- docs/data-download.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data-download.md b/docs/data-download.md index 9065bb050..9f0486262 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -290,7 +290,7 @@ This will download historical candle (OHLCV) data for all the currency pairs you - To change the exchange used to download the historical data from, please use a different configuration file (you'll probably need to adjust rate limits etc.) - To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`. - To download historical candle (OHLCV) data for only 10 days, use `--days 10` (defaults to 30 days). -- To download historical candle (OHLCV) data from a fixed starting point, use `--timerange 20200101-` - which will download all data from January 1st, 2020. +- To download historical candle (OHLCV) data from a fixed starting point, use `--timerange 20200101-` - which will download all data from January 1st, 2020. Eventually set end dates are ignored. - Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data. - To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options. From f0d7f18cf98c9ebd6d2b381fd9e092cb824f47af Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 17:32:22 +0200 Subject: [PATCH 0647/1197] Pad wins / draws / losses for hyperopt with spaces instead of 0's --- freqtrade/optimize/hyperopt.py | 7 ++++--- tests/optimize/test_hyperopt.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 6d29be08e..37de3bc4b 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -324,8 +324,9 @@ class Hyperopt: 'results_metrics.avg_profit', 'results_metrics.total_profit', 'results_metrics.profit', 'results_metrics.duration', 'loss', 'is_initial_point', 'is_best']] - trials.columns = ['Best', 'Epoch', 'Trades', 'W/D/L', 'Avg profit', 'Total profit', - 'Profit', 'Avg duration', 'Objective', 'is_initial_point', 'is_best'] + trials.columns = ['Best', 'Epoch', 'Trades', ' Win Draw Loss', 'Avg profit', + 'Total profit', 'Profit', 'Avg duration', 'Objective', + 'is_initial_point', 'is_best'] trials['is_profit'] = False trials.loc[trials['is_initial_point'], 'Best'] = '* ' trials.loc[trials['is_best'], 'Best'] = 'Best' @@ -574,7 +575,7 @@ class Hyperopt: 'wins': wins, 'draws': draws, 'losses': losses, - 'winsdrawslosses': f"{wins:04}/{draws:04}/{losses:04}", + 'winsdrawslosses': f"{wins:>4} {draws:>4} {losses:>4}", 'avg_profit': backtesting_results.profit_percent.mean() * 100.0, 'median_profit': backtesting_results.profit_percent.median() * 100.0, 'total_profit': backtesting_results.profit_abs.sum(), diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index e7a26cd5f..d58b91209 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -813,7 +813,7 @@ def test_generate_optimizer(mocker, hyperopt_conf) -> None: 'draws': 0, 'duration': 100.0, 'losses': 0, - 'winsdrawslosses': '0001/0000/0000', + 'winsdrawslosses': ' 1 0 0', 'median_profit': 2.3117, 'profit': 2.3117, 'total_profit': 0.000233, From 2a7935e35e55cf6daeeb937a0f16ff1bfcdaa8fc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 17:51:31 +0200 Subject: [PATCH 0648/1197] Rename custom_notification to startup_notification --- freqtrade/rpc/rpc.py | 2 +- freqtrade/rpc/rpc_manager.py | 4 ++-- freqtrade/rpc/telegram.py | 2 +- freqtrade/rpc/webhook.py | 2 +- tests/rpc/test_rpc_manager.py | 4 ++-- tests/rpc/test_rpc_telegram.py | 4 ++-- tests/rpc/test_rpc_webhook.py | 2 +- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index f4e5d3b8e..649dd0f58 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -27,7 +27,7 @@ logger = logging.getLogger(__name__) class RPCMessageType(Enum): STATUS_NOTIFICATION = 'status' WARNING_NOTIFICATION = 'warning' - CUSTOM_NOTIFICATION = 'custom' + STARTUP_NOTIFICATION = 'startup' BUY_NOTIFICATION = 'buy' BUY_CANCEL_NOTIFICATION = 'buy_cancel' SELL_NOTIFICATION = 'sell' diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 2cb44fec8..a9038d81c 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -76,7 +76,7 @@ class RPCManager: exchange_name = config['exchange']['name'] strategy_name = config.get('strategy', '') self.send_msg({ - 'type': RPCMessageType.CUSTOM_NOTIFICATION, + 'type': RPCMessageType.STARTUP_NOTIFICATION, 'status': f'*Exchange:* `{exchange_name}`\n' f'*Stake per trade:* `{stake_amount} {stake_currency}`\n' f'*Minimum ROI:* `{minimal_roi}`\n' @@ -85,7 +85,7 @@ class RPCManager: f'*Strategy:* `{strategy_name}`' }) self.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, + 'type': RPCMessageType.STARTUP_NOTIFICATION, 'status': f'Searching for {stake_currency} pairs to buy and sell ' f'based on {pairlist.short_desc()}' }) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a01efaed6..6d529a69f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -190,7 +190,7 @@ class Telegram(RPC): elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION: message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg) - elif msg['type'] == RPCMessageType.CUSTOM_NOTIFICATION: + elif msg['type'] == RPCMessageType.STARTUP_NOTIFICATION: message = '{status}'.format(**msg) else: diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 322d990ee..7235e7c15 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -48,7 +48,7 @@ class Webhook(RPC): elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: valuedict = self._config['webhook'].get('webhooksellcancel', None) elif msg['type'] in (RPCMessageType.STATUS_NOTIFICATION, - RPCMessageType.CUSTOM_NOTIFICATION, + RPCMessageType.STARTUP_NOTIFICATION, RPCMessageType.WARNING_NOTIFICATION): valuedict = self._config['webhook'].get('webhookstatus', None) else: diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index edf6bae4d..5972bf864 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -124,10 +124,10 @@ def test_send_msg_webhook_CustomMessagetype(mocker, default_conf, caplog) -> Non rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules] - rpc_manager.send_msg({'type': RPCMessageType.CUSTOM_NOTIFICATION, + rpc_manager.send_msg({'type': RPCMessageType.STARTUP_NOTIFICATION, 'status': 'TestMessage'}) assert log_has( - "Message type RPCMessageType.CUSTOM_NOTIFICATION not implemented by handler webhook.", + "Message type RPCMessageType.STARTUP_NOTIFICATION not implemented by handler webhook.", caplog) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 6feacd4bd..20f478b8a 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1485,7 +1485,7 @@ def test_warning_notification(default_conf, mocker) -> None: assert msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Warning:* `message`' -def test_custom_notification(default_conf, mocker) -> None: +def test_startup_notification(default_conf, mocker) -> None: msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -1495,7 +1495,7 @@ def test_custom_notification(default_conf, mocker) -> None: freqtradebot = get_patched_freqtradebot(mocker, default_conf) telegram = Telegram(freqtradebot) telegram.send_msg({ - 'type': RPCMessageType.CUSTOM_NOTIFICATION, + 'type': RPCMessageType.STARTUP_NOTIFICATION, 'status': '*Custom:* `Hello World`' }) assert msg_mock.call_args[0][0] == '*Custom:* `Hello World`' diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 1ced62746..2d2c5a4f4 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -150,7 +150,7 @@ def test_send_msg(default_conf, mocker): default_conf["webhook"]["webhooksellcancel"]["value3"].format(**msg)) for msgtype in [RPCMessageType.STATUS_NOTIFICATION, RPCMessageType.WARNING_NOTIFICATION, - RPCMessageType.CUSTOM_NOTIFICATION]: + RPCMessageType.STARTUP_NOTIFICATION]: # Test notification msg = { 'type': msgtype, From e53b88bde38b4309cef663436020e50e873716fe Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 19:38:33 +0200 Subject: [PATCH 0649/1197] Introduce notification_settings for telegram --- config_full.json.example | 11 ++++++++++- freqtrade/constants.py | 14 ++++++++++++++ freqtrade/rpc/telegram.py | 18 ++++++++++++++---- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index d5bfd3fe1..659580fb1 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -116,7 +116,16 @@ "telegram": { "enabled": true, "token": "your_telegram_token", - "chat_id": "your_telegram_chat_id" + "chat_id": "your_telegram_chat_id", + "notification_settings": { + "status": "on", + "warning": "on", + "startup": "on", + "buy": "on", + "sell": "on", + "buy_cancel": "on", + "sell_cancel": "on" + } }, "api_server": { "enabled": false, diff --git a/freqtrade/constants.py b/freqtrade/constants.py index c71b94bcb..de663bd4b 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -39,6 +39,8 @@ USERPATH_HYPEROPTS = 'hyperopts' USERPATH_STRATEGIES = 'strategies' USERPATH_NOTEBOOKS = 'notebooks' +TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent'] + # Soure files with destination directories within user-directory USER_DATA_FILES = { 'sample_strategy.py': USERPATH_STRATEGIES, @@ -201,6 +203,18 @@ CONF_SCHEMA = { 'enabled': {'type': 'boolean'}, 'token': {'type': 'string'}, 'chat_id': {'type': 'string'}, + 'notification_settings': { + 'type': 'object', + 'properties': { + 'status': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, + 'warning': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, + 'startup': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, + 'buy': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, + 'sell': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, + 'buy_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, + 'sell_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS} + } + } }, 'required': ['enabled', 'token', 'chat_id'] }, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 6d529a69f..905ed6755 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -132,6 +132,13 @@ class Telegram(RPC): def send_msg(self, msg: Dict[str, Any]) -> None: """ Send a message to telegram channel """ + noti = self._config['telegram'].get('notification_settings', {} + ).get(msg['type'].value, 'on') + if noti == 'off': + logger.info(f"Notification {msg['type']} not sent.") + # Notification disabled + return + if msg['type'] == RPCMessageType.BUY_NOTIFICATION: if self._fiat_converter: msg['stake_amount_fiat'] = self._fiat_converter.convert_amount( @@ -196,7 +203,7 @@ class Telegram(RPC): else: raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) - self._send_msg(message) + self._send_msg(message, disable_notification=(noti == 'silent')) def _get_sell_emoji(self, msg): """ @@ -773,7 +780,8 @@ class Telegram(RPC): f"*Current state:* `{val['state']}`" ) - def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: + def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN, + disable_notification: bool = False) -> None: """ Send given markdown message :param msg: message @@ -794,7 +802,8 @@ class Telegram(RPC): self._config['telegram']['chat_id'], text=msg, parse_mode=parse_mode, - reply_markup=reply_markup + reply_markup=reply_markup, + disable_notification=disable_notification, ) except NetworkError as network_err: # Sometimes the telegram server resets the current connection, @@ -807,7 +816,8 @@ class Telegram(RPC): self._config['telegram']['chat_id'], text=msg, parse_mode=parse_mode, - reply_markup=reply_markup + reply_markup=reply_markup, + disable_notification=disable_notification, ) except TelegramError as telegram_err: logger.warning( From 413d7ddf70f5750858d2bafb618deceb1ae01d39 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 19:42:56 +0200 Subject: [PATCH 0650/1197] Document telegram notification settings --- docs/telegram-usage.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 5f804386d..b718d40a6 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -41,6 +41,34 @@ Talk to the [userinfobot](https://telegram.me/userinfobot) Get your "Id", you will use it for the config parameter `chat_id`. +## Control telegram noise + +Freqtrade provides means to control the verbosity of your telegram bot. +Each setting has the follwoing possible values: + +* `on` - Messages will be sent, and user will be notified. +* `silent` - Message will be sent, Notification will be without sound / vibration. +* `off` - Skip sending a message-type all together. + +Example configuration showing the different settings: + +``` json +"telegram": { + "enabled": true, + "token": "your_telegram_token", + "chat_id": "your_telegram_chat_id", + "notification_settings": { + "status": "silent", + "warning": "on", + "startup": "off", + "buy": "silent", + "sell": "on", + "buy_cancel": "silent", + "sell_cancel": "on" + } + }, +``` + ## Telegram commands Per default, the Telegram bot shows predefined commands. Some commands From 2554dc48e4958cfb38733d1ce3c49cf6c256f9a0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 20:04:12 +0200 Subject: [PATCH 0651/1197] Add test for notification settings --- freqtrade/rpc/rpc.py | 3 +++ freqtrade/rpc/rpc_manager.py | 2 +- freqtrade/rpc/telegram.py | 4 ++-- freqtrade/rpc/webhook.py | 2 +- tests/rpc/test_rpc_manager.py | 2 +- tests/rpc/test_rpc_telegram.py | 26 +++++++++++++++++++++----- tests/rpc/test_rpc_webhook.py | 2 +- 7 files changed, 30 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 649dd0f58..b32af1596 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -36,6 +36,9 @@ class RPCMessageType(Enum): def __repr__(self): return self.value + def __str__(self): + return self.value + class RPCException(Exception): """ diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index a9038d81c..e54749369 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -59,7 +59,7 @@ class RPCManager: try: mod.send_msg(msg) except NotImplementedError: - logger.error(f"Message type {msg['type']} not implemented by handler {mod.name}.") + logger.error(f"Message type '{msg['type']}' not implemented by handler {mod.name}.") def startup_messages(self, config: Dict[str, Any], pairlist) -> None: if config['dry_run']: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 905ed6755..75c330dd0 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -133,9 +133,9 @@ class Telegram(RPC): """ Send a message to telegram channel """ noti = self._config['telegram'].get('notification_settings', {} - ).get(msg['type'].value, 'on') + ).get(msg['type'], 'on') if noti == 'off': - logger.info(f"Notification {msg['type']} not sent.") + logger.info(f"Notification '{msg['type']}' not sent.") # Notification disabled return diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 7235e7c15..f089550c3 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -54,7 +54,7 @@ class Webhook(RPC): else: raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) if not valuedict: - logger.info("Message type %s not configured for webhooks", msg['type']) + logger.info("Message type '%s' not configured for webhooks", msg['type']) return payload = {key: value.format(**msg) for (key, value) in valuedict.items()} diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 5972bf864..e8d0f648e 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -127,7 +127,7 @@ def test_send_msg_webhook_CustomMessagetype(mocker, default_conf, caplog) -> Non rpc_manager.send_msg({'type': RPCMessageType.STARTUP_NOTIFICATION, 'status': 'TestMessage'}) assert log_has( - "Message type RPCMessageType.STARTUP_NOTIFICATION not implemented by handler webhook.", + "Message type 'startup' not implemented by handler webhook.", caplog) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 20f478b8a..3958a825a 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1299,16 +1299,14 @@ def test_show_config_handle(default_conf, update, mocker) -> None: assert '*Initial Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] -def test_send_msg_buy_notification(default_conf, mocker) -> None: +def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) - telegram.send_msg({ + msg = { 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': 'Bittrex', 'pair': 'ETH/BTC', @@ -1321,7 +1319,10 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None: 'current_rate': 1.099e-05, 'amount': 1333.3333333333335, 'open_date': arrow.utcnow().shift(hours=-1) - }) + } + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(freqtradebot) + telegram.send_msg(msg) assert msg_mock.call_args[0][0] \ == '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' \ '*Amount:* `1333.33333333`\n' \ @@ -1329,6 +1330,21 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None: '*Current Rate:* `0.00001099`\n' \ '*Total:* `(0.001000 BTC, 12.345 USD)`' + freqtradebot.config['telegram']['notification_settings'] = {'buy': 'off'} + caplog.clear() + msg_mock.reset_mock() + telegram.send_msg(msg) + msg_mock.call_count == 0 + log_has("Notification 'buy' not sent.", caplog) + + freqtradebot.config['telegram']['notification_settings'] = {'buy': 'silent'} + caplog.clear() + msg_mock.reset_mock() + + telegram.send_msg(msg) + msg_mock.call_count == 1 + msg_mock.call_args_list[0][1]['disable_notification'] is True + def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: msg_mock = MagicMock() diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 2d2c5a4f4..9256a5316 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -174,7 +174,7 @@ def test_exception_send_msg(default_conf, mocker, caplog): webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION}) - assert log_has(f"Message type {RPCMessageType.BUY_NOTIFICATION} not configured for webhooks", + assert log_has(f"Message type '{RPCMessageType.BUY_NOTIFICATION}' not configured for webhooks", caplog) default_conf["webhook"] = get_webhook_dict() From a95dbdbde4a2706a19e3edf4190695b644902a6b Mon Sep 17 00:00:00 2001 From: HumanBot Date: Sat, 19 Sep 2020 14:31:23 -0400 Subject: [PATCH 0652/1197] Added 1M and 1y timeframes Huobi Pro allows monthly and yearly data downloading --- freqtrade/commands/cli_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 8eb5c3ce8..458aae325 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -375,7 +375,7 @@ AVAILABLE_CLI_OPTIONS = { help='Specify which tickers to download. Space-separated list. ' 'Default: `1m 5m`.', choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', - '6h', '8h', '12h', '1d', '3d', '1w'], + '6h', '8h', '12h', '1d', '3d', '1w','1M', '1y'], default=['1m', '5m'], nargs='+', ), From 8c9a600dec579bb8d960cb53e5c6e8907bf5fa0f Mon Sep 17 00:00:00 2001 From: HumanBot Date: Sat, 19 Sep 2020 14:36:12 -0400 Subject: [PATCH 0653/1197] changed epochs from 5000 to 500 5000 is an overkill for the hyperopt process, repetitive 500 produce better predictions --- docs/hyperopt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 530faf700..3f7a27ef0 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -229,7 +229,7 @@ Because hyperopt tries a lot of combinations to find the best parameters it will We strongly recommend to use `screen` or `tmux` to prevent any connection loss. ```bash -freqtrade hyperopt --config config.json --hyperopt -e 5000 --spaces all +freqtrade hyperopt --config config.json --hyperopt -e 500 --spaces all ``` Use `` as the name of the custom hyperopt used. From a31de431edfa46fcce2d411e8dd17180e20204a2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 19 Sep 2020 20:38:42 +0200 Subject: [PATCH 0654/1197] Explicitly convert to type to string --- 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 75c330dd0..87e52980a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -133,7 +133,7 @@ class Telegram(RPC): """ Send a message to telegram channel """ noti = self._config['telegram'].get('notification_settings', {} - ).get(msg['type'], 'on') + ).get(str(msg['type']), 'on') if noti == 'off': logger.info(f"Notification '{msg['type']}' not sent.") # Notification disabled From f51f445011f216dc491faf33cb659908d6b37263 Mon Sep 17 00:00:00 2001 From: HumanBot Date: Sat, 19 Sep 2020 14:45:36 -0400 Subject: [PATCH 0655/1197] 1M and 1y timeframes added Huobi Pro timeframes added --- docs/data-download.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index 9f0486262..63a56959f 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -22,7 +22,7 @@ usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-p PAIRS [PAIRS ...]] [--pairs-file FILE] [--days INT] [--timerange TIMERANGE] [--dl-trades] [--exchange EXCHANGE] - [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} ...]] + [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...]] [--erase] [--data-format-ohlcv {json,jsongz,hdf5}] [--data-format-trades {json,jsongz,hdf5}] @@ -41,7 +41,7 @@ optional arguments: as --timeframes/-t. --exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no config is provided. - -t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} ...] + -t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...] Specify which tickers to download. Space-separated list. Default: `1m 5m`. --erase Clean all existing data for the selected @@ -104,7 +104,7 @@ usage: freqtrade convert-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-p PAIRS [PAIRS ...]] --format-from {json,jsongz,hdf5} --format-to {json,jsongz,hdf5} [--erase] - [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} ...]] + [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...]] optional arguments: -h, --help show this help message and exit @@ -117,7 +117,7 @@ optional arguments: Destination format for data conversion. --erase Clean all existing data for the selected exchange/pairs/timeframes. - -t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w} ...] + -t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...] Specify which tickers to download. Space-separated list. Default: `1m 5m`. From c349499985714ac6c6616f7a4b8e64816f994bfb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 11:18:45 +0200 Subject: [PATCH 0656/1197] Also add 2w (supported by kraken) --- docs/data-download.md | 8 ++++---- freqtrade/commands/cli_options.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index 63a56959f..3a7e47c8b 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -22,7 +22,7 @@ usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-p PAIRS [PAIRS ...]] [--pairs-file FILE] [--days INT] [--timerange TIMERANGE] [--dl-trades] [--exchange EXCHANGE] - [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...]] + [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]] [--erase] [--data-format-ohlcv {json,jsongz,hdf5}] [--data-format-trades {json,jsongz,hdf5}] @@ -41,7 +41,7 @@ optional arguments: as --timeframes/-t. --exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no config is provided. - -t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...] + -t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...] Specify which tickers to download. Space-separated list. Default: `1m 5m`. --erase Clean all existing data for the selected @@ -104,7 +104,7 @@ usage: freqtrade convert-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-p PAIRS [PAIRS ...]] --format-from {json,jsongz,hdf5} --format-to {json,jsongz,hdf5} [--erase] - [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...]] + [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]] optional arguments: -h, --help show this help message and exit @@ -117,7 +117,7 @@ optional arguments: Destination format for data conversion. --erase Clean all existing data for the selected exchange/pairs/timeframes. - -t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,1M,1y} ...] + -t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...] Specify which tickers to download. Space-separated list. Default: `1m 5m`. diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 458aae325..81b8de1af 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -375,7 +375,7 @@ AVAILABLE_CLI_OPTIONS = { help='Specify which tickers to download. Space-separated list. ' 'Default: `1m 5m`.', choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', - '6h', '8h', '12h', '1d', '3d', '1w','1M', '1y'], + '6h', '8h', '12h', '1d', '3d', '1w', '2w', '1M', '1y'], default=['1m', '5m'], nargs='+', ), From 2b1d0b4ab5f78c2825c49d228eb3725ed647799f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 11:45:08 +0200 Subject: [PATCH 0657/1197] Rename references to "master" branch to "stable" closes #2496 --- .github/workflows/ci.yml | 7 ++++--- .github/workflows/docker_update_readme.yml | 2 +- CONTRIBUTING.md | 9 +++++---- README.md | 8 ++++---- docker-compose.yml | 2 +- docs/configuration.md | 2 +- docs/developer.md | 18 +++++++++--------- docs/docker.md | 6 +++--- docs/docker_quickstart.md | 6 +++--- docs/index.md | 2 +- docs/installation.md | 12 ++++++------ setup.sh | 14 +++++++------- 12 files changed, 45 insertions(+), 43 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8bc01fa6..392641677 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: push: branches: - master + - stable - develop tags: release: @@ -193,7 +194,7 @@ jobs: steps: - name: Cleanup previous runs on this branch uses: rokroskar/workflow-run-cleanup-action@v0.2.2 - if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/master' && github.repository == 'freqtrade/freqtrade'" + if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/stable' && github.repository == 'freqtrade/freqtrade'" env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" @@ -235,7 +236,7 @@ jobs: - name: Publish to PyPI (Test) uses: pypa/gh-action-pypi-publish@master - if: (steps.extract_branch.outputs.branch == 'master' || github.event_name == 'release') + if: (steps.extract_branch.outputs.branch == 'stable' || github.event_name == 'release') with: user: __token__ password: ${{ secrets.pypi_test_password }} @@ -243,7 +244,7 @@ jobs: - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@master - if: (steps.extract_branch.outputs.branch == 'master' || github.event_name == 'release') + if: (steps.extract_branch.outputs.branch == 'stable' || github.event_name == 'release') with: user: __token__ password: ${{ secrets.pypi_password }} diff --git a/.github/workflows/docker_update_readme.yml b/.github/workflows/docker_update_readme.yml index 57a7e591e..95e69be2a 100644 --- a/.github/workflows/docker_update_readme.yml +++ b/.github/workflows/docker_update_readme.yml @@ -2,7 +2,7 @@ name: Update Docker Hub Description on: push: branches: - - master + - stable jobs: dockerHubDescription: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 90594866a..97f62154d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,8 +8,9 @@ Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/ Few pointers for contributions: -- Create your PR against the `develop` branch, not `master`. -- New features need to contain unit tests and must be PEP8 conformant (max-line-length = 100). +- Create your PR against the `develop` branch, not `stable`. +- New features need to contain unit tests, must conform to PEP8 (max-line-length = 100) and should be documented with the introduction PR. +- PR's can be declared as `[WIP]` - which signify Work in Progress Pull Requests (which are not finished). If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. @@ -18,7 +19,7 @@ or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. Best start by reading the [documentation](https://www.freqtrade.io/) to get a feel for what is possible with the bot, or head straight to the [Developer-documentation](https://www.freqtrade.io/en/latest/developer/) (WIP) which should help you getting started. -## Before sending the PR: +## Before sending the PR ### 1. Run unit tests @@ -114,6 +115,6 @@ Contributors may be given commit privileges. Preference will be given to those w 1. Access to resources for cross-platform development and testing. 1. Time to devote to the project regularly. -Being a Committer does not grant write permission on `develop` or `master` for security reasons (Users trust Freqtrade with their Exchange API keys). +Being a Committer does not grant write permission on `develop` or `stable` for security reasons (Users trust Freqtrade with their Exchange API keys). After being Committer for some time, a Committer may be named Core Committer and given full repository access. diff --git a/README.md b/README.md index 90f303c6d..feea47299 100644 --- a/README.md +++ b/README.md @@ -127,8 +127,8 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor The project is currently setup in two main branches: -- `develop` - This branch has often new features, but might also cause breaking changes. -- `master` - This branch contains the latest stable release. The bot 'should' be stable on this branch, and is generally well tested. +- `develop` - This branch has often new features, but might also contain breaking changes. We try hard to keep this branch as stable as possible. +- `stable` - This branch contains the latest stable release. This branch is generally well tested. - `feat/*` - These are feature branches, which are being worked on heavily. Please don't use these unless you want to test a specific feature. ## Support @@ -171,11 +171,11 @@ Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/ **Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. -**Important:** Always create your PR against the `develop` branch, not `master`. +**Important:** Always create your PR against the `develop` branch, not `stable`. ## Requirements -### Uptodate clock +### Up-to-date clock The clock must be accurate, syncronized to a NTP server very frequently to avoid problems with communication to the exchanges. diff --git a/docker-compose.yml b/docker-compose.yml index 49d83aa5e..527e6d62a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: freqtrade: - image: freqtradeorg/freqtrade:master + image: freqtradeorg/freqtrade:stable # image: freqtradeorg/freqtrade:develop # Build step - only needed when additional dependencies are needed # build: diff --git a/docs/configuration.md b/docs/configuration.md index bf141f8e8..d6e26f80e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -375,7 +375,7 @@ Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports exchange markets and trading APIs. The complete up-to-date list can be found in the [CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). However, the bot was tested by the development team with only Bittrex, Binance and Kraken, - so the these are the only officially supported exhanges: + so the these are the only officially supported exchanges: - [Bittrex](https://bittrex.com/): "bittrex" - [Binance](https://www.binance.com/): "binance" diff --git a/docs/developer.md b/docs/developer.md index 9d47258b7..6eeaf47f4 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -263,13 +263,13 @@ jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown freqtrade This documents some decisions taken for the CI Pipeline. * CI runs on all OS variants, Linux (ubuntu), macOS and Windows. -* Docker images are build for the branches `master` and `develop`. -* Raspberry PI Docker images are postfixed with `_pi` - so tags will be `:master_pi` and `develop_pi`. +* Docker images are build for the branches `stable` and `develop`. +* Raspberry PI Docker images are postfixed with `_pi` - so tags will be `:stable_pi` and `develop_pi`. * Docker images contain a file, `/freqtrade/freqtrade_commit` containing the commit this image is based of. * Full docker image rebuilds are run once a week via schedule. * Deployments run on ubuntu. * ta-lib binaries are contained in the build_helpers directory to avoid fails related to external unavailability. -* All tests must pass for a PR to be merged to `master` or `develop`. +* All tests must pass for a PR to be merged to `stable` or `develop`. ## Creating a release @@ -286,19 +286,19 @@ git checkout -b new_release Determine if crucial bugfixes have been made between this commit and the current state, and eventually cherry-pick these. -* Merge the release branch (master) into this branch. +* Merge the release branch (stable) into this branch. * Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for July 2019). Minor versions can be `2019.7.1` should we need to do a second release that month. Version numbers must follow allowed versions from PEP0440 to avoid failures pushing to pypi. * Commit this part -* push that branch to the remote and create a PR against the master branch +* push that branch to the remote and create a PR against the stable branch ### Create changelog from git commits !!! Note - Make sure that the master branch is up-to-date! + Make sure that the release branch is up-to-date! ``` bash # Needs to be done before merging / pulling that branch. -git log --oneline --no-decorate --no-merges master..new_release +git log --oneline --no-decorate --no-merges stable..new_release ``` To keep the release-log short, best wrap the full git changelog into a collapsible details section. @@ -314,11 +314,11 @@ To keep the release-log short, best wrap the full git changelog into a collapsib ### Create github release / tag -Once the PR against master is merged (best right after merging): +Once the PR against stable is merged (best right after merging): * Use the button "Draft a new release" in the Github UI (subsection releases). * Use the version-number specified as tag. -* Use "master" as reference (this step comes after the above PR is merged). +* Use "stable" as reference (this step comes after the above PR is merged). * Use the above changelog as release comment (as codeblock) ## Releases diff --git a/docs/docker.md b/docs/docker.md index 3fe335cf0..601c842f7 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -10,9 +10,9 @@ Pull the image from docker hub. Branches / tags available can be checked out on [Dockerhub tags page](https://hub.docker.com/r/freqtradeorg/freqtrade/tags/). ```bash -docker pull freqtradeorg/freqtrade:master +docker pull freqtradeorg/freqtrade:stable # Optionally tag the repository so the run-commands remain shorter -docker tag freqtradeorg/freqtrade:master freqtrade +docker tag freqtradeorg/freqtrade:stable freqtrade ``` To update the image, simply run the above commands again and restart your running container. @@ -20,7 +20,7 @@ To update the image, simply run the above commands again and restart your runnin Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image). !!! Note "Docker image update frequency" - The official docker images with tags `master`, `develop` and `latest` are automatically rebuild once a week to keep the base image uptodate. + The official docker images with tags `stable`, `develop` and `latest` are automatically rebuild once a week to keep the base image up-to-date. In addition to that, every merge to `develop` will trigger a rebuild for `develop` and `latest`. ### Prepare the configuration files diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index c033e827b..caea4f599 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -29,7 +29,7 @@ Create a new directory and place the [docker-compose file](https://github.com/fr mkdir ft_userdata cd ft_userdata/ # Download the docker-compose file from the repository - curl https://raw.githubusercontent.com/freqtrade/freqtrade/master/docker-compose.yml -o docker-compose.yml + curl https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml -o docker-compose.yml # Pull the freqtrade image docker-compose pull @@ -46,7 +46,7 @@ Create a new directory and place the [docker-compose file](https://github.com/fr mkdir ft_userdata cd ft_userdata/ # Download the docker-compose file from the repository - curl https://raw.githubusercontent.com/freqtrade/freqtrade/master/docker-compose.yml -o docker-compose.yml + curl https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml -o docker-compose.yml # Pull the freqtrade image docker-compose pull @@ -61,7 +61,7 @@ Create a new directory and place the [docker-compose file](https://github.com/fr !!! Note "Change your docker Image" You have to change the docker image in the docker-compose file for your Raspberry build to work properly. ``` yml - image: freqtradeorg/freqtrade:master_pi + image: freqtradeorg/freqtrade:stable_pi # image: freqtradeorg/freqtrade:develop_pi ``` diff --git a/docs/index.md b/docs/index.md index 397c549aa..e7fc54628 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,7 +8,7 @@
    Fork -Download +Download Follow @freqtrade diff --git a/docs/installation.md b/docs/installation.md index 5f4807b99..9b15c9685 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -31,7 +31,7 @@ Freqtrade provides the Linux/MacOS Easy Installation script to install all depen The easiest way to install and run Freqtrade is to clone the bot Github repository and then run the Easy Installation script, if it's available for your platform. !!! Note "Version considerations" - When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests). The `master` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable). + When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests). The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable). !!! Note Python3.6 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository. @@ -41,11 +41,11 @@ This can be achieved with the following commands: ```bash git clone https://github.com/freqtrade/freqtrade.git cd freqtrade -# git checkout master # Optional, see (1) +# git checkout stable # Optional, see (1) ./setup.sh --install ``` -(1) This command switches the cloned repository to the use of the `master` branch. It's not needed if you wish to stay on the `develop` branch. You may later switch between branches at any time with the `git checkout master`/`git checkout develop` commands. +(1) This command switches the cloned repository to the use of the `stable` branch. It's not needed if you wish to stay on the `develop` branch. You may later switch between branches at any time with the `git checkout stable`/`git checkout develop` commands. ## Easy Installation Script (Linux/MacOS) @@ -56,7 +56,7 @@ $ ./setup.sh usage: -i,--install Install freqtrade from scratch -u,--update Command git pull to update. - -r,--reset Hard reset your develop/master branch. + -r,--reset Hard reset your develop/stable branch. -c,--config Easy config generator (Will override your existing file). ``` @@ -76,7 +76,7 @@ This option will pull the last version of your current branch and update your vi ** --reset ** -This option will hard reset your branch (only if you are on either `master` or `develop`) and recreate your virtualenv. +This option will hard reset your branch (only if you are on either `stable` or `develop`) and recreate your virtualenv. ** --config ** @@ -174,7 +174,7 @@ Clone the git repository: ```bash git clone https://github.com/freqtrade/freqtrade.git cd freqtrade -git checkout master +git checkout stable ``` #### 4. Install python dependencies diff --git a/setup.sh b/setup.sh index 918c41e6b..049a6a77e 100755 --- a/setup.sh +++ b/setup.sh @@ -120,13 +120,13 @@ function update() { updateenv } -# Reset Develop or Master branch +# Reset Develop or Stable branch function reset() { echo "----------------------------" echo "Reseting branch and virtual env" echo "----------------------------" - if [ "1" == $(git branch -vv |grep -cE "\* develop|\* master") ] + if [ "1" == $(git branch -vv |grep -cE "\* develop|\* stable") ] then read -p "Reset git branch? (This will remove all changes you made!) [y/N]? " @@ -138,14 +138,14 @@ function reset() { then echo "- Hard resetting of 'develop' branch." git reset --hard origin/develop - elif [ "1" == $(git branch -vv |grep -c "* master") ] + elif [ "1" == $(git branch -vv |grep -c "* stable") ] then - echo "- Hard resetting of 'master' branch." - git reset --hard origin/master + echo "- Hard resetting of 'stable' branch." + git reset --hard origin/stable fi fi else - echo "Reset ignored because you are not on 'master' or 'develop'." + echo "Reset ignored because you are not on 'stable' or 'develop'." fi if [ -d ".env" ]; then @@ -270,7 +270,7 @@ function help() { echo "usage:" echo " -i,--install Install freqtrade from scratch" echo " -u,--update Command git pull to update." - echo " -r,--reset Hard reset your develop/master branch." + echo " -r,--reset Hard reset your develop/stable branch." echo " -c,--config Easy config generator (Will override your existing file)." echo " -p,--plot Install dependencies for Plotting scripts." } From b3f0bfd77f8b2f5f5c107a7f6b2880ddf9ba0896 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 11:51:12 +0200 Subject: [PATCH 0658/1197] Fix a few random typos --- docs/faq.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 48f52a566..beed89801 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -89,7 +89,7 @@ Same fix should be done in the configuration file, if order types are defined in ### How do I search the bot logs for something? -By default, the bot writes its log into stderr stream. This is implemented this way so that you can easily separate the bot's diagnostics messages from Backtesting, Edge and Hyperopt results, output from other various Freqtrade utility subcommands, as well as from the output of your custom `print()`'s you may have inserted into your strategy. So if you need to search the log messages with the grep utility, you need to redirect stderr to stdout and disregard stdout. +By default, the bot writes its log into stderr stream. This is implemented this way so that you can easily separate the bot's diagnostics messages from Backtesting, Edge and Hyperopt results, output from other various Freqtrade utility sub-commands, as well as from the output of your custom `print()`'s you may have inserted into your strategy. So if you need to search the log messages with the grep utility, you need to redirect stderr to stdout and disregard stdout. * In unix shells, this normally can be done as simple as: ```shell @@ -114,7 +114,7 @@ and then grep it as: ```shell $ cat /path/to/mylogfile.log | grep 'something' ``` -or even on the fly, as the bot works and the logfile grows: +or even on the fly, as the bot works and the log file grows: ```shell $ tail -f /path/to/mylogfile.log | grep 'something' ``` @@ -137,7 +137,7 @@ compute. Since hyperopt uses Bayesian search, running for too many epochs may not produce greater results. -It's therefore recommended to run between 500-1000 epochs over and over until you hit at least 10.000 epocs in total (or are satisfied with the result). You can best judge by looking at the results - if the bot keeps discovering better strategies, it's best to keep on going. +It's therefore recommended to run between 500-1000 epochs over and over until you hit at least 10.000 epochs in total (or are satisfied with the result). You can best judge by looking at the results - if the bot keeps discovering better strategies, it's best to keep on going. ```bash freqtrade hyperopt -e 1000 @@ -153,7 +153,7 @@ for i in {1..100}; do freqtrade hyperopt -e 1000; done * Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) - or the Freqtrade [discord community](https://discord.gg/X89cVG). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you. -* If you wonder why it can take from 20 minutes to days to do 1000 epocs here are some answers: +* If you wonder why it can take from 20 minutes to days to do 1000 epochs here are some answers: This answer was written during the release 0.15.1, when we had: @@ -167,7 +167,7 @@ already 8\*10^9\*10 evaluations. A roughly total of 80 billion evals. Did you run 100 000 evals? Congrats, you've done roughly 1 / 100 000 th of the search space, assuming that the bot never tests the same parameters more than once. -* The time it takes to run 1000 hyperopt epocs depends on things like: The available cpu, harddisk, ram, timeframe, timerange, indicator settings, indicator count, amount of coins that hyperopt test strategies on and the resulting trade count - which can be 650 trades in a year or 10.0000 trades depending if the strategy aims for big profits by trading rarely or for many low profit trades. +* The time it takes to run 1000 hyperopt epochs depends on things like: The available cpu, hard-disk, ram, timeframe, timerange, indicator settings, indicator count, amount of coins that hyperopt test strategies on and the resulting trade count - which can be 650 trades in a year or 10.0000 trades depending if the strategy aims for big profits by trading rarely or for many low profit trades. Example: 4% profit 650 times vs 0,3% profit a trade 10.000 times in a year. If we assume you set the --timerange to 365 days. @@ -180,7 +180,7 @@ Example: The Edge module is mostly a result of brainstorming of [@mishaker](https://github.com/mishaker) and [@creslinux](https://github.com/creslinux) freqtrade team members. -You can find further info on expectancy, winrate, risk management and position size in the following sources: +You can find further info on expectancy, win rate, risk management and position size in the following sources: - https://www.tradeciety.com/ultimate-math-guide-for-traders/ - http://www.vantharp.com/tharp-concepts/expectancy.asp From 637fe35549ab3c9d30f6361adc02f53a11b5ba2a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 11:53:47 +0200 Subject: [PATCH 0659/1197] Fix typo in release documentation --- docs/developer.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developer.md b/docs/developer.md index 6eeaf47f4..21934916f 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -294,7 +294,7 @@ Determine if crucial bugfixes have been made between this commit and the current ### Create changelog from git commits !!! Note - Make sure that the release branch is up-to-date! + Make sure that the `stable` branch is up-to-date! ``` bash # Needs to be done before merging / pulling that branch. From b72cccae3c04e279f9f1e09580789576f7721e79 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 13:09:34 +0200 Subject: [PATCH 0660/1197] Add note about download-data in combination with startup period closes #2673 --- docs/data-download.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/data-download.md b/docs/data-download.md index 3a7e47c8b..e9c5c1865 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -71,6 +71,11 @@ Common arguments: ``` +!!! Note "Startup period" + `download-data` is a strategy-independent command. The idea is to download a big chunk of data once, and then iteratively increase the amount of data stored. + + For that reason, `download-data` does not care about the "startup-period" defined in a strategy. It's up to the user to download additional days if the backtest should start at a specific point in time (while respecting startup period). + ### Data format Freqtrade currently supports 3 data-formats for both OHLCV and trades data: From 8ff7ce8b17ad8b9df409f64a314e99ec44d67099 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 11:40:16 +0000 Subject: [PATCH 0661/1197] Introduce devcontainer --- .devcontainer/devcontainer.json | 38 ++++++++++++++++++++++++++++++++ .devcontainer/docker-compose.yml | 10 +++++++++ 2 files changed, 48 insertions(+) create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/docker-compose.yml diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..916f4e911 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,38 @@ +{ + "name": "freqtrade Develop", + + // Update the 'dockerComposeFile' list if you have more compose files or use different names. + // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. + "dockerComposeFile": [ + "docker-compose.yml" + // "docker-compose.vscode.yml" + ], + + "service": "freqtrade_develop", + + // The optional 'workspaceFolder' property is the path VS Code should open by default when + // connected. This is typically a file mount in .devcontainer/docker-compose.yml + "workspaceFolder": "/freqtrade/", + + "settings": { + "terminal.integrated.shell.linux": null + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [], + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Uncomment the next line if you want start specific services in your Docker Compose config. + // "runServices": [], + + // Uncomment the next line if you want to keep your containers running after VS Code shuts down. + // "shutdownAction": "none", + + // Uncomment the next line to run commands after the container is created - for example installing curl. + // "postCreateCommand": "sudo apt-get update && apt-get install -y git", + + // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "ftuser" +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 000000000..d5927c4a9 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,10 @@ +--- +version: '3' +services: + freqtrade_develop: + build: + context: .. + dockerfile: "Dockerfile.develop" + volumes: + - ..:/freqtrade:cached + command: /bin/sh -c "while sleep 1000; do :; done" From 7ead4f9fa3625dc06872494c34a7c198bd264e41 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 14:16:36 +0200 Subject: [PATCH 0662/1197] Update devcontainer settings --- .devcontainer/devcontainer.json | 63 +++++++++++++++++++------------- .devcontainer/docker-compose.yml | 9 ++++- 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 916f4e911..7a5e43cf1 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,38 +1,49 @@ { - "name": "freqtrade Develop", + "name": "freqtrade Develop", - // Update the 'dockerComposeFile' list if you have more compose files or use different names. - // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. - "dockerComposeFile": [ - "docker-compose.yml" - // "docker-compose.vscode.yml" - ], + // Update the 'dockerComposeFile' list if you have more compose files or use different names. + // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. + "dockerComposeFile": [ + "docker-compose.yml" + // "docker-compose.vscode.yml" + ], - "service": "freqtrade_develop", + "service": "ft_vscode", - // The optional 'workspaceFolder' property is the path VS Code should open by default when - // connected. This is typically a file mount in .devcontainer/docker-compose.yml - "workspaceFolder": "/freqtrade/", + // The optional 'workspaceFolder' property is the path VS Code should open by default when + // connected. This is typically a file mount in .devcontainer/docker-compose.yml + "workspaceFolder": "/freqtrade/", - "settings": { - "terminal.integrated.shell.linux": null - }, + "settings": { + "terminal.integrated.shell.linux": "/bin/bash", + "editor.insertSpaces": true, + "files.trimTrailingWhitespace": true, + "[markdown]": { + "files.trimTrailingWhitespace": false, + }, + "python.pythonPath": "/usr/local/bin/python", + }, - // Add the IDs of extensions you want installed when the container is created. - "extensions": [], + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "ms-python.python", + "ms-python.vscode-pylance", + "davidanson.vscode-markdownlint", + "ms-azuretools.vscode-docker", + ], - // Use 'forwardPorts' to make a list of ports inside the container available locally. - // "forwardPorts": [], + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], - // Uncomment the next line if you want start specific services in your Docker Compose config. - // "runServices": [], + // Uncomment the next line if you want start specific services in your Docker Compose config. + // "runServices": [], - // Uncomment the next line if you want to keep your containers running after VS Code shuts down. - // "shutdownAction": "none", + // Uncomment the next line if you want to keep your containers running after VS Code shuts down. + // "shutdownAction": "none", - // Uncomment the next line to run commands after the container is created - for example installing curl. - // "postCreateCommand": "sudo apt-get update && apt-get install -y git", + // Uncomment the next line to run commands after the container is created - for example installing curl. + // "postCreateCommand": "sudo apt-get update && apt-get install -y git", - // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. - "remoteUser": "ftuser" + // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "ftuser" } diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index d5927c4a9..93ffee309 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -1,10 +1,17 @@ --- version: '3' services: - freqtrade_develop: + ft_vscode: build: context: .. dockerfile: "Dockerfile.develop" volumes: - ..:/freqtrade:cached + - freqtrade-vscode-server:/home/ftuser/.vscode-server + - freqtrade-bashhistory:/home/ftuser/commandhistory + command: /bin/sh -c "while sleep 1000; do :; done" + +volumes: + freqtrade-vscode-server: + freqtrade-bashhistory: From 20e5c1b3885229af7fc1479346953404d7dabc2e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 12:16:58 +0000 Subject: [PATCH 0663/1197] Update Developer documentation related to docker --- docs/developer.md | 44 ++++++-------------------------------------- 1 file changed, 6 insertions(+), 38 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index 9d47258b7..e359ff34a 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -50,50 +50,18 @@ def test_method_to_test(caplog): ``` -### Local docker usage +### Devcontainer setup -The fastest and easiest way to start up is to use docker-compose.develop which gives developers the ability to start the bot up with all the required dependencies, *without* needing to install any freqtrade specific dependencies on your local machine. +The fastest and easiest way to get started is to use [VSCode](https://code.visualstudio.com/) with the Remote container extension. +This gives developers the ability to start the bot with all required dependencies *without* needing to install any freqtrade specific dependencies on your local machine. #### Install -* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +* [VSCode](https://code.visualstudio.com/) * [docker](https://docs.docker.com/install/) -* [docker-compose](https://docs.docker.com/compose/install/) +* [Remote container extension documentation](https://code.visualstudio.com/docs/remote) -#### Starting the bot - -##### Use the develop dockerfile - -``` bash -rm docker-compose.yml && mv docker-compose.develop.yml docker-compose.yml -``` - -#### Docker Compose - -##### Starting - -``` bash -docker-compose up -``` - -![Docker compose up](https://user-images.githubusercontent.com/419355/65456322-47f63a80-de06-11e9-90c6-3c74d1bad0b8.png) - -##### Rebuilding - -``` bash -docker-compose build -``` - -##### Executing (effectively SSH into the container) - -The `exec` command requires that the container already be running, if you want to start it -that can be effected by `docker-compose up` or `docker-compose run freqtrade_develop` - -``` bash -docker-compose exec freqtrade_develop /bin/bash -``` - -![image](https://user-images.githubusercontent.com/419355/65456522-ba671a80-de06-11e9-9598-df9ca0d8dcac.png) +For more information about the [Remote container extension](https://code.visualstudio.com/docs/remote), best consult the documentation. ## ErrorHandling From cf85a178f33da910010a423cebe90f0aa076d0df Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 12:34:57 +0000 Subject: [PATCH 0664/1197] Update developer documentation related to devcontainer --- docs/developer.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index e359ff34a..22de01f78 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -21,11 +21,24 @@ This will spin up a local server (usually on port 8000) so you can see if everyt ## Developer setup -To configure a development environment, best use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ". -Alternatively (if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -e .[all]`. +To configure a development environment, you can either use the provided [DevContainer](#devcontainer-setup), or use the `setup.sh` script and answer "y" when asked "Do you want to install dependencies for dev [y/N]? ". +Alternatively (e.g. if your system is not supported by the setup.sh script), follow the manual installation process and run `pip3 install -e .[all]`. This will install all required tools for development, including `pytest`, `flake8`, `mypy`, and `coveralls`. +### Devcontainer setup + +The fastest and easiest way to get started is to use [VSCode](https://code.visualstudio.com/) with the Remote container extension. +This gives developers the ability to start the bot with all required dependencies *without* needing to install any freqtrade specific dependencies on your local machine. + +#### Devcontainer dependencies + +* [VSCode](https://code.visualstudio.com/) +* [docker](https://docs.docker.com/install/) +* [Remote container extension documentation](https://code.visualstudio.com/docs/remote) + +For more information about the [Remote container extension](https://code.visualstudio.com/docs/remote), best consult the documentation. + ### Tests New code should be covered by basic unittests. Depending on the complexity of the feature, Reviewers may request more in-depth unittests. @@ -50,19 +63,6 @@ def test_method_to_test(caplog): ``` -### Devcontainer setup - -The fastest and easiest way to get started is to use [VSCode](https://code.visualstudio.com/) with the Remote container extension. -This gives developers the ability to start the bot with all required dependencies *without* needing to install any freqtrade specific dependencies on your local machine. - -#### Install - -* [VSCode](https://code.visualstudio.com/) -* [docker](https://docs.docker.com/install/) -* [Remote container extension documentation](https://code.visualstudio.com/docs/remote) - -For more information about the [Remote container extension](https://code.visualstudio.com/docs/remote), best consult the documentation. - ## ErrorHandling Freqtrade Exceptions all inherit from `FreqtradeException`. From 0a7b6f73c9fc895869ab4b2b4c0178e95bd8de3d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 12:35:08 +0000 Subject: [PATCH 0665/1197] Move devcontainer stuff to .devcontainer --- .devcontainer/Dockerfile | 22 ++++++++++++++++++++++ .devcontainer/devcontainer.json | 5 ----- .devcontainer/docker-compose.yml | 7 +++++-- docker-compose.develop.yml | 20 -------------------- 4 files changed, 27 insertions(+), 27 deletions(-) create mode 100644 .devcontainer/Dockerfile delete mode 100644 docker-compose.develop.yml diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..1c2ab8de0 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,22 @@ +FROM freqtradeorg/freqtrade:develop + +# Install dependencies +COPY requirements-dev.txt /freqtrade/ +RUN apt-get update \ + && apt-get -y install git sudo vim \ + && apt-get clean \ + && pip install numpy --no-cache-dir \ + # Install ALL dependencies + && pip install -r requirements-dev.txt --no-cache-dir \ + # Install documentation dependencies (to enable mkdocs) + && pip install -r docs/requirements-docs.txt --no-cache-dir \ + && useradd -m ftuser \ + && mkdir -p /home/ftuser/.vscode-server /home/ftuser/.vscode-server-insiders /home/ftuser/commandhistory \ + && echo "export PROMPT_COMMAND='history -a'" >> /home/ftuser/.bashrc \ + && echo "export HISTFILE=~/commandhistory/.bash_history" >> /home/ftuser/.bashrc \ + && chown ftuser: -R /home/ftuser/ + +USER ftuser + +# Empty the ENTRYPOINT to allow all commands +ENTRYPOINT [] diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7a5e43cf1..1882e3bdf 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,17 +1,12 @@ { "name": "freqtrade Develop", - // Update the 'dockerComposeFile' list if you have more compose files or use different names. - // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. "dockerComposeFile": [ "docker-compose.yml" - // "docker-compose.vscode.yml" ], "service": "ft_vscode", - // The optional 'workspaceFolder' property is the path VS Code should open by default when - // connected. This is typically a file mount in .devcontainer/docker-compose.yml "workspaceFolder": "/freqtrade/", "settings": { diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 93ffee309..7cf3ba2f5 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -4,14 +4,17 @@ services: ft_vscode: build: context: .. - dockerfile: "Dockerfile.develop" + dockerfile: ".devcontainer/Dockerfile" volumes: - ..:/freqtrade:cached - freqtrade-vscode-server:/home/ftuser/.vscode-server - freqtrade-bashhistory:/home/ftuser/commandhistory - + # Expose API port + ports: + - "127.0.0.1:8080:8080" command: /bin/sh -c "while sleep 1000; do :; done" + volumes: freqtrade-vscode-server: freqtrade-bashhistory: diff --git a/docker-compose.develop.yml b/docker-compose.develop.yml deleted file mode 100644 index 562b5960a..000000000 --- a/docker-compose.develop.yml +++ /dev/null @@ -1,20 +0,0 @@ ---- -version: '3' -services: - freqtrade_develop: - build: - context: . - dockerfile: "./Dockerfile.develop" - volumes: - - ".:/freqtrade" - entrypoint: - - "freqtrade" - - freqtrade_bash: - build: - context: . - dockerfile: "./Dockerfile.develop" - volumes: - - ".:/freqtrade" - entrypoint: - - "/bin/bash" From 4355f36cd6eae5e96acf8bef52bcbb9a8c6cd795 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 12:36:47 +0000 Subject: [PATCH 0666/1197] Add gitconfig to devcontainer --- .devcontainer/docker-compose.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 7cf3ba2f5..7b5e64609 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -6,7 +6,11 @@ services: context: .. dockerfile: ".devcontainer/Dockerfile" volumes: + # Allow git usage within container + - "/home/${USER}/.ssh:/home/ftuser/.ssh:ro" + - "/home/${USER}/.gitconfig:/home/ftuser/.gitconfig:ro" - ..:/freqtrade:cached + # Persist bash-history - freqtrade-vscode-server:/home/ftuser/.vscode-server - freqtrade-bashhistory:/home/ftuser/commandhistory # Expose API port From 096079a595f03294ffea6a03f70b2f4c90fbea93 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 12:41:17 +0000 Subject: [PATCH 0667/1197] Install autopep8 --- .devcontainer/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 1c2ab8de0..3430cac5a 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -5,7 +5,7 @@ COPY requirements-dev.txt /freqtrade/ RUN apt-get update \ && apt-get -y install git sudo vim \ && apt-get clean \ - && pip install numpy --no-cache-dir \ + && pip install autopep8--no-cache-dir \ # Install ALL dependencies && pip install -r requirements-dev.txt --no-cache-dir \ # Install documentation dependencies (to enable mkdocs) From 129cbf5ef58ad991465dbd5b2ce1971f63ac035a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 14:58:15 +0200 Subject: [PATCH 0668/1197] Add more Dockerfiles --- Dockerfile.develop => docker/Dockerfile.develop | 1 + docker/Dockerfile.jupyter | 7 +++++++ docker/Dockerfile.plot | 9 +++++++++ .../Dockerfile.technical | 0 docker/docker-compose-jupyter.yml | 16 ++++++++++++++++ 5 files changed, 33 insertions(+) rename Dockerfile.develop => docker/Dockerfile.develop (99%) create mode 100644 docker/Dockerfile.jupyter create mode 100644 docker/Dockerfile.plot rename Dockerfile.technical => docker/Dockerfile.technical (100%) create mode 100644 docker/docker-compose-jupyter.yml diff --git a/Dockerfile.develop b/docker/Dockerfile.develop similarity index 99% rename from Dockerfile.develop rename to docker/Dockerfile.develop index 8f6871c55..cb49984e2 100644 --- a/Dockerfile.develop +++ b/docker/Dockerfile.develop @@ -2,6 +2,7 @@ FROM freqtradeorg/freqtrade:develop # Install dependencies COPY requirements-dev.txt /freqtrade/ + RUN pip install numpy --no-cache-dir \ && pip install -r requirements-dev.txt --no-cache-dir diff --git a/docker/Dockerfile.jupyter b/docker/Dockerfile.jupyter new file mode 100644 index 000000000..b7499eeef --- /dev/null +++ b/docker/Dockerfile.jupyter @@ -0,0 +1,7 @@ +FROM freqtradeorg/freqtrade:develop_plot + + +RUN pip install jupyterlab --no-cache-dir + +# Empty the ENTRYPOINT to allow all commands +ENTRYPOINT [] diff --git a/docker/Dockerfile.plot b/docker/Dockerfile.plot new file mode 100644 index 000000000..9313a34b9 --- /dev/null +++ b/docker/Dockerfile.plot @@ -0,0 +1,9 @@ +FROM freqtradeorg/freqtrade:develop + +# Install dependencies +COPY requirements-plot.txt /freqtrade/ + +RUN pip install -r requirements-plot.txt --no-cache-dir + +# Empty the ENTRYPOINT to allow all commands +ENTRYPOINT [] diff --git a/Dockerfile.technical b/docker/Dockerfile.technical similarity index 100% rename from Dockerfile.technical rename to docker/Dockerfile.technical diff --git a/docker/docker-compose-jupyter.yml b/docker/docker-compose-jupyter.yml new file mode 100644 index 000000000..4b396d0f5 --- /dev/null +++ b/docker/docker-compose-jupyter.yml @@ -0,0 +1,16 @@ +--- +version: '3' +services: + freqtrade: + build: + context: .. + dockerfile: docker/Dockerfile.jupyter + restart: unless-stopped + container_name: freqtrade + ports: + - "18889:8888" + volumes: + - "./user_data:/freqtrade/user_data" + # Default command used when running `docker compose up` + command: > + jupyter lab --port=8888 --ip 0.0.0.0 --allow-root From 85ab6e43baee20e9b6666f7077fa7f6760af3c9d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 14:58:27 +0200 Subject: [PATCH 0669/1197] Build _plot dockerfile --- build_helpers/publish_docker.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build_helpers/publish_docker.sh b/build_helpers/publish_docker.sh index 03a95161b..8e132ecba 100755 --- a/build_helpers/publish_docker.sh +++ b/build_helpers/publish_docker.sh @@ -2,6 +2,7 @@ # Replace / with _ to create a valid tag TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") +TAG_PLOT=${TAG}_plot echo "Running for ${TAG}" # Add commit and commit_message to docker container @@ -16,6 +17,7 @@ else docker pull ${IMAGE_NAME}:${TAG} docker build --cache-from ${IMAGE_NAME}:${TAG} -t freqtrade:${TAG} . fi +docker build --cache-from freqtrade:${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot . if [ $? -ne 0 ]; then echo "failed building image" @@ -32,6 +34,7 @@ fi # Tag image for upload docker tag freqtrade:$TAG ${IMAGE_NAME}:$TAG +docker tag freqtrade:$TAG_PLOT ${IMAGE_NAME}:$TAG_PLOT if [ $? -ne 0 ]; then echo "failed tagging image" return 1 From 40132bbea400497143f464688d9733f6e4a0bdf0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 14:58:37 +0200 Subject: [PATCH 0670/1197] Add this branch to CI --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e8bc01fa6..34683ce9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,7 @@ on: branches: - master - develop + - add_devcontainer tags: release: types: [published] From f9efbed0765ed0597544f7c2f798219088086478 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 14:59:13 +0200 Subject: [PATCH 0671/1197] Ignore userdata from docker build --- .dockerignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.dockerignore b/.dockerignore index 223b3b110..09f4c9f0c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -13,3 +13,4 @@ CONTRIBUTING.md MANIFEST.in README.md freqtrade.service +user_data From 30c1253f75949871011843c96e5ea9c12a507aeb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 15:02:07 +0200 Subject: [PATCH 0672/1197] Use correct ports for jupyter compose file --- docker/docker-compose-jupyter.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose-jupyter.yml b/docker/docker-compose-jupyter.yml index 4b396d0f5..14e45983d 100644 --- a/docker/docker-compose-jupyter.yml +++ b/docker/docker-compose-jupyter.yml @@ -8,7 +8,7 @@ services: restart: unless-stopped container_name: freqtrade ports: - - "18889:8888" + - "127.0.0.1:8888:8888" volumes: - "./user_data:/freqtrade/user_data" # Default command used when running `docker compose up` From ab190f7a5b5acc9ca02935b08edad81c442a96e9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 15:12:30 +0200 Subject: [PATCH 0673/1197] Document jupyter with docker usage --- docs/data-analysis.md | 22 +++++++++++++++------- docs/docker_quickstart.md | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index fc4693b17..17da98935 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -1,12 +1,22 @@ # Analyzing bot data with Jupyter notebooks -You can analyze the results of backtests and trading history easily using Jupyter notebooks. Sample notebooks are located at `user_data/notebooks/`. +You can analyze the results of backtests and trading history easily using Jupyter notebooks. Sample notebooks are located at `user_data/notebooks/` after initializing the user directory with `freqtrade create-userdir --userdir user_data`. -## Pro tips +## Quick start with docker + +Freqtrade provides a docker-compose file which starts up a jupyter lab server. +You can run this server using the following command: `docker-compose -f docker/docker-compose-jupyter.yml up` + +This will create a dockercontainer running jupyter lab, which will be accessible using `https://127.0.0.1:8888/lab`. +Please use the link that's printed in the console after startup for simplified login. + +For more information, Please visit the [Data analysis with Docker](docker_quickstart.md#data-analayis-using-docker-compose) section. + +### Pro tips * See [jupyter.org](https://jupyter.org/documentation) for usage instructions. * Don't forget to start a Jupyter notebook server from within your conda or venv environment or use [nb_conda_kernels](https://github.com/Anaconda-Platform/nb_conda_kernels)* -* Copy the example notebook before use so your changes don't get clobbered with the next freqtrade update. +* Copy the example notebook before use so your changes don't get overwritten with the next freqtrade update. ### Using virtual environment with system-wide Jupyter installation @@ -28,10 +38,8 @@ ipython kernel install --user --name=freqtrade !!! Note This section is provided for completeness, the Freqtrade Team won't provide full support for problems with this setup and will recommend to install Jupyter in the virtual environment directly, as that is the easiest way to get jupyter notebooks up and running. For help with this setup please refer to the [Project Jupyter](https://jupyter.org/) [documentation](https://jupyter.org/documentation) or [help channels](https://jupyter.org/community). - -## Fine print - -Some tasks don't work especially well in notebooks. For example, anything using asynchronous execution is a problem for Jupyter. Also, freqtrade's primary entry point is the shell cli, so using pure python in a notebook bypasses arguments that provide required objects and parameters to helper functions. You may need to set those values or create expected objects manually. +!!! Warning + Some tasks don't work especially well in notebooks. For example, anything using asynchronous execution is a problem for Jupyter. Also, freqtrade's primary entry point is the shell cli, so using pure python in a notebook bypasses arguments that provide required objects and parameters to helper functions. You may need to set those values or create expected objects manually. ## Recommended workflow diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index c033e827b..853d57f3d 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -160,3 +160,21 @@ You'll then also need to modify the `docker-compose.yml` file and uncomment the ``` You can then run `docker-compose build` to build the docker image, and run it using the commands described above. + +## Data analayis using docker compose + +Freqtrade provides a docker-compose file which starts up a jupyter lab server. +You can run this server using the following command: + +``` bash +docker-compose -f docker/docker-compose-jupyter.yml up +``` + +This will create a dockercontainer running jupyter lab, which will be accessible using `https://127.0.0.1:8888/lab`. +Please use the link that's printed in the console after startup for simplified login. + +Since part of this image is built on your machine, it is recommended to rebuild the image from time to time to keep freqtrade (and dependencies) uptodate. + +``` bash +docker-compose -f docker/docker-compose-jupyter.yml build --no-cache +``` From b02c0904b6bcbbc1616798fb0edfb2fd67cc9d9c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 15:17:54 +0200 Subject: [PATCH 0674/1197] Use buildarg to use correct parent variable --- build_helpers/publish_docker.sh | 2 +- docker/Dockerfile.plot | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/build_helpers/publish_docker.sh b/build_helpers/publish_docker.sh index 8e132ecba..53e18063c 100755 --- a/build_helpers/publish_docker.sh +++ b/build_helpers/publish_docker.sh @@ -17,7 +17,7 @@ else docker pull ${IMAGE_NAME}:${TAG} docker build --cache-from ${IMAGE_NAME}:${TAG} -t freqtrade:${TAG} . fi -docker build --cache-from freqtrade:${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot . +docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot . if [ $? -ne 0 ]; then echo "failed building image" diff --git a/docker/Dockerfile.plot b/docker/Dockerfile.plot index 9313a34b9..1843efdcb 100644 --- a/docker/Dockerfile.plot +++ b/docker/Dockerfile.plot @@ -1,4 +1,5 @@ -FROM freqtradeorg/freqtrade:develop +ARG sourceimage=develop +FROM freqtradeorg/freqtrade:${sourceimage} # Install dependencies COPY requirements-plot.txt /freqtrade/ From 8ff1429e68b611db4d90ed4d4e5a6f3ecd5e6267 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 15:38:53 +0200 Subject: [PATCH 0675/1197] Add user_data to backtesting --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 22b0c43a7..cdf96abcd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,8 @@ RUN pip install numpy --no-cache-dir \ # Install and execute COPY . /freqtrade/ -RUN pip install -e . --no-cache-dir +RUN pip install -e . --no-cache-dir \ + && mkdir /freqtrade/user_data/ ENTRYPOINT ["freqtrade"] # Default to trade mode CMD [ "trade" ] From 3c460d37b66744a43ab6eaaa6fed19bcb0556231 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 16:20:01 +0200 Subject: [PATCH 0676/1197] Document existence of PLOT image --- docker-compose.yml | 2 ++ docs/developer.md | 1 + docs/docker_quickstart.md | 11 +++++++++++ 3 files changed, 14 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 49d83aa5e..ca8554b43 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,6 +4,8 @@ services: freqtrade: image: freqtradeorg/freqtrade:master # image: freqtradeorg/freqtrade:develop + # Use plotting image + # image: freqtradeorg/freqtrade:develop_plot # Build step - only needed when additional dependencies are needed # build: # context: . diff --git a/docs/developer.md b/docs/developer.md index 22de01f78..7c0d61094 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -232,6 +232,7 @@ This documents some decisions taken for the CI Pipeline. * CI runs on all OS variants, Linux (ubuntu), macOS and Windows. * Docker images are build for the branches `master` and `develop`. +* Docker images containing Plot dependencies are also available as `master_plot` and `develop_plot`. * Raspberry PI Docker images are postfixed with `_pi` - so tags will be `:master_pi` and `develop_pi`. * Docker images contain a file, `/freqtrade/freqtrade_commit` containing the commit this image is based of. * Full docker image rebuilds are run once a week via schedule. diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 853d57f3d..857c3d0cc 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -161,6 +161,17 @@ You'll then also need to modify the `docker-compose.yml` file and uncomment the You can then run `docker-compose build` to build the docker image, and run it using the commands described above. +## Plotting with docker-compose + +Commands `freqtrade plot-profit` and `freqtrade plot-dataframe` ([Documentation](plotting.md)) are available by changing the image to `*_plot` in your docker-compose.yml file. +You can then use these commands as follows: + +``` bash +docker-compose run --rm freqtrade plot-dataframe --strategy AwesomeStrategy -p BTC/ETH --timerange=20180801-20180805 +``` + +The output will be stored in the `user_data/plot` directory, and can be opened with any modern browser. + ## Data analayis using docker compose Freqtrade provides a docker-compose file which starts up a jupyter lab server. From 261b267160a240f196bdafc44fd848a4ae2e6879 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 16:20:17 +0200 Subject: [PATCH 0677/1197] Don't build devcontainer on push --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34683ce9b..e8bc01fa6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,6 @@ on: branches: - master - develop - - add_devcontainer tags: release: types: [published] From 7dadca421aca7dea08415061dba2adb3fd4c9a97 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 16:26:48 +0200 Subject: [PATCH 0678/1197] Update location of docker files --- docs/docker.md | 8 ++++---- docs/docker_quickstart.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/docker.md b/docs/docker.md index 3fe335cf0..59d03164b 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -70,16 +70,16 @@ cp -n config.json.example config.json Best start by pulling the official docker image from dockerhub as explained [here](#download-the-official-docker-image) to speed up building. -To add additional libraries to your docker image, best check out [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) which adds the [technical](https://github.com/freqtrade/technical) module to the image. +To add additional libraries to your docker image, best check out [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/docker/Dockerfile.technical) which adds the [technical](https://github.com/freqtrade/technical) module to the image. ```bash -docker build -t freqtrade -f Dockerfile.technical . +docker build -t freqtrade -f docker/Dockerfile.technical . ``` -If you are developing using Docker, use `Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies: +If you are developing using Docker, use `docker/Dockerfile.develop` to build a dev Docker image, which will also set up develop dependencies: ```bash -docker build -f Dockerfile.develop -t freqtrade-dev . +docker build -f docker/Dockerfile.develop -t freqtrade-dev . ``` !!! Warning "Include your config file manually" diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index 857c3d0cc..ad82aea3f 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -148,7 +148,7 @@ Head over to the [Backtesting Documentation](backtesting.md) to learn more. ### Additional dependencies with docker-compose If your strategy requires dependencies not included in the default image (like [technical](https://github.com/freqtrade/technical)) - it will be necessary to build the image on your host. -For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/Dockerfile.technical) for an example). +For this, please create a Dockerfile containing installation steps for the additional dependencies (have a look at [docker/Dockerfile.technical](https://github.com/freqtrade/freqtrade/blob/develop/docker/Dockerfile.technical) for an example). You'll then also need to modify the `docker-compose.yml` file and uncomment the build step, as well as rename the image to avoid naming collisions. From 50aec1d6d3d224023ffe278f1eca1601b0e6e8ba Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Sep 2020 20:19:07 +0200 Subject: [PATCH 0679/1197] Jupyter service should be called differently --- .devcontainer/Dockerfile | 8 ++------ docker/docker-compose-jupyter.yml | 2 +- docs/docker_quickstart.md | 2 +- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 3430cac5a..b333dc19d 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -5,12 +5,8 @@ COPY requirements-dev.txt /freqtrade/ RUN apt-get update \ && apt-get -y install git sudo vim \ && apt-get clean \ - && pip install autopep8--no-cache-dir \ - # Install ALL dependencies - && pip install -r requirements-dev.txt --no-cache-dir \ - # Install documentation dependencies (to enable mkdocs) - && pip install -r docs/requirements-docs.txt --no-cache-dir \ - && useradd -m ftuser \ + && pip install autopep8 -r docs/requirements-docs.txt -r requirements-dev.txt --no-cache-dir \ + && useradd -u 1000 -U -m ftuser \ && mkdir -p /home/ftuser/.vscode-server /home/ftuser/.vscode-server-insiders /home/ftuser/commandhistory \ && echo "export PROMPT_COMMAND='history -a'" >> /home/ftuser/.bashrc \ && echo "export HISTFILE=~/commandhistory/.bash_history" >> /home/ftuser/.bashrc \ diff --git a/docker/docker-compose-jupyter.yml b/docker/docker-compose-jupyter.yml index 14e45983d..11a01705c 100644 --- a/docker/docker-compose-jupyter.yml +++ b/docker/docker-compose-jupyter.yml @@ -1,7 +1,7 @@ --- version: '3' services: - freqtrade: + ft_jupyterlab: build: context: .. dockerfile: docker/Dockerfile.jupyter diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index ad82aea3f..dd89ff2e7 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -178,7 +178,7 @@ Freqtrade provides a docker-compose file which starts up a jupyter lab server. You can run this server using the following command: ``` bash -docker-compose -f docker/docker-compose-jupyter.yml up +docker-compose --rm -f docker/docker-compose-jupyter.yml up ``` This will create a dockercontainer running jupyter lab, which will be accessible using `https://127.0.0.1:8888/lab`. From 4cb5c9c85fa941e09e256148d38b815becac5395 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2020 05:39:56 +0000 Subject: [PATCH 0680/1197] Bump mkdocs-material from 5.5.12 to 5.5.13 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 5.5.12 to 5.5.13. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/5.5.12...5.5.13) Signed-off-by: dependabot[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 6408616a0..d4c93928e 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==5.5.12 +mkdocs-material==5.5.13 mdx_truly_sane_lists==1.2 From d1b3a16c1369d8af8f4ed6ed5b1442b8b002a363 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2020 05:39:56 +0000 Subject: [PATCH 0681/1197] Bump ccxt from 1.34.25 to 1.34.40 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.34.25 to 1.34.40. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.34.25...1.34.40) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5da544a3c..44d2f29a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.2 pandas==1.1.2 -ccxt==1.34.25 +ccxt==1.34.40 SQLAlchemy==1.3.19 python-telegram-bot==12.8 arrow==0.16.0 From be33556838161a1ee3b7657a54737318863b2f0b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2020 05:40:00 +0000 Subject: [PATCH 0682/1197] Bump nbconvert from 6.0.2 to 6.0.4 Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 6.0.2 to 6.0.4. - [Release notes](https://github.com/jupyter/nbconvert/releases) - [Commits](https://github.com/jupyter/nbconvert/compare/6.0.2...6.0.4) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c14a146fa..ffe2763a6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -15,4 +15,4 @@ pytest-mock==3.3.1 pytest-random-order==1.0.4 # Convert jupyter notebooks to markdown documents -nbconvert==6.0.2 +nbconvert==6.0.4 From 4b06c9e0aeb03bb2fcbf1c3dc260c09fccd95edb Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 22 Sep 2020 19:37:18 +0200 Subject: [PATCH 0683/1197] Add test verifying wrong behaviour --- tests/test_wallets.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 884470014..450dabc4d 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -19,12 +19,17 @@ def test_sync_wallet_at_boot(mocker, default_conf): "used": 0.0, "total": 0.260739 }, + "USDT": { + "free": 20, + "used": 20, + "total": 40 + }, }) ) freqtrade = get_patched_freqtradebot(mocker, default_conf) - assert len(freqtrade.wallets._wallets) == 2 + assert len(freqtrade.wallets._wallets) == 3 assert freqtrade.wallets._wallets['BNT'].free == 1.0 assert freqtrade.wallets._wallets['BNT'].used == 2.0 assert freqtrade.wallets._wallets['BNT'].total == 3.0 @@ -32,6 +37,7 @@ def test_sync_wallet_at_boot(mocker, default_conf): assert freqtrade.wallets._wallets['GAS'].used == 0.0 assert freqtrade.wallets._wallets['GAS'].total == 0.260739 assert freqtrade.wallets.get_free('BNT') == 1.0 + assert 'USDT' in freqtrade.wallets._wallets assert freqtrade.wallets._last_wallet_refresh > 0 mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -51,6 +57,7 @@ def test_sync_wallet_at_boot(mocker, default_conf): freqtrade.wallets.update() + # USDT is missing from the 2nd result - so should not be in this either. assert len(freqtrade.wallets._wallets) == 2 assert freqtrade.wallets._wallets['BNT'].free == 1.2 assert freqtrade.wallets._wallets['BNT'].used == 1.9 From 6b46a35b19737009db78ca82c843a550a64a6243 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 22 Sep 2020 19:37:31 +0200 Subject: [PATCH 0684/1197] Fix bug of balances not disappearing --- freqtrade/wallets.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index b913155bc..ac08f337c 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -2,6 +2,7 @@ """ Wallet """ import logging +from copy import deepcopy from typing import Any, Dict, NamedTuple import arrow @@ -93,6 +94,10 @@ class Wallets: balances[currency].get('used', None), balances[currency].get('total', None) ) + # Remove currencies no longer in get_balances output + for currency in deepcopy(self._wallets): + if currency not in balances: + del self._wallets[currency] def update(self, require_update: bool = True) -> None: """ From bb56d392a92e250ccedc3df5b01c42860cfad99a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 22 Sep 2020 20:19:46 +0200 Subject: [PATCH 0685/1197] Fix typo in documentation --- docs/telegram-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index b718d40a6..ce2d715a0 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -44,7 +44,7 @@ Get your "Id", you will use it for the config parameter `chat_id`. ## Control telegram noise Freqtrade provides means to control the verbosity of your telegram bot. -Each setting has the follwoing possible values: +Each setting has the following possible values: * `on` - Messages will be sent, and user will be notified. * `silent` - Message will be sent, Notification will be without sound / vibration. From 28411da83eedd4d0441e1c8f5836d0f3a0864e39 Mon Sep 17 00:00:00 2001 From: Xu Wang Date: Tue, 22 Sep 2020 22:28:12 +0100 Subject: [PATCH 0686/1197] Add the telegram command function template. --- freqtrade/rpc/telegram.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a01efaed6..a2dae387e 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -108,6 +108,7 @@ class Telegram(RPC): CommandHandler('edge', self._edge), CommandHandler('help', self._help), CommandHandler('version', self._version), + CommandHandler('stats', self._stats), ] for handle in handles: self._updater.dispatcher.add_handler(handle) @@ -738,6 +739,19 @@ class Telegram(RPC): """ self._send_msg('*Version:* `{}`'.format(__version__)) + @authorized_only + def _stats(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /stats + https://github.com/freqtrade/freqtrade/issues/3783 + Show stats of recent trades + :param update: message update + :return: None + """ + # TODO: self._send_msg(...) + trades = self._rpc_trade_history(-1) + + @authorized_only def _show_config(self, update: Update, context: CallbackContext) -> None: """ From 378f03a5b187da0a264003d6c583197d9a7e2632 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Sep 2020 06:37:40 +0200 Subject: [PATCH 0687/1197] Add relevant parameters to stored backtest result --- freqtrade/optimize/optimize_reports.py | 10 ++++++++++ tests/optimize/test_optimize_reports.py | 5 ++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 771ac91fb..18ca7a3a7 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -277,6 +277,16 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], 'max_open_trades': (config['max_open_trades'] if config['max_open_trades'] != float('inf') else -1), 'timeframe': config['timeframe'], + # Parameters relevant for backtesting + 'stoploss': config['stoploss'], + 'trailing_stop': config.get('trailing_stop', False), + 'trailing_stop_positive': config.get('trailing_stop_positive'), + 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset', 0.0), + 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached', False), + 'minimal_roi': config['minimal_roi'], + 'use_sell_signal': config['ask_strategy']['use_sell_signal'], + 'sell_profit_only': config['ask_strategy']['sell_profit_only'], + 'ignore_roi_if_buy_signal': config['ask_strategy']['ignore_roi_if_buy_signal'], **daily_stats, } result['strategy'][strategy] = strat_stats diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 4f62e2e23..d61fd2bc5 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -5,7 +5,6 @@ from pathlib import Path import pandas as pd import pytest from arrow import Arrow - from freqtrade.configuration import TimeRange from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.data import history @@ -22,6 +21,7 @@ from freqtrade.optimize.optimize_reports import (generate_backtest_stats, text_table_bt_results, text_table_sell_reason, text_table_strategy) +from freqtrade.resolvers.strategy_resolver import StrategyResolver from freqtrade.strategy.interface import SellType from tests.data.test_history import _backup_file, _clean_test_file @@ -57,6 +57,9 @@ def test_text_table_bt_results(default_conf, mocker): def test_generate_backtest_stats(default_conf, testdatadir): + default_conf.update({'strategy': 'DefaultStrategy'}) + StrategyResolver.load_strategy(default_conf) + results = {'DefStrat': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC"], "profit_percent": [0.003312, 0.010801, 0.013803, 0.002780], From d49488bf0e56fb7708b81903abef2a1540cb6325 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 25 Sep 2020 05:47:58 +0000 Subject: [PATCH 0688/1197] Bump python from 3.8.5-slim-buster to 3.8.6-slim-buster Bumps python from 3.8.5-slim-buster to 3.8.6-slim-buster. Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index cdf96abcd..2be65274e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8.5-slim-buster +FROM python:3.8.6-slim-buster RUN apt-get update \ && apt-get -y install curl build-essential libssl-dev sqlite3 \ From ff3e2641aed30b31dcdafcd3c87c7c5be2f3e53d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Sep 2020 20:39:00 +0200 Subject: [PATCH 0689/1197] generate_backtest_stats must take config options from the strategy config as a strategy can override certain options. --- freqtrade/optimize/backtesting.py | 23 ++++-- freqtrade/optimize/optimize_reports.py | 33 ++++---- tests/optimize/test_optimize_reports.py | 101 +++++++++++++----------- 3 files changed, 85 insertions(+), 72 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 005ec9fb8..bfdb67ec2 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -380,12 +380,6 @@ class Backtesting: logger.info('Using stake_currency: %s ...', self.config['stake_currency']) logger.info('Using stake_amount: %s ...', self.config['stake_amount']) - # Use max_open_trades in backtesting, except --disable-max-market-positions is set - if self.config.get('use_max_market_positions', True): - max_open_trades = self.config['max_open_trades'] - else: - logger.info('Ignoring max_open_trades (--disable-max-market-positions was used) ...') - max_open_trades = 0 position_stacking = self.config.get('position_stacking', False) data, timerange = self.load_bt_data() @@ -395,6 +389,15 @@ class Backtesting: logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) self._set_strategy(strat) + # Use max_open_trades in backtesting, except --disable-max-market-positions is set + if self.config.get('use_max_market_positions', True): + # Must come from strategy config, as the strategy may modify this setting. + max_open_trades = self.strategy.config['max_open_trades'] + else: + logger.info( + 'Ignoring max_open_trades (--disable-max-market-positions was used) ...') + max_open_trades = 0 + # need to reprocess data every time to populate signals preprocessed = self.strategy.ohlcvdata_to_dataframe(data) @@ -407,7 +410,7 @@ class Backtesting: f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' f'({(max_date - min_date).days} days)..') # Execute backtest and print results - all_results[self.strategy.get_strategy_name()] = self.backtest( + results = self.backtest( processed=preprocessed, stake_amount=self.config['stake_amount'], start_date=min_date, @@ -415,8 +418,12 @@ class Backtesting: max_open_trades=max_open_trades, position_stacking=position_stacking, ) + all_results[self.strategy.get_strategy_name()] = { + 'results': results, + 'config': self.strategy.config, + } - stats = generate_backtest_stats(self.config, data, all_results, + stats = generate_backtest_stats(data, all_results, min_date=min_date, max_date=max_date) if self.config.get('export', False): store_backtest_stats(self.config['exportfilename'], stats) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 18ca7a3a7..696e63b25 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -1,7 +1,7 @@ import logging from datetime import datetime, timedelta, timezone from pathlib import Path -from typing import Any, Dict, List +from typing import Any, Dict, List, Union from arrow import Arrow from pandas import DataFrame @@ -143,19 +143,18 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List return tabular_data -def generate_strategy_metrics(stake_currency: str, max_open_trades: int, - all_results: Dict) -> List[Dict]: +def generate_strategy_metrics(all_results: Dict) -> List[Dict]: """ Generate summary per strategy - :param stake_currency: stake-currency - used to correctly name headers - :param max_open_trades: Maximum allowed open trades used for backtest :param all_results: Dict of containing results for all strategies :return: List of Dicts containing the metrics per Strategy """ tabular_data = [] for strategy, results in all_results.items(): - tabular_data.append(_generate_result_line(results, max_open_trades, strategy)) + tabular_data.append(_generate_result_line( + results['results'], results['config']['max_open_trades'], strategy) + ) return tabular_data @@ -219,25 +218,29 @@ def generate_daily_stats(results: DataFrame) -> Dict[str, Any]: } -def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], - all_results: Dict[str, DataFrame], +def generate_backtest_stats(btdata: Dict[str, DataFrame], + all_results: Dict[str, Dict[str, Union[DataFrame, Dict]]], min_date: Arrow, max_date: Arrow ) -> Dict[str, Any]: """ - :param config: Configuration object used for backtest :param btdata: Backtest data - :param all_results: backtest result - dictionary with { Strategy: results}. + :param all_results: backtest result - dictionary in the form: + { Strategy: {'results: results, 'config: config}}. :param min_date: Backtest start date :param max_date: Backtest end date :return: Dictionary containing results per strategy and a stratgy summary. """ - stake_currency = config['stake_currency'] - max_open_trades = config['max_open_trades'] result: Dict[str, Any] = {'strategy': {}} market_change = calculate_market_change(btdata, 'close') - for strategy, results in all_results.items(): + for strategy, content in all_results.items(): + results: Dict[str, DataFrame] = content['results'] + if not isinstance(results, DataFrame): + continue + config = content['config'] + max_open_trades = config['max_open_trades'] + stake_currency = config['stake_currency'] pair_results = generate_pair_metrics(btdata, stake_currency=stake_currency, max_open_trades=max_open_trades, @@ -310,9 +313,7 @@ def generate_backtest_stats(config: Dict, btdata: Dict[str, DataFrame], 'drawdown_end_ts': 0, }) - strategy_results = generate_strategy_metrics(stake_currency=stake_currency, - max_open_trades=max_open_trades, - all_results=all_results) + strategy_results = generate_strategy_metrics(all_results=all_results) result['strategy_comparison'] = strategy_results diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index d61fd2bc5..fe030e315 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -60,32 +60,35 @@ def test_generate_backtest_stats(default_conf, testdatadir): default_conf.update({'strategy': 'DefaultStrategy'}) StrategyResolver.load_strategy(default_conf) - results = {'DefStrat': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", - "UNITTEST/BTC", "UNITTEST/BTC"], - "profit_percent": [0.003312, 0.010801, 0.013803, 0.002780], - "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], - "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, - Arrow(2017, 11, 14, 21, 36, 00).datetime, - Arrow(2017, 11, 14, 22, 12, 00).datetime, - Arrow(2017, 11, 14, 22, 44, 00).datetime], - "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, - Arrow(2017, 11, 14, 22, 10, 00).datetime, - Arrow(2017, 11, 14, 22, 43, 00).datetime, - Arrow(2017, 11, 14, 22, 58, 00).datetime], - "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], - "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], - "trade_duration": [123, 34, 31, 14], - "open_at_end": [False, False, False, True], - "sell_reason": [SellType.ROI, SellType.STOP_LOSS, - SellType.ROI, SellType.FORCE_SELL] - })} + results = {'DefStrat': { + 'results': pd.DataFrame({"pair": ["UNITTEST/BTC", "UNITTEST/BTC", + "UNITTEST/BTC", "UNITTEST/BTC"], + "profit_percent": [0.003312, 0.010801, 0.013803, 0.002780], + "profit_abs": [0.000003, 0.000011, 0.000014, 0.000003], + "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, + Arrow(2017, 11, 14, 21, 36, 00).datetime, + Arrow(2017, 11, 14, 22, 12, 00).datetime, + Arrow(2017, 11, 14, 22, 44, 00).datetime], + "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, + Arrow(2017, 11, 14, 22, 10, 00).datetime, + Arrow(2017, 11, 14, 22, 43, 00).datetime, + Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], + "close_rate": [0.002546, 0.003014, 0.003103, 0.003217], + "trade_duration": [123, 34, 31, 14], + "open_at_end": [False, False, False, True], + "sell_reason": [SellType.ROI, SellType.STOP_LOSS, + SellType.ROI, SellType.FORCE_SELL] + }), + 'config': default_conf} + } timerange = TimeRange.parse_timerange('1510688220-1510700340') min_date = Arrow.fromtimestamp(1510688220) max_date = Arrow.fromtimestamp(1510700340) btdata = history.load_data(testdatadir, '1m', ['UNITTEST/BTC'], timerange=timerange, fill_up_missing=True) - stats = generate_backtest_stats(default_conf, btdata, results, min_date, max_date) + stats = generate_backtest_stats(btdata, results, min_date, max_date) assert isinstance(stats, dict) assert 'strategy' in stats assert 'DefStrat' in stats['strategy'] @@ -93,29 +96,32 @@ def test_generate_backtest_stats(default_conf, testdatadir): strat_stats = stats['strategy']['DefStrat'] assert strat_stats['backtest_start'] == min_date.datetime assert strat_stats['backtest_end'] == max_date.datetime - assert strat_stats['total_trades'] == len(results['DefStrat']) + assert strat_stats['total_trades'] == len(results['DefStrat']['results']) # Above sample had no loosing trade assert strat_stats['max_drawdown'] == 0.0 - results = {'DefStrat': pd.DataFrame( - {"pair": ["UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC"], - "profit_percent": [0.003312, 0.010801, -0.013803, 0.002780], - "profit_abs": [0.000003, 0.000011, -0.000014, 0.000003], - "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, - Arrow(2017, 11, 14, 21, 36, 00).datetime, - Arrow(2017, 11, 14, 22, 12, 00).datetime, - Arrow(2017, 11, 14, 22, 44, 00).datetime], - "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, - Arrow(2017, 11, 14, 22, 10, 00).datetime, - Arrow(2017, 11, 14, 22, 43, 00).datetime, - Arrow(2017, 11, 14, 22, 58, 00).datetime], - "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], - "close_rate": [0.002546, 0.003014, 0.0032903, 0.003217], - "trade_duration": [123, 34, 31, 14], - "open_at_end": [False, False, False, True], - "sell_reason": [SellType.ROI, SellType.STOP_LOSS, - SellType.ROI, SellType.FORCE_SELL] - })} + results = {'DefStrat': { + 'results': pd.DataFrame( + {"pair": ["UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC", "UNITTEST/BTC"], + "profit_percent": [0.003312, 0.010801, -0.013803, 0.002780], + "profit_abs": [0.000003, 0.000011, -0.000014, 0.000003], + "open_date": [Arrow(2017, 11, 14, 19, 32, 00).datetime, + Arrow(2017, 11, 14, 21, 36, 00).datetime, + Arrow(2017, 11, 14, 22, 12, 00).datetime, + Arrow(2017, 11, 14, 22, 44, 00).datetime], + "close_date": [Arrow(2017, 11, 14, 21, 35, 00).datetime, + Arrow(2017, 11, 14, 22, 10, 00).datetime, + Arrow(2017, 11, 14, 22, 43, 00).datetime, + Arrow(2017, 11, 14, 22, 58, 00).datetime], + "open_rate": [0.002543, 0.003003, 0.003089, 0.003214], + "close_rate": [0.002546, 0.003014, 0.0032903, 0.003217], + "trade_duration": [123, 34, 31, 14], + "open_at_end": [False, False, False, True], + "sell_reason": [SellType.ROI, SellType.STOP_LOSS, + SellType.ROI, SellType.FORCE_SELL] + }), + 'config': default_conf} + } assert strat_stats['max_drawdown'] == 0.0 assert strat_stats['drawdown_start'] == Arrow.fromtimestamp(0).datetime @@ -283,9 +289,10 @@ def test_generate_sell_reason_stats(default_conf): assert stop_result['profit_mean_pct'] == round(stop_result['profit_mean'] * 100, 2) -def test_text_table_strategy(default_conf, mocker): +def test_text_table_strategy(default_conf): + default_conf['max_open_trades'] = 2 results = {} - results['TestStrategy1'] = pd.DataFrame( + results['TestStrategy1'] = {'results': pd.DataFrame( { 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], 'profit_percent': [0.1, 0.2, 0.3], @@ -296,8 +303,8 @@ def test_text_table_strategy(default_conf, mocker): 'losses': [0, 0, 1], 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] } - ) - results['TestStrategy2'] = pd.DataFrame( + ), 'config': default_conf} + results['TestStrategy2'] = {'results': pd.DataFrame( { 'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'], 'profit_percent': [0.4, 0.2, 0.3], @@ -308,7 +315,7 @@ def test_text_table_strategy(default_conf, mocker): 'losses': [0, 0, 1], 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] } - ) + ), 'config': default_conf} result_str = ( '| Strategy | Buys | Avg Profit % | Cum Profit % | Tot' @@ -321,9 +328,7 @@ def test_text_table_strategy(default_conf, mocker): ' 45.00 | 0:20:00 | 3 | 0 | 0 |' ) - strategy_results = generate_strategy_metrics(stake_currency='BTC', - max_open_trades=2, - all_results=results) + strategy_results = generate_strategy_metrics(all_results=results) assert text_table_strategy(strategy_results, 'BTC') == result_str From c56dd487f25a215a20f78939f81ee59651b7c68f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Sep 2020 21:00:58 +0200 Subject: [PATCH 0690/1197] Fix test failure --- tests/optimize/test_backtesting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index f5c313520..78a7130f9 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -14,7 +14,7 @@ from freqtrade.commands.optimize_commands import (setup_optimize_configuration, start_backtesting) from freqtrade.configuration import TimeRange from freqtrade.data import history -from freqtrade.data.btanalysis import evaluate_result_multi +from freqtrade.data.btanalysis import BT_DATA_COLUMNS, evaluate_result_multi from freqtrade.data.converter import clean_ohlcv_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange @@ -694,7 +694,7 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): patch_exchange(mocker) - backtestmock = MagicMock() + backtestmock = MagicMock(return_value=pd.DataFrame(columns=BT_DATA_COLUMNS + ['profit_abs'])) mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) From bb27b236ceb29ad0a37fefb8f1b290654c6ff5ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Sep 2020 14:55:12 +0200 Subject: [PATCH 0691/1197] Remove unused arguments --- freqtrade/optimize/backtesting.py | 4 ++-- tests/optimize/test_optimize_reports.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index bfdb67ec2..8d4a3a205 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -423,8 +423,8 @@ class Backtesting: 'config': self.strategy.config, } - stats = generate_backtest_stats(data, all_results, - min_date=min_date, max_date=max_date) + stats = generate_backtest_stats(data, all_results, min_date=min_date, max_date=max_date) + if self.config.get('export', False): store_backtest_stats(self.config['exportfilename'], stats) diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index fe030e315..b484e4390 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -26,7 +26,7 @@ from freqtrade.strategy.interface import SellType from tests.data.test_history import _backup_file, _clean_test_file -def test_text_table_bt_results(default_conf, mocker): +def test_text_table_bt_results(): results = pd.DataFrame( { @@ -174,7 +174,7 @@ def test_store_backtest_stats(testdatadir, mocker): assert str(dump_mock.call_args_list[0][0][0]).startswith(str(testdatadir / 'testresult')) -def test_generate_pair_metrics(default_conf, mocker): +def test_generate_pair_metrics(): results = pd.DataFrame( { @@ -222,7 +222,7 @@ def test_generate_daily_stats(testdatadir): assert res['losing_days'] == 0 -def test_text_table_sell_reason(default_conf): +def test_text_table_sell_reason(): results = pd.DataFrame( { @@ -254,7 +254,7 @@ def test_text_table_sell_reason(default_conf): stake_currency='BTC') == result_str -def test_generate_sell_reason_stats(default_conf): +def test_generate_sell_reason_stats(): results = pd.DataFrame( { @@ -333,7 +333,7 @@ def test_text_table_strategy(default_conf): assert text_table_strategy(strategy_results, 'BTC') == result_str -def test_generate_edge_table(edge_conf, mocker): +def test_generate_edge_table(): results = {} results['ETH/BTC'] = PairInfo(-0.01, 0.60, 2, 1, 3, 10, 60) From 44ad0f631c00ed00063fa61ebe3aefd6b5f736a1 Mon Sep 17 00:00:00 2001 From: Xu Wang Date: Sat, 26 Sep 2020 22:40:54 +0100 Subject: [PATCH 0692/1197] Summarize trade reason for telegram command /stats. --- freqtrade/rpc/telegram.py | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a2dae387e..47e9d67dc 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -749,9 +749,40 @@ class Telegram(RPC): :return: None """ # TODO: self._send_msg(...) - trades = self._rpc_trade_history(-1) - + def trade_win_loss(trade): + if trade['profit_abs'] > 0: + return 'Wins' + elif trade['profit_abs'] < 0: + return 'Losses' + else: + return 'Draws' + trades = self._rpc_trade_history(-1) + trades_closed = [trade for trade in trades if not trade['is_open']] + + # Sell reason + sell_reasons = {} + for trade in trades_closed: + if trade['sell_reason'] in sell_reasons: + sell_reasons[trade['sell_reason']][trade_win_loss(trade)] += 1 + else: + win_loss_count = {'Wins': 0, 'Losses': 0, 'Draws': 0} + win_loss_count[trade_win_loss(trade)] += 1 + sell_reasons[trade['sell_reason']] = win_loss_count + sell_reason_msg = [ + '| Sell Reason | Sells | Wins | Draws | Losses |', + '|-------------|------:|-----:|------:|-------:|' + ] + # | Sell Reason | Sells | Wins | Draws | Losses | + # |-------------|------:|-----:|------:|-------:| + # | test | 1 | 2 | 3 | 4 | + for reason, count in sell_reasons.items(): + msg = f'| `{reason}` | `{sum(count.values())}` | `{count['Wins']}` | `{count['Draws']}` | `{count['Losses']}` |' + sell_reason_msg.append(msg) + + # TODO: Duration + + @authorized_only def _show_config(self, update: Update, context: CallbackContext) -> None: """ From b736691e0e5a80a54108f20e3d438d265a281ca8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Sep 2020 16:18:28 +0200 Subject: [PATCH 0693/1197] Remove hyperopt --continue --- freqtrade/commands/arguments.py | 2 +- freqtrade/commands/cli_options.py | 7 ------- freqtrade/configuration/configuration.py | 3 --- freqtrade/optimize/hyperopt.py | 5 +---- tests/optimize/test_hyperopt.py | 11 ----------- 5 files changed, 2 insertions(+), 26 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index b61a4933e..a9ad6bc63 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -26,7 +26,7 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", "use_max_market_positions", "print_all", "print_colorized", "print_json", "hyperopt_jobs", "hyperopt_random_state", "hyperopt_min_trades", - "hyperopt_continue", "hyperopt_loss"] + "hyperopt_loss"] ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 81b8de1af..eeadd62f6 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -252,13 +252,6 @@ AVAILABLE_CLI_OPTIONS = { metavar='INT', default=1, ), - "hyperopt_continue": Arg( - "--continue", - help="Continue hyperopt from previous runs. " - "By default, temporary files will be removed and hyperopt will start from scratch.", - default=False, - action='store_true', - ), "hyperopt_loss": Arg( '--hyperopt-loss', help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). ' diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 930917fae..4a53ef7e5 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -295,9 +295,6 @@ class Configuration: self._args_to_config(config, argname='hyperopt_min_trades', logstring='Parameter --min-trades detected: {}') - self._args_to_config(config, argname='hyperopt_continue', - logstring='Hyperopt continue: {}') - self._args_to_config(config, argname='hyperopt_loss', logstring='Using Hyperopt loss class name: {}') diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 37de3bc4b..8682aa08d 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -86,10 +86,7 @@ class Hyperopt: self.current_best_loss = 100 - if not self.config.get('hyperopt_continue'): - self.clean_hyperopt() - else: - logger.info("Continuing on previous hyperopt results.") + self.clean_hyperopt() self.num_epochs_saved = 0 diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index d58b91209..a35b8b655 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -839,17 +839,6 @@ def test_clean_hyperopt(mocker, hyperopt_conf, caplog): assert log_has(f"Removing `{h.data_pickle_file}`.", caplog) -def test_continue_hyperopt(mocker, hyperopt_conf, caplog): - patch_exchange(mocker) - hyperopt_conf.update({'hyperopt_continue': True}) - mocker.patch("freqtrade.optimize.hyperopt.Path.is_file", MagicMock(return_value=True)) - unlinkmock = mocker.patch("freqtrade.optimize.hyperopt.Path.unlink", MagicMock()) - Hyperopt(hyperopt_conf) - - assert unlinkmock.call_count == 0 - assert log_has("Continuing on previous hyperopt results.", caplog) - - def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', From 7a652b07d587acfef3cc5cd090289a38948ea475 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Sep 2020 16:21:55 +0200 Subject: [PATCH 0694/1197] UPdate documentation to remove --continue --- docs/bot-usage.md | 5 +---- docs/hyperopt.md | 15 ++++++--------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 4a4496bbc..62c515b44 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -303,7 +303,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]] [--dmmp] [--print-all] [--no-color] [--print-json] [-j JOBS] [--random-state INT] [--min-trades INT] - [--continue] [--hyperopt-loss NAME] + [--hyperopt-loss NAME] optional arguments: -h, --help show this help message and exit @@ -349,9 +349,6 @@ optional arguments: reproducible hyperopt results. --min-trades INT Set minimal desired number of trades for evaluations in the hyperopt optimization path (default: 1). - --continue Continue hyperopt from previous runs. By default, - temporary files will be removed and hyperopt will - start from scratch. --hyperopt-loss NAME Specify the class name of the hyperopt loss function class (IHyperOptLoss). Different functions can generate completely different results, since the diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 3f7a27ef0..6162f766a 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -102,7 +102,7 @@ There you have two different types of indicators: 1. `guards` and 2. `triggers`. 1. Guards are conditions like "never buy if ADX < 10", or never buy if current price is over EMA10. 2. Triggers are ones that actually trigger buy in specific moment, like "buy when EMA5 crosses over EMA10" or "buy when close price touches lower Bollinger band". -Hyperoptimization will, for each eval round, pick one trigger and possibly +Hyper-optimization will, for each epoch round, pick one trigger and possibly multiple guards. The constructed strategy will be something like "*buy exactly when close price touches lower Bollinger band, BUT only if ADX > 10*". @@ -240,10 +240,7 @@ running at least several thousand evaluations. The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below. !!! Note - By default, hyperopt will erase previous results and start from scratch. Continuation can be archived by using `--continue`. - -!!! Warning - When switching parameters or changing configuration options, make sure to not use the argument `--continue` so temporary results can be removed. + By default, hyperopt will erase previous results and start from scratch. ### Execute Hyperopt with different historical data source @@ -253,7 +250,7 @@ uses data from directory `user_data/data`. ### Running Hyperopt with Smaller Testset -Use the `--timerange` argument to change how much of the testset you want to use. +Use the `--timerange` argument to change how much of the test-set you want to use. For example, to use one month of data, pass the following parameter to the hyperopt call: ```bash @@ -318,7 +315,7 @@ The initial state for generation of these random values (random state) is contro 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. +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 hyper-optimization results with same random state value used. ## Understand the Hyperopt Result @@ -371,7 +368,7 @@ By default, hyperopt prints colorized results -- epochs with positive profit are You can use the `--print-all` command line option if you would like to see all results in the hyperopt output, not only the best ones. When `--print-all` is used, current best results are also colorized by default -- they are printed in bold (bright) style. This can also be switched off with the `--no-color` command line option. !!! Note "Windows and color output" - Windows does not support color-output nativly, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL. + Windows does not support color-output natively, therefore it is automatically disabled. To have color-output for hyperopt running under windows, please consider using WSL. ### Understand Hyperopt ROI results @@ -494,7 +491,7 @@ Override the `trailing_space()` method and define the desired range in it if you ## Show details of Hyperopt results -After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` subcommands. The usage of these subcommands is described in the [Utils](utils.md#list-hyperopt-results) chapter. +After you run Hyperopt for the desired amount of epochs, you can later list all results for analysis, select only best or profitable once, and show the details for any of the epochs previously evaluated. This can be done with the `hyperopt-list` and `hyperopt-show` sub-commands. The usage of these sub-commands is described in the [Utils](utils.md#list-hyperopt-results) chapter. ## Validate backtesting results From ff96cf154c5c8b9c9595f695b0f048765af59be8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Sep 2020 16:33:26 +0200 Subject: [PATCH 0695/1197] Keep hyperopt result history --- freqtrade/optimize/hyperopt.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 8682aa08d..c07fa1788 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -10,6 +10,7 @@ import logging import random import warnings from collections import OrderedDict +from datetime import datetime from math import ceil from operator import itemgetter from pathlib import Path @@ -25,16 +26,15 @@ from joblib import (Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects) from pandas import DataFrame, isna, json_normalize -from freqtrade.constants import DATETIME_PRINT_FORMAT +from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN from freqtrade.data.converter import trim_dataframe from freqtrade.data.history import get_timerange from freqtrade.exceptions import OperationalException -from freqtrade.misc import plural, round_dict +from freqtrade.misc import file_dump_json, plural, round_dict from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 -from freqtrade.optimize.hyperopt_loss_interface import \ - IHyperOptLoss # noqa: F401 +from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401 from freqtrade.resolvers.hyperopt_resolver import (HyperOptLossResolver, HyperOptResolver) from freqtrade.strategy import IStrategy @@ -77,9 +77,9 @@ class Hyperopt: self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config) self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function - + time_now = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") self.results_file = (self.config['user_data_dir'] / - 'hyperopt_results' / 'hyperopt_results.pickle') + 'hyperopt_results' / f'hyperopt_results_{time_now}.pickle') self.data_pickle_file = (self.config['user_data_dir'] / 'hyperopt_results' / 'hyperopt_tickerdata.pkl') self.total_epochs = config.get('epochs', 0) @@ -162,6 +162,9 @@ class Hyperopt: self.num_epochs_saved = num_epochs logger.debug(f"{self.num_epochs_saved} {plural(self.num_epochs_saved, 'epoch')} " f"saved to '{self.results_file}'.") + # Store hyperopt filename + latest_filename = Path.joinpath(self.results_file.parent, LAST_BT_RESULT_FN) + file_dump_json(latest_filename, {'latest_hyperopt': str(self.results_file.name)}) @staticmethod def _read_results(results_file: Path) -> List: From c42a924df85150092d106bfd739f78a325513f25 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Sep 2020 16:50:22 +0200 Subject: [PATCH 0696/1197] Load latest file --- freqtrade/commands/hyperopt_commands.py | 8 ++-- freqtrade/data/btanalysis.py | 50 +++++++++++++++++++++++-- freqtrade/optimize/hyperopt.py | 2 - tests/optimize/test_hyperopt.py | 2 + 4 files changed, 53 insertions(+), 9 deletions(-) diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index 4fae51e28..de8764369 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -7,6 +7,7 @@ from colorama import init as colorama_init from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode +from freqtrade.data.btanalysis import get_latest_hyperopt_file logger = logging.getLogger(__name__) @@ -40,8 +41,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: 'filter_max_objective': config.get('hyperopt_list_max_objective', None), } - results_file = (config['user_data_dir'] / - 'hyperopt_results' / 'hyperopt_results.pickle') + results_file = get_latest_hyperopt_file(config['user_data_dir'] / 'hyperopt_results') # Previous evaluations epochs = Hyperopt.load_previous_results(results_file) @@ -80,8 +80,8 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: print_json = config.get('print_json', False) no_header = config.get('hyperopt_show_no_header', False) - results_file = (config['user_data_dir'] / - 'hyperopt_results' / 'hyperopt_results.pickle') + results_file = get_latest_hyperopt_file(config['user_data_dir'] / 'hyperopt_results') + n = config.get('hyperopt_show_index', -1) filteroptions = { diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 2d45a7222..55e4f11c4 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -21,10 +21,11 @@ BT_DATA_COLUMNS = ["pair", "profit_percent", "open_date", "close_date", "index", "open_rate", "close_rate", "open_at_end", "sell_reason"] -def get_latest_backtest_filename(directory: Union[Path, str]) -> str: +def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str: """ Get latest backtest export based on '.last_result.json'. :param directory: Directory to search for last result + :param variant: 'backtest' or 'hyperopt' - the method to return :return: string containing the filename of the latest backtest result :raises: ValueError in the following cases: * Directory does not exist @@ -44,10 +45,53 @@ def get_latest_backtest_filename(directory: Union[Path, str]) -> str: with filename.open() as file: data = json_load(file) - if 'latest_backtest' not in data: + if f'latest_{variant}' not in data: raise ValueError(f"Invalid '{LAST_BT_RESULT_FN}' format.") - return data['latest_backtest'] + return data[f'latest_{variant}'] + + +def get_latest_backtest_filename(directory: Union[Path, str]) -> str: + """ + Get latest backtest export based on '.last_result.json'. + :param directory: Directory to search for last result + :return: string containing the filename of the latest backtest result + :raises: ValueError in the following cases: + * Directory does not exist + * `directory/.last_result.json` does not exist + * `directory/.last_result.json` has the wrong content + """ + return get_latest_optimize_filename(directory, 'backtest') + + +def get_latest_hyperopt_filename(directory: Union[Path, str]) -> str: + """ + Get latest hyperopt export based on '.last_result.json'. + :param directory: Directory to search for last result + :return: string containing the filename of the latest hyperopt result + :raises: ValueError in the following cases: + * Directory does not exist + * `directory/.last_result.json` does not exist + * `directory/.last_result.json` has the wrong content + """ + try: + return get_latest_optimize_filename(directory, 'hyperopt') + except ValueError: + # Return default (legacy) pickle filename + return 'hyperopt_results.pickle' + + +def get_latest_hyperopt_file(directory: Union[Path, str]) -> Path: + """ + Get latest hyperopt export based on '.last_result.json'. + :param directory: Directory to search for last result + :return: string containing the filename of the latest hyperopt result + :raises: ValueError in the following cases: + * Directory does not exist + * `directory/.last_result.json` does not exist + * `directory/.last_result.json` has the wrong content + """ + return directory / get_latest_hyperopt_filename(directory) def load_backtest_stats(filename: Union[Path, str]) -> Dict[str, Any]: diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index c07fa1788..9d16bc6ba 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -657,8 +657,6 @@ class Hyperopt: self.backtesting.strategy.dp = None # type: ignore IStrategy.dp = None # type: ignore - self.epochs = self.load_previous_results(self.results_file) - cpus = cpu_count() logger.info(f"Found {cpus} CPU cores. Let's make them scream!") config_jobs = self.config.get('hyperopt_jobs', -1) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index a35b8b655..ec911c113 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -497,6 +497,7 @@ def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None: def test_save_results_saves_epochs(mocker, hyperopt, testdatadir, caplog) -> None: epochs = create_results(mocker, hyperopt, testdatadir) mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None) + mock_dump_json = mocker.patch('freqtrade.optimize.hyperopt.file_dump_json', return_value=None) results_file = testdatadir / 'optimize' / 'ut_results.pickle' caplog.set_level(logging.DEBUG) @@ -505,6 +506,7 @@ def test_save_results_saves_epochs(mocker, hyperopt, testdatadir, caplog) -> Non hyperopt._save_results() assert log_has(f"1 epoch saved to '{results_file}'.", caplog) mock_dump.assert_called_once() + mock_dump_json.assert_called_once() hyperopt.epochs = epochs + epochs hyperopt._save_results() From 3cb1a9a5a959a0c34ccf50219c5cb31aa194a557 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Sep 2020 17:00:23 +0200 Subject: [PATCH 0697/1197] Support loading results from a specific hyperopt history file --- freqtrade/commands/arguments.py | 4 ++-- freqtrade/commands/cli_options.py | 6 ++++++ freqtrade/commands/hyperopt_commands.py | 9 +++++++-- freqtrade/configuration/configuration.py | 3 +++ freqtrade/data/btanalysis.py | 5 ++++- tests/data/test_btanalysis.py | 9 +++++++++ 6 files changed, 31 insertions(+), 5 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index a9ad6bc63..72f60eb32 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -75,10 +75,10 @@ ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_list_min_total_profit", "hyperopt_list_max_total_profit", "hyperopt_list_min_objective", "hyperopt_list_max_objective", "print_colorized", "print_json", "hyperopt_list_no_details", - "export_csv"] + "hyperoptexportfilename", "export_csv"] ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_show_index", - "print_json", "hyperopt_show_no_header"] + "print_json", "hyperoptexportfilename", "hyperopt_show_no_header"] NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes", "list-markets", "list-pairs", "list-strategies", "list-data", diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index eeadd62f6..cd0bc426d 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -263,6 +263,12 @@ AVAILABLE_CLI_OPTIONS = { metavar='NAME', default=constants.DEFAULT_HYPEROPT_LOSS, ), + "hyperoptexportfilename": Arg( + '--hyperopt-filename', + help='Hyperopt result filename.' + 'Example: `--hyperopt-filename=hyperopt_results_2020-09-27_16-20-48.pickle`', + metavar='PATH', + ), # List exchanges "print_one_column": Arg( '-1', '--one-column', diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index de8764369..9075d19d2 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -41,7 +41,9 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: 'filter_max_objective': config.get('hyperopt_list_max_objective', None), } - results_file = get_latest_hyperopt_file(config['user_data_dir'] / 'hyperopt_results') + results_file = get_latest_hyperopt_file( + config['user_data_dir'] / 'hyperopt_results', + config.get('hyperoptexportfilename')) # Previous evaluations epochs = Hyperopt.load_previous_results(results_file) @@ -80,7 +82,10 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: print_json = config.get('print_json', False) no_header = config.get('hyperopt_show_no_header', False) - results_file = get_latest_hyperopt_file(config['user_data_dir'] / 'hyperopt_results') + results_file = get_latest_hyperopt_file( + config['user_data_dir'] / 'hyperopt_results', + config.get('hyperoptexportfilename')) + n = config.get('hyperopt_show_index', -1) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 4a53ef7e5..d022c1e62 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -263,6 +263,9 @@ class Configuration: self._args_to_config(config, argname='hyperopt_path', logstring='Using additional Hyperopt lookup path: {}') + self._args_to_config(config, argname='hyperoptexportfilename', + logstring='Using hyperopt file: {}') + self._args_to_config(config, argname='epochs', logstring='Parameter --epochs detected ... ' 'Will run Hyperopt with for {} epochs ...' diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 55e4f11c4..d4edd41a8 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -81,7 +81,7 @@ def get_latest_hyperopt_filename(directory: Union[Path, str]) -> str: return 'hyperopt_results.pickle' -def get_latest_hyperopt_file(directory: Union[Path, str]) -> Path: +def get_latest_hyperopt_file(directory: Union[Path, str], predef_filename: str = None) -> Path: """ Get latest hyperopt export based on '.last_result.json'. :param directory: Directory to search for last result @@ -91,6 +91,9 @@ def get_latest_hyperopt_file(directory: Union[Path, str]) -> Path: * `directory/.last_result.json` does not exist * `directory/.last_result.json` has the wrong content """ + + if predef_filename: + return directory / predef_filename return directory / get_latest_hyperopt_filename(directory) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 564dae0b1..43b1a7a4b 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -15,6 +15,7 @@ from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, create_cum_profit, extract_trades_of_period, get_latest_backtest_filename, + get_latest_hyperopt_file, load_backtest_data, load_trades, load_trades_from_db) from freqtrade.data.history import load_data, load_pair_history @@ -43,6 +44,14 @@ def test_get_latest_backtest_filename(testdatadir, mocker): get_latest_backtest_filename(testdatadir) +def test_get_latest_hyperopt_file(testdatadir, mocker): + res = get_latest_hyperopt_file(testdatadir / 'does_not_exist', 'testfile.pickle') + assert res == testdatadir / 'does_not_exist/testfile.pickle' + + res = get_latest_hyperopt_file(testdatadir.parent) + assert res == testdatadir.parent / "hyperopt_results.pickle" + + def test_load_backtest_data_old_format(testdatadir): filename = testdatadir / "backtest-result_test.json" From 8de9c46110f6420d12570d55a07b6c6d8c3bc85a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Sep 2020 17:09:33 +0200 Subject: [PATCH 0698/1197] Document hyperopt-filename usage --- docs/hyperopt.md | 3 +- docs/utils.md | 62 +++++++++++++++++++++++-------- freqtrade/commands/cli_options.py | 2 +- 3 files changed, 50 insertions(+), 17 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 6162f766a..8bfd2cf7d 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -240,7 +240,8 @@ running at least several thousand evaluations. The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below. !!! Note - By default, hyperopt will erase previous results and start from scratch. + Hyperopt will store hyperopt results with the timestamp of the hyperopt start time. + Reading commands (`hyperopt-list`, `hyperopt-show`) can use `--hyperopt-filename ` to read and display older hyperopt results. ### Execute Hyperopt with different historical data source diff --git a/docs/utils.md b/docs/utils.md index 8c7e381ff..409bcc134 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -423,7 +423,7 @@ freqtrade test-pairlist --config config.json --quote USDT BTC ## List Hyperopt results -You can list the hyperoptimization epochs the Hyperopt module evaluated previously with the `hyperopt-list` subcommand. +You can list the hyperoptimization epochs the Hyperopt module evaluated previously with the `hyperopt-list` sub-command. ``` usage: freqtrade hyperopt-list [-h] [-v] [--logfile FILE] [-V] [-c PATH] @@ -432,10 +432,11 @@ usage: freqtrade hyperopt-list [-h] [-v] [--logfile FILE] [-V] [-c PATH] [--max-trades INT] [--min-avg-time FLOAT] [--max-avg-time FLOAT] [--min-avg-profit FLOAT] [--max-avg-profit FLOAT] - [--min-total-profit FLOAT] [--max-total-profit FLOAT] + [--min-total-profit FLOAT] + [--max-total-profit FLOAT] [--min-objective FLOAT] [--max-objective FLOAT] [--no-color] [--print-json] [--no-details] - [--export-csv FILE] + [--hyperopt-filename PATH] [--export-csv FILE] optional arguments: -h, --help show this help message and exit @@ -443,24 +444,27 @@ optional arguments: --profitable Select only profitable epochs. --min-trades INT Select epochs with more than INT trades. --max-trades INT Select epochs with less than INT trades. - --min-avg-time FLOAT Select epochs on above average time. - --max-avg-time FLOAT Select epochs on under average time. + --min-avg-time FLOAT Select epochs above average time. + --max-avg-time FLOAT Select epochs below average time. --min-avg-profit FLOAT - Select epochs on above average profit. + Select epochs above average profit. --max-avg-profit FLOAT - Select epochs on below average profit. + Select epochs below average profit. --min-total-profit FLOAT - Select epochs on above total profit. + Select epochs above total profit. --max-total-profit FLOAT - Select epochs on below total profit. + Select epochs below total profit. --min-objective FLOAT - Select epochs on above objective (- is added by default). + Select epochs above objective. --max-objective FLOAT - Select epochs on below objective (- is added by default). + Select epochs below objective. --no-color Disable colorization of hyperopt results. May be useful if you are redirecting output to a file. - --print-json Print best result detailization in JSON format. + --print-json Print output in JSON format. --no-details Do not print best epoch details. + --hyperopt-filename FILENAME + Hyperopt result filename.Example: `--hyperopt- + filename=hyperopt_results_2020-09-27_16-20-48.pickle` --export-csv FILE Export to CSV-File. This will disable table print. Example: --export-csv hyperopt.csv @@ -480,7 +484,11 @@ Common arguments: --userdir PATH, --user-data-dir PATH Path to userdata directory. ``` - + +!!! Note + `hyperopt-list` will automatically use the latest available hyperopt results file. + You can override this using the `--hyperopt-filename` argument, and specify another, available filename (without path!). + ### Examples List all results, print details of the best result at the end: @@ -501,17 +509,41 @@ You can show the details of any hyperoptimization epoch previously evaluated by usage: freqtrade hyperopt-show [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [--best] [--profitable] [-n INT] [--print-json] - [--no-header] + [--hyperopt-filename PATH] [--no-header] optional arguments: -h, --help show this help message and exit --best Select only best epochs. --profitable Select only profitable epochs. -n INT, --index INT Specify the index of the epoch to print details for. - --print-json Print best result detailization in JSON format. + --print-json Print output in JSON format. + --hyperopt-filename FILENAME + Hyperopt result filename.Example: `--hyperopt- + filename=hyperopt_results_2020-09-27_16-20-48.pickle` --no-header Do not print epoch details header. + +Common arguments: + -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). + --logfile FILE Log to the file specified. Special values are: + 'syslog', 'journald'. See the documentation for more + details. + -V, --version show program's version number and exit + -c PATH, --config PATH + Specify configuration file (default: + `userdir/config.json` or `config.json` whichever + exists). Multiple --config options may be used. Can be + set to `-` to read config from stdin. + -d PATH, --datadir PATH + Path to directory with historical backtesting data. + --userdir PATH, --user-data-dir PATH + Path to userdata directory. + ``` +!!! Note + `hyperopt-show` will automatically use the latest available hyperopt results file. + You can override this using the `--hyperopt-filename` argument, and specify another, available filename (without path!). + ### Examples Print details for the epoch 168 (the number of the epoch is shown by the `hyperopt-list` subcommand or by Hyperopt itself during hyperoptimization run): diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index cd0bc426d..3c5775768 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -267,7 +267,7 @@ AVAILABLE_CLI_OPTIONS = { '--hyperopt-filename', help='Hyperopt result filename.' 'Example: `--hyperopt-filename=hyperopt_results_2020-09-27_16-20-48.pickle`', - metavar='PATH', + metavar='FILENAME', ), # List exchanges "print_one_column": Arg( From 5769b9244f83343c66309e7b4c4eb73cce03f1b7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Sep 2020 19:34:47 +0200 Subject: [PATCH 0699/1197] Mock test correctly --- tests/optimize/test_hyperopt.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index ec911c113..c57ff6852 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -843,6 +843,8 @@ def test_clean_hyperopt(mocker, hyperopt_conf, caplog): def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') + mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( From 6e70ae6e95758e0b5de881d956a81fbb88b0b0be Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Sep 2020 19:40:55 +0200 Subject: [PATCH 0700/1197] Improve code quality --- freqtrade/commands/hyperopt_commands.py | 1 - freqtrade/data/btanalysis.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index 9075d19d2..fb567321d 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -86,7 +86,6 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None: config['user_data_dir'] / 'hyperopt_results', config.get('hyperoptexportfilename')) - n = config.get('hyperopt_show_index', -1) filteroptions = { diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index d4edd41a8..b309a7634 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -91,7 +91,8 @@ def get_latest_hyperopt_file(directory: Union[Path, str], predef_filename: str = * `directory/.last_result.json` does not exist * `directory/.last_result.json` has the wrong content """ - + if isinstance(directory, str): + directory = Path(directory) if predef_filename: return directory / predef_filename return directory / get_latest_hyperopt_filename(directory) From f3de74f817ef9e5c0a6f3cecd327c2ffa9018c75 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Sep 2020 19:48:11 +0200 Subject: [PATCH 0701/1197] Mock all occurances of hyperopt.dump --- tests/optimize/test_hyperopt.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index c57ff6852..b889114e1 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -86,6 +86,7 @@ def create_results(mocker, hyperopt, testdatadir) -> List[Dict]: mocker.patch.object(Path, "unlink", MagicMock(return_value=True)) mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None) + mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') return [{'loss': 1, 'result': 'foo', 'params': {}}] @@ -538,6 +539,8 @@ def test_roi_table_generation(hyperopt) -> None: def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') + mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( @@ -900,6 +903,7 @@ def test_print_json_spaces_all(mocker, hyperopt_conf, capsys) -> None: def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( @@ -947,6 +951,7 @@ def test_print_json_spaces_default(mocker, hyperopt_conf, capsys) -> None: def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( @@ -993,6 +998,7 @@ def test_print_json_spaces_roi_stoploss(mocker, hyperopt_conf, capsys) -> None: def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( @@ -1045,6 +1051,7 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non def test_simplified_interface_all_failed(mocker, hyperopt_conf) -> None: mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( @@ -1071,6 +1078,7 @@ def test_simplified_interface_all_failed(mocker, hyperopt_conf) -> None: def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( @@ -1123,6 +1131,7 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None: def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( @@ -1181,6 +1190,7 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None: ]) def test_simplified_interface_failed(mocker, hyperopt_conf, method, space) -> None: mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) + mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( From 627e221b654e5b0ebf87d6f299f70e26c798e3b7 Mon Sep 17 00:00:00 2001 From: Xu Wang Date: Sun, 27 Sep 2020 20:23:13 +0100 Subject: [PATCH 0702/1197] Use tabulate to create sell reason message. --- freqtrade/rpc/telegram.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 47e9d67dc..ea8597469 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -769,16 +769,21 @@ class Telegram(RPC): win_loss_count = {'Wins': 0, 'Losses': 0, 'Draws': 0} win_loss_count[trade_win_loss(trade)] += 1 sell_reasons[trade['sell_reason']] = win_loss_count - sell_reason_msg = [ - '| Sell Reason | Sells | Wins | Draws | Losses |', - '|-------------|------:|-----:|------:|-------:|' - ] + sell_reasons_tabulate = [] # | Sell Reason | Sells | Wins | Draws | Losses | # |-------------|------:|-----:|------:|-------:| # | test | 1 | 2 | 3 | 4 | for reason, count in sell_reasons.items(): - msg = f'| `{reason}` | `{sum(count.values())}` | `{count['Wins']}` | `{count['Draws']}` | `{count['Losses']}` |' - sell_reason_msg.append(msg) + sell_reasons_tabulate.append([ + reason, sum(count.values()), + count['Wins'], + count['Draws'], + count['Losses'] + ]) + sell_reasons_msg = tabulate( + sell_reasons_tabulate, + headers=['Sell Reason', 'Sells', 'Wins', 'Draws', 'Losses'] + ) # TODO: Duration From 15bb0af1b354ddb197f081bb8b456269dc73841d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Sep 2020 00:35:19 +0200 Subject: [PATCH 0703/1197] Add some test-coverage --- tests/data/test_btanalysis.py | 3 +++ tests/optimize/test_hyperopt.py | 30 ++++++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 43b1a7a4b..62d0415f2 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -51,6 +51,9 @@ def test_get_latest_hyperopt_file(testdatadir, mocker): res = get_latest_hyperopt_file(testdatadir.parent) assert res == testdatadir.parent / "hyperopt_results.pickle" + res = get_latest_hyperopt_file(str(testdatadir.parent)) + assert res == testdatadir.parent / "hyperopt_results.pickle" + def test_load_backtest_data_old_format(testdatadir): diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index b889114e1..ce23af0d8 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -5,7 +5,7 @@ from datetime import datetime from pathlib import Path from copy import deepcopy from typing import Dict, List -from unittest.mock import MagicMock, PropertyMock +from unittest.mock import MagicMock import pandas as pd import pytest @@ -81,14 +81,14 @@ def create_results(mocker, hyperopt, testdatadir) -> List[Dict]: mocker.patch.object(Path, "is_file", MagicMock(return_value=False)) stat_mock = MagicMock() - stat_mock.st_size = PropertyMock(return_value=1) - mocker.patch.object(Path, "stat", MagicMock(return_value=False)) + stat_mock.st_size = 1 + mocker.patch.object(Path, "stat", MagicMock(return_value=stat_mock)) mocker.patch.object(Path, "unlink", MagicMock(return_value=True)) mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None) mocker.patch('freqtrade.optimize.hyperopt.file_dump_json') - return [{'loss': 1, 'result': 'foo', 'params': {}}] + return [{'loss': 1, 'result': 'foo', 'params': {}, 'is_best': True}] def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, caplog) -> None: @@ -524,6 +524,28 @@ def test_read_results_returns_epochs(mocker, hyperopt, testdatadir, caplog) -> N mock_load.assert_called_once() +def test_load_previous_results(mocker, hyperopt, testdatadir, caplog) -> None: + epochs = create_results(mocker, hyperopt, testdatadir) + mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=epochs) + mocker.patch.object(Path, 'is_file', MagicMock(return_value=True)) + statmock = MagicMock() + statmock.st_size = 5 + # mocker.patch.object(Path, 'stat', MagicMock(return_value=statmock)) + + results_file = testdatadir / 'optimize' / 'ut_results.pickle' + + hyperopt_epochs = hyperopt.load_previous_results(results_file) + + assert hyperopt_epochs == epochs + mock_load.assert_called_once() + + del epochs[0]['is_best'] + mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=epochs) + + with pytest.raises(OperationalException): + hyperopt.load_previous_results(results_file) + + def test_roi_table_generation(hyperopt) -> None: params = { 'roi_t1': 5, From 48347b49fd696583fcdf34560243a76a1dc55224 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2020 05:48:08 +0000 Subject: [PATCH 0704/1197] Bump pytest from 6.0.2 to 6.1.0 Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.0.2 to 6.1.0. - [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/6.0.2...6.1.0) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index ffe2763a6..a21604701 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.8.3 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 mypy==0.782 -pytest==6.0.2 +pytest==6.1.0 pytest-asyncio==0.14.0 pytest-cov==2.10.1 pytest-mock==3.3.1 From 1dee0eed75c529ba23b047a5af6ad29a03fe6240 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2020 05:48:12 +0000 Subject: [PATCH 0705/1197] Bump ccxt from 1.34.40 to 1.34.59 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.34.40 to 1.34.59. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.34.40...1.34.59) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 44d2f29a2..7f1a3dabe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.2 pandas==1.1.2 -ccxt==1.34.40 +ccxt==1.34.59 SQLAlchemy==1.3.19 python-telegram-bot==12.8 arrow==0.16.0 From 6d8fadd560e055d898c51ac848024d491728c78c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2020 05:48:14 +0000 Subject: [PATCH 0706/1197] Bump nbconvert from 6.0.4 to 6.0.6 Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 6.0.4 to 6.0.6. - [Release notes](https://github.com/jupyter/nbconvert/releases) - [Commits](https://github.com/jupyter/nbconvert/compare/6.0.4...6.0.6) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index ffe2763a6..b6ab80648 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -15,4 +15,4 @@ pytest-mock==3.3.1 pytest-random-order==1.0.4 # Convert jupyter notebooks to markdown documents -nbconvert==6.0.4 +nbconvert==6.0.6 From dd4d458ca82b76a36b7820b5c6775c0fcb77caf6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2020 05:48:15 +0000 Subject: [PATCH 0707/1197] Bump mkdocs-material from 5.5.13 to 6.0.1 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 5.5.13 to 6.0.1. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/5.5.13...6.0.1) Signed-off-by: dependabot[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 d4c93928e..091eacc9b 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==5.5.13 +mkdocs-material==6.0.1 mdx_truly_sane_lists==1.2 From 700529fe06e22443283271f7c494a70e2084fdeb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Sep 2020 08:36:40 +0200 Subject: [PATCH 0708/1197] Tag image before building next image --- build_helpers/publish_docker.sh | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/build_helpers/publish_docker.sh b/build_helpers/publish_docker.sh index 53e18063c..ac0cd2461 100755 --- a/build_helpers/publish_docker.sh +++ b/build_helpers/publish_docker.sh @@ -17,8 +17,13 @@ else docker pull ${IMAGE_NAME}:${TAG} docker build --cache-from ${IMAGE_NAME}:${TAG} -t freqtrade:${TAG} . fi +# Tag image for upload and next build step +docker tag freqtrade:$TAG ${IMAGE_NAME}:$TAG + docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot . +docker tag freqtrade:$TAG_PLOT ${IMAGE_NAME}:$TAG_PLOT + if [ $? -ne 0 ]; then echo "failed building image" return 1 @@ -32,9 +37,6 @@ if [ $? -ne 0 ]; then return 1 fi -# Tag image for upload -docker tag freqtrade:$TAG ${IMAGE_NAME}:$TAG -docker tag freqtrade:$TAG_PLOT ${IMAGE_NAME}:$TAG_PLOT if [ $? -ne 0 ]; then echo "failed tagging image" return 1 From 17e605e130f9306d06987b685148032efd7b0f0e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Sep 2020 15:22:06 +0200 Subject: [PATCH 0709/1197] Make it clear in samples that strategy is mandatory --- docs/advanced-hyperopt.md | 4 ++-- docs/faq.md | 8 +------- docs/hyperopt.md | 4 ++-- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index 5fc674b03..dfabf2b91 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -27,9 +27,9 @@ class MyAwesomeHyperOpt2(MyAwesomeHyperOpt): and then quickly switch between hyperopt classes, running optimization process with hyperopt class you need in each particular case: ``` -$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt ... +$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt --strategy MyAwesomeStrategy ... or -$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt2 ... +$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt2 --strategy MyAwesomeStrategy ... ``` ## Creating and using a custom loss function diff --git a/docs/faq.md b/docs/faq.md index beed89801..d8af7798c 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -140,13 +140,7 @@ Since hyperopt uses Bayesian search, running for too many epochs may not produce It's therefore recommended to run between 500-1000 epochs over and over until you hit at least 10.000 epochs in total (or are satisfied with the result). You can best judge by looking at the results - if the bot keeps discovering better strategies, it's best to keep on going. ```bash -freqtrade hyperopt -e 1000 -``` - -or if you want intermediate result to see - -```bash -for i in {1..100}; do freqtrade hyperopt -e 1000; done +freqtrade hyperopt --hyperop SampleHyperopt --strategy SampleStrategy -e 1000 ``` ### Why does it take a long time to run hyperopt? diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 8bfd2cf7d..d26cbeeb2 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -229,7 +229,7 @@ Because hyperopt tries a lot of combinations to find the best parameters it will We strongly recommend to use `screen` or `tmux` to prevent any connection loss. ```bash -freqtrade hyperopt --config config.json --hyperopt -e 500 --spaces all +freqtrade hyperopt --config config.json --hyperopt --strategy -e 500 --spaces all ``` Use `` as the name of the custom hyperopt used. @@ -255,7 +255,7 @@ Use the `--timerange` argument to change how much of the test-set you want to us For example, to use one month of data, pass the following parameter to the hyperopt call: ```bash -freqtrade hyperopt --timerange 20180401-20180501 +freqtrade hyperopt --hyperopt --strategy --timerange 20180401-20180501 ``` ### Running Hyperopt using methods from a strategy From 7623691a5feb2366b3261410df011cb287f2af7d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Sep 2020 08:53:53 +0200 Subject: [PATCH 0710/1197] PyPi Publis should only run for releases --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 392641677..f4ef3ba7e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -236,7 +236,7 @@ jobs: - name: Publish to PyPI (Test) uses: pypa/gh-action-pypi-publish@master - if: (steps.extract_branch.outputs.branch == 'stable' || github.event_name == 'release') + if: (github.event_name == 'release') with: user: __token__ password: ${{ secrets.pypi_test_password }} @@ -244,7 +244,7 @@ jobs: - name: Publish to PyPI uses: pypa/gh-action-pypi-publish@master - if: (steps.extract_branch.outputs.branch == 'stable' || github.event_name == 'release') + if: (github.event_name == 'release') with: user: __token__ password: ${{ secrets.pypi_password }} From 287604efd21891c1233ecb84b4ea0ce7a923ac9f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Sep 2020 17:35:04 +0200 Subject: [PATCH 0711/1197] Add isort to project dev dependencies --- .github/workflows/ci.yml | 8 ++++++++ requirements-dev.txt | 1 + setup.cfg | 4 ++++ 3 files changed, 13 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4ef3ba7e..19c5479a4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -94,6 +94,10 @@ jobs: run: | flake8 + - name: Sort imports (isort) + run: | + isort --check . + - name: Mypy run: | mypy freqtrade scripts @@ -156,6 +160,10 @@ jobs: run: | flake8 + - name: Sort imports (isort) + run: | + isort --check . + - name: Mypy run: | mypy freqtrade scripts diff --git a/requirements-dev.txt b/requirements-dev.txt index b71f1abfd..c16e387ef 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,6 +13,7 @@ pytest-asyncio==0.14.0 pytest-cov==2.10.1 pytest-mock==3.3.1 pytest-random-order==1.0.4 +isort==5.5.3 # Convert jupyter notebooks to markdown documents nbconvert==6.0.6 diff --git a/setup.cfg b/setup.cfg index 34f25482b..57a0c42e7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,6 +8,10 @@ exclude = .eggs, user_data, +[isort] +line_length=100 +multi_line_output=0 + [mypy] ignore_missing_imports = True From 201e71434343f8110944f96a0021d84ef92faf63 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Sep 2020 17:37:14 +0200 Subject: [PATCH 0712/1197] include isort to contributing --- CONTRIBUTING.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 97f62154d..399588f88 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -65,6 +65,14 @@ Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using mypy freqtrade ``` +### 4. Ensure all imports are correct + +#### Run isort + +``` bash +isort . +``` + ## (Core)-Committer Guide ### Process: Pull Requests From 253b7b763eaae842f15b1a6e989d355481432106 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Sep 2020 19:39:41 +0200 Subject: [PATCH 0713/1197] Apply isort to freqtrade codebase --- freqtrade/__main__.py | 1 + freqtrade/commands/__init__.py | 24 +++++++------------ freqtrade/commands/arguments.py | 15 ++++++------ freqtrade/commands/build_config_commands.py | 6 +++-- freqtrade/commands/data_commands.py | 10 ++++---- freqtrade/commands/deploy_commands.py | 4 ++-- freqtrade/commands/hyperopt_commands.py | 1 + freqtrade/commands/list_commands.py | 11 +++++---- freqtrade/commands/optimize_commands.py | 3 +++ freqtrade/commands/pairlist_commands.py | 1 + freqtrade/commands/trade_commands.py | 1 - freqtrade/configuration/__init__.py | 6 ++--- freqtrade/configuration/check_exchange.py | 6 ++--- freqtrade/configuration/config_setup.py | 6 +++-- freqtrade/configuration/config_validation.py | 1 + freqtrade/configuration/configuration.py | 4 ++-- .../configuration/directory_operations.py | 3 ++- freqtrade/configuration/load_config.py | 1 + freqtrade/data/btanalysis.py | 5 ++-- freqtrade/data/converter.py | 4 ++-- freqtrade/data/dataprovider.py | 1 + freqtrade/data/history/__init__.py | 10 ++++---- freqtrade/data/history/hdf5datahandler.py | 4 ++-- freqtrade/data/history/history_utils.py | 7 +++--- freqtrade/data/history/idatahandler.py | 4 ++-- freqtrade/data/history/jsondatahandler.py | 4 ++-- freqtrade/edge/edge_positioning.py | 5 ++-- freqtrade/exchange/__init__.py | 24 ++++++++----------- freqtrade/exchange/bibox.py | 1 + freqtrade/exchange/binance.py | 6 ++--- freqtrade/exchange/common.py | 4 ++-- freqtrade/exchange/exchange.py | 16 ++++++------- freqtrade/exchange/ftx.py | 6 ++--- freqtrade/exchange/kraken.py | 6 ++--- freqtrade/freqtradebot.py | 1 + freqtrade/loggers.py | 4 ++-- freqtrade/main.py | 1 + freqtrade/misc.py | 1 + freqtrade/optimize/backtesting.py | 7 +++--- freqtrade/optimize/edge_cli.py | 4 ++-- freqtrade/optimize/hyperopt.py | 10 ++++---- freqtrade/optimize/hyperopt_interface.py | 1 + freqtrade/optimize/hyperopt_loss_sharpe.py | 2 +- freqtrade/optimize/hyperopt_loss_sortino.py | 2 +- freqtrade/optimize/optimize_reports.py | 5 ++-- freqtrade/pairlist/AgeFilter.py | 3 ++- freqtrade/pairlist/PrecisionFilter.py | 3 ++- freqtrade/pairlist/pairlistmanager.py | 2 +- freqtrade/persistence/__init__.py | 3 +-- freqtrade/persistence/migrations.py | 1 + freqtrade/persistence/models.py | 5 ++-- freqtrade/plot/plotting.py | 12 ++++------ freqtrade/resolvers/__init__.py | 16 +++++++++---- freqtrade/resolvers/exchange_resolver.py | 3 ++- freqtrade/resolvers/hyperopt_resolver.py | 1 + freqtrade/resolvers/iresolver.py | 1 + freqtrade/resolvers/pairlist_resolver.py | 1 + freqtrade/resolvers/strategy_resolver.py | 4 ++-- freqtrade/rpc/__init__.py | 5 ++-- freqtrade/rpc/api_server.py | 6 ++--- freqtrade/rpc/rpc.py | 1 + freqtrade/rpc/rpc_manager.py | 1 + freqtrade/rpc/telegram.py | 3 ++- freqtrade/rpc/webhook.py | 4 ++-- freqtrade/strategy/__init__.py | 4 ++-- freqtrade/strategy/interface.py | 1 + freqtrade/strategy/strategy_helper.py | 1 + freqtrade/strategy/strategy_wrapper.py | 1 + freqtrade/templates/sample_hyperopt.py | 1 + .../templates/sample_hyperopt_advanced.py | 2 +- freqtrade/templates/sample_hyperopt_loss.py | 3 ++- freqtrade/templates/sample_strategy.py | 2 +- freqtrade/vendor/qtpylib/indicators.py | 3 ++- freqtrade/wallets.py | 1 + freqtrade/worker.py | 1 + scripts/rest_client.py | 3 ++- setup.cfg | 1 + 77 files changed, 188 insertions(+), 160 deletions(-) diff --git a/freqtrade/__main__.py b/freqtrade/__main__.py index 97ed9ae67..881a2f562 100644 --- a/freqtrade/__main__.py +++ b/freqtrade/__main__.py @@ -8,5 +8,6 @@ To launch Freqtrade as a module from freqtrade import main + if __name__ == '__main__': main.main() diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index 4ce3eb421..21c5d6812 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -8,23 +8,15 @@ Note: Be careful with file-scoped imports in these subfiles. """ from freqtrade.commands.arguments import Arguments from freqtrade.commands.build_config_commands import start_new_config -from freqtrade.commands.data_commands import (start_convert_data, - start_download_data, +from freqtrade.commands.data_commands import (start_convert_data, start_download_data, start_list_data) -from freqtrade.commands.deploy_commands import (start_create_userdir, - start_new_hyperopt, +from freqtrade.commands.deploy_commands import (start_create_userdir, start_new_hyperopt, start_new_strategy) -from freqtrade.commands.hyperopt_commands import (start_hyperopt_list, - start_hyperopt_show) -from freqtrade.commands.list_commands import (start_list_exchanges, - start_list_hyperopts, - start_list_markets, - start_list_strategies, - start_list_timeframes, - start_show_trades) -from freqtrade.commands.optimize_commands import (start_backtesting, - start_edge, start_hyperopt) +from freqtrade.commands.hyperopt_commands import start_hyperopt_list, start_hyperopt_show +from freqtrade.commands.list_commands import (start_list_exchanges, start_list_hyperopts, + start_list_markets, start_list_strategies, + start_list_timeframes, start_show_trades) +from freqtrade.commands.optimize_commands import start_backtesting, start_edge, start_hyperopt from freqtrade.commands.pairlist_commands import start_test_pairlist -from freqtrade.commands.plot_commands import (start_plot_dataframe, - start_plot_profit) +from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit from freqtrade.commands.trade_commands import start_trading diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index b61a4933e..3f970c6eb 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -9,6 +9,7 @@ from typing import Any, Dict, List, Optional from freqtrade.commands.cli_options import AVAILABLE_CLI_OPTIONS from freqtrade.constants import DEFAULT_CONFIG + ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"] ARGS_STRATEGY = ["strategy", "strategy_path"] @@ -161,16 +162,14 @@ class Arguments: self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot') self._build_args(optionlist=['version'], parser=self.parser) - from freqtrade.commands import (start_create_userdir, start_convert_data, - start_download_data, start_list_data, - start_hyperopt_list, start_hyperopt_show, + from freqtrade.commands import (start_backtesting, start_convert_data, start_create_userdir, + start_download_data, start_edge, start_hyperopt, + start_hyperopt_list, start_hyperopt_show, start_list_data, start_list_exchanges, start_list_hyperopts, start_list_markets, start_list_strategies, - start_list_timeframes, start_new_config, - start_new_hyperopt, start_new_strategy, - start_plot_dataframe, start_plot_profit, start_show_trades, - start_backtesting, start_hyperopt, start_edge, - start_test_pairlist, start_trading) + start_list_timeframes, start_new_config, start_new_hyperopt, + start_new_strategy, start_plot_dataframe, start_plot_profit, + start_show_trades, start_test_pairlist, start_trading) subparsers = self.parser.add_subparsers(dest='command', # Use custom message when no subhandler is added diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index 0c98b2e55..79256db1d 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -5,9 +5,11 @@ from typing import Any, Dict from questionary import Separator, prompt from freqtrade.constants import UNLIMITED_STAKE_AMOUNT -from freqtrade.exchange import available_exchanges, MAP_EXCHANGE_CHILDCLASS -from freqtrade.misc import render_template from freqtrade.exceptions import OperationalException +from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, available_exchanges +from freqtrade.misc import render_template + + logger = logging.getLogger(__name__) diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 956a8693e..7102eee38 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -6,16 +6,15 @@ from typing import Any, Dict, List import arrow from freqtrade.configuration import TimeRange, setup_utils_configuration -from freqtrade.data.converter import (convert_ohlcv_format, - convert_trades_format) -from freqtrade.data.history import (convert_trades_to_ohlcv, - refresh_backtest_ohlcv_data, +from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format +from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data, refresh_backtest_trades_data) from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes from freqtrade.resolvers import ExchangeResolver from freqtrade.state import RunMode + logger = logging.getLogger(__name__) @@ -105,8 +104,9 @@ def start_list_data(args: Dict[str, Any]) -> None: config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) - from freqtrade.data.history.idatahandler import get_datahandler from tabulate import tabulate + + from freqtrade.data.history.idatahandler import get_datahandler dhc = get_datahandler(config['datadir'], config['dataformat_ohlcv']) paircombs = dhc.ohlcv_get_available_data(config['datadir']) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index bfd68cb9b..0a49c55de 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -4,13 +4,13 @@ from pathlib import Path from typing import Any, Dict from freqtrade.configuration import setup_utils_configuration -from freqtrade.configuration.directory_operations import (copy_sample_files, - create_userdata_dir) +from freqtrade.configuration.directory_operations import copy_sample_files, create_userdata_dir from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES from freqtrade.exceptions import OperationalException from freqtrade.misc import render_template, render_template_with_fallback from freqtrade.state import RunMode + logger = logging.getLogger(__name__) diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index 4fae51e28..94447e6f9 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -8,6 +8,7 @@ from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode + logger = logging.getLogger(__name__) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index c8c820c61..e81ecf871 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -5,20 +5,20 @@ from collections import OrderedDict from pathlib import Path from typing import Any, Dict, List -from colorama import init as colorama_init -from colorama import Fore, Style import rapidjson +from colorama import Fore, Style +from colorama import init as colorama_init from tabulate import tabulate from freqtrade.configuration import setup_utils_configuration from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES from freqtrade.exceptions import OperationalException -from freqtrade.exchange import (available_exchanges, ccxt_exchanges, - market_is_active) +from freqtrade.exchange import available_exchanges, ccxt_exchanges, market_is_active from freqtrade.misc import plural from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode + logger = logging.getLogger(__name__) @@ -203,8 +203,9 @@ def start_show_trades(args: Dict[str, Any]) -> None: """ Show trades """ - from freqtrade.persistence import init, Trade import json + + from freqtrade.persistence import Trade, init config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) if 'db_url' not in config: diff --git a/freqtrade/commands/optimize_commands.py b/freqtrade/commands/optimize_commands.py index 2fc605926..7411ca9c6 100644 --- a/freqtrade/commands/optimize_commands.py +++ b/freqtrade/commands/optimize_commands.py @@ -6,6 +6,7 @@ from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.state import RunMode + logger = logging.getLogger(__name__) @@ -58,6 +59,7 @@ def start_hyperopt(args: Dict[str, Any]) -> None: # Import here to avoid loading hyperopt module when it's not used try: from filelock import FileLock, Timeout + from freqtrade.optimize.hyperopt import Hyperopt except ImportError as e: raise OperationalException( @@ -98,6 +100,7 @@ def start_edge(args: Dict[str, Any]) -> None: :return: None """ from freqtrade.optimize.edge_cli import EdgeCli + # Initialize configuration config = setup_optimize_configuration(args, RunMode.EDGE) logger.info('Starting freqtrade in Edge mode') diff --git a/freqtrade/commands/pairlist_commands.py b/freqtrade/commands/pairlist_commands.py index 77bcb04b4..e4ee80ca5 100644 --- a/freqtrade/commands/pairlist_commands.py +++ b/freqtrade/commands/pairlist_commands.py @@ -7,6 +7,7 @@ from freqtrade.configuration import setup_utils_configuration from freqtrade.resolvers import ExchangeResolver from freqtrade.state import RunMode + logger = logging.getLogger(__name__) diff --git a/freqtrade/commands/trade_commands.py b/freqtrade/commands/trade_commands.py index c058e4f9d..535844844 100644 --- a/freqtrade/commands/trade_commands.py +++ b/freqtrade/commands/trade_commands.py @@ -1,5 +1,4 @@ import logging - from typing import Any, Dict diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py index d41ac97ec..607f9cdef 100644 --- a/freqtrade/configuration/__init__.py +++ b/freqtrade/configuration/__init__.py @@ -1,7 +1,7 @@ # flake8: noqa: F401 -from freqtrade.configuration.config_setup import setup_utils_configuration from freqtrade.configuration.check_exchange import check_exchange, remove_credentials -from freqtrade.configuration.timerange import TimeRange -from freqtrade.configuration.configuration import Configuration +from freqtrade.configuration.config_setup import setup_utils_configuration from freqtrade.configuration.config_validation import validate_config_consistency +from freqtrade.configuration.configuration import Configuration +from freqtrade.configuration.timerange import TimeRange diff --git a/freqtrade/configuration/check_exchange.py b/freqtrade/configuration/check_exchange.py index 92daaf251..aa36de3ff 100644 --- a/freqtrade/configuration/check_exchange.py +++ b/freqtrade/configuration/check_exchange.py @@ -2,11 +2,11 @@ import logging from typing import Any, Dict from freqtrade.exceptions import OperationalException -from freqtrade.exchange import (available_exchanges, get_exchange_bad_reason, - is_exchange_bad, is_exchange_known_ccxt, - is_exchange_officially_supported) +from freqtrade.exchange import (available_exchanges, get_exchange_bad_reason, is_exchange_bad, + is_exchange_known_ccxt, is_exchange_officially_supported) from freqtrade.state import RunMode + logger = logging.getLogger(__name__) diff --git a/freqtrade/configuration/config_setup.py b/freqtrade/configuration/config_setup.py index 64f283e42..3b0f778f4 100644 --- a/freqtrade/configuration/config_setup.py +++ b/freqtrade/configuration/config_setup.py @@ -1,10 +1,12 @@ import logging from typing import Any, Dict +from freqtrade.state import RunMode + +from .check_exchange import remove_credentials from .config_validation import validate_config_consistency from .configuration import Configuration -from .check_exchange import remove_credentials -from freqtrade.state import RunMode + logger = logging.getLogger(__name__) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 5ba7ff294..d4612d8e0 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -9,6 +9,7 @@ from freqtrade import constants from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode + logger = logging.getLogger(__name__) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 930917fae..83be28956 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -10,14 +10,14 @@ from typing import Any, Callable, Dict, List, Optional from freqtrade import constants from freqtrade.configuration.check_exchange import check_exchange from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings -from freqtrade.configuration.directory_operations import (create_datadir, - create_userdata_dir) +from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir from freqtrade.configuration.load_config import load_config_file from freqtrade.exceptions import OperationalException from freqtrade.loggers import setup_logging from freqtrade.misc import deep_merge_dicts, json_load from freqtrade.state import NON_UTIL_MODES, TRADING_MODES, RunMode + logger = logging.getLogger(__name__) diff --git a/freqtrade/configuration/directory_operations.py b/freqtrade/configuration/directory_operations.py index 6b8c8cb5a..51310f013 100644 --- a/freqtrade/configuration/directory_operations.py +++ b/freqtrade/configuration/directory_operations.py @@ -3,8 +3,9 @@ import shutil from pathlib import Path from typing import Any, Dict, Optional -from freqtrade.exceptions import OperationalException from freqtrade.constants import USER_DATA_FILES +from freqtrade.exceptions import OperationalException + logger = logging.getLogger(__name__) diff --git a/freqtrade/configuration/load_config.py b/freqtrade/configuration/load_config.py index a24ee3d0a..726126034 100644 --- a/freqtrade/configuration/load_config.py +++ b/freqtrade/configuration/load_config.py @@ -11,6 +11,7 @@ import rapidjson from freqtrade.exceptions import OperationalException + logger = logging.getLogger(__name__) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 2d45a7222..887c30621 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -2,18 +2,19 @@ Helpers when analyzing backtest data """ import logging +from datetime import timezone from pathlib import Path -from typing import Dict, Union, Tuple, Any, Optional +from typing import Any, Dict, Optional, Tuple, Union import numpy as np import pandas as pd -from datetime import timezone from freqtrade import persistence from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.misc import json_load from freqtrade.persistence import Trade + logger = logging.getLogger(__name__) # must align with columns in backtest.py diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 100a578a2..38fa670e9 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -10,8 +10,8 @@ from typing import Any, Dict, List import pandas as pd from pandas import DataFrame, to_datetime -from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, - DEFAULT_TRADES_COLUMNS) +from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS + logger = logging.getLogger(__name__) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index ccb6cbf56..07dd94fc1 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -17,6 +17,7 @@ from freqtrade.exceptions import ExchangeError, OperationalException from freqtrade.exchange import Exchange from freqtrade.state import RunMode + logger = logging.getLogger(__name__) diff --git a/freqtrade/data/history/__init__.py b/freqtrade/data/history/__init__.py index 23f635a98..107f9c401 100644 --- a/freqtrade/data/history/__init__.py +++ b/freqtrade/data/history/__init__.py @@ -5,10 +5,8 @@ Includes: * load data for a pair (or a list of pairs) from disk * download data from exchange and store to disk """ - -from .history_utils import (convert_trades_to_ohlcv, # noqa: F401 - get_timerange, load_data, load_pair_history, - refresh_backtest_ohlcv_data, - refresh_backtest_trades_data, refresh_data, +# flake8: noqa: F401 +from .history_utils import (convert_trades_to_ohlcv, get_timerange, load_data, load_pair_history, + refresh_backtest_ohlcv_data, refresh_backtest_trades_data, refresh_data, validate_backtest_data) -from .idatahandler import get_datahandler # noqa: F401 +from .idatahandler import get_datahandler diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index 594a1598a..f6cf9e0d9 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -7,12 +7,12 @@ import pandas as pd from freqtrade import misc from freqtrade.configuration import TimeRange -from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, - DEFAULT_TRADES_COLUMNS, +from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, ListPairsWithTimeframes) from .idatahandler import IDataHandler, TradeList + logger = logging.getLogger(__name__) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index ac234a72e..a420b9dcc 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -9,15 +9,14 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS -from freqtrade.data.converter import (clean_ohlcv_dataframe, - ohlcv_to_dataframe, - trades_remove_duplicates, - trades_to_ohlcv) +from freqtrade.data.converter import (clean_ohlcv_dataframe, ohlcv_to_dataframe, + trades_remove_duplicates, trades_to_ohlcv) from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler from freqtrade.exceptions import OperationalException from freqtrade.exchange import Exchange from freqtrade.misc import format_ms_time + logger = logging.getLogger(__name__) diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index 01b14f501..a170a9dc5 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -14,10 +14,10 @@ from pandas import DataFrame from freqtrade.configuration import TimeRange from freqtrade.constants import ListPairsWithTimeframes -from freqtrade.data.converter import (clean_ohlcv_dataframe, - trades_remove_duplicates, trim_dataframe) +from freqtrade.data.converter import clean_ohlcv_dataframe, trades_remove_duplicates, trim_dataframe from freqtrade.exchange import timeframe_to_seconds + logger = logging.getLogger(__name__) # Type for trades list diff --git a/freqtrade/data/history/jsondatahandler.py b/freqtrade/data/history/jsondatahandler.py index 2e7c0f773..6436aa13d 100644 --- a/freqtrade/data/history/jsondatahandler.py +++ b/freqtrade/data/history/jsondatahandler.py @@ -8,12 +8,12 @@ from pandas import DataFrame, read_json, to_datetime from freqtrade import misc from freqtrade.configuration import TimeRange -from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, - ListPairsWithTimeframes) +from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, ListPairsWithTimeframes from freqtrade.data.converter import trades_dict_to_list from .idatahandler import IDataHandler, TradeList + logger = logging.getLogger(__name__) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 3213c1ef8..6a95ad91f 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -9,11 +9,12 @@ import utils_find_1st as utf1st from pandas import DataFrame from freqtrade.configuration import TimeRange -from freqtrade.constants import UNLIMITED_STAKE_AMOUNT, DATETIME_PRINT_FORMAT -from freqtrade.exceptions import OperationalException +from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT from freqtrade.data.history import get_timerange, load_data, refresh_data +from freqtrade.exceptions import OperationalException from freqtrade.strategy.interface import SellType + logger = logging.getLogger(__name__) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index bdf1f91ec..cbcf961bc 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -1,19 +1,15 @@ # flake8: noqa: F401 +# isort: off from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS from freqtrade.exchange.exchange import Exchange -from freqtrade.exchange.exchange import (get_exchange_bad_reason, - is_exchange_bad, - is_exchange_known_ccxt, - is_exchange_officially_supported, - ccxt_exchanges, - available_exchanges) -from freqtrade.exchange.exchange import (timeframe_to_seconds, - timeframe_to_minutes, - timeframe_to_msecs, - timeframe_to_next_date, - timeframe_to_prev_date) -from freqtrade.exchange.exchange import (market_is_active) -from freqtrade.exchange.kraken import Kraken -from freqtrade.exchange.binance import Binance +# isort: on from freqtrade.exchange.bibox import Bibox +from freqtrade.exchange.binance import Binance +from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, + get_exchange_bad_reason, is_exchange_bad, + is_exchange_known_ccxt, is_exchange_officially_supported, + market_is_active, timeframe_to_minutes, timeframe_to_msecs, + timeframe_to_next_date, timeframe_to_prev_date, + timeframe_to_seconds) from freqtrade.exchange.ftx import Ftx +from freqtrade.exchange.kraken import Kraken diff --git a/freqtrade/exchange/bibox.py b/freqtrade/exchange/bibox.py index 229abe766..f0c2dd00b 100644 --- a/freqtrade/exchange/bibox.py +++ b/freqtrade/exchange/bibox.py @@ -4,6 +4,7 @@ from typing import Dict from freqtrade.exchange import Exchange + logger = logging.getLogger(__name__) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index d7da34482..b85802aad 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -4,12 +4,12 @@ from typing import Dict import ccxt -from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, - InvalidOrderException, OperationalException, - TemporaryError) +from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, + OperationalException, TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier + logger = logging.getLogger(__name__) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index 9abd42aa7..ce0fde9e4 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -3,8 +3,8 @@ import logging import time from functools import wraps -from freqtrade.exceptions import (DDosProtection, RetryableOrderError, - TemporaryError) +from freqtrade.exceptions import DDosProtection, RetryableOrderError, TemporaryError + logger = logging.getLogger(__name__) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index aac45967d..bbb94e61f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -13,20 +13,20 @@ from typing import Any, Dict, List, Optional, Tuple import arrow import ccxt import ccxt.async_support as ccxt_async -from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, - TRUNCATE, decimal_to_precision) +from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, + decimal_to_precision) from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list -from freqtrade.exceptions import (DDosProtection, ExchangeError, - InsufficientFundsError, - InvalidOrderException, OperationalException, - RetryableOrderError, TemporaryError) -from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, - BAD_EXCHANGES, retrier, retrier_async) +from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, + InvalidOrderException, OperationalException, RetryableOrderError, + TemporaryError) +from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, BAD_EXCHANGES, retrier, + retrier_async) from freqtrade.misc import deep_merge_dicts, safe_value_fallback2 + CcxtModuleType = Any diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index a5ee0c408..f05490cbb 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -4,12 +4,12 @@ from typing import Any, Dict import ccxt -from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, - InvalidOrderException, OperationalException, - TemporaryError) +from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, + OperationalException, TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT, retrier + logger = logging.getLogger(__name__) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index e6b5da88e..5b7aa5c5b 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -4,12 +4,12 @@ from typing import Any, Dict import ccxt -from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, - InvalidOrderException, OperationalException, - TemporaryError) +from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, + OperationalException, TemporaryError) from freqtrade.exchange import Exchange from freqtrade.exchange.common import retrier + logger = logging.getLogger(__name__) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index eec09a17c..2bdd8da4b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -30,6 +30,7 @@ from freqtrade.strategy.interface import IStrategy, SellType from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets + logger = logging.getLogger(__name__) diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py index 8f5da9bee..169cd2610 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers.py @@ -1,12 +1,12 @@ import logging import sys from logging import Formatter -from logging.handlers import (BufferingHandler, RotatingFileHandler, - SysLogHandler) +from logging.handlers import BufferingHandler, RotatingFileHandler, SysLogHandler from typing import Any, Dict from freqtrade.exceptions import OperationalException + logger = logging.getLogger(__name__) LOGFORMAT = '%(asctime)s - %(name)s - %(levelname)s - %(message)s' diff --git a/freqtrade/main.py b/freqtrade/main.py index dc26c2a46..5f8d5d19d 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -7,6 +7,7 @@ import logging import sys from typing import Any, List + # check min. python version if sys.version_info < (3, 6): sys.exit("Freqtrade requires Python version >= 3.6") diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 623f6cb8f..35c69db98 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -12,6 +12,7 @@ from typing.io import IO import numpy as np import rapidjson + logger = logging.getLogger(__name__) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 8d4a3a205..afdb4fc37 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -11,22 +11,21 @@ from typing import Any, Dict, List, NamedTuple, Optional, Tuple import arrow from pandas import DataFrame -from freqtrade.configuration import (TimeRange, remove_credentials, - validate_config_consistency) +from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.data import history from freqtrade.data.converter import trim_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds -from freqtrade.optimize.optimize_reports import (generate_backtest_stats, - show_backtest_results, +from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, store_backtest_stats) from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType + logger = logging.getLogger(__name__) diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index be19688d8..a5f505bee 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -7,12 +7,12 @@ import logging from typing import Any, Dict from freqtrade import constants -from freqtrade.configuration import (TimeRange, remove_credentials, - validate_config_consistency) +from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency from freqtrade.edge import Edge from freqtrade.optimize.optimize_reports import generate_edge_table from freqtrade.resolvers import ExchangeResolver, StrategyResolver + logger = logging.getLogger(__name__) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 37de3bc4b..49c49c3b8 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -21,8 +21,7 @@ import rapidjson import tabulate from colorama import Fore, Style from colorama import init as colorama_init -from joblib import (Parallel, cpu_count, delayed, dump, load, - wrap_non_picklable_objects) +from joblib import Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects from pandas import DataFrame, isna, json_normalize from freqtrade.constants import DATETIME_PRINT_FORMAT @@ -33,12 +32,11 @@ from freqtrade.misc import plural, round_dict from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F401 -from freqtrade.optimize.hyperopt_loss_interface import \ - IHyperOptLoss # noqa: F401 -from freqtrade.resolvers.hyperopt_resolver import (HyperOptLossResolver, - HyperOptResolver) +from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F401 +from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver from freqtrade.strategy import IStrategy + # Suppress scikit-learn FutureWarnings from skopt with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=FutureWarning) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 65069b984..b8c44ed59 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -13,6 +13,7 @@ from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import round_dict + logger = logging.getLogger(__name__) diff --git a/freqtrade/optimize/hyperopt_loss_sharpe.py b/freqtrade/optimize/hyperopt_loss_sharpe.py index 29377bdd5..232fb33b6 100644 --- a/freqtrade/optimize/hyperopt_loss_sharpe.py +++ b/freqtrade/optimize/hyperopt_loss_sharpe.py @@ -6,8 +6,8 @@ Hyperoptimization. """ from datetime import datetime -from pandas import DataFrame import numpy as np +from pandas import DataFrame from freqtrade.optimize.hyperopt import IHyperOptLoss diff --git a/freqtrade/optimize/hyperopt_loss_sortino.py b/freqtrade/optimize/hyperopt_loss_sortino.py index d470a9977..c0ff0773a 100644 --- a/freqtrade/optimize/hyperopt_loss_sortino.py +++ b/freqtrade/optimize/hyperopt_loss_sortino.py @@ -6,8 +6,8 @@ Hyperoptimization. """ from datetime import datetime -from pandas import DataFrame import numpy as np +from pandas import DataFrame from freqtrade.optimize.hyperopt import IHyperOptLoss diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 696e63b25..3db9a312a 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -4,14 +4,15 @@ from pathlib import Path from typing import Any, Dict, List, Union from arrow import Arrow -from pandas import DataFrame from numpy import int64 +from pandas import DataFrame from tabulate import tabulate from freqtrade.constants import DATETIME_PRINT_FORMAT, LAST_BT_RESULT_FN -from freqtrade.data.btanalysis import calculate_max_drawdown, calculate_market_change +from freqtrade.data.btanalysis import calculate_market_change, calculate_max_drawdown from freqtrade.misc import file_dump_json + logger = logging.getLogger(__name__) diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index 64f01cb61..19cf1c090 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -2,9 +2,10 @@ Minimum age (days listed) pair list filter """ import logging -import arrow from typing import Any, Dict +import arrow + from freqtrade.exceptions import OperationalException from freqtrade.misc import plural from freqtrade.pairlist.IPairList import IPairList diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py index 3061d3d01..cf853397b 100644 --- a/freqtrade/pairlist/PrecisionFilter.py +++ b/freqtrade/pairlist/PrecisionFilter.py @@ -4,8 +4,9 @@ Precision pair list filter import logging from typing import Any, Dict -from freqtrade.pairlist.IPairList import IPairList from freqtrade.exceptions import OperationalException +from freqtrade.pairlist.IPairList import IPairList + logger = logging.getLogger(__name__) diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/pairlist/pairlistmanager.py index 81e52768e..89bab99be 100644 --- a/freqtrade/pairlist/pairlistmanager.py +++ b/freqtrade/pairlist/pairlistmanager.py @@ -7,10 +7,10 @@ from typing import Dict, List from cachetools import TTLCache, cached +from freqtrade.constants import ListPairsWithTimeframes from freqtrade.exceptions import OperationalException from freqtrade.pairlist.IPairList import IPairList from freqtrade.resolvers import PairListResolver -from freqtrade.constants import ListPairsWithTimeframes logger = logging.getLogger(__name__) diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index 764856f2b..ee2e40267 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,4 +1,3 @@ # flake8: noqa: F401 -from freqtrade.persistence.models import (Order, Trade, clean_dry_run_db, - cleanup, init) +from freqtrade.persistence.models import Order, Trade, clean_dry_run_db, cleanup, init diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 5089953b2..84f3ed7e6 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -3,6 +3,7 @@ from typing import List from sqlalchemy import inspect + logger = logging.getLogger(__name__) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 816e23fd3..8455a3b77 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -7,8 +7,8 @@ from decimal import Decimal from typing import Any, Dict, List, Optional import arrow -from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, - String, create_engine, desc, func, inspect) +from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, String, + create_engine, desc, func, inspect) from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import Query, relationship @@ -21,6 +21,7 @@ from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.misc import safe_value_fallback from freqtrade.persistence.migrations import check_migrate + logger = logging.getLogger(__name__) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 270fe615b..a89732df5 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -5,11 +5,8 @@ from typing import Any, Dict, List import pandas as pd from freqtrade.configuration import TimeRange -from freqtrade.data.btanalysis import (calculate_max_drawdown, - combine_dataframes_with_mean, - create_cum_profit, - extract_trades_of_period, - load_trades) +from freqtrade.data.btanalysis import (calculate_max_drawdown, combine_dataframes_with_mean, + create_cum_profit, extract_trades_of_period, load_trades) from freqtrade.data.converter import trim_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data @@ -19,13 +16,14 @@ from freqtrade.misc import pair_to_filename from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.strategy import IStrategy + logger = logging.getLogger(__name__) try: - from plotly.subplots import make_subplots - from plotly.offline import plot import plotly.graph_objects as go + from plotly.offline import plot + from plotly.subplots import make_subplots except ImportError: logger.exception("Module plotly not found \n Please install using `pip3 install plotly`") exit(1) diff --git a/freqtrade/resolvers/__init__.py b/freqtrade/resolvers/__init__.py index 8f79349fe..b42ec4931 100644 --- a/freqtrade/resolvers/__init__.py +++ b/freqtrade/resolvers/__init__.py @@ -1,6 +1,12 @@ -from freqtrade.resolvers.iresolver import IResolver # noqa: F401 -from freqtrade.resolvers.exchange_resolver import ExchangeResolver # noqa: F401 +# flake8: noqa: F401 +# isort: off +from freqtrade.resolvers.iresolver import IResolver +from freqtrade.resolvers.exchange_resolver import ExchangeResolver +# isort: on # Don't import HyperoptResolver to avoid loading the whole Optimize tree -# from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver # noqa: F401 -from freqtrade.resolvers.pairlist_resolver import PairListResolver # noqa: F401 -from freqtrade.resolvers.strategy_resolver import StrategyResolver # noqa: F401 +# from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver +from freqtrade.resolvers.pairlist_resolver import PairListResolver +from freqtrade.resolvers.strategy_resolver import StrategyResolver + + + diff --git a/freqtrade/resolvers/exchange_resolver.py b/freqtrade/resolvers/exchange_resolver.py index 2b6a731a9..ed6715d15 100644 --- a/freqtrade/resolvers/exchange_resolver.py +++ b/freqtrade/resolvers/exchange_resolver.py @@ -3,10 +3,11 @@ This module loads custom exchanges """ import logging -from freqtrade.exchange import Exchange, MAP_EXCHANGE_CHILDCLASS import freqtrade.exchange as exchanges +from freqtrade.exchange import MAP_EXCHANGE_CHILDCLASS, Exchange from freqtrade.resolvers import IResolver + logger = logging.getLogger(__name__) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 5dcf73d67..5e73498ae 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -13,6 +13,7 @@ from freqtrade.optimize.hyperopt_interface import IHyperOpt from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss from freqtrade.resolvers import IResolver + logger = logging.getLogger(__name__) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index b7d25ef2c..846c85a5c 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -11,6 +11,7 @@ from typing import Any, Dict, Iterator, List, Optional, Tuple, Type, Union from freqtrade.exceptions import OperationalException + logger = logging.getLogger(__name__) diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index 77db74084..4df5da37c 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -9,6 +9,7 @@ from pathlib import Path from freqtrade.pairlist.IPairList import IPairList from freqtrade.resolvers import IResolver + logger = logging.getLogger(__name__) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 121a04877..ead7424ec 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -11,12 +11,12 @@ from inspect import getfullargspec from pathlib import Path from typing import Any, Dict, Optional -from freqtrade.constants import (REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES, - USERPATH_STRATEGIES) +from freqtrade.constants import REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES, USERPATH_STRATEGIES from freqtrade.exceptions import OperationalException from freqtrade.resolvers import IResolver from freqtrade.strategy.interface import IStrategy + logger = logging.getLogger(__name__) diff --git a/freqtrade/rpc/__init__.py b/freqtrade/rpc/__init__.py index 31c854f82..88978519b 100644 --- a/freqtrade/rpc/__init__.py +++ b/freqtrade/rpc/__init__.py @@ -1,2 +1,3 @@ -from .rpc import RPC, RPCMessageType, RPCException # noqa -from .rpc_manager import RPCManager # noqa +# flake8: noqa: F401 +from .rpc import RPC, RPCException, RPCMessageType +from .rpc_manager import RPCManager diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index db22ce453..588062023 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -8,9 +8,8 @@ from arrow import Arrow from flask import Flask, jsonify, request from flask.json import JSONEncoder from flask_cors import CORS -from flask_jwt_extended import (JWTManager, create_access_token, - create_refresh_token, get_jwt_identity, - jwt_refresh_token_required, +from flask_jwt_extended import (JWTManager, create_access_token, create_refresh_token, + get_jwt_identity, jwt_refresh_token_required, verify_jwt_in_request_optional) from werkzeug.security import safe_str_cmp from werkzeug.serving import make_server @@ -21,6 +20,7 @@ from freqtrade.persistence import Trade from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.rpc import RPC, RPCException + logger = logging.getLogger(__name__) BASE_URI = "/api/v1" diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b32af1596..e4ac65981 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -21,6 +21,7 @@ from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State from freqtrade.strategy.interface import SellType + logger = logging.getLogger(__name__) diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index e54749369..b97a5357b 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -6,6 +6,7 @@ from typing import Any, Dict, List from freqtrade.rpc import RPC, RPCMessageType + logger = logging.getLogger(__name__) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 87e52980a..01d21c53c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -5,9 +5,9 @@ This module manage Telegram communication """ import json import logging -import arrow from typing import Any, Callable, Dict +import arrow from tabulate import tabulate from telegram import ParseMode, ReplyKeyboardMarkup, Update from telegram.error import NetworkError, TelegramError @@ -18,6 +18,7 @@ from freqtrade.__init__ import __version__ from freqtrade.rpc import RPC, RPCException, RPCMessageType from freqtrade.rpc.fiat_convert import CryptoToFiatConverter + logger = logging.getLogger(__name__) logger.debug('Included module rpc.telegram ...') diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index f089550c3..21413f165 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -2,9 +2,9 @@ This module manages webhook communication """ import logging -from typing import Any, Dict +from typing import Any, Dict -from requests import post, RequestException +from requests import RequestException, post from freqtrade.rpc import RPC, RPCMessageType diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index d1510489e..662156ae9 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -1,5 +1,5 @@ # flake8: noqa: F401 -from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_prev_date, - timeframe_to_seconds, timeframe_to_next_date, timeframe_to_msecs) +from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, + timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.strategy_helper import merge_informative_pair diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 92d9f6c48..04d7055ba 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -21,6 +21,7 @@ from freqtrade.persistence import Trade from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets + logger = logging.getLogger(__name__) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 1a5b2d0f8..ea0e234ec 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -1,4 +1,5 @@ import pandas as pd + from freqtrade.exchange import timeframe_to_minutes diff --git a/freqtrade/strategy/strategy_wrapper.py b/freqtrade/strategy/strategy_wrapper.py index 8fc548074..121189b68 100644 --- a/freqtrade/strategy/strategy_wrapper.py +++ b/freqtrade/strategy/strategy_wrapper.py @@ -2,6 +2,7 @@ import logging from freqtrade.exceptions import StrategyError + logger = logging.getLogger(__name__) diff --git a/freqtrade/templates/sample_hyperopt.py b/freqtrade/templates/sample_hyperopt.py index 0b6d030db..10743e911 100644 --- a/freqtrade/templates/sample_hyperopt.py +++ b/freqtrade/templates/sample_hyperopt.py @@ -1,4 +1,5 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +# isort: skip_file # --- Do not remove these libs --- from functools import reduce diff --git a/freqtrade/templates/sample_hyperopt_advanced.py b/freqtrade/templates/sample_hyperopt_advanced.py index 7f05c4430..52e397466 100644 --- a/freqtrade/templates/sample_hyperopt_advanced.py +++ b/freqtrade/templates/sample_hyperopt_advanced.py @@ -1,5 +1,5 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement - +# isort: skip_file # --- Do not remove these libs --- from functools import reduce from typing import Any, Callable, Dict, List diff --git a/freqtrade/templates/sample_hyperopt_loss.py b/freqtrade/templates/sample_hyperopt_loss.py index 4173d97f5..59e6d814a 100644 --- a/freqtrade/templates/sample_hyperopt_loss.py +++ b/freqtrade/templates/sample_hyperopt_loss.py @@ -1,10 +1,11 @@ -from math import exp from datetime import datetime +from math import exp from pandas import DataFrame from freqtrade.optimize.hyperopt import IHyperOptLoss + # Define some constants: # set TARGET_TRADES to suit your number concurrent trades so its realistic diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index e269848d2..103f68a43 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -1,5 +1,5 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement - +# isort: skip_file # --- Do not remove these libs --- import numpy as np # noqa import pandas as pd # noqa diff --git a/freqtrade/vendor/qtpylib/indicators.py b/freqtrade/vendor/qtpylib/indicators.py index e5a404862..4c0fb5b5c 100644 --- a/freqtrade/vendor/qtpylib/indicators.py +++ b/freqtrade/vendor/qtpylib/indicators.py @@ -19,14 +19,15 @@ # limitations under the License. # -import warnings import sys +import warnings from datetime import datetime, timedelta import numpy as np import pandas as pd from pandas.core.base import PandasObject + # ============================================= # check min, python version if sys.version_info < (3, 4): diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index ac08f337c..21a9466e1 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -10,6 +10,7 @@ import arrow from freqtrade.exchange import Exchange from freqtrade.persistence import Trade + logger = logging.getLogger(__name__) diff --git a/freqtrade/worker.py b/freqtrade/worker.py index 2fc206bd5..ec9331eef 100755 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -15,6 +15,7 @@ from freqtrade.exceptions import OperationalException, TemporaryError from freqtrade.freqtradebot import FreqtradeBot from freqtrade.state import State + logger = logging.getLogger(__name__) diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 95685fd64..8512777df 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -10,8 +10,8 @@ so it can be used as a standalone script. import argparse import inspect import json -import re import logging +import re import sys from pathlib import Path from urllib.parse import urlencode, urlparse, urlunparse @@ -20,6 +20,7 @@ import rapidjson import requests from requests.exceptions import ConnectionError + logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', diff --git a/setup.cfg b/setup.cfg index 57a0c42e7..be2cd450c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,6 +11,7 @@ exclude = [isort] line_length=100 multi_line_output=0 +lines_after_imports=2 [mypy] ignore_missing_imports = True From 9df366d9431b902a4061fa09dd9a230f0e28d196 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Sep 2020 19:43:15 +0200 Subject: [PATCH 0714/1197] Apply isort to tests --- tests/commands/test_build_config.py | 6 ++---- tests/commands/test_commands.py | 17 +++++++---------- tests/conftest.py | 5 +++-- tests/data/test_btanalysis.py | 15 +++++---------- tests/data/test_converter.py | 12 +++++------- tests/data/test_history.py | 21 ++++++++++----------- tests/edge/test_edge.py | 3 ++- tests/exchange/test_binance.py | 3 +-- tests/exchange/test_exchange.py | 15 ++++++--------- tests/exchange/test_ftx.py | 1 + tests/exchange/test_kraken.py | 1 + tests/optimize/__init__.py | 1 + tests/optimize/test_backtest_detail.py | 1 + tests/optimize/test_backtesting.py | 4 ++-- tests/optimize/test_hyperopt.py | 8 +++----- tests/optimize/test_optimize_reports.py | 16 ++++++---------- tests/rpc/test_rpc.py | 3 +-- tests/rpc/test_rpc_apiserver.py | 4 ++-- tests/rpc/test_rpc_manager.py | 6 +++--- tests/rpc/test_rpc_telegram.py | 5 ++--- tests/strategy/strats/legacy_strategy.py | 9 +++++---- tests/strategy/test_interface.py | 1 + tests/strategy/test_strategy_helpers.py | 2 +- tests/test_configuration.py | 12 +++++------- tests/test_directory_operations.py | 3 +-- tests/test_freqtradebot.py | 23 +++++++++-------------- tests/test_indicators.py | 3 ++- tests/test_main.py | 2 +- tests/test_misc.py | 5 ++--- tests/test_plotting.py | 11 ++++------- tests/test_talib.py | 2 +- 31 files changed, 96 insertions(+), 124 deletions(-) diff --git a/tests/commands/test_build_config.py b/tests/commands/test_build_config.py index 69b277e3b..291720f4b 100644 --- a/tests/commands/test_build_config.py +++ b/tests/commands/test_build_config.py @@ -4,10 +4,8 @@ from unittest.mock import MagicMock import pytest import rapidjson -from freqtrade.commands.build_config_commands import (ask_user_config, - ask_user_overwrite, - start_new_config, - validate_is_float, +from freqtrade.commands.build_config_commands import (ask_user_config, ask_user_overwrite, + start_new_config, validate_is_float, validate_is_int) from freqtrade.exceptions import OperationalException from tests.conftest import get_args, log_has_re diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 192e125f8..5b125697c 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -5,19 +5,16 @@ from unittest.mock import MagicMock, PropertyMock import arrow import pytest -from freqtrade.commands import (start_convert_data, start_create_userdir, - start_download_data, start_hyperopt_list, - start_hyperopt_show, start_list_data, - start_list_exchanges, start_list_hyperopts, - start_list_markets, start_list_strategies, - start_list_timeframes, start_new_hyperopt, - start_new_strategy, start_show_trades, - start_test_pairlist, start_trading) +from freqtrade.commands import (start_convert_data, start_create_userdir, start_download_data, + start_hyperopt_list, start_hyperopt_show, start_list_data, + start_list_exchanges, start_list_hyperopts, start_list_markets, + start_list_strategies, start_list_timeframes, start_new_hyperopt, + start_new_strategy, start_show_trades, start_test_pairlist, + start_trading) from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode -from tests.conftest import (create_mock_trades, get_args, log_has, log_has_re, - patch_exchange, +from tests.conftest import (create_mock_trades, get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) from tests.conftest_trades import MOCK_TRADE_COUNT diff --git a/tests/conftest.py b/tests/conftest.py index fe55c8784..dbd0df8f6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,8 +22,9 @@ from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver from freqtrade.worker import Worker -from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, - mock_trade_4, mock_trade_5, mock_trade_6) +from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, + mock_trade_5, mock_trade_6) + logging.getLogger('').setLevel(logging.INFO) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 564dae0b1..a6270e92d 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -7,16 +7,11 @@ from pandas import DataFrame, DateOffset, Timestamp, to_datetime from freqtrade.configuration import TimeRange from freqtrade.constants import LAST_BT_RESULT_FN -from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, - analyze_trade_parallelism, - calculate_market_change, - calculate_max_drawdown, - combine_dataframes_with_mean, - create_cum_profit, - extract_trades_of_period, - get_latest_backtest_filename, - load_backtest_data, load_trades, - load_trades_from_db) +from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, analyze_trade_parallelism, + calculate_market_change, calculate_max_drawdown, + combine_dataframes_with_mean, create_cum_profit, + extract_trades_of_period, get_latest_backtest_filename, + load_backtest_data, load_trades, load_trades_from_db) from freqtrade.data.history import load_data, load_pair_history from freqtrade.optimize.backtesting import BacktestResult from tests.conftest import create_mock_trades diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index 4a580366f..fdba7900f 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -2,13 +2,11 @@ import logging from freqtrade.configuration.timerange import TimeRange -from freqtrade.data.converter import (convert_ohlcv_format, - convert_trades_format, - ohlcv_fill_up_missing_data, - ohlcv_to_dataframe, trades_dict_to_list, - trades_remove_duplicates, trim_dataframe) -from freqtrade.data.history import (get_timerange, load_data, - load_pair_history, validate_backtest_data) +from freqtrade.data.converter import (convert_ohlcv_format, convert_trades_format, + ohlcv_fill_up_missing_data, ohlcv_to_dataframe, + trades_dict_to_list, trades_remove_duplicates, trim_dataframe) +from freqtrade.data.history import (get_timerange, load_data, load_pair_history, + validate_backtest_data) from tests.conftest import log_has from tests.data.test_history import _backup_file, _clean_test_file diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 787f62a75..c8324cf0b 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -15,20 +15,19 @@ from freqtrade.configuration import TimeRange from freqtrade.constants import AVAILABLE_DATAHANDLERS from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.data.history.hdf5datahandler import HDF5DataHandler -from freqtrade.data.history.history_utils import ( - _download_pair_history, _download_trades_history, - _load_cached_data_for_updating, convert_trades_to_ohlcv, get_timerange, - load_data, load_pair_history, refresh_backtest_ohlcv_data, - refresh_backtest_trades_data, refresh_data, validate_backtest_data) -from freqtrade.data.history.idatahandler import (IDataHandler, get_datahandler, - get_datahandlerclass) -from freqtrade.data.history.jsondatahandler import (JsonDataHandler, - JsonGzDataHandler) +from freqtrade.data.history.history_utils import (_download_pair_history, _download_trades_history, + _load_cached_data_for_updating, + convert_trades_to_ohlcv, get_timerange, load_data, + load_pair_history, refresh_backtest_ohlcv_data, + refresh_backtest_trades_data, refresh_data, + validate_backtest_data) +from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler, get_datahandlerclass +from freqtrade.data.history.jsondatahandler import JsonDataHandler, JsonGzDataHandler from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import file_dump_json from freqtrade.resolvers import StrategyResolver -from tests.conftest import (get_patched_exchange, log_has, log_has_re, - patch_exchange) +from tests.conftest import get_patched_exchange, log_has, log_has_re, patch_exchange + # Change this if modifying UNITTEST/BTC testdatafile _BTC_UNITTEST_LENGTH = 13681 diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index d35f7fcf6..f19590490 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -10,14 +10,15 @@ import numpy as np import pytest from pandas import DataFrame, to_datetime -from freqtrade.exceptions import OperationalException from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo +from freqtrade.exceptions import OperationalException from freqtrade.strategy.interface import SellType from tests.conftest import get_patched_freqtradebot, log_has from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, _get_frame_time_from_offset) + # Cases to be tested: # 1) Open trade should be removed from the end # 2) Two complete trades within dataframe (with sell hit for all) diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 72da708b4..f2b508761 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -4,8 +4,7 @@ from unittest.mock import MagicMock import ccxt import pytest -from freqtrade.exceptions import (DependencyException, InvalidOrderException, - OperationalException) +from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException from tests.conftest import get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index e0b97d157..7be9c77ac 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -9,21 +9,18 @@ import ccxt import pytest from pandas import DataFrame -from freqtrade.exceptions import (DDosProtection, DependencyException, - InvalidOrderException, OperationalException, - TemporaryError) +from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException, + OperationalException, TemporaryError) from freqtrade.exchange import Binance, Exchange, Kraken -from freqtrade.exchange.common import (API_RETRY_COUNT, API_FETCH_ORDER_RETRY_COUNT, +from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT, calculate_backoff) -from freqtrade.exchange.exchange import (market_is_active, - timeframe_to_minutes, - timeframe_to_msecs, - timeframe_to_next_date, - timeframe_to_prev_date, +from freqtrade.exchange.exchange import (market_is_active, timeframe_to_minutes, timeframe_to_msecs, + timeframe_to_next_date, timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.resolvers.exchange_resolver import ExchangeResolver from tests.conftest import get_patched_exchange, log_has, log_has_re + # Make sure to always keep one exchange here which is NOT subclassed!! EXCHANGES = ['bittrex', 'binance', 'kraken', 'ftx'] diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 16789af2c..17cfb26fa 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -10,6 +10,7 @@ from tests.conftest import get_patched_exchange from .test_exchange import ccxt_exceptionhandlers + STOPLOSS_ORDERTYPE = 'stop' diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 8f774a7ec..31b79a202 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -8,6 +8,7 @@ from freqtrade.exceptions import DependencyException, InvalidOrderException from tests.conftest import get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers + STOPLOSS_ORDERTYPE = 'stop-loss' diff --git a/tests/optimize/__init__.py b/tests/optimize/__init__.py index 8bc66f02c..306850ff6 100644 --- a/tests/optimize/__init__.py +++ b/tests/optimize/__init__.py @@ -6,6 +6,7 @@ from pandas import DataFrame from freqtrade.exchange import timeframe_to_minutes from freqtrade.strategy.interface import SellType + tests_start_time = arrow.get(2018, 10, 3) tests_timeframe = '1h' diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index f6ac95aeb..a5de64fe4 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -11,6 +11,7 @@ from tests.conftest import patch_exchange from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, _get_frame_time_from_offset, tests_timeframe) + # Test 0: Sell with signal sell in candle 3 # Test with Stop-loss at 1% tc0 = BTContainer(data=[ diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 78a7130f9..45cbea68e 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -10,8 +10,7 @@ import pytest from arrow import Arrow from freqtrade import constants -from freqtrade.commands.optimize_commands import (setup_optimize_configuration, - start_backtesting) +from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_backtesting from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import BT_DATA_COLUMNS, evaluate_result_multi @@ -26,6 +25,7 @@ from freqtrade.strategy.interface import SellType from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) + ORDER_TYPES = [ { 'buy': 'limit', diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index d58b91209..bf8a720b5 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1,9 +1,9 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 import locale import logging +from copy import deepcopy from datetime import datetime from pathlib import Path -from copy import deepcopy from typing import Dict, List from unittest.mock import MagicMock, PropertyMock @@ -13,14 +13,12 @@ from arrow import Arrow from filelock import Timeout from freqtrade import constants -from freqtrade.commands.optimize_commands import (setup_optimize_configuration, - start_hyperopt) +from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.optimize.default_hyperopt_loss import DefaultHyperOptLoss from freqtrade.optimize.hyperopt import Hyperopt -from freqtrade.resolvers.hyperopt_resolver import (HyperOptLossResolver, - HyperOptResolver) +from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver from freqtrade.state import RunMode from freqtrade.strategy.interface import SellType from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index b484e4390..d04929164 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -5,21 +5,17 @@ from pathlib import Path import pandas as pd import pytest from arrow import Arrow + from freqtrade.configuration import TimeRange from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.data import history -from freqtrade.data.btanalysis import (get_latest_backtest_filename, - load_backtest_data) +from freqtrade.data.btanalysis import get_latest_backtest_filename, load_backtest_data from freqtrade.edge import PairInfo -from freqtrade.optimize.optimize_reports import (generate_backtest_stats, - generate_daily_stats, - generate_edge_table, - generate_pair_metrics, +from freqtrade.optimize.optimize_reports import (generate_backtest_stats, generate_daily_stats, + generate_edge_table, generate_pair_metrics, generate_sell_reason_stats, - generate_strategy_metrics, - store_backtest_stats, - text_table_bt_results, - text_table_sell_reason, + generate_strategy_metrics, store_backtest_stats, + text_table_bt_results, text_table_sell_reason, text_table_strategy) from freqtrade.resolvers.strategy_resolver import StrategyResolver from freqtrade.strategy.interface import SellType diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index c2dee6439..977dfbc20 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -13,8 +13,7 @@ from freqtrade.persistence import Trade from freqtrade.rpc import RPC, RPCException from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State -from tests.conftest import (create_mock_trades, get_patched_freqtradebot, - patch_get_signal) +from tests.conftest import create_mock_trades, get_patched_freqtradebot, patch_get_signal # Functions for recurrent object patching diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 626586a4a..fea7b1c73 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -14,8 +14,8 @@ from freqtrade.loggers import setup_logging, setup_logging_pre from freqtrade.persistence import Trade from freqtrade.rpc.api_server import BASE_URI, ApiServer from freqtrade.state import State -from tests.conftest import (create_mock_trades, get_patched_freqtradebot, - log_has, patch_get_signal) +from tests.conftest import create_mock_trades, get_patched_freqtradebot, log_has, patch_get_signal + _TEST_USER = "FreqTrader" _TEST_PASS = "SuperSecurePassword1!" diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index e8d0f648e..4b715fc37 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -1,10 +1,10 @@ # pragma pylint: disable=missing-docstring, C0103 -import time import logging +import time from unittest.mock import MagicMock -from freqtrade.rpc import RPCMessageType, RPCManager -from tests.conftest import log_has, get_patched_freqtradebot +from freqtrade.rpc import RPCManager, RPCMessageType +from tests.conftest import get_patched_freqtradebot, log_has def test__init__(mocker, default_conf) -> None: diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 3958a825a..aee88104b 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -23,9 +23,8 @@ from freqtrade.rpc import RPCMessageType from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.state import State from freqtrade.strategy.interface import SellType -from tests.conftest import (create_mock_trades, get_patched_freqtradebot, - log_has, patch_exchange, patch_get_signal, - patch_whitelist) +from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, patch_exchange, + patch_get_signal, patch_whitelist) class DummyCls(Telegram): diff --git a/tests/strategy/strats/legacy_strategy.py b/tests/strategy/strats/legacy_strategy.py index 9cbce0ad5..1e7bb5e1e 100644 --- a/tests/strategy/strats/legacy_strategy.py +++ b/tests/strategy/strats/legacy_strategy.py @@ -1,12 +1,13 @@ # --- Do not remove these libs --- -from freqtrade.strategy.interface import IStrategy -from pandas import DataFrame -# -------------------------------- - # Add your lib to import here import talib.abstract as ta +from pandas import DataFrame +from freqtrade.strategy.interface import IStrategy + + +# -------------------------------- # This class is a sample. Feel free to customize it. class TestStrategyLegacy(IStrategy): diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index f1b5d0244..729b14f7b 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -19,6 +19,7 @@ from tests.conftest import log_has, log_has_re from .strats.default_strategy import DefaultStrategy + # Avoid to reinit the same object again and again _STRATEGY = DefaultStrategy(config={}) _STRATEGY.dp = DataProvider({}, None, None) diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 4b29bf304..1d3e80d24 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -1,5 +1,5 @@ -import pandas as pd import numpy as np +import pandas as pd from freqtrade.strategy import merge_informative_pair, timeframe_to_minutes diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 4428fe240..7d6c81f74 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -11,20 +11,18 @@ import pytest from jsonschema import ValidationError from freqtrade.commands import Arguments -from freqtrade.configuration import (Configuration, check_exchange, - remove_credentials, +from freqtrade.configuration import (Configuration, check_exchange, remove_credentials, validate_config_consistency) from freqtrade.configuration.config_validation import validate_config_schema -from freqtrade.configuration.deprecated_settings import ( - check_conflicting_settings, process_deprecated_setting, - process_temporary_deprecated_settings) +from freqtrade.configuration.deprecated_settings import (check_conflicting_settings, + process_deprecated_setting, + process_temporary_deprecated_settings) from freqtrade.configuration.load_config import load_config_file, log_config_error_range from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.exceptions import OperationalException from freqtrade.loggers import _set_loggers, setup_logging, setup_logging_pre from freqtrade.state import RunMode -from tests.conftest import (log_has, log_has_re, - patched_configuration_load_config_file) +from tests.conftest import log_has, log_has_re, patched_configuration_load_config_file @pytest.fixture(scope="function") diff --git a/tests/test_directory_operations.py b/tests/test_directory_operations.py index 71c91549f..a8058c514 100644 --- a/tests/test_directory_operations.py +++ b/tests/test_directory_operations.py @@ -4,8 +4,7 @@ from unittest.mock import MagicMock import pytest -from freqtrade.configuration.directory_operations import (copy_sample_files, - create_datadir, +from freqtrade.configuration.directory_operations import (copy_sample_files, create_datadir, create_userdata_dir) from freqtrade.exceptions import OperationalException from tests.conftest import log_has, log_has_re diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 0c12c05bb..8af3e12a7 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -10,12 +10,10 @@ from unittest.mock import ANY, MagicMock, PropertyMock import arrow import pytest -from freqtrade.constants import (CANCEL_REASON, MATH_CLOSE_PREC, - UNLIMITED_STAKE_AMOUNT) -from freqtrade.exceptions import (DependencyException, ExchangeError, - InsufficientFundsError, - InvalidOrderException, OperationalException, - PricingError, TemporaryError) +from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT +from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, + InvalidOrderException, OperationalException, PricingError, + TemporaryError) from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.persistence.models import Order @@ -23,15 +21,12 @@ from freqtrade.rpc import RPCMessageType from freqtrade.state import RunMode, State from freqtrade.strategy.interface import SellCheckTuple, SellType from freqtrade.worker import Worker -from tests.conftest import (create_mock_trades, get_patched_freqtradebot, - get_patched_worker, log_has, log_has_re, - patch_edge, patch_exchange, patch_get_signal, +from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker, + log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal, patch_wallet, patch_whitelist) -from tests.conftest_trades import (MOCK_TRADE_COUNT, mock_order_1, - mock_order_2, mock_order_2_sell, - mock_order_3, mock_order_3_sell, - mock_order_4, mock_order_5_stoploss, - mock_order_6_sell) +from tests.conftest_trades import (MOCK_TRADE_COUNT, mock_order_1, mock_order_2, mock_order_2_sell, + mock_order_3, mock_order_3_sell, mock_order_4, + mock_order_5_stoploss, mock_order_6_sell) def patch_RPCManager(mocker) -> MagicMock: diff --git a/tests/test_indicators.py b/tests/test_indicators.py index 2f9bdc0f9..8d02330a1 100644 --- a/tests/test_indicators.py +++ b/tests/test_indicators.py @@ -1,7 +1,8 @@ -import freqtrade.vendor.qtpylib.indicators as qtpylib import numpy as np import pandas as pd +import freqtrade.vendor.qtpylib.indicators as qtpylib + def test_crossed_numpy_types(): """ diff --git a/tests/test_main.py b/tests/test_main.py index dd0c877e8..9106d4c12 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,11 +1,11 @@ # pragma pylint: disable=missing-docstring from copy import deepcopy +from pathlib import Path from unittest.mock import MagicMock, PropertyMock import pytest -from pathlib import Path from freqtrade.commands import Arguments from freqtrade.exceptions import FreqtradeException, OperationalException from freqtrade.freqtradebot import FreqtradeBot diff --git a/tests/test_misc.py b/tests/test_misc.py index a185cbba4..6dcd9fbe5 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -7,9 +7,8 @@ from unittest.mock import MagicMock import pytest from freqtrade.data.converter import ohlcv_to_dataframe -from freqtrade.misc import (datesarray_to_datetimearray, file_dump_json, - file_load_json, format_ms_time, pair_to_filename, - plural, render_template, +from freqtrade.misc import (datesarray_to_datetimearray, file_dump_json, file_load_json, + format_ms_time, pair_to_filename, plural, render_template, render_template_with_fallback, safe_value_fallback, safe_value_fallback2, shorten_date) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index bcababbf1..401f66b60 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -13,13 +13,10 @@ from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data from freqtrade.exceptions import OperationalException -from freqtrade.plot.plotting import (add_indicators, add_profit, - create_plotconfig, - generate_candlestick_graph, - generate_plot_filename, - generate_profit_graph, init_plotscript, - load_and_plot_trades, plot_profit, - plot_trades, store_plot_file) +from freqtrade.plot.plotting import (add_indicators, add_profit, create_plotconfig, + generate_candlestick_graph, generate_plot_filename, + generate_profit_graph, init_plotscript, load_and_plot_trades, + plot_profit, plot_trades, store_plot_file) from freqtrade.resolvers import StrategyResolver from tests.conftest import get_args, log_has, log_has_re, patch_exchange diff --git a/tests/test_talib.py b/tests/test_talib.py index 4effc129b..f526fdd4d 100644 --- a/tests/test_talib.py +++ b/tests/test_talib.py @@ -1,5 +1,5 @@ -import talib.abstract as ta import pandas as pd +import talib.abstract as ta def test_talib_bollingerbands_near_zero_values(): From ce228f19dccda1f2604e9e8b575b314d39615dc4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Sep 2020 19:43:32 +0200 Subject: [PATCH 0715/1197] Apply isort to setup.py --- setup.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.py b/setup.py index 88d754668..c17722ade 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,15 @@ from sys import version_info + from setuptools import setup + if version_info.major == 3 and version_info.minor < 6 or \ version_info.major < 3: print('Your Python interpreter must be 3.6 or greater!') exit(1) from pathlib import Path # noqa: E402 + from freqtrade import __version__ # noqa: E402 From 0ea56548e4a435b403fb7ba48c48c7f433a0e4d4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Sep 2020 19:50:22 +0200 Subject: [PATCH 0716/1197] Try fix random test failure --- tests/rpc/test_rpc_telegram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 3958a825a..400e33c6b 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1139,9 +1139,9 @@ def test_telegram_logs(default_conf, update, mocker) -> None: context = MagicMock() context.args = [] telegram._logs(update=update, context=context) - # Called at least 3 times. Exact times will change with unrelated changes to setup messages + # Called at least 2 times. Exact times will change with unrelated changes to setup messages # Therefore we don't test for this explicitly. - assert msg_mock.call_count > 3 + assert msg_mock.call_count >= 2 def test_edge_disabled(default_conf, update, mocker) -> None: From ace28792657ece704189e04e4c06b5079dfa9d2a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Sep 2020 19:53:29 +0200 Subject: [PATCH 0717/1197] Don't run isort on windows - once is enough --- .github/workflows/ci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 19c5479a4..e6c6521eb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -160,10 +160,6 @@ jobs: run: | flake8 - - name: Sort imports (isort) - run: | - isort --check . - - name: Mypy run: | mypy freqtrade scripts From 2be8e8070ae1e4fd7874c72c01bd05ac13eabb7c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 28 Sep 2020 20:01:59 +0200 Subject: [PATCH 0718/1197] Add Python 3.8 to setup.py classifiers --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index c17722ade..9b57e8d2c 100644 --- a/setup.py +++ b/setup.py @@ -111,6 +111,7 @@ setup(name='freqtrade', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Operating System :: MacOS', 'Operating System :: Unix', 'Topic :: Office/Business :: Financial :: Investment', From 7bce2cd29daa65a8013d0f2f44fee817901b2465 Mon Sep 17 00:00:00 2001 From: Xu Wang Date: Mon, 28 Sep 2020 20:30:20 +0100 Subject: [PATCH 0719/1197] Add trade duration by win/loss. --- freqtrade/rpc/telegram.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ea8597469..bfe486951 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -763,16 +763,10 @@ class Telegram(RPC): # Sell reason sell_reasons = {} for trade in trades_closed: - if trade['sell_reason'] in sell_reasons: - sell_reasons[trade['sell_reason']][trade_win_loss(trade)] += 1 - else: - win_loss_count = {'Wins': 0, 'Losses': 0, 'Draws': 0} - win_loss_count[trade_win_loss(trade)] += 1 - sell_reasons[trade['sell_reason']] = win_loss_count + if trade['sell_reason'] not in sell_reasons: + sell_reasons[trade['sell_reason']] = {'Wins': 0, 'Losses': 0, 'Draws': 0} + sell_reasons[trade['sell_reason']][trade_win_loss(trade)] += 1 sell_reasons_tabulate = [] - # | Sell Reason | Sells | Wins | Draws | Losses | - # |-------------|------:|-----:|------:|-------:| - # | test | 1 | 2 | 3 | 4 | for reason, count in sell_reasons.items(): sell_reasons_tabulate.append([ reason, sum(count.values()), @@ -785,9 +779,22 @@ class Telegram(RPC): headers=['Sell Reason', 'Sells', 'Wins', 'Draws', 'Losses'] ) - # TODO: Duration + # Duration + dur = {'Wins': [], 'Draws': [], 'Losses': []} + for trade in trades_closed: + if trade['close_date'] is not None and trade['open_date'] is not None: + trade_dur = arrow.get(trade['close_date']) - arrow.get(trade['open_date']) + dur[trade_win_loss(trade)].append(trade_dur) + wins_dur = sum(dur['Wins']) / len(dur['Wins']) if len(dur['Wins']) > 0 else 'N/A' + draws_dur = sum(dur['Draws']) / len(dur['Draws']) if len(dur['Draws']) > 0 else 'N/A' + losses_dur = sum(dur['Losses']) / len(dur['Losses']) if len(dur['Losses']) > 0 else 'N/A' + duration_msg = tabulate( + [['Wins', str(wins_dur)], ['Draws', str(draws_dur)], ['Losses', str(losses_dur)]], + headers=['', 'Duration'] + ) + + self._send_msg('\n'.join([sell_reasons_msg, duration_msg])) - @authorized_only def _show_config(self, update: Update, context: CallbackContext) -> None: """ From 9dfbc1a7ff1b1dfa8263714f028ba6fa3b9d572c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 12 Jun 2020 19:32:44 +0200 Subject: [PATCH 0720/1197] Add analyzed_history endpoint --- freqtrade/rpc/api_server.py | 14 ++++++++++++++ freqtrade/rpc/rpc.py | 10 ++++++++++ 2 files changed, 24 insertions(+) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 588062023..7fe178288 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -212,6 +212,8 @@ class ApiServer(RPC): view_func=self._trades, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/trades/', 'trades_delete', view_func=self._trades_delete, methods=['DELETE']) + self.app.add_url_rule(f'{BASE_URI}/pair_history', 'pair_history', + view_func=self._analysed_history, methods=['GET']) # Combined actions and infos self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist, methods=['GET', 'POST']) @@ -500,3 +502,15 @@ class ApiServer(RPC): tradeid = request.json.get("tradeid") results = self._rpc_forcesell(tradeid) return self.rest_dump(results) + + @require_login + @rpc_catch_errors + def _analysed_history(self): + """ + Handler for /pair_history. + """ + pair = request.args.get("pair") + timeframe = request.args.get("timeframe") + + results = self._rpc_analysed_history(pair, timeframe) + return self.rest_dump(results) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e4ac65981..621971952 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -653,3 +653,13 @@ class RPC: if not self._freqtrade.edge: raise RPCException('Edge is not enabled.') return self._freqtrade.edge.accepted_pairs() + + def _rpc_analysed_history(self, pair, timeframe): + + data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe(pair, timeframe) + return { + 'columns': data.columns, + 'data': data.values.tolist(), + 'length': len(data), + 'last_analyzed': last_analyzed, + } From a38b33cd9cbe6236d9dd5793f7c173d50d6bc83f Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jun 2020 07:53:23 +0200 Subject: [PATCH 0721/1197] Support limiting analyzed history --- freqtrade/rpc/api_server.py | 17 ++++++++++++----- freqtrade/rpc/rpc.py | 4 +++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 7fe178288..78f5c5046 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -26,15 +26,15 @@ logger = logging.getLogger(__name__) BASE_URI = "/api/v1" -class ArrowJSONEncoder(JSONEncoder): +class FTJSONEncoder(JSONEncoder): def default(self, obj): try: if isinstance(obj, Arrow): return obj.for_json() - elif isinstance(obj, date): - return obj.strftime("%Y-%m-%d") elif isinstance(obj, datetime): return obj.strftime(DATETIME_PRINT_FORMAT) + elif isinstance(obj, date): + return obj.strftime("%Y-%m-%d") iterable = iter(obj) except TypeError: pass @@ -108,7 +108,7 @@ class ApiServer(RPC): 'jwt_secret_key', 'super-secret') self.jwt = JWTManager(self.app) - self.app.json_encoder = ArrowJSONEncoder + self.app.json_encoder = FTJSONEncoder self.app.teardown_appcontext(shutdown_session) @@ -508,9 +508,16 @@ class ApiServer(RPC): def _analysed_history(self): """ Handler for /pair_history. + Takes the following get arguments: + get: + parameters: + - pair: Pair + - timeframe: Timeframe to get data for (should be aligned to strategy.timeframe) + - limit: Limit return length to the latest X candles """ pair = request.args.get("pair") timeframe = request.args.get("timeframe") + limit = request.args.get("limit") - results = self._rpc_analysed_history(pair, timeframe) + results = self._rpc_analysed_history(pair, timeframe, limit) return self.rest_dump(results) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 621971952..a6215a892 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -654,9 +654,11 @@ class RPC: raise RPCException('Edge is not enabled.') return self._freqtrade.edge.accepted_pairs() - def _rpc_analysed_history(self, pair, timeframe): + def _rpc_analysed_history(self, pair, timeframe, limit): data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe(pair, timeframe) + if limit: + data = data.iloc[:-limit] return { 'columns': data.columns, 'data': data.values.tolist(), From 133ca9c770181387a9f9020953564171f996d03c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jun 2020 08:47:18 +0200 Subject: [PATCH 0722/1197] Convert types to support valid json --- freqtrade/rpc/api_server.py | 2 +- freqtrade/rpc/rpc.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 78f5c5046..a04452afd 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -517,7 +517,7 @@ class ApiServer(RPC): """ pair = request.args.get("pair") timeframe = request.args.get("timeframe") - limit = request.args.get("limit") + limit = request.args.get("limit", type=int) results = self._rpc_analysed_history(pair, timeframe, limit) return self.rest_dump(results) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index a6215a892..4b83a3da6 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -9,7 +9,7 @@ from math import isnan from typing import Any, Dict, List, Optional, Tuple, Union import arrow -from numpy import NAN, mean +from numpy import NAN, mean, int64 from freqtrade.constants import CANCEL_REASON from freqtrade.exceptions import ExchangeError, PricingError @@ -656,12 +656,14 @@ class RPC: def _rpc_analysed_history(self, pair, timeframe, limit): - data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe(pair, timeframe) + _data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe(pair, timeframe) if limit: - data = data.iloc[:-limit] + _data = _data.iloc[-limit:] + _data = _data.replace({NAN: None}) + _data['date'] = _data['date'].astype(int64) // 1000 // 1000 return { - 'columns': data.columns, - 'data': data.values.tolist(), - 'length': len(data), + 'columns': list(_data.columns), + 'data': _data.values.tolist(), + 'length': len(_data), 'last_analyzed': last_analyzed, } From d528c44974a318d5b40176a45c0d54c7255587a7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 15 Jun 2020 08:47:43 +0200 Subject: [PATCH 0723/1197] Add test for pair_history --- tests/rpc/test_rpc_apiserver.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index fea7b1c73..bb13531b5 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -811,3 +811,27 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets): data='{"tradeid": "1"}') assert_response(rc) assert rc.json == {'result': 'Created sell order for trade 1.'} + + +def test_api_pair_history(botclient, ohlcv_history): + ftbot, client = botclient + timeframe = '5m' + amount = 2 + ohlcv_history['sma'] = ohlcv_history['close'].rolling(2).mean() + ftbot.dataprovider._set_cached_df("XRP/BTC", timeframe, ohlcv_history) + + rc = client_get(client, + f"{BASE_URI}/pair_history?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}") + assert_response(rc) + assert 'columns' in rc.json + assert isinstance(rc.json['columns'], list) + assert rc.json['columns'] == ['date', 'open', 'high', 'low', 'close', 'volume', 'sma'] + + assert 'data' in rc.json + assert len(rc.json['data']) == amount + + assert (rc.json['data'] == + [[1511686200000, 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869, None], + [1511686500000, 8.88e-05, 8.942e-05, 8.88e-05, + 8.893e-05, 0.05874751, 8.886500000000001e-05] + ]) From 677078350f1fcdabdccc5162ad99e8f801808db5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 23 Jun 2020 06:49:53 +0200 Subject: [PATCH 0724/1197] Add plot_config endpoint --- freqtrade/rpc/api_server.py | 10 ++++++++++ freqtrade/rpc/rpc.py | 6 +++++- tests/rpc/test_rpc_apiserver.py | 15 +++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index a04452afd..5f84713f2 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -214,6 +214,8 @@ class ApiServer(RPC): view_func=self._trades_delete, methods=['DELETE']) self.app.add_url_rule(f'{BASE_URI}/pair_history', 'pair_history', view_func=self._analysed_history, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/plot_config', 'plot_config', + view_func=self._plot_config, methods=['GET']) # Combined actions and infos self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist, methods=['GET', 'POST']) @@ -521,3 +523,11 @@ class ApiServer(RPC): results = self._rpc_analysed_history(pair, timeframe, limit) return self.rest_dump(results) + + @require_login + @rpc_catch_errors + def _plot_config(self): + """ + Handler for /plot_config. + """ + return self.rest_dump(self._rpc_plot_config()) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 4b83a3da6..e8ec099e5 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -654,7 +654,7 @@ class RPC: raise RPCException('Edge is not enabled.') return self._freqtrade.edge.accepted_pairs() - def _rpc_analysed_history(self, pair, timeframe, limit): + def _rpc_analysed_history(self, pair, timeframe, limit) -> Dict[str, Any]: _data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe(pair, timeframe) if limit: @@ -667,3 +667,7 @@ class RPC: 'length': len(_data), 'last_analyzed': last_analyzed, } + + def _rpc_plot_config(self) -> Dict[str, Any]: + + return self._freqtrade.strategy.plot_config diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index bb13531b5..9c814e1ac 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -835,3 +835,18 @@ def test_api_pair_history(botclient, ohlcv_history): [1511686500000, 8.88e-05, 8.942e-05, 8.88e-05, 8.893e-05, 0.05874751, 8.886500000000001e-05] ]) + + +def test_api_plot_config(botclient): + ftbot, client = botclient + + rc = client_get(client, f"{BASE_URI}/plot_config") + assert_response(rc) + assert rc.json == {} + + ftbot.strategy.plot_config = {'main_plot': {'sma': {}}, + 'subplots': {'RSI': {'rsi': {'color': 'red'}}}} + rc = client_get(client, f"{BASE_URI}/plot_config") + assert_response(rc) + assert rc.json == ftbot.strategy.plot_config + assert isinstance(rc.json['main_plot'], dict) From f5dc10e4aeafb50634cdaa8d476845fe32f8fc5a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jul 2020 07:10:56 +0200 Subject: [PATCH 0725/1197] Add pair_history endpoint --- freqtrade/rpc/api_server.py | 29 ++++++++++++++++++++-- freqtrade/rpc/rpc.py | 44 +++++++++++++++++++++++++-------- tests/rpc/test_rpc_apiserver.py | 6 +++-- 3 files changed, 65 insertions(+), 14 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 5f84713f2..081dcd623 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -212,6 +212,8 @@ class ApiServer(RPC): view_func=self._trades, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/trades/', 'trades_delete', view_func=self._trades_delete, methods=['DELETE']) + self.app.add_url_rule(f'{BASE_URI}/pair_candles', 'pair_candles', + view_func=self._analysed_candles, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/pair_history', 'pair_history', view_func=self._analysed_history, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/plot_config', 'plot_config', @@ -507,9 +509,10 @@ class ApiServer(RPC): @require_login @rpc_catch_errors - def _analysed_history(self): + def _analysed_candles(self): """ Handler for /pair_history. + Returns the dataframe the bot is using during live/dry operations. Takes the following get arguments: get: parameters: @@ -521,9 +524,31 @@ class ApiServer(RPC): timeframe = request.args.get("timeframe") limit = request.args.get("limit", type=int) - results = self._rpc_analysed_history(pair, timeframe, limit) + results = self._analysed_dataframe(pair, timeframe, limit) return self.rest_dump(results) + @require_login + @rpc_catch_errors + def _analysed_history(self): + """ + Handler for /pair_history. + Returns the dataframe of a given timerange + Takes the following get arguments: + get: + parameters: + - pair: Pair + - timeframe: Timeframe to get data for (should be aligned to strategy.timeframe) + - timerange: timerange in the format YYYYMMDD-YYYYMMDD (YYYYMMDD- or (-YYYYMMDD)) + are als possible. If omitted uses all available data. + """ + pair = request.args.get("pair") + timeframe = request.args.get("timeframe") + timerange = request.args.get("timerange") + + results = self._rpc_analysed_history_full(pair, timeframe, timerange) + return self.rest_dump(results) + + @require_login @rpc_catch_errors def _plot_config(self): diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e8ec099e5..c9d589c31 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -9,9 +9,11 @@ from math import isnan from typing import Any, Dict, List, Optional, Tuple, Union import arrow -from numpy import NAN, mean, int64 +from numpy import NAN, int64, mean +from freqtrade.configuration.timerange import TimeRange from freqtrade.constants import CANCEL_REASON +from freqtrade.data.history import load_data from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.loggers import bufferHandler @@ -654,19 +656,41 @@ class RPC: raise RPCException('Edge is not enabled.') return self._freqtrade.edge.accepted_pairs() - def _rpc_analysed_history(self, pair, timeframe, limit) -> Dict[str, Any]: + def _convert_dataframe_to_dict(self, pair, dataframe, last_analyzed): + dataframe = dataframe.replace({NAN: None}) + dataframe['date'] = dataframe['date'].astype(int64) // 1000 // 1000 + return { + 'pair': pair, + 'columns': list(dataframe.columns), + 'data': dataframe.values.tolist(), + 'length': len(dataframe), + 'last_analyzed': last_analyzed, + } + + def _analysed_dataframe(self, pair: str, timeframe: str, limit: int) -> Dict[str, Any]: _data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe(pair, timeframe) if limit: _data = _data.iloc[-limit:] - _data = _data.replace({NAN: None}) - _data['date'] = _data['date'].astype(int64) // 1000 // 1000 - return { - 'columns': list(_data.columns), - 'data': _data.values.tolist(), - 'length': len(_data), - 'last_analyzed': last_analyzed, - } + return self._convert_dataframe_to_dict(pair, _data, last_analyzed) + + def _rpc_analysed_history_full(self, pair: str, timeframe: str, + timerange: str) -> Dict[str, Any]: + timerange = TimeRange.parse_timerange(None if self.config.get( + 'timerange') is None else str(self.config.get('timerange'))) + + _data = load_data( + datadir=self._freqtrade.config.get("datadir"), + pairs=[pair], + timeframe=self._freqtrade.config.get('timeframe', '5m'), + timerange=timerange, + data_format=self._freqtrade.config.get('dataformat_ohlcv', 'json'), + ) + from freqtrade.resolvers.strategy_resolver import StrategyResolver + strategy = StrategyResolver.load_strategy(self._freqtrade.config) + df_analyzed = strategy.analyze_ticker(_data, {'pair': pair}) + + return self._convert_dataframe_to_dict(pair, df_analyzed, arrow.Arrow.utcnow().datetime) def _rpc_plot_config(self) -> Dict[str, Any]: diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 9c814e1ac..91870aa6b 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -813,7 +813,7 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets): assert rc.json == {'result': 'Created sell order for trade 1.'} -def test_api_pair_history(botclient, ohlcv_history): +def test_api_pair_candles(botclient, ohlcv_history): ftbot, client = botclient timeframe = '5m' amount = 2 @@ -821,11 +821,13 @@ def test_api_pair_history(botclient, ohlcv_history): ftbot.dataprovider._set_cached_df("XRP/BTC", timeframe, ohlcv_history) rc = client_get(client, - f"{BASE_URI}/pair_history?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}") + f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}") assert_response(rc) assert 'columns' in rc.json assert isinstance(rc.json['columns'], list) assert rc.json['columns'] == ['date', 'open', 'high', 'low', 'close', 'volume', 'sma'] + assert 'pair' in rc.json + assert rc.json['pair'] == 'XRP/BTC' assert 'data' in rc.json assert len(rc.json['data']) == amount From 482f1faa88b9c0545185870e9b03b9d3dc149d4f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jul 2020 08:39:07 +0200 Subject: [PATCH 0726/1197] Don't fail if no buy-signal is present --- freqtrade/rpc/rpc.py | 15 +++++++++++---- tests/rpc/test_rpc_apiserver.py | 13 ++++++++++--- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c9d589c31..ef44aeee2 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -657,8 +657,16 @@ class RPC: return self._freqtrade.edge.accepted_pairs() def _convert_dataframe_to_dict(self, pair, dataframe, last_analyzed): - dataframe = dataframe.replace({NAN: None}) dataframe['date'] = dataframe['date'].astype(int64) // 1000 // 1000 + # Move open to seperate column when signal for easy plotting + if 'buy' in dataframe.columns: + buy_mask = (dataframe['buy'] == 1) + dataframe.loc[buy_mask, '_buy_signal_open'] = dataframe.loc[buy_mask, 'open'] + if 'sell' in dataframe.columns: + sell_mask = (dataframe['sell'] == 1) + dataframe.loc[sell_mask, '_sell_signal_open'] = dataframe.loc[sell_mask, 'open'] + dataframe = dataframe.replace({NAN: None}) + return { 'pair': pair, 'columns': list(dataframe.columns), @@ -676,8 +684,7 @@ class RPC: def _rpc_analysed_history_full(self, pair: str, timeframe: str, timerange: str) -> Dict[str, Any]: - timerange = TimeRange.parse_timerange(None if self.config.get( - 'timerange') is None else str(self.config.get('timerange'))) + timerange = TimeRange.parse_timerange(timerange) _data = load_data( datadir=self._freqtrade.config.get("datadir"), @@ -688,7 +695,7 @@ class RPC: ) from freqtrade.resolvers.strategy_resolver import StrategyResolver strategy = StrategyResolver.load_strategy(self._freqtrade.config) - df_analyzed = strategy.analyze_ticker(_data, {'pair': pair}) + df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair}) return self._convert_dataframe_to_dict(pair, df_analyzed, arrow.Arrow.utcnow().datetime) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 91870aa6b..d084bd64a 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -818,6 +818,10 @@ def test_api_pair_candles(botclient, ohlcv_history): timeframe = '5m' amount = 2 ohlcv_history['sma'] = ohlcv_history['close'].rolling(2).mean() + ohlcv_history['buy'] = 0 + ohlcv_history.iloc[1]['buy'] = 1 + ohlcv_history['sell'] = 0 + ftbot.dataprovider._set_cached_df("XRP/BTC", timeframe, ohlcv_history) rc = client_get(client, @@ -825,7 +829,9 @@ def test_api_pair_candles(botclient, ohlcv_history): assert_response(rc) assert 'columns' in rc.json assert isinstance(rc.json['columns'], list) - assert rc.json['columns'] == ['date', 'open', 'high', 'low', 'close', 'volume', 'sma'] + assert rc.json['columns'] == ['date', 'open', 'high', + 'low', 'close', 'volume', 'sma', 'buy', 'sell', + '_buy_signal_open', '_sell_signal_open'] assert 'pair' in rc.json assert rc.json['pair'] == 'XRP/BTC' @@ -833,9 +839,10 @@ def test_api_pair_candles(botclient, ohlcv_history): assert len(rc.json['data']) == amount assert (rc.json['data'] == - [[1511686200000, 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869, None], + [[1511686200000, 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869, + None, 0, 0, None, None], [1511686500000, 8.88e-05, 8.942e-05, 8.88e-05, - 8.893e-05, 0.05874751, 8.886500000000001e-05] + 8.893e-05, 0.05874751, 8.886500000000001e-05, 0, 0, None, None] ]) From b93ad8840a2d053cae06b15f0b122456d35c2f7f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Jul 2020 15:20:50 +0200 Subject: [PATCH 0727/1197] Return date column unmodified --- freqtrade/rpc/api_server.py | 1 - freqtrade/rpc/rpc.py | 9 ++++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 081dcd623..b0620d6e2 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -548,7 +548,6 @@ class ApiServer(RPC): results = self._rpc_analysed_history_full(pair, timeframe, timerange) return self.rest_dump(results) - @require_login @rpc_catch_errors def _plot_config(self): diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index ef44aeee2..fa125721c 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -10,6 +10,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union import arrow from numpy import NAN, int64, mean +from pandas import DataFrame from freqtrade.configuration.timerange import TimeRange from freqtrade.constants import CANCEL_REASON @@ -656,8 +657,9 @@ class RPC: raise RPCException('Edge is not enabled.') return self._freqtrade.edge.accepted_pairs() - def _convert_dataframe_to_dict(self, pair, dataframe, last_analyzed): - dataframe['date'] = dataframe['date'].astype(int64) // 1000 // 1000 + def _convert_dataframe_to_dict(self, pair: str, dataframe: DataFrame, last_analyzed: datetime): + + dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].astype(int64) // 1000 // 1000 # Move open to seperate column when signal for easy plotting if 'buy' in dataframe.columns: buy_mask = (dataframe['buy'] == 1) @@ -673,13 +675,14 @@ class RPC: 'data': dataframe.values.tolist(), 'length': len(dataframe), 'last_analyzed': last_analyzed, + 'last_analyzed_ts': int(last_analyzed.timestamp()), } def _analysed_dataframe(self, pair: str, timeframe: str, limit: int) -> Dict[str, Any]: _data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe(pair, timeframe) if limit: - _data = _data.iloc[-limit:] + _data = _data.iloc[-limit:].copy() return self._convert_dataframe_to_dict(pair, _data, last_analyzed) def _rpc_analysed_history_full(self, pair: str, timeframe: str, From bf0e75e2a5e6bde4fec4f4598939944282cb479d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Jul 2020 21:06:10 +0200 Subject: [PATCH 0728/1197] Include data start and end date in dataframe api --- freqtrade/rpc/rpc.py | 4 ++++ tests/rpc/test_rpc_apiserver.py | 18 +++++++++++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index fa125721c..e66314230 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -676,6 +676,10 @@ class RPC: 'length': len(dataframe), 'last_analyzed': last_analyzed, 'last_analyzed_ts': int(last_analyzed.timestamp()), + 'data_start': str(dataframe.iloc[0]['date']), + 'data_start_ts': int(dataframe.iloc[0]['__date_ts']), + 'data_stop': str(dataframe.iloc[-1]['date']), + 'data_stop_ts': int(dataframe.iloc[-1]['__date_ts']), } def _analysed_dataframe(self, pair: str, timeframe: str, limit: int) -> Dict[str, Any]: diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index d084bd64a..a46caaee1 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -828,10 +828,18 @@ def test_api_pair_candles(botclient, ohlcv_history): f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}") assert_response(rc) assert 'columns' in rc.json + assert 'data_start_ts' in rc.json + assert 'data_start' in rc.json + assert 'data_stop' in rc.json + assert 'data_stop_ts' in rc.json + assert rc.json['data_start'] == '2017-11-26 08:50:00+00:00' + assert rc.json['data_start_ts'] == 1511686200000 + assert rc.json['data_stop'] == '2017-11-26 08:55:00+00:00' + assert rc.json['data_stop_ts'] == 1511686500000 assert isinstance(rc.json['columns'], list) assert rc.json['columns'] == ['date', 'open', 'high', 'low', 'close', 'volume', 'sma', 'buy', 'sell', - '_buy_signal_open', '_sell_signal_open'] + '__date_ts', '_buy_signal_open', '_sell_signal_open'] assert 'pair' in rc.json assert rc.json['pair'] == 'XRP/BTC' @@ -839,10 +847,10 @@ def test_api_pair_candles(botclient, ohlcv_history): assert len(rc.json['data']) == amount assert (rc.json['data'] == - [[1511686200000, 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869, - None, 0, 0, None, None], - [1511686500000, 8.88e-05, 8.942e-05, 8.88e-05, - 8.893e-05, 0.05874751, 8.886500000000001e-05, 0, 0, None, None] + [['2017-11-26 08:50:00', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869, + None, 0, 0, 1511686200000, None, None], + ['2017-11-26 08:55:00', 8.88e-05, 8.942e-05, 8.88e-05, + 8.893e-05, 0.05874751, 8.886500000000001e-05, 0, 0, 1511686500000, None, None] ]) From f227f6a755b093a3bd8d93deb8f8eca29083ac28 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Jul 2020 20:31:11 +0200 Subject: [PATCH 0729/1197] Use passed in config object to allow this to work in webserver mode --- freqtrade/rpc/api_server.py | 10 ++++++++-- freqtrade/rpc/rpc.py | 10 +++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index b0620d6e2..be8c01b8b 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -1,3 +1,4 @@ +from copy import deepcopy import logging import threading from datetime import date, datetime @@ -538,14 +539,19 @@ class ApiServer(RPC): parameters: - pair: Pair - timeframe: Timeframe to get data for (should be aligned to strategy.timeframe) + - strategy: Strategy to use - Must exist in configured strategy-path! - timerange: timerange in the format YYYYMMDD-YYYYMMDD (YYYYMMDD- or (-YYYYMMDD)) are als possible. If omitted uses all available data. """ pair = request.args.get("pair") timeframe = request.args.get("timeframe") timerange = request.args.get("timerange") - - results = self._rpc_analysed_history_full(pair, timeframe, timerange) + strategy = request.args.get("strategy") + config = deepcopy(self._config) + config.update({ + 'strategy': strategy, + }) + results = self._rpc_analysed_history_full(config, pair, timeframe, timerange) return self.rest_dump(results) @require_login diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e66314230..c40bface3 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -689,19 +689,19 @@ class RPC: _data = _data.iloc[-limit:].copy() return self._convert_dataframe_to_dict(pair, _data, last_analyzed) - def _rpc_analysed_history_full(self, pair: str, timeframe: str, + def _rpc_analysed_history_full(self, config: Dict[str, any], pair: str, timeframe: str, timerange: str) -> Dict[str, Any]: timerange = TimeRange.parse_timerange(timerange) _data = load_data( - datadir=self._freqtrade.config.get("datadir"), + datadir=config.get("datadir"), pairs=[pair], - timeframe=self._freqtrade.config.get('timeframe', '5m'), + timeframe=timeframe, timerange=timerange, - data_format=self._freqtrade.config.get('dataformat_ohlcv', 'json'), + data_format=config.get('dataformat_ohlcv', 'json'), ) from freqtrade.resolvers.strategy_resolver import StrategyResolver - strategy = StrategyResolver.load_strategy(self._freqtrade.config) + strategy = StrategyResolver.load_strategy(config) df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair}) return self._convert_dataframe_to_dict(pair, df_analyzed, arrow.Arrow.utcnow().datetime) From 18bbfdd34190154100b6acc28b53689f46c8b7e9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 31 Jul 2020 07:31:20 +0200 Subject: [PATCH 0730/1197] Add /strategies endpoint --- freqtrade/rpc/api_server.py | 19 +++++++++++++++++-- tests/rpc/test_rpc_apiserver.py | 9 +++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index be8c01b8b..8978117e9 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -1,8 +1,9 @@ -from copy import deepcopy import logging import threading +from copy import deepcopy from datetime import date, datetime from ipaddress import IPv4Address +from pathlib import Path from typing import Any, Callable, Dict from arrow import Arrow @@ -16,7 +17,7 @@ from werkzeug.security import safe_str_cmp from werkzeug.serving import make_server from freqtrade.__init__ import __version__ -from freqtrade.constants import DATETIME_PRINT_FORMAT +from freqtrade.constants import DATETIME_PRINT_FORMAT, USERPATH_STRATEGIES from freqtrade.persistence import Trade from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.rpc import RPC, RPCException @@ -219,6 +220,9 @@ class ApiServer(RPC): view_func=self._analysed_history, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/plot_config', 'plot_config', view_func=self._plot_config, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/strategies', 'strategies', + view_func=self._list_strategies, methods=['GET']) + # Combined actions and infos self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist, methods=['GET', 'POST']) @@ -561,3 +565,14 @@ class ApiServer(RPC): Handler for /plot_config. """ return self.rest_dump(self._rpc_plot_config()) + + @require_login + @rpc_catch_errors + def _list_strategies(self): + directory = Path(self._config.get( + 'strategy_path', self._config['user_data_dir'] / USERPATH_STRATEGIES)) + from freqtrade.resolvers.strategy_resolver import StrategyResolver + strategy_objs = StrategyResolver.search_all_objects(directory, False) + strategy_objs = sorted(strategy_objs, key=lambda x: x['name']) + + return self.rest_dump([x['name'] for x in strategy_objs]) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index a46caaee1..4bd65fc24 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -867,3 +867,12 @@ def test_api_plot_config(botclient): assert_response(rc) assert rc.json == ftbot.strategy.plot_config assert isinstance(rc.json['main_plot'], dict) + + +def test_api_strategies(botclient): + ftbot, client = botclient + + rc = client_get(client, f"{BASE_URI}/strategies") + + assert_response(rc) + assert rc.json == ['DefaultStrategy', 'TestStrategyLegacy'] From 32e6ea314c3cd01ce35fedb5329c57a9da621c55 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 31 Jul 2020 07:32:27 +0200 Subject: [PATCH 0731/1197] Return strategy with analyzed data --- freqtrade/rpc/rpc.py | 10 +++++++--- tests/rpc/test_rpc_apiserver.py | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c40bface3..df361cc4b 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -657,7 +657,8 @@ class RPC: raise RPCException('Edge is not enabled.') return self._freqtrade.edge.accepted_pairs() - def _convert_dataframe_to_dict(self, pair: str, dataframe: DataFrame, last_analyzed: datetime): + def _convert_dataframe_to_dict(self, strategy: str, pair: str, dataframe: DataFrame, + last_analyzed: datetime): dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].astype(int64) // 1000 // 1000 # Move open to seperate column when signal for easy plotting @@ -671,6 +672,7 @@ class RPC: return { 'pair': pair, + 'strategy': strategy, 'columns': list(dataframe.columns), 'data': dataframe.values.tolist(), 'length': len(dataframe), @@ -687,7 +689,8 @@ class RPC: _data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe(pair, timeframe) if limit: _data = _data.iloc[-limit:].copy() - return self._convert_dataframe_to_dict(pair, _data, last_analyzed) + return self._convert_dataframe_to_dict(self._freqtrade.config['strategy'], + pair, _data, last_analyzed) def _rpc_analysed_history_full(self, config: Dict[str, any], pair: str, timeframe: str, timerange: str) -> Dict[str, Any]: @@ -704,7 +707,8 @@ class RPC: strategy = StrategyResolver.load_strategy(config) df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair}) - return self._convert_dataframe_to_dict(pair, df_analyzed, arrow.Arrow.utcnow().datetime) + return self._convert_dataframe_to_dict(strategy.get_strategy_name(), pair, df_analyzed, + arrow.Arrow.utcnow().datetime) def _rpc_plot_config(self) -> Dict[str, Any]: diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 4bd65fc24..fc46d0a7f 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -827,6 +827,8 @@ def test_api_pair_candles(botclient, ohlcv_history): rc = client_get(client, f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}") assert_response(rc) + assert 'strategy' in rc.json + assert rc.json['strategy'] == 'DefaultStrategy' assert 'columns' in rc.json assert 'data_start_ts' in rc.json assert 'data_start' in rc.json From 6a59740f8394962f95603df5ee7231875b92eafb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Aug 2020 17:12:32 +0200 Subject: [PATCH 0732/1197] Strategies should be a nested object --- freqtrade/rpc/api_server.py | 6 +++++- tests/rpc/test_rpc_apiserver.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 8978117e9..61081157b 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -551,6 +551,10 @@ class ApiServer(RPC): timeframe = request.args.get("timeframe") timerange = request.args.get("timerange") strategy = request.args.get("strategy") + + if not pair or not timeframe or not timerange or not strategy: + return self.rest_error("Mandatory parameter missing.") + config = deepcopy(self._config) config.update({ 'strategy': strategy, @@ -575,4 +579,4 @@ class ApiServer(RPC): strategy_objs = StrategyResolver.search_all_objects(directory, False) strategy_objs = sorted(strategy_objs, key=lambda x: x['name']) - return self.rest_dump([x['name'] for x in strategy_objs]) + return self.rest_dump({'strategies': [x['name'] for x in strategy_objs]}) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index fc46d0a7f..fbee1cf64 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -877,4 +877,4 @@ def test_api_strategies(botclient): rc = client_get(client, f"{BASE_URI}/strategies") assert_response(rc) - assert rc.json == ['DefaultStrategy', 'TestStrategyLegacy'] + assert rc.json == {'strategies': ['DefaultStrategy', 'TestStrategyLegacy']} From 1de248fe38af6bc2c4a88183bee53fc48bcce0ef Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Aug 2020 17:49:59 +0200 Subject: [PATCH 0733/1197] add list_available_pairs endpoint --- freqtrade/rpc/api_server.py | 39 ++++++++++++++++++++++++++++++++- tests/conftest.py | 1 + tests/rpc/test_rpc_apiserver.py | 26 ++++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 61081157b..51610bd98 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -222,6 +222,8 @@ class ApiServer(RPC): view_func=self._plot_config, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/strategies', 'strategies', view_func=self._list_strategies, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/available_pairs', 'pairs', + view_func=self._list_available_pairs, methods=['GET']) # Combined actions and infos self.app.add_url_rule(f'{BASE_URI}/blacklist', 'blacklist', view_func=self._blacklist, @@ -540,7 +542,7 @@ class ApiServer(RPC): Returns the dataframe of a given timerange Takes the following get arguments: get: - parameters: + parameters: - pair: Pair - timeframe: Timeframe to get data for (should be aligned to strategy.timeframe) - strategy: Strategy to use - Must exist in configured strategy-path! @@ -580,3 +582,38 @@ class ApiServer(RPC): strategy_objs = sorted(strategy_objs, key=lambda x: x['name']) return self.rest_dump({'strategies': [x['name'] for x in strategy_objs]}) + + @require_login + @rpc_catch_errors + def _list_available_pairs(self): + """ + Handler for /available_pairs. + Returns an object, with pairs, available pair length and pair_interval combinations + Takes the following get arguments: + get: + parameters: + - stake_currency: Filter on this stake currency + - timeframe: Timeframe to get data for Filter elements to this timeframe + """ + timeframe = request.args.get("timeframe") + stake_currency = request.args.get("stake_currency") + + from freqtrade.data.history import get_datahandler + dh = get_datahandler(self._config['datadir'], self._config.get('dataformat_ohlcv', None)) + + pair_interval = dh.ohlcv_get_available_data(self._config['datadir']) + + if timeframe: + pair_interval = [pair for pair in pair_interval if pair[1] == timeframe] + if stake_currency: + pair_interval = [pair for pair in pair_interval if pair[0].endswith(stake_currency)] + pair_interval = sorted(pair_interval, key=lambda x: x[0]) + + pairs = list({x[0] for x in pair_interval}) + + result = { + 'length': len(pairs), + 'pairs': pairs, + 'pair_interval': pair_interval, + } + return self.rest_dump(result) diff --git a/tests/conftest.py b/tests/conftest.py index dbd0df8f6..2153fd327 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -146,6 +146,7 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: :return: FreqtradeBot """ patch_freqtradebot(mocker, config) + config['datadir'] = Path(config['datadir']) return FreqtradeBot(config) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index fbee1cf64..d6c938373 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -878,3 +878,29 @@ def test_api_strategies(botclient): assert_response(rc) assert rc.json == {'strategies': ['DefaultStrategy', 'TestStrategyLegacy']} + + +def test_list_available_pairs(botclient): + ftbot, client = botclient + + rc = client_get(client, f"{BASE_URI}/available_pairs") + + assert_response(rc) + assert rc.json['length'] == 12 + assert isinstance(rc.json['pairs'], list) + + rc = client_get(client, f"{BASE_URI}/available_pairs?timeframe=5m") + assert_response(rc) + assert rc.json['length'] == 12 + + rc = client_get(client, f"{BASE_URI}/available_pairs?stake_currency=ETH") + assert_response(rc) + assert rc.json['length'] == 1 + assert rc.json['pairs'] == ['XRP/ETH'] + assert len(rc.json['pair_interval']) == 2 + + rc = client_get(client, f"{BASE_URI}/available_pairs?stake_currency=ETH&timeframe=5m") + assert_response(rc) + assert rc.json['length'] == 1 + assert rc.json['pairs'] == ['XRP/ETH'] + assert len(rc.json['pair_interval']) == 1 From c59a1be154acc35851f11c6c6e6c655052182dfa Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 1 Aug 2020 17:55:03 +0200 Subject: [PATCH 0734/1197] show_config should not use freqtrade object --- freqtrade/rpc/api_server.py | 2 +- freqtrade/rpc/rpc.py | 5 ++--- freqtrade/rpc/telegram.py | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 51610bd98..e2189b8e9 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -328,7 +328,7 @@ class ApiServer(RPC): """ Prints the bot's version """ - return self.rest_dump(self._rpc_show_config()) + return self.rest_dump(self._rpc_show_config(self._config)) @require_login @rpc_catch_errors diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index df361cc4b..d3df9f4bd 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -93,13 +93,12 @@ class RPC: def send_msg(self, msg: Dict[str, str]) -> None: """ Sends a message to all registered rpc modules """ - def _rpc_show_config(self) -> Dict[str, Any]: + def _rpc_show_config(self, config) -> Dict[str, Any]: """ Return a dict of config options. Explicitly does NOT return the full config to avoid leakage of sensitive information via rpc. """ - config = self._freqtrade.config val = { 'dry_run': config['dry_run'], 'stake_currency': config['stake_currency'], @@ -120,7 +119,7 @@ class RPC: 'forcebuy_enabled': config.get('forcebuy_enable', False), 'ask_strategy': config.get('ask_strategy', {}), 'bid_strategy': config.get('bid_strategy', {}), - 'state': str(self._freqtrade.state) + 'state': str(self._freqtrade.state) if self._freqtrade else '', } return val diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 01d21c53c..7a6607632 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -755,7 +755,7 @@ class Telegram(RPC): :param update: message update :return: None """ - val = self._rpc_show_config() + val = self._rpc_show_config(self._freqtrade.config) if val['trailing_stop']: sl_info = ( f"*Initial Stoploss:* `{val['stoploss']}`\n" From c0654f3caf2b07fcff4a9bd51b6ba4462af43771 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 24 Aug 2020 20:38:01 +0200 Subject: [PATCH 0735/1197] Add resiliancy against not having a analyzed dataframe yet --- freqtrade/rpc/rpc.py | 46 +++++++++++++++++++++------------ tests/rpc/test_rpc_apiserver.py | 10 +++++++ 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index d3df9f4bd..94a4bbed3 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -657,19 +657,21 @@ class RPC: return self._freqtrade.edge.accepted_pairs() def _convert_dataframe_to_dict(self, strategy: str, pair: str, dataframe: DataFrame, - last_analyzed: datetime): + last_analyzed: datetime) -> Dict[str, Any]: + has_content = len(dataframe) != 0 + if has_content: - dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].astype(int64) // 1000 // 1000 - # Move open to seperate column when signal for easy plotting - if 'buy' in dataframe.columns: - buy_mask = (dataframe['buy'] == 1) - dataframe.loc[buy_mask, '_buy_signal_open'] = dataframe.loc[buy_mask, 'open'] - if 'sell' in dataframe.columns: - sell_mask = (dataframe['sell'] == 1) - dataframe.loc[sell_mask, '_sell_signal_open'] = dataframe.loc[sell_mask, 'open'] - dataframe = dataframe.replace({NAN: None}) + dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].astype(int64) // 1000 // 1000 + # Move open to seperate column when signal for easy plotting + if 'buy' in dataframe.columns: + buy_mask = (dataframe['buy'] == 1) + dataframe.loc[buy_mask, '_buy_signal_open'] = dataframe.loc[buy_mask, 'open'] + if 'sell' in dataframe.columns: + sell_mask = (dataframe['sell'] == 1) + dataframe.loc[sell_mask, '_sell_signal_open'] = dataframe.loc[sell_mask, 'open'] + dataframe = dataframe.replace({NAN: None}) - return { + res = { 'pair': pair, 'strategy': strategy, 'columns': list(dataframe.columns), @@ -677,17 +679,27 @@ class RPC: 'length': len(dataframe), 'last_analyzed': last_analyzed, 'last_analyzed_ts': int(last_analyzed.timestamp()), - 'data_start': str(dataframe.iloc[0]['date']), - 'data_start_ts': int(dataframe.iloc[0]['__date_ts']), - 'data_stop': str(dataframe.iloc[-1]['date']), - 'data_stop_ts': int(dataframe.iloc[-1]['__date_ts']), + 'data_start': '', + 'data_start_ts': 0, + 'data_stop': '', + 'data_stop_ts': 0, } + if has_content: + res.update({ + 'data_start': str(dataframe.iloc[0]['date']), + 'data_start_ts': int(dataframe.iloc[0]['__date_ts']), + 'data_stop': str(dataframe.iloc[-1]['date']), + 'data_stop_ts': int(dataframe.iloc[-1]['__date_ts']), + }) + return res def _analysed_dataframe(self, pair: str, timeframe: str, limit: int) -> Dict[str, Any]: - _data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe(pair, timeframe) + _data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe( + pair, timeframe) + _data = _data.copy() if limit: - _data = _data.iloc[-limit:].copy() + _data = _data.iloc[-limit:] return self._convert_dataframe_to_dict(self._freqtrade.config['strategy'], pair, _data, last_analyzed) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index d6c938373..b98483061 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -817,6 +817,16 @@ def test_api_pair_candles(botclient, ohlcv_history): ftbot, client = botclient timeframe = '5m' amount = 2 + + rc = client_get(client, + f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}") + assert_response(rc) + assert 'columns' in rc.json + assert 'data_start_ts' in rc.json + assert 'data_start' in rc.json + assert 'data_stop' in rc.json + assert 'data_stop_ts' in rc.json + assert len(rc.json['data']) == 0 ohlcv_history['sma'] = ohlcv_history['close'].rolling(2).mean() ohlcv_history['buy'] = 0 ohlcv_history.iloc[1]['buy'] = 1 From bb4993dc20064c9368790d753fce3dc690b6d0b6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Sep 2020 20:15:54 +0200 Subject: [PATCH 0736/1197] Add new endpoints to the documentation --- docs/rest-api.md | 7 ++++++- freqtrade/rpc/api_server.py | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 075bd7e64..fc628ac71 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -104,7 +104,7 @@ By default, the script assumes `127.0.0.1` (localhost) and port `8080` to be use python3 scripts/rest_client.py --config rest_config.json [optional parameters] ``` -## Available commands +## Available endpoints | Command | Description | |----------|-------------| @@ -129,6 +129,11 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `whitelist` | Show the current whitelist | `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. | `edge` | Show validated pairs by Edge if it is enabled. +| `pair_candles` | Returns dataframe for a pair / timeframe combination while the bot is running. +| `pair_history` | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy. +| `plot_config` | Get plot config from the strategy (or nothing if not configured). +| `strategies` | List strategies in strategy directory. +| `available_pairs` | List available backtest data. | `version` | Show version Possible commands can be listed from the rest-client script using the `help` command. diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index e2189b8e9..4d1c5731c 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -214,6 +214,7 @@ class ApiServer(RPC): view_func=self._trades, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/trades/', 'trades_delete', view_func=self._trades_delete, methods=['DELETE']) + self.app.add_url_rule(f'{BASE_URI}/pair_candles', 'pair_candles', view_func=self._analysed_candles, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/pair_history', 'pair_history', @@ -518,7 +519,7 @@ class ApiServer(RPC): @rpc_catch_errors def _analysed_candles(self): """ - Handler for /pair_history. + Handler for /pair_candles. Returns the dataframe the bot is using during live/dry operations. Takes the following get arguments: get: From f82d39e1b05dcc3a7549293118be5173b725127f Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Sep 2020 20:31:02 +0200 Subject: [PATCH 0737/1197] Enhance restclient and add tests for new api methods --- docs/rest-api.md | 28 ++++++++++++++++- freqtrade/rpc/api_server.py | 8 +++-- scripts/rest_client.py | 56 +++++++++++++++++++++++++++++++++ tests/rpc/test_rpc_apiserver.py | 10 ++++++ 4 files changed, 98 insertions(+), 4 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index fc628ac71..73a7b9c89 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -145,6 +145,12 @@ python3 scripts/rest_client.py help ``` output Possible commands: +available_pairs + Return available pair (backtest data) based on timeframe / stake_currency selection + + :param timeframe: Only pairs with this timeframe available. + :param stake_currency: Only pairs that include this timeframe + balance Get the account balance. @@ -184,9 +190,27 @@ logs :param limit: Limits log messages to the last logs. No limit to get all the trades. +pair_candles + Return live dataframe for . + + :param pair: Pair to get data for + :param timeframe: Only pairs with this timeframe available. + :param limit: Limit result to the last n candles. + +pair_history + Return historic, analyzed dataframe + + :param pair: Pair to get data for + :param timeframe: Only pairs with this timeframe available. + :param strategy: Strategy to analyze and get values for + :param timerange: Timerange to get data for (same format than --timerange endpoints) + performance Return the performance of the different coins. +plot_config + Return plot configuration if the strategy defines one. + profit Return the profit summary. @@ -209,6 +233,9 @@ stop stopbuy Stop buying (but handle sells gracefully). Use `reload_config` to reset. +strategies + Lists available strategies + trades Return trades history. @@ -220,7 +247,6 @@ version whitelist Show the current whitelist. - ``` ## Advanced API usage using JWT tokens diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 4d1c5731c..0b80283d9 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -166,8 +166,8 @@ class ApiServer(RPC): """ Helper function to jsonify object for a webserver """ return jsonify(return_value) - def rest_error(self, error_msg): - return jsonify({"error": error_msg}), 502 + def rest_error(self, error_msg, error_code=502): + return jsonify({"error": error_msg}), error_code def register_rest_rpc_urls(self): """ @@ -531,6 +531,8 @@ class ApiServer(RPC): pair = request.args.get("pair") timeframe = request.args.get("timeframe") limit = request.args.get("limit", type=int) + if not pair or not timeframe: + return self.rest_error("Mandatory parameter missing.", 400) results = self._analysed_dataframe(pair, timeframe, limit) return self.rest_dump(results) @@ -556,7 +558,7 @@ class ApiServer(RPC): strategy = request.args.get("strategy") if not pair or not timeframe or not timerange or not strategy: - return self.rest_error("Mandatory parameter missing.") + return self.rest_error("Mandatory parameter missing.", 400) config = deepcopy(self._config) config.update({ diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 8512777df..8889b82c1 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -224,6 +224,62 @@ class FtRestClient(): return self._post("forcesell", data={"tradeid": tradeid}) + def strategies(self): + """Lists available strategies + + :return: json object + """ + return self._get("strategies") + + def plot_config(self): + """Return plot configuration if the strategy defines one. + + :return: json object + """ + return self._get("plot_config") + + def available_pairs(self, timeframe=None, stake_currency=None): + """Return available pair (backtest data) based on timeframe / stake_currency selection + + :param timeframe: Only pairs with this timeframe available. + :param stake_currency: Only pairs that include this timeframe + :return: json object + """ + return self._get("available_pairs", params={ + "stake_currency": stake_currency if timeframe else '', + "timeframe": timeframe if timeframe else '', + }) + + def pair_candles(self, pair, timeframe, limit=None): + """Return live dataframe for . + + :param pair: Pair to get data for + :param timeframe: Only pairs with this timeframe available. + :param limit: Limit result to the last n candles. + :return: json object + """ + return self._get("available_pairs", params={ + "pair": pair, + "timeframe": timeframe, + "limit": limit, + }) + + def pair_history(self, pair, timeframe, strategy, timerange=None): + """Return historic, analyzed dataframe + + :param pair: Pair to get data for + :param timeframe: Only pairs with this timeframe available. + :param strategy: Strategy to analyze and get values for + :param timerange: Timerange to get data for (same format than --timerange endpoints) + :return: json object + """ + return self._get("pair_history", params={ + "pair": pair, + "timeframe": timeframe, + "strategy": strategy, + "timerange": timerange if timerange else '', + }) + def add_arguments(): parser = argparse.ArgumentParser() diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index b98483061..47afc61a1 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -818,6 +818,16 @@ def test_api_pair_candles(botclient, ohlcv_history): timeframe = '5m' amount = 2 + # No pair + rc = client_get(client, + f"{BASE_URI}/pair_candles?limit={amount}&timeframe={timeframe}") + assert_response(rc, 400) + + # No timeframe + rc = client_get(client, + f"{BASE_URI}/pair_candles?pair=XRP%2FBTC") + assert_response(rc, 400) + rc = client_get(client, f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}") assert_response(rc) From 816c8295f1ba20622fce1e686bf480165c4b78ac Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 11 Sep 2020 20:45:59 +0200 Subject: [PATCH 0738/1197] Add test for pair_history --- tests/rpc/test_rpc_apiserver.py | 46 +++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 47afc61a1..2972324ba 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -876,6 +876,52 @@ def test_api_pair_candles(botclient, ohlcv_history): ]) +def test_api_pair_history(botclient, ohlcv_history): + ftbot, client = botclient + timeframe = '5m' + + # No pair + rc = client_get(client, + f"{BASE_URI}/pair_history?timeframe={timeframe}" + "&timerange=20180111-20180112&strategy=DefaultStrategy") + assert_response(rc, 400) + + # No Timeframe + rc = client_get(client, + f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC" + "&timerange=20180111-20180112&strategy=DefaultStrategy") + assert_response(rc, 400) + + # No timerange + rc = client_get(client, + f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" + "&strategy=DefaultStrategy") + assert_response(rc, 400) + + # No strategy + rc = client_get(client, + f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" + "&timerange=20180111-20180112") + assert_response(rc, 400) + + # Working + rc = client_get(client, + f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" + "&timerange=20180111-20180112&strategy=DefaultStrategy") + assert_response(rc, 200) + assert rc.json['length'] == 289 + assert len(rc.json['data']) == rc.json['length'] + assert 'columns' in rc.json + assert 'data' in rc.json + assert rc.json['pair'] == 'UNITTEST/BTC' + assert rc.json['strategy'] == 'DefaultStrategy' + assert rc.json['data_start'] == '2018-01-11 00:00:00+00:00' + assert rc.json['data_start_ts'] == 1515628800000 + assert rc.json['data_stop'] == '2018-01-12 00:00:00+00:00' + assert rc.json['data_stop_ts'] == 1515715200000 + + + def test_api_plot_config(botclient): ftbot, client = botclient From a3d0889dab7314b902507f68782fba37cf7c9845 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Sep 2020 20:09:54 +0200 Subject: [PATCH 0739/1197] Add alpha to endpoint documentation --- docs/rest-api.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 73a7b9c89..b832b8b0c 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -129,13 +129,16 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `whitelist` | Show the current whitelist | `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. | `edge` | Show validated pairs by Edge if it is enabled. -| `pair_candles` | Returns dataframe for a pair / timeframe combination while the bot is running. -| `pair_history` | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy. -| `plot_config` | Get plot config from the strategy (or nothing if not configured). -| `strategies` | List strategies in strategy directory. -| `available_pairs` | List available backtest data. +| `pair_candles` | Returns dataframe for a pair / timeframe combination while the bot is running. **Alpha** +| `pair_history` | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy. **Alpha** +| `plot_config` | Get plot config from the strategy (or nothing if not configured). **Alpha** +| `strategies` | List strategies in strategy directory. **Alpha** +| `available_pairs` | List available backtest data. **Alpha** | `version` | Show version +!!! Warning "Alpha status" + Endpoints labeled with *Alpha status* above may change at any time without notice. + Possible commands can be listed from the rest-client script using the `help` command. ``` bash From 4b6b7f83433bc41265b038c5525a3865108eb5d9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Sep 2020 07:59:47 +0200 Subject: [PATCH 0740/1197] Add timeframe to candle return values --- freqtrade/rpc/rpc.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 94a4bbed3..db1343455 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -656,8 +656,8 @@ class RPC: raise RPCException('Edge is not enabled.') return self._freqtrade.edge.accepted_pairs() - def _convert_dataframe_to_dict(self, strategy: str, pair: str, dataframe: DataFrame, - last_analyzed: datetime) -> Dict[str, Any]: + def _convert_dataframe_to_dict(self, strategy: str, pair: str, timeframe: str, + dataframe: DataFrame, last_analyzed: datetime) -> Dict[str, Any]: has_content = len(dataframe) != 0 if has_content: @@ -673,6 +673,8 @@ class RPC: res = { 'pair': pair, + 'timeframe': timeframe, + 'timeframe_ms': timeframe_to_msecs(timeframe), 'strategy': strategy, 'columns': list(dataframe.columns), 'data': dataframe.values.tolist(), @@ -701,7 +703,7 @@ class RPC: if limit: _data = _data.iloc[-limit:] return self._convert_dataframe_to_dict(self._freqtrade.config['strategy'], - pair, _data, last_analyzed) + pair, timeframe, _data, last_analyzed) def _rpc_analysed_history_full(self, config: Dict[str, any], pair: str, timeframe: str, timerange: str) -> Dict[str, Any]: @@ -718,8 +720,8 @@ class RPC: strategy = StrategyResolver.load_strategy(config) df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair}) - return self._convert_dataframe_to_dict(strategy.get_strategy_name(), pair, df_analyzed, - arrow.Arrow.utcnow().datetime) + return self._convert_dataframe_to_dict(strategy.get_strategy_name(), pair, timeframe, + df_analyzed, arrow.Arrow.utcnow().datetime) def _rpc_plot_config(self) -> Dict[str, Any]: From ba10bd77565e8f86e0a0525ffbbcdd3bfadc98b3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 17 Sep 2020 06:56:51 +0200 Subject: [PATCH 0741/1197] Add strategy code to __code__ --- freqtrade/resolvers/iresolver.py | 22 ++++++++++++---------- freqtrade/rpc/api_server.py | 20 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 846c85a5c..83c614650 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -67,9 +67,10 @@ class IResolver: return iter([None]) valid_objects_gen = ( - obj for name, obj in inspect.getmembers(module, inspect.isclass) - if ((object_name is None or object_name == name) and - issubclass(obj, cls.object_type) and obj is not cls.object_type) + (obj, inspect.getsource(module)) for name, obj in inspect.getmembers( + module, inspect.isclass) if ((object_name is None or object_name == name) + and issubclass(obj, cls.object_type) + and obj is not cls.object_type) ) return valid_objects_gen @@ -93,7 +94,8 @@ class IResolver: obj = next(cls._get_valid_object(module_path, object_name), None) if obj: - return (obj, module_path) + obj[0].__code__ = obj[1] + return (obj[0], module_path) return (None, None) @classmethod @@ -133,10 +135,10 @@ class IResolver: user_subdir=cls.user_subdir, extra_dir=extra_dir) - pairlist = cls._load_object(paths=abs_paths, object_name=object_name, - kwargs=kwargs) - if pairlist: - return pairlist + found_object = cls._load_object(paths=abs_paths, object_name=object_name, + kwargs=kwargs) + if found_object: + return found_object raise OperationalException( f"Impossible to load {cls.object_type_str} '{object_name}'. This class does not exist " "or contains Python code errors." @@ -164,8 +166,8 @@ class IResolver: for obj in cls._get_valid_object(module_path, object_name=None, enum_failed=enum_failed): objects.append( - {'name': obj.__name__ if obj is not None else '', - 'class': obj, + {'name': obj[0].__name__ if obj is not None else '', + 'class': obj[0] if obj is not None else None, 'location': entry, }) return objects diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 0b80283d9..21c0e5629 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -223,6 +223,8 @@ class ApiServer(RPC): view_func=self._plot_config, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/strategies', 'strategies', view_func=self._list_strategies, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/strategy/', 'strategy', + view_func=self._get_strategy, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/available_pairs', 'pairs', view_func=self._list_available_pairs, methods=['GET']) @@ -586,6 +588,24 @@ class ApiServer(RPC): return self.rest_dump({'strategies': [x['name'] for x in strategy_objs]}) + @require_login + @rpc_catch_errors + def _get_strategy(self, strategy: str): + """ + Get a single strategy + get: + parameters: + - strategy: Only get this strategy + """ + config = deepcopy(self._config) + from freqtrade.resolvers.strategy_resolver import StrategyResolver + strategy_obj = StrategyResolver._load_strategy(strategy, config, None) + + return self.rest_dump({ + 'strategy': strategy_obj.get_strategy_name(), + 'code': strategy_obj.__code__, + }) + @require_login @rpc_catch_errors def _list_available_pairs(self): From becccca3d15fccca3a8f1554b2a7450bcdaa5440 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 17 Sep 2020 07:38:56 +0200 Subject: [PATCH 0742/1197] Add test for __code__ loading --- freqtrade/resolvers/iresolver.py | 17 +++++++++++------ freqtrade/resolvers/strategy_resolver.py | 4 +++- tests/rpc/test_rpc_apiserver.py | 1 - tests/strategy/test_strategy.py | 9 +++++++-- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 83c614650..801ad956b 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -51,7 +51,8 @@ class IResolver: :param object_name: Class name of the object :param enum_failed: If True, will return None for modules which fail. Otherwise, failing modules are skipped. - :return: generator containing matching objects + :return: generator containing tuple of matching objects + Tuple format: [Object, source] """ # Generate spec based on absolute path @@ -67,7 +68,8 @@ class IResolver: return iter([None]) valid_objects_gen = ( - (obj, inspect.getsource(module)) for name, obj in inspect.getmembers( + (obj, inspect.getsource(module)) for + name, obj in inspect.getmembers( module, inspect.isclass) if ((object_name is None or object_name == name) and issubclass(obj, cls.object_type) and obj is not cls.object_type) @@ -75,7 +77,7 @@ class IResolver: return valid_objects_gen @classmethod - def _search_object(cls, directory: Path, object_name: str + def _search_object(cls, directory: Path, object_name: str, add_source: bool = False ) -> Union[Tuple[Any, Path], Tuple[None, None]]: """ Search for the objectname in the given directory @@ -94,12 +96,14 @@ class IResolver: obj = next(cls._get_valid_object(module_path, object_name), None) if obj: - obj[0].__code__ = obj[1] + obj[0].__file__ = str(entry) + if add_source: + obj[0].__code__ = obj[1] return (obj[0], module_path) return (None, None) @classmethod - def _load_object(cls, paths: List[Path], object_name: str, + def _load_object(cls, paths: List[Path], object_name: str, add_source: bool = False, kwargs: dict = {}) -> Optional[Any]: """ Try to load object from path list. @@ -108,7 +112,8 @@ class IResolver: for _path in paths: try: (module, module_path) = cls._search_object(directory=_path, - object_name=object_name) + object_name=object_name, + add_source=add_source) if module: logger.info( f"Using resolved {cls.object_type.__name__.lower()[1:]} {object_name} " diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index ead7424ec..63a3f784e 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -174,7 +174,9 @@ class StrategyResolver(IResolver): strategy = StrategyResolver._load_object(paths=abs_paths, object_name=strategy_name, - kwargs={'config': config}) + add_source=True, + kwargs={'config': config}, + ) if strategy: strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 2972324ba..8e1d09b95 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -921,7 +921,6 @@ def test_api_pair_history(botclient, ohlcv_history): assert rc.json['data_stop_ts'] == 1515715200000 - def test_api_plot_config(botclient): ftbot, client = botclient diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py index 240f3d8ec..1641098e9 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -18,13 +18,15 @@ def test_search_strategy(): s, _ = StrategyResolver._search_object( directory=default_location, - object_name='DefaultStrategy' + object_name='DefaultStrategy', + add_source=True, ) assert issubclass(s, IStrategy) s, _ = StrategyResolver._search_object( directory=default_location, - object_name='NotFoundStrategy' + object_name='NotFoundStrategy', + add_source=True, ) assert s is None @@ -53,6 +55,9 @@ def test_load_strategy(default_conf, result): 'strategy_path': str(Path(__file__).parents[2] / 'freqtrade/templates') }) strategy = StrategyResolver.load_strategy(default_conf) + assert isinstance(strategy.__code__, str) + assert 'class SampleStrategy' in strategy.__code__ + assert isinstance(strategy.__file__, str) assert 'rsi' in strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) From b38f68b3b09a1e969e7514b74a59f44c6b81f252 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 17 Sep 2020 07:53:22 +0200 Subject: [PATCH 0743/1197] Add 404 when strategy is not found --- docs/rest-api.md | 6 ++++++ freqtrade/rpc/api_server.py | 7 ++++++- scripts/rest_client.py | 8 ++++++++ tests/rpc/test_rpc_apiserver.py | 16 ++++++++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index b832b8b0c..44f0b07cf 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -133,6 +133,7 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `pair_history` | Returns an analyzed dataframe for a given timerange, analyzed by a given strategy. **Alpha** | `plot_config` | Get plot config from the strategy (or nothing if not configured). **Alpha** | `strategies` | List strategies in strategy directory. **Alpha** +| `strategy ` | Get specific Strategy content. **Alpha** | `available_pairs` | List available backtest data. **Alpha** | `version` | Show version @@ -239,6 +240,11 @@ stopbuy strategies Lists available strategies +strategy + Get strategy details + + :param strategy: Strategy class name + trades Return trades history. diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 21c0e5629..489b8760a 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -1,3 +1,4 @@ +from freqtrade.exceptions import OperationalException import logging import threading from copy import deepcopy @@ -599,7 +600,11 @@ class ApiServer(RPC): """ config = deepcopy(self._config) from freqtrade.resolvers.strategy_resolver import StrategyResolver - strategy_obj = StrategyResolver._load_strategy(strategy, config, None) + try: + strategy_obj = StrategyResolver._load_strategy(strategy, config, + extra_dir=config.get('strategy_path')) + except OperationalException: + return self.rest_error("Strategy not found.", 404) return self.rest_dump({ 'strategy': strategy_obj.get_strategy_name(), diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 8889b82c1..46966d447 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -231,6 +231,14 @@ class FtRestClient(): """ return self._get("strategies") + def strategy(self, strategy): + """Get strategy details + + :param strategy: Strategy class name + :return: json object + """ + return self._get(f"strategy/{strategy}") + def plot_config(self): """Return plot configuration if the strategy defines one. diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 8e1d09b95..4cc947159 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -3,6 +3,7 @@ Unit test file for rpc/api_server.py """ from datetime import datetime +from pathlib import Path from unittest.mock import ANY, MagicMock, PropertyMock import pytest @@ -945,6 +946,21 @@ def test_api_strategies(botclient): assert rc.json == {'strategies': ['DefaultStrategy', 'TestStrategyLegacy']} +def test_api_strategy(botclient): + ftbot, client = botclient + + rc = client_get(client, f"{BASE_URI}/strategy/DefaultStrategy") + + assert_response(rc) + assert rc.json['strategy'] == 'DefaultStrategy' + + data = (Path(__file__).parents[1] / "strategy/strats/default_strategy.py").read_text() + assert rc.json['code'] == data + + rc = client_get(client, f"{BASE_URI}/strategy/NoStrat") + assert_response(rc, 404) + + def test_list_available_pairs(botclient): ftbot, client = botclient From 350fcc071e07952e50a518f27b79de6b387cab19 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Sep 2020 16:33:48 +0200 Subject: [PATCH 0744/1197] Don't use __code__ __code__ is a special method name used by python already source: https://docs.python.org/3/reference/datamodel.html#special-method-names --- freqtrade/resolvers/iresolver.py | 2 +- freqtrade/rpc/api_server.py | 2 +- tests/strategy/test_strategy.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 801ad956b..9c3a33f2b 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -98,7 +98,7 @@ class IResolver: if obj: obj[0].__file__ = str(entry) if add_source: - obj[0].__code__ = obj[1] + obj[0].__source__ = obj[1] return (obj[0], module_path) return (None, None) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 489b8760a..ea616c0ec 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -608,7 +608,7 @@ class ApiServer(RPC): return self.rest_dump({ 'strategy': strategy_obj.get_strategy_name(), - 'code': strategy_obj.__code__, + 'code': strategy_obj.__source__, }) @require_login diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py index 1641098e9..1c692d2da 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -55,8 +55,8 @@ def test_load_strategy(default_conf, result): 'strategy_path': str(Path(__file__).parents[2] / 'freqtrade/templates') }) strategy = StrategyResolver.load_strategy(default_conf) - assert isinstance(strategy.__code__, str) - assert 'class SampleStrategy' in strategy.__code__ + assert isinstance(strategy.__source__, str) + assert 'class SampleStrategy' in strategy.__source__ assert isinstance(strategy.__file__, str) assert 'rsi' in strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) From 62110dc2fcb4c9a76c96579249eda5a10ab07e48 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Sep 2020 09:24:39 +0200 Subject: [PATCH 0745/1197] Add buy / sell signal count to dataframe interface --- freqtrade/rpc/rpc.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index db1343455..db44ecda4 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -659,15 +659,19 @@ class RPC: def _convert_dataframe_to_dict(self, strategy: str, pair: str, timeframe: str, dataframe: DataFrame, last_analyzed: datetime) -> Dict[str, Any]: has_content = len(dataframe) != 0 + buy_signals = 0 + sell_signals = 0 if has_content: dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].astype(int64) // 1000 // 1000 # Move open to seperate column when signal for easy plotting if 'buy' in dataframe.columns: buy_mask = (dataframe['buy'] == 1) + buy_signals = int(buy_mask.sum()) dataframe.loc[buy_mask, '_buy_signal_open'] = dataframe.loc[buy_mask, 'open'] if 'sell' in dataframe.columns: sell_mask = (dataframe['sell'] == 1) + sell_signals = int(sell_mask.sum()) dataframe.loc[sell_mask, '_sell_signal_open'] = dataframe.loc[sell_mask, 'open'] dataframe = dataframe.replace({NAN: None}) @@ -679,6 +683,8 @@ class RPC: 'columns': list(dataframe.columns), 'data': dataframe.values.tolist(), 'length': len(dataframe), + 'buy_signals': buy_signals, + 'sell_signals': sell_signals, 'last_analyzed': last_analyzed, 'last_analyzed_ts': int(last_analyzed.timestamp()), 'data_start': '', From 66b77d2f53cddb752737ae7604d99f9336abd3f5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 2 Oct 2020 06:41:28 +0200 Subject: [PATCH 0746/1197] Fix some types --- freqtrade/rpc/rpc.py | 6 +++--- freqtrade/strategy/interface.py | 2 ++ tests/rpc/test_rpc_apiserver.py | 4 ++-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index db44ecda4..effe4725e 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -711,15 +711,15 @@ class RPC: return self._convert_dataframe_to_dict(self._freqtrade.config['strategy'], pair, timeframe, _data, last_analyzed) - def _rpc_analysed_history_full(self, config: Dict[str, any], pair: str, timeframe: str, + def _rpc_analysed_history_full(self, config, pair: str, timeframe: str, timerange: str) -> Dict[str, Any]: - timerange = TimeRange.parse_timerange(timerange) + timerange_parsed = TimeRange.parse_timerange(timerange) _data = load_data( datadir=config.get("datadir"), pairs=[pair], timeframe=timeframe, - timerange=timerange, + timerange=timerange_parsed, data_format=config.get('dataformat_ohlcv', 'json'), ) from freqtrade.resolvers.strategy_resolver import StrategyResolver diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 04d7055ba..b6b36b1a4 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -123,6 +123,8 @@ class IStrategy(ABC): # and wallets - access to the current balance. dp: Optional[DataProvider] = None wallets: Optional[Wallets] = None + # container variable for strategy source code + __source__: str = '' # Definition of plot_config. See plotting documentation for more details. plot_config: Dict = {} diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 4cc947159..d0e5d3c37 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -840,7 +840,7 @@ def test_api_pair_candles(botclient, ohlcv_history): assert len(rc.json['data']) == 0 ohlcv_history['sma'] = ohlcv_history['close'].rolling(2).mean() ohlcv_history['buy'] = 0 - ohlcv_history.iloc[1]['buy'] = 1 + ohlcv_history.loc[1, 'buy'] = 1 ohlcv_history['sell'] = 0 ftbot.dataprovider._set_cached_df("XRP/BTC", timeframe, ohlcv_history) @@ -873,7 +873,7 @@ def test_api_pair_candles(botclient, ohlcv_history): [['2017-11-26 08:50:00', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869, None, 0, 0, 1511686200000, None, None], ['2017-11-26 08:55:00', 8.88e-05, 8.942e-05, 8.88e-05, - 8.893e-05, 0.05874751, 8.886500000000001e-05, 0, 0, 1511686500000, None, None] + 8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0, 1511686500000, 8.88e-05, None] ]) From 176006da29cb21dbbaa6c901a2ee93915de7541c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 2 Oct 2020 07:00:45 +0200 Subject: [PATCH 0747/1197] Sort imports --- freqtrade/rpc/api_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index ea616c0ec..fcbdc003f 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -1,4 +1,3 @@ -from freqtrade.exceptions import OperationalException import logging import threading from copy import deepcopy @@ -19,6 +18,7 @@ from werkzeug.serving import make_server from freqtrade.__init__ import __version__ from freqtrade.constants import DATETIME_PRINT_FORMAT, USERPATH_STRATEGIES +from freqtrade.exceptions import OperationalException from freqtrade.persistence import Trade from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.rpc.rpc import RPC, RPCException From cb74c9bcde49ab35b8c64f7d104a764ffec3e8e6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 3 Oct 2020 13:27:06 +0200 Subject: [PATCH 0748/1197] Fix hyperopt output --- freqtrade/misc.py | 8 +++++--- freqtrade/optimize/hyperopt.py | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 35c69db98..071693f8d 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -42,7 +42,7 @@ def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray: return dates.dt.to_pydatetime() -def file_dump_json(filename: Path, data: Any, is_zip: bool = False) -> None: +def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool = True) -> None: """ Dump JSON data into a file :param filename: file to create @@ -53,12 +53,14 @@ def file_dump_json(filename: Path, data: Any, is_zip: bool = False) -> None: if is_zip: if filename.suffix != '.gz': filename = filename.with_suffix('.gz') - logger.info(f'dumping json to "{filename}"') + if log: + logger.info(f'dumping json to "{filename}"') with gzip.open(filename, 'w') as fp: rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE) else: - logger.info(f'dumping json to "{filename}"') + if log: + logger.info(f'dumping json to "{filename}"') with open(filename, 'w') as fp: rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 75a4e7810..edb51677e 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -163,7 +163,8 @@ class Hyperopt: f"saved to '{self.results_file}'.") # Store hyperopt filename latest_filename = Path.joinpath(self.results_file.parent, LAST_BT_RESULT_FN) - file_dump_json(latest_filename, {'latest_hyperopt': str(self.results_file.name)}) + file_dump_json(latest_filename, {'latest_hyperopt': str(self.results_file.name)}, + log=False) @staticmethod def _read_results(results_file: Path) -> List: From 63e1cba5979e60b7dd9271122bfaf769ecbc27ba Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 Oct 2020 09:12:52 +0200 Subject: [PATCH 0749/1197] fix some typos --- freqtrade/resolvers/iresolver.py | 6 +++--- freqtrade/rpc/api_server.py | 6 +++--- freqtrade/rpc/rpc.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 9c3a33f2b..37cfd70e6 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -77,7 +77,7 @@ class IResolver: return valid_objects_gen @classmethod - def _search_object(cls, directory: Path, object_name: str, add_source: bool = False + def _search_object(cls, directory: Path, *, object_name: str, add_source: bool = False ) -> Union[Tuple[Any, Path], Tuple[None, None]]: """ Search for the objectname in the given directory @@ -103,7 +103,7 @@ class IResolver: return (None, None) @classmethod - def _load_object(cls, paths: List[Path], object_name: str, add_source: bool = False, + def _load_object(cls, paths: List[Path], *, object_name: str, add_source: bool = False, kwargs: dict = {}) -> Optional[Any]: """ Try to load object from path list. @@ -125,7 +125,7 @@ class IResolver: return None @classmethod - def load_object(cls, object_name: str, config: dict, kwargs: dict, + def load_object(cls, object_name: str, config: dict, *, kwargs: dict, extra_dir: Optional[str] = None) -> Any: """ Search and loads the specified object as configured in hte child class. diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index fcbdc003f..3d9e24481 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -172,7 +172,7 @@ class ApiServer(RPC): def register_rest_rpc_urls(self): """ - Registers flask app URLs that are calls to functonality in rpc.rpc. + Registers flask app URLs that are calls to functionality in rpc.rpc. First two arguments passed are /URL and 'Label' Label can be used as a shortcut when refactoring @@ -314,7 +314,7 @@ class ApiServer(RPC): @rpc_catch_errors def _ping(self): """ - simple poing version + simple ping version """ return self.rest_dump({"status": "pong"}) @@ -537,7 +537,7 @@ class ApiServer(RPC): if not pair or not timeframe: return self.rest_error("Mandatory parameter missing.", 400) - results = self._analysed_dataframe(pair, timeframe, limit) + results = self._rpc_analysed_dataframe(pair, timeframe, limit) return self.rest_dump(results) @require_login diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index effe4725e..b89284acf 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -701,7 +701,7 @@ class RPC: }) return res - def _analysed_dataframe(self, pair: str, timeframe: str, limit: int) -> Dict[str, Any]: + def _rpc_analysed_dataframe(self, pair: str, timeframe: str, limit: int) -> Dict[str, Any]: _data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe( pair, timeframe) From c9b3766fa3deb06f7c5ae1f997c713d3400a8599 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 4 Oct 2020 09:14:46 +0200 Subject: [PATCH 0750/1197] Remove rest_dump it's just a wrapper around jsonify with no benefits --- freqtrade/rpc/api_server.py | 68 +++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 3d9e24481..4e262b1ec 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -163,10 +163,6 @@ class ApiServer(RPC): """ pass - def rest_dump(self, return_value): - """ Helper function to jsonify object for a webserver """ - return jsonify(return_value) - def rest_error(self, error_msg, error_code=502): return jsonify({"error": error_msg}), error_code @@ -244,7 +240,7 @@ class ApiServer(RPC): """ Return "404 not found", 404. """ - return self.rest_dump({ + return jsonify({ 'status': 'error', 'reason': f"There's no API call for {request.base_url}.", 'code': 404 @@ -264,7 +260,7 @@ class ApiServer(RPC): 'access_token': create_access_token(identity=keystuff), 'refresh_token': create_refresh_token(identity=keystuff), } - return self.rest_dump(ret) + return jsonify(ret) return jsonify({"error": "Unauthorized"}), 401 @@ -279,7 +275,7 @@ class ApiServer(RPC): new_token = create_access_token(identity=current_user, fresh=False) ret = {'access_token': new_token} - return self.rest_dump(ret) + return jsonify(ret) @require_login @rpc_catch_errors @@ -289,7 +285,7 @@ class ApiServer(RPC): Starts TradeThread in bot if stopped. """ msg = self._rpc_start() - return self.rest_dump(msg) + return jsonify(msg) @require_login @rpc_catch_errors @@ -299,7 +295,7 @@ class ApiServer(RPC): Stops TradeThread in bot if running """ msg = self._rpc_stop() - return self.rest_dump(msg) + return jsonify(msg) @require_login @rpc_catch_errors @@ -309,14 +305,14 @@ class ApiServer(RPC): Sets max_open_trades to 0 and gracefully sells all open trades """ msg = self._rpc_stopbuy() - return self.rest_dump(msg) + return jsonify(msg) @rpc_catch_errors def _ping(self): """ simple ping version """ - return self.rest_dump({"status": "pong"}) + return jsonify({"status": "pong"}) @require_login @rpc_catch_errors @@ -324,7 +320,7 @@ class ApiServer(RPC): """ Prints the bot's version """ - return self.rest_dump({"version": __version__}) + return jsonify({"version": __version__}) @require_login @rpc_catch_errors @@ -332,7 +328,7 @@ class ApiServer(RPC): """ Prints the bot's version """ - return self.rest_dump(self._rpc_show_config(self._config)) + return jsonify(self._rpc_show_config(self._config)) @require_login @rpc_catch_errors @@ -342,7 +338,7 @@ class ApiServer(RPC): Triggers a config file reload """ msg = self._rpc_reload_config() - return self.rest_dump(msg) + return jsonify(msg) @require_login @rpc_catch_errors @@ -352,7 +348,7 @@ class ApiServer(RPC): Returns the number of trades running """ msg = self._rpc_count() - return self.rest_dump(msg) + return jsonify(msg) @require_login @rpc_catch_errors @@ -370,7 +366,7 @@ class ApiServer(RPC): self._config.get('fiat_display_currency', '') ) - return self.rest_dump(stats) + return jsonify(stats) @require_login @rpc_catch_errors @@ -382,7 +378,7 @@ class ApiServer(RPC): limit: Only get a certain number of records """ limit = int(request.args.get('limit', 0)) or None - return self.rest_dump(self._rpc_get_logs(limit)) + return jsonify(self._rpc_get_logs(limit)) @require_login @rpc_catch_errors @@ -393,7 +389,7 @@ class ApiServer(RPC): """ stats = self._rpc_edge() - return self.rest_dump(stats) + return jsonify(stats) @require_login @rpc_catch_errors @@ -409,7 +405,7 @@ class ApiServer(RPC): self._config.get('fiat_display_currency') ) - return self.rest_dump(stats) + return jsonify(stats) @require_login @rpc_catch_errors @@ -422,7 +418,7 @@ class ApiServer(RPC): """ stats = self._rpc_performance() - return self.rest_dump(stats) + return jsonify(stats) @require_login @rpc_catch_errors @@ -434,9 +430,9 @@ class ApiServer(RPC): """ try: results = self._rpc_trade_status() - return self.rest_dump(results) + return jsonify(results) except RPCException: - return self.rest_dump([]) + return jsonify([]) @require_login @rpc_catch_errors @@ -448,7 +444,7 @@ class ApiServer(RPC): """ results = self._rpc_balance(self._config['stake_currency'], self._config.get('fiat_display_currency', '')) - return self.rest_dump(results) + return jsonify(results) @require_login @rpc_catch_errors @@ -460,7 +456,7 @@ class ApiServer(RPC): """ limit = int(request.args.get('limit', 0)) results = self._rpc_trade_history(limit) - return self.rest_dump(results) + return jsonify(results) @require_login @rpc_catch_errors @@ -473,7 +469,7 @@ class ApiServer(RPC): tradeid: Numeric trade-id assigned to the trade. """ result = self._rpc_delete(tradeid) - return self.rest_dump(result) + return jsonify(result) @require_login @rpc_catch_errors @@ -482,7 +478,7 @@ class ApiServer(RPC): Handler for /whitelist. """ results = self._rpc_whitelist() - return self.rest_dump(results) + return jsonify(results) @require_login @rpc_catch_errors @@ -492,7 +488,7 @@ class ApiServer(RPC): """ add = request.json.get("blacklist", None) if request.method == 'POST' else None results = self._rpc_blacklist(add) - return self.rest_dump(results) + return jsonify(results) @require_login @rpc_catch_errors @@ -504,9 +500,9 @@ class ApiServer(RPC): price = request.json.get("price", None) trade = self._rpc_forcebuy(asset, price) if trade: - return self.rest_dump(trade.to_json()) + return jsonify(trade.to_json()) else: - return self.rest_dump({"status": f"Error buying pair {asset}."}) + return jsonify({"status": f"Error buying pair {asset}."}) @require_login @rpc_catch_errors @@ -516,7 +512,7 @@ class ApiServer(RPC): """ tradeid = request.json.get("tradeid") results = self._rpc_forcesell(tradeid) - return self.rest_dump(results) + return jsonify(results) @require_login @rpc_catch_errors @@ -538,7 +534,7 @@ class ApiServer(RPC): return self.rest_error("Mandatory parameter missing.", 400) results = self._rpc_analysed_dataframe(pair, timeframe, limit) - return self.rest_dump(results) + return jsonify(results) @require_login @rpc_catch_errors @@ -568,7 +564,7 @@ class ApiServer(RPC): 'strategy': strategy, }) results = self._rpc_analysed_history_full(config, pair, timeframe, timerange) - return self.rest_dump(results) + return jsonify(results) @require_login @rpc_catch_errors @@ -576,7 +572,7 @@ class ApiServer(RPC): """ Handler for /plot_config. """ - return self.rest_dump(self._rpc_plot_config()) + return jsonify(self._rpc_plot_config()) @require_login @rpc_catch_errors @@ -587,7 +583,7 @@ class ApiServer(RPC): strategy_objs = StrategyResolver.search_all_objects(directory, False) strategy_objs = sorted(strategy_objs, key=lambda x: x['name']) - return self.rest_dump({'strategies': [x['name'] for x in strategy_objs]}) + return jsonify({'strategies': [x['name'] for x in strategy_objs]}) @require_login @rpc_catch_errors @@ -606,7 +602,7 @@ class ApiServer(RPC): except OperationalException: return self.rest_error("Strategy not found.", 404) - return self.rest_dump({ + return jsonify({ 'strategy': strategy_obj.get_strategy_name(), 'code': strategy_obj.__source__, }) @@ -644,4 +640,4 @@ class ApiServer(RPC): 'pairs': pairs, 'pair_interval': pair_interval, } - return self.rest_dump(result) + return jsonify(result) From 887b2fdb5e66b8a2ab9c293ab1bac1d91d323d4f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2020 05:41:23 +0000 Subject: [PATCH 0751/1197] Bump isort from 5.5.3 to 5.5.4 Bumps [isort](https://github.com/pycqa/isort) from 5.5.3 to 5.5.4. - [Release notes](https://github.com/pycqa/isort/releases) - [Changelog](https://github.com/PyCQA/isort/blob/develop/CHANGELOG.md) - [Commits](https://github.com/pycqa/isort/compare/5.5.3...5.5.4) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c16e387ef..5e7c9ff06 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,7 +13,7 @@ pytest-asyncio==0.14.0 pytest-cov==2.10.1 pytest-mock==3.3.1 pytest-random-order==1.0.4 -isort==5.5.3 +isort==5.5.4 # Convert jupyter notebooks to markdown documents nbconvert==6.0.6 From 80890e0f596687cb47dbe9608e4eea6f79b49c79 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2020 05:41:37 +0000 Subject: [PATCH 0752/1197] Bump flake8 from 3.8.3 to 3.8.4 Bumps [flake8](https://gitlab.com/pycqa/flake8) from 3.8.3 to 3.8.4. - [Release notes](https://gitlab.com/pycqa/flake8/tags) - [Commits](https://gitlab.com/pycqa/flake8/compare/3.8.3...3.8.4) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c16e387ef..030b4b2a2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,7 @@ -r requirements-hyperopt.txt coveralls==2.1.2 -flake8==3.8.3 +flake8==3.8.4 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 mypy==0.782 From 8d4f7ce84f8c2d983756b7dfde3e57fa9fd686fb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2020 05:41:39 +0000 Subject: [PATCH 0753/1197] Bump ccxt from 1.34.59 to 1.35.22 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.34.59 to 1.35.22. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.34.59...1.35.22) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7f1a3dabe..fdd616770 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.2 pandas==1.1.2 -ccxt==1.34.59 +ccxt==1.35.22 SQLAlchemy==1.3.19 python-telegram-bot==12.8 arrow==0.16.0 From 0574a406934d1abd081f4910097d0704a34c1b16 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2020 05:41:39 +0000 Subject: [PATCH 0754/1197] Bump questionary from 1.5.2 to 1.6.0 Bumps [questionary](https://github.com/tmbo/questionary) from 1.5.2 to 1.6.0. - [Release notes](https://github.com/tmbo/questionary/releases) - [Commits](https://github.com/tmbo/questionary/compare/1.5.2...1.6.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7f1a3dabe..0a5f3581d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,5 +34,5 @@ flask-cors==3.0.9 # Support for colorized terminal output colorama==0.4.3 # Building config files interactively -questionary==1.5.2 +questionary==1.6.0 prompt-toolkit==3.0.7 From 52b6f6b940009b11c342da46e5e883dfe72d4dc9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2020 05:41:40 +0000 Subject: [PATCH 0755/1197] Bump pycoingecko from 1.3.0 to 1.4.0 Bumps [pycoingecko](https://github.com/man-c/pycoingecko) from 1.3.0 to 1.4.0. - [Release notes](https://github.com/man-c/pycoingecko/releases) - [Changelog](https://github.com/man-c/pycoingecko/blob/master/CHANGELOG.md) - [Commits](https://github.com/man-c/pycoingecko/compare/1.3.0...1.4.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7f1a3dabe..40eeb59c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ wrapt==1.12.1 jsonschema==3.2.0 TA-Lib==0.4.18 tabulate==0.8.7 -pycoingecko==1.3.0 +pycoingecko==1.4.0 jinja2==2.11.2 tables==3.6.1 blosc==1.9.2 From 56647bb4985d258c67b6c7094162b633c906c3ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2020 05:41:42 +0000 Subject: [PATCH 0756/1197] Bump plotly from 4.10.0 to 4.11.0 Bumps [plotly](https://github.com/plotly/plotly.py) from 4.10.0 to 4.11.0. - [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.10.0...v4.11.0) Signed-off-by: dependabot[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index a91b3bd38..7c3e04723 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.10.0 +plotly==4.11.0 From 64de911e16bae16f9a8f2da11bb16c0d79ecdcee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2020 05:41:44 +0000 Subject: [PATCH 0757/1197] Bump pytest from 6.1.0 to 6.1.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.1.0 to 6.1.1. - [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/6.1.0...6.1.1) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c16e387ef..820febc26 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.8.3 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 mypy==0.782 -pytest==6.1.0 +pytest==6.1.1 pytest-asyncio==0.14.0 pytest-cov==2.10.1 pytest-mock==3.3.1 From 688442507e4069b0e6c90c59162b2b8e297c09ad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2020 05:41:44 +0000 Subject: [PATCH 0758/1197] Bump mkdocs-material from 6.0.1 to 6.0.2 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 6.0.1 to 6.0.2. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/6.0.1...6.0.2) Signed-off-by: dependabot[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 091eacc9b..66225d6d4 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==6.0.1 +mkdocs-material==6.0.2 mdx_truly_sane_lists==1.2 From 234f6c2f5e2b4e1fb5621bd5c01d258fc1a84212 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2020 05:41:44 +0000 Subject: [PATCH 0759/1197] Bump joblib from 0.16.0 to 0.17.0 Bumps [joblib](https://github.com/joblib/joblib) from 0.16.0 to 0.17.0. - [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.16.0...0.17.0) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index b47331aa3..d267961bd 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -6,5 +6,5 @@ scipy==1.5.2 scikit-learn==0.23.2 scikit-optimize==0.8.1 filelock==3.0.12 -joblib==0.16.0 +joblib==0.17.0 progressbar2==3.53.1 From 40b61bbfe37582575c9c94944da23676a8b56e70 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Oct 2020 07:44:12 +0200 Subject: [PATCH 0760/1197] Adjust trailing-stop to be python compliant --- freqtrade/optimize/hyperopt.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index edb51677e..5997e077b 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -262,6 +262,11 @@ class Hyperopt: ), default=str, indent=4, number_mode=rapidjson.NM_NATIVE) params_result += f"minimal_roi = {minimal_roi_result}" + elif space == 'trailing': + + for k, v in space_params.items(): + params_result += f'{k} = {v}\n' + else: params_result += f"{space}_params = {pformat(space_params, indent=4)}" params_result = params_result.replace("}", "\n}").replace("{", "{\n ") From 06759234b64b5bf93598243cad3f01053299785d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Oct 2020 08:07:53 +0200 Subject: [PATCH 0761/1197] Add test to verify output of roi / trailing stop hyperopt --- tests/optimize/test_hyperopt.py | 37 +++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 7f0e0eb6d..6c49f090c 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -1,6 +1,7 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 import locale import logging +import re from copy import deepcopy from datetime import datetime from pathlib import Path @@ -1230,3 +1231,39 @@ def test_simplified_interface_failed(mocker, hyperopt_conf, method, space) -> No with pytest.raises(OperationalException, match=f"The '{space}' space is included into *"): hyperopt.start() + + +def test_print_epoch_details(capsys): + test_result = { + 'params_details': { + 'trailing': { + 'trailing_stop': True, + 'trailing_stop_positive': 0.02, + 'trailing_stop_positive_offset': 0.04, + 'trailing_only_offset_is_reached': True + }, + 'roi': { + 0: 0.18, + 90: 0.14, + 225: 0.05, + 430: 0}, + }, + 'results_explanation': 'foo result', + 'is_initial_point': False, + 'total_profit': 0, + 'current_epoch': 2, # This starts from 1 (in a human-friendly manner) + 'is_best': True + } + + Hyperopt.print_epoch_details(test_result, 5, False, no_header=True) + captured = capsys.readouterr() + assert '# Trailing stop:' in captured.out + # re.match(r"Pairs for .*", captured.out) + assert re.search(r'^\s+trailing_stop = True$', captured.out, re.MULTILINE) + assert re.search(r'^\s+trailing_stop_positive = 0.02$', captured.out, re.MULTILINE) + assert re.search(r'^\s+trailing_stop_positive_offset = 0.04$', captured.out, re.MULTILINE) + assert re.search(r'^\s+trailing_only_offset_is_reached = True$', captured.out, re.MULTILINE) + + assert '# ROI table:' in captured.out + assert re.search(r'^\s+minimal_roi = \{$', captured.out, re.MULTILINE) + assert re.search(r'^\s+\"90\"\:\s0.14,\s*$', captured.out, re.MULTILINE) From 14c66afecc4c10f90b7238b99abe1ae5a12f77d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Oct 2020 07:11:13 +0000 Subject: [PATCH 0762/1197] Bump ta-lib from 0.4.18 to 0.4.19 Bumps [ta-lib](https://github.com/mrjbq7/ta-lib) from 0.4.18 to 0.4.19. - [Release notes](https://github.com/mrjbq7/ta-lib/releases) - [Changelog](https://github.com/mrjbq7/ta-lib/blob/master/CHANGELOG) - [Commits](https://github.com/mrjbq7/ta-lib/compare/TA_Lib-0.4.18...TA_Lib-0.4.19) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 510e77dbe..7d219f7ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ requests==2.24.0 urllib3==1.25.10 wrapt==1.12.1 jsonschema==3.2.0 -TA-Lib==0.4.18 +TA-Lib==0.4.19 tabulate==0.8.7 pycoingecko==1.4.0 jinja2==2.11.2 From 355afc082e4619e7de11640bae4b6dfc8cc61f81 Mon Sep 17 00:00:00 2001 From: Xu Wang Date: Mon, 5 Oct 2020 10:05:15 +0100 Subject: [PATCH 0763/1197] Add command 'stats' in expected test output. --- tests/rpc/test_rpc_telegram.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 762780111..bcb9abc85 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -78,7 +78,8 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], " "['delete'], ['performance'], ['daily'], ['count'], ['reload_config', " "'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], " - "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']]") + "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version'], " + "['stats']]") assert log_has(message_str, caplog) From 4b53c2bca4aabe75b73fdc0714a4072c17319deb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Oct 2020 16:12:41 +0200 Subject: [PATCH 0764/1197] Complete TA-lib update with new binary files --- .../TA_Lib-0.4.18-cp37-cp37m-win_amd64.whl | Bin 537651 -> 0 bytes .../TA_Lib-0.4.18-cp38-cp38-win_amd64.whl | Bin 555204 -> 0 bytes .../TA_Lib-0.4.19-cp37-cp37m-win_amd64.whl | Bin 0 -> 482390 bytes .../TA_Lib-0.4.19-cp38-cp38-win_amd64.whl | Bin 0 -> 504687 bytes build_helpers/install_windows.ps1 | 4 ++-- docs/windows_installation.md | 4 ++-- 6 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 build_helpers/TA_Lib-0.4.18-cp37-cp37m-win_amd64.whl delete mode 100644 build_helpers/TA_Lib-0.4.18-cp38-cp38-win_amd64.whl create mode 100644 build_helpers/TA_Lib-0.4.19-cp37-cp37m-win_amd64.whl create mode 100644 build_helpers/TA_Lib-0.4.19-cp38-cp38-win_amd64.whl diff --git a/build_helpers/TA_Lib-0.4.18-cp37-cp37m-win_amd64.whl b/build_helpers/TA_Lib-0.4.18-cp37-cp37m-win_amd64.whl deleted file mode 100644 index bd61e812bed0dd263dbe3da2b490a2fef17165ab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 537651 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 @P=lyz#M&c-Ho?CmUddCi7nP+S2o0F&2hk zk%BNhgqm#_8JmB7! zl54y8i{#oiv9Q_{kxbhWr!(b1dWkX1cA5pu=)i2J>6|IynCeJz%QeU+=hrj5^aJjF za%!wEdBD9dd2F$nZ#(sXd(L6qtMb5eKO>6PI&A`xW*c-IE8cEJ3x>SA0-2v$dEkMo zi+j-b-#&z0_B|xwRqeBiyRMHq*kL^DokXrJP=a$KT%*>|b4ZXOGfP{7P=vnp1DUWM3EI2k?_ zQzTpVrOae1Q45}kgDKG4uA<5Ek^6OL-)wb^_i*zRYGlg(x_H(!_f4UTl4e?rr#CvD zQY59eUMOuPPJ!4N;Vj`rDuv5ivAcj$fmDnl#f%urxJzT-0(#e<4n4EDtQZ~(P5iW2 zGT$>`W_m6i80vK57S8_=Oki{#g*U`wSF2o%m=cX9GZRV$r%K@|%fk{SCN~ewbjW49 zkW;!quHt3ikV(?tSYS@HY`c8!xM{om`SGOBh&iJnnG*8Y^ITks63zn6Vk>Z-F zo35!}Ryh{uiAU-f^f@tMQWO$J6 zp~LV->F24VpBA29e{T$DibQ7xjDDE$KrI+T&;i|T%RJ@mjCL`SSUI7I(gNJjEJYqy|OT6 z$ISxP+#9#%-Vs(1${y%8ZGZhhh_odN3Z%70aXj=pX^WIZ?T#3Fk#eRjQX^7l+^qu~ z!klcGnLqM$tCVxdRceGYy=5r?Wtkdr+GWZ)WIXZ`X zH*9RuYe?=`O}$8ciSxpO@D=Uw(^94USA8k|t2<-U#edZbm{3P)j89$IQ==tDSkV&B zRKz$cx=s97{pvJxW3Ml<&Ifk9Q`k{)V~V`QH}x}M#uQ#Wovb9hNW67AS;;v_RvHmM zg#p%{@zEsAiJgiQvFBO2W{|k_-{QozXPXn(r4Y)cI4g}%DMyny2OUi^B26~gYVzUQ zq%UCW#xf^k}{uCh6^%8O<*xIo^X1l**aW!lGbJI|6npp{GV261l}n zwzp$OG^bR@-YH!a>6xo>ZKh6cRj>jj2|aqb2g$Q$&4^Advh>C2nrrGX-)nZI+g|RG zL;%{PlMtMIWvfVZV;9g-Q*IuEIMk(_U{s{L)g1r7RHYXk#S(esO&&i%37A40Tqe8q zXU49CTdbQCpGqi`I5TV2?NY#WWJdh|)|hg5p4(OT3gBlb)1IUe)vBJ@&SD=zjN7SH zH%5-ggK*>L0A^!r>hU(4@L}LaS$~mD-XbsjlYm@TDiLwm=Bo!fgpl$ocJVE8fP7Q&Kqo+RuH7f8Y-7w%UffMVjPjj`%*k@-L0=Xu{Vmjo|c_OtDO`}t@{&Uw#!dEVP|{XVQF zNS*8CHxs3NYzfG94^RFuy9pN;1gEN;qD$rB3CZW`xOL@ zI7<_^yNhyS{heuqSW#TBp5@oVBI4n~)G{o=PZuYd8|NnT&Gw&i#FXCMbfsJ1W4!035e zQ(5AlB@tr?lkn{_)_m7t;}13(D^@rJQE*lA!T*8w&li;93uX=5=HB<~jjv zqVGu1h#U#}US57Fk5?;H=dnUHr9luhS7S;l{pM?aZy#pC6RO=vLPWIo zpx^vs$f|Tktx1iHIqCN{C7N*!HWU=6=_hj!`#KEE@01&7nAu?!3`0Icy6PX_=de{^ zI>*b`_}bqQwr(nncz4hZoDF0pPI&10D$Qr8pDqeEF3_z4oxnB{2=Saw<-GyprcJ5u z_E}Q_JLNV?di)u$js&c!@Zb#5%kIqYwpmlN@j+d)3n)U&2h>G=XT`jhMkE}@nIF$Ggi9U^*Z$esK$gEEs z2LgjA?q}wOmMrw3jJ02x9(RGPA%HGK3R;FPynM7s;w)#A49hU7ypTsL)$fTsk;G?f zNxU~tj-Gi=r4!zhr~Gg_>6hN(L>h=;1R#c=^=lMEzYSxkp;=X%rbOJ*0mWHsXGsXl zEMhRPm|DsBh3p>Vv_T8|MHFXQx`%&HsHC$HrTyM-vJ5`sC6=&Nl7lBiAlJ}@Dv7}X z_QR1g7=Ans@f*D6v#;knBDL{w?N)Z8EE4lw2^;k$Y8MN5G$EoC3K-uB!va-;D*2do zb8p1#oNBq^VRN@|${~??%zaVs;UMwI*AGatNW?laa2)QJki>Ml?c|-U^na&v3amUq zf9&qV_Vk0HE*%iCI=byRBGcvII5>cL(QPM@l7ipI9}5sOYt7g-pCJU3I?FWRyR^)e z>>~Vdqjo1o;DqxhGYxq z5Xk^`*eHT!X>$;&JrMHkGprM=czIHguW#-_fT!i!JDBEB<`T_6t!S=5*`_;-KxgO! z;bYRv^*Fe`gc(Va8_$&K~#6b{#4zCd=Y?3xgy*L+dYSFKBs(;Jj*(v(X0ZV zIynD87kyK+4>aVc&6Np(hFb7V#n>M}llzUwSc6tSz;a~0u=qy(*QMdPUCJtdv$=;^x~%3kQ&ug53oWyvNGl{+NmSUo!(zj>}$(+d_u@CQ43SchCH>S7M8 zB>t`DkU(nYi}UIgpQA5OY2PRXrXz@V(z%Y_SUDI>Xsjc6L|y1*1`~hqTt>Q7Nqa6w zb1u}CZkZd_V=zA*I#51Q8o?M=4kb<2Ic4B#GaimppGd!IwVXZ{fElkyo zA~-ByQuZ?`VeHwB+5yMY$)Uy(JY+uxuCyt=(wmc&9^guMsX2N?&k^GtcEx#)niW&7 zD8y}c^vBj>Xp@*i{Y;^Lp-{g~AypC-lKHFb?yF3Rn%y&?^B~+PPdBk{Kcy}1Mn^n0 zIXN3~Zc8*lopHM@Rnndf4>uyHYGtY&h6l@Acs$=BlVNz~b9KwQ;^L`sRAO>#jwn*~ z7%Rc_sg104)me2oog zcEhpynEwT4_l!D2ut*EK zaB6qz0%F_2B<0P1Ddp^N@Yw-K7d|_%VBlP7A0HQ;;o~FEOCR+WoPi+WY zu7B3&G{e@abILR4kUDDV=bXcQ;L`uc)%C-$wdzds+l4#0@QXdMxjceOIR(#l0@~jp zJR`ursbdh|9kz?|8-Dv@4;!g)TEQ#`gxmR?H1nW3wPxOj5nbm@$VQ{If#@T+NNKgTj`uuIk9>nS+I zCot@?ht=lV9b3*j>teOv{i;+oHaM+*N34eTRYl6lK-}G-bkX~1`+#z!a4)yrWY@?k%?w2>2r=?6kXna2q9tP<`S2T4k|T!^yp-0*GsBV-lC0MW;~ zkw20kq;t=Qz3)JAfV~m%z7_GFig=It&G&Scn4mS)S$ZI;AEWxFD!T;++l#cssF zl>v9PNz)~AL7XiZNl}8%yqev9)6}`$AWC%dxyZmSzgEfJ_6bZ4`xpWj=b%dNw%1ts zjK$}j%LC&gewoN^8H6X4U@jFnI~ z@qKYYTo|_%ibdP7-^O;-4 zzys&#>f8%ttyOLV1^O0fF+wN4-_7jV9rC!dZyNgu!9^(Wq(jk<28H|Z3p^`xF2rs1 zQtTFVgr{qcOxK(Xa3P*h_SESY;zpbt*9GK$xIM0&eWvq_>AQZ@;`30jQoufkqlJU9 zsI-9l&?PU1!JKshCSVvuhI8fRaa}@LMk~b40)U>w(qO{qil>coklob`&&9m+F65Si zjNDFp8JP#i4x^%l0w~W$rGg9DClx!nK#gOJZG62T*@DK*N0+M$hV!Bp%=}v{7rgNc zP_x2>=lSAwN z5-j>tQ?kR!R8q35V_f;Oitm(|R!ZnbeEEdF5f7Y58dOqlhE)HlhDfpdjypLwby36= zV^7%Z3)MCw>kQ{)AFZtlMgE$ZlJjX?mSkqXEK8+lLUy-GzVC)-DFy5qzM19}&*poP zlfH1WW?YwIUpOH4h0^WN1A8LosfgLlS=+xM;@l;_-<_|rx6@T5%6+~@VdM#0fnp>D z-3$G3Vbp96nQup-KkEwD^=607Lt&ucdH)#oZVh`6h7*ke^VLX$tN2#y`c>)i)e)@D z>Jaw+KExX|$9VMwIO9Sf8}K_#v=Z`Nh=9+k2TeWk38YJjjll-diX;RKIF^b zQ~epu{b`VA$aSwRV{c_EzV6j*VdJnfiH$pu&A#c8ZPdYqe&6=xpVZ*9Vk#5FS!IsV z{fek7KPrHd*n3g$L72`$!dzpem%}+^S#g<{Hs!{k*^bRJ(U`G`Q-?|3uxoy?Gv8qj zgsi2`Q0;!2uI#zSf+qMWASFp*)Vh8H?G(VZ6L$gchyH0+K}k4!0}L@9HokeA;5p(( z=DVeVhJxa81;q(hF}-#{JHYNet_uRlh}@PX?h+B=SIv!9?y!itC*(V2JbuKXz=7Lr zQ`|QkoEyUzF%Lw%hr@NT>~K>oE1YPd_P;Ip#Sk7r*FUr`_hl1Lfji#>C`EakgOw(C zr@rb5Sbo^!=uqqn7?Yb*-}PFz!5mlars^`k+e_bN{^b(^7Y-Wx{jb6C1Oe!gXzQfIw`db=k<-422tQWULARK1}qR zja0Zby9q@=Y5O&uLBnAF^!o;kwF};p>lHhTxe6IQPn71`UPOWtQB0 zW^QrD{u8u_Ymm>(Y^8bc8-IpfnVOA9eGCZENu%wXm{9j=%9r{~(|_CNlVnd!wbvtm zB3-9pge7&SJ_nx0m;Kj~LSfp9Gs3k^5$}HX zM%_cwG||+xCv4r}3}kO+R^P#lcy5;Tp-}a}jr)6WuOYB<@}Ld8G*Z{eJh{-ZdStEO zot+^u=_kOi@@~6s2>*aYV@vYB7C7kjeAD$Y2DEa(&({hUyu(%!5Byt4Di8ddMvC2^ z5y>0A%e$U$-%pNISNFvuB}%j3p)U0iBZV_&mm`#jkC2PnBl?y;`m^_@bPs&=8Xowc z#Zlo%)3>-YJn#h@U3!w*Z`|Ktv4d7+0(zEb`TJ|GDLaqVEjWMtW*kuj>^X17wGBVu z{T-V3{*Ijn5?ySgH)bR_0&KS5Jj~%=6|9mew^cF|*^l(bj(qX8F9L!@5CFpesQvBR zcnf!8R=#Gt+qZ>lyTrpeV(t*cCx$usv%sQcbwC3FeLW-&@Rq^i>>Uo z(V_zYuRY>8HxXgzNXod}N_UhtE_c$?d4%?t4(pQdTb;NqyU*(2oC+4LW5Lpbuf)N5 zyPOqswS#kmLr*JW!2rX&F%>3y195pN{>_4?EorWI#QZi+G$UshIAd3#Z{n9UVnCxs zI|ov-BRDgAC7Bsn9C$iZD=CjdwRDgj2FGLFn`Lu6ybQNu8Lrj&?;_?7I=SL-tT#K5 z|1NCqq!DE1znf-_{rWmqdH9q0?|2C3Dgho(7nB}1*izh}1bCc4vbxeDbZf(3 zR!USNRd+0F;)BoF*Z2 zhorybW+eR`Pv1Gnf44{b-W(0tksersQ7iWh2PQ_^@*Sm;@aO6_j!WbmyU;s$kB4j9 zO1DSM8@~RT^XShvqE^d!i&IYgGpH7BQgzy;9AtN)LbB!;xp5y`aYy#U2%%9MMle9uVs%@0C@kM4R3Uir7qiy9!fui(;;3 zFY6%7Le+k-6+~cNuTKnl(_YKWk@V>+a|suS(bUam4h6mS@-*- zOM5%()r4p&tdlG7%yu&bhV6F%vhBTZl2Njm$vF#QoorKIQY7J=K<#G#kR=BFeB%Y^ zY5m5O&RACIrbKH)?$^~MN36+CZGIEQ;&^74XqAnTIiyAUfLPyzd{ej#6Pa~_1+l-? z#y5|U87p^ulFW+gtp4=4#wSURf@8E>_eiPCUf2?GxyNsQUHPDMU>OeaPpSprmzY;c z$z6S!>g_R3Ad7Q-te%M}Tu|nV^AF|5K96JmuJ@g_DXMz08IDVtK|IK}V_wFw_86Vd z9ULm!g1s4L1!(|yK!?8*vG$x3_!#n7to@17jk?9pT-o^MuxhlLRio2cHOl->39&zH zJk+*BxRUB%`)3?tRRhb_Fj2E@A*Bvtzw&ACXfQ}aeu}ss@@_x)=TN4Z)Z(y zf~CTw@AWSoA~F`H=$lw!6=w0TfcDN52q zvMycM5ykxdn(>6~X%t=V`OI&ErGx=-6g{xBkig&cBH(U?ZSiaI^GbCMXR=kBm|n=|{QzQC(B46xIdo?hbE4bZ6sfej3QK~#bSMP%mZJUcZiv3yOs z3bgLTh0BL=0h-C2VH3`CEMDGFF~6Z=VME2DhKi*P70Vjdu^waH3jVp8f3D%5>-gsr z{PQXP`85B0hJSvSfAW4<_apxKJpcR||6I>Mf5Sf;`R4`}a#}*Qn;NFCo?j{g9QAKm zHlCU}|KKHArgoDVSUo=lKgP_tjh;7z=E(>2x1att%jdP|`0T6k_alvGmk(#KF}|0@ z!xKZsXLG8ZSt>sJ4_Z1ruB5{rB^}ymA1x!5S^if_IuvU>Nr(SWNrxrcP5h~mY1a6L zk%Qy29jWoz?`MryJuy{JlHJxW1Aem+vLTy1&AkZEZs!lK7(MMNA`3hnp0zCydc!hw{$J;I}_%;a;KM?WMfs?(--7f&x3^{!-Kv$W~UO z_DlwB7PCNkiEC>tWJS+_h7WNM4C#xn^BoDN{wg1r{}QS_5HSxR1&MXPtVJ8Xrc>6U za*)SbWc%I+tVI$NzxqG73I{%D70QHE*sHK#twI*+-t2qjT(81`bG!=QO6gspEUUEM z6($#>qfqy*P_Ms?iRLvpAZu`*UW4l#8Poc~YjCVb$y0}8Hzma!w+fvsMfb+rtIwle z>bzDSTgY*wZ4h3CZ}BSpI^;zL)39!hvnp-u3;B*)tz1TzyF9Tz-w{e|2_-tUQx5XE zXrG){Y(J@pL%^IXPW zCM2V@va?Q3mIB1F#ZA9)W%C2G7SHsX^~zaiw7BP2@8ne;@%viU4!w?te3@iO{S@!e z>)-)H)88$g^s_cdkpUfNob=K``loJx-EWhT(=AN54G_zoR6aUbKL@!WO^l%I+@T@? zh_TXR^1C^&69z?{GwN|;qBj#RgN@EuS)|0?-!_Wao4_DXK5+Zu3K}F1DjH;tQL-Zz z!q{@v*o^93k|nrU9$-X*?1L^MGZcJlsyi~q1;?5_s$Lz)@D`8kk66-HyHopYlL%CU zz?zMWf_~qF(yQ9VpRaFRWS}P^)S)LYs%Qt~?n1-t0N!!mREe7oqMgg7uuXUBB6~Lr zE`0UN)X>4$Z`eZ{-9&iDVSM*Jru@H`HYPSsW^5o$5f)Z2V)+j~YZqzp5B{RB#Fn9h z7po3Jh~vdD_A-|IE{n}X)eu&5*;e7YPDyi~i&SBWjvk>t(n&ONK3-(Yd>7d=-(~5Z z*J^DJsLa7J)!PE@EmF!>bJ{j+=RLfozhd{cfO|W(Os&f)ti`BCZ*NWQty1I`+A2xC zj3wy1aBviKnbf6+lAa37FR8G?DTqcmwgSGx#@gx>_Z<>Z&z<*Wn~W6?;c?xv;@QLse~BM%H&)#36q);B&0bg0gWfu3l7X|hH{jbZ7RQAU zeI{!5I8eOQ=8e$-mW9LC)PA-pmL6bZXBb{NBqN2rz0uk&rSC+&&5#$10@fYQNbNwt z`pTTCR_>-WJjH6acK3{wxHbI57CLa6{l4bK4`R94zJe}zw{+tjzz#0tbaDCgbH@^- z&gC3$P4|DfbbCX=FX7$X8csA}ZA5*Y%b&3QT#8tXV=I`lEz>aJcEv-cQ3cpwhC!m0 zpV)4uXuMf>>|Z&{+!KryyOw^2Jxf-h1^Ka>!cwll(MszeHJx z%eAfVZco!7Q+ByFVKTh{9=u)bpqN&QeAna|$Vl4@2eNm+0`r@hV zO)#l&s`4aA>Wm6mXO!+RFqYRz*eZB54c`=CcL2r1B@dku4E0zJ5n3gT$PBF!(tDzu zp~^YAn462ab+M%8==%}#`r?pij~2-k7h#Ia!nG}|LTXVeB%a?u_I@pHR4N^kG-6L_ z594`&knVsSE#(ZGDigua^?Z7g^XbW)&ybnZbP?hm5NWT2^fUbD?m*(i&?B!s%o>+B z!{%Orj~JuXEqH+}fwBPZ5{U0Rx}32=!2TwcEPEZPN)~;aqWRRGLJ{-~OBx$saEE%! znDLTEU45?bJcmYE6>QaT?PWTiOKglzHj#-nY>@qsR!-LY7{dlqte77{ zP6dEtj|~_GE9OyKN~PO1OJKenL)stZeTMmwW5Ux=QIF}`AaC+(Pnw0+Bf=_z%(VqP zqzeujTC%P}DN>kLmy%FXSBiit1v71iMG~P7`Sd=PPY-LfFrLqlHN{qenXN3JV)`(T z=ANO9ncGv1nH+ag;2heR=}4QuOoO_F&htU=4Ypsd`61#(rm%3D_mA{P)N=KP>e@~- zW-y(wA;gBf6)fEp@tz8rJLxhxMVzF#oyZRJTlYATGGxNlS~LUW^QtjrBMY0;GWGWTe@@5QKJ44PrL{*K|qnpBo)BqsGlu3Tz z>D-M%j;ds;axJz{6fU#}t;C{DA+oJ&z6xRMm`CIOF zd=R$ld^p6)!3$M5&SC}Y%1Y~oB9dxuuyI{WQ*d3Ahq&zzK7#$PKMWqy9e2TgHycfJ zPGPVyo6R>U4Dx$vp(6p_ zZoep0pyz;6rLFhsUJs5&V8`bVN7{;T-5buu-{U1TQCoE(AFoz7{xh=Jh$L8GaW!isf8?e7k-`7`S=f6EUh(`-*?R>~f4cup8e}<4QJ`%YE|&;!HS}Q1IrBCT z_vhIvxb;WbicNbW`~A_u7G*1I+CABrw-w4QlZf;=d})P=q3a`tt`7;XXCsjH6FSd> z4u)VWruZVOu7Gk2quvb=3CjGxlIREGuKnigtROIEYzmrhi*CUCX1MmnaBXYQ+>Bl2 zF=jMI8l)SDfgs_aRRuMWL}@nry&D7OgyKXa!Wn1+CLId;b{ke*mZ%d9zuA%95d;oT z8~f|O;uz$w|9yw}>svP$W;*P<*kM1#K3$>W8MXo33m*78*k8Xw;u&t*p89HMfP@3^ z2prFl{%)7}>sLrTL;AZN^j$VSNb}c+-Y4Q~d1xH#)>;R3qlv0%hR=Ru!0e`i%lGKn z3(~1*ty1v|!xcd~H9?LKCdhZ*k09ysRiMXSzDcPgh}hU~(*u}l+frX0Rq+c%k6s$y z=G1quCh4&^^Sk{Lzi?A#{6Y#XGP(d-G$~rNGcA~UiXJyN6cqUrt-*%eVj!?>a3w<+ ze6Jd7f2nRoEa?ESmREjR15LIJa(2O_}rhAI3%7=wzCR?Hgifz+nAL8to16PISwz{={$`M5m247Y!FV)8SIb${8(UzO5um zWV_^}>Lq^gL~#i}*lw)YG3v|5&JYOQ`9H zD{=n%KX_<_jUm9g`mOTp*j*8e&g57gqFRdV=FFTKBhN!x&|t%bbP@#`?$p$5NkNt? z3HV-F9t@#^h_7+ch_Kn7IKPHk5MkEAHLYQ)A`8%OR!SBiB=#iM2;^s!xoSGw;V|HK z!F&89>{7@N8Z=)Ecza=+LfX(ULo0@APx%uY*^S^C)SBpGgVnlh+~z&F zE50S6&68mIn1ooHnm6lC(f+nKpnOB*+3Yw$d|8*^H)gFGQQFG1y*IgSiF&y|yCb-E}}Ek zPEnL>5V<v% z%?BFRzj;P)S>8YO2aL6kDIo#-DC#|gh30(+YpZr!AUhVe3R+pidvVx=u3{txLKFF! z6!IO%h-ebW^H{!&K@dTaau0x=G6ecLzY&w=O;2Km@^XHHE_7Lz*r&MIzq4!R(mcPehjU(#diG zYZMw;X~Ron5REWm(8*Yt!;<|+pp{UGuhSJgxJYev@kAO1>{vLAFqX&9V;e$5#=;I) zJb+FNOXm1E4TmwuX*4t@;Fy|+g&)q%PZ%%D536!p-yh`K<=K3mXY)BVk(fdz71dsHHWJD38T+s#p_e|a zf;e~i_-E4~PGp_sStof;-U~PNc?8#4;kQ-<8p`^1*zx>ZbUc6GrV|R`CaKR&;rGq` zh=%S@XZS^*d%#$|8KKJijWx|Wl=+7r!0$h%`Ta*S`Td28-`A1Pvwb^0fY(>L+WCst zUqrnAuWeqRczh^qOM>PWX7rJ41QP4XfR}S+dBU}Auop@XB31!d2U=V^L)JoPG<$c{ zn$XH&%!>Orw|F;`1dwQ=h367^7HW`5p9n(l*P!_SDBYrmt=y7`*&6b_Ydjv~joF5n zMa}L0fFB(WTfxE*?NH)&L&WXUl8RWaZ)N(YegOZ!;~eq-Rp58`D}Fa5{C_q0|2B!) zJq!QeqIllDRFHUHS8-AV=rXLwln4+cnslYmq-QWqzReQA9?&G>-JK4z{I&mm{{L0x z|5Ir3{``MiQWheb&{+Z_x;wEE2o1SZ-A0^4`L1G^i0d{$6!682wf~tS2LSCVRRoxC zivWut0?>+2ZU=}9>03gifTetCz6dGcURw&7pOgYVpC$$Hhe;`b|Frr4;mr6WL!{#P z3qxk!_Y-3r+5ZHm-?}f)b6S%VMzA?urw0DN=&l)jo-CxD=QsD!Ny7TH3*;=(TIRY9 z3;i{-M2GNOWac|#toeCLW&J(I_I?td_#Brd! zuaufE*kC|5>&?@uIN!SueTq2x#U<4~M&&Xj-V*vg1#$?~RuHJ>i;Eh+Y)2foUzvu8rP zQSx5ky49enQNI{wx25x(0%Hl7h8f$<9n4byEAIcgMvrEj!&Yv4z-&}(v$s2ty>~A3 z^`GxoC`Ifq0KAdzm`lvSo_`RW&*}|Byu0juK69VtGng~OFlVl+-@cr#^_D>Pe%LKh zo%ktv#?dzi`J0{m%~sa_ZLZ(`(Eho`f)>#L!Vjv3!HwWPwS7r0?%HO6ifoCEpjVvb zb`$eKf|nEU9bO)$Nh)o`JPy(+i>g@%+D7b%51no?V+{tp2mFZ^YRo9vp{V^db6Z=4 z*wp1LY;@4PGnL?}^lm$M^!~D2@P_Lm zC&|E)_0(7sV>Ket?=BB#lqo>Y9aq4*BcY7%L2GOsvY*lj?(2tn=O`Um)X$d&7?HMW zCvpf6L8=GhdLT?Pl!NVM$g-bF7Hw5U+s;<4`$JYstYnccDy|%Wt|l5hc#Ab90WCa` zDVk`lKnu?fc%3p+RlD)m_4HR&=b$+Q(WO$VYFRr`miWYnzCu;{&26X^wSoVIZLpJy z(`Cq2O4Xf4Nj$U8 zY0XuBpIzQ$N9o%{ww3r3eDTB>hxzb2_9w{7)Tc+l(`zR5PAH5V9OXaQserS~&D2f> zd2Z!Y;Bq_ClI$_k75oa03|Arkdxy&jz`>g^#{i`tM?s+u;`qgI@hg}pK^#AzKYOR7 z0{ja;0Q?JlxPpt)f;dW?LnqmLKC|D@NilAsxESEpf`Mf{pHSCKjnm~Z?q}lk$H5m$ zKZr|Yq2If5om0hh%;pG=3L90ioSlyEvw-8jH(YxD9TPdAqlk^v3o@J#+Sv(V z?V2H+5R|X{9u-oZ>4Y%I17YVm@RbL$>A**DL7;<^J>|Fh8I_{)O-Y`2jvVE=H|Nez z{=mRFbd%qqybzR^`~vNTU^(mw6)yzkCEpfB3cA5w2&s`8JI_g^#txn8RYIzDpk^q6 zoDQBVPBy?PzKhW;57F-tvpZrQfa;OWcb4m|($^vH7qSP~>m(CIyoZA3X;Sn)&E6m!B z>}PJQyiA-9jxit0j-Eu5er6$%fe;y zNag-g9thQdUJ!W5SRN(=E(^*l|95L_mxT_VDS-P{eeEe8V)Y-=lYniY|6c=JqVO%d zja3u)-rvF`MA)=~LvJ!xUOtF)L>Z4JT_)9ttbY^=9X3|JD!-jpAnC?cF=Hi=I0u5p z%w~m%-a0hN?pi!Ojbz9z4oHQqu_-pmaz_eL=p-VNT`G?-(x8wE*sRq6Ag$cQ^x6V? zb%GI1qt`B`7cT0mwS(!kc?f#7b1t-F>EP__)Unau;QCRrLPocD}YtV-8= z5U`wm4>=^~fB-m;}$#qt8&m%RZ?2{Z{&_r-)YBu?Bro~ zyvs0l%*TEnZad}!hv3*idgk6xV&ktwBZ>6AzmY7lYu74=cie&j1*F5i?nUhDK9POh z%h}gmxboS|tS@A37VbA_v&7;3XG%o-nQ{V=D^hy^DgziN7h`9Tb#cvstc)j}L6X<) zV0+dp{bRH}3zsw7vl2IaTidhR`^O~TD5&2G(t7p~B zY=Ql0AWMYUSJ`V?_#u72v-+z|LtFkXOY~rkS`YRdJs=JW!yIIMk`5ZH8nT2oJz6$i z$`2$A9(MjG*vSU6uD}fW8=U@ztlpDnAmRqhUY2r~4=2juoFZZOp%Pfe%gjB{c}+As z!Hbn3V=2sSQFBA@LqAO<`hEymhl8*eCK|KbLbdIorX$&j^SAHxa7M^7*m>!`>F|3y zLe>g3ha!{{_lUHRR2MXu<>nqvA<53&y`P}U&0g9F*W+C%00$W=8?b5USImtnJ@k(h*r}QTy_)Q)qKQ8I#u!?wEpUh` z4w)R*T)%zMjUf`JEZ4<2adr}meSl7!?Ic{aA+L+pNAVe0)x_`^mF!SW=fDz>gV8zo#fss-nrKg@e((K43O ztQk($GOTy=3Ax}@uoNfYf+Au7ZpV}6~ z=HbqU1IaIwu2|aTibyL?GYk^zNu1xiowXlNoYDL4>u2=7OQCMwBd^modIZ<_S>mM;r5YghlNK9>+R62ewm8bc57Du z(<)xONLc;CA>y^M!R&Z#Pa3OVn8E5drYaYb-Nx?6IVfOW;?u_HT^j%EikYvC;j;lt zR3+2qGjN=Q$rv*dc0TRjqESER&<>Csz-h7>7@s783LDUiWq?Z-JfZ--CIZk4DsWDs z{XlvW?Y+*kCDBen_3B~>TFNKCI!rc4Xwye#~J{u*Ualqlt_FMUnBnLLZFprBDxt#P>>r_nQ1c z_+F7(!C48>!EPQ71K(?9mdd)Q@V$WN%)SxUG&gm;fQ6Xc6O8M)i%Q`lpcNXW^Pkpd za%Cc&c3lI1IChoB|9{{0DZq2I3eUDaM+~_>-RbM|*hhn&)0kg71oP{N{$ zED=)24I~l2)H@Y(>QvG=v<*o#GPgn_i*agg*px}GRQsh=wD-}kAlT>)!T+M)Bi=J~ z%p-gf!AZ5$$>Ra*OU?*P@5DpWNW=v1%nZQJ#QWNQ7QC--D!i{r!22rV>pTs(szqX@ zx$;8i+3>!YtY^df^80qHK=>jK)GdoWs<%J}-dE9Jysxr>q4B;>X5xMMeVq#Lt4Q#^ z%Fc%ORWuadSJ}{bU%xG|MKVz~Y-6GoiKscB@xDUJNC6IKzm4|=Z~H&$G+>?R-DGOA zgPMk~(AN8JF5M1y`+{$&1wb=&-GB!6Dz<^WK4C*;eU%kWiN#1HjvNiB=E#y4?%DQq z>=z*M#fpT8j4lvDa~So7v1*X}wgDUSD}&VXfdF=guf5;|$moSsUBRhFAF zjRO)UE?{LNq{e{^urjzk3?L^$lZO3jU`Gg&ITL(%9`kh>Yk#1mOy(-r@O8q1v0RPS>*!wuWv_NCadN3!1s?pPTXD#9-Mmq9{%%RW zqxZ;q56dS@m#}=2*GT_TXEq(7alkrB!hyM($J12g+XtP95!naZTd!~b^7H*|!-N4= z*>3wTfGWP>(tvp?;5`BB>oYvKXpb@D^=NIEKhYMhZKI!Ha%X1=lUtFcpqP=b zK|iM>8yx*C!AlI|-gQQPa2_pVw~=4ZXG4Cph<<%ael4S4pO;@N0=|>R<9~$5A&G3y z(psHnP1wX1ner?FEXg@ zwGQ?8@@e!qTKjIOAtSPVne8|6`i>dQ9j&8ncpgh1!?BPA?p?=JJjXh`LGOvS`Wep<`kT1tB4rE zM%H;#2g*WF?^Hq?Qniy;IoDPpJ*IA8eNBI{LZb0K!PUg~p`#>@WWm~9yfkD4_rd&v z6~>Eo@oMIg@;R#=4jQ3J74ZOoN%ut8p=Dlf53cLGUek6S_h()TZG%r~n%wym*8(o9 z6O`>1%C2}3tMe?xUBfSRRnO|G9;6Lc9eoD1JoDjbyW&M&_@Jm~=6Y^THF4*9rSjg% z?LC!jZ^etx>z;w)^!f>YB4n(NK94@xab(kZkIsBv6R=SeFk}<3B$a^eOhD|~2W9b; zp(dKgsu%GKLXDAOb`foF`g1>2*($v(=DM3hII9B6f0ov51q|WnxgU%n7o#7W1MKff zMXDm4X}hwQzmz=ZRjq*bF{<%RYuYz&GCCIxeQv51jyoT5-7sOj^_-GYF}*>Mi_$RsT2@S=^1|Ywb@@B@&z^5vL4iN%Ul? zEQv)~Dof(TEQL5zo+Yf&vwU?2@zB0_^;KLVmowhr`o;@f=d6r}kq*~>ELW2D^asgFf_Tgx?M$Le#QmX^(|w~ zI6kD|4vOfFk**X4`XE2l*RqqEY4!|2(j99`0y;%3avP53(DDg7$w=wzVs>C&u>?B5 zrs!0&x4Y<2`13az`G`ol>seE zIKekP%k2e>AGZX}V*zguwDa?zc%N;|I1sHpZAG2o+EaA%`nSb{=2i{}g{MHn3KgZd zC*n&ORu`jv9!hs;{O8?`2TqTY7{Rx~?Y_lE0M%M}kdy^%%BX^Q3g8Pw{xSOvet z6*CX3oamMqFvd64HM#g1ud{N1qf~3xs@o9vo-3d7A^-tjnIT9l3}SO*%l;wLOINQuB&#_T~L%+y&En~qP4j)!Dk2&j00)IzD*B(F;u%T0g$r=Jw&`6 zu#tqUf{jew+703CT~W)`jLbbv*}d$(1z{F!`9`?Qmozh7HlqU}b|CWBM=$=USARJ8qV@34a=yMR`S*ojBfyPB z*8`QQEl}(p398U95aAv4Z;^h0O9&5OtHHmaZ232$3RdWXvt1wuz>4UPr8)Q`_;6cr z9purr;1gU;@F}h(`0Q~0=SN&e@OgRI=;nVm$iwy={%7w9{^y`P?2(84^1&bFVLXR* zKBJb86rBK{EA<}}0{`>&F|;&Y#cWOV82Jxl(!jltfS12osr862q&GdhiUZ8ztnSxu z%IQs9eFKJRwL-BcTGAVjQ@(-Fv${yXSx9eSamP1cWml^V_R-b!qMSbof*)O7p`JfO z&nx+ZAVjg%^VIY8^n9U|hZME?VfDP5p0Aej5b#z%rJncF^Jk=dKb}9Yo|D-0@Ou6| ze>GGWv2mf=CORPD7QJvAQOM)kik@#J3aw~w7fEhSyDVbjXFOmM647ih2N|BEso7HRY0 zY6S>g;#3yL`_u8C=bd}}=bD;y{O8{ch5y_=hApDtY&6iQjRWoX)^lzk?>A>VQ<5~3 z2){`nqEi8(VK)>&Ba*G7=BaRP9J0PZL$mMJ$iK^Xz~sVSXUj9(3ioj@Eu0|c^#N>xf+0v?kp*ztL0fyFKM}LyTjUM1tPQMIcQ#=%l4rTK$ z`)}C1?b-VPqhgBDyHy(%?NxcC4;TxBjow#(6r;DjE>p~aZk;|ocJ0tsMR$f(afJK* z>PsmjmhK2dn%Bi3=88S z@q>l&dn{wAjdjF6lWAe(cj*R3e;QV_y+V&MG|Rula=3f60I_KzR_-BI4!&ALoLGXj zf?YMRFdirI7*~J9`%bvFIaqpNnpJQ{14xIwZv?D|=5W?N1rg2mZZ-(o34r{@l}#Du z#xL`F$W`Y3-3%ZMz$#`k5Py)1E;vQQRNb!NnGuk%%~<(WWpaFOHedO#aNLOBnAL-p z`i#ezEwBGCra7w<>l+u}OOxLB1GO6Ej2w;gZ(OVnGkzs6##|=m=mBjXQhCD0>9qr2 z)4u6qSKMYJF?X}M@j7|j+&2m7h2j~Fu)pUeQoTT3&)g%nRIZ+ulJKk8!a(vdyG-CSv+j38AxE-q4JLYWJsS6JfX#V&d4pn35lxB8&Tp|5ibv}fta z0gViHfz&mS3Yd&U_4^cg;ngfz85*UtW4W`%SeP~#^&$HIcL6Hg|=<(fXZIaOF~>;_tF1H%m7;>4?=^`B-Y44 zhBdOqSi5RSkmo-U-uX8f@XiX>xlD77kaO7+5BA9}8{T*DVFN6}yOtZy7Wuwy>qD_W?M zY6I`(S%=EP(ikweO9YRdq!aJ_vvfY~9{`C25j@{=iN7qPSu3D1@V2X&nO#J(&N!0! zLojU4cPYQVS{FOaf?u}veXMv)L2J1YWyEP?uCd^)xyHhG9FHWV@b52`!Z+)}V^Da7 zc<`k8^W9heMgPQ zzs@jO8~nz!4M4+vd{wpxAI~@htcK$h8zfUz&>RSO`@^+wgc2M5)--2B1S+gtMhHus zU-kbyU}K(RA%e(XkvRV*dQ`eSTDvo7)i^_De-I!U@TT`8mhn;O59)4M`s^4-ma*pd z4rppxZ?$swL@d|JDVFO~q1yJaQJh7>6(`h7d zZ}B#Tty?{j+Qz9??zYm#x$6v%zdvM!i|U$PZyC!E%{7j<)4y$Vjk4>S<{ESN%{BgU z2R)$s@sZpf0^mRN~LS!9@b_X@s;93g>S18d$<(Bn8 zxLnIpA5a?@_#Y+_ke+Ub}92PRWZfS5`8S=gN;B`Qrt_WLqy2IJTBS|EU;2m5ycJ1QP zZ7$@2p^sdiP(v2_jIL9^j|j+wDeF93*XBZ^ZQxw_-ZR!tVL0*#7U*-|2QWov;{5OO zFDp2BfL@g|P=(0I9MEp6onXpN&`6LW8|6=(3v}8p45>_u&`r)xJzrF#WxCO;QX18$ z64IjI+z>PMD+M>n$2p*|V+~@=cvPRSb_41DAC1ZQ#_S>R0tE<5^2@C#W*OW(0n8@o zX2NDKCbd%hJ|Z-t5DH?)<7h3TqqT&mHCUfW#zL|@Kg(YufP+8dSjaubn*VUiSz>3W zTIW`Zj8$$4q$qbP6~_{{0&~BYT}0??okPF$Ucsu1IGtYcK*O@)2?5{9<;4Ij7Kaa| zxKJ&m_5%la5exrqIKy#aF?{lOnX)*}`$bhz`+Es<>`|%;EN8mBZ}{o-d}u0EkmCxS z%4&^hbYVnaCrXP0Ap|)lSVMvAR<92j`Ey`5AnyMJAD;CR1Q+a$np=s(@;zIL_&`P{#DbeXIE_CBEd60l`wPW=VjtjH~eh3da7< z59Z{q6fHm+@UkwSbeGiL*~cZ%#gVjxH|FG6!vn4LSI({Pyl+_CvVSp zgsnROQJ(i@`aNV7oHUow5~^!C!@dd6@CCP>-48&3us0SoUt#qQL(fl)TDb?p>|_o= zN$;VcHQ5>Vz7+*l@Bzjb_{<9m$#%kc363NAVKM^^N5&)Q_-$fp+vf(2tD5MSM!HVw zPb)u%+ueTqi#g*3Wd#Ly2B@Il*KX8rS0x|tyr2s%8t*}wGgPVHYCHuk!JtvYWk{mzoa4qH0eshXt97G=Y0gX;UO=uNBkKxz9o> z7mDXz!0xZJHV&wOMpg16`Il*)r-!o*ghv%nZ{&RKR+<%Tw47zP<`1HKz__aCzAq_4 z(TvU7Z9H~>3AIZRY9-$$kli|n=tUJm^oh4-5S_%gt@n-hb3`MuA)@BpOD@9m{v~6u zS_hVl!arTcs?BnACR(O6xW4W;EnbwHjQU^8oO}T^pEDlTzZ;&Xc8uw-^!2M*VU0uX z1x%8@FEI(Vj!{Td4LOeK#8dLrG&Lo+>M3#A(=ka+$091zcU6K}`h-#c1=v8T+XKeR z8Td5-#F&*4{7T*5ZLIXGUr!h-uf)_!`j@z%Hc z3thb9Sm<&|jG^a-_0%V=_pK6L+PF9p{X`{sFv)J!qvPzCIvP4QPEw4`I*yz0$8`yH zc-|Jb-HAshb!-RNKuRa=Yt$Ab+>1fL@o0y&J_cPCSd^S`s8B0(4G$I;3ATO~az(a8 zre3DK5_Y+Itxd`oxe{lM@l z{WyD`iO^LEe;$=0H_`0pqm;_GuqBVLg2$*-!1s!=HhdCn81WJT6F0iXM_I(e>3kMW zzpI7Qr&Z?2k|&f6p+sN6C2}RRUx<=hdlW+^H@n>QVALimZ(0e})VV0iNNjC-SBtH` zd^ah!Cb@>#T|kiUL(k6`6Izd9W>Usm1U^NDUoIvA@fl$k5K&6!4blomTOjP%LozT zLr$GLbcEutenP+W7N@4EIJSI(=BW@9nzS*Yg8o5W14C89ywwIUQWLr06&L>^g^^;s z2z9$lNLZ3zQfVsWj%O3j&9}e{F&?hnD%X4vE}sGIEV&5G)Ct3y3%iw_r=k~7UY08! zHTS@XgADDv!`|Hyb6>=JIA|MGviR0uHpgKbRM@akUBRkrC>J?qi@i~yq3YYm%oS~w zNy=)1D%c>xoK_@DDq)ibsZ=i4fW!%luP*!tqBOfkW52R%0X#px)Z@0$tk z3}H*snwzg5lu))5FkL&8|1l?@&}k=>2N%}=F>MbLvZ<3H^VJ=H+T^4c)O|^m;lX!jPQi6mR|K@ zalLLl_Fr7_kEG&%V6Ks_if-NmSjj{Txcyagod;tU#^JFa?1lSd>|$IsG2|YrE{0vF zW*=YgHFCXQ{^U*E!)qSA2|e7|R|R}CP2+y1aewSuNqoVNlxyUyM&{j!*!`NyDb(G= z)WughL{@}Rbuvl&HA$K8^BPAF>cWOnxa=K?&h5w4_a5Br2yLm^$W9b2(^ z$rbpgW67r=adxsB>BrD$2L!S}hhs^eWEXKuBVb6I{0*+JCii85IyspA>g;8+!BjOI zS%B5FJ$X3Hd_}mZyrRMNYLh@jm(AdgR10$w+`^nQy{zlkEzsZKBHv4GVfq-xpq$_2P159XFOvt>>Z(( zof?doqPya{fF6wP9?=mWJeWC&xXe>LW1g7Xe=n_tGtxpLt_$dSvN`_u(mDRTL;ie; zRHN;4)DK*sCI-icW9M6PHHLkgjkQ;v(5&mZ=UTN*g@H?;6J#zGz#z^4*%tu~Zr#M6AKEc)wgw1#|{EIj4c4M_0c)V{KYaH3)?YfWw0xCEF>}Zx^T=!-1 zd8o*MkQ;2MuXA&$%m6rkuw<%!m_Zwepsc&2~C8Qz>8}jiRcOV}tqUx;%%vuHe9?z_@&r!;P+!4b9iJJ2oL^eW?(bfki34rCwqa|ZFH~}zfZGJ|`Mxao$d{fmLyBYzm_gzmHIqPR;Hv4%7 zxdHlUX1f3)r8I!JG%1T~p(FNL*vr!z+i{l0fxr3>{;&BD694>@Z-RdYOKJYo(sRH+cWC~3!`b-f?`Zz{sZ9QPRVM$u zB%Oc$`~MyOdF|8h&p!*2-#3?@3;y}Bt3Q~3-uhSm`Tytq^VYvW{&~?~`RD&D{PUte zOa6IlD*t@9*XEyZ_5Nx6vyJxm5%^~g41Qn!d2^SOd{4AZlXz0%7>dua^J3CtH8r)gMKJPdy z`+OAa^XmU~_PN_;pIhIbecr0Gsl5PDgOqG)SE(4B{9gfSz_;0mZZkLcjrKbob}&w^ zej2CKMw^K^-saA`;8BkLhiXrT!E^Tp%x$R*H{Q)s?&06!yvQewHBSx~KAV|VVqW(; zB;gk2g9U0bjS-PF#Jr?WOPAv;5j% zDRKD@oo$4T0_gf)#mWnZt_GPT3|M;R)`P*r$wvSDJ5#N5pn$3Jz zvn>>~^73s47e>#ff&&w-D{Afv3EnrOL*sfDNaZN~uku||cjtps@_aFFl)Q{TmyMK6 z%YLN$y;P8A{xHDlv8p%f{80f$OlHjc;t_JLdaeVi9nSTU@@jKTq16+#|Jo37X#ulkQZ1eseH}|x2hdE)42LY%(Q)|`Y%08{mfPA20Xd}kKKUh zPjA4ZfXNsTXD9Pis=fp20=8=rKI=Y~0BCw2;q$3O*sG1e#Aax3nnL3Hv*95+0sP@a zOc(L<*E^E4b;(e(HCfX$r8n?dJ8N%{-3P#XwL6f6$G67-d_aT0c>FcZ`8V6xOE+XO zz%x9nSu|#ZDq{bvX&&O~ONh4uX!pda*7YSQ1~4Xi9m?*FWcNg@NfW2hi?G>*DwFt2 z0hN0%WEDKB)+db?F0-CVSL^X!_aVl6S=xBdI#%Ha;8g2phmAR-uoISziZec=XOV%O z@CTm%vrqNN_(yJx#g<}td0jtBnwn%JTRQ8m82M2fa>}6GJSevwl>5UB%KecC<;EhH zwR{u1LCSRky-T+vdHP3Gs8`sx&3GJO%i-F_h<7tF;1T?H9O({e#q17Sw>bmZ*q9Sa zQkQ$l2TVT*<@)f;J%w1{NqF^}_H6@Jqlj6JiOg!0i;I95s@Z2C)sX-fj##ZfXXj+D zIFV!^^yQT1?|i4kk3GeQc+PhhC^)AT;(Cl2Kc^j0=3rd?H>oLvmqg9Iq1spB2OU>_ z&}Hz04uOYA29+sqlJ9_#ecf1nfa6sEV5~tzE4YMir$QC}WwzoD{!<0N_Ba)H@XhSZ zyfx<~!*CzYmZ-*wq54h6iuje%UxF_51Y*sK1HSi``$QWjUeVCWNlCQU z&BI+I!!F(0yFG`&f}UdM@tT5Iu=ySxRV4YIA*RH8DB^7jW_PFOdj<=AUdRfOBwGoT zde?>U>!-&xoK)S_HIL3U zt~~xg)*?6kd+34T@W(uk(4!jongaf`<`^^!Ve1xWD6x?)I5{@>i=arFJdPSnZ2v4hOdPHP7l8d*-s$Jk-*JmQ6s!0i4{ z(a)S67w0?f9~(0B{;NLeK4YizscUp)damhsR~%L)>S$Wc0U3SBO|_JNjU!^?VLq@n zbL@J+Y(n;_*@(@kq+iOD$+aHYfNEyV6sy7cqm|VlceDh#hE!~ZM|P#k>I!9m(&WTF%<2fN8!Z^5ij(>74>f;Vw!@K3Ns9i6V@-(~gFSEv zDdl2!zyVd^wCxTsh){1M<`w6y7$cFkk~tAamcuDlh<-<^^G>(6N%V;Ep^*m& zw&-WGLtDhVC*<7|@wP?GeNpcr=HC!G;^RFyWaLP7R?-x}kt0SWIPD`)RP=$jCjOl^^UZtkwMYA9<5r`VI%GV0gUFT5j$pA zXA*^`)M4viS*yw_{i7;iMCnztl6qB#=vDbpeBBQ>div#Mt-x%Nppm_2I1&~#){(v- zK5O2Og?#%A>tW`vT6;1ZTi6(&)!ON~L6g>&$!WQ2T*}qIat|%>ldR0A;?q4SQQ_t$ zZV?6bWQn_(ENN_%!kZpOP!^P)oRPY9a(WsYX$cWJ$yV&hqx4oh@~tPW6_3o*WEJzH z<9jBjXSW%u6+1C)dMkzUZDCp~D%I=cj8v~uGQAaT1Ue}TVHPe6%r~1AQ|I>(&o3l; zn$E*CotRfTTD>iLLM5zG8G`hLj%5;#^~6fizoa3kP`%WKphDXal(Yl9s8{JJW<|aZ zCHq9W--2d6C(_xDIFy_Mj$63l_4ANo0!fSMuH;ZPR=#|Eu>O3b@z@)z)#hY72+Cb8 z*M4;;T*<4uV0SsB0Cpv74kFL#C4$PkdV=5rf)b3fH%LWsULP=4{wQ08j4NlCm(bYD z#$&H=V@E`V&fX9z4mcT>)YZ!&IvX4c17Hk#)4}$C>Q3K6iKayaIepUA!|@P3s*K8$ z!hKh0<<(rKa(~}vMUnm@>pu@mq8kL3e4MlFwDw)B^y>!d?dX+Hdt*1Ett)i!5w*w> z7znuFr6hqF)%;kkB_ufAg#@QMOdaD?0VbUY=V3SG=iG{|MKni%EB15jcNGWfG^+%$ zt(+8gWwnIhgM_V2!bYau;=7ZfMqL~Tf!ax``XVAlmfXw9zF&8@3WucK6McdDs<$iH>pG zIi%dE_wz(__FbPc5!D@HZS0E2ChLjdSPG8tk@tGileWe#u{C;ly3q`S&DR{%k?VN} zJt}fNui29?YmdVzy5bTA6;sl8pCVYVv2wpd&=a<iUq6zB_iG@8H14)w|~i%?DSNk*Xk^u6;FgCl$!1bU;>7amL)1 z2G>{BaRQu+1+R9O?rvifGGU3-m6GKM5SYQ^M0*P+pjpA zkEd-|*}#18c>+?0+PYlpD?PlbCeyNdMb~U^Ic(_{v8dE;8%PP^;qybnq`Fj~RdRJn z02XcYYty%Rb;>s1m)z#n_BNM5s~0(dhq^cConFvp&btz5b*lgh;Kkd$=q$a6eHKkz z&4YQjmq*DTaS?U&Pk%>(tuVZIlhIa4b~;}LTN#xxVMy$uaY$8Hf9B960IRD%IW)CD zd9UKVCVqc%<0J#EQij$qL+c+jw0;>{f7;OcWoZ4nh&{CaG7?710awU0phZYmuxP zf+KLvyjj#p`GbJ-+$fL+v&m2b=Xyij)cTbs+ZLphqy*3$ z$dW4!r4D=05Uu7!?X@=F5OJ?e%b|a3$W1GLYeAJ%u2Vc+-ZLXrLM2&VSC2=PA{@$eT5m@j10r(zabLT7HBW~UH zJ!;avN1MXxxJJ8xU85&iV^Zqao8f&_&HA&vyf2M$*r{)2u1Ub<%(ok9 zQll_D!ERwd3ePe_~6} zcT9`w*Hqs(Mv^c8f6RRgd{ot)|4be*K;TX=>{5$0ZDV(EP-!DA?P$%wjLzVU#u_E* zCO|i6*O^{NGXh+ZU{?rPuK*X999c!h)j3D1D2B#c4`3Ij;y z|NH)a=iIpyf{(4+{r6MJoqNwc_nhDPoyYI>{cW%WcJl{$PL+{)L%4G5`|_wiHf8nt zKN5e&KOBGd+4-~I<{S5i^XIXYcrE-%{AL%gb6qJmwP(E5oO$W`K1%M4r+nj5Y{}#M zf6A7}ge^mr!j_@RWW?+ZAtUp0Vatu*oJfCCiJ3dWm+NqV4YNCO7a(9F-fn1*v^()e zxD$gbnLWeFrrTmuo@77bS<3fP{D}8xKjK-IAF(^#j~JwOC9LO))Go`9*gcFZPq82I zEODtU=Y*0Gm+~W?rF0;-V=tSu}kP$-+Eb9q39^2Reo`r09+Y*)a5U$jrX=8xx7g zrVVjIX22Cj%(Z-A3KqTvqXB#Td)ou%Dqh^Z2Lnof9**T5#L*C^t-{mn+WD9>7qB_( zm-B7922@eyhLE={9J`?<5-Zpyry!|6Y;K_07tx@WnQfuW9@t9rBbfvDu1EaC;)wtd ze#ow+YA_ysmz5N!wU+8>hh#LJ0_PTZ{_PRx!?LG))RP+^=wEGk&J0SHqaG@Bl4Raf zM%_G#vcDiWURrQ(@cXFZS^Vt2hpep6^7+^K^Sv0whdApq6CsR0^Mq7N^Ql< z#FWcf<2SK?p4M7@b`aTxXR+I}6wq2f^rVLs0~o=Hth4fpuB6AFoEfSTLpz0zitw!rP;WZjQ zCB>WWDOYN~LkIpez5q?XQ}d~2QLwAwzH&rf#j(>6c zMeLtj%uX{4*lA`7s|Z$U6@m85eMxBs8XTetU}ShxWJ{P!#cfCf40^P5%dWZCoz%*K zt4>}7-cr$Q`Qt+7xB24+l|Sz2UtjJ>gT$Yvy;j}9!UkWbarcPWpndRTffvqaAqn`78*6z_vOw|0 zX_=J_SRE{%8}5wV?e>_1vF|}h3-X>jW`n$k+Ww&-ewbBwIzFam$Ynh86Pp@WRch;@Zdk#Nv1N%dKp2~Eod#^r# zCAZ^&Pid#;alyh(ezUXxvVR6goOM0m!msRq9*G&~e}R-fO1W?oFW7w^aLX@_+c#!8 z9K>F0XQ%`T0%{`}NTGFMX941gH4gFXUd4Z_3YIa_BO^O9tHd)?KH6?SQzB5mmIon? zX__m-|NQeZSa}% zy<@E2kRbs3eN4uMzzc;(wSwftkLm0=N8HMg`d@o)D9NWTRyG;%Zu<^PQI88ejVUB% z$MPa}7dv{I_%-8;vXB|IKjQHtz7CiT%okyEXTaPPHn;WNa~|zKQZ+GJIhyVwdu+(u z(r0{;2jq<39Bn*$6&BmtE1hCC8!#T-%C)xe@x@q&=(~{jRLC2Tcu(2=r!M0}!a1K{ z;|x;JBl~i07BFdZ^FBdGZP!cn^B0doPQI4vO)<^9lak zYT!Nl>u+Mu$0E3;En%=Or0QITWnq;w}q>?Ox#dgq($7#POF=#R<%@r)^4i$?3`H6W$eCbn@{55=t{Qfzz_NWoH=`FS3FES#lV_nw8xY|FhMLwyF{a5 zFz3Ovo;xwj=P8ne*E3}>bMoPoS)}YA7tu(v+NOThJI-)k%^qHr$_!vG7L8Qfgw zsFzK#d453}c6cbHa9Z+FU2rKA}QF3fC`L`9x;dT zPi70gN}Y#XuOI@;Y;2DClQ;et2ck^+F~W(Zh}Z`n(xIq1V4fug8lxGA`KKTJ(`~Fh ztT|?T`s$$Lywq2WM_zO)RP2A@h2^Z@h@36lFwHsp2UttZ)tNwS z#~^4gJ!4S97q?mY)b~x(JNsqt8fP$@y^i^58mTi&>H=F zmQ==B(SZ*v*Bq4{q&G(KG5cP4lUV}L7&+a~R?Hk$LQhsqpzrR$)tD`?!-<PVgn z!oYsRL4*%&Z+Pd1`7Zqdeo5#0+%>tbB1z)l)#fDAz>ihPSG~^Komt7e3g-?PVy3K81hq+IJ1@=<80I|W|KxU8Mcb-FRBR?`=&s_%k<#D*Hl>uhTNLjlNl) zoK)vSf~o`r>5q-{o!Bjz@h7Nsa#FbuA~%=(j4l~6x7!ORC*4Ga{PYehd36RYlFv)M ziwgpNb5{U_Ft? zi;D|NjM^qzu;?me)MDwP6|K~_xS+_W{df9BSLH_Sv$)@doNGI&%9;x<Iphhn$5#b3a1hXyz=M}rGs zFSL`Cx4}4PJoyw<+0SG!q2 z?NOXj!~$>Zt|Sji#BAkpp|hLfg+rPb7C$jWE8%vtFF9$?Of>`@bW!_O)yMT)iteR!@pAGq684s!*J9t|^C_`UFFIR?@7KIl1DU7T zZ|(`0o6WO&Gx+apwHfHeye_p{>+t@uu~}-HU97ik+Ls%!FDG0Nlv;aoX8N9dleHfO z3-_V$dDjD8Nh#X5`}6Xy{+>2i-#f!ZYvihaCjW-Q_j`i@aNpfD?@D;{kWy=D}rE`{S)ed-7^j>&}^NMWkWI!Cen zQ*EMS{pN)Loroi}Cu0Xak}cQ@;wVSUyB?15{+BZy>K>yk==y$=+S_m#XGHpl zY|#Asj<3AQ%6llf`bBby&GYBV-%OEOoiK)tOv+_!oAwn8nC}F<1C|#aYDLR`1NN|- z$jtMgRuKcO)kg|Kx5TFXi7__`cROS5>9DW0>ejGtL)Fbl7r7|pYpl8<>gzRD{|ue> z?Kf8cJDTL%W32uM@KZ}`fEml-_inW5z_%1)CFK-Dj7{AZ@EtcE`-&()uc|oWb_8QL zJHwSN;n+7PM=K8iu#3dc*42DY!2cpsSJ3BUOPm<5z^$>o-bnr9q9>yPS{8~e-Z(5? zbfVukWW-=>VLXPb`~1if+D6M@7)p&kK7KZb7|lWCqJQJXH1F!D*%k5b4%ZyZgwqE5 zYUKO?E0%3PnKfW|;2@q@nrV38o!w?OvV>!Jx{aG#?N2%aBx^vnF+9BiJoslUv~0b&CDa0lFO6n`imil^ z9bUX=;-3tgTVS?d(EnXJ2;imrC*mQ^8EAXxeG-7)MqB;9gGOw;iggGQ?GD;#cj98& zA<-_;4c3W^NVlHpMc-+I^LxY6tbpM`Pa74@ z#m9(7C_hhPJ{5&yvz*IaY_@KUPvluNs650F+tGqM3;ZH!GH?5nEyB_;8`jGVVAW_R zN#gj#8ab(ilg$C&3(A@}_5z<_v6kS7$l5_#FBb87(HK(UopR7bh&v7~*feOh_wbSB zm9;}j%e-g~Sl+I~Esb=ft~M{L-BI;TEGJm{lACe7Ur@LSwU+{|j?)eB89D6}che^# zm6_&1sIml-z*=67aN43tx4UoNC*6*k6$K$j)tE5y(f;^4IxwBC{#gBW{b$Z4lk#XB zlPfvtVR~5DP(Qjp(`@PAdroicJ?ByzyM1vSW?wQp)y6X^WM-cUPjvhmDm$}iak4Fz zRoraZzufe=haOu`lt>Wuhm&9}(rX3{@}fG+>QDYT5w=+Z0+q4jhrq_y%+ghCAIT(nLBj8GI}q}9a2p>Yk0OMfp#FF-+>2B^RqHky9?8kv$@{1Or+eqbPl;;(o%0}+COS*+lqw|tFW20)x{7y-b zMYoQD6jb~1rK91J|IP!msLAZ>HE)~7N7;&hHgI~<(hhD3mmPe7lScZHT3ED!WgFl; z(Ll6%>?CYB#LLu}c}=yOjCH|u;^kSy>$LGh08UR>VeRzWWYN$49`tj62>NZ^`(E^W zADZ!@#SH%M|`bD>~=OS#u+Eqduk3bXV^;znmNw2^Uz5AX;F9Tdr)`k zhoJ6#d;U=BPWaj zyYVWu8?RTp@v{zb9XZ5847_@7wD!S5+dimQ@HBs$yGbbLB%rWqx_Hx?Cz}=aWM8wf zmX8jw4KS=tRBtT-K2cN^gm`!QJPUR!V2A4}OaeK^*JC`E$vo3+oeh7@r}V`IlS8pA z1DnT}@)-)c^58r`~KdRPVPTgX5*6}5X&=81!7$}PRKvBZ11ZD$=8#<{euTiOP$vp9~pk~osu#(cz?f>|9RgT=ri%mfHM z+1bBNjr}JTt7()NcZt)&Z9(DqB6Q=c)Q!o57Ag&z%qfBuz)S`DrIG{7>i|u+B?rDF zCQN(`k^(2J5D1PG!?;L(cFf3ta+!T4DJO0BH4Pnf#xC5!=k4eCy21Rd3YUF`SF)Ht z1O8+1j{-hrH{%Dc*v(0NbGq?w-fr6H<1Y5Ky|=m`zeEEvak>VN?ti+`y<9hnAOD zf7LH>_mZ)9H?_Z;Co{)9b6U$?2+JwijR>AZMs)!K)psTM+)}ZdK_2F}N&)eDDebg5 z^mi*93VcBWKW}7~06>QOKYREfa8Ln7^eRy7{Xh^f>fTfyZ-tgRcM>L6cGUuTItR2B)p##-mMK<8PS(Yk$je71iG)NvZ!Q4)MVr z&wb};q22MnL*uXIuH*cMEW{O^>Whf?rZT$sbaz|-UrUrkheDe0SV$AM$%AeEVFfku z2a~uYEmOIl^4Hm1i8Bx9+lwx-5m11@D0rO{q)~!{c9cCZC;v>D3RAy zFJLpHvhlyhhvxCr?&g5~U0i9&8mKmhdaE9Pi{$oXtGgw$0XLQlsO7)gijv!Hi;}a_x%Nsr*Ms=P zYpG8xJiz%}52k(MpnRe?p$n)_^ji5`52o_D9@5RV-oT)3D!^AF){ z2gBysh*r^>eHxrFKxZA=L>C3vUN{}9-cp7pv_nMjslJ>s2^)+xPcSCo24n5oG)zK) zvS5MaFDEFk*##3sGFhM{liMeVGIEjjocr%^_Q$Oo=Pi} z!V{EaJH>9Jqy=b#0f2l1LIKc0kl+1?wZ4Av(r|($f(eL2yY+JaKgpZ4kDH>RovZ~K zmV@}{SixCNMmRGX?oyX71DS(r+&rj{v8uchEL{2pcj^Uh$E&Jk@`3_8?21poVRJj5 zx9H-iK5g%nBWI_A6$H)G1|n3sk5T4enI^jbHbr+F!!M>g9m7HPtI{K}%Q?Qe)P;08 zMrj{FyB^gP{=}flHU!#IQMVvXa0mP1_kxzyb8SJ`qCQLkGrETb73&D=6eMGMjPe5s$m+=@nKb%uacH=mu z&V>wLR?tvTR7k6`o5Va<<{w%DW)c|L$L4 zb>x8GfI?6!+sztCoSg-w?XfKCV~~-MtUfN@mcZ%nfYryp%92Cm3594`K}R}bv+2g8 zhxvHD&%v3?YndN#RHbk1GD;@BxPjH6?F{ADo{;!yZo!6B3&v*&l?&Rf22^e1OyO@> z4d~BIHGp>eiv7l;zu_d^x#;3b~2Mn_y%=0jc1-qM(hm zCzRP_{zgq*?Dt;sEraoLYGx%qI_&g4@0ag4enBHUef-l*$QIobCcb=#E4 z_d_N5>;s{Cm)}>tSe>s20qq6Et4Y`sKHlth*tzh}twWb!>2=2aDE2lr0nGTx5txw; zOS(TF=l)ERk4R`Ymb4i^qY7-|3WMUcHmV+u|4+KYp#L+(77p<)gFuJL-^#`=NE!?h>15q2#f=DNfhhIunc7QAEmdd_| zU7^5ZW7^&Qn{1My0vOyBe*&HOgt`-C3!6|xY3xfk$Nz+C*$z}jSdF%@7+9_tI(u7# zv~kkU8@P=P{d1LIc$wx9k8|cNfUL+jw}L5)^xvFR8wdDC1@O1$}fIr$w_-HDHwQ!TNCO{t5|3 zFHKR4rIM}j?Rp(kciyx&9FiiA;oDe7+#b(Thh&FK$O1ZLJ1=+fLVWgW9yRfdYF7`H z(y?6)87^!iHj$%;hGf7T7%;^V05wVS(PBt*1X5THNyo$(lS~bkz#$I}8!L#DAqw*rx&DO*@p$2Z0gRVb|H59rd(dA00jVG5<+-Z=;D9CS57>t4Qpkw^t30T> z#EOaoiD6yR>I++!M0;^Cp`Yt6jikLeY6<`QaooH6H*Q8m#BuLSbj^^6t*i)W6l@l& z`~Qrpt-q24QpmvT;q`ysdzOXS>D;4ePpGn+M!2#y*~+4Aeugrwxih#ydDt@9 zq30RSt`bnmLif@u#0#5PszquNeqXYcKwM!vc@GyV%O1`BDBaA_%5B3RZ=l<>NA8p0 zWBM4k2HPN~yu;@huX9q{%c!PovsH;za0^$*haxIm-s!qzU3i^~xZl5)$?soFeyT&M z3iyc!^^llEuLcX>LeFz67U3Tk{GsUYvtoqA7f3c(#NUA)0WTPHH_02%^Ft^L8szgv z?hi4wFhj3zv>Lo#&3_na<0bos1Y7WsYe8jodUGmBmE{a^9yxT@@z zZZ?NM1^j~84V)9MSh#n_JHK$V4yPzXWp@}*ey>-Zq9>8c=E8j}#~xC*ueqKo3ss(> z3EZdiYvr6YJQs+{H$IEXI3yisdaQ$`H}3oeA5=?E2E=++k4rO{+@??w5G| zN#8&6ISv%LOxjwiG^YY4HuwL?>Rb16F^Z->b zyBzQsbF;7gB~RQvd_6WC2cMi2=BOK>pCt-`GTU z;3c74@FjY@`T+Co-0Z=h+i2&p(QfSlLG+i}5bd1(gP*0y7t-Ww1Nm0`Op!07cj{cc zLZ5k7K4S`fALrk!3zGRpq$_696*K8V7U_yD(s}Hp>tNDhcj{E zm;uw5e!fjV2UH?oBn;{hqgq)jb2jm3sYLv9j4Uo7j1O$X;pS!W1sO3Hq5f%p@}%Hk6ctqTqZvqgSoRLqL;z$Q zXS-cl_U50+%x?($23F+wlN)Jn=Z*^3v_N2=v4Wm5h{YAheu{Gx0+X+L@h5^Ill+OB z_g=VF=VMJ<_XGz#RS)oy>5x+$nFgG49Kz~_<4|#i9EYBgjaYVD#xIsVn2%R#mzEuz zk5{s%dt}cK)L)B}6wU(i903l>mgABc<3mm1*1iXx&fsGftmAZw!r{k1Pu#8P81fHO z0GYOm4GwGIX%z4^2Fx`$#F^_Q8SQP20pDi&vpMLaJ?-{3TH3@M!TwKo%IWH%H=(nS znkS>?A@dwVa9pPC40yty{PX78q_L(X&*AW$H`Ye-#Pi7tk^O9*IzM^xhNU|C(>!r> zS|pB6F7vbee%I9gB&F2YUa5#szFjOg7+05pU zvNV|IVGUuI{Wwlu@Ch_eMa*mN`8XRNutkaQ)E?|keC(Z@!B45koMYQ37@>hX|JZg4@k%y%#YPJ9+6#<&X{?_u7awxvNBjs zQqGi;D1J)|archqy8#*uBJ};7`Wx z-TKGiO&GQ;555n=oB`fI6?f8)P5nndV)leC?W5yQbnv5PP}C?Kx=0!85O#%OXD~k7 z@J7T(?*D>2K}2THCshMOl|x}*N;`+GkqK(B7;&=_!hII!aa>}zaixRW%1j00X}RT~ zr~LV_UcrH-TM#kN!jP!gu3Q={u68oWiFhTtbl3ays5TUNC0|Q zjT4h#hm6w=X;wryT2~$jnWuQn%nX4%8$MO{$W#@S?LdLcK2^89_f%c4akd2|yO^mH z4_o-rC5*FO;!bETWQc62#uVJPBcZ#f3SP|9O&=I#o2P7pnzFt4(fHAzY)d;nI+Sgm z`OP04%JzOeTbqRo?zJ-Z<@3mY-(ef!to;#Ahj{6UYB^7Le{58MBM}~V&mOUZPsYr=^ zL^-A&1u&>l_YW$?T}r)liu8=y9Ys z-K4+MGU=nSsQKc?BJhF7h2M=U4vA5x_%nlMi2V_Hn0~hdtB8mqP^!4 zNr<(S{r0ZZw*u@a%F$M=R>bJEu_1gb8^T#kpVzK-6U+wmdKdQ^-U9C5LCV3KdpH3& z5_!*SLP)w%r>w;H)!Ko-H!T0l>aXiWT>&<7&=?uo%DaQP>n^L=c?U5XdA)-D?U7rs zSSo9`?3i14tf*j4FOS$H9x+4JFOG*iaPN-DWiD~W08ALPzszU7V%INH#_6}|`K$QU zk>!=uU!%oioHpfl#ph$)$g{UrnSO7U)q@{Ey~7RE=GLZRcH)m&cH*7!@pe1$ZcTOF zWU6l(eWDCuYp*LV+iBE2p{&GjBe&dMHWhEGGSIdlUYP<{%meK5AuTn^dPS)LRqwn# z7_+4%V7LCIQQ=0w05mNvk}PpPG-Hy~fl2+uOfhS^qzw=P^nj*VCCx zpBRXL1Dvve8GP@6npXV6etF?dF#*pu=12Aqq}C-lnj9OjypK^%ol=hL$_MQb<2yy{ zaxZ3UahFSHJeA`n5mRuY+bO`#NK(%u)RrznIJMAZdLD2h(@OEXgl%5iOdW6T2jw3X>_rXZn<0*+ZuBl-kcY5PS7x6H|QNUOvO9WkH`|EFbH0e1>?JS3Om`pY7$i zoz%7C{J~LzdGGQGWXN)|epd<(m)^CPwUrT3TTp=iq3P`rTM#h0+k zbCtFduh&-MXSJ31C2b{c&{pCOWhFkOti+;86D^wsmY}S~0f9k)N{<6+zx1ZI6Hk9L zWhb^N^M=`j|G)4bpMKEzU-KWQzA40v%NX6Z@7KSjqb#7Os6+}c;|kidR!->qq5UC) zEXkFErSdd09^{>O0THt1&C-wpV~m>pz=J%-6f`siOZ^q|72ZGW=zxQK-YJgc;0MHu zG71pLS?g4||0|pl902}c74Tn!t$xmEg9^=m^zRjn#?2Z=;})j^;rKENxAy^0)|W6M z3Ezd4mxmNGU*TH<&jc+v(3#>l!j=6ghX4c?2h0lrZ_*#j`a%l-|Cm8-83?iaSM26Y z_@Q%o864FOQ+31Yu>Od-9iV{VcyBuxq$t_Q04q^*n{E@p0u}f_5bE;#%72qgntgPN z9M4E9cTo$>jq$p|xBRi&opiRi0saDCcp3<4yCan?Bvb)~#q5uGo1>X6_pawfcrw=6 ztjpzOn41Z3INS z<%dWbtG|(v!U`SzJG3MXD)cs`Hf(UH0EHVfMBUE=vkklljtUmO7O7thtnPeG6T0lX z_os07=!@k9F^L)Q0Bk^$zuE6Qy>c|sXs1J<)4&JNxfe%#$?EKo*&F|)x1E^8Ns~wO z&trWcbq_Q6qYklHcRlHL#G=l7jr$ryG2spz)3H#@^n2f7b_fuI!~k?;wlYAW-`s5i z-*$ive))aB`YC{ZfX)9E%$7<)n+L&G`&swWF5fr!`^>*ZAzyFRn2@<8Imt&;Vds5g(jfvMJibjbd67jCxn@33I! zUmZ$K3{9Fe{~~kz*PMvgVZ)ip`IiE|2J`z8f>Xa<>}L%DUz6G7H*Rn6`x;k{qhEC0 z5u4>~hL?b?Mz*Kj#UVb}4jEA3kf{*ubJKAC%CXk&GuC`#qJp$vI8o63I`mQ+o~Xih ziYBH~(ekQ$-9!-$I13rT?LRS5>}f?}Pc!IeUw+a8x(5oabZy_t&uN^%lud1BnoaEm zjUQk&Q(MpcFsVrv4ZnE`%W)fDo{yL_E_Y01TOej|&^!;58V8b171jjMo%i?a*=SbB zFiFYne(&$anD)zMP8HLmpaj;c6Ufw(f+g&CT=0ZjkOdZ4$O6SFo0_2Ny%nw8$96T~ z37ls5c-D4RXN+WuSZC93ZdPV8##HM<3c*X5$wa@&T3}1Rxq}X+XI19+3_fa?I1=&e zZCZAv-$aF6sKO@O#khb&>tjV6ZIym z?LU1?yk&_ zY>n6(pOYfCMDjWkHe!q5$4rUm7O@j4VkgqZ?$n3l$N@V?p8V)Ia`2<&$WtFZN1jUA z#>BwlunjFZ(MoFh-uAG)@nFgx#>_ay8UNf!_Ati?_AsZtUCfTB#9AAwJg7}S-@61e zb_z3A{STN?<){8%UmuUXH#h2tO3jUZiW^}_DDCH<%8!H_kA1Y<7*CN%2Xv;9NSS>F zyT!8lYCcR~f|wVTC&A)jyjb;r&5O!&KxNDFo5|#s0F1NWg)?-s-`i^CpVo^y-8Fx( zmd^>THGNs(%I%?Kqb&)wYJajJ9Lw42kKOPH=b#2FAdyP>9m0ciB(JhxOp_qo7#JbX z=^y}3x^joCjV{fFVO{c~;or*%sQsKw5(izs$|H&d)P9|(v&`bc9{ZZON8DP@>Fq{X zY>cE)mhA>+1r9f{($K6iB!H8pEhD_y0M~B9`bOFtYV^f?sCY=-a*(#KU zag5tmp)8DJtpZkssg-ezePxvV)B7a<^j{D+VzAda#1hZPS97BpUmCSW&Z{vSm@3xlAXytb36g>B?hwY}$vfUWhwi=HjG|#B*bns&M9X2!fvThUl z(H+_Y^9MSe>h-;Btk}wz-OKi5egv(o(u#{7Qt&I7RvhaW;h51>Bxwg#W7K|5 z0!$nFuTi0Z!@qXDm=5Kpm<~586HkP{Ag06dy6W9xfckyPawzeBLN9GOlvfWco6f67 z-JdHHTAkSXb_?*fgMf2n#V(MDPNW=NnPEa+Uxp7^KH#N5=Ky=<(Z7WHzA4oE`b z-&a{JYU36G&$8@CtJDZGpa%o`>=C0LA~3l{>)F;(@s4JW<=?9QCN*cR3{@VJrZ*uL z4_T&4ZG@Bu2NHBN)zNH`UV($zFWwRd5-D?rE^TqSMb|Dx*R>gI9BZBV8(~k9El)pE;0F4ipi0_wpCc2)!7?_;Y;@RuW@Mq7{wx>_(shs zY}eT6h~FbvS0k_|ROTB-W>S=Hwe;v4tnmAdx?0OzG75mOj)g zfp^n8kQmm13uzs=;E<%Zm+Zj1m+nB)>cG2J2QKJpN9aJ6tpkHPWiQ)Qr<_sk$4>pW z)&5%5{ws9*srm0%&BrSLjzcUKr6V;T`}^CMSR~vzXDqCox5eY#9e77qIzr>u+8U31 z!+)md!7L7zl-a=a-)gnrOs7AWE#KzQJU^RbGUJQTfeJlQ6ho`|f)UtyiRpqKZ?}eE zt2G4j0eojzFBpiYyvv_#;v6wyMC8Hp5>7ToOy2SL27HgR1;kj_b5;y4GsWN{9>bn0 z#^+hg_&gG;SI#CFjwbPF1&C+0m4%qKC?3(OM3kx&8tyiSoaH4eu_$UJL&tP&W$_%> z`mj$N9|qf9J`At){|EbuW8?pxeZ|cF)?2v_a}^Y+H4QhtU%cU}cRAj0RTjq^{<1Sy z#T$08>;Er0GCAHbWY+d%;wW<{QeVBy91PV5J?5VJl7dhw>JX9#ROGPg_)ZyXe=Vo1d@l1z-FX-lf8Kx%4 zu);xC0C4aD&QbheGY2q^|MEi|xcJRCbFt*NnQNE|jW?;F#j@=2@{uS_i+Knv3nBoe zy2y8M5wn+h9(^YyvPc3J8nT$tKs_C>0Da(sENmtv zK;)fDTMH^NPX)Xu*>!$;7S2~K#@v1NSNds2Rkpm6;ZV_HZd8CU06iRTsAy=6y zG_0o2eUOS=AE2W9-5;c;$seSq{0~x-C#|NDs+q2;DQ}m${2Z3^)g`HlRP87{^Mf?3 z=z}yY^g(JW{vb7#j!+Zv;7Ii?Nc9zaHXiKa2a9kB4n5SwL1@5$LGDeX_JIq@WKaU| zR;fu|%VF4aH}N%g1)k>(p;#>gYsGFI#W&XQzimOYi!as(5l)9!up0$U25YEgW18UD z<_2-fPw>@KL4DQ3|31rw^To3zx30KgC>%s}DZUMP}-fJ ziv^^#{8y3s$&va}7yaiB#fpmPZx8)P%XVoW{Wn1WB{=tKHx(pWw4pEznvcC80pVBt z#|qeTflG3Y=erW{y6C%zQEjG0zc?Hdyk?kjUg8BRtATw~^RcR#aV%C>xTt)za)?up zuzLs`x8Fu|Vf%84Xl1VP8>LuaW;ppXrJVsJK_QN*UPT{uNFYXTa20(P2Zm$xsO*{_ zE_VwPfqK<55mH&|myEw)!`v5kF{_+zL6R0FzN~%=1og*eakT3!BX=#|0KbN7E%VM<(w(;r4q{daw?Sb<^EeOsvlDH;SLKUhlhxx)g~s+VJr^-O7co5-SoB;pXpbtk=s7Sb_6 zB`$?~IOddET!g|hdvhxm4C;VcmD7MjZsBfCz0$(TNJ>1rEmBJ&?sPl%Xj=n#W&hp;afg6J_ zr*8CwYWG!-{>^8pmUw0;xh2`^8(Q%qzEeGuBQTMNq9prrbP>s+jJh8=86;sIl3X

    IPlCCYj=`Vt#Y;I&%yr}E(uKPBICs?@ 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

    gdRa>+ep+<(`?3@ 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 GjQ8h_aJB9Llx7LrH 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

    1$o_ns8 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�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

    n@Q9P#TzkJd-0T)Ne(b|%PuZ6u@ymIf|EuvR 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 diff --git a/build_helpers/TA_Lib-0.4.18-cp38-cp38-win_amd64.whl b/build_helpers/TA_Lib-0.4.18-cp38-cp38-win_amd64.whl deleted file mode 100644 index f81addb44a7c943135e3a113fe3a25987af8a23d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 555204 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%0Ap6GvR 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�Uz)P1LKJsVu9Cm{s{1?<7h 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

    6&y*MJ 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

    l{PithDa{q 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

    d5>^1clM71`yiuah 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
    kOM+?hL&ACy}ZxlJNB*( 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 Oc{PsjG+)Ow*#85I*)doE literal 0 HcmV?d00001 diff --git a/build_helpers/TA_Lib-0.4.19-cp38-cp38-win_amd64.whl b/build_helpers/TA_Lib-0.4.19-cp38-cp38-win_amd64.whl new file mode 100644 index 0000000000000000000000000000000000000000..b652c7ee042df991c4820569f660cd4ca04c7bf5 GIT binary patch literal 504687 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 M09=V6&=7T#bp;_A!3;nVRZF z7!*`CPR)bLtWNdp_%MT$=#O+1*TUgxE~IcEaBk-c?8HYD`puBS8m(S{Fj9>kZ21(6 z`y;%(r|TmEq3Qm#jb=XI5LqL{wTTSgb z+)4?W{?-r0G}Db{W(RzlAi*$_mX2Rdjd&0}DIqZ)M-Iho3IaEcQ=_5Vmq!S1{hi$tfQ2xp0iVh!tno*MvLtZ83|5-N&I(v_M@cvi)�I> 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

    0{$ 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 z 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≠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 z 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 Date: Mon, 5 Oct 2020 16:17:37 +0200 Subject: [PATCH 0765/1197] Update typehint --- freqtrade/commands/build_config_commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index 79256db1d..7bdbcc057 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Any, Dict +from typing import Any, Dict, List from questionary import Separator, prompt @@ -48,7 +48,7 @@ def ask_user_config() -> Dict[str, Any]: Interactive questions built using https://github.com/tmbo/questionary :returns: Dict with keys to put into template """ - questions = [ + questions: List[Dict[str, Any]] = [ { "type": "confirm", "name": "dry_run", From 378b214a565c425e42309c9e94e0f08c008666db Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Oct 2020 19:27:28 +0200 Subject: [PATCH 0766/1197] Remove hyperopt-loss default option Force users to make a concious choice on a hyperopt-loss function --- .github/workflows/ci.yml | 2 +- .travis.yml | 2 +- docs/advanced-hyperopt.md | 4 ++-- docs/bot-usage.md | 3 +-- docs/faq.md | 2 +- docs/hyperopt.md | 12 +++++------- freqtrade/commands/cli_options.py | 4 +--- freqtrade/constants.py | 1 - freqtrade/resolvers/hyperopt_resolver.py | 10 +++++----- 9 files changed, 17 insertions(+), 23 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6c6521eb..fa599f361 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,7 +88,7 @@ jobs: run: | cp config.json.example config.json freqtrade create-userdir --userdir user_data - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --print-all + freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all - name: Flake8 run: | diff --git a/.travis.yml b/.travis.yml index 0cb76b78b..9b8448db5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,7 +33,7 @@ jobs: - script: - cp config.json.example config.json - freqtrade create-userdir --userdir user_data - - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt + - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily name: hyperopt - script: flake8 name: flake8 diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index dfabf2b91..59ebc16b5 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -27,9 +27,9 @@ class MyAwesomeHyperOpt2(MyAwesomeHyperOpt): and then quickly switch between hyperopt classes, running optimization process with hyperopt class you need in each particular case: ``` -$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt --strategy MyAwesomeStrategy ... +$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --strategy MyAwesomeStrategy ... or -$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt2 --strategy MyAwesomeStrategy ... +$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt2 --hyperopt-loss SharpeHyperOptLossDaily --strategy MyAwesomeStrategy ... ``` ## Creating and using a custom loss function diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 62c515b44..a07a34b94 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -356,8 +356,7 @@ optional arguments: Hyperopt-loss-functions are: DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, SharpeHyperOptLossDaily, SortinoHyperOptLoss, - SortinoHyperOptLossDaily.(default: - `DefaultHyperOptLoss`). + SortinoHyperOptLossDaily. Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/docs/faq.md b/docs/faq.md index d8af7798c..a775060de 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -140,7 +140,7 @@ Since hyperopt uses Bayesian search, running for too many epochs may not produce It's therefore recommended to run between 500-1000 epochs over and over until you hit at least 10.000 epochs in total (or are satisfied with the result). You can best judge by looking at the results - if the bot keeps discovering better strategies, it's best to keep on going. ```bash -freqtrade hyperopt --hyperop SampleHyperopt --strategy SampleStrategy -e 1000 +freqtrade hyperopt --hyperop SampleHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy SampleStrategy -e 1000 ``` ### Why does it take a long time to run hyperopt? diff --git a/docs/hyperopt.md b/docs/hyperopt.md index d26cbeeb2..ff1c4a3d7 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -80,7 +80,7 @@ Rarely you may also need to override: # Have a working strategy at hand. freqtrade new-hyperopt --hyperopt EmptyHyperopt - freqtrade hyperopt --hyperopt EmptyHyperopt --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100 + freqtrade hyperopt --hyperopt EmptyHyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100 ``` ### 1. Install a Custom Hyperopt File @@ -205,14 +205,12 @@ add it to the `populate_indicators()` method in your custom hyperopt file. Each hyperparameter tuning requires a target. This is usually defined as a loss function (sometimes also called objective function), which should decrease for more desirable results, and increase for bad results. -By default, Freqtrade uses a loss function, which has been with freqtrade since the beginning and optimizes mostly for short trade duration and avoiding losses. - -A different loss function can be specified by using the `--hyperopt-loss ` argument. +A loss function must be specified via the `--hyperopt-loss ` argument (or optionally via the configuration under the `"hyperopt_loss"` key). This class should be in its own file within the `user_data/hyperopts/` directory. Currently, the following loss functions are builtin: -* `DefaultHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function) +* `DefaultHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses. * `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration) * `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on trade returns relative to standard deviation) * `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation) @@ -229,7 +227,7 @@ Because hyperopt tries a lot of combinations to find the best parameters it will We strongly recommend to use `screen` or `tmux` to prevent any connection loss. ```bash -freqtrade hyperopt --config config.json --hyperopt --strategy -e 500 --spaces all +freqtrade hyperopt --config config.json --hyperopt --hyperopt-loss --strategy -e 500 --spaces all ``` Use `` as the name of the custom hyperopt used. @@ -263,7 +261,7 @@ freqtrade hyperopt --hyperopt --strategy --timeran Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided. ```bash -freqtrade hyperopt --strategy SampleStrategy --hyperopt SampleHyperopt +freqtrade hyperopt --hyperopt SampleHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy SampleStrategy ``` ### Running Hyperopt with Smaller Search Space diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 3c5775768..f991f6a4d 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -258,10 +258,8 @@ AVAILABLE_CLI_OPTIONS = { 'Different functions can generate completely different results, ' 'since the target for optimization is different. Built-in Hyperopt-loss-functions are: ' 'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, SharpeHyperOptLossDaily, ' - 'SortinoHyperOptLoss, SortinoHyperOptLossDaily.' - '(default: `%(default)s`).', + 'SortinoHyperOptLoss, SortinoHyperOptLossDaily.', metavar='NAME', - default=constants.DEFAULT_HYPEROPT_LOSS, ), "hyperoptexportfilename": Arg( '--hyperopt-filename', diff --git a/freqtrade/constants.py b/freqtrade/constants.py index de663bd4b..8e92d3ed8 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -11,7 +11,6 @@ DEFAULT_EXCHANGE = 'bittrex' PROCESS_THROTTLE_SECS = 5 # sec HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec -DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss' DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite' UNLIMITED_STAKE_AMOUNT = 'unlimited' diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 5e73498ae..2fd7abb93 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -7,7 +7,7 @@ import logging from pathlib import Path from typing import Dict -from freqtrade.constants import DEFAULT_HYPEROPT_LOSS, USERPATH_HYPEROPTS +from freqtrade.constants import USERPATH_HYPEROPTS from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt_interface import IHyperOpt from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss @@ -70,10 +70,10 @@ class HyperOptLossResolver(IResolver): :param config: configuration dictionary """ - # Verify the hyperopt_loss is in the configuration, otherwise fallback to the - # default hyperopt loss - hyperoptloss_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS - + hyperoptloss_name = config.get('hyperopt_loss') + if not hyperoptloss_name: + raise OperationalException("No Hyperopt loss set. Please use `--hyperopt-loss` to " + "specify the Hyperopt-Loss class to use.") hyperoptloss = HyperOptLossResolver.load_object(hyperoptloss_name, config, kwargs={}, extra_dir=config.get('hyperopt_path')) From fa1d1679f0310c5ee2cd7942a45a57f873ab0149 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Oct 2020 19:33:50 +0200 Subject: [PATCH 0767/1197] Adapt tests to work without default hyperoptloss --- tests/optimize/test_hyperopt.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 6c49f090c..636c24c97 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -33,6 +33,7 @@ def hyperopt_conf(default_conf): hyperconf = deepcopy(default_conf) hyperconf.update({ 'hyperopt': 'DefaultHyperOpt', + 'hyperopt_loss': 'DefaultHyperOptLoss', 'hyperopt_path': str(Path(__file__).parent / 'hyperopts'), 'epochs': 1, 'timerange': None, @@ -236,6 +237,7 @@ def test_hyperoptlossresolver(mocker, default_conf) -> None: 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver.load_object', MagicMock(return_value=hl) ) + default_conf.update({'hyperopt_loss': 'DefaultHyperoptLoss'}) x = HyperOptLossResolver.load_hyperoptloss(default_conf) assert hasattr(x, "hyperopt_loss_function") @@ -278,6 +280,7 @@ def test_start(mocker, hyperopt_conf, caplog) -> None: 'hyperopt', '--config', 'config.json', '--hyperopt', 'DefaultHyperOpt', + '--hyperopt-loss', 'DefaultHyperOptLoss', '--epochs', '5' ] pargs = get_args(args) @@ -301,6 +304,7 @@ def test_start_no_data(mocker, hyperopt_conf) -> None: 'hyperopt', '--config', 'config.json', '--hyperopt', 'DefaultHyperOpt', + '--hyperopt-loss', 'DefaultHyperOptLoss', '--epochs', '5' ] pargs = get_args(args) @@ -318,6 +322,7 @@ def test_start_filelock(mocker, hyperopt_conf, caplog) -> None: 'hyperopt', '--config', 'config.json', '--hyperopt', 'DefaultHyperOpt', + '--hyperopt-loss', 'DefaultHyperOptLoss', '--epochs', '5' ] pargs = get_args(args) @@ -325,8 +330,8 @@ def test_start_filelock(mocker, hyperopt_conf, caplog) -> None: assert log_has("Another running instance of freqtrade Hyperopt detected.", caplog) -def test_loss_calculation_prefer_correct_trade_count(default_conf, hyperopt_results) -> None: - hl = HyperOptLossResolver.load_hyperoptloss(default_conf) +def test_loss_calculation_prefer_correct_trade_count(hyperopt_conf, hyperopt_results) -> None: + hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) correct = hl.hyperopt_loss_function(hyperopt_results, 600, datetime(2019, 1, 1), datetime(2019, 5, 1)) over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100, @@ -337,11 +342,11 @@ def test_loss_calculation_prefer_correct_trade_count(default_conf, hyperopt_resu assert under > correct -def test_loss_calculation_prefer_shorter_trades(default_conf, hyperopt_results) -> None: +def test_loss_calculation_prefer_shorter_trades(hyperopt_conf, hyperopt_results) -> None: resultsb = hyperopt_results.copy() resultsb.loc[1, 'trade_duration'] = 20 - hl = HyperOptLossResolver.load_hyperoptloss(default_conf) + hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) longer = hl.hyperopt_loss_function(hyperopt_results, 100, datetime(2019, 1, 1), datetime(2019, 5, 1)) shorter = hl.hyperopt_loss_function(resultsb, 100, @@ -349,13 +354,13 @@ def test_loss_calculation_prefer_shorter_trades(default_conf, hyperopt_results) assert shorter < longer -def test_loss_calculation_has_limited_profit(default_conf, hyperopt_results) -> None: +def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) -> None: results_over = hyperopt_results.copy() results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 results_under = hyperopt_results.copy() results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - hl = HyperOptLossResolver.load_hyperoptloss(default_conf) + hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) correct = hl.hyperopt_loss_function(hyperopt_results, 600, datetime(2019, 1, 1), datetime(2019, 5, 1)) over = hl.hyperopt_loss_function(results_over, 600, From a4a8abfdc058542b8a2b85db3625318bc842cd1e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Oct 2020 20:06:34 +0200 Subject: [PATCH 0768/1197] Update hyperopt documentation --- docs/hyperopt.md | 80 +++++++++++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index ff1c4a3d7..87302a675 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -37,12 +37,20 @@ pip install -r requirements-hyperopt.txt Before we start digging into Hyperopt, we recommend you to take a look at the sample hyperopt file located in [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt.py). -Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar and a lot of code can be copied across from the strategy. +Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar. -The simplest way to get started is to use `freqtrade new-hyperopt --hyperopt AwesomeHyperopt`. -This will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. +!!! Tip "About this page" + For this page, we will be using a fictional strategy called `AwesomeStrategy` - which will be optimized using the `AwesomeHyperopt` class. -### Checklist on all tasks / possibilities in hyperopt +The simplest way to get started is to use the following, command, which will create a new hyperopt file from a template, which will be located under `user_data/hyperopts/AwesomeHyperopt.py`. + +``` bash +freqtrade new-hyperopt --hyperopt AwesomeHyperopt +``` + +### Hyperopt checklist + +Checklist on all tasks / possibilities in hyperopt Depending on the space you want to optimize, only some of the below are required: @@ -54,17 +62,15 @@ Depending on the space you want to optimize, only some of the below are required !!! Note `populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. -Optional - can also be loaded from a strategy: +Optional in hyperopt - can also be loaded from a strategy (recommended): * copy `populate_indicators` from your strategy - otherwise default-strategy will be used * copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used * copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used -!!! Note - Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead. - !!! Note You always have to provide a strategy to Hyperopt, even if your custom Hyperopt class contains all methods. + Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead. Rarely you may also need to override: @@ -83,14 +89,17 @@ Rarely you may also need to override: freqtrade hyperopt --hyperopt EmptyHyperopt --hyperopt-loss SharpeHyperOptLossDaily --spaces roi stoploss trailing --strategy MyWorkingStrategy --config config.json -e 100 ``` -### 1. Install a Custom Hyperopt File +### Create a Custom Hyperopt File -Put your hyperopt file into the directory `user_data/hyperopts`. +Let assume you want a hyperopt file `AwesomeHyperopt.py`: -Let assume you want a hyperopt file `awesome_hyperopt.py`: -Copy the file `user_data/hyperopts/sample_hyperopt.py` into `user_data/hyperopts/awesome_hyperopt.py` +``` bash +freqtrade new-hyperopt --hyperopt AwesomeHyperopt +``` -### 2. Configure your Guards and Triggers +This command will create a new hyperopt file from a template, allowing you to get started quickly. + +### Configure your Guards and Triggers There are two places you need to change in your hyperopt file to add a new buy hyperopt for testing: @@ -102,14 +111,16 @@ There you have two different types of indicators: 1. `guards` and 2. `triggers`. 1. Guards are conditions like "never buy if ADX < 10", or never buy if current price is over EMA10. 2. Triggers are ones that actually trigger buy in specific moment, like "buy when EMA5 crosses over EMA10" or "buy when close price touches lower Bollinger band". +!!! Hint "Guards and Triggers" + Technically, there is no difference between Guards and Triggers. + However, this guide will make this distinction to make it clear that signals should not be "sticking". + Sticking signals are signals that are active for multiple candles. This can lead into buying a signal late (right before the signal disappears - which means that the chance of success is a lot lower than right at the beginning). + Hyper-optimization will, for each epoch round, pick one trigger and possibly -multiple guards. The constructed strategy will be something like -"*buy exactly when close price touches lower Bollinger band, BUT only if +multiple guards. The constructed strategy will be something like "*buy exactly when close price touches lower Bollinger band, BUT only if ADX > 10*". -If you have updated the buy strategy, i.e. changed the contents of -`populate_buy_trend()` method, you have to update the `guards` and -`triggers` your hyperopt must use correspondingly. +If you have updated the buy strategy, i.e. changed the contents of `populate_buy_trend()` method, you have to update the `guards` and `triggers` your hyperopt must use correspondingly. #### Sell optimization @@ -154,7 +165,7 @@ We will start by defining a search space: Above definition says: I have five parameters I want you to randomly combine to find the best combination. Two of them are integer values (`adx-value` -and `rsi-value`) and I want you test in the range of values 20 to 40. +and `rsi-value`) and I want you test in the range of values 20 to 40. Then we have three category variables. First two are either `True` or `False`. We use these to either enable or disable the ADX and RSI guards. The last one we call `trigger` and use it to decide which buy trigger we want to use. @@ -192,14 +203,14 @@ So let's write the buy strategy using these values: return populate_buy_trend ``` -Hyperopting will now call this `populate_buy_trend` as many times you ask it (`epochs`) -with different value combinations. It will then use the given historical data and make -buys based on the buy signals generated with the above function and based on the results -it will end with telling you which parameter combination produced the best profits. +Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations. +It will use the given historical data and make buys based on the buy signals generated with the above function. +Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [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. +!!! Note + 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 strategy or hyperopt file. ## Loss-functions @@ -232,14 +243,15 @@ freqtrade hyperopt --config config.json --hyperopt --hyperopt-los Use `` as the name of the custom hyperopt used. -The `-e` option will set how many evaluations hyperopt will do. We recommend -running at least several thousand evaluations. +The `-e` option will set how many evaluations hyperopt will do. Since hyperopt uses Bayesian search, running too many epochs at once may not produce greater results. Experience has shown that best results are usually not improving much after 500-1000 epochs. +Doing multiple runs (executions) with a few 1000 epochs and different random state will most likely produce different results. The `--spaces all` option determines that all possible parameters should be optimized. Possibilities are listed below. !!! Note Hyperopt will store hyperopt results with the timestamp of the hyperopt start time. Reading commands (`hyperopt-list`, `hyperopt-show`) can use `--hyperopt-filename ` to read and display older hyperopt results. + You can find a list of filenames with `ls -l user_data/hyperopt_results/`. ### Execute Hyperopt with different historical data source @@ -247,7 +259,7 @@ If you would like to hyperopt parameters using an alternate historical data set you have on-disk, use the `--datadir PATH` option. By default, hyperopt uses data from directory `user_data/data`. -### Running Hyperopt with Smaller Testset +### Running Hyperopt with a smaller test-set Use the `--timerange` argument to change how much of the test-set you want to use. For example, to use one month of data, pass the following parameter to the hyperopt call: @@ -437,7 +449,7 @@ Stoploss: -0.27996 In order to use this best stoploss value found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `stoploss` attribute of your custom strategy: -``` +``` python # Optimal stoploss designed for the strategy # This attribute will be overridden if the config file contains "stoploss" stoploss = -0.27996 @@ -471,7 +483,7 @@ Trailing stop: In order to use these best trailing stop parameters found by Hyperopt in backtesting and for live trades/dry-run, copy-paste them as the values of the corresponding attributes of your custom strategy: -``` +``` python # Trailing stop # These attributes will be overridden if the config file contains corresponding values. trailing_stop = True @@ -496,4 +508,8 @@ After you run Hyperopt for the desired amount of epochs, you can later list all Once the optimized strategy has been implemented into your strategy, you should backtest this strategy to make sure everything is working as expected. -To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same set of arguments `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. +To achieve same results (number of trades, their durations, profit, etc.) than during Hyperopt, please use same configuration and parameters (timerange, timeframe, ...) used for hyperopt `--dmmp`/`--disable-max-market-positions` and `--eps`/`--enable-position-stacking` for Backtesting. + +Should results don't match, please double-check to make sure you transferred all conditions correctly. +Pay special care to the stoploss (and trailing stoploss) parameters, as these are often set in configuration files, which override changes to the strategy. +You should also carefully review the log of your backtest to ensure that there were no parameters inadvertently set by the configuration (like `stoploss` or `trailing_stop`). From 14e87ed4a172ae3ecaa80cfce2f7a0130e36d150 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Oct 2020 20:13:09 +0200 Subject: [PATCH 0769/1197] Improvements to hyperopt docs --- docs/hyperopt.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 87302a675..91bc32e48 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -137,7 +137,7 @@ To avoid naming collisions in the search-space, please prefix all sell-spaces wi The Strategy class exposes the timeframe value as the `self.timeframe` attribute. The same value is available as class-attribute `HyperoptName.timeframe`. -In the case of the linked sample-value this would be `SampleHyperOpt.timeframe`. +In the case of the linked sample-value this would be `AwesomeHyperopt.timeframe`. ## Solving a Mystery @@ -273,16 +273,15 @@ freqtrade hyperopt --hyperopt --strategy --timeran Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided. ```bash -freqtrade hyperopt --hyperopt SampleHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy SampleStrategy +freqtrade hyperopt --hyperopt AwesomeHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy AwesomeStrategy ``` ### Running Hyperopt with Smaller Search Space Use the `--spaces` option to limit the search space used by hyperopt. -Letting Hyperopt optimize everything is a huuuuge search space. Often it -might make more sense to start by just searching for initial buy algorithm. -Or maybe you just want to optimize your stoploss or roi table for that awesome -new buy strategy you have. +Letting Hyperopt optimize everything is a huuuuge search space. +Often it might make more sense to start by just searching for initial buy algorithm. +Or maybe you just want to optimize your stoploss or roi table for that awesome new buy strategy you have. Legal values are: @@ -427,7 +426,9 @@ These ranges should be sufficient in most cases. The minutes in the steps (ROI d If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. -Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). A sample for these methods can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). +Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). + +A sample for these methods can be found in [sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). ### Understand Hyperopt Stoploss results From 8c2f7631939e89118715349ef3277b4b942652cf Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 5 Oct 2020 20:36:16 +0200 Subject: [PATCH 0770/1197] Add test to ensure --hyperopt-loss is mandatory --- .github/workflows/ci.yml | 2 +- freqtrade/resolvers/hyperopt_resolver.py | 4 ---- tests/optimize/test_hyperopt.py | 7 +++++++ 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa599f361..5e9612837 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -154,7 +154,7 @@ jobs: run: | cp config.json.example config.json freqtrade create-userdir --userdir user_data - freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --print-all + freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all - name: Flake8 run: | diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 2fd7abb93..328dc488b 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -82,8 +82,4 @@ class HyperOptLossResolver(IResolver): hyperoptloss.__class__.ticker_interval = str(config['timeframe']) hyperoptloss.__class__.timeframe = str(config['timeframe']) - if not hasattr(hyperoptloss, 'hyperopt_loss_function'): - raise OperationalException( - f"Found HyperoptLoss class {hyperoptloss_name} does not " - "implement `hyperopt_loss_function`.") return hyperoptloss diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 636c24c97..f699473f7 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -230,6 +230,13 @@ def test_hyperoptresolver_noname(default_conf): HyperOptResolver.load_hyperopt(default_conf) +def test_hyperoptlossresolver_noname(default_conf): + with pytest.raises(OperationalException, + match="No Hyperopt loss set. Please use `--hyperopt-loss` to specify " + "the Hyperopt-Loss class to use."): + HyperOptLossResolver.load_hyperoptloss(default_conf) + + def test_hyperoptlossresolver(mocker, default_conf) -> None: hl = DefaultHyperOptLoss From 299285a7bb89c397357a5b0cc913683094661c21 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 6 Oct 2020 09:18:49 +0200 Subject: [PATCH 0771/1197] Update actions image to ubuntu20.04 --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6c6521eb..2aecd7010 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ ubuntu-18.04, macos-latest ] + os: [ ubuntu-18.04, ubuntu-20.04, macos-latest ] python-version: [3.7, 3.8] steps: @@ -70,7 +70,7 @@ jobs: pytest --random-order --cov=freqtrade --cov-config=.coveragerc - name: Coveralls - if: (startsWith(matrix.os, 'ubuntu') && matrix.python-version == '3.8') + if: (startsWith(matrix.os, 'ubuntu-20') && matrix.python-version == '3.8') env: # Coveralls token. Not used as secret due to github not providing secrets to forked repositories COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu @@ -176,7 +176,7 @@ jobs: url: ${{ secrets.SLACK_WEBHOOK }} docs_check: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 @@ -194,7 +194,7 @@ jobs: url: ${{ secrets.SLACK_WEBHOOK }} cleanup-prior-runs: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Cleanup previous runs on this branch uses: rokroskar/workflow-run-cleanup-action@v0.2.2 @@ -205,7 +205,7 @@ jobs: # Notify on slack only once - when CI completes (and after deploy) in case it's successfull notify-complete: needs: [ build, build_windows, docs_check ] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Slack Notification uses: homoluctus/slatify@v1.8.0 @@ -218,7 +218,7 @@ jobs: deploy: needs: [ build, build_windows, docs_check ] - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade' steps: - uses: actions/checkout@v2 From 72cf3147b865063000100fee974a5e4ae4a7ef04 Mon Sep 17 00:00:00 2001 From: apneamona <65978183+apneamona@users.noreply.github.com> Date: Tue, 6 Oct 2020 20:17:05 +0200 Subject: [PATCH 0772/1197] Update configuration.md --- docs/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index d6e26f80e..44736e0ba 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -59,8 +59,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `trailing_stop_positive` | Changes stoploss once profit has been reached. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-custom-positive-loss). [Strategy Override](#parameters-in-the-strategy).
    **Datatype:** Float | `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `0.0` (no offset).*
    **Datatype:** Float | `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
    *Defaults to `false`.*
    **Datatype:** Boolean -| `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).
    **Datatype:** Integer -| `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).
    **Datatype:** Integer +| `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
    **Datatype:** Integer +| `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).
    **Datatype:** Integer | `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).
    *Defaults to `bid`.*
    **Datatype:** String (either `ask` or `bid`). | `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#buy-price-without-orderbook-enabled). | `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled).
    **Datatype:** Boolean From 52502193c43a4c060c755dc13095ad4e7fbf1ba1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Oct 2020 20:59:05 +0200 Subject: [PATCH 0773/1197] Backtesting should not double-loop for sell signals --- freqtrade/optimize/backtesting.py | 182 +++++++++++++++--------------- 1 file changed, 89 insertions(+), 93 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index afdb4fc37..c29240994 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -4,6 +4,7 @@ This module contains the backtesting logic """ import logging +from collections import defaultdict from copy import deepcopy from datetime import datetime, timedelta from typing import Any, Dict, List, NamedTuple, Optional, Tuple @@ -215,72 +216,29 @@ class Backtesting: else: return sell_row.open - def _get_sell_trade_entry( - self, pair: str, buy_row: DataFrame, - partial_ohlcv: List, trade_count_lock: Dict, - stake_amount: float, max_open_trades: int) -> Optional[BacktestResult]: + def _get_sell_trade_entry(self, trade: Trade, sell_row: DataFrame) -> Optional[BacktestResult]: - trade = Trade( - pair=pair, - open_rate=buy_row.open, - open_date=buy_row.date, - stake_amount=stake_amount, - amount=round(stake_amount / buy_row.open, 8), - fee_open=self.fee, - fee_close=self.fee, - is_open=True, - ) - logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.") - # calculate win/lose forwards from buy point - for sell_row in partial_ohlcv: - if max_open_trades > 0: - # Increase trade_count_lock for every iteration - trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1 + sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, sell_row.buy, + sell_row.sell, low=sell_row.low, high=sell_row.high) + if sell.sell_flag: + logger.debug(f"Fund sell signal {sell.sell_flag}") + trade_dur = int((sell_row.date - trade.open_date).total_seconds() // 60) + closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) - sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, sell_row.buy, - 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) - closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) - - return BacktestResult(pair=pair, - profit_percent=trade.calc_profit_ratio(rate=closerate), - profit_abs=trade.calc_profit(rate=closerate), - open_date=buy_row.date, - open_rate=buy_row.open, - open_fee=self.fee, - close_date=sell_row.date, - close_rate=closerate, - close_fee=self.fee, - amount=trade.amount, - trade_duration=trade_dur, - open_at_end=False, - sell_reason=sell.sell_type - ) - if partial_ohlcv: - # no sell condition found - trade stil open at end of backtest period - sell_row = partial_ohlcv[-1] - bt_res = BacktestResult(pair=pair, - profit_percent=trade.calc_profit_ratio(rate=sell_row.open), - profit_abs=trade.calc_profit(rate=sell_row.open), - open_date=buy_row.date, - open_rate=buy_row.open, - open_fee=self.fee, - close_date=sell_row.date, - close_rate=sell_row.open, - close_fee=self.fee, - amount=trade.amount, - trade_duration=int(( - sell_row.date - buy_row.date).total_seconds() // 60), - open_at_end=True, - sell_reason=SellType.FORCE_SELL - ) - logger.debug(f"{pair} - Force selling still open trade, " - f"profit percent: {bt_res.profit_percent}, " - f"profit abs: {bt_res.profit_abs}") - - return bt_res - return None + return BacktestResult(pair=trade.pair, + profit_percent=trade.calc_profit_ratio(rate=closerate), + profit_abs=trade.calc_profit(rate=closerate), + open_date=trade.open_date, + open_rate=trade.open_rate, + open_fee=self.fee, + close_date=sell_row.date, + close_rate=closerate, + close_fee=self.fee, + amount=trade.amount, + trade_duration=trade_dur, + open_at_end=False, + sell_reason=sell.sell_type + ) def backtest(self, processed: Dict, stake_amount: float, start_date: arrow.Arrow, end_date: arrow.Arrow, @@ -305,19 +263,21 @@ class Backtesting: f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}" ) trades = [] - trade_count_lock: Dict = {} # Use dict of lists with data for performance # (looping lists is a lot faster than pandas DataFrames) data: Dict = self._get_ohlcv_as_lists(processed) - 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_min) + open_trades: Dict[str, List] = defaultdict(list) + open_trade_count = 0 + # Loop timerange and get candle for each pair at that point in time - while tmp < end_date: + while tmp <= end_date: + open_trade_count_start = open_trade_count for i, pair in enumerate(data): if pair not in indexes: @@ -336,37 +296,73 @@ class Backtesting: indexes[pair] += 1 - if row.buy == 0 or row.sell == 1: - continue # skip rows where no buy signal or that would immediately sell off + # without positionstacking, we can only have one open trade per pair. + # max_open_trades must be respected + # don't open on the last row + if ((position_stacking or len(open_trades[pair]) == 0) + and max_open_trades > 0 and open_trade_count_start < max_open_trades + and tmp != end_date + and row.buy == 1 and row.sell != 1): + # Enter trade + trade = Trade( + pair=pair, + open_rate=row.open, + open_date=row.date, + stake_amount=stake_amount, + amount=round(stake_amount / row.open, 8), + fee_open=self.fee, + fee_close=self.fee, + is_open=True, + ) + # TODO: hacky workaround to avoid opening > max_open_trades + # This emulates previous behaviour - not sure if this is correct + # Prevents buying if the trade-slot was freed in this candle + open_trade_count_start += 1 + open_trade_count += 1 + logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.") + open_trades[pair].append(trade) - if (not position_stacking and pair in lock_pair_until - and row.date <= lock_pair_until[pair]): - # without positionstacking, we can only have one open trade per pair. - continue + for trade in open_trades[pair]: + # logger.debug(f"{pair} - Checking for sells for {trade} at {row.date}") - if max_open_trades > 0: - # Check if max_open_trades has already been reached for the given date - if not trade_count_lock.get(row.date, 0) < max_open_trades: - continue - trade_count_lock[row.date] = trade_count_lock.get(row.date, 0) + 1 - - # since indexes has been incremented before, we need to go one step back to - # also check the buying candle for sell conditions. - trade_entry = self._get_sell_trade_entry(pair, row, data[pair][indexes[pair]-1:], - trade_count_lock, stake_amount, - max_open_trades) - - if trade_entry: - logger.debug(f"{pair} - Locking pair till " - f"close_date={trade_entry.close_date}") - lock_pair_until[pair] = trade_entry.close_date - trades.append(trade_entry) - else: - # Set lock_pair_until to end of testing period if trade could not be closed - lock_pair_until[pair] = end_date.datetime + # since indexes has been incremented before, we need to go one step back to + # also check the buying candle for sell conditions. + trade_entry = self._get_sell_trade_entry(trade, row) + # Sell occured + if trade_entry: + logger.debug(f"{pair} - Backtesting sell {trade}") + open_trade_count -= 1 + open_trades[pair].remove(trade) + trades.append(trade_entry) # Move time one configured time_interval ahead. tmp += timedelta(minutes=self.timeframe_min) + + # Handle trades that were left open + for pair in open_trades.keys(): + if len(open_trades[pair]) == 0: + continue + else: + for trade in open_trades[pair]: + sell_row = data[pair][-1] + trade_entry = BacktestResult(pair=trade.pair, + profit_percent=trade.calc_profit_ratio( + rate=sell_row.open), + profit_abs=trade.calc_profit(rate=sell_row.open), + open_date=trade.open_date, + open_rate=trade.open_rate, + open_fee=self.fee, + close_date=sell_row.date, + close_rate=sell_row.open, + close_fee=self.fee, + amount=trade.amount, + trade_duration=int(( + sell_row.date - trade.open_date).total_seconds() // 60), + open_at_end=True, + sell_reason=SellType.FORCE_SELL + ) + trades.append(trade_entry) + return DataFrame.from_records(trades, columns=BacktestResult._fields) def start(self) -> None: From 1b5cb3427e36498efa85e0bf432b3238b34cb78d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Oct 2020 08:09:55 +0200 Subject: [PATCH 0774/1197] Fix example R calculation in edge documentation --- docs/edge.md | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/docs/edge.md b/docs/edge.md index 500c3c833..7b6a6d8e8 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -82,20 +82,33 @@ Risk Reward Ratio ($R$) is a formula used to measure the expected gains of a giv $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ ???+ Example "Worked example of $R$ calculation" - Let's say that you think that the price of *stonecoin* today is $10.0. You believe that, because they will start mining stonecoin, it will go up to $15.0 tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to $0 tomorrow. You are planning to invest $100.
    - Your potential profit is calculated as:
    + Let's say that you think that the price of *stonecoin* today is $10.0. You believe that, because they will start mining stonecoin, it will go up to $15.0 tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to $0 tomorrow. You are planning to invest $100. + + Your potential profit is calculated as: + $\begin{aligned} - \text{potential_profit} &= (\text{potential_price} - \text{cost_per_unit}) * \frac{\text{investment}}{\text{cost_per_unit}} \\ - &= (15 - 10) * \frac{100}{15}\\ - &= 33.33 - \end{aligned}$
    - Since the price might go to $0, the $100 dolars invested could turn into 0. We can compute the Risk Reward Ratio as follows:
    + \text{potential_profit} &= (\text{potential_price} - \text{entry_price}) * \text{investment} \\ + &= (15 - 10) * 100\\ + &= 500 + \end{aligned}$ + + Since the price might go to $0, the $100 dollars invested could turn into 0. + + We do however use a stoploss of 15% - so in the worst case, we'll sell 15% below entry price (or at 8.5). + + $\begin{aligned} + \text{risk} &= (\text{entry_price} - \text{stoploss}) * \text{investment} \\ + &= (10 - (10 * (1 - 0.15))) * 100\\ + &= 150 + \end{aligned}$ + + We can compute the Risk Reward Ratio as follows:
    $\begin{aligned} R &= \frac{\text{potential_profit}}{\text{potential_loss}}\\ - &= \frac{33.33}{100}\\ - &= 0.333... + &= \frac{500}{150}\\ + &= 3.33 \end{aligned}$
    - What it effectivelly means is that the strategy have the potential to make $0.33 for each $1 invested. + What it effectively means is that the strategy have the potential to make 3$ for each $1 invested. On a long horizon, that is, on many trades, we can calculate the risk reward by dividing the strategy' average profit on winning trades by the strategy' average loss on losing trades. We can calculate the average profit, $\mu_{win}$, as follows: From 48750b0ef85c3c52cc20c5431bee13c68ab619ba Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Oct 2020 08:23:56 +0200 Subject: [PATCH 0775/1197] Improve wording in formula --- docs/edge.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/edge.md b/docs/edge.md index 7b6a6d8e8..0891a851e 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -93,11 +93,11 @@ $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ \end{aligned}$ Since the price might go to $0, the $100 dollars invested could turn into 0. - + We do however use a stoploss of 15% - so in the worst case, we'll sell 15% below entry price (or at 8.5). $\begin{aligned} - \text{risk} &= (\text{entry_price} - \text{stoploss}) * \text{investment} \\ + \text{potential_loss} &= (\text{entry_price} - \text{stoploss}) * \text{investment} \\ &= (10 - (10 * (1 - 0.15))) * 100\\ &= 150 \end{aligned}$ From 6bb045f5655959c01455d4a886ac6485f17e354b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Oct 2020 08:30:30 +0200 Subject: [PATCH 0776/1197] Simplify stoploss calculation --- docs/edge.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/edge.md b/docs/edge.md index 0891a851e..d3a6af8ba 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -94,11 +94,11 @@ $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ Since the price might go to $0, the $100 dollars invested could turn into 0. - We do however use a stoploss of 15% - so in the worst case, we'll sell 15% below entry price (or at 8.5). + We do however use a stoploss of 15% - so in the worst case, we'll sell 15% below entry price (or at 8.5$). $\begin{aligned} \text{potential_loss} &= (\text{entry_price} - \text{stoploss}) * \text{investment} \\ - &= (10 - (10 * (1 - 0.15))) * 100\\ + &= (10 - 8.5) * 100\\ &= 150 \end{aligned}$ @@ -108,7 +108,7 @@ $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ &= \frac{500}{150}\\ &= 3.33 \end{aligned}$
    - What it effectively means is that the strategy have the potential to make 3$ for each $1 invested. + What it effectively means is that the strategy have the potential to make 3.33$ for each $1 invested. On a long horizon, that is, on many trades, we can calculate the risk reward by dividing the strategy' average profit on winning trades by the strategy' average loss on losing trades. We can calculate the average profit, $\mu_{win}$, as follows: From 7f0afe12446fa879463b91ad1dda3611caec7948 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Oct 2020 10:24:52 +0200 Subject: [PATCH 0777/1197] Fix calculation to not show losses > initial investment --- docs/edge.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/edge.md b/docs/edge.md index d3a6af8ba..7442f1927 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -82,14 +82,14 @@ Risk Reward Ratio ($R$) is a formula used to measure the expected gains of a giv $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ ???+ Example "Worked example of $R$ calculation" - Let's say that you think that the price of *stonecoin* today is $10.0. You believe that, because they will start mining stonecoin, it will go up to $15.0 tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to $0 tomorrow. You are planning to invest $100. + Let's say that you think that the price of *stonecoin* today is $10.0. You believe that, because they will start mining stonecoin, it will go up to $15.0 tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to $0 tomorrow. You are planning to invest $100, which will give you 10 shares (100 / 10). Your potential profit is calculated as: $\begin{aligned} - \text{potential_profit} &= (\text{potential_price} - \text{entry_price}) * \text{investment} \\ - &= (15 - 10) * 100\\ - &= 500 + \text{potential_profit} &= (\text{potential_price} - \text{entry_price}) * \frac{\text{investment}}{\text{entry_price}} \\ + &= (15 - 10) * (100 / 10) \\ + &= 50 \end{aligned}$ Since the price might go to $0, the $100 dollars invested could turn into 0. @@ -97,15 +97,16 @@ $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ We do however use a stoploss of 15% - so in the worst case, we'll sell 15% below entry price (or at 8.5$). $\begin{aligned} - \text{potential_loss} &= (\text{entry_price} - \text{stoploss}) * \text{investment} \\ - &= (10 - 8.5) * 100\\ - &= 150 + \text{potential_loss} &= (\text{entry_price} - \text{stoploss}) * \frac{\text{investment}}{\text{entry_price}} \\ + &= (10 - 8.5) * (100 / 10)\\ + &= 15 \end{aligned}$ - We can compute the Risk Reward Ratio as follows:
    + We can compute the Risk Reward Ratio as follows: + $\begin{aligned} R &= \frac{\text{potential_profit}}{\text{potential_loss}}\\ - &= \frac{500}{150}\\ + &= \frac{50}{15}\\ &= 3.33 \end{aligned}$
    What it effectively means is that the strategy have the potential to make 3.33$ for each $1 invested. From d1db847612cbac7618a170e538cb1878b538608e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Oct 2020 19:27:00 +0200 Subject: [PATCH 0778/1197] Fix "storing information" documentation closes #3843 --- docs/strategy-customization.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 14d5fcd84..a6cdef864 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -312,12 +312,17 @@ The name of the variable can be chosen at will, but should be prefixed with `cus class Awesomestrategy(IStrategy): # Create custom dictionary cust_info = {} + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: # Check if the entry already exists + if not metadata["pair"] in self._cust_info: + # Create empty entry for this pair + self._cust_info[metadata["pair"]] = {} + if "crosstime" in self.cust_info[metadata["pair"]: - self.cust_info[metadata["pair"]["crosstime"] += 1 + self.cust_info[metadata["pair"]]["crosstime"] += 1 else: - self.cust_info[metadata["pair"]["crosstime"] = 1 + self.cust_info[metadata["pair"]]["crosstime"] = 1 ``` !!! Warning From e8f2c09f08a583f307ad188d638243ff042d563b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Oct 2020 20:10:00 +0200 Subject: [PATCH 0779/1197] Extract handling of left open trades to seperate method --- freqtrade/optimize/backtesting.py | 56 ++++++++++++++++++------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index c29240994..236b86eb1 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -239,6 +239,37 @@ class Backtesting: open_at_end=False, sell_reason=sell.sell_type ) + return None + + def handle_left_open(self, open_trades: Dict[str, List], + data: Dict[str, DataFrame]) -> List[BacktestResult]: + """ + Handling of left open trades at the end of backtesting + """ + trades = [] + for pair in open_trades.keys(): + if len(open_trades[pair]) > 0: + for trade in open_trades[pair]: + sell_row = data[pair][-1] + trade_entry = BacktestResult(pair=trade.pair, + profit_percent=trade.calc_profit_ratio( + rate=sell_row.open), + profit_abs=trade.calc_profit(rate=sell_row.open), + open_date=trade.open_date, + open_rate=trade.open_rate, + open_fee=self.fee, + close_date=sell_row.date, + close_rate=sell_row.open, + close_fee=self.fee, + amount=trade.amount, + trade_duration=int(( + sell_row.date - trade.open_date + ).total_seconds() // 60), + open_at_end=True, + sell_reason=SellType.FORCE_SELL + ) + trades.append(trade_entry) + return trades def backtest(self, processed: Dict, stake_amount: float, start_date: arrow.Arrow, end_date: arrow.Arrow, @@ -338,30 +369,7 @@ class Backtesting: # Move time one configured time_interval ahead. tmp += timedelta(minutes=self.timeframe_min) - # Handle trades that were left open - for pair in open_trades.keys(): - if len(open_trades[pair]) == 0: - continue - else: - for trade in open_trades[pair]: - sell_row = data[pair][-1] - trade_entry = BacktestResult(pair=trade.pair, - profit_percent=trade.calc_profit_ratio( - rate=sell_row.open), - profit_abs=trade.calc_profit(rate=sell_row.open), - open_date=trade.open_date, - open_rate=trade.open_rate, - open_fee=self.fee, - close_date=sell_row.date, - close_rate=sell_row.open, - close_fee=self.fee, - amount=trade.amount, - trade_duration=int(( - sell_row.date - trade.open_date).total_seconds() // 60), - open_at_end=True, - sell_reason=SellType.FORCE_SELL - ) - trades.append(trade_entry) + trades += self.handle_left_open(open_trades, data=data) return DataFrame.from_records(trades, columns=BacktestResult._fields) From 23278e52db01f1b2a351d20c1206d796e2abb35c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 8 Oct 2020 20:22:45 +0200 Subject: [PATCH 0780/1197] remove obsolete logging statements --- freqtrade/optimize/backtesting.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 236b86eb1..f59a782e8 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -221,7 +221,6 @@ class Backtesting: sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, sell_row.buy, sell_row.sell, low=sell_row.low, high=sell_row.high) if sell.sell_flag: - logger.debug(f"Fund sell signal {sell.sell_flag}") trade_dur = int((sell_row.date - trade.open_date).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) @@ -354,8 +353,6 @@ class Backtesting: open_trades[pair].append(trade) for trade in open_trades[pair]: - # logger.debug(f"{pair} - Checking for sells for {trade} at {row.date}") - # since indexes has been incremented before, we need to go one step back to # also check the buying candle for sell conditions. trade_entry = self._get_sell_trade_entry(trade, row) From f676156ec79348adfe82727845eb71ea2dd929a9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Oct 2020 06:39:13 +0200 Subject: [PATCH 0781/1197] Implement division/0 checks for win and loss columns in edge closes #3839 --- freqtrade/edge/edge_positioning.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index 6a95ad91f..a40b63d67 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -310,8 +310,10 @@ class Edge: # Calculating number of losing trades, average win and average loss df['nb_loss_trades'] = df['nb_trades'] - df['nb_win_trades'] - df['average_win'] = df['profit_sum'] / df['nb_win_trades'] - df['average_loss'] = df['loss_sum'] / df['nb_loss_trades'] + df['average_win'] = np.where(df['nb_win_trades'] == 0, 0.0, + df['profit_sum'] / df['nb_win_trades']) + df['average_loss'] = np.where(df['nb_loss_trades'] == 0, 0.0, + df['loss_sum'] / df['nb_loss_trades']) # Win rate = number of profitable trades / number of trades df['winrate'] = df['nb_win_trades'] / df['nb_trades'] From 59b00ad6624645776be1f09ee6baf8856feaf933 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Oct 2020 06:47:02 +0200 Subject: [PATCH 0782/1197] Add test for only-win scenario --- tests/edge/test_edge.py | 58 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index f19590490..a4bfa1085 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -499,3 +499,61 @@ def test_process_expectancy_remove_pumps(mocker, edge_conf, fee,): assert final['TEST/BTC'].stoploss == -0.9 assert final['TEST/BTC'].nb_trades == len(trades_df) - 1 assert round(final['TEST/BTC'].winrate, 10) == 0.0 + + +def test_process_expectancy_only_wins(mocker, edge_conf, fee,): + edge_conf['edge']['min_trade_number'] = 2 + freqtrade = get_patched_freqtradebot(mocker, edge_conf) + + freqtrade.exchange.get_fee = fee + edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) + + trades = [ + {'pair': 'TEST/BTC', + 'stoploss': -0.9, + 'profit_percent': '', + 'profit_abs': '', + 'open_date': np.datetime64('2018-10-03T00:05:00.000000000'), + 'close_date': np.datetime64('2018-10-03T00:10:00.000000000'), + 'open_index': 1, + 'close_index': 1, + 'trade_duration': '', + 'open_rate': 15, + 'close_rate': 17, + 'exit_type': 'sell_signal'}, + {'pair': 'TEST/BTC', + 'stoploss': -0.9, + 'profit_percent': '', + 'profit_abs': '', + 'open_date': np.datetime64('2018-10-03T00:20:00.000000000'), + 'close_date': np.datetime64('2018-10-03T00:25:00.000000000'), + 'open_index': 4, + 'close_index': 4, + 'trade_duration': '', + 'open_rate': 10, + 'close_rate': 20, + 'exit_type': 'sell_signal'}, + {'pair': 'TEST/BTC', + 'stoploss': -0.9, + 'profit_percent': '', + 'profit_abs': '', + 'open_date': np.datetime64('2018-10-03T00:30:00.000000000'), + 'close_date': np.datetime64('2018-10-03T00:40:00.000000000'), + 'open_index': 6, + 'close_index': 7, + 'trade_duration': '', + 'open_rate': 26, + 'close_rate': 134, + 'exit_type': 'sell_signal'} + ] + + trades_df = DataFrame(trades) + trades_df = edge._fill_calculable_fields(trades_df) + final = edge._process_expectancy(trades_df) + + assert 'TEST/BTC' in final + assert final['TEST/BTC'].stoploss == -0.9 + assert final['TEST/BTC'].nb_trades == len(trades_df) + assert round(final['TEST/BTC'].winrate, 10) == 1.0 + assert round(final['TEST/BTC'].risk_reward_ratio, 10) == float('inf') + assert round(final['TEST/BTC'].expectancy, 10) == float('inf') From 53984a059f43f004e2877d9363f0ae849375a2ee Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Oct 2020 09:02:20 +0200 Subject: [PATCH 0783/1197] Configure mkdocs to allow page includes --- .github/workflows/ci.yml | 5 +++++ docs/developer.md | 2 +- docs/requirements-docs.txt | 1 + mkdocs.yml | 8 ++++---- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5dad1443b..a89254349 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -184,6 +184,11 @@ jobs: run: | ./tests/test_docs.sh + - name: Documentation build + run: | + pip install -r docs/requirements-docs.txt + mkdocs build + - name: Slack Notification uses: homoluctus/slatify@v1.8.0 if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) diff --git a/docs/developer.md b/docs/developer.md index 788e961cd..8ef816d5d 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -96,7 +96,7 @@ Below is an outline of exception inheritance hierarchy: ## Modules -### Dynamic Pairlist +### Pairlists You have a great idea for a new pair selection algorithm you would like to try out? Great. Hopefully you also want to contribute this back upstream. diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 66225d6d4..69ae33649 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,3 @@ mkdocs-material==6.0.2 mdx_truly_sane_lists==1.2 +pymdown-extensions==8.0.1 diff --git a/mkdocs.yml b/mkdocs.yml index 26494ae45..8d1ce1cfe 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -55,16 +55,16 @@ markdown_extensions: permalink: true - pymdownx.arithmatex: generic: true - - pymdownx.caret - - pymdownx.critic - pymdownx.details - pymdownx.inlinehilite - pymdownx.magiclink - - pymdownx.mark + - pymdownx.pathconverter - pymdownx.smartsymbols + - pymdownx.snippets: + base_path: docs + check_paths: true - pymdownx.tabbed - pymdownx.superfences - pymdownx.tasklist: custom_checkbox: true - - pymdownx.tilde - mdx_truly_sane_lists From f43bd250a2155ea276f9e30f8a9bf0c0728ca03d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Oct 2020 09:02:44 +0200 Subject: [PATCH 0784/1197] Extract pairlists from configuration --- docs/configuration.md | 139 +------------------------------------ docs/includes/pairlists.md | 137 ++++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 138 deletions(-) create mode 100644 docs/includes/pairlists.md diff --git a/docs/configuration.md b/docs/configuration.md index d6e26f80e..f8e8aabcd 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -574,144 +574,7 @@ Assuming both buy and sell are using market orders, a configuration similar to t ``` Obviously, if only one side is using limit orders, different pricing combinations can be used. - -## Pairlists and Pairlist Handlers - -Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings. - -In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) Pairlist Handler). - -Additionaly, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter) and [`SpreadFilter`](#spreadfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist. - -If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You should always configure either `StaticPairList` or `VolumePairList` as the starting Pairlist Handler. - -Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist. - -### Available Pairlist Handlers - -* [`StaticPairList`](#static-pair-list) (default, if not configured differently) -* [`VolumePairList`](#volume-pair-list) -* [`AgeFilter`](#agefilter) -* [`PrecisionFilter`](#precisionfilter) -* [`PriceFilter`](#pricefilter) -* [`ShuffleFilter`](#shufflefilter) -* [`SpreadFilter`](#spreadfilter) - -!!! Tip "Testing pairlists" - Pairlist configurations can be quite tricky to get right. Best use the [`test-pairlist`](utils.md#test-pairlist) utility subcommand to test your configuration quickly. - -#### Static Pair List - -By default, the `StaticPairList` method is used, which uses a statically defined pair whitelist from the configuration. - -It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`. - -```json -"pairlists": [ - {"method": "StaticPairList"} - ], -``` - -#### Volume Pair List - -`VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`). - -When used in the chain of Pairlist Handlers in a non-leading position (after StaticPairList and other Pairlist Filters), `VolumePairList` considers outputs of previous Pairlist Handlers, adding its sorting/selection of the pairs by the trading volume. - -When used on the leading position of the chain of Pairlist Handlers, it does not consider `pair_whitelist` configuration setting, but selects the top assets from all available markets (with matching stake-currency) on the exchange. - -The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes). - -`VolumePairList` is based on the ticker data from exchange, as reported by the ccxt library: - -* The `quoteVolume` is the amount of quote (stake) currency traded (bought or sold) in last 24 hours. - -```json -"pairlists": [{ - "method": "VolumePairList", - "number_assets": 20, - "sort_key": "quoteVolume", - "refresh_period": 1800, -}], -``` - -#### AgeFilter - -Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`). - -When pairs are first listed on an exchange they can suffer huge price drops and volatility -in the first few days while the pair goes through its price-discovery period. Bots can often -be caught out buying before the pair has finished dropping in price. - -This filter allows freqtrade to ignore pairs until they have been listed for at least `min_days_listed` days. - -#### PrecisionFilter - -Filters low-value coins which would not allow setting stoplosses. - -#### PriceFilter - -The `PriceFilter` allows filtering of pairs by price. Currently the following price filters are supported: - -* `min_price` -* `max_price` -* `low_price_ratio` - -The `min_price` setting removes pairs where the price is below the specified price. This is useful if you wish to avoid trading very low-priced pairs. -This option is disabled by default, and will only apply if set to > 0. - -The `max_price` setting removes pairs where the price is above the specified price. This is useful if you wish to trade only low-priced pairs. -This option is disabled by default, and will only apply if set to > 0. - -The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio. -This option is disabled by default, and will only apply if set to > 0. - -For `PriceFiler` at least one of its `min_price`, `max_price` or `low_price_ratio` settings must be applied. - -Calculation example: - -Min price precision for SHITCOIN/BTC is 8 decimals. If its price is 0.00000011 - one price step above would be 0.00000012, which is ~9% higher than the previous price value. You may filter out this pair by using PriceFilter with `low_price_ratio` set to 0.09 (9%) or with `min_price` set to 0.00000011, correspondingly. - -!!! Warning "Low priced pairs" - Low priced pairs with high "1 pip movements" are dangerous since they are often illiquid and it may also be impossible to place the desired stoploss, which can often result in high losses since price needs to be rounded to the next tradable price - so instead of having a stoploss of -5%, you could end up with a stoploss of -9% simply due to price rounding. - -#### ShuffleFilter - -Shuffles (randomizes) pairs in the pairlist. It can be used for preventing the bot from trading some of the pairs more frequently then others when you want all pairs be treated with the same priority. - -!!! Tip - You may set the `seed` value for this Pairlist to obtain reproducible results, which can be useful for repeated backtesting sessions. If `seed` is not set, the pairs are shuffled in the non-repeatable random order. - -#### SpreadFilter - -Removes pairs that have a difference between asks and bids above the specified ratio, `max_spread_ratio` (defaults to `0.005`). - -Example: - -If `DOGE/BTC` maximum bid is 0.00000026 and minimum ask is 0.00000027, the ratio is calculated as: `1 - bid/ask ~= 0.037` which is `> 0.005` and this pair will be filtered out. - -### Full example of Pairlist Handlers - -The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies both [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 priceunit is > 1%. Then the `SpreadFilter` is applied and pairs are finally shuffled with the random seed set to some predefined value. - -```json -"exchange": { - "pair_whitelist": [], - "pair_blacklist": ["BNB/BTC"] -}, -"pairlists": [ - { - "method": "VolumePairList", - "number_assets": 20, - "sort_key": "quoteVolume", - }, - {"method": "AgeFilter", "min_days_listed": 10}, - {"method": "PrecisionFilter"}, - {"method": "PriceFilter", "low_price_ratio": 0.01}, - {"method": "SpreadFilter", "max_spread_ratio": 0.005}, - {"method": "ShuffleFilter", "seed": 42} - ], -``` +--8<-- "includes/pairlists.md" ## Switch to Dry-run mode diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md new file mode 100644 index 000000000..ae4ec818d --- /dev/null +++ b/docs/includes/pairlists.md @@ -0,0 +1,137 @@ +## Pairlists and Pairlist Handlers + +Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings. + +In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) Pairlist Handler). + +Additionally, [`AgeFilter`](#agefilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter) and [`SpreadFilter`](#spreadfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist. + +If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You should always configure either `StaticPairList` or `VolumePairList` as the starting Pairlist Handler. + +Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist. + +### Available Pairlist Handlers + +* [`StaticPairList`](#static-pair-list) (default, if not configured differently) +* [`VolumePairList`](#volume-pair-list) +* [`AgeFilter`](#agefilter) +* [`PrecisionFilter`](#precisionfilter) +* [`PriceFilter`](#pricefilter) +* [`ShuffleFilter`](#shufflefilter) +* [`SpreadFilter`](#spreadfilter) + +!!! Tip "Testing pairlists" + Pairlist configurations can be quite tricky to get right. Best use the [`test-pairlist`](utils.md#test-pairlist) utility sub-command to test your configuration quickly. + +#### Static Pair List + +By default, the `StaticPairList` method is used, which uses a statically defined pair whitelist from the configuration. + +It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklist`. + +```json +"pairlists": [ + {"method": "StaticPairList"} + ], +``` + +#### Volume Pair List + +`VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`). + +When used in the chain of Pairlist Handlers in a non-leading position (after StaticPairList and other Pairlist Filters), `VolumePairList` considers outputs of previous Pairlist Handlers, adding its sorting/selection of the pairs by the trading volume. + +When used on the leading position of the chain of Pairlist Handlers, it does not consider `pair_whitelist` configuration setting, but selects the top assets from all available markets (with matching stake-currency) on the exchange. + +The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes). + +`VolumePairList` is based on the ticker data from exchange, as reported by the ccxt library: + +* The `quoteVolume` is the amount of quote (stake) currency traded (bought or sold) in last 24 hours. + +```json +"pairlists": [{ + "method": "VolumePairList", + "number_assets": 20, + "sort_key": "quoteVolume", + "refresh_period": 1800, +}], +``` + +#### AgeFilter + +Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`). + +When pairs are first listed on an exchange they can suffer huge price drops and volatility +in the first few days while the pair goes through its price-discovery period. Bots can often +be caught out buying before the pair has finished dropping in price. + +This filter allows freqtrade to ignore pairs until they have been listed for at least `min_days_listed` days. + +#### PrecisionFilter + +Filters low-value coins which would not allow setting stoplosses. + +#### PriceFilter + +The `PriceFilter` allows filtering of pairs by price. Currently the following price filters are supported: + +* `min_price` +* `max_price` +* `low_price_ratio` + +The `min_price` setting removes pairs where the price is below the specified price. This is useful if you wish to avoid trading very low-priced pairs. +This option is disabled by default, and will only apply if set to > 0. + +The `max_price` setting removes pairs where the price is above the specified price. This is useful if you wish to trade only low-priced pairs. +This option is disabled by default, and will only apply if set to > 0. + +The `low_price_ratio` setting removes pairs where a raise of 1 price unit (pip) is above the `low_price_ratio` ratio. +This option is disabled by default, and will only apply if set to > 0. + +For `PriceFiler` at least one of its `min_price`, `max_price` or `low_price_ratio` settings must be applied. + +Calculation example: + +Min price precision for SHITCOIN/BTC is 8 decimals. If its price is 0.00000011 - one price step above would be 0.00000012, which is ~9% higher than the previous price value. You may filter out this pair by using PriceFilter with `low_price_ratio` set to 0.09 (9%) or with `min_price` set to 0.00000011, correspondingly. + +!!! Warning "Low priced pairs" + Low priced pairs with high "1 pip movements" are dangerous since they are often illiquid and it may also be impossible to place the desired stoploss, which can often result in high losses since price needs to be rounded to the next tradable price - so instead of having a stoploss of -5%, you could end up with a stoploss of -9% simply due to price rounding. + +#### ShuffleFilter + +Shuffles (randomizes) pairs in the pairlist. It can be used for preventing the bot from trading some of the pairs more frequently then others when you want all pairs be treated with the same priority. + +!!! Tip + You may set the `seed` value for this Pairlist to obtain reproducible results, which can be useful for repeated backtesting sessions. If `seed` is not set, the pairs are shuffled in the non-repeatable random order. + +#### SpreadFilter + +Removes pairs that have a difference between asks and bids above the specified ratio, `max_spread_ratio` (defaults to `0.005`). + +Example: + +If `DOGE/BTC` maximum bid is 0.00000026 and minimum ask is 0.00000027, the ratio is calculated as: `1 - bid/ask ~= 0.037` which is `> 0.005` and this pair will be filtered out. + +### Full example of Pairlist Handlers + +The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies both [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 price unit is > 1%. Then the `SpreadFilter` is applied and pairs are finally shuffled with the random seed set to some predefined value. + +```json +"exchange": { + "pair_whitelist": [], + "pair_blacklist": ["BNB/BTC"] +}, +"pairlists": [ + { + "method": "VolumePairList", + "number_assets": 20, + "sort_key": "quoteVolume", + }, + {"method": "AgeFilter", "min_days_listed": 10}, + {"method": "PrecisionFilter"}, + {"method": "PriceFilter", "low_price_ratio": 0.01}, + {"method": "SpreadFilter", "max_spread_ratio": 0.005}, + {"method": "ShuffleFilter", "seed": 42} + ], +``` From cedddd02dac533ab79799c0dea8b0da5536797fe Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 9 Oct 2020 09:08:58 +0200 Subject: [PATCH 0785/1197] Install mkdocs for ci --- .github/workflows/ci.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a89254349..f259129d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: - uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} @@ -125,7 +125,7 @@ jobs: - uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} @@ -184,9 +184,15 @@ jobs: run: | ./tests/test_docs.sh + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Documentation build run: | pip install -r docs/requirements-docs.txt + pip install mkdocs mkdocs build - name: Slack Notification @@ -229,7 +235,7 @@ jobs: - uses: actions/checkout@v2 - name: Set up Python - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: 3.8 From 23bad8fd9f320d2754c9d71e134c40a771b4e88f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 10 Oct 2020 14:22:29 +0200 Subject: [PATCH 0786/1197] Rename DefahltHyperoptLoss function to ShortTradeDurHyperOptLoss --- docs/bot-usage.md | 2 +- docs/hyperopt.md | 2 +- freqtrade/commands/cli_options.py | 4 ++-- freqtrade/optimize/default_hyperopt_loss.py | 8 ++++++-- tests/optimize/test_hyperopt.py | 16 ++++++++-------- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index a07a34b94..4d07435c7 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -353,7 +353,7 @@ optional arguments: class (IHyperOptLoss). Different functions can generate completely different results, since the target for optimization is different. Built-in - Hyperopt-loss-functions are: DefaultHyperOptLoss, + Hyperopt-loss-functions are: ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, SharpeHyperOptLossDaily, SortinoHyperOptLoss, SortinoHyperOptLossDaily. diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 91bc32e48..5f5ffbee0 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -221,7 +221,7 @@ This class should be in its own file within the `user_data/hyperopts/` directory Currently, the following loss functions are builtin: -* `DefaultHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses. +* `ShortTradeHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses. * `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration) * `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on trade returns relative to standard deviation) * `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index f991f6a4d..8ea945ae7 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -257,8 +257,8 @@ AVAILABLE_CLI_OPTIONS = { help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). ' 'Different functions can generate completely different results, ' 'since the target for optimization is different. Built-in Hyperopt-loss-functions are: ' - 'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, SharpeHyperOptLossDaily, ' - 'SortinoHyperOptLoss, SortinoHyperOptLossDaily.', + 'ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, ' + 'SharpeHyperOptLossDaily, SortinoHyperOptLoss, SortinoHyperOptLossDaily.', metavar='NAME', ), "hyperoptexportfilename": Arg( diff --git a/freqtrade/optimize/default_hyperopt_loss.py b/freqtrade/optimize/default_hyperopt_loss.py index 9e780d0ea..9dbdc4403 100644 --- a/freqtrade/optimize/default_hyperopt_loss.py +++ b/freqtrade/optimize/default_hyperopt_loss.py @@ -1,5 +1,5 @@ """ -DefaultHyperOptLoss +ShortTradeDurHyperOptLoss This module defines the default HyperoptLoss class which is being used for Hyperoptimization. """ @@ -26,7 +26,7 @@ EXPECTED_MAX_PROFIT = 3.0 MAX_ACCEPTED_TRADE_DURATION = 300 -class DefaultHyperOptLoss(IHyperOptLoss): +class ShortTradeDurHyperOptLoss(IHyperOptLoss): """ Defines the default loss function for hyperopt """ @@ -50,3 +50,7 @@ class DefaultHyperOptLoss(IHyperOptLoss): duration_loss = 0.4 * min(trade_duration / MAX_ACCEPTED_TRADE_DURATION, 1) result = trade_loss + profit_loss + duration_loss return result + + +# Create an alias for This to allow the legacy Method to work as well. +DefaultHyperOptLoss = ShortTradeDurHyperOptLoss diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index f699473f7..41ad6f5de 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -17,7 +17,7 @@ from freqtrade import constants from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.optimize.default_hyperopt_loss import DefaultHyperOptLoss +from freqtrade.optimize.default_hyperopt_loss import ShortTradeDurHyperOptLoss from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver from freqtrade.state import RunMode @@ -33,7 +33,7 @@ def hyperopt_conf(default_conf): hyperconf = deepcopy(default_conf) hyperconf.update({ 'hyperopt': 'DefaultHyperOpt', - 'hyperopt_loss': 'DefaultHyperOptLoss', + 'hyperopt_loss': 'ShortTradeDurHyperOptLoss', 'hyperopt_path': str(Path(__file__).parent / 'hyperopts'), 'epochs': 1, 'timerange': None, @@ -239,12 +239,12 @@ def test_hyperoptlossresolver_noname(default_conf): def test_hyperoptlossresolver(mocker, default_conf) -> None: - hl = DefaultHyperOptLoss + hl = ShortTradeDurHyperOptLoss mocker.patch( 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver.load_object', MagicMock(return_value=hl) ) - default_conf.update({'hyperopt_loss': 'DefaultHyperoptLoss'}) + default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'}) x = HyperOptLossResolver.load_hyperoptloss(default_conf) assert hasattr(x, "hyperopt_loss_function") @@ -287,7 +287,7 @@ def test_start(mocker, hyperopt_conf, caplog) -> None: 'hyperopt', '--config', 'config.json', '--hyperopt', 'DefaultHyperOpt', - '--hyperopt-loss', 'DefaultHyperOptLoss', + '--hyperopt-loss', 'SharpeHyperOptLossDaily', '--epochs', '5' ] pargs = get_args(args) @@ -311,7 +311,7 @@ def test_start_no_data(mocker, hyperopt_conf) -> None: 'hyperopt', '--config', 'config.json', '--hyperopt', 'DefaultHyperOpt', - '--hyperopt-loss', 'DefaultHyperOptLoss', + '--hyperopt-loss', 'SharpeHyperOptLossDaily', '--epochs', '5' ] pargs = get_args(args) @@ -329,7 +329,7 @@ def test_start_filelock(mocker, hyperopt_conf, caplog) -> None: 'hyperopt', '--config', 'config.json', '--hyperopt', 'DefaultHyperOpt', - '--hyperopt-loss', 'DefaultHyperOptLoss', + '--hyperopt-loss', 'SharpeHyperOptLossDaily', '--epochs', '5' ] pargs = get_args(args) @@ -384,7 +384,7 @@ def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> N results_under = hyperopt_results.copy() results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - default_conf.update({'hyperopt_loss': 'SharpeHyperOptLoss'}) + default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'}) hl = HyperOptLossResolver.load_hyperoptloss(default_conf) correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), datetime(2019, 1, 1), datetime(2019, 5, 1)) From 3d911557d1aed4301a2d38679b24027e54c9bc5e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 11 Oct 2020 08:37:47 +0200 Subject: [PATCH 0787/1197] Fix typo in docs --- docs/hyperopt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 5f5ffbee0..fc7a0dd93 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -221,7 +221,7 @@ This class should be in its own file within the `user_data/hyperopts/` directory Currently, the following loss functions are builtin: -* `ShortTradeHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses. +* `ShortTradeDurHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function) - Mostly for short trade duration and avoiding losses. * `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration) * `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on trade returns relative to standard deviation) * `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation) From fa7dc742d0b288c10c44665ec774a63ce5b7ebc3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Oct 2020 06:07:57 +0200 Subject: [PATCH 0788/1197] Plot-image should have freqtrade as entrypoint --- docker/Dockerfile.plot | 3 --- 1 file changed, 3 deletions(-) diff --git a/docker/Dockerfile.plot b/docker/Dockerfile.plot index 1843efdcb..40bc72bc5 100644 --- a/docker/Dockerfile.plot +++ b/docker/Dockerfile.plot @@ -5,6 +5,3 @@ FROM freqtradeorg/freqtrade:${sourceimage} COPY requirements-plot.txt /freqtrade/ RUN pip install -r requirements-plot.txt --no-cache-dir - -# Empty the ENTRYPOINT to allow all commands -ENTRYPOINT [] From 491af5a0cbe6af346745558735b9e8972bc7a496 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2020 05:43:19 +0000 Subject: [PATCH 0789/1197] Bump pandas from 1.1.2 to 1.1.3 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.1.2 to 1.1.3. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.1.2...v1.1.3) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 51313c32c..47847c637 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ numpy==1.19.2 -pandas==1.1.2 +pandas==1.1.3 ccxt==1.35.22 SQLAlchemy==1.3.19 From a2bc9d60a08890d5002aa2c5ebcc3760cebc7d42 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2020 05:43:23 +0000 Subject: [PATCH 0790/1197] Bump arrow from 0.16.0 to 0.17.0 Bumps [arrow](https://github.com/arrow-py/arrow) from 0.16.0 to 0.17.0. - [Release notes](https://github.com/arrow-py/arrow/releases) - [Changelog](https://github.com/arrow-py/arrow/blob/master/CHANGELOG.rst) - [Commits](https://github.com/arrow-py/arrow/compare/0.16.0...0.17.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 51313c32c..b0f9a84e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ pandas==1.1.2 ccxt==1.35.22 SQLAlchemy==1.3.19 python-telegram-bot==12.8 -arrow==0.16.0 +arrow==0.17.0 cachetools==4.1.1 requests==2.24.0 urllib3==1.25.10 From a33865e8c289d24b11269e6d88528e0aaaf41175 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2020 05:43:37 +0000 Subject: [PATCH 0791/1197] Bump nbconvert from 6.0.6 to 6.0.7 Bumps [nbconvert](https://github.com/jupyter/nbconvert) from 6.0.6 to 6.0.7. - [Release notes](https://github.com/jupyter/nbconvert/releases) - [Commits](https://github.com/jupyter/nbconvert/compare/6.0.6...6.0.7) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 0710882a4..7330d079b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -16,4 +16,4 @@ pytest-random-order==1.0.4 isort==5.5.4 # Convert jupyter notebooks to markdown documents -nbconvert==6.0.6 +nbconvert==6.0.7 From 80569c5f2179d4ba6ac8cf7df3f9fa9561297a65 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2020 05:43:38 +0000 Subject: [PATCH 0792/1197] Bump mypy from 0.782 to 0.790 Bumps [mypy](https://github.com/python/mypy) from 0.782 to 0.790. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.782...v0.790) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 0710882a4..a622bac5d 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ coveralls==2.1.2 flake8==3.8.4 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 -mypy==0.782 +mypy==0.790 pytest==6.1.1 pytest-asyncio==0.14.0 pytest-cov==2.10.1 From 623cee61e676362dbdd8c2f13c69dcfd7facd7cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2020 07:31:08 +0000 Subject: [PATCH 0793/1197] Bump isort from 5.5.4 to 5.6.3 Bumps [isort](https://github.com/pycqa/isort) from 5.5.4 to 5.6.3. - [Release notes](https://github.com/pycqa/isort/releases) - [Changelog](https://github.com/PyCQA/isort/blob/develop/CHANGELOG.md) - [Commits](https://github.com/pycqa/isort/compare/5.5.4...5.6.3) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 7330d079b..316c8cb8e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,7 +13,7 @@ pytest-asyncio==0.14.0 pytest-cov==2.10.1 pytest-mock==3.3.1 pytest-random-order==1.0.4 -isort==5.5.4 +isort==5.6.3 # Convert jupyter notebooks to markdown documents nbconvert==6.0.7 From e39c2f4a9630072546b54015b9bd8ff74297a59b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2020 07:35:11 +0000 Subject: [PATCH 0794/1197] Bump ccxt from 1.35.22 to 1.36.2 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.35.22 to 1.36.2. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.35.22...1.36.2) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8c6635aaa..1d0290b3c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.2 pandas==1.1.3 -ccxt==1.35.22 +ccxt==1.36.2 SQLAlchemy==1.3.19 python-telegram-bot==12.8 arrow==0.17.0 From f299c4188b1f1372c23c7e77fe6cad49ffed8c71 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 12 Oct 2020 08:13:28 +0000 Subject: [PATCH 0795/1197] Bump python-telegram-bot from 12.8 to 13.0 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 12.8 to 13.0. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v12.8...v13.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1d0290b3c..8a344a716 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pandas==1.1.3 ccxt==1.36.2 SQLAlchemy==1.3.19 -python-telegram-bot==12.8 +python-telegram-bot==13.0 arrow==0.17.0 cachetools==4.1.1 requests==2.24.0 From 44e374878c3443745fe17e13c6326fffee7e2f74 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Oct 2020 19:28:14 +0200 Subject: [PATCH 0796/1197] Fix mypy errors due to new version --- freqtrade/freqtradebot.py | 4 ++-- freqtrade/misc.py | 4 ++-- freqtrade/pairlist/IPairList.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2bdd8da4b..9562519aa 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -58,8 +58,8 @@ class FreqtradeBot: # Cache values for 1800 to avoid frequent polling of the exchange for prices # Caching only applies to RPC methods, so prices for open trades are still # refreshed once every iteration. - self._sell_rate_cache = TTLCache(maxsize=100, ttl=1800) - self._buy_rate_cache = TTLCache(maxsize=100, ttl=1800) + self._sell_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800) + self._buy_rate_cache: TTLCache = TTLCache(maxsize=100, ttl=1800) self.strategy: IStrategy = StrategyResolver.load_strategy(self.config) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index 071693f8d..359d0d0e4 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -56,8 +56,8 @@ def file_dump_json(filename: Path, data: Any, is_zip: bool = False, log: bool = if log: logger.info(f'dumping json to "{filename}"') - with gzip.open(filename, 'w') as fp: - rapidjson.dump(data, fp, default=str, number_mode=rapidjson.NM_NATIVE) + with gzip.open(filename, 'w') as fpz: + rapidjson.dump(data, fpz, default=str, number_mode=rapidjson.NM_NATIVE) else: if log: logger.info(f'dumping json to "{filename}"') diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index 67a96cc60..6b5bd11e7 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -36,7 +36,7 @@ class IPairList(ABC): self._pairlist_pos = pairlist_pos self.refresh_period = self._pairlistconfig.get('refresh_period', 1800) self._last_refresh = 0 - self._log_cache = TTLCache(maxsize=1024, ttl=self.refresh_period) + self._log_cache: TTLCache = TTLCache(maxsize=1024, ttl=self.refresh_period) @property def name(self) -> str: From a39898a5b3cb3bd55c0567e2dbe0fb18ce88b885 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Oct 2020 19:44:13 +0200 Subject: [PATCH 0797/1197] Fix mock for telegram update --- tests/conftest.py | 2 +- tests/rpc/test_rpc_telegram.py | 17 +++++------------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2153fd327..f90f0e62b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -297,7 +297,7 @@ def default_conf(testdatadir): @pytest.fixture def update(): _update = Update(0) - _update.message = Message(0, 0, datetime.utcnow(), Chat(0, 0)) + _update.message = Message(0, datetime.utcnow(), Chat(0, 0)) return _update diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index c62282cf0..230df0df9 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -82,7 +82,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: assert log_has(message_str, caplog) -def test_cleanup(default_conf, mocker) -> None: +def test_cleanup(default_conf, mocker, ) -> None: updater_mock = MagicMock() updater_mock.stop = MagicMock() mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock) @@ -92,13 +92,9 @@ def test_cleanup(default_conf, mocker) -> None: assert telegram._updater.stop.call_count == 1 -def test_authorized_only(default_conf, mocker, caplog) -> None: +def test_authorized_only(default_conf, mocker, caplog, update) -> None: patch_exchange(mocker) - chat = Chat(0, 0) - update = Update(randint(1, 100)) - update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat) - default_conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf) patch_get_signal(bot, (True, False)) @@ -114,7 +110,7 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: patch_exchange(mocker) chat = Chat(0xdeadbeef, 0) update = Update(randint(1, 100)) - update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat) + update.message = Message(randint(1, 100), datetime.utcnow(), chat) default_conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf) @@ -127,12 +123,9 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: assert not log_has('Exception occurred within Telegram module', caplog) -def test_authorized_only_exception(default_conf, mocker, caplog) -> None: +def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None: patch_exchange(mocker) - update = Update(randint(1, 100)) - update.message = Message(randint(1, 100), 0, datetime.utcnow(), Chat(0, 0)) - default_conf['telegram']['enabled'] = False bot = FreqtradeBot(default_conf) @@ -146,7 +139,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None: assert log_has('Exception occurred within Telegram module', caplog) -def test_telegram_status(default_conf, update, mocker, fee, ticker,) -> None: +def test_telegram_status(default_conf, update, mocker) -> None: update.message.chat.id = "123" default_conf['telegram']['enabled'] = False default_conf['telegram']['chat_id'] = "123" From 5aa0d3e05c08a8ecfe15428173de3596c17a5e27 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Oct 2020 20:08:40 +0200 Subject: [PATCH 0798/1197] Add multidict and aiohttp requirements --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index 51313c32c..986739b46 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,8 @@ numpy==1.19.2 pandas==1.1.2 ccxt==1.35.22 +multidict==4.7.6 +aiohttp==3.6.3 SQLAlchemy==1.3.19 python-telegram-bot==12.8 arrow==0.16.0 From ecddaa663b3c8be6af8f305efd5f48548e52d2b7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 12 Oct 2020 19:58:04 +0200 Subject: [PATCH 0799/1197] Convert timestamp to int_timestamp for all arrow occurances --- freqtrade/configuration/timerange.py | 8 ++++---- freqtrade/edge/edge_positioning.py | 4 ++-- freqtrade/exchange/exchange.py | 12 ++++++------ freqtrade/optimize/optimize_reports.py | 4 ++-- freqtrade/wallets.py | 4 ++-- tests/commands/test_commands.py | 5 +++-- tests/conftest.py | 12 ++++++------ tests/data/test_history.py | 4 ++-- tests/edge/test_edge.py | 12 ++++++------ tests/exchange/test_exchange.py | 23 ++++++++++++----------- 10 files changed, 45 insertions(+), 43 deletions(-) diff --git a/freqtrade/configuration/timerange.py b/freqtrade/configuration/timerange.py index 151003999..32bbd02a0 100644 --- a/freqtrade/configuration/timerange.py +++ b/freqtrade/configuration/timerange.py @@ -52,11 +52,11 @@ class TimeRange: :return: None (Modifies the object in place) """ if (not self.starttype or (startup_candles - and min_date.timestamp >= self.startts)): + and min_date.int_timestamp >= self.startts)): # If no startts was defined, or backtest-data starts at the defined backtest-date logger.warning("Moving start-date by %s candles to account for startup time.", startup_candles) - self.startts = (min_date.timestamp + timeframe_secs * startup_candles) + self.startts = (min_date.int_timestamp + timeframe_secs * startup_candles) self.starttype = 'date' @staticmethod @@ -89,7 +89,7 @@ class TimeRange: if stype[0]: starts = rvals[index] if stype[0] == 'date' and len(starts) == 8: - start = arrow.get(starts, 'YYYYMMDD').timestamp + start = arrow.get(starts, 'YYYYMMDD').int_timestamp elif len(starts) == 13: start = int(starts) // 1000 else: @@ -98,7 +98,7 @@ class TimeRange: if stype[1]: stops = rvals[index] if stype[1] == 'date' and len(stops) == 8: - stop = arrow.get(stops, 'YYYYMMDD').timestamp + stop = arrow.get(stops, 'YYYYMMDD').int_timestamp elif len(stops) == 13: stop = int(stops) // 1000 else: diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index a40b63d67..037717c68 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -87,7 +87,7 @@ class Edge: heartbeat = self.edge_config.get('process_throttle_secs') if (self._last_updated > 0) and ( - self._last_updated + heartbeat > arrow.utcnow().timestamp): + self._last_updated + heartbeat > arrow.utcnow().int_timestamp): return False data: Dict[str, Any] = {} @@ -146,7 +146,7 @@ class Edge: # Fill missing, calculable columns, profit, duration , abs etc. trades_df = self._fill_calculable_fields(DataFrame(trades)) self._cached_pairs = self._process_expectancy(trades_df) - self._last_updated = arrow.utcnow().timestamp + self._last_updated = arrow.utcnow().int_timestamp return True diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index bbb94e61f..d6836ee73 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -291,7 +291,7 @@ class Exchange: try: self._api.load_markets() self._load_async_markets() - self._last_markets_refresh = arrow.utcnow().timestamp + self._last_markets_refresh = arrow.utcnow().int_timestamp except ccxt.BaseError as e: logger.warning('Unable to initialize markets. Reason: %s', e) @@ -300,14 +300,14 @@ class Exchange: # Check whether markets have to be reloaded if (self._last_markets_refresh > 0) and ( self._last_markets_refresh + self.markets_refresh_interval - > arrow.utcnow().timestamp): + > arrow.utcnow().int_timestamp): return None logger.debug("Performing scheduled market reload..") try: self._api.load_markets(reload=True) # Also reload async markets to avoid issues with newly listed pairs self._load_async_markets(reload=True) - self._last_markets_refresh = arrow.utcnow().timestamp + self._last_markets_refresh = arrow.utcnow().int_timestamp except ccxt.BaseError: logger.exception("Could not reload markets.") @@ -501,7 +501,7 @@ class Exchange: 'side': side, 'remaining': _amount, 'datetime': arrow.utcnow().isoformat(), - 'timestamp': int(arrow.utcnow().timestamp * 1000), + 'timestamp': int(arrow.utcnow().int_timestamp * 1000), 'status': "closed" if ordertype == "market" else "open", 'fee': None, 'info': {} @@ -696,7 +696,7 @@ class Exchange: ) input_coroutines = [self._async_get_candle_history( pair, timeframe, since) for since in - range(since_ms, arrow.utcnow().timestamp * 1000, one_call)] + range(since_ms, arrow.utcnow().int_timestamp * 1000, one_call)] results = await asyncio.gather(*input_coroutines, return_exceptions=True) @@ -759,7 +759,7 @@ class Exchange: interval_in_sec = timeframe_to_seconds(timeframe) return not ((self._pairs_last_refresh_time.get((pair, timeframe), 0) - + interval_in_sec) >= arrow.utcnow().timestamp) + + interval_in_sec) >= arrow.utcnow().int_timestamp) @retrier_async async def _async_get_candle_history(self, pair: str, timeframe: str, diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 3db9a312a..c977a991b 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -268,9 +268,9 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], 'profit_total': results['profit_percent'].sum(), 'profit_total_abs': results['profit_abs'].sum(), 'backtest_start': min_date.datetime, - 'backtest_start_ts': min_date.timestamp * 1000, + 'backtest_start_ts': min_date.int_timestamp * 1000, 'backtest_end': max_date.datetime, - 'backtest_end_ts': max_date.timestamp * 1000, + 'backtest_end_ts': max_date.int_timestamp * 1000, 'backtest_days': backtest_days, 'trades_per_day': round(len(results) / backtest_days, 2) if backtest_days > 0 else 0, diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 21a9466e1..3680dd416 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -108,13 +108,13 @@ class Wallets: for trading operations, the latest balance is needed. :param require_update: Allow skipping an update if balances were recently refreshed """ - if (require_update or (self._last_wallet_refresh + 3600 < arrow.utcnow().timestamp)): + if (require_update or (self._last_wallet_refresh + 3600 < arrow.utcnow().int_timestamp)): if self._config['dry_run']: self._update_dry() else: self._update_live() logger.info('Wallets synced.') - self._last_wallet_refresh = arrow.utcnow().timestamp + self._last_wallet_refresh = arrow.utcnow().int_timestamp def get_all_balances(self) -> Dict[str, Any]: return self._wallets diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 5b125697c..bf845a2e1 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -579,7 +579,7 @@ def test_download_data_timerange(mocker, caplog, markets): start_download_data(get_args(args)) assert dl_mock.call_count == 1 # 20days ago - days_ago = arrow.get(arrow.utcnow().shift(days=-20).date()).timestamp + days_ago = arrow.get(arrow.utcnow().shift(days=-20).date()).int_timestamp assert dl_mock.call_args_list[0][1]['timerange'].startts == days_ago dl_mock.reset_mock() @@ -592,7 +592,8 @@ def test_download_data_timerange(mocker, caplog, markets): start_download_data(get_args(args)) assert dl_mock.call_count == 1 - assert dl_mock.call_args_list[0][1]['timerange'].startts == arrow.Arrow(2020, 1, 1).timestamp + assert dl_mock.call_args_list[0][1]['timerange'].startts == arrow.Arrow( + 2020, 1, 1).int_timestamp def test_download_data_no_markets(mocker, caplog): diff --git a/tests/conftest.py b/tests/conftest.py index 2153fd327..a99404ac2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -792,7 +792,7 @@ def limit_buy_order_open(): 'side': 'buy', 'symbol': 'mocked', 'datetime': arrow.utcnow().isoformat(), - 'timestamp': arrow.utcnow().timestamp, + 'timestamp': arrow.utcnow().int_timestamp, 'price': 0.00001099, 'amount': 90.99181073, 'filled': 0.0, @@ -911,7 +911,7 @@ def limit_buy_order_canceled_empty(request): 'info': {}, 'id': '1234512345', 'clientOrderId': None, - 'timestamp': arrow.utcnow().shift(minutes=-601).timestamp, + 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp, 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'lastTradeTimestamp': None, 'symbol': 'LTC/USDT', @@ -932,7 +932,7 @@ def limit_buy_order_canceled_empty(request): 'info': {}, 'id': 'AZNPFF-4AC4N-7MKTAT', 'clientOrderId': None, - 'timestamp': arrow.utcnow().shift(minutes=-601).timestamp, + 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp, 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'lastTradeTimestamp': None, 'status': 'canceled', @@ -953,7 +953,7 @@ def limit_buy_order_canceled_empty(request): 'info': {}, 'id': '1234512345', 'clientOrderId': 'alb1234123', - 'timestamp': arrow.utcnow().shift(minutes=-601).timestamp, + 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp, 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'lastTradeTimestamp': None, 'symbol': 'LTC/USDT', @@ -974,7 +974,7 @@ def limit_buy_order_canceled_empty(request): 'info': {}, 'id': '1234512345', 'clientOrderId': 'alb1234123', - 'timestamp': arrow.utcnow().shift(minutes=-601).timestamp, + 'timestamp': arrow.utcnow().shift(minutes=-601).int_timestamp, 'datetime': arrow.utcnow().shift(minutes=-601).isoformat(), 'lastTradeTimestamp': None, 'symbol': 'LTC/USDT', @@ -1000,7 +1000,7 @@ def limit_sell_order_open(): 'side': 'sell', 'pair': 'mocked', 'datetime': arrow.utcnow().isoformat(), - 'timestamp': arrow.utcnow().timestamp, + 'timestamp': arrow.utcnow().int_timestamp, 'price': 0.00001173, 'amount': 90.99181073, 'filled': 0.0, diff --git a/tests/data/test_history.py b/tests/data/test_history.py index c8324cf0b..bbc6e55b4 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -323,7 +323,7 @@ def test_load_partial_missing(testdatadir, caplog) -> None: start = arrow.get('2018-01-01T00:00:00') end = arrow.get('2018-01-11T00:00:00') data = load_data(testdatadir, '5m', ['UNITTEST/BTC'], startup_candles=20, - timerange=TimeRange('date', 'date', start.timestamp, end.timestamp)) + timerange=TimeRange('date', 'date', start.int_timestamp, end.int_timestamp)) assert log_has( 'Using indicator startup period: 20 ...', caplog ) @@ -339,7 +339,7 @@ def test_load_partial_missing(testdatadir, caplog) -> None: start = arrow.get('2018-01-10T00:00:00') end = arrow.get('2018-02-20T00:00:00') data = load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], - timerange=TimeRange('date', 'date', start.timestamp, end.timestamp)) + timerange=TimeRange('date', 'date', start.int_timestamp, end.int_timestamp)) # timedifference in 5 minutes td = ((end - start).total_seconds() // 60 // 5) + 1 assert td != len(data['UNITTEST/BTC']) diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index a4bfa1085..f25dad35b 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -50,7 +50,7 @@ def _build_dataframe(buy_ohlc_sell_matrice): 'date': tests_start_time.shift( minutes=( ohlc[0] * - timeframe_in_minute)).timestamp * + timeframe_in_minute)).int_timestamp * 1000, 'buy': ohlc[1], 'open': ohlc[2], @@ -71,7 +71,7 @@ def _build_dataframe(buy_ohlc_sell_matrice): def _time_on_candle(number): return np.datetime64(tests_start_time.shift( - minutes=(number * timeframe_in_minute)).timestamp * 1000, 'ms') + minutes=(number * timeframe_in_minute)).int_timestamp * 1000, 'ms') # End helper functions @@ -251,7 +251,7 @@ def test_edge_heartbeat_calculate(mocker, edge_conf): heartbeat = edge_conf['edge']['process_throttle_secs'] # should not recalculate if heartbeat not reached - edge._last_updated = arrow.utcnow().timestamp - heartbeat + 1 + edge._last_updated = arrow.utcnow().int_timestamp - heartbeat + 1 assert edge.calculate() is False @@ -263,7 +263,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m', NEOBTC = [ [ - tests_start_time.shift(minutes=(x * timeframe_in_minute)).timestamp * 1000, + tests_start_time.shift(minutes=(x * timeframe_in_minute)).int_timestamp * 1000, math.sin(x * hz) / 1000 + base, math.sin(x * hz) / 1000 + base + 0.0001, math.sin(x * hz) / 1000 + base - 0.0001, @@ -275,7 +275,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m', base = 0.002 LTCBTC = [ [ - tests_start_time.shift(minutes=(x * timeframe_in_minute)).timestamp * 1000, + tests_start_time.shift(minutes=(x * timeframe_in_minute)).int_timestamp * 1000, math.sin(x * hz) / 1000 + base, math.sin(x * hz) / 1000 + base + 0.0001, math.sin(x * hz) / 1000 + base - 0.0001, @@ -299,7 +299,7 @@ def test_edge_process_downloaded_data(mocker, edge_conf): assert edge.calculate() assert len(edge._cached_pairs) == 2 - assert edge._last_updated <= arrow.utcnow().timestamp + 2 + assert edge._last_updated <= arrow.utcnow().int_timestamp + 2 def test_edge_process_no_data(mocker, edge_conf, caplog): diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 7be9c77ac..7df596098 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -385,7 +385,7 @@ def test_reload_markets(default_conf, mocker, caplog): exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance", mock_markets=False) exchange._load_async_markets = MagicMock() - exchange._last_markets_refresh = arrow.utcnow().timestamp + exchange._last_markets_refresh = arrow.utcnow().int_timestamp updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}} assert exchange.markets == initial_markets @@ -396,7 +396,7 @@ def test_reload_markets(default_conf, mocker, caplog): assert exchange._load_async_markets.call_count == 0 # more than 10 minutes have passed, reload is executed - exchange._last_markets_refresh = arrow.utcnow().timestamp - 15 * 60 + exchange._last_markets_refresh = arrow.utcnow().int_timestamp - 15 * 60 exchange.reload_markets() assert exchange.markets == updated_markets assert exchange._load_async_markets.call_count == 1 @@ -1264,7 +1264,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) ohlcv = [ [ - arrow.utcnow().timestamp * 1000, # unix timestamp ms + arrow.utcnow().int_timestamp * 1000, # unix timestamp ms 1, # open 2, # high 3, # low @@ -1281,7 +1281,8 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): # one_call calculation * 1.8 should do 2 calls since = 5 * 60 * exchange._ft_has['ohlcv_candle_limit'] * 1.8 - ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000)) + ret = exchange.get_historic_ohlcv(pair, "5m", int(( + arrow.utcnow().int_timestamp - since) * 1000)) assert exchange._async_get_candle_history.call_count == 2 # Returns twice the above OHLCV data @@ -1291,7 +1292,7 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: ohlcv = [ [ - (arrow.utcnow().timestamp - 1) * 1000, # unix timestamp ms + (arrow.utcnow().int_timestamp - 1) * 1000, # unix timestamp ms 1, # open 2, # high 3, # low @@ -1299,7 +1300,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: 5, # volume (in quote currency) ], [ - arrow.utcnow().timestamp * 1000, # unix timestamp ms + arrow.utcnow().int_timestamp * 1000, # unix timestamp ms 3, # open 1, # high 4, # low @@ -1345,7 +1346,7 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name): ohlcv = [ [ - arrow.utcnow().timestamp * 1000, # unix timestamp ms + arrow.utcnow().int_timestamp * 1000, # unix timestamp ms 1, # open 2, # high 3, # low @@ -1380,14 +1381,14 @@ async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_ api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError("Unknown error")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) await exchange._async_get_candle_history(pair, "5m", - (arrow.utcnow().timestamp - 2000) * 1000) + (arrow.utcnow().int_timestamp - 2000) * 1000) with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching ' r'historical candle \(OHLCV\) data\..*'): api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported("Not supported")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) await exchange._async_get_candle_history(pair, "5m", - (arrow.utcnow().timestamp - 2000) * 1000) + (arrow.utcnow().int_timestamp - 2000) * 1000) @pytest.mark.asyncio @@ -1599,13 +1600,13 @@ async def test__async_fetch_trades(default_conf, mocker, caplog, exchange_name, with pytest.raises(OperationalException, match=r'Could not fetch trade data*'): api_mock.fetch_trades = MagicMock(side_effect=ccxt.BaseError("Unknown error")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - await exchange._async_fetch_trades(pair, since=(arrow.utcnow().timestamp - 2000) * 1000) + await exchange._async_fetch_trades(pair, since=(arrow.utcnow().int_timestamp - 2000) * 1000) with pytest.raises(OperationalException, match=r'Exchange.* does not support fetching ' r'historical trade data\..*'): api_mock.fetch_trades = MagicMock(side_effect=ccxt.NotSupported("Not supported")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - await exchange._async_fetch_trades(pair, since=(arrow.utcnow().timestamp - 2000) * 1000) + await exchange._async_fetch_trades(pair, since=(arrow.utcnow().int_timestamp - 2000) * 1000) @pytest.mark.asyncio From 43532a2ffa265129498bbb3135ba952a68e7f74b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2020 12:39:31 +0000 Subject: [PATCH 0800/1197] Bump colorama from 0.4.3 to 0.4.4 Bumps [colorama](https://github.com/tartley/colorama) from 0.4.3 to 0.4.4. - [Release notes](https://github.com/tartley/colorama/releases) - [Changelog](https://github.com/tartley/colorama/blob/master/CHANGELOG.rst) - [Commits](https://github.com/tartley/colorama/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ef9182f3b..a03b4ba20 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,7 +34,7 @@ flask-jwt-extended==3.24.1 flask-cors==3.0.9 # Support for colorized terminal output -colorama==0.4.3 +colorama==0.4.4 # Building config files interactively questionary==1.6.0 prompt-toolkit==3.0.7 From 5f5fc513fa710ec83423af4f3c250cebf166b259 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2020 12:39:35 +0000 Subject: [PATCH 0801/1197] Bump isort from 5.6.3 to 5.6.4 Bumps [isort](https://github.com/pycqa/isort) from 5.6.3 to 5.6.4. - [Release notes](https://github.com/pycqa/isort/releases) - [Changelog](https://github.com/PyCQA/isort/blob/develop/CHANGELOG.md) - [Commits](https://github.com/pycqa/isort/compare/5.6.3...5.6.4) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 33aae2dfb..916bb2ec2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,7 +13,7 @@ pytest-asyncio==0.14.0 pytest-cov==2.10.1 pytest-mock==3.3.1 pytest-random-order==1.0.4 -isort==5.6.3 +isort==5.6.4 # Convert jupyter notebooks to markdown documents nbconvert==6.0.7 From fd9c8df049eda9598b596458e6263af5cdb08720 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2020 12:39:47 +0000 Subject: [PATCH 0802/1197] Bump ccxt from 1.36.2 to 1.36.12 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.36.2 to 1.36.12. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.36.2...1.36.12) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index ef9182f3b..17008d48a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.2 pandas==1.1.3 -ccxt==1.36.2 +ccxt==1.36.12 multidict==4.7.6 aiohttp==3.6.3 SQLAlchemy==1.3.19 From 7c1402ef1144d76f612d9194cfd0f4960e4390d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2020 13:06:37 +0000 Subject: [PATCH 0803/1197] Bump prompt-toolkit from 3.0.7 to 3.0.8 Bumps [prompt-toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) from 3.0.7 to 3.0.8. - [Release notes](https://github.com/prompt-toolkit/python-prompt-toolkit/releases) - [Changelog](https://github.com/prompt-toolkit/python-prompt-toolkit/blob/master/CHANGELOG) - [Commits](https://github.com/prompt-toolkit/python-prompt-toolkit/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a03b4ba20..9b0b53cae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,4 +37,4 @@ flask-cors==3.0.9 colorama==0.4.4 # Building config files interactively questionary==1.6.0 -prompt-toolkit==3.0.7 +prompt-toolkit==3.0.8 From 6a0ab83684fc1a2234408362b8e22d0237d538e8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2020 13:20:24 +0000 Subject: [PATCH 0804/1197] Bump sqlalchemy from 1.3.19 to 1.3.20 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.19 to 1.3.20. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 786b1d4a7..72d16465c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ pandas==1.1.3 ccxt==1.36.12 multidict==4.7.6 aiohttp==3.6.3 -SQLAlchemy==1.3.19 +SQLAlchemy==1.3.20 python-telegram-bot==13.0 arrow==0.17.0 cachetools==4.1.1 From 077374ac42eda2fb4166d46288cee439f3acd245 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Oct 2020 20:02:47 +0200 Subject: [PATCH 0805/1197] Implement generic solution for l2 limited limit --- freqtrade/exchange/binance.py | 13 +------------ freqtrade/exchange/exchange.py | 15 +++++++++++++-- tests/exchange/test_exchange.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index b85802aad..099f282a2 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -20,20 +20,9 @@ class Binance(Exchange): "order_time_in_force": ['gtc', 'fok', 'ioc'], "trades_pagination": "id", "trades_pagination_arg": "fromId", + "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], } - def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: - """ - get order book level 2 from exchange - - 20180619: binance support limits but only on specific range - """ - limit_range = [5, 10, 20, 50, 100, 500, 1000] - # get next-higher step in the limit_range list - limit = min(list(filter(lambda x: limit <= x, limit_range))) - - return super().fetch_l2_order_book(pair, limit) - def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: """ Verify stop_loss against stoploss-order value (limit or price) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index bbb94e61f..66126785f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -53,7 +53,7 @@ class Exchange: "ohlcv_partial_candle": True, "trades_pagination": "time", # Possible are "time" or "id" "trades_pagination_arg": "since", - + "l2_limit_range": None, } _ft_has: Dict = {} @@ -1069,6 +1069,16 @@ class Exchange: return self.fetch_stoploss_order(order_id, pair) return self.fetch_order(order_id, pair) + @staticmethod + def get_next_limit_in_list(limit: int, limit_range: Optional[List[int]]): + """ + Get next greater limit + """ + if not limit_range: + return limit + + return min(list(filter(lambda x: limit <= x, limit_range))) + @retrier def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: """ @@ -1077,9 +1087,10 @@ class Exchange: Returns a dict in the format {'asks': [price, volume], 'bids': [price, volume]} """ + limit1 = self.get_next_limit_in_list(limit, self._ft_has['l2_limit_range']) try: - return self._api.fetch_l2_order_book(pair, limit) + return self._api.fetch_l2_order_book(pair, limit1) except ccxt.NotSupported as e: raise OperationalException( f'Exchange {self._api.name} does not support fetching order book.' diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 7be9c77ac..4adc8b2ea 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1438,6 +1438,25 @@ def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog): assert log_has("Async code raised an exception: TypeError", caplog) +def test_get_next_limit_in_list(): + limit_range = [5, 10, 20, 50, 100, 500, 1000] + assert Exchange.get_next_limit_in_list(1, limit_range) == 5 + assert Exchange.get_next_limit_in_list(5, limit_range) == 5 + assert Exchange.get_next_limit_in_list(6, limit_range) == 10 + assert Exchange.get_next_limit_in_list(9, limit_range) == 10 + assert Exchange.get_next_limit_in_list(10, limit_range) == 10 + assert Exchange.get_next_limit_in_list(11, limit_range) == 20 + assert Exchange.get_next_limit_in_list(19, limit_range) == 20 + assert Exchange.get_next_limit_in_list(21, limit_range) == 50 + assert Exchange.get_next_limit_in_list(51, limit_range) == 100 + assert Exchange.get_next_limit_in_list(1000, limit_range) == 1000 + # assert Exchange.get_next_limit_in_list(1001, limit_range) == 1001 + + assert Exchange.get_next_limit_in_list(21, None) == 21 + assert Exchange.get_next_limit_in_list(100, None) == 100 + assert Exchange.get_next_limit_in_list(1000, None) == 1000 + + @pytest.mark.parametrize("exchange_name", EXCHANGES) def test_fetch_l2_order_book(default_conf, mocker, order_book_l2, exchange_name): default_conf['exchange']['name'] = exchange_name @@ -1450,6 +1469,20 @@ def test_fetch_l2_order_book(default_conf, mocker, order_book_l2, exchange_name) assert 'asks' in order_book assert len(order_book['bids']) == 10 assert len(order_book['asks']) == 10 + assert api_mock.fetch_l2_order_book.call_args_list[0][0][0] == 'ETH/BTC' + assert api_mock.fetch_l2_order_book.call_args_list[0][0][1] == 10 + + for val in [1, 5, 12, 20, 50, 100]: + api_mock.fetch_l2_order_book.reset_mock() + + order_book = exchange.fetch_l2_order_book(pair='ETH/BTC', limit=val) + assert api_mock.fetch_l2_order_book.call_args_list[0][0][0] == 'ETH/BTC' + # Not all exchanges support all limits for orderbook + if not exchange._ft_has['l2_limit_range'] or val in exchange._ft_has['l2_limit_range']: + assert api_mock.fetch_l2_order_book.call_args_list[0][0][1] == val + else: + next_limit = exchange.get_next_limit_in_list(val, exchange._ft_has['l2_limit_range']) + assert api_mock.fetch_l2_order_book.call_args_list[0][0][1] == next_limit @pytest.mark.parametrize("exchange_name", EXCHANGES) From 8962b6d5c9ec49462b43d09c9e66b9b88b87284d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Oct 2020 20:09:43 +0200 Subject: [PATCH 0806/1197] Add Bittrex subclass to correctly handle L2 orderbook --- freqtrade/exchange/__init__.py | 1 + freqtrade/exchange/bittrex.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 freqtrade/exchange/bittrex.py diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index cbcf961bc..5b58d7a95 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -5,6 +5,7 @@ from freqtrade.exchange.exchange import Exchange # isort: on from freqtrade.exchange.bibox import Bibox from freqtrade.exchange.binance import Binance +from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, get_exchange_bad_reason, is_exchange_bad, is_exchange_known_ccxt, is_exchange_officially_supported, diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py new file mode 100644 index 000000000..4318f9cf0 --- /dev/null +++ b/freqtrade/exchange/bittrex.py @@ -0,0 +1,23 @@ +""" Bittrex exchange subclass """ +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + + +logger = logging.getLogger(__name__) + + +class Bittrex(Exchange): + """ + Bittrex exchange class. Contains adjustments needed for Freqtrade to work + with this exchange. + + Please note that this exchange is not included in the list of exchanges + officially supported by the Freqtrade development team. So some features + may still not work as expected. + """ + + _ft_has: Dict = { + "l2_limit_range": [1, 25, 500], + } From 2ed20eee4e5a5e5c4b66deba5400293744271931 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Oct 2020 20:10:50 +0200 Subject: [PATCH 0807/1197] Configs should default to dry-run --- config.json.example | 4 ++-- config_full.json.example | 2 +- config_kraken.json.example | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/config.json.example b/config.json.example index ab517b77c..af45dac74 100644 --- a/config.json.example +++ b/config.json.example @@ -5,15 +5,15 @@ "tradable_balance_ratio": 0.99, "fiat_display_currency": "USD", "timeframe": "5m", - "dry_run": false, + "dry_run": true, "cancel_open_orders_on_exit": false, "unfilledtimeout": { "buy": 10, "sell": 30 }, "bid_strategy": { - "ask_last_balance": 0.0, "use_order_book": false, + "ask_last_balance": 0.0, "order_book_top": 1, "check_depth_of_market": { "enabled": false, diff --git a/config_full.json.example b/config_full.json.example index 659580fb1..45c5c695c 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -7,7 +7,7 @@ "amount_reserve_percent": 0.05, "amend_last_stake_amount": false, "last_stake_amount_min_ratio": 0.5, - "dry_run": false, + "dry_run": true, "cancel_open_orders_on_exit": false, "timeframe": "5m", "trailing_stop": false, diff --git a/config_kraken.json.example b/config_kraken.json.example index fd0b2b95d..5f3b57854 100644 --- a/config_kraken.json.example +++ b/config_kraken.json.example @@ -27,12 +27,11 @@ "use_sell_signal": true, "sell_profit_only": false, "ignore_roi_if_buy_signal": false - }, "exchange": { "name": "kraken", - "key": "", - "secret": "", + "key": "your_exchange_key", + "secret": "your_exchange_key", "ccxt_config": {"enableRateLimit": true}, "ccxt_async_config": { "enableRateLimit": true, From 8165cc11df8f3b68806ae2668b377ae4c17d4084 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Oct 2020 20:29:51 +0200 Subject: [PATCH 0808/1197] Change get_next_limit_in_list to use list comprehension --- freqtrade/exchange/exchange.py | 6 +++--- tests/data/test_dataprovider.py | 2 +- tests/exchange/test_exchange.py | 15 +++++++++++---- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 66126785f..ffe81ab39 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1072,12 +1072,12 @@ class Exchange: @staticmethod def get_next_limit_in_list(limit: int, limit_range: Optional[List[int]]): """ - Get next greater limit + Get next greater value in the list. + Used by fetch_l2_order_book if the api only supports a limited range """ if not limit_range: return limit - - return min(list(filter(lambda x: limit <= x, limit_range))) + return min([x for x in limit_range if limit <= x]) @retrier def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index c2ecf4b80..a64dce908 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -132,7 +132,7 @@ def test_orderbook(mocker, default_conf, order_book_l2): res = dp.orderbook('ETH/BTC', 5) assert order_book_l2.call_count == 1 assert order_book_l2.call_args_list[0][0][0] == 'ETH/BTC' - assert order_book_l2.call_args_list[0][0][1] == 5 + assert order_book_l2.call_args_list[0][0][1] >= 5 assert type(res) is dict assert 'bids' in res diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 4adc8b2ea..7b86d3866 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -11,7 +11,7 @@ from pandas import DataFrame from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException, OperationalException, TemporaryError) -from freqtrade.exchange import Binance, Exchange, Kraken +from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT, calculate_backoff) from freqtrade.exchange.exchange import (market_is_active, timeframe_to_minutes, timeframe_to_msecs, @@ -148,11 +148,19 @@ def test_exchange_resolver(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - exchange = ExchangeResolver.load_exchange('Bittrex', default_conf) + + exchange = ExchangeResolver.load_exchange('huobi', default_conf) assert isinstance(exchange, Exchange) assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog) caplog.clear() + exchange = ExchangeResolver.load_exchange('Bittrex', default_conf) + assert isinstance(exchange, Exchange) + assert isinstance(exchange, Bittrex) + assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", + caplog) + caplog.clear() + exchange = ExchangeResolver.load_exchange('kraken', default_conf) assert isinstance(exchange, Exchange) assert isinstance(exchange, Kraken) @@ -1470,9 +1478,8 @@ def test_fetch_l2_order_book(default_conf, mocker, order_book_l2, exchange_name) assert len(order_book['bids']) == 10 assert len(order_book['asks']) == 10 assert api_mock.fetch_l2_order_book.call_args_list[0][0][0] == 'ETH/BTC' - assert api_mock.fetch_l2_order_book.call_args_list[0][0][1] == 10 - for val in [1, 5, 12, 20, 50, 100]: + for val in [1, 5, 10, 12, 20, 50, 100]: api_mock.fetch_l2_order_book.reset_mock() order_book = exchange.fetch_l2_order_book(pair='ETH/BTC', limit=val) From 07da21e6335bc511e7ba363545b6b47b590721da Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Oct 2020 20:38:02 +0200 Subject: [PATCH 0809/1197] Fix problem when limit is > max allowed limit --- freqtrade/exchange/exchange.py | 2 +- tests/exchange/test_exchange.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ffe81ab39..c0d737f26 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1077,7 +1077,7 @@ class Exchange: """ if not limit_range: return limit - return min([x for x in limit_range if limit <= x]) + return min([x for x in limit_range if limit <= x] + [max(limit_range)]) @retrier def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 7b86d3866..19f2c7239 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1458,7 +1458,9 @@ def test_get_next_limit_in_list(): assert Exchange.get_next_limit_in_list(21, limit_range) == 50 assert Exchange.get_next_limit_in_list(51, limit_range) == 100 assert Exchange.get_next_limit_in_list(1000, limit_range) == 1000 - # assert Exchange.get_next_limit_in_list(1001, limit_range) == 1001 + # Going over the limit ... + assert Exchange.get_next_limit_in_list(1001, limit_range) == 1000 + assert Exchange.get_next_limit_in_list(2000, limit_range) == 1000 assert Exchange.get_next_limit_in_list(21, None) == 21 assert Exchange.get_next_limit_in_list(100, None) == 100 From ec713ff5aee4657a3e968ed515906d5c95ca1bb9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Oct 2020 06:26:57 +0200 Subject: [PATCH 0810/1197] Convert _rpc_analysed_history_full to static method --- freqtrade/rpc/api_server.py | 2 +- freqtrade/rpc/rpc.py | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 4e262b1ec..f31d7b0b5 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -563,7 +563,7 @@ class ApiServer(RPC): config.update({ 'strategy': strategy, }) - results = self._rpc_analysed_history_full(config, pair, timeframe, timerange) + results = RPC._rpc_analysed_history_full(config, pair, timeframe, timerange) return jsonify(results) @require_login diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b89284acf..911b2d731 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -656,8 +656,9 @@ class RPC: raise RPCException('Edge is not enabled.') return self._freqtrade.edge.accepted_pairs() - def _convert_dataframe_to_dict(self, strategy: str, pair: str, timeframe: str, - dataframe: DataFrame, last_analyzed: datetime) -> Dict[str, Any]: + @staticmethod + def _convert_dataframe_to_dict(strategy: str, pair: str, timeframe: str, dataframe: DataFrame, + last_analyzed: datetime) -> Dict[str, Any]: has_content = len(dataframe) != 0 buy_signals = 0 sell_signals = 0 @@ -711,7 +712,8 @@ class RPC: return self._convert_dataframe_to_dict(self._freqtrade.config['strategy'], pair, timeframe, _data, last_analyzed) - def _rpc_analysed_history_full(self, config, pair: str, timeframe: str, + @staticmethod + def _rpc_analysed_history_full(config, pair: str, timeframe: str, timerange: str) -> Dict[str, Any]: timerange_parsed = TimeRange.parse_timerange(timerange) @@ -726,8 +728,8 @@ class RPC: strategy = StrategyResolver.load_strategy(config) df_analyzed = strategy.analyze_ticker(_data[pair], {'pair': pair}) - return self._convert_dataframe_to_dict(strategy.get_strategy_name(), pair, timeframe, - df_analyzed, arrow.Arrow.utcnow().datetime) + return RPC._convert_dataframe_to_dict(strategy.get_strategy_name(), pair, timeframe, + df_analyzed, arrow.Arrow.utcnow().datetime) def _rpc_plot_config(self) -> Dict[str, Any]: From 685d18940a52eff4534d1a87bdb440cabb413bf1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Oct 2020 08:13:31 +0200 Subject: [PATCH 0811/1197] specify min-version for arrow int_timestamp was introduced in this version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9b57e8d2c..b47427709 100644 --- a/setup.py +++ b/setup.py @@ -69,7 +69,7 @@ setup(name='freqtrade', 'ccxt>=1.24.96', 'SQLAlchemy', 'python-telegram-bot', - 'arrow', + 'arrow>=0.17.0', 'cachetools', 'requests', 'urllib3', From 8cdc795a44306a57bccd4f7caf64c36a5d8d48b3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Oct 2020 07:39:12 +0200 Subject: [PATCH 0812/1197] Rename persistence.init to init_db --- freqtrade/commands/list_commands.py | 4 ++-- freqtrade/data/btanalysis.py | 5 ++--- freqtrade/freqtradebot.py | 8 ++++---- freqtrade/persistence/__init__.py | 2 +- freqtrade/persistence/models.py | 6 +++--- tests/commands/test_commands.py | 2 +- tests/conftest.py | 8 ++++---- tests/data/test_btanalysis.py | 2 +- tests/test_freqtradebot.py | 5 ++--- tests/test_main.py | 10 +++++----- tests/test_persistence.py | 20 ++++++++++---------- 11 files changed, 35 insertions(+), 37 deletions(-) diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index e81ecf871..9e6076dfb 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -205,14 +205,14 @@ def start_show_trades(args: Dict[str, Any]) -> None: """ import json - from freqtrade.persistence import Trade, init + from freqtrade.persistence import Trade, init_db config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) if 'db_url' not in config: raise OperationalException("--db-url is required for this command.") logger.info(f'Using DB: "{config["db_url"]}"') - init(config['db_url'], clean_open_orders=False) + init_db(config['db_url'], clean_open_orders=False) tfilter = [] if config.get('trade_ids'): diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 6af685712..513fba9e7 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -9,10 +9,9 @@ from typing import Any, Dict, Optional, Tuple, Union import numpy as np import pandas as pd -from freqtrade import persistence from freqtrade.constants import LAST_BT_RESULT_FN from freqtrade.misc import json_load -from freqtrade.persistence import Trade +from freqtrade.persistence import Trade, init_db logger = logging.getLogger(__name__) @@ -218,7 +217,7 @@ def load_trades_from_db(db_url: str, strategy: Optional[str] = None) -> pd.DataF Can also serve as protection to load the correct result. :return: Dataframe containing Trades """ - persistence.init(db_url, clean_open_orders=False) + init_db(db_url, clean_open_orders=False) columns = ["pair", "open_date", "close_date", "profit", "profit_percent", "open_rate", "close_rate", "amount", "trade_duration", "sell_reason", diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9562519aa..cfc68a3ec 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -12,7 +12,7 @@ from typing import Any, Dict, List, Optional import arrow from cachetools import TTLCache -from freqtrade import __version__, constants, persistence +from freqtrade import __version__, constants from freqtrade.configuration import validate_config_consistency from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider @@ -22,7 +22,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.pairlist.pairlistmanager import PairListManager -from freqtrade.persistence import Order, Trade +from freqtrade.persistence import Order, Trade, cleanup_db, init_db from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.state import State @@ -68,7 +68,7 @@ class FreqtradeBot: self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) - persistence.init(self.config.get('db_url', None), clean_open_orders=self.config['dry_run']) + init_db(self.config.get('db_url', None), clean_open_orders=self.config['dry_run']) self.wallets = Wallets(self.config, self.exchange) @@ -123,7 +123,7 @@ class FreqtradeBot: self.check_for_open_trades() self.rpc.cleanup() - persistence.cleanup() + cleanup_db() def startup(self) -> None: """ diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index ee2e40267..a3ec13e98 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,3 +1,3 @@ # flake8: noqa: F401 -from freqtrade.persistence.models import Order, Trade, clean_dry_run_db, cleanup, init +from freqtrade.persistence.models import Order, Trade, clean_dry_run_db, cleanup_db, init_db diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 8455a3b77..e5acbf937 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -29,7 +29,7 @@ _DECL_BASE: Any = declarative_base() _SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls' -def init(db_url: str, clean_open_orders: bool = False) -> None: +def init_db(db_url: str, clean_open_orders: bool = False) -> None: """ Initializes this module with the given config, registers all known command handlers @@ -72,7 +72,7 @@ def init(db_url: str, clean_open_orders: bool = False) -> None: clean_dry_run_db() -def cleanup() -> None: +def cleanup_db() -> None: """ Flushes all pending operations to disk. :return: None @@ -399,7 +399,7 @@ class Trade(_DECL_BASE): self.close(order['average']) else: raise ValueError(f'Unknown order type: {order_type}') - cleanup() + cleanup_db() def close(self, rate: float) -> None: """ diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 5b125697c..713386a8e 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -1149,7 +1149,7 @@ def test_start_list_data(testdatadir, capsys): @pytest.mark.usefixtures("init_persistence") def test_show_trades(mocker, fee, capsys, caplog): - mocker.patch("freqtrade.persistence.init") + mocker.patch("freqtrade.persistence.init_db") create_mock_trades(fee) args = [ "show-trades", diff --git a/tests/conftest.py b/tests/conftest.py index f90f0e62b..520b53b31 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,13 +13,13 @@ import numpy as np import pytest from telegram import Chat, Message, Update -from freqtrade import constants, persistence +from freqtrade import constants from freqtrade.commands import Arguments from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.persistence import Trade +from freqtrade.persistence import Trade, init_db from freqtrade.resolvers import ExchangeResolver from freqtrade.worker import Worker from tests.conftest_trades import (mock_trade_1, mock_trade_2, mock_trade_3, mock_trade_4, @@ -131,7 +131,7 @@ def patch_freqtradebot(mocker, config) -> None: :return: None """ mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - persistence.init(config['db_url']) + init_db(config['db_url']) patch_exchange(mocker) mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock()) @@ -219,7 +219,7 @@ def patch_coingekko(mocker) -> None: @pytest.fixture(scope='function') def init_persistence(default_conf): - persistence.init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) @pytest.fixture(scope="function") diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 7696dd96a..1592fac10 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -114,7 +114,7 @@ def test_load_trades_from_db(default_conf, fee, mocker): create_mock_trades(fee) # remove init so it does not init again - init_mock = mocker.patch('freqtrade.persistence.init', MagicMock()) + init_mock = mocker.patch('freqtrade.data.btanalysis.init_db', MagicMock()) trades = load_trades_from_db(db_url=default_conf['db_url']) assert init_mock.call_count == 1 diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 8af3e12a7..bb7ff26e7 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -15,8 +15,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie InvalidOrderException, OperationalException, PricingError, TemporaryError) from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.persistence import Trade -from freqtrade.persistence.models import Order +from freqtrade.persistence import Order, Trade from freqtrade.rpc import RPCMessageType from freqtrade.state import RunMode, State from freqtrade.strategy.interface import SellCheckTuple, SellType @@ -66,7 +65,7 @@ def test_process_stopped(mocker, default_conf) -> None: def test_bot_cleanup(mocker, default_conf, caplog) -> None: - mock_cleanup = mocker.patch('freqtrade.persistence.cleanup') + mock_cleanup = mocker.patch('freqtrade.freqtradebot.cleanup_db') coo_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cancel_all_open_orders') freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade.cleanup() diff --git a/tests/test_main.py b/tests/test_main.py index 9106d4c12..f55aea336 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -65,7 +65,7 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None: mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=Exception)) patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) + mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) args = ['trade', '-c', 'config.json.example'] @@ -83,7 +83,7 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None: 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()) + mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) args = ['trade', '-c', 'config.json.example'] @@ -104,7 +104,7 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None: 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()) + mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) args = ['trade', '-c', 'config.json.example'] @@ -155,7 +155,7 @@ def test_main_reload_config(mocker, default_conf, caplog) -> None: reconfigure_mock = mocker.patch('freqtrade.worker.Worker._reconfigure', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) + mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) args = Arguments(['trade', '-c', 'config.json.example']).get_parsed_arg() worker = Worker(args=args, config=default_conf) @@ -178,7 +178,7 @@ def test_reconfigure(mocker, default_conf) -> None: 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()) + mocker.patch('freqtrade.freqtradebot.init_db', MagicMock()) args = Arguments(['trade', '-c', 'config.json.example']).get_parsed_arg() worker = Worker(args=args, config=default_conf) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index adfa18876..4216565ac 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -8,13 +8,13 @@ from sqlalchemy import create_engine from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.persistence import Order, Trade, clean_dry_run_db, init +from freqtrade.persistence import Order, Trade, clean_dry_run_db, init_db from tests.conftest import create_mock_trades, log_has, log_has_re def test_init_create_session(default_conf): # Check if init create a session - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) assert hasattr(Trade, 'session') assert 'scoped_session' in type(Trade.session).__name__ @@ -24,7 +24,7 @@ def test_init_custom_db_url(default_conf, mocker): default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'}) create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite' @@ -33,7 +33,7 @@ def test_init_invalid_db_url(default_conf): # Update path to a value other than default, but still in-memory default_conf.update({'db_url': 'unknown:///some.url'}) with pytest.raises(OperationalException, match=r'.*no valid database URL*'): - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) def test_init_prod_db(default_conf, mocker): @@ -42,7 +42,7 @@ def test_init_prod_db(default_conf, mocker): create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite' @@ -53,7 +53,7 @@ def test_init_dryrun_db(default_conf, mocker): create_engine_mock = mocker.patch('freqtrade.persistence.models.create_engine', MagicMock()) - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.dryrun.sqlite' @@ -482,7 +482,7 @@ def test_migrate_old(mocker, default_conf, fee): engine.execute(insert_table_old) engine.execute(insert_table_old2) # Run init to test migration - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) assert len(Trade.query.filter(Trade.id == 1).all()) == 1 trade = Trade.query.filter(Trade.id == 1).first() @@ -581,7 +581,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): engine.execute("create table trades_bak1 as select * from trades") # Run init to test migration - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) assert len(Trade.query.filter(Trade.id == 1).all()) == 1 trade = Trade.query.filter(Trade.id == 1).first() @@ -661,7 +661,7 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog): engine.execute(insert_table_old) # Run init to test migration - init(default_conf['db_url'], default_conf['dry_run']) + init_db(default_conf['db_url'], default_conf['dry_run']) assert len(Trade.query.filter(Trade.id == 1).all()) == 1 trade = Trade.query.filter(Trade.id == 1).first() @@ -904,7 +904,7 @@ def test_to_json(default_conf, fee): def test_stoploss_reinitialization(default_conf, fee): - init(default_conf['db_url']) + init_db(default_conf['db_url']) trade = Trade( pair='ETH/BTC', stake_amount=0.001, From cd940daaf42cd5736876439b6c960b39acc8370c Mon Sep 17 00:00:00 2001 From: sanket-k Date: Sat, 17 Oct 2020 17:17:43 +0530 Subject: [PATCH 0813/1197] updated discord link to documentation. --- README.md | 4 ++++ docs/index.md | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/README.md b/README.md index feea47299..a3c6168fa 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,10 @@ information about the bot, we encourage you to join our slack channel. - [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE). +To those interested to check out the newly created discord channel. Click [here](https://discord.gg/MA9v74M) + +P.S. currently since discord channel is relatively new, answers to questions might be slightly delayed as currently the user base quite small. + ### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) If you discover a bug in the bot, please diff --git a/docs/index.md b/docs/index.md index e7fc54628..b2f3e417e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -64,6 +64,10 @@ For any questions not covered by the documentation or for further information ab Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) to join the Freqtrade Slack channel. +To those interested to check out the newly created discord channel. Click [here](https://discord.gg/MA9v74M) + +P.S. currently since discord channel is relatively new, answers to questions might be slightly delayed as currently the user base quite small. + ## Ready to try? Begin by reading our installation guide [for docker](docker.md), or for [installation without docker](installation.md). From 2591a34db4167cfb64f680493be1a0899719f02e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Oct 2020 16:18:52 +0200 Subject: [PATCH 0814/1197] Don't use arrow objects for backtesting --- freqtrade/optimize/backtesting.py | 11 +++++------ freqtrade/optimize/hyperopt.py | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index f59a782e8..61a83afc1 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -9,7 +9,6 @@ from copy import deepcopy from datetime import datetime, timedelta from typing import Any, Dict, List, NamedTuple, Optional, Tuple -import arrow from pandas import DataFrame from freqtrade.configuration import TimeRange, remove_credentials, validate_config_consistency @@ -173,7 +172,7 @@ class Backtesting: # Convert from Pandas to list for performance reasons # (Looping Pandas is slow.) - data[pair] = [x for x in df_analyzed.itertuples()] + data[pair] = [x for x in df_analyzed.itertuples(index=False)] return data def _get_close_rate(self, sell_row, trade: Trade, sell: SellCheckTuple, @@ -271,7 +270,7 @@ class Backtesting: return trades def backtest(self, processed: Dict, stake_amount: float, - start_date: arrow.Arrow, end_date: arrow.Arrow, + start_date: datetime, end_date: datetime, max_open_trades: int = 0, position_stacking: bool = False) -> DataFrame: """ Implement backtesting functionality @@ -321,7 +320,7 @@ class Backtesting: continue # Waits until the time-counter reaches the start of the data for this pair. - if row.date > tmp.datetime: + if row.date > tmp: continue indexes[pair] += 1 @@ -413,8 +412,8 @@ class Backtesting: results = self.backtest( processed=preprocessed, stake_amount=self.config['stake_amount'], - start_date=min_date, - end_date=max_date, + start_date=min_date.datetime, + end_date=max_date.datetime, max_open_trades=max_open_trades, position_stacking=position_stacking, ) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 5997e077b..e24212536 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -538,8 +538,8 @@ class Hyperopt: backtesting_results = self.backtesting.backtest( processed=processed, stake_amount=self.config['stake_amount'], - start_date=min_date, - end_date=max_date, + start_date=min_date.datetime, + end_date=max_date.datetime, max_open_trades=self.max_open_trades, position_stacking=self.position_stacking, ) From b80a219d035a11c665f3a683e25d3d7b3341f0d1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Oct 2020 16:35:23 +0200 Subject: [PATCH 0815/1197] Improve typehints for backtesting --- freqtrade/optimize/backtesting.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 61a83afc1..01885de26 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -147,7 +147,7 @@ class Backtesting: return data, timerange - def _get_ohlcv_as_lists(self, processed: Dict) -> Dict[str, DataFrame]: + def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, DataFrame]: """ Helper function to convert a processed dataframes into lists for performance reasons. @@ -215,7 +215,10 @@ class Backtesting: else: return sell_row.open - def _get_sell_trade_entry(self, trade: Trade, sell_row: DataFrame) -> Optional[BacktestResult]: + def _get_sell_trade_entry(self, trade: Trade, sell_row) -> Optional[BacktestResult]: + """ + sell_row is a named tuple with attributes for date, buy, open, close, sell, low, high. + """ sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, sell_row.buy, sell_row.sell, low=sell_row.low, high=sell_row.high) @@ -322,7 +325,6 @@ class Backtesting: # Waits until the time-counter reaches the start of the data for this pair. if row.date > tmp: continue - indexes[pair] += 1 # without positionstacking, we can only have one open trade per pair. From 5d3a67d324166cf68671d257be08b86f1f5d9152 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Oct 2020 16:38:16 +0200 Subject: [PATCH 0816/1197] Don't debug-log during backtesting. Even though log-messages are surpressed, calling "debug" will always have to do something. --- 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 01885de26..8b0c05718 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -350,7 +350,7 @@ class Backtesting: # Prevents buying if the trade-slot was freed in this candle open_trade_count_start += 1 open_trade_count += 1 - logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.") + # logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.") open_trades[pair].append(trade) for trade in open_trades[pair]: @@ -359,7 +359,7 @@ class Backtesting: trade_entry = self._get_sell_trade_entry(trade, row) # Sell occured if trade_entry: - logger.debug(f"{pair} - Backtesting sell {trade}") + # logger.debug(f"{pair} - Backtesting sell {trade}") open_trade_count -= 1 open_trades[pair].remove(trade) trades.append(trade_entry) From cf2ae788d74a189e5fbca693020cdad84288c4af Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Oct 2020 17:16:57 +0200 Subject: [PATCH 0817/1197] Convert backtesting rows to Tuples for performance gains --- freqtrade/optimize/backtesting.py | 71 +++++++++++++++++-------------- freqtrade/optimize/hyperopt.py | 24 +++++------ 2 files changed, 52 insertions(+), 43 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 8b0c05718..47bb9edd9 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -28,6 +28,15 @@ from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType logger = logging.getLogger(__name__) +# Indexes for backtest tuples +DATE_IDX = 0 +BUY_IDX = 1 +OPEN_IDX = 2 +CLOSE_IDX = 3 +SELL_IDX = 4 +LOW_IDX = 5 +HIGH_IDX = 6 + class BacktestResult(NamedTuple): """ @@ -115,7 +124,7 @@ class Backtesting: """ Load strategy into backtesting """ - self.strategy = strategy + self.strategy: IStrategy = strategy # Set stoploss_on_exchange to false for backtesting, # since a "perfect" stoploss-sell is assumed anyway # And the regular "stoploss" function would not apply to that case @@ -147,12 +156,14 @@ class Backtesting: return data, timerange - def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, DataFrame]: + def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]: """ Helper function to convert a processed dataframes into lists for performance reasons. Used by backtest() - so keep this optimized for performance. """ + # Every change to this headers list must evaluate further usages of the resulting tuple + # and eventually change the constants for indexes at the top headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high'] data: Dict = {} # Create dict with data @@ -172,10 +183,10 @@ class Backtesting: # Convert from Pandas to list for performance reasons # (Looping Pandas is slow.) - data[pair] = [x for x in df_analyzed.itertuples(index=False)] + data[pair] = [x for x in df_analyzed.itertuples(index=False, name=None)] return data - def _get_close_rate(self, sell_row, trade: Trade, sell: SellCheckTuple, + def _get_close_rate(self, sell_row: Tuple, trade: Trade, sell: SellCheckTuple, trade_dur: int) -> float: """ Get close rate for backtesting result @@ -186,12 +197,12 @@ class Backtesting: 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 is not None and roi_entry is not None: 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 - return sell_row.open + return sell_row[OPEN_IDX] # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) close_rate = - (trade.open_rate * roi + trade.open_rate * @@ -199,31 +210,29 @@ class Backtesting: if (trade_dur > 0 and trade_dur == roi_entry and roi_entry % self.timeframe_min == 0 - and sell_row.open > close_rate): + and sell_row[OPEN_IDX] > close_rate): # new ROI entry came into effect. # use Open rate if open_rate > calculated sell rate - return sell_row.open + return sell_row[OPEN_IDX] # 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(close_rate, sell_row.low) + return max(close_rate, sell_row[LOW_IDX]) else: # This should not be reached... - return sell_row.open + return sell_row[OPEN_IDX] else: - return sell_row.open + return sell_row[OPEN_IDX] - def _get_sell_trade_entry(self, trade: Trade, sell_row) -> Optional[BacktestResult]: - """ - sell_row is a named tuple with attributes for date, buy, open, close, sell, low, high. - """ + def _get_sell_trade_entry(self, trade: Trade, sell_row: Tuple) -> Optional[BacktestResult]: - sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, sell_row.buy, - sell_row.sell, low=sell_row.low, high=sell_row.high) + sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], sell_row[DATE_IDX], + sell_row[BUY_IDX], sell_row[SELL_IDX], + low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]) if sell.sell_flag: - trade_dur = int((sell_row.date - trade.open_date).total_seconds() // 60) + trade_dur = int((sell_row[DATE_IDX] - trade.open_date).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) return BacktestResult(pair=trade.pair, @@ -232,7 +241,7 @@ class Backtesting: open_date=trade.open_date, open_rate=trade.open_rate, open_fee=self.fee, - close_date=sell_row.date, + close_date=sell_row[DATE_IDX], close_rate=closerate, close_fee=self.fee, amount=trade.amount, @@ -242,8 +251,8 @@ class Backtesting: ) return None - def handle_left_open(self, open_trades: Dict[str, List], - data: Dict[str, DataFrame]) -> List[BacktestResult]: + def handle_left_open(self, open_trades: Dict[str, List[Trade]], + data: Dict[str, List[Tuple]]) -> List[BacktestResult]: """ Handling of left open trades at the end of backtesting """ @@ -254,17 +263,17 @@ class Backtesting: sell_row = data[pair][-1] trade_entry = BacktestResult(pair=trade.pair, profit_percent=trade.calc_profit_ratio( - rate=sell_row.open), - profit_abs=trade.calc_profit(rate=sell_row.open), + rate=sell_row[OPEN_IDX]), + profit_abs=trade.calc_profit(sell_row[OPEN_IDX]), open_date=trade.open_date, open_rate=trade.open_rate, open_fee=self.fee, - close_date=sell_row.date, - close_rate=sell_row.open, + close_date=sell_row[DATE_IDX], + close_rate=sell_row[OPEN_IDX], close_fee=self.fee, amount=trade.amount, trade_duration=int(( - sell_row.date - trade.open_date + sell_row[DATE_IDX] - trade.open_date ).total_seconds() // 60), open_at_end=True, sell_reason=SellType.FORCE_SELL @@ -323,7 +332,7 @@ class Backtesting: continue # Waits until the time-counter reaches the start of the data for this pair. - if row.date > tmp: + if row[DATE_IDX] > tmp: continue indexes[pair] += 1 @@ -333,14 +342,14 @@ class Backtesting: if ((position_stacking or len(open_trades[pair]) == 0) and max_open_trades > 0 and open_trade_count_start < max_open_trades and tmp != end_date - and row.buy == 1 and row.sell != 1): + and row[BUY_IDX] == 1 and row[SELL_IDX] != 1): # Enter trade trade = Trade( pair=pair, - open_rate=row.open, - open_date=row.date, + open_rate=row[OPEN_IDX], + open_date=row[DATE_IDX], stake_amount=stake_amount, - amount=round(stake_amount / row.open, 8), + amount=round(stake_amount / row[OPEN_IDX], 8), fee_open=self.fee, fee_close=self.fee, is_open=True, diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index e24212536..7870ba1cf 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -94,14 +94,14 @@ class Hyperopt: # Populate functions here (hasattr is slow so should not be run during "regular" operations) if hasattr(self.custom_hyperopt, 'populate_indicators'): - self.backtesting.strategy.advise_indicators = \ - self.custom_hyperopt.populate_indicators # type: ignore + self.backtesting.strategy.advise_indicators = ( # type: ignore + self.custom_hyperopt.populate_indicators) # type: ignore if hasattr(self.custom_hyperopt, 'populate_buy_trend'): - self.backtesting.strategy.advise_buy = \ - self.custom_hyperopt.populate_buy_trend # type: ignore + self.backtesting.strategy.advise_buy = ( # type: ignore + self.custom_hyperopt.populate_buy_trend) # type: ignore if hasattr(self.custom_hyperopt, 'populate_sell_trend'): - self.backtesting.strategy.advise_sell = \ - self.custom_hyperopt.populate_sell_trend # type: ignore + self.backtesting.strategy.advise_sell = ( # type: ignore + self.custom_hyperopt.populate_sell_trend) # type: ignore # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set if self.config.get('use_max_market_positions', True): @@ -508,16 +508,16 @@ class Hyperopt: params_details = self._get_params_details(params_dict) if self.has_space('roi'): - self.backtesting.strategy.minimal_roi = \ - self.custom_hyperopt.generate_roi_table(params_dict) + self.backtesting.strategy.minimal_roi = ( # type: ignore + self.custom_hyperopt.generate_roi_table(params_dict)) if self.has_space('buy'): - self.backtesting.strategy.advise_buy = \ - self.custom_hyperopt.buy_strategy_generator(params_dict) + self.backtesting.strategy.advise_buy = ( # type: ignore + self.custom_hyperopt.buy_strategy_generator(params_dict)) if self.has_space('sell'): - self.backtesting.strategy.advise_sell = \ - self.custom_hyperopt.sell_strategy_generator(params_dict) + self.backtesting.strategy.advise_sell = ( # type: ignore + self.custom_hyperopt.sell_strategy_generator(params_dict)) if self.has_space('stoploss'): self.backtesting.strategy.stoploss = params_dict['stoploss'] From 1c27aaab724b781487a92f103363071d1276c18f Mon Sep 17 00:00:00 2001 From: Xu Wang Date: Sun, 18 Oct 2020 20:24:13 +0100 Subject: [PATCH 0818/1197] Declare type of 'dur'. --- freqtrade/rpc/telegram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index bfe486951..8404625f1 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -6,7 +6,7 @@ This module manage Telegram communication import json import logging import arrow -from typing import Any, Callable, Dict +from typing import Any, Callable, Dict, List from tabulate import tabulate from telegram import ParseMode, ReplyKeyboardMarkup, Update @@ -780,7 +780,7 @@ class Telegram(RPC): ) # Duration - dur = {'Wins': [], 'Draws': [], 'Losses': []} + dur: Dict[str, List[int]] = {'Wins': [], 'Draws': [], 'Losses': []} for trade in trades_closed: if trade['close_date'] is not None and trade['open_date'] is not None: trade_dur = arrow.get(trade['close_date']) - arrow.get(trade['open_date']) From 79972985380df1f5dbf2500be32beafce8ac51c7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2020 05:37:36 +0000 Subject: [PATCH 0819/1197] Bump scipy from 1.5.2 to 1.5.3 Bumps [scipy](https://github.com/scipy/scipy) from 1.5.2 to 1.5.3. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.5.2...v1.5.3) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index d267961bd..5b68c1ea1 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.5.2 +scipy==1.5.3 scikit-learn==0.23.2 scikit-optimize==0.8.1 filelock==3.0.12 From 8975558595e9d12641c0e8f001723f145dd68a5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2020 05:37:54 +0000 Subject: [PATCH 0820/1197] Bump questionary from 1.6.0 to 1.7.0 Bumps [questionary](https://github.com/tmbo/questionary) from 1.6.0 to 1.7.0. - [Release notes](https://github.com/tmbo/questionary/releases) - [Commits](https://github.com/tmbo/questionary/compare/1.6.0...1.7.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 35f1e4c18..b1d21e697 100644 --- a/requirements.txt +++ b/requirements.txt @@ -36,5 +36,5 @@ flask-cors==3.0.9 # Support for colorized terminal output colorama==0.4.4 # Building config files interactively -questionary==1.6.0 +questionary==1.7.0 prompt-toolkit==3.0.8 From b7eec3fc8240f7c6430609a1cad1a9d3a4761613 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2020 05:37:57 +0000 Subject: [PATCH 0821/1197] Bump mkdocs-material from 6.0.2 to 6.1.0 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 6.0.2 to 6.1.0. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/6.0.2...6.1.0) Signed-off-by: dependabot[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 69ae33649..f30710a1f 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==6.0.2 +mkdocs-material==6.1.0 mdx_truly_sane_lists==1.2 pymdown-extensions==8.0.1 From 3e7c9bd485d53b89f357a9e062aada6377f1da6c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2020 11:57:03 +0000 Subject: [PATCH 0822/1197] Bump ccxt from 1.36.12 to 1.36.66 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.36.12 to 1.36.66. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.36.12...1.36.66) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b1d21e697..76e92eb3f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.2 pandas==1.1.3 -ccxt==1.36.12 +ccxt==1.36.66 multidict==4.7.6 aiohttp==3.6.3 SQLAlchemy==1.3.20 From 2d04c2dd4fd5ab2b24a438fd7a25927e08fe3ec1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Oct 2020 06:24:46 +0200 Subject: [PATCH 0823/1197] Fix small bug when cancel-order does not contain id happens with kraken ... --- freqtrade/persistence/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index e5acbf937..93b39860a 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -167,12 +167,12 @@ class Order(_DECL_BASE): """ Get all non-closed orders - useful when trying to batch-update orders """ - filtered_orders = [o for o in orders if o.order_id == order['id']] + filtered_orders = [o for o in orders if o.order_id == order.get('id')] if filtered_orders: oobj = filtered_orders[0] oobj.update_from_ccxt_object(order) else: - logger.warning(f"Did not find order for {order['id']}.") + logger.warning(f"Did not find order for {order}.") @staticmethod def parse_from_ccxt_object(order: Dict[str, Any], pair: str, side: str) -> 'Order': From 6eab20e337c5b855fd89e48a8a44a097e144a09f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Oct 2020 11:25:42 +0200 Subject: [PATCH 0824/1197] Use constant to format datetime --- freqtrade/persistence/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 93b39860a..03133107a 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -17,6 +17,7 @@ from sqlalchemy.orm.session import sessionmaker from sqlalchemy.pool import StaticPool from sqlalchemy.sql.schema import UniqueConstraint +from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.misc import safe_value_fallback from freqtrade.persistence.migrations import check_migrate @@ -251,7 +252,7 @@ class Trade(_DECL_BASE): 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' + open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed' return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, ' f'open_rate={self.open_rate:.8f}, open_since={open_since})') From e513871fd5eed769558159f64c6187390fa8565b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Oct 2020 11:28:34 +0200 Subject: [PATCH 0825/1197] Persist pairlocks closes #3034 --- docs/strategy-customization.md | 6 +-- freqtrade/freqtradebot.py | 7 +-- freqtrade/persistence/__init__.py | 3 +- freqtrade/persistence/models.py | 76 +++++++++++++++++++++++++++++++ freqtrade/strategy/interface.py | 29 ++++++------ tests/strategy/test_interface.py | 18 ++++---- tests/test_freqtradebot.py | 4 +- 7 files changed, 111 insertions(+), 32 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index a6cdef864..c0506203f 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -693,15 +693,15 @@ Locked pairs will show the message `Pair is currently locked.`. Sometimes it may be desired to lock a pair after certain events happen (e.g. multiple losing trades in a row). -Freqtrade has an easy method to do this from within the strategy, by calling `self.lock_pair(pair, until)`. -`until` must be a datetime object in the future, after which trading will be reenabled for that pair. +Freqtrade has an easy method to do this from within the strategy, by calling `self.lock_pair(pair, until, [reason])`. +`until` must be a datetime object in the future, after which trading will be re-enabled for that pair, while `reason` is an optional string detailing why the pair was locked. Locks can also be lifted manually, by calling `self.unlock_pair(pair)`. To verify if a pair is currently locked, use `self.is_pair_locked(pair)`. !!! Note - Locked pairs are not persisted, so a restart of the bot, or calling `/reload_config` will reset locked pairs. + Locked pairs will always be rounded up to the next candle. So assuming a `5m` timeframe, a lock with `until` set to 10:18 will lock the pair until the candle from 10:15-10:20 will be finished. !!! Warning Locking pairs is not functioning during backtesting. diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index cfc68a3ec..e004ed51c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -937,8 +937,8 @@ class FreqtradeBot: self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, stoploss_order=True) # Lock pair for one candle to prevent immediate rebuys - self.strategy.lock_pair(trade.pair, - timeframe_to_next_date(self.config['timeframe'])) + self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe']), + reason='auto_lock_1_candle') self._notify_sell(trade, "stoploss") return True @@ -1264,7 +1264,8 @@ class FreqtradeBot: Trade.session.flush() # Lock pair for one candle to prevent immediate rebuys - self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe'])) + self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe']), + reason='auto_lock_1_candle') self._notify_sell(trade, order_type) diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index a3ec13e98..e184e7d9a 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,3 +1,4 @@ # flake8: noqa: F401 -from freqtrade.persistence.models import Order, Trade, clean_dry_run_db, cleanup_db, init_db +from freqtrade.persistence.models import (Order, PairLock, Trade, clean_dry_run_db, cleanup_db, + init_db) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 03133107a..b2f8f4274 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -64,6 +64,9 @@ def init_db(db_url: str, clean_open_orders: bool = False) -> None: # Copy session attributes to order object too Order.session = Trade.session Order.query = Order.session.query_property() + PairLock.session = Trade.session + PairLock.query = PairLock.session.query_property() + previous_tables = inspect(engine).get_table_names() _DECL_BASE.metadata.create_all(engine) check_migrate(engine, decl_base=_DECL_BASE, previous_tables=previous_tables) @@ -655,3 +658,76 @@ class Trade(_DECL_BASE): trade.stop_loss = None trade.adjust_stop_loss(trade.open_rate, desired_stoploss) logger.info(f"New stoploss: {trade.stop_loss}.") + + +class PairLock(_DECL_BASE): + """ + Pair Locks database model. + """ + __tablename__ = 'pair_lock' + + id = Column(Integer, primary_key=True) + + pair = Column(String, nullable=False) + reason = Column(String, nullable=True) + # Time the pair was locked (start time) + lock_time = Column(DateTime, nullable=False) + # Time until the pair is locked (end time) + lock_end_time = Column(DateTime, nullable=False) + + active = Column(Boolean, nullable=False, default=True) + + def __repr__(self): + lock_time = self.open_date.strftime(DATETIME_PRINT_FORMAT) + lock_end_time = self.open_date.strftime(DATETIME_PRINT_FORMAT) + return (f'PairLock(id={self.id}, pair={self.pair}, lock_time={lock_time}, ' + f'lock_end_time={lock_end_time})') + + @staticmethod + def get_pair_locks(pair: str, now: Optional[datetime] = None) -> List['PairLock']: + """ + Get all locks for this pair + :param pair: Pair to check for + :param now: Datetime object (generated via datetime.utcnow()). defaults to datetime.utcnow() + """ + if not now: + now = datetime.now(timezone.utc) + + return PairLock.query.filter( + PairLock.pair == pair, + func.datetime(PairLock.lock_time) <= now, + func.datetime(PairLock.lock_end_time) >= now, + # Only active locks + PairLock.active.is_(True), + ).all() + + @staticmethod + def unlock_pair(pair: str, now: Optional[datetime] = None) -> None: + """ + Release all locks for this pair. + """ + if not now: + now = datetime.now(timezone.utc) + + logger.info(f"Releasing all locks for {pair}.") + locks = PairLock.get_pair_locks(pair, now) + for lock in locks: + lock.active = False + PairLock.session.flush() + + @staticmethod + def is_pair_locked(pair: str, now: Optional[datetime] = None) -> bool: + """ + :param pair: Pair to check for + :param now: Datetime object (generated via datetime.utcnow()). defaults to datetime.utcnow() + """ + if not now: + now = datetime.now(timezone.utc) + + return PairLock.query.filter( + PairLock.pair == pair, + func.datetime(PairLock.lock_time) <= now, + func.datetime(PairLock.lock_end_time) >= now, + # Only active locks + PairLock.active.is_(True), + ).scalar() is not None diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index b6b36b1a4..d9485e27a 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -17,7 +17,7 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange.exchange import timeframe_to_next_date -from freqtrade.persistence import Trade +from freqtrade.persistence import PairLock, Trade from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -133,7 +133,6 @@ class IStrategy(ABC): self.config = config # Dict to determine if analysis is necessary self._last_candle_seen_per_pair: Dict[str, datetime] = {} - self._pair_locked_until: Dict[str, datetime] = {} @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -278,7 +277,7 @@ class IStrategy(ABC): """ return self.__class__.__name__ - def lock_pair(self, pair: str, until: datetime) -> None: + def lock_pair(self, pair: str, until: datetime, reason: str = None) -> None: """ Locks pair until a given timestamp happens. Locked pairs are not analyzed, and are prevented from opening new trades. @@ -288,8 +287,15 @@ class IStrategy(ABC): :param until: datetime in UTC until the pair should be blocked from opening new trades. Needs to be timezone aware `datetime.now(timezone.utc)` """ - if pair not in self._pair_locked_until or self._pair_locked_until[pair] < until: - self._pair_locked_until[pair] = until + lock = PairLock( + pair=pair, + lock_time=datetime.now(timezone.utc), + lock_end_time=until, + reason=reason, + active=True + ) + PairLock.session.add(lock) + PairLock.session.flush() def unlock_pair(self, pair: str) -> None: """ @@ -298,8 +304,7 @@ class IStrategy(ABC): manually from within the strategy, to allow an easy way to unlock pairs. :param pair: Unlock pair to allow trading again """ - if pair in self._pair_locked_until: - del self._pair_locked_until[pair] + PairLock.unlock_pair(pair, datetime.now(timezone.utc)) def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool: """ @@ -311,15 +316,13 @@ class IStrategy(ABC): :param candle_date: Date of the last candle. Optional, defaults to current date :returns: locking state of the pair in question. """ - if pair not in self._pair_locked_until: - return False + if not candle_date: - return self._pair_locked_until[pair] >= datetime.now(timezone.utc) + # Simple call ... + return PairLock.is_pair_locked(pair, candle_date) else: - # Locking should happen until a new candle arrives lock_time = timeframe_to_next_date(self.timeframe, candle_date) - # lock_time = candle_date + timedelta(minutes=timeframe_to_minutes(self.timeframe)) - return self._pair_locked_until[pair] > lock_time + return PairLock.is_pair_locked(pair, lock_time) def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 729b14f7b..e9d9bcc75 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -1,5 +1,4 @@ # pragma pylint: disable=missing-docstring, C0103 - import logging from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock @@ -12,7 +11,7 @@ from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data from freqtrade.exceptions import StrategyError -from freqtrade.persistence import Trade +from freqtrade.persistence import PairLock, Trade from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from tests.conftest import log_has, log_has_re @@ -360,11 +359,12 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> assert log_has('Skipping TA Analysis for already analyzed candle', caplog) +@pytest.mark.usefixtures("init_persistence") def test_is_pair_locked(default_conf): default_conf.update({'strategy': 'DefaultStrategy'}) strategy = StrategyResolver.load_strategy(default_conf) - # dict should be empty - assert not strategy._pair_locked_until + # No lock should be present + assert len(PairLock.query.all()) == 0 pair = 'ETH/BTC' assert not strategy.is_pair_locked(pair) @@ -372,11 +372,6 @@ def test_is_pair_locked(default_conf): # ETH/BTC locked for 4 minutes assert strategy.is_pair_locked(pair) - # Test lock does not change - lock = strategy._pair_locked_until[pair] - strategy.lock_pair(pair, arrow.utcnow().shift(minutes=2).datetime) - assert lock == strategy._pair_locked_until[pair] - # XRP/BTC should not be locked now pair = 'XRP/BTC' assert not strategy.is_pair_locked(pair) @@ -393,7 +388,10 @@ def test_is_pair_locked(default_conf): # Lock until 14:30 lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc) strategy.lock_pair(pair, lock_time) - # Lock is in the past ... + # Lock is in the past, so we must fake the lock + lock = PairLock.query.filter(PairLock.pair == pair).first() + lock.lock_time = lock_time - timedelta(hours=2) + assert not strategy.is_pair_locked(pair) # latest candle is from 14:20, lock goes to 14:30 assert strategy.is_pair_locked(pair, lock_time + timedelta(minutes=-10)) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index bb7ff26e7..2a1b0c3cc 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -15,7 +15,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie InvalidOrderException, OperationalException, PricingError, TemporaryError) from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.persistence import Order, Trade +from freqtrade.persistence import Order, PairLock, Trade from freqtrade.rpc import RPCMessageType from freqtrade.state import RunMode, State from freqtrade.strategy.interface import SellCheckTuple, SellType @@ -2799,6 +2799,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c trade = Trade.query.first() Trade.session = MagicMock() + PairLock.session = MagicMock() freqtrade.config['dry_run'] = False trade.stoploss_order_id = "abcd" @@ -3249,7 +3250,6 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], sell_reason=SellType.STOP_LOSS) trade.close(ticker_sell_down()['bid']) - assert trade.pair in freqtrade.strategy._pair_locked_until assert freqtrade.strategy.is_pair_locked(trade.pair) # reinit - should buy other pair. From 7caa6cfe312621dae341245d973c4fcda6920c0a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Oct 2020 11:40:01 +0200 Subject: [PATCH 0826/1197] Add tests for pairlock --- freqtrade/persistence/models.py | 16 ++++++++++-- freqtrade/strategy/interface.py | 10 +------ tests/test_persistence.py | 46 ++++++++++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index b2f8f4274..4394b783a 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -678,11 +678,23 @@ class PairLock(_DECL_BASE): active = Column(Boolean, nullable=False, default=True) def __repr__(self): - lock_time = self.open_date.strftime(DATETIME_PRINT_FORMAT) - lock_end_time = self.open_date.strftime(DATETIME_PRINT_FORMAT) + lock_time = self.lock_time.strftime(DATETIME_PRINT_FORMAT) + lock_end_time = self.lock_end_time.strftime(DATETIME_PRINT_FORMAT) return (f'PairLock(id={self.id}, pair={self.pair}, lock_time={lock_time}, ' f'lock_end_time={lock_end_time})') + @staticmethod + def lock_pair(pair: str, until: datetime, reason: str = None) -> None: + lock = PairLock( + pair=pair, + lock_time=datetime.now(timezone.utc), + lock_end_time=until, + reason=reason, + active=True + ) + PairLock.session.add(lock) + PairLock.session.flush() + @staticmethod def get_pair_locks(pair: str, now: Optional[datetime] = None) -> List['PairLock']: """ diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index d9485e27a..36abfd05a 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -287,15 +287,7 @@ class IStrategy(ABC): :param until: datetime in UTC until the pair should be blocked from opening new trades. Needs to be timezone aware `datetime.now(timezone.utc)` """ - lock = PairLock( - pair=pair, - lock_time=datetime.now(timezone.utc), - lock_end_time=until, - reason=reason, - active=True - ) - PairLock.session.add(lock) - PairLock.session.flush() + PairLock.lock_pair(pair, until, reason) def unlock_pair(self, pair: str) -> None: """ diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 4216565ac..6ac1e36a4 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1,5 +1,6 @@ # pragma pylint: disable=missing-docstring, C0103 import logging +from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock import arrow @@ -8,7 +9,7 @@ from sqlalchemy import create_engine from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.persistence import Order, Trade, clean_dry_run_db, init_db +from freqtrade.persistence import Order, PairLock, Trade, clean_dry_run_db, init_db from tests.conftest import create_mock_trades, log_has, log_has_re @@ -1158,3 +1159,46 @@ def test_select_order(fee): assert order.ft_order_side == 'stoploss' order = trades[4].select_order('sell', False) assert order is None + + +@pytest.mark.usefixtures("init_persistence") +def test_PairLock(default_conf): + # No lock should be present + assert len(PairLock.query.all()) == 0 + + pair = 'ETH/BTC' + assert not PairLock.is_pair_locked(pair) + PairLock.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) + # ETH/BTC locked for 4 minutes + assert PairLock.is_pair_locked(pair) + + # XRP/BTC should not be locked now + pair = 'XRP/BTC' + assert not PairLock.is_pair_locked(pair) + + # Unlocking a pair that's not locked should not raise an error + PairLock.unlock_pair(pair) + + # Unlock original pair + pair = 'ETH/BTC' + PairLock.unlock_pair(pair) + assert not PairLock.is_pair_locked(pair) + + pair = 'BTC/USDT' + # Lock until 14:30 + lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc) + PairLock.lock_pair(pair, lock_time) + # Lock is in the past, so we must fake the lock + lock = PairLock.query.filter(PairLock.pair == pair).first() + lock.lock_time = lock_time - timedelta(hours=2) + + assert not PairLock.is_pair_locked(pair) + assert PairLock.is_pair_locked(pair, lock_time + timedelta(minutes=-10)) + assert PairLock.is_pair_locked(pair, lock_time + timedelta(minutes=-50)) + + # Should not be locked after time expired + assert not PairLock.is_pair_locked(pair, lock_time + timedelta(minutes=10)) + + locks = PairLock.get_pair_locks(pair, lock_time + timedelta(minutes=-2)) + assert len(locks) == 1 + assert 'PairLock' in str(locks[0]) From 7a9768ffa67a3640073c5bd5176fe5f24e054f61 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Oct 2020 15:15:35 +0200 Subject: [PATCH 0827/1197] Add /locks Telegram endpoint --- freqtrade/persistence/models.py | 35 ++++++++++++++++++--------- freqtrade/rpc/rpc.py | 13 +++++++++- freqtrade/rpc/telegram.py | 27 +++++++++++++++++++++ tests/rpc/test_rpc_telegram.py | 42 +++++++++++++++++++++++++++++++-- tests/test_persistence.py | 8 ++++++- 5 files changed, 110 insertions(+), 15 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 4394b783a..1f9a9a5b0 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -668,14 +668,14 @@ class PairLock(_DECL_BASE): id = Column(Integer, primary_key=True) - pair = Column(String, nullable=False) + pair = Column(String, nullable=False, index=True) reason = Column(String, nullable=True) # Time the pair was locked (start time) lock_time = Column(DateTime, nullable=False) # Time until the pair is locked (end time) lock_end_time = Column(DateTime, nullable=False) - active = Column(Boolean, nullable=False, default=True) + active = Column(Boolean, nullable=False, default=True, index=True) def __repr__(self): lock_time = self.lock_time.strftime(DATETIME_PRINT_FORMAT) @@ -696,21 +696,24 @@ class PairLock(_DECL_BASE): PairLock.session.flush() @staticmethod - def get_pair_locks(pair: str, now: Optional[datetime] = None) -> List['PairLock']: + def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> List['PairLock']: """ Get all locks for this pair - :param pair: Pair to check for - :param now: Datetime object (generated via datetime.utcnow()). defaults to datetime.utcnow() + :param pair: Pair to check for. Returns all current locks if pair is empty + :param now: Datetime object (generated via datetime.now(timezone.utc)). + defaults to datetime.utcnow() """ if not now: now = datetime.now(timezone.utc) + filters = [func.datetime(PairLock.lock_time) <= now, + func.datetime(PairLock.lock_end_time) >= now, + # Only active locks + PairLock.active.is_(True), ] + if pair: + filters.append(PairLock.pair == pair) return PairLock.query.filter( - PairLock.pair == pair, - func.datetime(PairLock.lock_time) <= now, - func.datetime(PairLock.lock_end_time) >= now, - # Only active locks - PairLock.active.is_(True), + *filters ).all() @staticmethod @@ -731,7 +734,8 @@ class PairLock(_DECL_BASE): def is_pair_locked(pair: str, now: Optional[datetime] = None) -> bool: """ :param pair: Pair to check for - :param now: Datetime object (generated via datetime.utcnow()). defaults to datetime.utcnow() + :param now: Datetime object (generated via datetime.now(timezone.utc)). + defaults to datetime.utcnow() """ if not now: now = datetime.now(timezone.utc) @@ -743,3 +747,12 @@ class PairLock(_DECL_BASE): # Only active locks PairLock.active.is_(True), ).scalar() is not None + + def to_json(self) -> Dict[str, Any]: + return { + 'pair': self.pair, + 'lock_time': self.lock_time, + 'lock_end_time': self.lock_end_time, + 'reason': self.reason, + 'active': self.active, + } diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 911b2d731..dbdb956b6 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -19,7 +19,7 @@ from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.loggers import bufferHandler from freqtrade.misc import shorten_date -from freqtrade.persistence import Trade +from freqtrade.persistence import PairLock, Trade from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State from freqtrade.strategy.interface import SellType @@ -599,6 +599,17 @@ class RPC: 'total_stake': sum((trade.open_rate * trade.amount) for trade in trades) } + def _rpc_locks(self) -> Dict[str, Any]: + """ Returns the current locks""" + if self._freqtrade.state != State.RUNNING: + raise RPCException('trader is not running') + + locks = PairLock.get_pair_locks(None) + return { + 'lock_count': len(locks), + 'locks': [lock.to_json() for lock in locks] + } + def _rpc_whitelist(self) -> Dict: """ Returns the currently active whitelist""" res = {'method': self._freqtrade.pairlists.name_list, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 7a6607632..6a0fd5acd 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -15,6 +15,7 @@ from telegram.ext import CallbackContext, CommandHandler, Updater from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ +from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.rpc import RPC, RPCException, RPCMessageType from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -100,6 +101,8 @@ class Telegram(RPC): CommandHandler('performance', self._performance), CommandHandler('daily', self._daily), CommandHandler('count', self._count), + CommandHandler('locks', self._locks), + CommandHandler(['reload_config', 'reload_conf'], self._reload_config), CommandHandler(['show_config', 'show_conf'], self._show_config), CommandHandler('stopbuy', self._stopbuy), @@ -608,6 +611,29 @@ class Telegram(RPC): except RPCException as e: self._send_msg(str(e)) + @authorized_only + def _locks(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /locks. + Returns the number of trades running + :param bot: telegram bot + :param update: message update + :return: None + """ + try: + locks = self._rpc_locks() + message = tabulate([[ + lock['pair'], + lock['lock_end_time'].strftime(DATETIME_PRINT_FORMAT), + lock['reason']] for lock in locks['locks']], + headers=['Pair', 'Until', 'Reason'], + tablefmt='simple') + message = "

    {}
    ".format(message) + logger.debug(message) + self._send_msg(message, parse_mode=ParseMode.HTML) + except RPCException as e: + self._send_msg(str(e)) + @authorized_only def _whitelist(self, update: Update, context: CallbackContext) -> None: """ @@ -720,6 +746,7 @@ class Telegram(RPC): "*/performance:* `Show performance of each finished trade grouped by pair`\n" "*/daily :* `Shows profit or loss per day, over the last n days`\n" "*/count:* `Show number of trades running compared to allowed number of trades`" + "*/locks:* `Show currently locked pairs`" "\n" "*/balance:* `Show account balance per currency`\n" "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 230df0df9..47d0a90c9 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2,6 +2,7 @@ # pragma pylint: disable=protected-access, unused-argument, invalid-name # pragma pylint: disable=too-many-lines, too-many-arguments +from freqtrade.persistence.models import PairLock import re from datetime import datetime from random import choice, randint @@ -75,8 +76,8 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], " - "['delete'], ['performance'], ['daily'], ['count'], ['reload_config', " - "'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], " + "['delete'], ['performance'], ['daily'], ['count'], ['locks'], " + "['reload_config', 'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], " "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']]") assert log_has(message_str, caplog) @@ -1024,6 +1025,43 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: assert msg in msg_mock.call_args_list[0][0][0] +def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None: + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker, + get_fee=fee, + ) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + patch_get_signal(freqtradebot, (True, False)) + telegram = Telegram(freqtradebot) + + freqtradebot.state = State.STOPPED + telegram._locks(update=update, context=MagicMock()) + assert msg_mock.call_count == 1 + assert 'not running' in msg_mock.call_args_list[0][0][0] + msg_mock.reset_mock() + freqtradebot.state = State.RUNNING + + PairLock.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason') + PairLock.lock_pair('XRP/BTC', arrow.utcnow().shift(minutes=20).datetime, 'deadbeef') + + telegram._locks(update=update, context=MagicMock()) + + assert 'Pair' in msg_mock.call_args_list[0][0][0] + assert 'Until' in msg_mock.call_args_list[0][0][0] + assert 'Reason\n' in msg_mock.call_args_list[0][0][0] + assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0] + assert 'XRP/BTC' in msg_mock.call_args_list[0][0][0] + assert 'deadbeef' in msg_mock.call_args_list[0][0][0] + assert 'randreason' in msg_mock.call_args_list[0][0][0] + + def test_whitelist_static(default_conf, update, mocker) -> None: msg_mock = MagicMock() mocker.patch.multiple( diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 6ac1e36a4..59b1fa31b 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1175,10 +1175,16 @@ def test_PairLock(default_conf): # XRP/BTC should not be locked now pair = 'XRP/BTC' assert not PairLock.is_pair_locked(pair) - # Unlocking a pair that's not locked should not raise an error PairLock.unlock_pair(pair) + PairLock.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) + assert PairLock.is_pair_locked(pair) + + # Get both locks from above + locks = PairLock.get_pair_locks(None) + assert len(locks) == 2 + # Unlock original pair pair = 'ETH/BTC' PairLock.unlock_pair(pair) From cd2866eaec6898299336128d52f32bd1c33528a5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Oct 2020 17:58:07 +0200 Subject: [PATCH 0828/1197] Add rest endpoint for /locks --- freqtrade/persistence/models.py | 7 +++++-- freqtrade/rpc/api_server.py | 10 ++++++++++ scripts/rest_client.py | 7 +++++++ tests/rpc/test_rpc_apiserver.py | 29 +++++++++++++++++++++++++++-- 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 1f9a9a5b0..2621bb852 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -751,8 +751,11 @@ class PairLock(_DECL_BASE): def to_json(self) -> Dict[str, Any]: return { 'pair': self.pair, - 'lock_time': self.lock_time, - 'lock_end_time': self.lock_end_time, + 'lock_time': self.lock_time.strftime("%Y-%m-%d %H:%M:%S"), + 'lock_timestamp': int(self.lock_time.replace(tzinfo=timezone.utc).timestamp() * 1000), + 'lock_end_time': self.lock_end_time.strftime("%Y-%m-%d %H:%M:%S"), + 'lock_end_timestamp': int(self.lock_end_time.replace(tzinfo=timezone.utc + ).timestamp() * 1000), 'reason': self.reason, 'active': self.active, } diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index f31d7b0b5..89e0f88c7 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -192,6 +192,7 @@ class ApiServer(RPC): self.app.add_url_rule(f'{BASE_URI}/balance', 'balance', view_func=self._balance, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/count', 'count', view_func=self._count, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/locks', 'locks', view_func=self._locks, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/daily', 'daily', view_func=self._daily, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/edge', 'edge', view_func=self._edge, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/logs', 'log', view_func=self._get_logs, methods=['GET']) @@ -350,6 +351,15 @@ class ApiServer(RPC): msg = self._rpc_count() return jsonify(msg) + @require_login + @rpc_catch_errors + def _locks(self): + """ + Handler for /locks. + Returns the number of trades running + """ + return jsonify(self._rpc_locks()) + @require_login @rpc_catch_errors def _daily(self): diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 46966d447..268e81397 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -111,6 +111,13 @@ class FtRestClient(): """ return self._get("count") + def locks(self): + """Return current locks + + :return: json object + """ + return self._get("locks") + def daily(self, days=None): """Return the amount of open trades. diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index d0e5d3c37..2b4242f5a 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -2,8 +2,9 @@ Unit test file for rpc/api_server.py """ -from datetime import datetime +from datetime import datetime, timedelta from pathlib import Path +from time import sleep from unittest.mock import ANY, MagicMock, PropertyMock import pytest @@ -12,7 +13,7 @@ from requests.auth import _basic_auth_str from freqtrade.__init__ import __version__ from freqtrade.loggers import setup_logging, setup_logging_pre -from freqtrade.persistence import Trade +from freqtrade.persistence import PairLock, Trade from freqtrade.rpc.api_server import BASE_URI, ApiServer from freqtrade.state import State from tests.conftest import create_mock_trades, get_patched_freqtradebot, log_has, patch_get_signal @@ -328,6 +329,30 @@ def test_api_count(botclient, mocker, ticker, fee, markets): assert rc.json["max"] == 1.0 +def test_api_locks(botclient): + ftbot, client = botclient + + rc = client_get(client, f"{BASE_URI}/locks") + assert_response(rc) + + assert 'locks' in rc.json + + assert rc.json['lock_count'] == 0 + assert rc.json['lock_count'] == len(rc.json['locks']) + + PairLock.lock_pair('ETH/BTC', datetime.utcnow() + timedelta(minutes=4), 'randreason') + PairLock.lock_pair('XRP/BTC', datetime.utcnow() + timedelta(minutes=20), 'deadbeef') + + rc = client_get(client, f"{BASE_URI}/locks") + assert_response(rc) + + assert rc.json['lock_count'] == 2 + assert rc.json['lock_count'] == len(rc.json['locks']) + assert 'ETH/BTC' in (rc.json['locks'][0]['pair'], rc.json['locks'][1]['pair']) + assert 'randreason' in (rc.json['locks'][0]['reason'], rc.json['locks'][1]['reason']) + assert 'deadbeef' in (rc.json['locks'][0]['reason'], rc.json['locks'][1]['reason']) + + def test_api_show_config(botclient, mocker): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) From 0daf77f313c71f00951787cbaa74cf2cc5a32dca Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Oct 2020 19:41:58 +0200 Subject: [PATCH 0829/1197] Don't check for lock start date --- freqtrade/persistence/models.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 2621bb852..da53a827f 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -706,8 +706,7 @@ class PairLock(_DECL_BASE): if not now: now = datetime.now(timezone.utc) - filters = [func.datetime(PairLock.lock_time) <= now, - func.datetime(PairLock.lock_end_time) >= now, + filters = [func.datetime(PairLock.lock_end_time) >= now, # Only active locks PairLock.active.is_(True), ] if pair: @@ -742,7 +741,6 @@ class PairLock(_DECL_BASE): return PairLock.query.filter( PairLock.pair == pair, - func.datetime(PairLock.lock_time) <= now, func.datetime(PairLock.lock_end_time) >= now, # Only active locks PairLock.active.is_(True), From 1156f5e68629bae8344a053c43f2cf48b8171a19 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Oct 2020 20:32:23 +0200 Subject: [PATCH 0830/1197] Use constant for times --- freqtrade/persistence/models.py | 10 +++++----- freqtrade/rpc/rpc.py | 4 ++-- freqtrade/rpc/telegram.py | 3 +-- tests/rpc/test_rpc_telegram.py | 3 +-- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index da53a827f..b06aab7df 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -281,7 +281,7 @@ class Trade(_DECL_BASE): 'fee_close_currency': self.fee_close_currency, 'open_date_hum': arrow.get(self.open_date).humanize(), - 'open_date': self.open_date.strftime("%Y-%m-%d %H:%M:%S"), + 'open_date': self.open_date.strftime(DATETIME_PRINT_FORMAT), 'open_timestamp': int(self.open_date.replace(tzinfo=timezone.utc).timestamp() * 1000), 'open_rate': self.open_rate, 'open_rate_requested': self.open_rate_requested, @@ -289,7 +289,7 @@ class Trade(_DECL_BASE): 'close_date_hum': (arrow.get(self.close_date).humanize() if self.close_date else None), - 'close_date': (self.close_date.strftime("%Y-%m-%d %H:%M:%S") + 'close_date': (self.close_date.strftime(DATETIME_PRINT_FORMAT) if self.close_date else None), 'close_timestamp': int(self.close_date.replace( tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None, @@ -305,7 +305,7 @@ class Trade(_DECL_BASE): 'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, 'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None, 'stoploss_order_id': self.stoploss_order_id, - 'stoploss_last_update': (self.stoploss_last_update.strftime("%Y-%m-%d %H:%M:%S") + 'stoploss_last_update': (self.stoploss_last_update.strftime(DATETIME_PRINT_FORMAT) if self.stoploss_last_update else None), 'stoploss_last_update_timestamp': int(self.stoploss_last_update.replace( tzinfo=timezone.utc).timestamp() * 1000) if self.stoploss_last_update else None, @@ -749,9 +749,9 @@ class PairLock(_DECL_BASE): def to_json(self) -> Dict[str, Any]: return { 'pair': self.pair, - 'lock_time': self.lock_time.strftime("%Y-%m-%d %H:%M:%S"), + 'lock_time': self.lock_time.strftime(DATETIME_PRINT_FORMAT), 'lock_timestamp': int(self.lock_time.replace(tzinfo=timezone.utc).timestamp() * 1000), - 'lock_end_time': self.lock_end_time.strftime("%Y-%m-%d %H:%M:%S"), + 'lock_end_time': self.lock_end_time.strftime(DATETIME_PRINT_FORMAT), 'lock_end_timestamp': int(self.lock_end_time.replace(tzinfo=timezone.utc ).timestamp() * 1000), 'reason': self.reason, diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index dbdb956b6..de8bcaefb 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -13,7 +13,7 @@ from numpy import NAN, int64, mean from pandas import DataFrame from freqtrade.configuration.timerange import TimeRange -from freqtrade.constants import CANCEL_REASON +from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT from freqtrade.data.history import load_data from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs @@ -649,7 +649,7 @@ class RPC: buffer = bufferHandler.buffer[-limit:] else: buffer = bufferHandler.buffer - records = [[datetime.fromtimestamp(r.created).strftime("%Y-%m-%d %H:%M:%S"), + records = [[datetime.fromtimestamp(r.created).strftime(DATETIME_PRINT_FORMAT), r.created * 1000, r.name, r.levelname, r.message + ('\n' + r.exc_text if r.exc_text else '')] for r in buffer] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 6a0fd5acd..f3581c38f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -102,7 +102,6 @@ class Telegram(RPC): CommandHandler('daily', self._daily), CommandHandler('count', self._count), CommandHandler('locks', self._locks), - CommandHandler(['reload_config', 'reload_conf'], self._reload_config), CommandHandler(['show_config', 'show_conf'], self._show_config), CommandHandler('stopbuy', self._stopbuy), @@ -624,7 +623,7 @@ class Telegram(RPC): locks = self._rpc_locks() message = tabulate([[ lock['pair'], - lock['lock_end_time'].strftime(DATETIME_PRINT_FORMAT), + lock['lock_end_time'], lock['reason']] for lock in locks['locks']], headers=['Pair', 'Until', 'Reason'], tablefmt='simple') diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 47d0a90c9..c412313ad 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2,7 +2,6 @@ # pragma pylint: disable=protected-access, unused-argument, invalid-name # pragma pylint: disable=too-many-lines, too-many-arguments -from freqtrade.persistence.models import PairLock import re from datetime import datetime from random import choice, randint @@ -19,7 +18,7 @@ from freqtrade.constants import CANCEL_REASON from freqtrade.edge import PairInfo from freqtrade.freqtradebot import FreqtradeBot from freqtrade.loggers import setup_logging -from freqtrade.persistence import Trade +from freqtrade.persistence import PairLock, Trade from freqtrade.rpc import RPCMessageType from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.state import State From 64e680d7eeb5f664cb241ecc596db962d0bbbe52 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Oct 2020 10:48:39 +0200 Subject: [PATCH 0831/1197] Document new api method --- docs/rest-api.md | 27 ++++++++++++++------------- freqtrade/rpc/telegram.py | 5 ++--- tests/strategy/test_interface.py | 3 --- tests/test_persistence.py | 3 --- 4 files changed, 16 insertions(+), 22 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 44f0b07cf..7726ab875 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -109,24 +109,25 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | Command | Description | |----------|-------------| | `ping` | Simple command testing the API Readiness - requires no authentication. -| `start` | Starts the trader -| `stop` | Stops the trader +| `start` | Starts the trader. +| `stop` | Stops the trader. | `stopbuy` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. -| `reload_config` | Reloads the configuration file +| `reload_config` | Reloads the configuration file. | `trades` | List last trades. | `delete_trade ` | Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange. -| `show_config` | Shows part of the current configuration with relevant settings to operation -| `logs` | Shows last log messages -| `status` | Lists all open trades -| `count` | Displays number of trades used and available -| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance +| `show_config` | Shows part of the current configuration with relevant settings to operation. +| `logs` | Shows last log messages. +| `status` | Lists all open trades. +| `count` | Displays number of trades used and available. +| `locks` | Displays currently locked pairs. +| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance. | `forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). | `forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). | `forcebuy [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True) -| `performance` | Show performance of each finished trade grouped by pair -| `balance` | Show account balance per currency -| `daily ` | Shows profit or loss per day, over the last n days (n defaults to 7) -| `whitelist` | Show the current whitelist +| `performance` | Show performance of each finished trade grouped by pair. +| `balance` | Show account balance per currency. +| `daily ` | Shows profit or loss per day, over the last n days (n defaults to 7). +| `whitelist` | Show the current whitelist. | `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. | `edge` | Show validated pairs by Edge if it is enabled. | `pair_candles` | Returns dataframe for a pair / timeframe combination while the bot is running. **Alpha** @@ -135,7 +136,7 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `strategies` | List strategies in strategy directory. **Alpha** | `strategy ` | Get specific Strategy content. **Alpha** | `available_pairs` | List available backtest data. **Alpha** -| `version` | Show version +| `version` | Show version. !!! Warning "Alpha status" Endpoints labeled with *Alpha status* above may change at any time without notice. diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index f3581c38f..18360c418 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -744,9 +744,8 @@ class Telegram(RPC): "*/delete :* `Instantly delete the given trade in the database`\n" "*/performance:* `Show performance of each finished trade grouped by pair`\n" "*/daily :* `Shows profit or loss per day, over the last n days`\n" - "*/count:* `Show number of trades running compared to allowed number of trades`" - "*/locks:* `Show currently locked pairs`" - "\n" + "*/count:* `Show number of active trades compared to allowed number of trades`\n" + "*/locks:* `Show currently locked pairs`\n" "*/balance:* `Show account balance per currency`\n" "*/stopbuy:* `Stops buying, but handles open trades gracefully` \n" "*/reload_config:* `Reload configuration file` \n" diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index e9d9bcc75..dc5cd47e7 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -388,9 +388,6 @@ def test_is_pair_locked(default_conf): # Lock until 14:30 lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc) strategy.lock_pair(pair, lock_time) - # Lock is in the past, so we must fake the lock - lock = PairLock.query.filter(PairLock.pair == pair).first() - lock.lock_time = lock_time - timedelta(hours=2) assert not strategy.is_pair_locked(pair) # latest candle is from 14:20, lock goes to 14:30 diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 59b1fa31b..243da3396 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1194,9 +1194,6 @@ def test_PairLock(default_conf): # Lock until 14:30 lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc) PairLock.lock_pair(pair, lock_time) - # Lock is in the past, so we must fake the lock - lock = PairLock.query.filter(PairLock.pair == pair).first() - lock.lock_time = lock_time - timedelta(hours=2) assert not PairLock.is_pair_locked(pair) assert PairLock.is_pair_locked(pair, lock_time + timedelta(minutes=-10)) From 5f63fdd8adb1533322afa5b8f3d67b3a77d70ec8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Oct 2020 19:39:38 +0200 Subject: [PATCH 0832/1197] Use better lock message --- freqtrade/freqtradebot.py | 4 ++-- freqtrade/rpc/api_server.py | 2 +- freqtrade/rpc/telegram.py | 6 +----- tests/rpc/test_rpc_apiserver.py | 1 - 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e004ed51c..6112a599e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -938,7 +938,7 @@ class FreqtradeBot: stoploss_order=True) # Lock pair for one candle to prevent immediate rebuys self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe']), - reason='auto_lock_1_candle') + reason='Auto lock') self._notify_sell(trade, "stoploss") return True @@ -1265,7 +1265,7 @@ class FreqtradeBot: # Lock pair for one candle to prevent immediate rebuys self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe']), - reason='auto_lock_1_candle') + reason='Auto lock') self._notify_sell(trade, order_type) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 89e0f88c7..be21179ad 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -356,7 +356,7 @@ class ApiServer(RPC): def _locks(self): """ Handler for /locks. - Returns the number of trades running + Returns the currently active locks. """ return jsonify(self._rpc_locks()) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 18360c418..3dcb7ab72 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -15,7 +15,6 @@ from telegram.ext import CallbackContext, CommandHandler, Updater from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ -from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.rpc import RPC, RPCException, RPCMessageType from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -614,10 +613,7 @@ class Telegram(RPC): def _locks(self, update: Update, context: CallbackContext) -> None: """ Handler for /locks. - Returns the number of trades running - :param bot: telegram bot - :param update: message update - :return: None + Returns the currently active locks """ try: locks = self._rpc_locks() diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 2b4242f5a..34e959875 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -4,7 +4,6 @@ Unit test file for rpc/api_server.py from datetime import datetime, timedelta from pathlib import Path -from time import sleep from unittest.mock import ANY, MagicMock, PropertyMock import pytest From adffd402ea6aff03719d21bf725448f8da0d8b27 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Oct 2020 20:11:38 +0200 Subject: [PATCH 0833/1197] Replace some pointless occurances of arrow --- freqtrade/commands/data_commands.py | 4 ++-- freqtrade/data/dataprovider.py | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 7102eee38..c12c20ddd 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -1,9 +1,9 @@ import logging import sys from collections import defaultdict +from datetime import datetime, timedelta from typing import Any, Dict, List -import arrow from freqtrade.configuration import TimeRange, setup_utils_configuration from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format @@ -29,7 +29,7 @@ def start_download_data(args: Dict[str, Any]) -> None: "You can only specify one or the other.") timerange = TimeRange() if 'days' in config: - time_since = arrow.utcnow().shift(days=-config['days']).strftime("%Y%m%d") + time_since = (datetime.now() - timedelta(days=config['days'])).strftime("%Y%m%d") timerange = TimeRange.parse_timerange(f'{time_since}-') if 'timerange' in config: diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 07dd94fc1..ba43044a1 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -8,7 +8,6 @@ import logging from datetime import datetime, timezone from typing import Any, Dict, List, Optional, Tuple -from arrow import Arrow from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe @@ -38,7 +37,7 @@ class DataProvider: :param timeframe: Timeframe to get data for :param dataframe: analyzed dataframe """ - self.__cached_pairs[(pair, timeframe)] = (dataframe, Arrow.utcnow().datetime) + self.__cached_pairs[(pair, timeframe)] = (dataframe, datetime.now(timezone.utc)) def add_pairlisthandler(self, pairlists) -> None: """ From fd6018f67aaa9620c237098f8139bcfb06832d62 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 21 Oct 2020 06:21:13 +0200 Subject: [PATCH 0834/1197] Fix dependency sorting --- freqtrade/commands/data_commands.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index c12c20ddd..b28e0c357 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -4,7 +4,6 @@ from collections import defaultdict from datetime import datetime, timedelta from typing import Any, Dict, List - from freqtrade.configuration import TimeRange, setup_utils_configuration from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data, From 42d9e2e7dc2b69e7120d2b47a3cae20ffb674070 Mon Sep 17 00:00:00 2001 From: pure <37597027+deeppaz@users.noreply.github.com> Date: Wed, 21 Oct 2020 17:06:26 +0300 Subject: [PATCH 0835/1197] update quick start steps --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index a3c6168fa..4290dec8f 100644 --- a/README.md +++ b/README.md @@ -55,9 +55,8 @@ Please find the complete documentation on our [website](https://www.freqtrade.io Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot. ```bash -git clone git@github.com:freqtrade/freqtrade.git +git clone -b develop https://github.com/freqtrade/freqtrade.git cd freqtrade -git checkout develop ./setup.sh --install ``` From a143f7bc434deb7cac6d37fe43dd66ce2d74a25a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 21 Oct 2020 19:35:57 +0200 Subject: [PATCH 0836/1197] Improve pairlock docstrings --- freqtrade/persistence/models.py | 3 +++ freqtrade/strategy/interface.py | 1 + 2 files changed, 4 insertions(+) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index b06aab7df..f1f9a4e67 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -719,6 +719,9 @@ class PairLock(_DECL_BASE): def unlock_pair(pair: str, now: Optional[datetime] = None) -> None: """ Release all locks for this pair. + :param pair: Pair to unlock + :param now: Datetime object (generated via datetime.now(timezone.utc)). + defaults to datetime.utcnow() """ if not now: now = datetime.now(timezone.utc) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 36abfd05a..e6256cafb 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -286,6 +286,7 @@ class IStrategy(ABC): :param pair: Pair to lock :param until: datetime in UTC until the pair should be blocked from opening new trades. Needs to be timezone aware `datetime.now(timezone.utc)` + :param reason: Optional string explaining why the pair was locked. """ PairLock.lock_pair(pair, until, reason) From cf1a7261987b806ac8004ac6cd7247cd0115e1da Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Oct 2020 07:35:25 +0200 Subject: [PATCH 0837/1197] Rename table to be inline with other table naming --- freqtrade/persistence/models.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index f1f9a4e67..477a94bad 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -664,7 +664,7 @@ class PairLock(_DECL_BASE): """ Pair Locks database model. """ - __tablename__ = 'pair_lock' + __tablename__ = 'pairlocks' id = Column(Integer, primary_key=True) @@ -673,7 +673,7 @@ class PairLock(_DECL_BASE): # Time the pair was locked (start time) lock_time = Column(DateTime, nullable=False) # Time until the pair is locked (end time) - lock_end_time = Column(DateTime, nullable=False) + lock_end_time = Column(DateTime, nullable=False, index=True) active = Column(Boolean, nullable=False, default=True, index=True) From ffcc47d8dd0f150f1b6610e8badc9711e48aaf67 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Oct 2020 07:42:47 +0200 Subject: [PATCH 0838/1197] Cleanup sql cheatsheet --- docs/sql_cheatsheet.md | 71 +++++------------------------------------- 1 file changed, 7 insertions(+), 64 deletions(-) diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index 168d416ab..569af33ff 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -43,52 +43,6 @@ sqlite3 .schema ``` -### Trade table structure - -```sql -CREATE TABLE trades( - id INTEGER NOT NULL, - exchange VARCHAR NOT NULL, - pair VARCHAR NOT NULL, - is_open BOOLEAN NOT NULL, - fee_open FLOAT NOT NULL, - fee_open_cost FLOAT, - fee_open_currency VARCHAR, - fee_close FLOAT NOT NULL, - fee_close_cost FLOAT, - fee_close_currency VARCHAR, - open_rate FLOAT, - open_rate_requested FLOAT, - open_trade_price FLOAT, - close_rate FLOAT, - close_rate_requested FLOAT, - close_profit FLOAT, - close_profit_abs FLOAT, - stake_amount FLOAT NOT NULL, - amount FLOAT, - open_date DATETIME NOT NULL, - close_date DATETIME, - open_order_id VARCHAR, - stop_loss FLOAT, - stop_loss_pct FLOAT, - initial_stop_loss FLOAT, - initial_stop_loss_pct FLOAT, - stoploss_order_id VARCHAR, - stoploss_last_update DATETIME, - max_rate FLOAT, - min_rate FLOAT, - sell_reason VARCHAR, - strategy VARCHAR, - timeframe INTEGER, - PRIMARY KEY (id), - CHECK (is_open IN (0, 1)) -); -CREATE INDEX ix_trades_stoploss_order_id ON trades (stoploss_order_id); -CREATE INDEX ix_trades_pair ON trades (pair); -CREATE INDEX ix_trades_is_open ON trades (is_open); - -``` - ## Get all trades in the table ```sql @@ -98,11 +52,11 @@ SELECT * FROM trades; ## Fix trade still open after a manual sell on the exchange !!! Warning - Manually selling a pair on the exchange will not be detected by the bot and it will try to sell anyway. Whenever possible, forcesell should be used to accomplish the same thing. - It is strongly advised to backup your database file before making any manual changes. + Manually selling a pair on the exchange will not be detected by the bot and it will try to sell anyway. Whenever possible, forcesell should be used to accomplish the same thing. + It is strongly advised to backup your database file before making any manual changes. !!! Note - This should not be necessary after /forcesell, as forcesell orders are closed automatically by the bot on the next iteration. + This should not be necessary after /forcesell, as forcesell orders are closed automatically by the bot on the next iteration. ```sql UPDATE trades @@ -128,23 +82,12 @@ SET is_open=0, WHERE id=31; ``` -## Manually insert a new trade - -```sql -INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date) -VALUES ('binance', 'ETH/BTC', 1, 0.0025, 0.0025, , , , '') -``` - -### Insert trade example - -```sql -INSERT INTO trades (exchange, pair, is_open, fee_open, fee_close, open_rate, stake_amount, amount, open_date) -VALUES ('binance', 'ETH/BTC', 1, 0.0025, 0.0025, 0.00258580, 0.002, 0.7715262081, '2020-06-28 12:44:24.000000') -``` - ## Remove trade from the database -Maybe you'd like to remove a trade from the database, because something went wrong. +!!! Tip "Use RPC Methods to delete trades" + Consider using `/delete ` via telegram or rest API. That's the recommended way to deleting trades. + +If you'd still like to remove a trade from the database directly, you can use the below query. ```sql DELETE FROM trades WHERE id = ; From cd8610cb2456b60bef31381fb9c999dade7a5738 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Oct 2020 07:50:09 +0200 Subject: [PATCH 0839/1197] Update readme.md files --- README.md | 29 ++++++++++++++--------------- docs/index.md | 10 ++++++---- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 4290dec8f..c9f4d0a52 100644 --- a/README.md +++ b/README.md @@ -110,17 +110,17 @@ optional arguments: Telegram is not mandatory. However, this is a great way to control your bot. More details and the full command list on our [documentation](https://www.freqtrade.io/en/latest/telegram-usage/) -- `/start`: Starts the trader -- `/stop`: Stops the trader -- `/status [table]`: Lists all open trades -- `/count`: Displays number of open trades +- `/start`: Starts the trader. +- `/stop`: Stops the trader. +- `/stopbuy`: Stop entering new trades. +- `/status [table]`: Lists all open trades. - `/profit`: Lists cumulative profit from all finished trades - `/forcesell |all`: Instantly sells the given trade (Ignoring `minimum_roi`). - `/performance`: Show performance of each finished trade grouped by pair -- `/balance`: Show account balance per currency -- `/daily `: Shows profit or loss per day, over the last n days -- `/help`: Show help message -- `/version`: Show version +- `/balance`: Show account balance per currency. +- `/daily `: Shows profit or loss per day, over the last n days. +- `/help`: Show help message. +- `/version`: Show version. ## Development branches @@ -132,16 +132,15 @@ The project is currently setup in two main branches: ## Support -### Help / Slack +### Help / Slack / Discord -For any questions not covered by the documentation or for further -information about the bot, we encourage you to join our slack channel. +For any questions not covered by the documentation or for further information about the bot, we encourage you to join our slack channel. - [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE). -To those interested to check out the newly created discord channel. Click [here](https://discord.gg/MA9v74M) +Alternatively, check out the newly created [discord server](https://discord.gg/MA9v74M). -P.S. currently since discord channel is relatively new, answers to questions might be slightly delayed as currently the user base quite small. +*Note*: Since the discord server is relatively new, answers to questions might be slightly delayed as currently the user base quite small. ### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) @@ -169,7 +168,7 @@ Please read our [Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md) to understand the requirements before sending your pull-requests. -Coding is not a neccessity to contribute - maybe start with improving our documentation? +Coding is not a necessity to contribute - maybe start with improving our documentation? Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase. **Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. @@ -180,7 +179,7 @@ Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/ ### Up-to-date clock -The clock must be accurate, syncronized to a NTP server very frequently to avoid problems with communication to the exchanges. +The clock must be accurate, synchronized to a NTP server very frequently to avoid problems with communication to the exchanges. ### Min hardware required diff --git a/docs/index.md b/docs/index.md index b2f3e417e..5608587db 100644 --- a/docs/index.md +++ b/docs/index.md @@ -59,15 +59,17 @@ Alternatively ## Support -### Help / Slack +### Help / Slack / Discord + For any questions not covered by the documentation or for further information about the bot, we encourage you to join our passionate Slack community. Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) to join the Freqtrade Slack channel. -To those interested to check out the newly created discord channel. Click [here](https://discord.gg/MA9v74M) +Alternatively, check out the newly created [discord server](https://discord.gg/MA9v74M). -P.S. currently since discord channel is relatively new, answers to questions might be slightly delayed as currently the user base quite small. +!!! Note + Since the discord server is relatively new, answers to questions might be slightly delayed as currently the user base quite small. ## Ready to try? -Begin by reading our installation guide [for docker](docker.md), or for [installation without docker](installation.md). +Begin by reading our installation guide [for docker](docker.md) (recommended), or for [installation without docker](installation.md). From 99990179534f947b25a465d3c890489ad1136bd0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Oct 2020 08:04:48 +0200 Subject: [PATCH 0840/1197] Fix small bug in case of duplicate locks --- freqtrade/persistence/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 477a94bad..22efed78d 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -747,7 +747,7 @@ class PairLock(_DECL_BASE): func.datetime(PairLock.lock_end_time) >= now, # Only active locks PairLock.active.is_(True), - ).scalar() is not None + ).first() is not None def to_json(self) -> Dict[str, Any]: return { From b8c12f657670ecc52bcb15bb9c600e63c4408314 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Oct 2020 07:45:11 +0200 Subject: [PATCH 0841/1197] Test if return value is an exception when downloading historic data --- freqtrade/exchange/exchange.py | 17 ++++++++++++----- tests/exchange/test_exchange.py | 9 +++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c0d737f26..da3c83b0c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -687,6 +687,9 @@ class Exchange: async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, since_ms: int) -> List: + """ + Download historic ohlcv + """ one_call = timeframe_to_msecs(timeframe) * self._ohlcv_candle_limit logger.debug( @@ -702,9 +705,14 @@ class Exchange: # Combine gathered results data: List = [] - for p, timeframe, res in results: + for res in results: + if isinstance(res, Exception): + logger.warning("Async code raised an exception: %s", res.__class__.__name__) + continue + # Deconstruct tuple if it's not an exception + p, _, new_data = res if p == pair: - data.extend(res) + data.extend(new_data) # Sort data again after extending the result - above calls return in "async order" data = sorted(data, key=lambda x: x[0]) logger.info("Downloaded data for %s with length %s.", pair, len(data)) @@ -741,9 +749,8 @@ class Exchange: if isinstance(res, Exception): logger.warning("Async code raised an exception: %s", res.__class__.__name__) continue - pair = res[0] - timeframe = res[1] - ticks = res[2] + # Deconstruct tuple (has 3 elements) + pair, timeframe, ticks = res # keeping last candle time as last refreshed time of the pair if ticks: self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000 diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 19f2c7239..b23c18bb3 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1295,6 +1295,15 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): # Returns twice the above OHLCV data assert len(ret) == 2 + caplog.clear() + + async def mock_get_candle_hist_error(pair, *args, **kwargs): + raise TimeoutError() + + exchange._async_get_candle_history = MagicMock(side_effect=mock_get_candle_hist_error) + ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000)) + assert log_has_re(r"Async code raised an exception: .*", caplog) + def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: ohlcv = [ From 3439e6c5c42693f5c75e2eb76557405e53806983 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2020 05:45:13 +0000 Subject: [PATCH 0842/1197] Bump urllib3 from 1.25.10 to 1.25.11 Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.25.10 to 1.25.11. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/master/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.25.10...1.25.11) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 76e92eb3f..c03f852c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ python-telegram-bot==13.0 arrow==0.17.0 cachetools==4.1.1 requests==2.24.0 -urllib3==1.25.10 +urllib3==1.25.11 wrapt==1.12.1 jsonschema==3.2.0 TA-Lib==0.4.19 From df5e6aa58b9d4e049a10e62b5c756da035d5bda5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2020 05:45:32 +0000 Subject: [PATCH 0843/1197] Bump aiohttp from 3.6.3 to 3.7.1 Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.6.3 to 3.7.1. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.6.3...v3.7.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 76e92eb3f..bf597c256 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pandas==1.1.3 ccxt==1.36.66 multidict==4.7.6 -aiohttp==3.6.3 +aiohttp==3.7.1 SQLAlchemy==1.3.20 python-telegram-bot==13.0 arrow==0.17.0 From 2831a78d0e0bf51bbd9388f6b3506658617cac2c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2020 05:45:33 +0000 Subject: [PATCH 0844/1197] Bump ccxt from 1.36.66 to 1.36.85 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.36.66 to 1.36.85. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.36.66...1.36.85) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 76e92eb3f..dcd02c752 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.2 pandas==1.1.3 -ccxt==1.36.66 +ccxt==1.36.85 multidict==4.7.6 aiohttp==3.6.3 SQLAlchemy==1.3.20 From 95d11bd0d24318bb9846bf0a00b7b688ebccba72 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2020 05:45:34 +0000 Subject: [PATCH 0845/1197] Bump python-rapidjson from 0.9.1 to 0.9.3 Bumps [python-rapidjson](https://github.com/python-rapidjson/python-rapidjson) from 0.9.1 to 0.9.3. - [Release notes](https://github.com/python-rapidjson/python-rapidjson/releases) - [Changelog](https://github.com/python-rapidjson/python-rapidjson/blob/master/CHANGES.rst) - [Commits](https://github.com/python-rapidjson/python-rapidjson/compare/v0.9.1...v0.9.3) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 76e92eb3f..fb961e5d5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,7 +23,7 @@ blosc==1.9.2 py_find_1st==1.1.4 # Load ticker files 30% faster -python-rapidjson==0.9.1 +python-rapidjson==0.9.3 # Notify systemd sdnotify==0.3.2 From 066ea45ce0e6ac37ca39261354cfcc82c4ac5030 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 26 Oct 2020 05:45:39 +0000 Subject: [PATCH 0846/1197] Bump plotly from 4.11.0 to 4.12.0 Bumps [plotly](https://github.com/plotly/plotly.py) from 4.11.0 to 4.12.0. - [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.11.0...v4.12.0) Signed-off-by: dependabot[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index 7c3e04723..bd40bc0b5 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.11.0 +plotly==4.12.0 From 442e9d20e12061e94e652070818414e724a9535a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Oct 2020 16:25:46 +0100 Subject: [PATCH 0847/1197] Remove pinned dependency of multidict --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1b97965f6..baf368adf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ numpy==1.19.2 pandas==1.1.3 ccxt==1.36.85 -multidict==4.7.6 aiohttp==3.7.1 SQLAlchemy==1.3.20 python-telegram-bot==13.0 From 69e8da30e53ee6a69183b9ce645b632eda37b0e4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Oct 2020 10:13:54 +0100 Subject: [PATCH 0848/1197] Ensure times that fall on a candle are also shifted --- tests/exchange/test_exchange.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index b23c18bb3..a01700e5d 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1,6 +1,6 @@ import copy import logging -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone from random import randint from unittest.mock import MagicMock, Mock, PropertyMock, patch @@ -2300,6 +2300,9 @@ def test_timeframe_to_next_date(): date = datetime.now(tz=timezone.utc) assert timeframe_to_next_date("5m") > date + date = datetime(2019, 8, 12, 13, 30, 0, tzinfo=timezone.utc) + assert timeframe_to_next_date("5m", date) == date + timedelta(minutes=5) + @pytest.mark.parametrize("market_symbol,base,quote,exchange,add_dict,expected_result", [ ("BTC/USDT", 'BTC', 'USDT', "binance", {}, True), From e602ac3406ae2d23210811bc2b21503b39303fa9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 25 Oct 2020 10:54:30 +0100 Subject: [PATCH 0849/1197] Introduce Pairlocks middleware --- freqtrade/freqtradebot.py | 32 +++---- freqtrade/persistence/__init__.py | 4 +- freqtrade/persistence/models.py | 50 +--------- freqtrade/persistence/pairlock_middleware.py | 97 ++++++++++++++++++++ freqtrade/rpc/rpc.py | 4 +- freqtrade/strategy/interface.py | 10 +- tests/pairlist/test_pairlocks.py | 81 ++++++++++++++++ tests/rpc/test_rpc_apiserver.py | 6 +- tests/rpc/test_rpc_telegram.py | 6 +- tests/strategy/test_interface.py | 4 +- tests/test_freqtradebot.py | 3 +- tests/test_persistence.py | 49 +--------- 12 files changed, 216 insertions(+), 130 deletions(-) create mode 100644 freqtrade/persistence/pairlock_middleware.py create mode 100644 tests/pairlist/test_pairlocks.py diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6112a599e..5a399801a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -345,23 +345,23 @@ class FreqtradeBot: whitelist = copy.deepcopy(self.active_pair_whitelist) if not whitelist: logger.info("Active pair whitelist is empty.") - else: - # Remove pairs for currently opened trades from the whitelist - for trade in Trade.get_open_trades(): - if trade.pair in whitelist: - whitelist.remove(trade.pair) - logger.debug('Ignoring %s in pair whitelist', trade.pair) + return trades_created + # Remove pairs for currently opened trades from the whitelist + for trade in Trade.get_open_trades(): + if trade.pair in whitelist: + whitelist.remove(trade.pair) + logger.debug('Ignoring %s in pair whitelist', trade.pair) - if not whitelist: - logger.info("No currency pair in active pair whitelist, " - "but checking to sell open trades.") - else: - # Create entity and execute trade for each pair from whitelist - for pair in whitelist: - try: - trades_created += self.create_trade(pair) - except DependencyException as exception: - logger.warning('Unable to create trade for %s: %s', pair, exception) + if not whitelist: + logger.info("No currency pair in active pair whitelist, " + "but checking to sell open trades.") + return trades_created + # Create entity and execute trade for each pair from whitelist + for pair in whitelist: + try: + trades_created += self.create_trade(pair) + except DependencyException as exception: + logger.warning('Unable to create trade for %s: %s', pair, exception) if not trades_created: logger.debug("Found no buy signals for whitelisted currencies. " diff --git a/freqtrade/persistence/__init__.py b/freqtrade/persistence/__init__.py index e184e7d9a..35f2bc406 100644 --- a/freqtrade/persistence/__init__.py +++ b/freqtrade/persistence/__init__.py @@ -1,4 +1,4 @@ # flake8: noqa: F401 -from freqtrade.persistence.models import (Order, PairLock, Trade, clean_dry_run_db, cleanup_db, - init_db) +from freqtrade.persistence.models import Order, Trade, clean_dry_run_db, cleanup_db, init_db +from freqtrade.persistence.pairlock_middleware import PairLocks diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 22efed78d..62b033bdf 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -684,19 +684,7 @@ class PairLock(_DECL_BASE): f'lock_end_time={lock_end_time})') @staticmethod - def lock_pair(pair: str, until: datetime, reason: str = None) -> None: - lock = PairLock( - pair=pair, - lock_time=datetime.now(timezone.utc), - lock_end_time=until, - reason=reason, - active=True - ) - PairLock.session.add(lock) - PairLock.session.flush() - - @staticmethod - def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> List['PairLock']: + def query_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> Query: """ Get all locks for this pair :param pair: Pair to check for. Returns all current locks if pair is empty @@ -713,41 +701,7 @@ class PairLock(_DECL_BASE): filters.append(PairLock.pair == pair) return PairLock.query.filter( *filters - ).all() - - @staticmethod - def unlock_pair(pair: str, now: Optional[datetime] = None) -> None: - """ - Release all locks for this pair. - :param pair: Pair to unlock - :param now: Datetime object (generated via datetime.now(timezone.utc)). - defaults to datetime.utcnow() - """ - if not now: - now = datetime.now(timezone.utc) - - logger.info(f"Releasing all locks for {pair}.") - locks = PairLock.get_pair_locks(pair, now) - for lock in locks: - lock.active = False - PairLock.session.flush() - - @staticmethod - def is_pair_locked(pair: str, now: Optional[datetime] = None) -> bool: - """ - :param pair: Pair to check for - :param now: Datetime object (generated via datetime.now(timezone.utc)). - defaults to datetime.utcnow() - """ - if not now: - now = datetime.now(timezone.utc) - - return PairLock.query.filter( - PairLock.pair == pair, - func.datetime(PairLock.lock_end_time) >= now, - # Only active locks - PairLock.active.is_(True), - ).first() is not None + ) def to_json(self) -> Dict[str, Any]: return { diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py new file mode 100644 index 000000000..ca2c31e36 --- /dev/null +++ b/freqtrade/persistence/pairlock_middleware.py @@ -0,0 +1,97 @@ + + +import logging +from datetime import datetime, timezone +from typing import List, Optional + +from freqtrade.persistence.models import PairLock + + +logger = logging.getLogger(__name__) + + +class PairLocks(): + """ + Pairlocks intermediate class + + """ + + use_db = True + locks: List[PairLock] = [] + + @staticmethod + def lock_pair(pair: str, until: datetime, reason: str = None) -> None: + lock = PairLock( + pair=pair, + lock_time=datetime.now(timezone.utc), + lock_end_time=until, + reason=reason, + active=True + ) + if PairLocks.use_db: + PairLock.session.add(lock) + PairLock.session.flush() + else: + PairLocks.locks.append(lock) + + @staticmethod + def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> List[PairLock]: + """ + Get all currently active locks for this pair + :param pair: Pair to check for. Returns all current locks if pair is empty + :param now: Datetime object (generated via datetime.now(timezone.utc)). + defaults to datetime.utcnow() + """ + if not now: + now = datetime.now(timezone.utc) + + if PairLocks.use_db: + return PairLock.query_pair_locks(pair, now).all() + else: + locks = [lock for lock in PairLocks.locks if ( + lock.lock_end_time > now + and lock.active is True + and (pair is None or lock.pair == pair) + )] + return locks + + @staticmethod + def unlock_pair(pair: str, now: Optional[datetime] = None) -> None: + """ + Release all locks for this pair. + :param pair: Pair to unlock + :param now: Datetime object (generated via datetime.now(timezone.utc)). + defaults to datetime.now(timezone.utc) + """ + if not now: + now = datetime.now(timezone.utc) + + logger.info(f"Releasing all locks for {pair}.") + locks = PairLocks.get_pair_locks(pair, now) + for lock in locks: + lock.active = False + if PairLocks.use_db: + PairLock.session.flush() + + @staticmethod + def is_global_lock(now: Optional[datetime] = None) -> bool: + """ + :param now: Datetime object (generated via datetime.now(timezone.utc)). + defaults to datetime.now(timezone.utc) + """ + if not now: + now = datetime.now(timezone.utc) + + return len(PairLocks.get_pair_locks('*', now)) > 0 + + @staticmethod + def is_pair_locked(pair: str, now: Optional[datetime] = None) -> bool: + """ + :param pair: Pair to check for + :param now: Datetime object (generated via datetime.now(timezone.utc)). + defaults to datetime.now(timezone.utc) + """ + if not now: + now = datetime.now(timezone.utc) + + return len(PairLocks.get_pair_locks(pair, now)) > 0 or PairLocks.is_global_lock(now) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index de8bcaefb..10aaf56fa 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -19,7 +19,7 @@ from freqtrade.exceptions import ExchangeError, PricingError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs from freqtrade.loggers import bufferHandler from freqtrade.misc import shorten_date -from freqtrade.persistence import PairLock, Trade +from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc.fiat_convert import CryptoToFiatConverter from freqtrade.state import State from freqtrade.strategy.interface import SellType @@ -604,7 +604,7 @@ class RPC: if self._freqtrade.state != State.RUNNING: raise RPCException('trader is not running') - locks = PairLock.get_pair_locks(None) + locks = PairLocks.get_pair_locks(None) return { 'lock_count': len(locks), 'locks': [lock.to_json() for lock in locks] diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index e6256cafb..1c6aa535d 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -17,7 +17,7 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes from freqtrade.exchange.exchange import timeframe_to_next_date -from freqtrade.persistence import PairLock, Trade +from freqtrade.persistence import PairLocks, Trade from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -288,7 +288,7 @@ class IStrategy(ABC): Needs to be timezone aware `datetime.now(timezone.utc)` :param reason: Optional string explaining why the pair was locked. """ - PairLock.lock_pair(pair, until, reason) + PairLocks.lock_pair(pair, until, reason) def unlock_pair(self, pair: str) -> None: """ @@ -297,7 +297,7 @@ class IStrategy(ABC): manually from within the strategy, to allow an easy way to unlock pairs. :param pair: Unlock pair to allow trading again """ - PairLock.unlock_pair(pair, datetime.now(timezone.utc)) + PairLocks.unlock_pair(pair, datetime.now(timezone.utc)) def is_pair_locked(self, pair: str, candle_date: datetime = None) -> bool: """ @@ -312,10 +312,10 @@ class IStrategy(ABC): if not candle_date: # Simple call ... - return PairLock.is_pair_locked(pair, candle_date) + return PairLocks.is_pair_locked(pair, candle_date) else: lock_time = timeframe_to_next_date(self.timeframe, candle_date) - return PairLock.is_pair_locked(pair, lock_time) + return PairLocks.is_pair_locked(pair, lock_time) def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ diff --git a/tests/pairlist/test_pairlocks.py b/tests/pairlist/test_pairlocks.py new file mode 100644 index 000000000..3ed7d643e --- /dev/null +++ b/tests/pairlist/test_pairlocks.py @@ -0,0 +1,81 @@ +from datetime import datetime, timedelta, timezone + +import arrow +import pytest + +from freqtrade.persistence import PairLocks +from freqtrade.persistence.models import PairLock + + +@pytest.mark.parametrize('use_db', (False, True)) +@pytest.mark.usefixtures("init_persistence") +def test_PairLocks(use_db): + # No lock should be present + if use_db: + assert len(PairLock.query.all()) == 0 + else: + PairLocks.use_db = False + + assert PairLocks.use_db == use_db + + pair = 'ETH/BTC' + assert not PairLocks.is_pair_locked(pair) + PairLocks.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) + # ETH/BTC locked for 4 minutes + assert PairLocks.is_pair_locked(pair) + + # XRP/BTC should not be locked now + pair = 'XRP/BTC' + assert not PairLocks.is_pair_locked(pair) + # Unlocking a pair that's not locked should not raise an error + PairLocks.unlock_pair(pair) + + PairLocks.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) + assert PairLocks.is_pair_locked(pair) + + # Get both locks from above + locks = PairLocks.get_pair_locks(None) + assert len(locks) == 2 + + # Unlock original pair + pair = 'ETH/BTC' + PairLocks.unlock_pair(pair) + assert not PairLocks.is_pair_locked(pair) + assert not PairLocks.is_global_lock() + + pair = 'BTC/USDT' + # Lock until 14:30 + lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc) + PairLocks.lock_pair(pair, lock_time) + + assert not PairLocks.is_pair_locked(pair) + assert PairLocks.is_pair_locked(pair, lock_time + timedelta(minutes=-10)) + assert not PairLocks.is_global_lock(lock_time + timedelta(minutes=-10)) + assert PairLocks.is_pair_locked(pair, lock_time + timedelta(minutes=-50)) + assert not PairLocks.is_global_lock(lock_time + timedelta(minutes=-50)) + + # Should not be locked after time expired + assert not PairLocks.is_pair_locked(pair, lock_time + timedelta(minutes=10)) + + locks = PairLocks.get_pair_locks(pair, lock_time + timedelta(minutes=-2)) + assert len(locks) == 1 + assert 'PairLock' in str(locks[0]) + + # Unlock all + PairLocks.unlock_pair(pair, lock_time + timedelta(minutes=-2)) + assert not PairLocks.is_global_lock(lock_time + timedelta(minutes=-50)) + + # Global lock + PairLocks.lock_pair('*', lock_time) + assert PairLocks.is_global_lock(lock_time + timedelta(minutes=-50)) + # Global lock also locks every pair seperately + assert PairLocks.is_pair_locked(pair, lock_time + timedelta(minutes=-50)) + assert PairLocks.is_pair_locked('XRP/USDT', lock_time + timedelta(minutes=-50)) + + if use_db: + assert len(PairLock.query.all()) > 0 + else: + # Nothing was pushed to the database + assert len(PairLock.query.all()) == 0 + # Reset use-db variable + PairLocks.use_db = True diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 34e959875..0dd15a777 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -12,7 +12,7 @@ from requests.auth import _basic_auth_str from freqtrade.__init__ import __version__ from freqtrade.loggers import setup_logging, setup_logging_pre -from freqtrade.persistence import PairLock, Trade +from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc.api_server import BASE_URI, ApiServer from freqtrade.state import State from tests.conftest import create_mock_trades, get_patched_freqtradebot, log_has, patch_get_signal @@ -339,8 +339,8 @@ def test_api_locks(botclient): assert rc.json['lock_count'] == 0 assert rc.json['lock_count'] == len(rc.json['locks']) - PairLock.lock_pair('ETH/BTC', datetime.utcnow() + timedelta(minutes=4), 'randreason') - PairLock.lock_pair('XRP/BTC', datetime.utcnow() + timedelta(minutes=20), 'deadbeef') + PairLocks.lock_pair('ETH/BTC', datetime.utcnow() + timedelta(minutes=4), 'randreason') + PairLocks.lock_pair('XRP/BTC', datetime.utcnow() + timedelta(minutes=20), 'deadbeef') rc = client_get(client, f"{BASE_URI}/locks") assert_response(rc) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index c412313ad..f1246005f 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -18,7 +18,7 @@ from freqtrade.constants import CANCEL_REASON from freqtrade.edge import PairInfo from freqtrade.freqtradebot import FreqtradeBot from freqtrade.loggers import setup_logging -from freqtrade.persistence import PairLock, Trade +from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPCMessageType from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.state import State @@ -1047,8 +1047,8 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None msg_mock.reset_mock() freqtradebot.state = State.RUNNING - PairLock.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason') - PairLock.lock_pair('XRP/BTC', arrow.utcnow().shift(minutes=20).datetime, 'deadbeef') + PairLocks.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason') + PairLocks.lock_pair('XRP/BTC', arrow.utcnow().shift(minutes=20).datetime, 'deadbeef') telegram._locks(update=update, context=MagicMock()) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index dc5cd47e7..96d4882da 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -11,7 +11,7 @@ from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data from freqtrade.exceptions import StrategyError -from freqtrade.persistence import PairLock, Trade +from freqtrade.persistence import PairLocks, Trade from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from tests.conftest import log_has, log_has_re @@ -364,7 +364,7 @@ def test_is_pair_locked(default_conf): default_conf.update({'strategy': 'DefaultStrategy'}) strategy = StrategyResolver.load_strategy(default_conf) # No lock should be present - assert len(PairLock.query.all()) == 0 + assert len(PairLocks.get_pair_locks(None)) == 0 pair = 'ETH/BTC' assert not strategy.is_pair_locked(pair) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 2a1b0c3cc..29df9c012 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -15,7 +15,8 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie InvalidOrderException, OperationalException, PricingError, TemporaryError) from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.persistence import Order, PairLock, Trade +from freqtrade.persistence import Order, PairLocks, Trade +from freqtrade.persistence.models import PairLock from freqtrade.rpc import RPCMessageType from freqtrade.state import RunMode, State from freqtrade.strategy.interface import SellCheckTuple, SellType diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 243da3396..4216565ac 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -1,6 +1,5 @@ # pragma pylint: disable=missing-docstring, C0103 import logging -from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock import arrow @@ -9,7 +8,7 @@ from sqlalchemy import create_engine from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.persistence import Order, PairLock, Trade, clean_dry_run_db, init_db +from freqtrade.persistence import Order, Trade, clean_dry_run_db, init_db from tests.conftest import create_mock_trades, log_has, log_has_re @@ -1159,49 +1158,3 @@ def test_select_order(fee): assert order.ft_order_side == 'stoploss' order = trades[4].select_order('sell', False) assert order is None - - -@pytest.mark.usefixtures("init_persistence") -def test_PairLock(default_conf): - # No lock should be present - assert len(PairLock.query.all()) == 0 - - pair = 'ETH/BTC' - assert not PairLock.is_pair_locked(pair) - PairLock.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) - # ETH/BTC locked for 4 minutes - assert PairLock.is_pair_locked(pair) - - # XRP/BTC should not be locked now - pair = 'XRP/BTC' - assert not PairLock.is_pair_locked(pair) - # Unlocking a pair that's not locked should not raise an error - PairLock.unlock_pair(pair) - - PairLock.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) - assert PairLock.is_pair_locked(pair) - - # Get both locks from above - locks = PairLock.get_pair_locks(None) - assert len(locks) == 2 - - # Unlock original pair - pair = 'ETH/BTC' - PairLock.unlock_pair(pair) - assert not PairLock.is_pair_locked(pair) - - pair = 'BTC/USDT' - # Lock until 14:30 - lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc) - PairLock.lock_pair(pair, lock_time) - - assert not PairLock.is_pair_locked(pair) - assert PairLock.is_pair_locked(pair, lock_time + timedelta(minutes=-10)) - assert PairLock.is_pair_locked(pair, lock_time + timedelta(minutes=-50)) - - # Should not be locked after time expired - assert not PairLock.is_pair_locked(pair, lock_time + timedelta(minutes=10)) - - locks = PairLock.get_pair_locks(pair, lock_time + timedelta(minutes=-2)) - assert len(locks) == 1 - assert 'PairLock' in str(locks[0]) From 9c54c9a2bfdec85369c02d93d649f45e130ba72c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Oct 2020 07:36:25 +0100 Subject: [PATCH 0850/1197] Use correct timezone for tests --- tests/rpc/test_rpc_apiserver.py | 6 +++--- tests/strategy/test_interface.py | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 0dd15a777..7b4e2e153 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -2,7 +2,7 @@ Unit test file for rpc/api_server.py """ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from pathlib import Path from unittest.mock import ANY, MagicMock, PropertyMock @@ -339,8 +339,8 @@ def test_api_locks(botclient): assert rc.json['lock_count'] == 0 assert rc.json['lock_count'] == len(rc.json['locks']) - PairLocks.lock_pair('ETH/BTC', datetime.utcnow() + timedelta(minutes=4), 'randreason') - PairLocks.lock_pair('XRP/BTC', datetime.utcnow() + timedelta(minutes=20), 'deadbeef') + PairLocks.lock_pair('ETH/BTC', datetime.now(timezone.utc) + timedelta(minutes=4), 'randreason') + PairLocks.lock_pair('XRP/BTC', datetime.now(timezone.utc) + timedelta(minutes=20), 'deadbeef') rc = client_get(client, f"{BASE_URI}/locks") assert_response(rc) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 96d4882da..e87fb7182 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -362,13 +362,14 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> @pytest.mark.usefixtures("init_persistence") def test_is_pair_locked(default_conf): default_conf.update({'strategy': 'DefaultStrategy'}) + PairLocks.timeframe = default_conf['timeframe'] strategy = StrategyResolver.load_strategy(default_conf) # No lock should be present assert len(PairLocks.get_pair_locks(None)) == 0 pair = 'ETH/BTC' assert not strategy.is_pair_locked(pair) - strategy.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) + strategy.lock_pair(pair, arrow.now(timezone.utc).shift(minutes=4).datetime) # ETH/BTC locked for 4 minutes assert strategy.is_pair_locked(pair) From 6c913fa6179e93ba922a6a7ef62fec839391d485 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 26 Oct 2020 07:37:07 +0100 Subject: [PATCH 0851/1197] Fix locking - should round before storing to have a consistent picture --- docs/strategy-customization.md | 2 +- freqtrade/freqtradebot.py | 18 ++++++++++-------- freqtrade/persistence/models.py | 2 +- freqtrade/persistence/pairlock_middleware.py | 7 +++++-- tests/strategy/test_interface.py | 3 ++- tests/test_freqtradebot.py | 2 +- 6 files changed, 20 insertions(+), 14 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index c0506203f..6c7d78864 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -704,7 +704,7 @@ To verify if a pair is currently locked, use `self.is_pair_locked(pair)`. Locked pairs will always be rounded up to the next candle. So assuming a `5m` timeframe, a lock with `until` set to 10:18 will lock the pair until the candle from 10:15-10:20 will be finished. !!! Warning - Locking pairs is not functioning during backtesting. + Locking pairs is not available during backtesting. #### Pair locking example diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5a399801a..ae46d335b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -4,7 +4,7 @@ Freqtrade is the main module of this bot. It contains the class Freqtrade() import copy import logging import traceback -from datetime import datetime +from datetime import datetime, timezone from math import isclose from threading import Lock from typing import Any, Dict, List, Optional @@ -19,10 +19,10 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, PricingError) -from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date +from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.pairlist.pairlistmanager import PairListManager -from freqtrade.persistence import Order, Trade, cleanup_db, init_db +from freqtrade.persistence import Order, Trade, cleanup_db, init_db, PairLocks from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.state import State @@ -72,6 +72,8 @@ class FreqtradeBot: self.wallets = Wallets(self.config, self.exchange) + PairLocks.timeframe = self.config['timeframe'] + self.pairlists = PairListManager(self.exchange, self.config) self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists) @@ -363,9 +365,9 @@ class FreqtradeBot: except DependencyException as exception: logger.warning('Unable to create trade for %s: %s', pair, exception) - if not trades_created: - logger.debug("Found no buy signals for whitelisted currencies. " - "Trying again...") + if not trades_created: + logger.debug("Found no buy signals for whitelisted currencies. " + "Trying again...") return trades_created @@ -937,7 +939,7 @@ class FreqtradeBot: self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, stoploss_order=True) # Lock pair for one candle to prevent immediate rebuys - self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe']), + self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), reason='Auto lock') self._notify_sell(trade, "stoploss") return True @@ -1264,7 +1266,7 @@ class FreqtradeBot: Trade.session.flush() # Lock pair for one candle to prevent immediate rebuys - self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['timeframe']), + self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), reason='Auto lock') self._notify_sell(trade, order_type) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 62b033bdf..3c62a7268 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -694,7 +694,7 @@ class PairLock(_DECL_BASE): if not now: now = datetime.now(timezone.utc) - filters = [func.datetime(PairLock.lock_end_time) >= now, + filters = [PairLock.lock_end_time > now, # Only active locks PairLock.active.is_(True), ] if pair: diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index ca2c31e36..c1acc2423 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -5,6 +5,7 @@ from datetime import datetime, timezone from typing import List, Optional from freqtrade.persistence.models import PairLock +from freqtrade.exchange import timeframe_to_next_date logger = logging.getLogger(__name__) @@ -19,12 +20,14 @@ class PairLocks(): use_db = True locks: List[PairLock] = [] + timeframe: str = '' + @staticmethod def lock_pair(pair: str, until: datetime, reason: str = None) -> None: lock = PairLock( pair=pair, lock_time=datetime.now(timezone.utc), - lock_end_time=until, + lock_end_time=timeframe_to_next_date(PairLocks.timeframe, until), reason=reason, active=True ) @@ -49,7 +52,7 @@ class PairLocks(): return PairLock.query_pair_locks(pair, now).all() else: locks = [lock for lock in PairLocks.locks if ( - lock.lock_end_time > now + lock.lock_end_time >= now and lock.active is True and (pair is None or lock.pair == pair) )] diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index e87fb7182..7cf9a0624 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -388,7 +388,8 @@ def test_is_pair_locked(default_conf): pair = 'BTC/USDT' # Lock until 14:30 lock_time = datetime(2020, 5, 1, 14, 30, 0, tzinfo=timezone.utc) - strategy.lock_pair(pair, lock_time) + # Subtract 2 seconds, as locking rounds up to the next candle. + strategy.lock_pair(pair, lock_time - timedelta(seconds=2)) assert not strategy.is_pair_locked(pair) # latest candle is from 14:20, lock goes to 14:30 diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 29df9c012..1f5b3ecaa 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -15,7 +15,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie InvalidOrderException, OperationalException, PricingError, TemporaryError) from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.persistence import Order, PairLocks, Trade +from freqtrade.persistence import Order, Trade from freqtrade.persistence.models import PairLock from freqtrade.rpc import RPCMessageType from freqtrade.state import RunMode, State From 5c8779b1550ab2cd8ad4649420833e5c7fb592e7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 27 Oct 2020 08:09:18 +0100 Subject: [PATCH 0852/1197] Sort imports --- freqtrade/freqtradebot.py | 2 +- freqtrade/persistence/pairlock_middleware.py | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ae46d335b..7416d8236 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -22,7 +22,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.pairlist.pairlistmanager import PairListManager -from freqtrade.persistence import Order, Trade, cleanup_db, init_db, PairLocks +from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.state import State diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index c1acc2423..44fc228f6 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -1,11 +1,9 @@ - - import logging from datetime import datetime, timezone from typing import List, Optional -from freqtrade.persistence.models import PairLock from freqtrade.exchange import timeframe_to_next_date +from freqtrade.persistence.models import PairLock logger = logging.getLogger(__name__) @@ -13,8 +11,9 @@ logger = logging.getLogger(__name__) class PairLocks(): """ - Pairlocks intermediate class - + Pairlocks middleware class + Abstracts the database layer away so it becomes optional - which will be necessary to support + backtesting and hyperopt in the future. """ use_db = True @@ -43,7 +42,7 @@ class PairLocks(): Get all currently active locks for this pair :param pair: Pair to check for. Returns all current locks if pair is empty :param now: Datetime object (generated via datetime.now(timezone.utc)). - defaults to datetime.utcnow() + defaults to datetime.now(timezone.utc) """ if not now: now = datetime.now(timezone.utc) From 72f61f4682e926a33018e6767b005a1bd78236c6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 27 Oct 2020 10:08:24 +0100 Subject: [PATCH 0853/1197] Remove optional, now is not optional --- freqtrade/persistence/models.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 3c62a7268..7e6d967c1 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -684,15 +684,12 @@ class PairLock(_DECL_BASE): f'lock_end_time={lock_end_time})') @staticmethod - def query_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> Query: + def query_pair_locks(pair: Optional[str], now: datetime) -> Query: """ Get all locks for this pair :param pair: Pair to check for. Returns all current locks if pair is empty :param now: Datetime object (generated via datetime.now(timezone.utc)). - defaults to datetime.utcnow() """ - if not now: - now = datetime.now(timezone.utc) filters = [PairLock.lock_end_time > now, # Only active locks From 28d6c3419b90c2dad1105f6631a42903cba41edc Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 27 Oct 2020 20:01:23 +0100 Subject: [PATCH 0854/1197] Fix random test failure in pairlocks --- tests/pairlist/test_pairlocks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/pairlist/test_pairlocks.py b/tests/pairlist/test_pairlocks.py index 3ed7d643e..0b6b89717 100644 --- a/tests/pairlist/test_pairlocks.py +++ b/tests/pairlist/test_pairlocks.py @@ -10,6 +10,7 @@ from freqtrade.persistence.models import PairLock @pytest.mark.parametrize('use_db', (False, True)) @pytest.mark.usefixtures("init_persistence") def test_PairLocks(use_db): + PairLocks.timeframe = '5m' # No lock should be present if use_db: assert len(PairLock.query.all()) == 0 From 5cb3735a57448c6fd83d9ffcb290f44d0e8616b6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Oct 2020 07:58:55 +0100 Subject: [PATCH 0855/1197] Improve error when hyperopt-loss-function is missing --- freqtrade/commands/cli_options.py | 4 ++-- freqtrade/constants.py | 3 +++ freqtrade/resolvers/hyperopt_resolver.py | 9 ++++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 8ea945ae7..4769bccde 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -4,6 +4,7 @@ Definition of cli arguments used in arguments.py from argparse import ArgumentTypeError from freqtrade import __version__, constants +from freqtrade.constants import HYPEROPT_LOSS_BUILTIN def check_int_positive(value: str) -> int: @@ -257,8 +258,7 @@ AVAILABLE_CLI_OPTIONS = { help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). ' 'Different functions can generate completely different results, ' 'since the target for optimization is different. Built-in Hyperopt-loss-functions are: ' - 'ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, ' - 'SharpeHyperOptLossDaily, SortinoHyperOptLoss, SortinoHyperOptLossDaily.', + f'{", ".join(HYPEROPT_LOSS_BUILTIN)}', metavar='NAME', ), "hyperoptexportfilename": Arg( diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 8e92d3ed8..dc5384f6f 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -20,6 +20,9 @@ REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] ORDERBOOK_SIDES = ['ask', 'bid'] ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] +HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', + 'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily', + 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PrecisionFilter', 'PriceFilter', 'ShuffleFilter', 'SpreadFilter'] diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 328dc488b..8327a4d13 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -7,7 +7,7 @@ import logging from pathlib import Path from typing import Dict -from freqtrade.constants import USERPATH_HYPEROPTS +from freqtrade.constants import HYPEROPT_LOSS_BUILTIN, USERPATH_HYPEROPTS from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt_interface import IHyperOpt from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss @@ -72,8 +72,11 @@ class HyperOptLossResolver(IResolver): hyperoptloss_name = config.get('hyperopt_loss') if not hyperoptloss_name: - raise OperationalException("No Hyperopt loss set. Please use `--hyperopt-loss` to " - "specify the Hyperopt-Loss class to use.") + raise OperationalException( + "No Hyperopt loss set. Please use `--hyperopt-loss` to " + "specify the Hyperopt-Loss class to use.\n" + f"Built-in Hyperopt-loss-functions are: {', '.join(HYPEROPT_LOSS_BUILTIN)}" + ) hyperoptloss = HyperOptLossResolver.load_object(hyperoptloss_name, config, kwargs={}, extra_dir=config.get('hyperopt_path')) From e1e2829ef3b5b1cbd77cac106039ceddda8ab621 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Oct 2020 14:36:19 +0100 Subject: [PATCH 0856/1197] Improve and refactor hyperopt tests --- tests/optimize/conftest.py | 51 +++++++ tests/optimize/test_hyperopt.py | 206 +--------------------------- tests/optimize/test_hyperoptloss.py | 165 ++++++++++++++++++++++ 3 files changed, 219 insertions(+), 203 deletions(-) create mode 100644 tests/optimize/conftest.py create mode 100644 tests/optimize/test_hyperoptloss.py diff --git a/tests/optimize/conftest.py b/tests/optimize/conftest.py new file mode 100644 index 000000000..f06b0ecd3 --- /dev/null +++ b/tests/optimize/conftest.py @@ -0,0 +1,51 @@ +from copy import deepcopy +from datetime import datetime +from pathlib import Path + +import pandas as pd +import pytest + +from freqtrade.optimize.hyperopt import Hyperopt +from freqtrade.strategy.interface import SellType +from tests.conftest import patch_exchange + + +@pytest.fixture(scope='function') +def hyperopt_conf(default_conf): + hyperconf = deepcopy(default_conf) + hyperconf.update({ + 'hyperopt': 'DefaultHyperOpt', + 'hyperopt_loss': 'ShortTradeDurHyperOptLoss', + 'hyperopt_path': str(Path(__file__).parent / 'hyperopts'), + 'epochs': 1, + 'timerange': None, + 'spaces': ['default'], + 'hyperopt_jobs': 1, + }) + return hyperconf + + +@pytest.fixture(scope='function') +def hyperopt(hyperopt_conf, mocker): + + patch_exchange(mocker) + return Hyperopt(hyperopt_conf) + + +@pytest.fixture(scope='function') +def hyperopt_results(): + return pd.DataFrame( + { + 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], + 'profit_percent': [-0.1, 0.2, 0.3], + 'profit_abs': [-0.2, 0.4, 0.6], + 'trade_duration': [10, 30, 10], + 'sell_reason': [SellType.STOP_LOSS, SellType.ROI, SellType.ROI], + 'close_date': + [ + datetime(2019, 1, 1, 9, 26, 3, 478039), + datetime(2019, 2, 1, 9, 26, 3, 478039), + datetime(2019, 3, 1, 9, 26, 3, 478039) + ] + } + ) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 41ad6f5de..82be894d3 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -2,7 +2,6 @@ import locale import logging import re -from copy import deepcopy from datetime import datetime from pathlib import Path from typing import Dict, List @@ -17,58 +16,15 @@ from freqtrade import constants from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.optimize.default_hyperopt_loss import ShortTradeDurHyperOptLoss from freqtrade.optimize.hyperopt import Hyperopt -from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver, HyperOptResolver +from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode -from freqtrade.strategy.interface import SellType from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) from .hyperopts.default_hyperopt import DefaultHyperOpt -@pytest.fixture(scope='function') -def hyperopt_conf(default_conf): - hyperconf = deepcopy(default_conf) - hyperconf.update({ - 'hyperopt': 'DefaultHyperOpt', - 'hyperopt_loss': 'ShortTradeDurHyperOptLoss', - 'hyperopt_path': str(Path(__file__).parent / 'hyperopts'), - 'epochs': 1, - 'timerange': None, - 'spaces': ['default'], - 'hyperopt_jobs': 1, - }) - return hyperconf - - -@pytest.fixture(scope='function') -def hyperopt(hyperopt_conf, mocker): - - patch_exchange(mocker) - return Hyperopt(hyperopt_conf) - - -@pytest.fixture(scope='function') -def hyperopt_results(): - return pd.DataFrame( - { - 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], - 'profit_percent': [-0.1, 0.2, 0.3], - 'profit_abs': [-0.2, 0.4, 0.6], - 'trade_duration': [10, 30, 10], - 'sell_reason': [SellType.STOP_LOSS, SellType.ROI, SellType.ROI], - 'close_date': - [ - datetime(2019, 1, 1, 9, 26, 3, 478039), - datetime(2019, 2, 1, 9, 26, 3, 478039), - datetime(2019, 3, 1, 9, 26, 3, 478039) - ] - } - ) - - # Functions for recurrent object patching def create_results(mocker, hyperopt, testdatadir) -> List[Dict]: """ @@ -230,32 +186,6 @@ def test_hyperoptresolver_noname(default_conf): HyperOptResolver.load_hyperopt(default_conf) -def test_hyperoptlossresolver_noname(default_conf): - with pytest.raises(OperationalException, - match="No Hyperopt loss set. Please use `--hyperopt-loss` to specify " - "the Hyperopt-Loss class to use."): - HyperOptLossResolver.load_hyperoptloss(default_conf) - - -def test_hyperoptlossresolver(mocker, default_conf) -> None: - - hl = ShortTradeDurHyperOptLoss - mocker.patch( - 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver.load_object', - MagicMock(return_value=hl) - ) - default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'}) - x = HyperOptLossResolver.load_hyperoptloss(default_conf) - assert hasattr(x, "hyperopt_loss_function") - - -def test_hyperoptlossresolver_wrongname(default_conf) -> None: - default_conf.update({'hyperopt_loss': "NonExistingLossClass"}) - - with pytest.raises(OperationalException, match=r'Impossible to load HyperoptLoss.*'): - HyperOptLossResolver.load_hyperoptloss(default_conf) - - def test_start_not_installed(mocker, default_conf, import_fails) -> None: start_mock = MagicMock() patched_configuration_load_config_file(mocker, default_conf) @@ -269,7 +199,8 @@ def test_start_not_installed(mocker, default_conf, import_fails) -> None: '--hyperopt', 'DefaultHyperOpt', '--hyperopt-path', str(Path(__file__).parent / "hyperopts"), - '--epochs', '5' + '--epochs', '5', + '--hyperopt-loss', 'SharpeHyperOptLossDaily', ] pargs = get_args(args) @@ -337,137 +268,6 @@ def test_start_filelock(mocker, hyperopt_conf, caplog) -> None: assert log_has("Another running instance of freqtrade Hyperopt detected.", caplog) -def test_loss_calculation_prefer_correct_trade_count(hyperopt_conf, hyperopt_results) -> None: - hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) - correct = hl.hyperopt_loss_function(hyperopt_results, 600, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hl.hyperopt_loss_function(hyperopt_results, 600 - 100, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert over > correct - assert under > correct - - -def test_loss_calculation_prefer_shorter_trades(hyperopt_conf, hyperopt_results) -> None: - resultsb = hyperopt_results.copy() - resultsb.loc[1, 'trade_duration'] = 20 - - hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) - longer = hl.hyperopt_loss_function(hyperopt_results, 100, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - shorter = hl.hyperopt_loss_function(resultsb, 100, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert shorter < longer - - -def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) -> None: - results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 - results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - - hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) - correct = hl.hyperopt_loss_function(hyperopt_results, 600, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hl.hyperopt_loss_function(results_over, 600, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hl.hyperopt_loss_function(results_under, 600, - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert over < correct - assert under > correct - - -def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: - results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 - results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - - default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'}) - hl = HyperOptLossResolver.load_hyperoptloss(default_conf) - correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert over < correct - assert under > correct - - -def test_sharpe_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None: - results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 - results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - - default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'}) - hl = HyperOptLossResolver.load_hyperoptloss(default_conf) - correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert over < correct - assert under > correct - - -def test_sortino_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: - results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 - results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - - default_conf.update({'hyperopt_loss': 'SortinoHyperOptLoss'}) - hl = HyperOptLossResolver.load_hyperoptloss(default_conf) - correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert over < correct - assert under > correct - - -def test_sortino_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None: - results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 - results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - - default_conf.update({'hyperopt_loss': 'SortinoHyperOptLossDaily'}) - hl = HyperOptLossResolver.load_hyperoptloss(default_conf) - correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert over < correct - assert under > correct - - -def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: - results_over = hyperopt_results.copy() - results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 - results_under = hyperopt_results.copy() - results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - - default_conf.update({'hyperopt_loss': 'OnlyProfitHyperOptLoss'}) - hl = HyperOptLossResolver.load_hyperoptloss(default_conf) - correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), - datetime(2019, 1, 1), datetime(2019, 5, 1)) - assert over < correct - assert under > correct - - def test_log_results_if_loss_improves(hyperopt, capsys) -> None: hyperopt.current_best_loss = 2 hyperopt.total_epochs = 2 diff --git a/tests/optimize/test_hyperoptloss.py b/tests/optimize/test_hyperoptloss.py new file mode 100644 index 000000000..63012ee48 --- /dev/null +++ b/tests/optimize/test_hyperoptloss.py @@ -0,0 +1,165 @@ +from datetime import datetime +from unittest.mock import MagicMock + +import pytest + +from freqtrade.exceptions import OperationalException +from freqtrade.optimize.default_hyperopt_loss import ShortTradeDurHyperOptLoss +from freqtrade.resolvers.hyperopt_resolver import HyperOptLossResolver + + +def test_hyperoptlossresolver_noname(default_conf): + with pytest.raises(OperationalException, + match="No Hyperopt loss set. Please use `--hyperopt-loss` to specify " + "the Hyperopt-Loss class to use."): + HyperOptLossResolver.load_hyperoptloss(default_conf) + + +def test_hyperoptlossresolver(mocker, default_conf) -> None: + + hl = ShortTradeDurHyperOptLoss + mocker.patch( + 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver.load_object', + MagicMock(return_value=hl) + ) + default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'}) + x = HyperOptLossResolver.load_hyperoptloss(default_conf) + assert hasattr(x, "hyperopt_loss_function") + + +def test_hyperoptlossresolver_wrongname(default_conf) -> None: + default_conf.update({'hyperopt_loss': "NonExistingLossClass"}) + + with pytest.raises(OperationalException, match=r'Impossible to load HyperoptLoss.*'): + HyperOptLossResolver.load_hyperoptloss(default_conf) + + +def test_loss_calculation_prefer_correct_trade_count(hyperopt_conf, hyperopt_results) -> None: + hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) + correct = hl.hyperopt_loss_function(hyperopt_results, 600, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(hyperopt_results, 600 - 100, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over > correct + assert under > correct + + +def test_loss_calculation_prefer_shorter_trades(hyperopt_conf, hyperopt_results) -> None: + resultsb = hyperopt_results.copy() + resultsb.loc[1, 'trade_duration'] = 20 + + hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) + longer = hl.hyperopt_loss_function(hyperopt_results, 100, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + shorter = hl.hyperopt_loss_function(resultsb, 100, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert shorter < longer + + +def test_loss_calculation_has_limited_profit(hyperopt_conf, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + hl = HyperOptLossResolver.load_hyperoptloss(hyperopt_conf) + correct = hl.hyperopt_loss_function(hyperopt_results, 600, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, 600, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, 600, + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over < correct + assert under > correct + + +def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + default_conf.update({'hyperopt_loss': 'SharpeHyperOptLoss'}) + hl = HyperOptLossResolver.load_hyperoptloss(default_conf) + correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over < correct + assert under > correct + + +def test_sharpe_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'}) + hl = HyperOptLossResolver.load_hyperoptloss(default_conf) + correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over < correct + assert under > correct + + +def test_sortino_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + default_conf.update({'hyperopt_loss': 'SortinoHyperOptLoss'}) + hl = HyperOptLossResolver.load_hyperoptloss(default_conf) + correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over < correct + assert under > correct + + +def test_sortino_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + default_conf.update({'hyperopt_loss': 'SortinoHyperOptLossDaily'}) + hl = HyperOptLossResolver.load_hyperoptloss(default_conf) + correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over < correct + assert under > correct + + +def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None: + results_over = hyperopt_results.copy() + results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2 + results_under = hyperopt_results.copy() + results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 + + default_conf.update({'hyperopt_loss': 'OnlyProfitHyperOptLoss'}) + hl = HyperOptLossResolver.load_hyperoptloss(default_conf) + correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + over = hl.hyperopt_loss_function(results_over, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + under = hl.hyperopt_loss_function(results_under, len(hyperopt_results), + datetime(2019, 1, 1), datetime(2019, 5, 1)) + assert over < correct + assert under > correct From ffa67979586447479e4fdf6ccdd03559f0e7f2c3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Oct 2020 16:29:08 +0100 Subject: [PATCH 0857/1197] Improve test coverage --- tests/test_arguments.py | 3 +++ tests/test_wallets.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/tests/test_arguments.py b/tests/test_arguments.py index 2af36277b..315d47876 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -249,6 +249,9 @@ def test_check_int_positive() -> None: with pytest.raises(argparse.ArgumentTypeError): check_int_positive('0') + with pytest.raises(argparse.ArgumentTypeError): + check_int_positive(0) + with pytest.raises(argparse.ArgumentTypeError): check_int_positive('3.5') diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 450dabc4d..b7aead0c4 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -74,6 +74,10 @@ def test_sync_wallet_at_boot(mocker, default_conf): freqtrade.wallets.update() assert update_mock.call_count == 1 + assert freqtrade.wallets.get_free('NOCURRENCY') == 0 + assert freqtrade.wallets.get_used('NOCURRENCY') == 0 + assert freqtrade.wallets.get_total('NOCURRENCY') == 0 + def test_sync_wallet_missing_data(mocker, default_conf): default_conf['dry_run'] = False From 86725847edb3ea10ec6922cd87498de1d992b729 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 28 Oct 2020 16:58:39 +0100 Subject: [PATCH 0858/1197] Add explicit test for check_int_nonzero --- tests/test_arguments.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/test_arguments.py b/tests/test_arguments.py index 315d47876..e2a1ae53c 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock import pytest from freqtrade.commands import Arguments -from freqtrade.commands.cli_options import check_int_positive +from freqtrade.commands.cli_options import check_int_nonzero, check_int_positive # Parse common command-line-arguments. Used for all tools @@ -257,3 +257,23 @@ def test_check_int_positive() -> None: with pytest.raises(argparse.ArgumentTypeError): check_int_positive('DeadBeef') + + +def test_check_int_nonzero() -> None: + assert check_int_nonzero('3') == 3 + assert check_int_nonzero('1') == 1 + assert check_int_nonzero('100') == 100 + + assert check_int_nonzero('-2') == -2 + + with pytest.raises(argparse.ArgumentTypeError): + check_int_nonzero('0') + + with pytest.raises(argparse.ArgumentTypeError): + check_int_nonzero(0) + + with pytest.raises(argparse.ArgumentTypeError): + check_int_nonzero('3.5') + + with pytest.raises(argparse.ArgumentTypeError): + check_int_nonzero('DeadBeef') From 19fcbc92a7d0fe4570f34c9ed5cdb9394373aa15 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Oct 2020 07:43:40 +0100 Subject: [PATCH 0859/1197] Remove stake-currency for download-data - it's not needed --- freqtrade/commands/data_commands.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 7102eee38..df4c52de0 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -35,6 +35,9 @@ def start_download_data(args: Dict[str, Any]) -> None: if 'timerange' in config: timerange = timerange.parse_timerange(config['timerange']) + # Remove stake-currency to skip checks which are not relevant for datadownload + config['stake_currency'] = '' + if 'pairs' not in config: raise OperationalException( "Downloading data requires a list of pairs. " From f4d39f2a12538a3e75d921f737b961ee0ee29b38 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Oct 2020 07:44:03 +0100 Subject: [PATCH 0860/1197] Improve test coverage of deploy_commands --- freqtrade/commands/deploy_commands.py | 2 +- tests/commands/test_commands.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 0a49c55de..a0105e140 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -133,7 +133,7 @@ def start_new_hyperopt(args: Dict[str, Any]) -> None: if new_path.exists(): raise OperationalException(f"`{new_path}` already exists. " - "Please choose another Strategy Name.") + "Please choose another Hyperopt Name.") deploy_new_hyperopt(args['hyperopt'], new_path, args['template']) else: raise OperationalException("`new-hyperopt` requires --hyperopt to be set.") diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 713386a8e..6861e0cd9 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -476,6 +476,12 @@ def test_start_new_strategy(mocker, caplog): assert "CoolNewStrategy" in wt_mock.call_args_list[0][0][0] assert log_has_re("Writing strategy to .*", caplog) + mocker.patch('freqtrade.commands.deploy_commands.setup_utils_configuration') + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + with pytest.raises(OperationalException, + match=r".* already exists. Please choose another Strategy Name\."): + start_new_strategy(get_args(args)) + def test_start_new_strategy_DefaultStrat(mocker, caplog): args = [ @@ -512,6 +518,12 @@ def test_start_new_hyperopt(mocker, caplog): assert "CoolNewhyperopt" in wt_mock.call_args_list[0][0][0] assert log_has_re("Writing hyperopt to .*", caplog) + mocker.patch('freqtrade.commands.deploy_commands.setup_utils_configuration') + mocker.patch.object(Path, "exists", MagicMock(return_value=True)) + with pytest.raises(OperationalException, + match=r".* already exists. Please choose another Hyperopt Name\."): + start_new_hyperopt(get_args(args)) + def test_start_new_hyperopt_DefaultHyperopt(mocker, caplog): args = [ From d8ff79a2faa69ac714d5898ca1b1386cf70afacf Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Oct 2020 07:54:42 +0100 Subject: [PATCH 0861/1197] Improve tests of list commands --- tests/commands/test_commands.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 6861e0cd9..305b6b376 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -435,6 +435,16 @@ def test_list_markets(mocker, markets, capsys): assert re.search(r"^BLK/BTC$", captured.out, re.MULTILINE) assert re.search(r"^LTC/USD$", captured.out, re.MULTILINE) + mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(side_effect=ValueError)) + # Test --one-column + args = [ + "list-markets", + '--config', 'config.json.example', + "--one-column" + ] + with pytest.raises(OperationalException, match=r"Cannot get markets.*"): + start_list_markets(get_args(args), False) + def test_create_datadir_failed(caplog): @@ -707,6 +717,7 @@ def test_start_list_strategies(mocker, caplog, capsys): "list-strategies", "--strategy-path", str(Path(__file__).parent.parent / "strategy" / "strats"), + '--no-color', ] pargs = get_args(args) # pargs['config'] = None From 3ca97223f21c820363386aa0a68570388660b31c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 29 Oct 2020 08:09:50 +0100 Subject: [PATCH 0862/1197] Improve test for test_pairlist --- tests/commands/test_commands.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 305b6b376..a392b74cf 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -792,6 +792,25 @@ def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): assert re.match(r"Pairs for .*", captured.out) assert re.match("['ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC', 'XRP/BTC']", captured.out) + args = [ + 'test-pairlist', + '-c', 'config.json.example', + '--one-column', + ] + start_test_pairlist(get_args(args)) + captured = capsys.readouterr() + assert re.match(r"ETH/BTC\nTKN/BTC\nBLK/BTC\nLTC/BTC\nXRP/BTC\n", captured.out) + + args = [ + 'test-pairlist', + '-c', 'config.json.example', + '--print-json', + ] + start_test_pairlist(get_args(args)) + captured = capsys.readouterr() + assert re.match(r'Pairs for BTC: \n\["ETH/BTC","TKN/BTC","BLK/BTC","LTC/BTC","XRP/BTC"\]\n', + captured.out) + def test_hyperopt_list(mocker, capsys, caplog, hyperopt_results): mocker.patch( From 38fc5d680b937c7aff9ed148ecab34804dd9dfa0 Mon Sep 17 00:00:00 2001 From: Matthias Spiller Date: Sat, 31 Oct 2020 10:31:58 +0000 Subject: [PATCH 0863/1197] Enable usage of devcontainer for macOS users --- .devcontainer/docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 7b5e64609..20ec247d1 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -7,8 +7,8 @@ services: dockerfile: ".devcontainer/Dockerfile" volumes: # Allow git usage within container - - "/home/${USER}/.ssh:/home/ftuser/.ssh:ro" - - "/home/${USER}/.gitconfig:/home/ftuser/.gitconfig:ro" + - "${HOME}/.ssh:/home/ftuser/.ssh:ro" + - "${HOME}/.gitconfig:/home/ftuser/.gitconfig:ro" - ..:/freqtrade:cached # Persist bash-history - freqtrade-vscode-server:/home/ftuser/.vscode-server From 78874fa86573f810d0c57e248651fc8aa1d80286 Mon Sep 17 00:00:00 2001 From: Matthias Spiller Date: Sat, 31 Oct 2020 10:53:51 +0000 Subject: [PATCH 0864/1197] informative_pairs does not honor dataformat --- freqtrade/data/dataprovider.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 07dd94fc1..3ca38e865 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -88,7 +88,8 @@ class DataProvider: """ return load_pair_history(pair=pair, timeframe=timeframe or self._config['timeframe'], - datadir=self._config['datadir'] + datadir=self._config['datadir'], + data_format=self._config.get('dataformat_ohlcv', 'json') ) def get_pair_dataframe(self, pair: str, timeframe: str = None) -> DataFrame: From 0d11f0bd75da23e2c285778a435cb7cf2f65bff4 Mon Sep 17 00:00:00 2001 From: Matthias Spiller Date: Sat, 31 Oct 2020 11:45:46 +0000 Subject: [PATCH 0865/1197] Add unit test for hdf5 dataformat for informative pairs --- tests/data/test_dataprovider.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index a64dce908..a3c57a77b 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -52,6 +52,31 @@ def test_historic_ohlcv(mocker, default_conf, ohlcv_history): assert historymock.call_args_list[0][1]["timeframe"] == "5m" +def test_historic_ohlcv_dataformat(mocker, default_conf, ohlcv_history): + hdf5loadmock = MagicMock(return_value=ohlcv_history) + jsonloadmock = MagicMock(return_value=ohlcv_history) + mocker.patch("freqtrade.data.history.hdf5datahandler.HDF5DataHandler._ohlcv_load", hdf5loadmock) + mocker.patch("freqtrade.data.history.jsondatahandler.JsonDataHandler._ohlcv_load", jsonloadmock) + + default_conf["runmode"] = RunMode.BACKTEST + exchange = get_patched_exchange(mocker, default_conf) + dp = DataProvider(default_conf, exchange) + data = dp.historic_ohlcv("UNITTEST/BTC", "5m") + assert isinstance(data, DataFrame) + hdf5loadmock.assert_not_called() + jsonloadmock.assert_called_once() + + # Swiching to dataformat hdf5 + hdf5loadmock.reset_mock() + jsonloadmock.reset_mock() + default_conf["dataformat_ohlcv"] = "hdf5" + dp = DataProvider(default_conf, exchange) + data = dp.historic_ohlcv("UNITTEST/BTC", "5m") + assert isinstance(data, DataFrame) + hdf5loadmock.assert_called_once() + jsonloadmock.assert_not_called() + + def test_get_pair_dataframe(mocker, default_conf, ohlcv_history): default_conf["runmode"] = RunMode.DRY_RUN timeframe = default_conf["timeframe"] From e73203acb85b16ef79bfebac6b0275ca72ddf8f9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 1 Nov 2020 10:51:07 +0100 Subject: [PATCH 0866/1197] FIx bug with dmmp --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 47bb9edd9..883f7338c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -340,7 +340,7 @@ class Backtesting: # max_open_trades must be respected # don't open on the last row if ((position_stacking or len(open_trades[pair]) == 0) - and max_open_trades > 0 and open_trade_count_start < max_open_trades + and (max_open_trades <= 0 or open_trade_count_start < max_open_trades) and tmp != end_date and row[BUY_IDX] == 1 and row[SELL_IDX] != 1): # Enter trade From 81fb0c572614805a393fa5a10ad4389877f65498 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2020 05:54:57 +0000 Subject: [PATCH 0867/1197] Bump numpy from 1.19.2 to 1.19.3 Bumps [numpy](https://github.com/numpy/numpy) from 1.19.2 to 1.19.3. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/master/doc/HOWTO_RELEASE.rst.txt) - [Commits](https://github.com/numpy/numpy/compare/v1.19.2...v1.19.3) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7d2017beb..e2687539e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.19.2 +numpy==1.19.3 pandas==1.1.3 ccxt==1.36.85 From 6c3753ac7f64c1d61e8b4ecfcace66db9c5d5946 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2020 05:55:07 +0000 Subject: [PATCH 0868/1197] Bump mkdocs-material from 6.1.0 to 6.1.2 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 6.1.0 to 6.1.2. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/6.1.0...6.1.2) Signed-off-by: dependabot[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 f30710a1f..47f0eff1c 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==6.1.0 +mkdocs-material==6.1.2 mdx_truly_sane_lists==1.2 pymdown-extensions==8.0.1 From 21b22760a7087409cf3f91ea860f29329d831745 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2020 05:55:12 +0000 Subject: [PATCH 0869/1197] Bump pytest from 6.1.1 to 6.1.2 Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.1.1 to 6.1.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/6.1.1...6.1.2) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 916bb2ec2..1c96a880a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.8.4 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 mypy==0.790 -pytest==6.1.1 +pytest==6.1.2 pytest-asyncio==0.14.0 pytest-cov==2.10.1 pytest-mock==3.3.1 From aed44ef6b36f071d784bda57c9f910e90c4d953a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2020 07:46:28 +0000 Subject: [PATCH 0870/1197] Bump pandas from 1.1.3 to 1.1.4 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.1.3 to 1.1.4. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.1.3...v1.1.4) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e2687539e..05520d4d9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ numpy==1.19.3 -pandas==1.1.3 +pandas==1.1.4 ccxt==1.36.85 aiohttp==3.7.1 From 74d8a985e22d0b6d2881b4d3dcd989186f3ad7b1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2020 08:44:26 +0000 Subject: [PATCH 0871/1197] Bump aiohttp from 3.7.1 to 3.7.2 Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.7.1 to 3.7.2. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.7.1...v3.7.2) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 05520d4d9..3df54ad06 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.19.3 pandas==1.1.4 ccxt==1.36.85 -aiohttp==3.7.1 +aiohttp==3.7.2 SQLAlchemy==1.3.20 python-telegram-bot==13.0 arrow==0.17.0 From d56da41679f022365ab6ac913989f09d88ff920d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 2 Nov 2020 13:50:07 +0000 Subject: [PATCH 0872/1197] Bump ccxt from 1.36.85 to 1.37.14 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.36.85 to 1.37.14. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.36.85...1.37.14) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3df54ad06..afe48171c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.3 pandas==1.1.4 -ccxt==1.36.85 +ccxt==1.37.14 aiohttp==3.7.2 SQLAlchemy==1.3.20 python-telegram-bot==13.0 From cf89a773da498915cb7f08e590d1d061c3d46bc8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 Nov 2020 07:34:21 +0100 Subject: [PATCH 0873/1197] Standardize trade api outputs there should be no difference between current_profit and close_profit it's always profit, and the information if it's a closed trade is available elsewhere --- freqtrade/persistence/models.py | 9 +++++++-- freqtrade/rpc/rpc.py | 13 +++++++------ tests/rpc/test_rpc.py | 8 +++++++- tests/rpc/test_rpc_apiserver.py | 7 +++++++ tests/test_persistence.py | 8 ++++++++ 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 7e6d967c1..3019d3d5f 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -295,8 +295,13 @@ class Trade(_DECL_BASE): tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None, 'close_rate': self.close_rate, 'close_rate_requested': self.close_rate_requested, - 'close_profit': self.close_profit, - 'close_profit_abs': self.close_profit_abs, + 'close_profit': self.close_profit, # Deprecated + 'close_profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, + 'close_profit_abs': self.close_profit_abs, # Deprecated + + 'profit_ratio': self.close_profit, + 'profit_pct': round(self.close_profit * 100, 2) if self.close_profit else None, + 'profit_abs': self.close_profit_abs, 'sell_reason': self.sell_reason, 'sell_order_status': self.sell_order_status, diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 10aaf56fa..4370fe897 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -152,17 +152,18 @@ class RPC: stoploss_current_dist = trade.stop_loss - current_rate stoploss_current_dist_ratio = stoploss_current_dist / current_rate - fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%' - if trade.close_profit is not None else None) trade_dict = trade.to_json() trade_dict.update(dict( base_currency=self._freqtrade.config['stake_currency'], close_profit=trade.close_profit if trade.close_profit is not None else None, - close_profit_pct=fmt_close_profit, current_rate=current_rate, - current_profit=current_profit, - current_profit_pct=round(current_profit * 100, 2), - current_profit_abs=current_profit_abs, + current_profit=current_profit, # Deprectated + current_profit_pct=round(current_profit * 100, 2), # Deprectated + current_profit_abs=current_profit_abs, # Deprectated + profit_ratio=current_profit, + profit_pct=round(current_profit * 100, 2), + profit_abs=current_profit_abs, + stoploss_current_dist=stoploss_current_dist, stoploss_current_dist_ratio=round(stoploss_current_dist_ratio, 8), stoploss_current_dist_pct=round(stoploss_current_dist_ratio * 100, 2), diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 977dfbc20..4a4f3053e 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -70,7 +70,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'max_rate': ANY, 'strategy': ANY, 'ticker_interval': ANY, - 'timeframe': ANY, + 'timeframe': 5, 'open_order_id': ANY, 'close_date': None, 'close_date_hum': None, @@ -87,6 +87,9 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'current_profit': -0.00408133, 'current_profit_pct': -0.41, 'current_profit_abs': -4.09e-06, + 'profit_ratio': -0.00408133, + 'profit_pct': -0.41, + 'profit_abs': -4.09e-06, 'stop_loss': 9.882e-06, 'stop_loss_abs': 9.882e-06, 'stop_loss_pct': -10.0, @@ -152,6 +155,9 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'current_profit': ANY, 'current_profit_pct': ANY, 'current_profit_abs': ANY, + 'profit_ratio': ANY, + 'profit_pct': ANY, + 'profit_abs': ANY, 'stop_loss': 9.882e-06, 'stop_loss_abs': 9.882e-06, 'stop_loss_pct': -10.0, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 7b4e2e153..fd5d2fce2 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -639,6 +639,9 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'current_profit': -0.00408133, 'current_profit_pct': -0.41, 'current_profit_abs': -4.09e-06, + 'profit_ratio': -0.00408133, + 'profit_pct': -0.41, + 'profit_abs': -4.09e-06, 'current_rate': 1.099e-05, 'open_date': ANY, 'open_date_hum': 'just now', @@ -791,8 +794,12 @@ def test_api_forcebuy(botclient, mocker, fee): 'initial_stop_loss_pct': None, 'initial_stop_loss_ratio': None, 'close_profit': None, + 'close_profit_pct': None, 'close_profit_abs': None, 'close_rate_requested': None, + 'profit_ratio': None, + 'profit_pct': None, + 'profit_abs': None, 'fee_close': 0.0025, 'fee_close_cost': None, 'fee_close_currency': None, diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 4216565ac..b2d2b716c 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -816,7 +816,11 @@ def test_to_json(default_conf, fee): 'amount_requested': 123.0, 'stake_amount': 0.001, 'close_profit': None, + 'close_profit_pct': None, 'close_profit_abs': None, + 'profit_ratio': None, + 'profit_pct': None, + 'profit_abs': None, 'sell_reason': None, 'sell_order_status': None, 'stop_loss': None, @@ -880,7 +884,11 @@ def test_to_json(default_conf, fee): 'initial_stop_loss_pct': None, 'initial_stop_loss_ratio': None, 'close_profit': None, + 'close_profit_pct': None, 'close_profit_abs': None, + 'profit_ratio': None, + 'profit_pct': None, + 'profit_abs': None, 'close_rate_requested': None, 'fee_close': 0.0025, 'fee_close_cost': None, From d1dab2328379446b502287fdbba0deb842fcb68a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 Nov 2020 08:25:47 +0100 Subject: [PATCH 0874/1197] Remove deprecated api fields --- freqtrade/persistence/models.py | 3 --- freqtrade/rpc/rpc.py | 1 - freqtrade/rpc/telegram.py | 4 ++-- tests/rpc/test_rpc.py | 6 ------ tests/rpc/test_rpc_apiserver.py | 7 ------- tests/test_persistence.py | 6 ------ 6 files changed, 2 insertions(+), 25 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 3019d3d5f..8160ffbbf 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -270,7 +270,6 @@ class Trade(_DECL_BASE): 'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None, 'stake_amount': round(self.stake_amount, 8), 'strategy': self.strategy, - 'ticker_interval': self.timeframe, # DEPRECATED 'timeframe': self.timeframe, 'fee_open': self.fee_open, @@ -305,7 +304,6 @@ class Trade(_DECL_BASE): 'sell_reason': self.sell_reason, 'sell_order_status': self.sell_order_status, - 'stop_loss': self.stop_loss, # Deprecated - should not be used 'stop_loss_abs': self.stop_loss, 'stop_loss_ratio': self.stop_loss_pct if self.stop_loss_pct else None, 'stop_loss_pct': (self.stop_loss_pct * 100) if self.stop_loss_pct else None, @@ -314,7 +312,6 @@ class Trade(_DECL_BASE): if self.stoploss_last_update else None), 'stoploss_last_update_timestamp': int(self.stoploss_last_update.replace( tzinfo=timezone.utc).timestamp() * 1000) if self.stoploss_last_update else None, - 'initial_stop_loss': self.initial_stop_loss, # Deprecated - should not be used 'initial_stop_loss_abs': self.initial_stop_loss, 'initial_stop_loss_ratio': (self.initial_stop_loss_pct if self.initial_stop_loss_pct else None), diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 4370fe897..efeb361ae 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -110,7 +110,6 @@ class RPC: 'trailing_stop_positive': config.get('trailing_stop_positive'), 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'), 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'), - 'ticker_interval': config['timeframe'], # DEPRECATED 'timeframe': config['timeframe'], 'timeframe_ms': timeframe_to_msecs(config['timeframe']), 'timeframe_min': timeframe_to_minutes(config['timeframe']), diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 3dcb7ab72..9bbae871d 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -251,14 +251,14 @@ class Telegram(RPC): if r['close_profit_pct'] is not None else ""), "*Current Profit:* `{current_profit_pct:.2f}%`", ] - if (r['stop_loss'] != r['initial_stop_loss'] + if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] and r['initial_stop_loss_pct'] is not None): # Adding initial stoploss only if it is different from stoploss lines.append("*Initial Stoploss:* `{initial_stop_loss:.8f}` " "`({initial_stop_loss_pct:.2f}%)`") # Adding stoploss and stoploss percentage only if it is not None - lines.append("*Stoploss:* `{stop_loss:.8f}` " + + lines.append("*Stoploss:* `{stop_loss_abs:.8f}` " + ("`({stop_loss_pct:.2f}%)`" if r['stop_loss_pct'] else "")) lines.append("*Stoploss distance:* `{stoploss_current_dist:.8f}` " "`({stoploss_current_dist_pct:.2f}%)`") diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 4a4f3053e..23ca53e53 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -69,7 +69,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'min_rate': ANY, 'max_rate': ANY, 'strategy': ANY, - 'ticker_interval': ANY, 'timeframe': 5, 'open_order_id': ANY, 'close_date': None, @@ -90,14 +89,12 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'profit_ratio': -0.00408133, 'profit_pct': -0.41, 'profit_abs': -4.09e-06, - 'stop_loss': 9.882e-06, 'stop_loss_abs': 9.882e-06, 'stop_loss_pct': -10.0, 'stop_loss_ratio': -0.1, 'stoploss_order_id': None, 'stoploss_last_update': ANY, 'stoploss_last_update_timestamp': ANY, - 'initial_stop_loss': 9.882e-06, 'initial_stop_loss_abs': 9.882e-06, 'initial_stop_loss_pct': -10.0, 'initial_stop_loss_ratio': -0.1, @@ -137,7 +134,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'min_rate': ANY, 'max_rate': ANY, 'strategy': ANY, - 'ticker_interval': ANY, 'timeframe': ANY, 'open_order_id': ANY, 'close_date': None, @@ -158,14 +154,12 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'profit_ratio': ANY, 'profit_pct': ANY, 'profit_abs': ANY, - 'stop_loss': 9.882e-06, 'stop_loss_abs': 9.882e-06, 'stop_loss_pct': -10.0, 'stop_loss_ratio': -0.1, 'stoploss_order_id': None, 'stoploss_last_update': ANY, 'stoploss_last_update_timestamp': ANY, - 'initial_stop_loss': 9.882e-06, 'initial_stop_loss_abs': 9.882e-06, 'initial_stop_loss_pct': -10.0, 'initial_stop_loss_ratio': -0.1, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index fd5d2fce2..80de839e4 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -360,7 +360,6 @@ def test_api_show_config(botclient, mocker): assert_response(rc) assert 'dry_run' in rc.json assert rc.json['exchange'] == 'bittrex' - assert rc.json['ticker_interval'] == '5m' assert rc.json['timeframe'] == '5m' assert rc.json['timeframe_ms'] == 300000 assert rc.json['timeframe_min'] == 5 @@ -650,14 +649,12 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'open_rate': 1.098e-05, 'pair': 'ETH/BTC', 'stake_amount': 0.001, - 'stop_loss': 9.882e-06, 'stop_loss_abs': 9.882e-06, 'stop_loss_pct': -10.0, 'stop_loss_ratio': -0.1, 'stoploss_order_id': None, 'stoploss_last_update': ANY, 'stoploss_last_update_timestamp': ANY, - 'initial_stop_loss': 9.882e-06, 'initial_stop_loss_abs': 9.882e-06, 'initial_stop_loss_pct': -10.0, 'initial_stop_loss_ratio': -0.1, @@ -685,7 +682,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'sell_reason': None, 'sell_order_status': None, 'strategy': 'DefaultStrategy', - 'ticker_interval': 5, 'timeframe': 5, 'exchange': 'bittrex', }] @@ -782,14 +778,12 @@ def test_api_forcebuy(botclient, mocker, fee): 'open_rate': 0.245441, 'pair': 'ETH/ETH', 'stake_amount': 1, - 'stop_loss': None, 'stop_loss_abs': None, 'stop_loss_pct': None, 'stop_loss_ratio': None, 'stoploss_order_id': None, 'stoploss_last_update': None, 'stoploss_last_update_timestamp': None, - 'initial_stop_loss': None, 'initial_stop_loss_abs': None, 'initial_stop_loss_pct': None, 'initial_stop_loss_ratio': None, @@ -815,7 +809,6 @@ def test_api_forcebuy(botclient, mocker, fee): 'sell_reason': None, 'sell_order_status': None, 'strategy': None, - 'ticker_interval': None, 'timeframe': None, 'exchange': 'bittrex', } diff --git a/tests/test_persistence.py b/tests/test_persistence.py index b2d2b716c..41b99b34f 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -823,21 +823,18 @@ def test_to_json(default_conf, fee): 'profit_abs': None, 'sell_reason': None, 'sell_order_status': None, - 'stop_loss': None, 'stop_loss_abs': None, 'stop_loss_ratio': None, 'stop_loss_pct': None, 'stoploss_order_id': None, 'stoploss_last_update': None, 'stoploss_last_update_timestamp': None, - 'initial_stop_loss': None, 'initial_stop_loss_abs': None, 'initial_stop_loss_pct': None, 'initial_stop_loss_ratio': None, 'min_rate': None, 'max_rate': None, 'strategy': None, - 'ticker_interval': None, 'timeframe': None, 'exchange': 'bittrex', } @@ -872,14 +869,12 @@ def test_to_json(default_conf, fee): 'amount': 100.0, 'amount_requested': 101.0, 'stake_amount': 0.001, - 'stop_loss': None, 'stop_loss_abs': None, 'stop_loss_pct': None, 'stop_loss_ratio': None, 'stoploss_order_id': None, 'stoploss_last_update': None, 'stoploss_last_update_timestamp': None, - 'initial_stop_loss': None, 'initial_stop_loss_abs': None, 'initial_stop_loss_pct': None, 'initial_stop_loss_ratio': None, @@ -905,7 +900,6 @@ def test_to_json(default_conf, fee): 'sell_reason': None, 'sell_order_status': None, 'strategy': None, - 'ticker_interval': None, 'timeframe': None, 'exchange': 'bittrex', } From b58d6d38b5e080d7bfcf06d9813f55f55bd55e69 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 Nov 2020 08:34:12 +0100 Subject: [PATCH 0875/1197] Use correct fields in telegram --- freqtrade/rpc/telegram.py | 7 +++---- tests/rpc/test_rpc_telegram.py | 11 ++++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 9bbae871d..31ec33b63 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -247,14 +247,13 @@ class Telegram(RPC): "*Open Rate:* `{open_rate:.8f}`", "*Close Rate:* `{close_rate}`" if r['close_rate'] else "", "*Current Rate:* `{current_rate:.8f}`", - ("*Close Profit:* `{close_profit_pct}`" - if r['close_profit_pct'] is not None else ""), - "*Current Profit:* `{current_profit_pct:.2f}%`", + ("*Current Profit:* " if r['is_open'] else "*Close Profit: *") + + "`{profit_pct:.2f}%`", ] if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] and r['initial_stop_loss_pct'] is not None): # Adding initial stoploss only if it is different from stoploss - lines.append("*Initial Stoploss:* `{initial_stop_loss:.8f}` " + lines.append("*Initial Stoploss:* `{initial_stop_loss_abs:.8f}` " "`({initial_stop_loss_pct:.2f}%)`") # Adding stoploss and stoploss percentage only if it is not None diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index f1246005f..8e81db106 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -163,16 +163,17 @@ def test_telegram_status(default_conf, update, mocker) -> None: 'amount': 90.99181074, 'stake_amount': 90.99181074, 'close_profit_pct': None, - 'current_profit': -0.0059, - 'current_profit_pct': -0.59, - 'initial_stop_loss': 1.098e-05, - 'stop_loss': 1.099e-05, + 'profit': -0.0059, + 'profit_pct': -0.59, + 'initial_stop_loss_abs': 1.098e-05, + 'stop_loss_abs': 1.099e-05, 'sell_order_status': None, 'initial_stop_loss_pct': -0.05, 'stoploss_current_dist': 1e-08, 'stoploss_current_dist_pct': -0.02, 'stop_loss_pct': -0.01, - 'open_order': '(limit buy rem=0.00000000)' + 'open_order': '(limit buy rem=0.00000000)', + 'is_open': True }]), _status_table=status_table, _send_msg=msg_mock From 7d2bd00f0ca421857ba475838269397bc64ec536 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 3 Nov 2020 09:23:07 +0100 Subject: [PATCH 0876/1197] Update forgotten arrow.timestamp occurance --- tests/exchange/test_exchange.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index af08d753e..e4452a83c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1302,7 +1302,8 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): raise TimeoutError() exchange._async_get_candle_history = MagicMock(side_effect=mock_get_candle_hist_error) - ret = exchange.get_historic_ohlcv(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000)) + ret = exchange.get_historic_ohlcv(pair, "5m", int( + (arrow.utcnow().int_timestamp - since) * 1000)) assert log_has_re(r"Async code raised an exception: .*", caplog) From 8e03fee8685ccbb8098cabb81bc622498e8dbf53 Mon Sep 17 00:00:00 2001 From: radwayne <73605415+radwayne@users.noreply.github.com> Date: Fri, 6 Nov 2020 13:56:46 +0100 Subject: [PATCH 0877/1197] Update interface.py Changed The should_sell() method, to handle the case where both ROI and trailing stoploss are reached in backtest. --- freqtrade/strategy/interface.py | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 1c6aa535d..44a281ebe 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -475,16 +475,27 @@ class IStrategy(ABC): stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, current_time=date, current_profit=current_profit, force_stoploss=force_stoploss, high=high) - - if stoplossflag.sell_flag: - logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, " - f"sell_type={stoplossflag.sell_type}") - return stoplossflag - + # Set current rate to high for backtesting sell current_rate = high or rate current_profit = trade.calc_profit_ratio(current_rate) config_ask_strategy = self.config.get('ask_strategy', {}) + + roi_reached = self.min_roi_reached(trade=trade, current_profit=current_profit, + current_time=date) + + if stoplossflag.sell_flag: + + # When backtesting, in the case of trailing_stop_loss, + # make sure we don't make a profit higher than ROI. + if stoplossflag.sell_type == SellType.TRAILING_STOP_LOSS and roi_reached: + logger.debug(f"{trade.pair} - Required profit reached. sell_flag=True, " + f"sell_type=SellType.ROI") + return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) + + logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, " + f"sell_type={stoplossflag.sell_type}") + return stoplossflag if buy and config_ask_strategy.get('ignore_roi_if_buy_signal', False): # This one is noisy, commented out @@ -492,7 +503,7 @@ class IStrategy(ABC): return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) # Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee) - if self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date): + if roi_reached: logger.debug(f"{trade.pair} - Required profit reached. sell_flag=True, " f"sell_type=SellType.ROI") return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) From 2af1c80fd536a94aca53f45e5cd0f3c2f8a69781 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Nov 2020 11:26:02 +0100 Subject: [PATCH 0878/1197] Convert _rpc_show_config to static method --- freqtrade/rpc/api_server.py | 2 +- freqtrade/rpc/rpc.py | 20 ++++++++++++-------- freqtrade/rpc/telegram.py | 3 ++- tests/rpc/test_rpc_apiserver.py | 4 ++-- tests/rpc/test_rpc_telegram.py | 3 ++- 5 files changed, 19 insertions(+), 13 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index be21179ad..7f4773d57 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -329,7 +329,7 @@ class ApiServer(RPC): """ Prints the bot's version """ - return jsonify(self._rpc_show_config(self._config)) + return jsonify(RPC._rpc_show_config(self._config, self._freqtrade.state)) @require_login @rpc_catch_errors diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index efeb361ae..888dc11ec 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -93,7 +93,8 @@ class RPC: def send_msg(self, msg: Dict[str, str]) -> None: """ Sends a message to all registered rpc modules """ - def _rpc_show_config(self, config) -> Dict[str, Any]: + @staticmethod + def _rpc_show_config(config, botstate: State) -> Dict[str, Any]: """ Return a dict of config options. Explicitly does NOT return the full config to avoid leakage of sensitive @@ -104,21 +105,24 @@ class RPC: 'stake_currency': config['stake_currency'], 'stake_amount': config['stake_amount'], 'max_open_trades': config['max_open_trades'], - 'minimal_roi': config['minimal_roi'].copy(), - 'stoploss': config['stoploss'], - 'trailing_stop': config['trailing_stop'], + 'minimal_roi': config['minimal_roi'].copy() if 'minimal_roi' in config else {}, + 'stoploss': config.get('stoploss'), + 'trailing_stop': config.get('trailing_stop'), 'trailing_stop_positive': config.get('trailing_stop_positive'), 'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'), 'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'), - 'timeframe': config['timeframe'], - 'timeframe_ms': timeframe_to_msecs(config['timeframe']), - 'timeframe_min': timeframe_to_minutes(config['timeframe']), + 'timeframe': config.get('timeframe'), + 'timeframe_ms': timeframe_to_msecs(config['timeframe'] + ) if 'timeframe' in config else '', + 'timeframe_min': timeframe_to_minutes(config['timeframe'] + ) if 'timeframe' in config else '', 'exchange': config['exchange']['name'], 'strategy': config['strategy'], 'forcebuy_enabled': config.get('forcebuy_enable', False), 'ask_strategy': config.get('ask_strategy', {}), 'bid_strategy': config.get('bid_strategy', {}), - 'state': str(self._freqtrade.state) if self._freqtrade else '', + 'state': str(botstate), + 'runmode': config['runmode'].value } return val diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 31ec33b63..31d5bbfbd 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -775,7 +775,8 @@ class Telegram(RPC): :param update: message update :return: None """ - val = self._rpc_show_config(self._freqtrade.config) + val = RPC._rpc_show_config(self._freqtrade.config, self._freqtrade.state) + if val['trailing_stop']: sl_info = ( f"*Initial Stoploss:* `{val['stoploss']}`\n" diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 80de839e4..0dc43474f 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -14,7 +14,7 @@ from freqtrade.__init__ import __version__ from freqtrade.loggers import setup_logging, setup_logging_pre from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc.api_server import BASE_URI, ApiServer -from freqtrade.state import State +from freqtrade.state import RunMode, State from tests.conftest import create_mock_trades, get_patched_freqtradebot, log_has, patch_get_signal @@ -26,7 +26,7 @@ _TEST_PASS = "SuperSecurePassword1!" def botclient(default_conf, mocker): setup_logging_pre() setup_logging(default_conf) - + default_conf['runmode'] = RunMode.DRY_RUN default_conf.update({"api_server": {"enabled": True, "listen_ip_address": "127.0.0.1", "listen_port": 8080, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 8e81db106..7885a251d 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -21,7 +21,7 @@ from freqtrade.loggers import setup_logging from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPCMessageType from freqtrade.rpc.telegram import Telegram, authorized_only -from freqtrade.state import State +from freqtrade.state import RunMode, State from freqtrade.strategy.interface import SellType from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, patch_exchange, patch_get_signal, patch_whitelist) @@ -1309,6 +1309,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) + default_conf['runmode'] = RunMode.DRY_RUN freqtradebot = get_patched_freqtradebot(mocker, default_conf) telegram = Telegram(freqtradebot) From 5243214a36a880a1ee830e9fe1b89bf1fcece92f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2020 05:44:29 +0000 Subject: [PATCH 0879/1197] Bump mkdocs-material from 6.1.2 to 6.1.4 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 6.1.2 to 6.1.4. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/6.1.2...6.1.4) Signed-off-by: dependabot[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 47f0eff1c..f034b0b36 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==6.1.2 +mkdocs-material==6.1.4 mdx_truly_sane_lists==1.2 pymdown-extensions==8.0.1 From 42d9e3a28f788e2fd7f59f42f620d6a991dc6ed2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2020 05:44:40 +0000 Subject: [PATCH 0880/1197] Bump numpy from 1.19.3 to 1.19.4 Bumps [numpy](https://github.com/numpy/numpy) from 1.19.3 to 1.19.4. - [Release notes](https://github.com/numpy/numpy/releases) - [Changelog](https://github.com/numpy/numpy/blob/master/doc/HOWTO_RELEASE.rst.txt) - [Commits](https://github.com/numpy/numpy/compare/v1.19.3...v1.19.4) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index afe48171c..818741207 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -numpy==1.19.3 +numpy==1.19.4 pandas==1.1.4 ccxt==1.37.14 From 6063f2f91f319593085fe0bab300df638fd08faf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2020 05:44:49 +0000 Subject: [PATCH 0881/1197] Bump questionary from 1.7.0 to 1.8.0 Bumps [questionary](https://github.com/tmbo/questionary) from 1.7.0 to 1.8.0. - [Release notes](https://github.com/tmbo/questionary/releases) - [Commits](https://github.com/tmbo/questionary/compare/1.7.0...1.8.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index afe48171c..2789ca16a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,5 +35,5 @@ flask-cors==3.0.9 # Support for colorized terminal output colorama==0.4.4 # Building config files interactively -questionary==1.7.0 +questionary==1.8.0 prompt-toolkit==3.0.8 From 88b2f3f0d1b914f76e1a9921b3547a81d2040ea1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2020 05:44:51 +0000 Subject: [PATCH 0882/1197] Bump scipy from 1.5.3 to 1.5.4 Bumps [scipy](https://github.com/scipy/scipy) from 1.5.3 to 1.5.4. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.5.3...v1.5.4) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 5b68c1ea1..7e480b8c9 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.5.3 +scipy==1.5.4 scikit-learn==0.23.2 scikit-optimize==0.8.1 filelock==3.0.12 From 59e846d554f36d1eb126f5ae0d0e52ad22a0a6af Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Nov 2020 07:48:14 +0000 Subject: [PATCH 0883/1197] Bump ccxt from 1.37.14 to 1.37.41 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.37.14 to 1.37.41. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.37.14...1.37.41) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 37a57e3f5..4622afa15 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.4 pandas==1.1.4 -ccxt==1.37.14 +ccxt==1.37.41 aiohttp==3.7.2 SQLAlchemy==1.3.20 python-telegram-bot==13.0 From 13da8f936812bbfd255c4dfce4dd451dc9134c46 Mon Sep 17 00:00:00 2001 From: Daniel Goller Date: Mon, 9 Nov 2020 08:34:40 +0000 Subject: [PATCH 0884/1197] Added ConstPairList handler to skip validation of pairs if you want to backtest a pair that's not live any more, e.g. expiring contracts. --- freqtrade/constants.py | 2 +- freqtrade/pairlist/ConstPairList.py | 60 +++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 freqtrade/pairlist/ConstPairList.py diff --git a/freqtrade/constants.py b/freqtrade/constants.py index dc5384f6f..21308b2dc 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -23,7 +23,7 @@ ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', 'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily', 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily'] -AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', +AVAILABLE_PAIRLISTS = ['ConstPairList', 'StaticPairList', 'VolumePairList', 'AgeFilter', 'PrecisionFilter', 'PriceFilter', 'ShuffleFilter', 'SpreadFilter'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] diff --git a/freqtrade/pairlist/ConstPairList.py b/freqtrade/pairlist/ConstPairList.py new file mode 100644 index 000000000..e5b009c55 --- /dev/null +++ b/freqtrade/pairlist/ConstPairList.py @@ -0,0 +1,60 @@ +""" +Const Pair List provider + +Provides pair white list as it configured in config without checking for active markets +""" +import logging +from typing import Any, Dict, List + +from freqtrade.exceptions import OperationalException +from freqtrade.pairlist.IPairList import IPairList + + +logger = logging.getLogger(__name__) + + +class ConstPairList(IPairList): + + def __init__(self, exchange, pairlistmanager, + config: Dict[str, Any], pairlistconfig: Dict[str, Any], + pairlist_pos: int) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + + if self._pairlist_pos != 0: + raise OperationalException(f"{self.name} can only be used in the first position " + "in the list of Pairlist Handlers.") + + @property + def needstickers(self) -> bool: + """ + Boolean property defining if tickers are necessary. + If no Pairlist requires tickers, an empty List is passed + as tickers argument to filter_pairlist + """ + return False + + def short_desc(self) -> str: + """ + Short whitelist method description - used for startup-messages + -> Please overwrite in subclasses + """ + return f"{self.name}" + + def gen_pairlist(self, cached_pairlist: List[str], tickers: Dict) -> List[str]: + """ + Generate the pairlist + :param cached_pairlist: Previously generated pairlist (cached) + :param tickers: Tickers (from exchange.get_tickers()). + :return: List of pairs + """ + return self._config['exchange']['pair_whitelist'] + + def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: + """ + Filters and sorts pairlist and returns the whitelist again. + Called on each bot iteration - please use internal caching if necessary + :param pairlist: pairlist to filter or sort + :param tickers: Tickers (from exchange.get_tickers()). May be cached. + :return: new whitelist + """ + return pairlist From 916776bb53642b25c4be09bca82ef0c1467798d8 Mon Sep 17 00:00:00 2001 From: Daniel Goller Date: Mon, 9 Nov 2020 08:37:38 +0000 Subject: [PATCH 0885/1197] Option to skip exchange validation, required to backtest pairs that are not live on the exchange any more. --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 659ff59bc..baa379db1 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -115,7 +115,7 @@ class Exchange: logger.info('Using Exchange "%s"', self.name) - if validate: + if validate and not exchange_config.get('skip_validation'): # Check if timeframe is available self.validate_timeframes(config.get('timeframe')) From 2640dfee9387613480e66da00a4fac5b260c1e41 Mon Sep 17 00:00:00 2001 From: Daniel Goller Date: Thu, 12 Nov 2020 11:27:30 +0000 Subject: [PATCH 0886/1197] Revert "Added ConstPairList handler to skip validation of pairs if you want to backtest a pair that's not live any more, e.g. expiring contracts." This reverts commit 13da8f936812bbfd255c4dfce4dd451dc9134c46. --- freqtrade/constants.py | 2 +- freqtrade/pairlist/ConstPairList.py | 60 ----------------------------- 2 files changed, 1 insertion(+), 61 deletions(-) delete mode 100644 freqtrade/pairlist/ConstPairList.py diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 21308b2dc..dc5384f6f 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -23,7 +23,7 @@ ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', 'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily', 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily'] -AVAILABLE_PAIRLISTS = ['ConstPairList', 'StaticPairList', 'VolumePairList', +AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PrecisionFilter', 'PriceFilter', 'ShuffleFilter', 'SpreadFilter'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] diff --git a/freqtrade/pairlist/ConstPairList.py b/freqtrade/pairlist/ConstPairList.py deleted file mode 100644 index e5b009c55..000000000 --- a/freqtrade/pairlist/ConstPairList.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -Const Pair List provider - -Provides pair white list as it configured in config without checking for active markets -""" -import logging -from typing import Any, Dict, List - -from freqtrade.exceptions import OperationalException -from freqtrade.pairlist.IPairList import IPairList - - -logger = logging.getLogger(__name__) - - -class ConstPairList(IPairList): - - def __init__(self, exchange, pairlistmanager, - config: Dict[str, Any], pairlistconfig: Dict[str, Any], - pairlist_pos: int) -> None: - super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) - - if self._pairlist_pos != 0: - raise OperationalException(f"{self.name} can only be used in the first position " - "in the list of Pairlist Handlers.") - - @property - def needstickers(self) -> bool: - """ - Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed - as tickers argument to filter_pairlist - """ - return False - - def short_desc(self) -> str: - """ - Short whitelist method description - used for startup-messages - -> Please overwrite in subclasses - """ - return f"{self.name}" - - def gen_pairlist(self, cached_pairlist: List[str], tickers: Dict) -> List[str]: - """ - Generate the pairlist - :param cached_pairlist: Previously generated pairlist (cached) - :param tickers: Tickers (from exchange.get_tickers()). - :return: List of pairs - """ - return self._config['exchange']['pair_whitelist'] - - def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: - """ - Filters and sorts pairlist and returns the whitelist again. - Called on each bot iteration - please use internal caching if necessary - :param pairlist: pairlist to filter or sort - :param tickers: Tickers (from exchange.get_tickers()). May be cached. - :return: new whitelist - """ - return pairlist From 2424ac94c27c146a553aed362c04ac1850733381 Mon Sep 17 00:00:00 2001 From: Daniel Goller Date: Thu, 12 Nov 2020 11:29:46 +0000 Subject: [PATCH 0887/1197] skip the check for active markets with flag for existing StaticPairList --- freqtrade/pairlist/StaticPairList.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/freqtrade/pairlist/StaticPairList.py b/freqtrade/pairlist/StaticPairList.py index aa6268ba3..3b6440763 100644 --- a/freqtrade/pairlist/StaticPairList.py +++ b/freqtrade/pairlist/StaticPairList.py @@ -24,6 +24,8 @@ class StaticPairList(IPairList): raise OperationalException(f"{self.name} can only be used in the first position " "in the list of Pairlist Handlers.") + self._allow_inactive = self._pairlistconfig.get('allow_inactive', False) + @property def needstickers(self) -> bool: """ @@ -47,7 +49,10 @@ class StaticPairList(IPairList): :param tickers: Tickers (from exchange.get_tickers()). :return: List of pairs """ - return self._whitelist_for_active_markets(self._config['exchange']['pair_whitelist']) + if self._allow_inactive: + return self._config['exchange']['pair_whitelist'] + else: + return self._whitelist_for_active_markets(self._config['exchange']['pair_whitelist']) def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: """ From 2d6bfe1592ab61258d523065cdf4b38cb3857521 Mon Sep 17 00:00:00 2001 From: Daniel Goller Date: Thu, 12 Nov 2020 11:32:45 +0000 Subject: [PATCH 0888/1197] only skip pair validation rather than all of it --- freqtrade/exchange/exchange.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index baa379db1..0e982a06f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -115,7 +115,7 @@ class Exchange: logger.info('Using Exchange "%s"', self.name) - if validate and not exchange_config.get('skip_validation'): + if validate: # Check if timeframe is available self.validate_timeframes(config.get('timeframe')) @@ -124,7 +124,8 @@ class Exchange: # Check if all pairs are available self.validate_stakecurrency(config['stake_currency']) - self.validate_pairs(config['exchange']['pair_whitelist']) + if not exchange_config.get('skip_pair_validation'): + self.validate_pairs(config['exchange']['pair_whitelist']) self.validate_ordertypes(config.get('order_types', {})) self.validate_order_time_in_force(config.get('order_time_in_force', {})) self.validate_required_startup_candles(config.get('startup_candle_count', 0)) From 4eb96cfc4f24bb2a7e5c3b93f4374b2d8a14c088 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 13 Nov 2020 06:51:45 +0100 Subject: [PATCH 0889/1197] Allow locks to be gathered even when the bot is stopped --- freqtrade/rpc/rpc.py | 2 -- tests/rpc/test_rpc_telegram.py | 7 ------- 2 files changed, 9 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 888dc11ec..90564a19d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -605,8 +605,6 @@ class RPC: def _rpc_locks(self) -> Dict[str, Any]: """ Returns the current locks""" - if self._freqtrade.state != State.RUNNING: - raise RPCException('trader is not running') locks = PairLocks.get_pair_locks(None) return { diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 7885a251d..ace44a34a 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1041,13 +1041,6 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) - freqtradebot.state = State.STOPPED - telegram._locks(update=update, context=MagicMock()) - assert msg_mock.call_count == 1 - assert 'not running' in msg_mock.call_args_list[0][0][0] - msg_mock.reset_mock() - freqtradebot.state = State.RUNNING - PairLocks.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason') PairLocks.lock_pair('XRP/BTC', arrow.utcnow().shift(minutes=20).datetime, 'deadbeef') From 08b52926c816331033087b47c2afd1cb047c4452 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 13 Nov 2020 10:43:48 +0100 Subject: [PATCH 0890/1197] Catch asyncio.TimeoutError when reloading async markets --- freqtrade/exchange/exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 659ff59bc..e74f5668c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -282,7 +282,7 @@ class Exchange: asyncio.get_event_loop().run_until_complete( self._api_async.load_markets(reload=reload)) - except ccxt.BaseError as e: + except (asyncio.TimeoutError, ccxt.BaseError) as e: logger.warning('Could not load async markets. Reason: %s', e) return From 164105acf24503a7349621add1c3b178b74ec5ff Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Nov 2020 08:25:57 +0100 Subject: [PATCH 0891/1197] Adjust startup_candle_count of sample strategies --- freqtrade/templates/base_strategy.py.j2 | 2 +- freqtrade/templates/sample_strategy.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index ce2c6d5c0..4a1b43e36 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -63,7 +63,7 @@ class {{ strategy }}(IStrategy): ignore_roi_if_buy_signal = False # Number of candles the strategy requires before producing valid signals - startup_candle_count: int = 20 + startup_candle_count: int = 30 # Optional order type mapping. order_types = { diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 103f68a43..44590dbbe 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -64,7 +64,7 @@ class SampleStrategy(IStrategy): ignore_roi_if_buy_signal = False # Number of candles the strategy requires before producing valid signals - startup_candle_count: int = 20 + startup_candle_count: int = 30 # Optional order type mapping. order_types = { From 05f0cc787c432235fb31255725645fb42bb32fd0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Nov 2020 09:28:00 +0100 Subject: [PATCH 0892/1197] Plotting should use startup_candles too closes #3943 --- freqtrade/plot/plotting.py | 18 ++++++++++++++---- tests/test_plotting.py | 3 ++- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index a89732df5..f7d300593 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -9,9 +9,9 @@ from freqtrade.data.btanalysis import (calculate_max_drawdown, combine_dataframe create_cum_profit, extract_trades_of_period, load_trades) from freqtrade.data.converter import trim_dataframe from freqtrade.data.dataprovider import DataProvider -from freqtrade.data.history import load_data +from freqtrade.data.history import get_timerange, load_data from freqtrade.exceptions import OperationalException -from freqtrade.exchange import timeframe_to_prev_date +from freqtrade.exchange import timeframe_to_prev_date, timeframe_to_seconds from freqtrade.misc import pair_to_filename from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.strategy import IStrategy @@ -29,7 +29,7 @@ except ImportError: exit(1) -def init_plotscript(config): +def init_plotscript(config, startup_candles: int = 0): """ Initialize objects needed for plotting :return: Dict with candle (OHLCV) data, trades and pairs @@ -48,9 +48,16 @@ def init_plotscript(config): pairs=pairs, timeframe=config.get('timeframe', '5m'), timerange=timerange, + startup_candles=startup_candles, data_format=config.get('dataformat_ohlcv', 'json'), ) + if startup_candles: + min_date, max_date = get_timerange(data) + logger.info(f"Loading data from {min_date} to {max_date}") + timerange.adjust_start_if_necessary(timeframe_to_seconds(config.get('timeframe', '5m')), + startup_candles, min_date) + no_trades = False filename = config.get('exportfilename') if config.get('no_trades', False): @@ -72,6 +79,7 @@ def init_plotscript(config): return {"ohlcv": data, "trades": trades, "pairs": pairs, + "timerange": timerange, } @@ -474,7 +482,8 @@ def load_and_plot_trades(config: Dict[str, Any]): exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config) IStrategy.dp = DataProvider(config, exchange) - plot_elements = init_plotscript(config) + plot_elements = init_plotscript(config, strategy.startup_candle_count) + timerange = plot_elements['timerange'] trades = plot_elements['trades'] pair_counter = 0 for pair, data in plot_elements["ohlcv"].items(): @@ -482,6 +491,7 @@ def load_and_plot_trades(config: Dict[str, Any]): logger.info("analyse pair %s", pair) df_analyzed = strategy.analyze_ticker(data, {'pair': pair}) + df_analyzed = trim_dataframe(df_analyzed, timerange) trades_pair = trades.loc[trades['pair'] == pair] trades_pair = extract_trades_of_period(df_analyzed, trades_pair) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 401f66b60..d3f97013d 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -51,9 +51,10 @@ def test_init_plotscript(default_conf, mocker, testdatadir): assert "ohlcv" in ret assert "trades" in ret assert "pairs" in ret + assert 'timerange' in ret default_conf['pairs'] = ["TRX/BTC", "ADA/BTC"] - ret = init_plotscript(default_conf) + ret = init_plotscript(default_conf, 20) assert "ohlcv" in ret assert "TRX/BTC" in ret["ohlcv"] assert "ADA/BTC" in ret["ohlcv"] From 7243c8ee56852d1fd649ffcbc5bff28fd57e7e8a Mon Sep 17 00:00:00 2001 From: SamVerhaegen Date: Sun, 15 Nov 2020 13:06:05 +0100 Subject: [PATCH 0893/1197] Fix typo in windows installation docs. --- docs/windows_installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/windows_installation.md b/docs/windows_installation.md index 0ef0f131f..924459a6d 100644 --- a/docs/windows_installation.md +++ b/docs/windows_installation.md @@ -32,7 +32,7 @@ python -m venv .env .env\Scripts\activate.ps1 # optionally install ta-lib from wheel # Eventually adjust the below filename to match the downloaded wheel -pip install build_helpes/TA_Lib‑0.4.19‑cp38‑cp38‑win_amd64.whl +pip install build_helpers/TA_Lib-0.4.19-cp38-cp38-win_amd64.whl pip install -r requirements.txt pip install -e . freqtrade From da16474b2561a12636e0440d2100b0bb691a8f2d Mon Sep 17 00:00:00 2001 From: Aleksey Savin Date: Sun, 15 Nov 2020 15:13:44 +0300 Subject: [PATCH 0894/1197] Update telegram-usage.md --- docs/telegram-usage.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index ce2d715a0..5def6efd2 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -35,12 +35,16 @@ Copy the API Token (`22222222:APITOKEN` in the above example) and keep use it fo Don't forget to start the conversation with your bot, by clicking `/START` button -### 2. Get your user id +### 2. Telegram user_id +#### Get your user id Talk to the [userinfobot](https://telegram.me/userinfobot) Get your "Id", you will use it for the config parameter `chat_id`. +#### Use Group id +You can use bots in telegram groups just adding them to it. You can find the group id by adding a [RawDataBot](https://telegram.me/rawdatabot) to it, group id is the `"chat":{"id":-1001332619709}` in the [RawDataBot](https://telegram.me/rawdatabot) message. Dont forget about "-" (minus symbol) in start of value if it is and use string type in config, for example: `"chat_id":"-1001332619709"`. + ## Control telegram noise Freqtrade provides means to control the verbosity of your telegram bot. From 7b4c1ec3ced1ebcc2f8be06e2602f7223d964d5d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Nov 2020 15:40:40 +0100 Subject: [PATCH 0895/1197] Small wording changes --- docs/telegram-usage.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 5def6efd2..09cf21223 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -36,6 +36,7 @@ Copy the API Token (`22222222:APITOKEN` in the above example) and keep use it fo Don't forget to start the conversation with your bot, by clicking `/START` button ### 2. Telegram user_id + #### Get your user id Talk to the [userinfobot](https://telegram.me/userinfobot) @@ -43,7 +44,20 @@ Talk to the [userinfobot](https://telegram.me/userinfobot) Get your "Id", you will use it for the config parameter `chat_id`. #### Use Group id -You can use bots in telegram groups just adding them to it. You can find the group id by adding a [RawDataBot](https://telegram.me/rawdatabot) to it, group id is the `"chat":{"id":-1001332619709}` in the [RawDataBot](https://telegram.me/rawdatabot) message. Dont forget about "-" (minus symbol) in start of value if it is and use string type in config, for example: `"chat_id":"-1001332619709"`. + +You can use bots in telegram groups by just adding them to the group. You can find the group id by first adding a [RawDataBot](https://telegram.me/rawdatabot) to your group. The Group id is shown as id in the `"chat"` section, which the RawDataBot will send to you: + +``` json +"chat":{ + "id":-1001332619709 +} +``` + +For the Freqtrade configuration, you can then use the the full value (including `-` if it's there) as string: + +```json + "chat_id": "-1001332619709" +``` ## Control telegram noise From 26176d4c91c20cb24a762d4eb8ab204a9951d1ac Mon Sep 17 00:00:00 2001 From: Samaoo Date: Sun, 15 Nov 2020 19:55:09 +0100 Subject: [PATCH 0896/1197] Update exchanges.md According to https://blog.kraken.com/post/5282/stop-loss-limit-take-profit-limit-two-new-advanced-orders-go-live-on-kraken/ Stop Loss Limit orders are enabled again --- docs/exchanges.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index fcf7c1cad..ac386c937 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -23,7 +23,7 @@ Binance has been split into 3, and users must use the correct ccxt exchange ID f ## Kraken !!! Tip "Stoploss on Exchange" - Kraken supports `stoploss_on_exchange` and uses stop-loss-market orders. It provides great advantages, so we recommend to benefit from it, however since the resulting order is a stoploss-market order, sell-rates are not guaranteed, which makes this feature less secure than on other exchanges. This limitation is based on kraken's policy [source](https://blog.kraken.com/post/1234/announcement-delisting-pairs-and-temporary-suspension-of-advanced-order-types/) and [source2](https://blog.kraken.com/post/1494/kraken-enables-advanced-orders-and-adds-10-currency-pairs/) - which has stoploss-limit orders disabled. + Kraken supports `stoploss_on_exchange` and uses stop-loss-market orders. It provides great advantages, so we recommend to benefit from it. ### Historic Kraken data From ef4ab601a9fc2a059d0eaf3fd77c4bb7f63f8ac3 Mon Sep 17 00:00:00 2001 From: Samaoo Date: Sun, 15 Nov 2020 20:02:19 +0100 Subject: [PATCH 0897/1197] Update exchanges.md --- docs/exchanges.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index ac386c937..5d7505795 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -23,7 +23,7 @@ Binance has been split into 3, and users must use the correct ccxt exchange ID f ## Kraken !!! Tip "Stoploss on Exchange" - Kraken supports `stoploss_on_exchange` and uses stop-loss-market orders. It provides great advantages, so we recommend to benefit from it. + Kraken supports `stoploss_on_exchange`. It provides great advantages, so we recommend to benefit from it. ### Historic Kraken data From 6ebc2f38974eff34140d63b2955f1e1b54f82b3e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Nov 2020 05:40:25 +0000 Subject: [PATCH 0898/1197] Bump mkdocs-material from 6.1.4 to 6.1.5 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 6.1.4 to 6.1.5. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/6.1.4...6.1.5) Signed-off-by: dependabot[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 f034b0b36..cd294bef6 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==6.1.4 +mkdocs-material==6.1.5 mdx_truly_sane_lists==1.2 pymdown-extensions==8.0.1 From e52c181a2a2a8a369a69fdff2f758767bb42b969 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Nov 2020 05:40:35 +0000 Subject: [PATCH 0899/1197] Bump flask-jwt-extended from 3.24.1 to 3.25.0 Bumps [flask-jwt-extended](https://github.com/vimalloc/flask-jwt-extended) from 3.24.1 to 3.25.0. - [Release notes](https://github.com/vimalloc/flask-jwt-extended/releases) - [Commits](https://github.com/vimalloc/flask-jwt-extended/compare/3.24.1...3.25.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4622afa15..ea33cdf3c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,7 +29,7 @@ sdnotify==0.3.2 # Api server flask==1.1.2 -flask-jwt-extended==3.24.1 +flask-jwt-extended==3.25.0 flask-cors==3.0.9 # Support for colorized terminal output From 23947cf30b0dcfc7aab066d48237e0dcd229fb6b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Nov 2020 05:40:50 +0000 Subject: [PATCH 0900/1197] Bump requests from 2.24.0 to 2.25.0 Bumps [requests](https://github.com/psf/requests) from 2.24.0 to 2.25.0. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/master/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.24.0...v2.25.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 4622afa15..e56dcf4d1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ SQLAlchemy==1.3.20 python-telegram-bot==13.0 arrow==0.17.0 cachetools==4.1.1 -requests==2.24.0 +requests==2.25.0 urllib3==1.25.11 wrapt==1.12.1 jsonschema==3.2.0 From f092a923990515976cfccabc59010d8ccddfd5ac Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Nov 2020 07:51:10 +0000 Subject: [PATCH 0901/1197] Bump urllib3 from 1.25.11 to 1.26.2 Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.25.11 to 1.26.2. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/master/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.25.11...1.26.2) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 5ce060819..d60a0ebba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ python-telegram-bot==13.0 arrow==0.17.0 cachetools==4.1.1 requests==2.25.0 -urllib3==1.25.11 +urllib3==1.26.2 wrapt==1.12.1 jsonschema==3.2.0 TA-Lib==0.4.19 From 3f2addb729f0d462ec11a40197eba4f00c17706a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Nov 2020 09:17:22 +0000 Subject: [PATCH 0902/1197] Bump ccxt from 1.37.41 to 1.37.69 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.37.41 to 1.37.69. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.37.41...1.37.69) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d60a0ebba..67b69a5dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.4 pandas==1.1.4 -ccxt==1.37.41 +ccxt==1.37.69 aiohttp==3.7.2 SQLAlchemy==1.3.20 python-telegram-bot==13.0 From 9621734adc37eb6f6edbd24907e42d35628d70ea Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Nov 2020 06:53:38 +0100 Subject: [PATCH 0903/1197] Allow setting datafromat via configuration closes #3953 --- freqtrade/commands/cli_options.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 4769bccde..619a300ae 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -354,13 +354,11 @@ AVAILABLE_CLI_OPTIONS = { '--data-format-ohlcv', help='Storage format for downloaded candle (OHLCV) data. (default: `%(default)s`).', choices=constants.AVAILABLE_DATAHANDLERS, - default='json' ), "dataformat_trades": Arg( '--data-format-trades', help='Storage format for downloaded trades data. (default: `%(default)s`).', choices=constants.AVAILABLE_DATAHANDLERS, - default='jsongz' ), "exchange": Arg( '--exchange', From 4a215821cdc2c3017c15fff2082a2b5e58773c61 Mon Sep 17 00:00:00 2001 From: Samaoo Date: Tue, 17 Nov 2020 14:07:24 +0100 Subject: [PATCH 0904/1197] Fix typo in windows installation docs --- docs/windows_installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/windows_installation.md b/docs/windows_installation.md index 924459a6d..0c1eeef64 100644 --- a/docs/windows_installation.md +++ b/docs/windows_installation.md @@ -50,7 +50,7 @@ freqtrade error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools ``` -Unfortunately, many packages requiring compilation don't provide a pre-build wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use. +Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use. The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first. From 854d0c481f0b7569f4e28418a64e088aeb6c0bd4 Mon Sep 17 00:00:00 2001 From: Samaoo Date: Tue, 17 Nov 2020 14:14:42 +0100 Subject: [PATCH 0905/1197] Update windows_installation.md --- docs/windows_installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/windows_installation.md b/docs/windows_installation.md index 0c1eeef64..5341ce96b 100644 --- a/docs/windows_installation.md +++ b/docs/windows_installation.md @@ -52,6 +52,6 @@ error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use. -The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first. +The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building C code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first. --- From 181d3a3808412f35be91165e9e092bc0ea5209ba Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2020 05:49:32 +0000 Subject: [PATCH 0906/1197] Bump python from 3.8.6-slim-buster to 3.9.0-slim-buster Bumps python from 3.8.6-slim-buster to 3.9.0-slim-buster. Signed-off-by: dependabot[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2be65274e..68b37afe3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.8.6-slim-buster +FROM python:3.9.0-slim-buster RUN apt-get update \ && apt-get -y install curl build-essential libssl-dev sqlite3 \ From dd42d61d03d584051b39c4ada5132f9d223bcdbd Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 7 Oct 2020 08:48:45 +0200 Subject: [PATCH 0907/1197] Run CI on 3.9 --- .github/workflows/ci.yml | 4 ++-- setup.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f259129d4..d48dec2d3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: os: [ ubuntu-18.04, ubuntu-20.04, macos-latest ] - python-version: [3.7, 3.8] + python-version: [3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 @@ -119,7 +119,7 @@ jobs: strategy: matrix: os: [ windows-latest ] - python-version: [3.7, 3.8] + python-version: [3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 diff --git a/setup.sh b/setup.sh index 049a6a77e..419fde9bb 100755 --- a/setup.sh +++ b/setup.sh @@ -42,7 +42,7 @@ function check_installed_python() { fi if [ -z ${PYTHON} ]; then - echo "No usable python found. Please make sure to have python3.6 or python3.7 installed" + echo "No usable python found. Please make sure to have python3.6, python3.7 or python3.8 installed" exit 1 fi } From 52c9a2c37f62196d43f09b41fb0a90dc187a37e2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 19 Nov 2020 07:30:28 +0100 Subject: [PATCH 0908/1197] Convert np to None when loading hdf5 trades to allow duplicate detection --- freqtrade/data/history/hdf5datahandler.py | 4 +++- tests/data/test_history.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index f6cf9e0d9..00e41673d 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -3,6 +3,7 @@ import re from pathlib import Path from typing import List, Optional +import numpy as np import pandas as pd from freqtrade import misc @@ -175,7 +176,8 @@ class HDF5DataHandler(IDataHandler): if timerange.stoptype == 'date': where.append(f"timestamp < {timerange.stopts * 1e3}") - trades = pd.read_hdf(filename, key=key, mode="r", where=where) + trades: pd.DataFrame = pd.read_hdf(filename, key=key, mode="r", where=where) + trades[['id', 'type']] = trades[['id', 'type']].replace({np.nan: None}) return trades.values.tolist() def trades_purge(self, pair: str) -> bool: diff --git a/tests/data/test_history.py b/tests/data/test_history.py index bbc6e55b4..538a0840f 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -724,6 +724,8 @@ def test_hdf5datahandler_trades_load(testdatadir): trades2 = dh._trades_load('XRP/ETH', timerange) assert len(trades) > len(trades2) + # Check that ID is None (If it's nan, it's wrong) + assert trades2[0][2] is None # unfiltered load has trades before starttime assert len([t for t in trades if t[0] < timerange.startts * 1000]) >= 0 From f88fe5d950daf3ab68f9153648bf8d94f20593b0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 19 Nov 2020 19:14:43 +0100 Subject: [PATCH 0909/1197] Document new "allow_inactive" option --- docs/includes/pairlists.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index ae4ec818d..aebd084ab 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -35,6 +35,10 @@ It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklis ], ``` +By default, only currently enabled pairs are allowed. +To skip pair validation against active markets, set `"allow_inactive": true` within the `StaticPairList` configuration. +This can be useful for backtesting expired pairs (like quarterly spot-markets). + #### Volume Pair List `VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`). From 97e58a42f4caf1803af411937123461bda7ca244 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 19 Nov 2020 19:17:31 +0100 Subject: [PATCH 0910/1197] Update documentation with new options --- docs/configuration.md | 1 + docs/includes/pairlists.md | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index 47362e525..080ddd046 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -87,6 +87,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `exchange.ccxt_sync_config` | Additional CCXT parameters passed to the regular (sync) ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
    **Datatype:** Dict | `exchange.ccxt_async_config` | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
    **Datatype:** Dict | `exchange.markets_refresh_interval` | The interval in minutes in which markets are reloaded.
    *Defaults to `60` minutes.*
    **Datatype:** Positive Integer +| `exchange.skip_pair_validation` | Skip pairlist validation on startup.
    *Defaults to `false`
    **Datatype:** Boolean | `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation. | `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
    *Defaults to `true`.*
    **Datatype:** Boolean | `pairlists` | Define one or more pairlists to be used. [More information below](#pairlists-and-pairlist-handlers).
    *Defaults to `StaticPairList`.*
    **Datatype:** List of Dicts diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index aebd084ab..e6a9fc1a8 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -38,6 +38,7 @@ It uses configuration from `exchange.pair_whitelist` and `exchange.pair_blacklis By default, only currently enabled pairs are allowed. To skip pair validation against active markets, set `"allow_inactive": true` within the `StaticPairList` configuration. This can be useful for backtesting expired pairs (like quarterly spot-markets). +This option must be configured along with `exchange.skip_pair_validation` in the exchange configuration. #### Volume Pair List From aa0c3dced8bac13b35e241f287d1d9a4cbcecd31 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 20 Nov 2020 13:14:02 +0100 Subject: [PATCH 0911/1197] Improve order types documentation --- docs/configuration.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 080ddd046..56ba13414 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -314,22 +314,21 @@ Configuration: } ``` -!!! Note +!!! Note "Market order support" Not all exchanges support "market" orders. The following message will be shown if your exchange does not support market orders: - `"Exchange does not support market orders."` + `"Exchange does not support market orders."` and the bot will refuse to start. -!!! Note - Stoploss on exchange interval is not mandatory. Do not change its value if you are +!!! Warning "Using market orders" + Please carefully read the section [Market order pricing](#market-order-pricing) section when using market orders. + +!!! Note "Stoploss on exchange" + `stoploss_on_exchange_interval` is not mandatory. Do not change its value if you are unsure of what you are doing. For more information about how stoploss works please refer to [the stoploss documentation](stoploss.md). -!!! Note If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new stoploss order. -!!! Warning "Using market orders" - Please read the section [Market order pricing](#market-order-pricing) section when using market orders. - !!! Warning "Warning: stoploss_on_exchange failures" If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however this is not advised. From 5ed85963a950eada9571b2f6f19130ff15878157 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 10:39:49 +0100 Subject: [PATCH 0912/1197] Allow forcebuy price to be a string by converting it to float fix #3970 --- freqtrade/rpc/api_server.py | 2 ++ freqtrade/rpc/rpc.py | 2 +- tests/rpc/test_rpc.py | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 7f4773d57..384d7c6c2 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -508,6 +508,8 @@ class ApiServer(RPC): """ asset = request.json.get("pair") price = request.json.get("price", None) + price = float(price) if price is not None else price + trade = self._rpc_forcebuy(asset, price) if trade: return jsonify(trade.to_json()) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 90564a19d..e608a2274 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -524,7 +524,7 @@ class RPC: stake_currency = self._freqtrade.config.get('stake_currency') if not self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency: raise RPCException( - f'Wrong pair selected. Please pairs with stake {stake_currency} pairs only') + f'Wrong pair selected. Only pairs with stake-currency {stake_currency} allowed.') # check if valid pair # check if pair already has an open pair diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 23ca53e53..47e0f763d 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -868,7 +868,8 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) -> assert trade.open_rate == 0.0001 # Test buy pair not with stakes - with pytest.raises(RPCException, match=r'Wrong pair selected. Please pairs with stake.*'): + with pytest.raises(RPCException, + match=r'Wrong pair selected. Only pairs with stake-currency.*'): rpc._rpc_forcebuy('LTC/ETH', 0.0001) pair = 'XRP/BTC' From 83861fabdea798100c5322a9efe5db4f7a23c84a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 10:52:15 +0100 Subject: [PATCH 0913/1197] Fix #3967, move TradeList type to constants --- freqtrade/constants.py | 3 +++ freqtrade/data/converter.py | 10 +++++---- freqtrade/data/history/hdf5datahandler.py | 4 ++-- freqtrade/data/history/idatahandler.py | 5 +---- freqtrade/data/history/jsondatahandler.py | 4 ++-- tests/data/test_converter.py | 27 ++++++++++++++++++++++- 6 files changed, 40 insertions(+), 13 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index dc5384f6f..3271dda39 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -365,3 +365,6 @@ CANCEL_REASON = { # List of pairs with their timeframes PairWithTimeframe = Tuple[str, str] ListPairsWithTimeframes = List[PairWithTimeframe] + +# Type for trades list +TradeList = List[List] diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 38fa670e9..09930950a 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -10,7 +10,7 @@ from typing import Any, Dict, List import pandas as pd from pandas import DataFrame, to_datetime -from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS +from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList logger = logging.getLogger(__name__) @@ -168,7 +168,7 @@ def trades_remove_duplicates(trades: List[List]) -> List[List]: return [i for i, _ in itertools.groupby(sorted(trades, key=itemgetter(0)))] -def trades_dict_to_list(trades: List[Dict]) -> List[List]: +def trades_dict_to_list(trades: List[Dict]) -> TradeList: """ Convert fetch_trades result into a List (to be more memory efficient). :param trades: List of trades, as returned by ccxt.fetch_trades. @@ -177,16 +177,18 @@ def trades_dict_to_list(trades: List[Dict]) -> List[List]: return [[t[col] for col in DEFAULT_TRADES_COLUMNS] for t in trades] -def trades_to_ohlcv(trades: List, timeframe: str) -> DataFrame: +def trades_to_ohlcv(trades: TradeList, timeframe: str) -> DataFrame: """ Converts trades list to OHLCV list - TODO: This should get a dedicated test :param trades: List of trades, as returned by ccxt.fetch_trades. :param timeframe: Timeframe to resample data to :return: OHLCV Dataframe. + :raises: Valueerror if no trades are provided """ from freqtrade.exchange import timeframe_to_minutes timeframe_minutes = timeframe_to_minutes(timeframe) + if not trades: + raise ValueError('Trade-list empty.') df = pd.DataFrame(trades, columns=DEFAULT_TRADES_COLUMNS) df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms', utc=True,) diff --git a/freqtrade/data/history/hdf5datahandler.py b/freqtrade/data/history/hdf5datahandler.py index 00e41673d..d116637e7 100644 --- a/freqtrade/data/history/hdf5datahandler.py +++ b/freqtrade/data/history/hdf5datahandler.py @@ -9,9 +9,9 @@ import pandas as pd from freqtrade import misc from freqtrade.configuration import TimeRange from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, - ListPairsWithTimeframes) + ListPairsWithTimeframes, TradeList) -from .idatahandler import IDataHandler, TradeList +from .idatahandler import IDataHandler logger = logging.getLogger(__name__) diff --git a/freqtrade/data/history/idatahandler.py b/freqtrade/data/history/idatahandler.py index a170a9dc5..070d9039d 100644 --- a/freqtrade/data/history/idatahandler.py +++ b/freqtrade/data/history/idatahandler.py @@ -13,16 +13,13 @@ from typing import List, Optional, Type from pandas import DataFrame from freqtrade.configuration import TimeRange -from freqtrade.constants import ListPairsWithTimeframes +from freqtrade.constants import ListPairsWithTimeframes, TradeList from freqtrade.data.converter import clean_ohlcv_dataframe, trades_remove_duplicates, trim_dataframe from freqtrade.exchange import timeframe_to_seconds logger = logging.getLogger(__name__) -# Type for trades list -TradeList = List[List] - class IDataHandler(ABC): diff --git a/freqtrade/data/history/jsondatahandler.py b/freqtrade/data/history/jsondatahandler.py index 6436aa13d..9122170d5 100644 --- a/freqtrade/data/history/jsondatahandler.py +++ b/freqtrade/data/history/jsondatahandler.py @@ -8,10 +8,10 @@ from pandas import DataFrame, read_json, to_datetime from freqtrade import misc from freqtrade.configuration import TimeRange -from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, ListPairsWithTimeframes +from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, ListPairsWithTimeframes, TradeList from freqtrade.data.converter import trades_dict_to_list -from .idatahandler import IDataHandler, TradeList +from .idatahandler import IDataHandler logger = logging.getLogger(__name__) diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index fdba7900f..4fdcce4d2 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -1,10 +1,13 @@ # pragma pylint: disable=missing-docstring, C0103 import logging +import pytest + from freqtrade.configuration.timerange import TimeRange from freqtrade.data.converter import (convert_ohlcv_format, convert_trades_format, ohlcv_fill_up_missing_data, ohlcv_to_dataframe, - trades_dict_to_list, trades_remove_duplicates, trim_dataframe) + trades_dict_to_list, trades_remove_duplicates, + trades_to_ohlcv, trim_dataframe) from freqtrade.data.history import (get_timerange, load_data, load_pair_history, validate_backtest_data) from tests.conftest import log_has @@ -26,6 +29,28 @@ def test_ohlcv_to_dataframe(ohlcv_history_list, caplog): assert log_has('Converting candle (OHLCV) data to dataframe for pair UNITTEST/BTC.', caplog) +def test_trades_to_ohlcv(ohlcv_history_list, caplog): + + caplog.set_level(logging.DEBUG) + with pytest.raises(ValueError, match="Trade-list empty."): + trades_to_ohlcv([], '1m') + + trades = [ + [1570752011620, "13519807", None, "sell", 0.00141342, 23.0, 0.03250866], + [1570752011620, "13519808", None, "sell", 0.00141266, 54.0, 0.07628364], + [1570752017964, "13519809", None, "sell", 0.00141266, 8.0, 0.01130128]] + + df = trades_to_ohlcv(trades, '1m') + assert not df.empty + assert len(df) == 1 + assert 'open' in df.columns + assert 'high' in df.columns + assert 'low' in df.columns + assert 'close' in df.columns + assert df.loc[:, 'high'][0] == 0.00141342 + assert df.loc[:, 'low'][0] == 0.00141266 + + def test_ohlcv_fill_up_missing_data(testdatadir, caplog): data = load_pair_history(datadir=testdatadir, timeframe='1m', From e8e3ca0c3c114637a460fdddeea5fca155bdf534 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 10:57:19 +0100 Subject: [PATCH 0914/1197] Catch ValueError from trade_conversion closes #3967 --- freqtrade/data/converter.py | 2 +- freqtrade/data/history/history_utils.py | 9 ++++++--- tests/data/test_history.py | 6 ++++++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index 09930950a..d4053abaa 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -183,7 +183,7 @@ def trades_to_ohlcv(trades: TradeList, timeframe: str) -> DataFrame: :param trades: List of trades, as returned by ccxt.fetch_trades. :param timeframe: Timeframe to resample data to :return: OHLCV Dataframe. - :raises: Valueerror if no trades are provided + :raises: ValueError if no trades are provided """ from freqtrade.exchange import timeframe_to_minutes timeframe_minutes = timeframe_to_minutes(timeframe) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index a420b9dcc..17b510b92 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -356,9 +356,12 @@ def convert_trades_to_ohlcv(pairs: List[str], timeframes: List[str], if erase: if data_handler_ohlcv.ohlcv_purge(pair, timeframe): logger.info(f'Deleting existing data for pair {pair}, interval {timeframe}.') - ohlcv = trades_to_ohlcv(trades, timeframe) - # Store ohlcv - data_handler_ohlcv.ohlcv_store(pair, timeframe, data=ohlcv) + try: + ohlcv = trades_to_ohlcv(trades, timeframe) + # Store ohlcv + data_handler_ohlcv.ohlcv_store(pair, timeframe, data=ohlcv) + except ValueError: + logger.exception(f'Could not convert {pair} to OHLCV.') def get_timerange(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 538a0840f..905798041 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -620,6 +620,12 @@ def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog): _clean_test_file(file1) _clean_test_file(file5) + assert not log_has('Could not convert NoDatapair to OHLCV.', caplog) + + convert_trades_to_ohlcv(['NoDatapair'], timeframes=['1m', '5m'], + datadir=testdatadir, timerange=tr, erase=True) + assert log_has('Could not convert NoDatapair to OHLCV.', caplog) + def test_datahandler_ohlcv_get_pairs(testdatadir): pairs = JsonDataHandler.ohlcv_get_pairs(testdatadir, '5m') From 89ea8dbef2a73c050649d45db6185de8fb8cf789 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 11:13:44 +0100 Subject: [PATCH 0915/1197] Update slack invite --- CONTRIBUTING.md | 3 +-- README.md | 2 +- docs/developer.md | 2 +- docs/faq.md | 2 +- docs/index.md | 2 +- docs/strategy-customization.md | 2 -- 6 files changed, 5 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 399588f88..6b4e8adaf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,8 +12,7 @@ Few pointers for contributions: - New features need to contain unit tests, must conform to PEP8 (max-line-length = 100) and should be documented with the introduction PR. - PR's can be declared as `[WIP]` - which signify Work in Progress Pull Requests (which are not finished). -If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) -or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. +If you are unsure, discuss the feature on our [discord server](https://discord.gg/MA9v74M), on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg) or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. ## Getting started diff --git a/README.md b/README.md index c9f4d0a52..4daa1854a 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ to understand the requirements before sending your pull-requests. Coding is not a necessity to contribute - maybe start with improving our documentation? Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase. -**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. +**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [discord](https://discord.gg/MA9v74M) or [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. **Important:** Always create your PR against the `develop` branch, not `stable`. diff --git a/docs/developer.md b/docs/developer.md index 8ef816d5d..c253f4460 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -2,7 +2,7 @@ This page is intended for developers of Freqtrade, people who want to contribute to the Freqtrade codebase or documentation, or people who want to understand the source code of the application they're running. -All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel in [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) where you can ask questions. +All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel on [discord](https://discord.gg/MA9v74M) or [slack](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg) where you can ask questions. ## Documentation diff --git a/docs/faq.md b/docs/faq.md index a775060de..aa33218fb 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -145,7 +145,7 @@ freqtrade hyperopt --hyperop SampleHyperopt --hyperopt-loss SharpeHyperOptLossDa ### Why does it take a long time to run hyperopt? -* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) - or the Freqtrade [discord community](https://discord.gg/X89cVG). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you. +* Discovering a great strategy with Hyperopt takes time. Study www.freqtrade.io, the Freqtrade Documentation page, join the Freqtrade [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg) - or the Freqtrade [discord community](https://discord.gg/X89cVG). While you patiently wait for the most advanced, free crypto bot in the world, to hand you a possible golden strategy specially designed just for you. * If you wonder why it can take from 20 minutes to days to do 1000 epochs here are some answers: diff --git a/docs/index.md b/docs/index.md index 5608587db..c6697d165 100644 --- a/docs/index.md +++ b/docs/index.md @@ -63,7 +63,7 @@ Alternatively For any questions not covered by the documentation or for further information about the bot, we encourage you to join our passionate Slack community. -Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) to join the Freqtrade Slack channel. +Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg) to join the Freqtrade Slack channel. Alternatively, check out the newly created [discord server](https://discord.gg/MA9v74M). diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 6c7d78864..db007985f 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -770,8 +770,6 @@ To get additional Ideas for strategies, head over to our [strategy repository](h Feel free to use any of them as inspiration for your own strategies. We're happy to accept Pull Requests containing new Strategies to that repo. -We also got a *strategy-sharing* channel in our [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) which is a great place to get and/or share ideas. - ## Next step Now you have a perfect strategy you probably want to backtest it. From 4d60a4cf4e9a850a1c03ac2923e163b6969564fd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 11:32:46 +0100 Subject: [PATCH 0916/1197] Add warning to StochRSI in sample strategy closes #2961 --- freqtrade/templates/sample_strategy.py | 2 ++ freqtrade/templates/subtemplates/indicators_full.j2 | 2 ++ 2 files changed, 4 insertions(+) diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 44590dbbe..b3f9fef07 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -184,6 +184,8 @@ class SampleStrategy(IStrategy): dataframe['fastk'] = stoch_fast['fastk'] # # Stochastic RSI + # Please read https://github.com/freqtrade/freqtrade/issues/2961 before using this. + # STOCHRSI is NOT aligned with tradingview, which may result in non-expected results. # stoch_rsi = ta.STOCHRSI(dataframe) # dataframe['fastd_rsi'] = stoch_rsi['fastd'] # dataframe['fastk_rsi'] = stoch_rsi['fastk'] diff --git a/freqtrade/templates/subtemplates/indicators_full.j2 b/freqtrade/templates/subtemplates/indicators_full.j2 index 60a358bec..57d2ca665 100644 --- a/freqtrade/templates/subtemplates/indicators_full.j2 +++ b/freqtrade/templates/subtemplates/indicators_full.j2 @@ -62,6 +62,8 @@ dataframe['fastd'] = stoch_fast['fastd'] dataframe['fastk'] = stoch_fast['fastk'] # # Stochastic RSI +# Please read https://github.com/freqtrade/freqtrade/issues/2961 before using this. +# STOCHRSI is NOT aligned with tradingview, which may result in non-expected results. # stoch_rsi = ta.STOCHRSI(dataframe) # dataframe['fastd_rsi'] = stoch_rsi['fastd'] # dataframe['fastk_rsi'] = stoch_rsi['fastk'] From 73f0e6e704fc85c57f0b7d982a1ff0df0fae7fa5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 11:40:28 +0100 Subject: [PATCH 0917/1197] Improve wording for discord server fix link to correct docker install guide --- README.md | 10 ++++------ docs/index.md | 13 +++++-------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 4daa1854a..8526b5c91 100644 --- a/README.md +++ b/README.md @@ -132,15 +132,13 @@ The project is currently setup in two main branches: ## Support -### Help / Slack / Discord +### Help / Discord / Slack -For any questions not covered by the documentation or for further information about the bot, we encourage you to join our slack channel. +For any questions not covered by the documentation or for further information about the bot, or to simply engage with like-minded individuals, we encourage you to join our slack channel. -- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE). +Please check out our [discord server](https://discord.gg/MA9v74M). -Alternatively, check out the newly created [discord server](https://discord.gg/MA9v74M). - -*Note*: Since the discord server is relatively new, answers to questions might be slightly delayed as currently the user base quite small. +You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg). ### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) diff --git a/docs/index.md b/docs/index.md index c6697d165..f63aeb6b8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -59,17 +59,14 @@ Alternatively ## Support -### Help / Slack / Discord +### Help / Discord / Slack -For any questions not covered by the documentation or for further information about the bot, we encourage you to join our passionate Slack community. +For any questions not covered by the documentation or for further information about the bot, or to simply engage with like-minded individuals, we encourage you to join our slack channel. -Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg) to join the Freqtrade Slack channel. +Please check out our [discord server](https://discord.gg/MA9v74M). -Alternatively, check out the newly created [discord server](https://discord.gg/MA9v74M). - -!!! Note - Since the discord server is relatively new, answers to questions might be slightly delayed as currently the user base quite small. +You can also join our [Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/zt-jaut7r4m-Y17k4x5mcQES9a9swKuxbg). ## Ready to try? -Begin by reading our installation guide [for docker](docker.md) (recommended), or for [installation without docker](installation.md). +Begin by reading our installation guide [for docker](docker_quickstart.md) (recommended), or for [installation without docker](installation.md). From fb86d8f8ff43646ded784c1ebace16cc1e8fd616 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 15:28:50 +0100 Subject: [PATCH 0918/1197] Add get_historic_ohlcv_as_df to support VolatilityFilter --- freqtrade/exchange/exchange.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2bbdb0d59..2f52c512f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -679,12 +679,25 @@ class Exchange: :param pair: Pair to download :param timeframe: Timeframe to get data for :param since_ms: Timestamp in milliseconds to get history from - :returns List with candle (OHLCV) data + :return: List with candle (OHLCV) data """ return asyncio.get_event_loop().run_until_complete( self._async_get_historic_ohlcv(pair=pair, timeframe=timeframe, since_ms=since_ms)) + def get_historic_ohlcv_as_df(self, pair: str, timeframe: str, + since_ms: int) -> DataFrame: + """ + Minimal wrapper around get_historic_ohlcv - converting the result into a dataframe + :param pair: Pair to download + :param timeframe: Timeframe to get data for + :param since_ms: Timestamp in milliseconds to get history from + :return: OHLCV DataFrame + """ + ticks = self.get_historic_ohlcv(pair, timeframe, since_ms=since_ms) + return ohlcv_to_dataframe(ticks, timeframe, pair=pair, fill_missing=True, + drop_incomplete=self._ohlcv_partial_candle) + async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, since_ms: int) -> List: From 109824c9a80cb78c7c4ec9d6f90cb1c8c3afa640 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 15:29:11 +0100 Subject: [PATCH 0919/1197] Add VolatilityFilter --- freqtrade/constants.py | 2 +- freqtrade/pairlist/AgeFilter.py | 2 +- freqtrade/pairlist/volatilityfilter.py | 89 ++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 freqtrade/pairlist/volatilityfilter.py diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 3271dda39..55d802587 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -25,7 +25,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PrecisionFilter', 'PriceFilter', - 'ShuffleFilter', 'SpreadFilter'] + 'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S' diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index 19cf1c090..352fff082 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -49,7 +49,7 @@ class AgeFilter(IPairList): return (f"{self.name} - Filtering pairs with age less than " f"{self._min_days_listed} {plural(self._min_days_listed, 'day')}.") - def _validate_pair(self, ticker: dict) -> bool: + def _validate_pair(self, ticker: Dict) -> bool: """ Validate age for the ticker :param ticker: ticker dict as returned from ccxt.load_markets() diff --git a/freqtrade/pairlist/volatilityfilter.py b/freqtrade/pairlist/volatilityfilter.py new file mode 100644 index 000000000..6039f6f69 --- /dev/null +++ b/freqtrade/pairlist/volatilityfilter.py @@ -0,0 +1,89 @@ +""" +Minimum age (days listed) pair list filter +""" +import logging +from typing import Any, Dict + +import arrow +from cachetools.ttl import TTLCache + +from freqtrade.exceptions import OperationalException +from freqtrade.misc import plural +from freqtrade.pairlist.IPairList import IPairList + + +logger = logging.getLogger(__name__) + + +class VolatilityFilter(IPairList): + + def __init__(self, exchange, pairlistmanager, + config: Dict[str, Any], pairlistconfig: Dict[str, Any], + pairlist_pos: int) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + + self._days = pairlistconfig.get('volatility_over_days', 10) + self._min_volatility = pairlistconfig.get('min_volatility', 0.01) + self._refresh_period = pairlistconfig.get('refresh_period', 1440) + + self._pair_cache: TTLCache = TTLCache(maxsize=100, ttl=self._refresh_period) + + if self._days < 1: + raise OperationalException("VolatilityFilter requires min_days_listed to be >= 1") + if self._days > exchange.ohlcv_candle_limit: + raise OperationalException("VolatilityFilter requires min_days_listed to not exceed " + "exchange max request size " + f"({exchange.ohlcv_candle_limit})") + + @property + def needstickers(self) -> bool: + """ + Boolean property defining if tickers are necessary. + If no Pairlist requires tickers, an empty List is passed + as tickers argument to filter_pairlist + """ + return True + + def short_desc(self) -> str: + """ + Short whitelist method description - used for startup-messages + """ + return (f"{self.name} - Filtering pairs with volatility below {self._min_volatility} " + f"over the last {plural(self._days, 'day')}.") + + def _validate_pair(self, ticker: Dict) -> bool: + """ + Validate volatility + :param ticker: ticker dict as returned from ccxt.load_markets() + :return: True if the pair can stay, False if it should be removed + """ + pair = ticker['symbol'] + # Check symbol in cache + if pair in self._pair_cache: + return self._pair_cache[pair] + + since_ms = int(arrow.utcnow() + .floor('day') + .shift(days=-self._days) + .float_timestamp) * 1000 + + daily_candles = self._exchange.get_historic_ohlcv_as_df(pair=pair, + timeframe='1d', + since_ms=since_ms) + result = False + if daily_candles is not None: + highest_high = daily_candles['high'].max() + lowest_low = daily_candles['low'].min() + pct_change = (highest_high - lowest_low) / lowest_low + if pct_change >= self._min_volatility: + result = True + else: + self.log_on_refresh(logger.info, + f"Removed {pair} from whitelist, " + f"because volatility over {plural(self._days, 'day')} is " + f"{pct_change:.3f}, which is below the " + f"threshold of {self._min_volatility}.") + result = False + self._pair_cache[pair] = result + + return result From 191616e4e5cbb558f48ec39e67bf5399fbf87da5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 15:39:04 +0100 Subject: [PATCH 0920/1197] Add first tests for volatilityFilter --- tests/pairlist/test_pairlist.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 1f05bef1e..3e1cca30c 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -340,6 +340,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"}, {"method": "PriceFilter", "low_price_ratio": 0.02}], "USDT", ['ETH/USDT', 'NANO/USDT']), + ([{"method": "StaticPairList"}, + {"method": "VolatilityFilter", "volatility_over_days": 10, + "min_volatility": 0.01, "refresh_period": 1440}], + "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), ]) def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, ohlcv_history_list, pairlists, base_currency, @@ -617,6 +621,11 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_his None, "PriceFilter requires max_price to be >= 0" ), # OperationalException expected + ({"method": "VolatilityFilter", "volatility_over_days": 10, "min_volatility": 0.01}, + "[{'VolatilityFilter': 'VolatilityFilter - Filtering pairs with volatility below 0.01 " + "over the last days.'}]", + None + ), ]) def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, desc_expected, exception_expected): From 6b672cd0b95f8a35fe83dab95e6b931e6b85c51d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 15:43:29 +0100 Subject: [PATCH 0921/1197] Document volatilityFilter --- docs/includes/pairlists.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index e6a9fc1a8..301a5453d 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -19,6 +19,7 @@ Inactive markets are always removed from the resulting pairlist. Explicitly blac * [`PriceFilter`](#pricefilter) * [`ShuffleFilter`](#shufflefilter) * [`SpreadFilter`](#spreadfilter) +* [`VolatilityFilter`](#volatilityfilter) !!! Tip "Testing pairlists" Pairlist configurations can be quite tricky to get right. Best use the [`test-pairlist`](utils.md#test-pairlist) utility sub-command to test your configuration quickly. @@ -118,6 +119,27 @@ Example: If `DOGE/BTC` maximum bid is 0.00000026 and minimum ask is 0.00000027, the ratio is calculated as: `1 - bid/ask ~= 0.037` which is `> 0.005` and this pair will be filtered out. +#### VolatilityFilter + +Removes pairs where the difference between lowest low and highest high over `volatility_over_days` days is below `min_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. + +In the below example: +If volatility over the last 10 days is <1%, remove the pair from the whitelist. + +```json +"pairlists": [ + { + "method": "VolatilityFilter", + "volatility_over_days": 10, + "min_volatility": 0.01, + "refresh_period": 1440 + } +] +``` + +!!! Tip + This Filter can be used to automatically remove stable coin pairs, which have a very low volatility, and are therefore extremely hard to trade with profit. + ### Full example of Pairlist Handlers The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting pairs by `quoteVolume` and applies both [`PrecisionFilter`](#precisionfilter) and [`PriceFilter`](#price-filter), filtering all assets where 1 price unit is > 1%. Then the `SpreadFilter` is applied and pairs are finally shuffled with the random seed set to some predefined value. @@ -137,6 +159,12 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, {"method": "PrecisionFilter"}, {"method": "PriceFilter", "low_price_ratio": 0.01}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}, + { + "method": "VolatilityFilter", + "volatility_over_days": 10, + "min_volatility": 0.01, + "refresh_period": 1440 + }, {"method": "ShuffleFilter", "seed": 42} ], ``` From f8fab5c4f8d120b7838cac24c6a0c7d30df85fc2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 15:51:39 +0100 Subject: [PATCH 0922/1197] Add tests for failure cases --- freqtrade/pairlist/volatilityfilter.py | 4 ++-- tests/pairlist/test_pairlist.py | 33 ++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/freqtrade/pairlist/volatilityfilter.py b/freqtrade/pairlist/volatilityfilter.py index 6039f6f69..e9ca61794 100644 --- a/freqtrade/pairlist/volatilityfilter.py +++ b/freqtrade/pairlist/volatilityfilter.py @@ -29,9 +29,9 @@ class VolatilityFilter(IPairList): self._pair_cache: TTLCache = TTLCache(maxsize=100, ttl=self._refresh_period) if self._days < 1: - raise OperationalException("VolatilityFilter requires min_days_listed to be >= 1") + raise OperationalException("VolatilityFilter requires volatility_over_days to be >= 1") if self._days > exchange.ohlcv_candle_limit: - raise OperationalException("VolatilityFilter requires min_days_listed to not exceed " + raise OperationalException("VolatilityFilter requires volatility_over_days to not exceed " "exchange max request size " f"({exchange.ohlcv_candle_limit})") diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 3e1cca30c..5bbb233b4 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -58,7 +58,7 @@ def whitelist_conf_2(default_conf): @pytest.fixture(scope="function") -def whitelist_conf_3(default_conf): +def whitelist_conf_agefilter(default_conf): default_conf['stake_currency'] = 'BTC' default_conf['exchange']['pair_whitelist'] = [ 'ETH/BTC', 'TKN/BTC', 'BLK/BTC', 'LTC/BTC', @@ -532,7 +532,7 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers): assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == lrf -def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tickers, caplog): +def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tickers): default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, {'method': 'AgeFilter', 'min_days_listed': -1}] @@ -547,7 +547,7 @@ def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tick get_patched_freqtradebot(mocker, default_conf) -def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tickers, caplog): +def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tickers): default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, {'method': 'AgeFilter', 'min_days_listed': 99999}] @@ -563,7 +563,7 @@ def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tick get_patched_freqtradebot(mocker, default_conf) -def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_history_list): +def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, ohlcv_history_list): mocker.patch.multiple('freqtrade.exchange.Exchange', markets=PropertyMock(return_value=markets), @@ -575,7 +575,7 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_his get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), ) - freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_3) + freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_agefilter) assert freqtrade.exchange.get_historic_ohlcv.call_count == 0 freqtrade.pairlists.refresh_pairlist() assert freqtrade.exchange.get_historic_ohlcv.call_count > 0 @@ -586,6 +586,29 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_3, tickers, ohlcv_his assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count +def test_volatilityfilter_checks(mocker, default_conf, markets, tickers): + default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, + {'method': 'VolatilityFilter', 'volatility_over_days': 99999}] + + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers + ) + + with pytest.raises(OperationalException, + match=r'VolatilityFilter requires volatility_over_days to not exceed ' + r'exchange max request size \([0-9]+\)'): + get_patched_freqtradebot(mocker, default_conf) + + default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, + {'method': 'VolatilityFilter', 'volatility_over_days': 0}] + + with pytest.raises(OperationalException, + match='VolatilityFilter requires volatility_over_days to be >= 1'): + get_patched_freqtradebot(mocker, default_conf) + + @pytest.mark.parametrize("pairlistconfig,desc_expected,exception_expected", [ ({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010, "max_price": 1.0}, From 2e1551a2ebce9cd9d288ba03a019778ff758b7ad Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 16:01:52 +0100 Subject: [PATCH 0923/1197] Improve tests of volatilityfilter --- docs/includes/pairlists.md | 2 +- freqtrade/pairlist/volatilityfilter.py | 4 ++-- tests/pairlist/test_pairlist.py | 33 ++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 301a5453d..7cd2369b1 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -138,7 +138,7 @@ If volatility over the last 10 days is <1%, remove the pair from the whitelist. ``` !!! Tip - This Filter can be used to automatically remove stable coin pairs, which have a very low volatility, and are therefore extremely hard to trade with profit. + This Filter can be used to automatically remove stable coin pairs, which have a very low volatility, and are therefore extremely difficult to trade with profit. ### Full example of Pairlist Handlers diff --git a/freqtrade/pairlist/volatilityfilter.py b/freqtrade/pairlist/volatilityfilter.py index e9ca61794..415b6e89e 100644 --- a/freqtrade/pairlist/volatilityfilter.py +++ b/freqtrade/pairlist/volatilityfilter.py @@ -31,8 +31,8 @@ class VolatilityFilter(IPairList): if self._days < 1: raise OperationalException("VolatilityFilter requires volatility_over_days to be >= 1") if self._days > exchange.ohlcv_candle_limit: - raise OperationalException("VolatilityFilter requires volatility_over_days to not exceed " - "exchange max request size " + raise OperationalException("VolatilityFilter requires volatility_over_days to not " + "exceed exchange max request size " f"({exchange.ohlcv_candle_limit})") @property diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 5bbb233b4..e9df5d3f4 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -609,6 +609,39 @@ def test_volatilityfilter_checks(mocker, default_conf, markets, tickers): get_patched_freqtradebot(mocker, default_conf) +@pytest.mark.parametrize('min_volatility,expected_length', [ + (0.01, 5), + (0.05, 0), # Setting volatility to 5% removes all pairs from the whitelist. +]) +def test_volatilityfilter_caching(mocker, markets, default_conf, tickers, ohlcv_history_list, + min_volatility, expected_length): + default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, + {'method': 'VolatilityFilter', 'volatility_over_days': 2, + 'min_volatility': min_volatility}] + + mocker.patch.multiple('freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers + ) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), + ) + + freqtrade = get_patched_freqtradebot(mocker, default_conf) + assert freqtrade.exchange.get_historic_ohlcv.call_count == 0 + freqtrade.pairlists.refresh_pairlist() + assert len(freqtrade.pairlists.whitelist) == expected_length + assert freqtrade.exchange.get_historic_ohlcv.call_count > 0 + + previous_call_count = freqtrade.exchange.get_historic_ohlcv.call_count + freqtrade.pairlists.refresh_pairlist() + assert len(freqtrade.pairlists.whitelist) == expected_length + # Should not have increased since first call. + assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count + + @pytest.mark.parametrize("pairlistconfig,desc_expected,exception_expected", [ ({"method": "PriceFilter", "low_price_ratio": 0.001, "min_price": 0.00000010, "max_price": 1.0}, From f12a8afd4151d6a2f069f5375291dc57e6b862b8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Nov 2020 10:56:19 +0100 Subject: [PATCH 0924/1197] Add test for ohlcv_as_df --- tests/exchange/test_exchange.py | 51 +++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index e4452a83c..42681b367 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1307,6 +1307,57 @@ def test_get_historic_ohlcv(default_conf, mocker, caplog, exchange_name): assert log_has_re(r"Async code raised an exception: .*", caplog) +@pytest.mark.parametrize("exchange_name", EXCHANGES) +def test_get_historic_ohlcv_as_df(default_conf, mocker, exchange_name): + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + ohlcv = [ + [ + arrow.utcnow().int_timestamp * 1000, # unix timestamp ms + 1, # open + 2, # high + 3, # low + 4, # close + 5, # volume (in quote currency) + ], + [ + arrow.utcnow().shift(minutes=5).int_timestamp * 1000, # unix timestamp ms + 1, # open + 2, # high + 3, # low + 4, # close + 5, # volume (in quote currency) + ], + [ + arrow.utcnow().shift(minutes=10).int_timestamp * 1000, # unix timestamp ms + 1, # open + 2, # high + 3, # low + 4, # close + 5, # volume (in quote currency) + ] + ] + pair = 'ETH/BTC' + + async def mock_candle_hist(pair, timeframe, since_ms): + return pair, timeframe, ohlcv + + exchange._async_get_candle_history = Mock(wraps=mock_candle_hist) + # one_call calculation * 1.8 should do 2 calls + + since = 5 * 60 * exchange._ft_has['ohlcv_candle_limit'] * 1.8 + ret = exchange.get_historic_ohlcv_as_df(pair, "5m", int(( + arrow.utcnow().int_timestamp - since) * 1000)) + + assert exchange._async_get_candle_history.call_count == 2 + # Returns twice the above OHLCV data + assert len(ret) == 2 + assert isinstance(ret, DataFrame) + assert 'date' in ret.columns + assert 'open' in ret.columns + assert 'close' in ret.columns + assert 'high' in ret.columns + + def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: ohlcv = [ [ From 7e4fe23bf94128fa1df7477011d65d6d3ff2afd8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Nov 2020 11:08:01 +0100 Subject: [PATCH 0925/1197] Add VolatilityFilter to full config --- config_full.json.example | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/config_full.json.example b/config_full.json.example index 45c5c695c..0d82b9a2b 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -67,7 +67,13 @@ {"method": "AgeFilter", "min_days_listed": 10}, {"method": "PrecisionFilter"}, {"method": "PriceFilter", "low_price_ratio": 0.01, "min_price": 0.00000010}, - {"method": "SpreadFilter", "max_spread_ratio": 0.005} + {"method": "SpreadFilter", "max_spread_ratio": 0.005}, + { + "method": "VolatilityFilter", + "volatility_over_days": 10, + "min_volatility": 0.01, + "refresh_period": 1440 + } ], "exchange": { "name": "bittrex", From 29c6a9263de13b3a480662d1c59b203512df8bd3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Nov 2020 15:50:44 +0100 Subject: [PATCH 0926/1197] Protect against 0 values --- freqtrade/pairlist/volatilityfilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/pairlist/volatilityfilter.py b/freqtrade/pairlist/volatilityfilter.py index 415b6e89e..14ac0c617 100644 --- a/freqtrade/pairlist/volatilityfilter.py +++ b/freqtrade/pairlist/volatilityfilter.py @@ -74,7 +74,7 @@ class VolatilityFilter(IPairList): if daily_candles is not None: highest_high = daily_candles['high'].max() lowest_low = daily_candles['low'].min() - pct_change = (highest_high - lowest_low) / lowest_low + pct_change = ((highest_high - lowest_low) / lowest_low) if lowest_low > 0 else 0 if pct_change >= self._min_volatility: result = True else: From ec330112552afc3902bef12b6ea3884a39a16193 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Nov 2020 05:50:43 +0000 Subject: [PATCH 0927/1197] Bump ccxt from 1.37.69 to 1.38.13 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.37.69 to 1.38.13. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.37.69...1.38.13) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 67b69a5dd..7f2ac98b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.4 pandas==1.1.4 -ccxt==1.37.69 +ccxt==1.38.13 aiohttp==3.7.2 SQLAlchemy==1.3.20 python-telegram-bot==13.0 From 83b4cd7b39f1ea2eba4d820043f47b7a42c96fd7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Nov 2020 05:50:51 +0000 Subject: [PATCH 0928/1197] Bump mkdocs-material from 6.1.5 to 6.1.6 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 6.1.5 to 6.1.6. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/6.1.5...6.1.6) Signed-off-by: dependabot[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 cd294bef6..87bc6dfdd 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==6.1.5 +mkdocs-material==6.1.6 mdx_truly_sane_lists==1.2 pymdown-extensions==8.0.1 From be4807d85c24c719f87a3ee43d13012d5988de41 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Nov 2020 05:50:52 +0000 Subject: [PATCH 0929/1197] Bump questionary from 1.8.0 to 1.8.1 Bumps [questionary](https://github.com/tmbo/questionary) from 1.8.0 to 1.8.1. - [Release notes](https://github.com/tmbo/questionary/releases) - [Commits](https://github.com/tmbo/questionary/compare/1.8.0...1.8.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 67b69a5dd..66fa04824 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,5 +35,5 @@ flask-cors==3.0.9 # Support for colorized terminal output colorama==0.4.4 # Building config files interactively -questionary==1.8.0 +questionary==1.8.1 prompt-toolkit==3.0.8 From 7c7a8190abf5548ba54cc54141929668492ac56b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Nov 2020 05:50:54 +0000 Subject: [PATCH 0930/1197] Bump python-rapidjson from 0.9.3 to 0.9.4 Bumps [python-rapidjson](https://github.com/python-rapidjson/python-rapidjson) from 0.9.3 to 0.9.4. - [Release notes](https://github.com/python-rapidjson/python-rapidjson/releases) - [Changelog](https://github.com/python-rapidjson/python-rapidjson/blob/master/CHANGES.rst) - [Commits](https://github.com/python-rapidjson/python-rapidjson/compare/v0.9.3...v0.9.4) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 67b69a5dd..487b19159 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ blosc==1.9.2 py_find_1st==1.1.4 # Load ticker files 30% faster -python-rapidjson==0.9.3 +python-rapidjson==0.9.4 # Notify systemd sdnotify==0.3.2 From 56629d882e1d5fc39c00de14f5995af9ce16ce6e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Nov 2020 05:51:03 +0000 Subject: [PATCH 0931/1197] Bump coveralls from 2.1.2 to 2.2.0 Bumps [coveralls](https://github.com/coveralls-clients/coveralls-python) from 2.1.2 to 2.2.0. - [Release notes](https://github.com/coveralls-clients/coveralls-python/releases) - [Changelog](https://github.com/coveralls-clients/coveralls-python/blob/master/CHANGELOG.md) - [Commits](https://github.com/coveralls-clients/coveralls-python/compare/2.1.2...2.2.0) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1c96a880a..e681274c8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,7 +3,7 @@ -r requirements-plot.txt -r requirements-hyperopt.txt -coveralls==2.1.2 +coveralls==2.2.0 flake8==3.8.4 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.1.0 From 1ec99e6b76010c0f0471d009b64e7f3bef0c433d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Nov 2020 08:05:45 +0000 Subject: [PATCH 0932/1197] Bump aiohttp from 3.7.2 to 3.7.3 Bumps [aiohttp](https://github.com/aio-libs/aiohttp) from 3.7.2 to 3.7.3. - [Release notes](https://github.com/aio-libs/aiohttp/releases) - [Changelog](https://github.com/aio-libs/aiohttp/blob/master/CHANGES.rst) - [Commits](https://github.com/aio-libs/aiohttp/compare/v3.7.2...v3.7.3) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index fa3024e30..7490688d4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.19.4 pandas==1.1.4 ccxt==1.38.13 -aiohttp==3.7.2 +aiohttp==3.7.3 SQLAlchemy==1.3.20 python-telegram-bot==13.0 arrow==0.17.0 From 312533fded2c794798a860b3ad523aec6f10ecc1 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Mon, 23 Nov 2020 22:08:53 -0600 Subject: [PATCH 0933/1197] Match current dev file --- docs/installation.md | 249 +++++++++++++++++++------------------------ 1 file changed, 111 insertions(+), 138 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 35cdcda62..ec2d27174 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -2,6 +2,8 @@ This page explains how to prepare your environment for running the bot. +Please consider using the prebuilt [docker images](docker.md) to get started quickly while trying out freqtrade evaluating how it operates. + ## Prerequisite ### Requirements @@ -11,70 +13,78 @@ Click each one for install guide: * [Python >= 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/) * [pip](https://pip.pypa.io/en/stable/installing/) * [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) -* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended) +* [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended) * [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) (install instructions below) -### API keys + We also recommend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot), which is optional but recommended. -Before running your bot in production you will need to setup few -external API. In production mode, the bot will require valid Exchange API -credentials. We also recommend a [Telegram bot](telegram-usage.md#setup-your-telegram-bot) (optional but recommended). - -### Setup your exchange account - -You will need to create API Keys (Usually you get `key` and `secret`) from the Exchange website and insert this into the appropriate fields in the configuration or when asked by the installation script. +!!! Warning "Up-to-date clock" + The clock on the system running the bot must be accurate, synchronized to a NTP server frequently enough to avoid problems with communication to the exchanges. ## Quick start -Freqtrade provides a Linux/MacOS script to install all dependencies and help you to configure the bot. - -!!! Note - Python3.6 or higher and the corresponding pip are assumed to be available. The install-script will warn and stop if that's not the case. - -```bash -git clone git@github.com:freqtrade/freqtrade.git -cd freqtrade -git checkout develop -./setup.sh --install -``` +Freqtrade provides the Linux/MacOS Easy Installation script to install all dependencies and help you configure the bot. !!! Note Windows installation is explained [here](#windows). -## Easy Installation - Linux Script +The easiest way to install and run Freqtrade is to clone the bot Github repository and then run the Easy Installation script, if it's available for your platform. -If you are on Debian, Ubuntu or MacOS freqtrade provides a script to Install, Update, Configure, and Reset your bot. +!!! Note "Version considerations" + When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests). The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable). + +!!! Note + Python3.6 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository. + +This can be achieved with the following commands: + +```bash +git clone https://github.com/freqtrade/freqtrade.git +cd freqtrade +# git checkout stable # Optional, see (1) +./setup.sh --install +``` + +(1) This command switches the cloned repository to the use of the `stable` branch. It's not needed if you wish to stay on the `develop` branch. You may later switch between branches at any time with the `git checkout stable`/`git checkout develop` commands. + +## Easy Installation Script (Linux/MacOS) + +If you are on Debian, Ubuntu or MacOS Freqtrade provides the script to install, update, configure and reset the codebase of your bot. ```bash $ ./setup.sh usage: -i,--install Install freqtrade from scratch -u,--update Command git pull to update. - -r,--reset Hard reset your develop/master branch. + -r,--reset Hard reset your develop/stable branch. -c,--config Easy config generator (Will override your existing file). ``` ** --install ** -This script will install everything you need to run the bot: +With this option, the script will install the bot and most dependencies: +You will need to have git and python3.6+ installed beforehand for this to work. * Mandatory software as: `ta-lib` -* Setup your virtualenv -* Configure your `config.json` file +* Setup your virtualenv under `.env/` -This script is a combination of `install script` `--reset`, `--config` +This option is a combination of installation tasks, `--reset` and `--config`. ** --update ** -Update parameter will pull the last version of your current branch and update your virtualenv. +This option will pull the last version of your current branch and update your virtualenv. Run the script with this option periodically to update your bot. ** --reset ** -Reset parameter will hard reset your branch (only if you are on `master` or `develop`) and recreate your virtualenv. +This option will hard reset your branch (only if you are on either `stable` or `develop`) and recreate your virtualenv. ** --config ** -Config parameter is a `config.json` configurator. This script will ask you questions to setup your bot and create your `config.json`. +DEPRECATED - use `freqtrade new-config -c config.json` instead. + +### Activate your virtual environment + +Each time you open a new terminal, you must run `source .env/bin/activate`. ------ @@ -86,40 +96,50 @@ OS Specific steps are listed first, the [Common](#common) section below is neces !!! Note Python3.6 or higher and the corresponding pip are assumed to be available. -### Linux - Ubuntu 16.04 +=== "Ubuntu 16.04" + #### Install necessary dependencies -#### Install necessary dependencies + ```bash + sudo apt-get update + sudo apt-get install build-essential git + ``` -```bash -sudo apt-get update -sudo apt-get install build-essential git -``` +=== "RaspberryPi/Raspbian" + The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/) from at least September 2019. + This image comes with python3.7 preinstalled, making it easy to get freqtrade up and running. -#### Raspberry Pi / Raspbian + Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied. -Before installing FreqTrade on a Raspberry Pi running the official Raspbian Image, make sure you have at least Python 3.6 installed. The default image only provides Python 3.5. Probably the easiest way to get a recent version of python is [miniconda](https://repo.continuum.io/miniconda/). + ``` bash + sudo apt-get install python3-venv libatlas-base-dev + git clone https://github.com/freqtrade/freqtrade.git + cd freqtrade -The following assumes that miniconda3 is installed and available in your environment. Last miniconda3 installation file use python 3.4, we will update to python 3.6 on this installation. -It's recommended to use (mini)conda for this as installation/compilation of `numpy`, `scipy` and `pandas` takes a long time. + bash setup.sh -i + ``` -Additional package to install on your Raspbian, `libffi-dev` required by cryptography (from python-telegram-bot). + !!! Note "Installation duration" + Depending on your internet speed and the Raspberry Pi version, installation can take multiple hours to complete. -``` bash -conda config --add channels rpi -conda install python=3.6 -conda create -n freqtrade python=3.6 -conda activate freqtrade -conda install scipy pandas numpy - -sudo apt install libffi-dev -python3 -m pip install -r requirements-common.txt -python3 -m pip install -e . -``` + !!! Note + The above does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`. + We do not advise to run hyperopt on a Raspberry Pi, since this is a very resource-heavy operation, which should be done on powerful machine. ### Common #### 1. Install TA-Lib +Use the provided ta-lib installation script + +```bash +sudo ./build_helpers/install_ta-lib.sh +``` + +!!! Note + This will use the ta-lib tar.gz included in this repository. + +##### TA-Lib manual installation + Official webpage: https://mrjbq7.github.io/ta-lib/install.html ```bash @@ -147,126 +167,79 @@ python3 -m venv .env source .env/bin/activate ``` -#### 3. Install FreqTrade +#### 3. Install Freqtrade Clone the git repository: ```bash git clone https://github.com/freqtrade/freqtrade.git - -``` - -Optionally checkout the master branch to get the latest stable release: - -```bash -git checkout master -``` - -#### 4. Initialize the configuration - -```bash cd freqtrade -cp config.json.example config.json +git checkout stable ``` -> *To edit the config please refer to [Bot Configuration](configuration.md).* - -#### 5. Install python dependencies +#### 4. Install python dependencies ``` bash python3 -m pip install --upgrade pip -pip install numpy -python3 -m pip install -r requirements.txt python3 -m pip install -e . ``` +#### 5. Initialize the configuration + +```bash +# Initialize the user_directory +freqtrade create-userdir --userdir user_data/ + +# Create a new configuration file +freqtrade new-config --config config.json +``` + +> *To edit the config please refer to [Bot Configuration](configuration.md).* + #### 6. Run the Bot If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins. ```bash -freqtrade -c config.json +freqtrade trade -c config.json ``` *Note*: If you run the bot on a server, you should consider using [Docker](docker.md) or a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout. -#### 7. [Optional] Configure `freqtrade` as a `systemd` service +#### 7. (Optional) Post-installation Tasks -From the freqtrade repo... copy `freqtrade.service` to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup. - -After that you can start the daemon with: - -```bash -systemctl --user start freqtrade -``` - -For this to be persistent (run when user is logged out) you'll need to enable `linger` for your freqtrade user. - -```bash -sudo loginctl enable-linger "$USER" -``` - -If you run the bot as a service, you can use systemd service manager as a software watchdog monitoring freqtrade bot -state and restarting it in the case of failures. If the `internals.sd_notify` parameter is set to true in the -configuration or the `--sd-notify` command line option is used, the bot will send keep-alive ping messages to systemd -using the sd_notify (systemd notifications) protocol and will also tell systemd its current state (Running or Stopped) -when it changes. - -The `freqtrade.service.watchdog` file contains an example of the service unit configuration file which uses systemd -as the watchdog. - -!!! Note - The sd_notify communication between the bot and the systemd service manager will not work if the bot runs in a Docker container. +On Linux, as an optional post-installation task, you may wish to setup the bot to run as a `systemd` service or configure it to send the log messages to the `syslog`/`rsyslog` or `journald` daemons. See [Advanced Logging](advanced-setup.md#advanced-logging) for details. ------ -## Windows +### Anaconda -We recommend that Windows users use [Docker](docker.md) as this will work much easier and smoother (also more secure). +Freqtrade can also be installed using Anaconda (or Miniconda). -If that is not possible, try using the Windows Linux subsystem (WSL) - for which the Ubuntu instructions should work. -If that is not available on your system, feel free to try the instructions below, which led to success for some. - -### Install freqtrade manually - -#### Clone the git repository - -```bash -git clone https://github.com/freqtrade/freqtrade.git -``` - -#### Install ta-lib - -Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows). - -As compiling from source on windows has heavy dependencies (requires a partial visual studio installation), there is also a repository of unofficial precompiled windows Wheels [here](https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib), which needs to be downloaded and installed using `pip install TA_Lib‑0.4.17‑cp36‑cp36m‑win32.whl` (make sure to use the version matching your python version) - -```cmd ->cd \path\freqtrade-develop ->python -m venv .env ->cd .env\Scripts ->activate.bat ->cd \path\freqtrade-develop -REM optionally install ta-lib from wheel -REM >pip install TA_Lib‑0.4.17‑cp36‑cp36m‑win32.whl ->pip install -r requirements.txt ->pip install -e . ->python freqtrade\main.py -``` - -> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222) - -#### Error during installation under Windows +!!! Note + This requires the [ta-lib](#1-install-ta-lib) C-library to be installed first. See below. ``` bash -error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools +conda env create -f environment.yml ``` -Unfortunately, many packages requiring compilation don't provide a pre-build wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use. +----- +## Troubleshooting -The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker](docker.md) first. +### MacOS installation error ---- +Newer versions of MacOS may have installation failed with errors like `error: command 'g++' failed with exit status 1`. + +This error will require explicit installation of the SDK Headers, which are not installed by default in this version of MacOS. +For MacOS 10.14, this can be accomplished with the below command. + +``` bash +open /Library/Developer/CommandLineTools/Packages/macOS_SDK_headers_for_macOS_10.14.pkg +``` + +If this file is inexistent, then you're probably on a different version of MacOS, so you may need to consult the internet for specific resolution details. + +----- Now you have an environment ready, the next step is -[Bot Configuration](configuration.md). +[Bot Configuration](configuration.md). \ No newline at end of file From 730c9ce4719ee257b62a149d2c807c5da43e07d8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Nov 2020 06:57:11 +0100 Subject: [PATCH 0934/1197] Add Max_open_trades to summary metrics --- docs/backtesting.md | 9 +++++++-- freqtrade/optimize/optimize_reports.py | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 84911568b..277b11083 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -162,6 +162,8 @@ A backtesting result will look like that: |-----------------------+---------------------| | Backtesting from | 2019-01-01 00:00:00 | | Backtesting to | 2019-05-01 00:00:00 | +| Max open trades | 3 | +| | | | Total trades | 429 | | First trade | 2019-01-01 18:30:00 | | First trade Pair | EOS/USDT | @@ -233,6 +235,8 @@ It contains some useful key metrics about performance of your strategy on backte |-----------------------+---------------------| | Backtesting from | 2019-01-01 00:00:00 | | Backtesting to | 2019-05-01 00:00:00 | +| Max open trades | 3 | +| | | | Total trades | 429 | | First trade | 2019-01-01 18:30:00 | | First trade Pair | EOS/USDT | @@ -251,16 +255,17 @@ It contains some useful key metrics about performance of your strategy on backte ``` +- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option). +- `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - to clearly see settings for this. - `Total trades`: Identical to the total trades of the backtest output table. - `First trade`: First trade entered. - `First trade pair`: Which pair was part of the first trade. -- `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option). - `Total Profit %`: Total profit per stake amount. Aligned to the TOTAL column of the first table. - `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). - `Best day` / `Worst day`: Best and worst day based on daily profit. - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. - `Max Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced). -- `Drawdown Start` / `Drawdown End`: Start and end datetimes for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command). +- `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command). - `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column. ### Assumptions made by backtesting diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index c977a991b..fc04cbd93 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -396,6 +396,8 @@ def text_table_add_metrics(strat_results: Dict) -> str: metrics = [ ('Backtesting from', strat_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)), ('Backtesting to', strat_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)), + ('Max open trades', strat_results['max_open_trades']), + ('', ''), # Empty line to improve readability ('Total trades', strat_results['total_trades']), ('First trade', min_trade['open_date'].strftime(DATETIME_PRINT_FORMAT)), ('First trade Pair', min_trade['pair']), From 006436a18d2d2c821ca4a51ff1e604074ddacf9e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Nov 2020 07:47:35 +0100 Subject: [PATCH 0935/1197] Require use_sell_signal to be true for edge Otherwise edge will have strange results, as edge runs with sell signal, while the bot runs without sell signal, causing results to be invalid closes #3900 --- freqtrade/configuration/config_validation.py | 4 ++++ tests/test_configuration.py | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index d4612d8e0..ab21bc686 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -137,6 +137,10 @@ def _validate_edge(conf: Dict[str, Any]) -> None: "Edge and VolumePairList are incompatible, " "Edge will override whatever pairs VolumePairlist selects." ) + if not conf.get('ask_strategy', {}).get('use_sell_signal', True): + raise OperationalException( + "Edge requires `use_sell_signal` to be True, otherwise no sells will happen." + ) def _validate_whitelist(conf: Dict[str, Any]) -> None: diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 7d6c81f74..9594b6413 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -812,6 +812,21 @@ def test_validate_edge(edge_conf): validate_config_consistency(edge_conf) +def test_validate_edge2(edge_conf): + edge_conf.update({"ask_strategy": { + "use_sell_signal": True, + }}) + # Passes test + validate_config_consistency(edge_conf) + + edge_conf.update({"ask_strategy": { + "use_sell_signal": False, + }}) + with pytest.raises(OperationalException, match="Edge requires `use_sell_signal` to be True, " + "otherwise no sells will happen."): + validate_config_consistency(edge_conf) + + def test_validate_whitelist(default_conf): default_conf['runmode'] = RunMode.DRY_RUN # Test regular case - has whitelist and uses StaticPairlist From bd98ff6332f9bd3d4ea73d1ee18446b48f0e187e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Nov 2020 20:24:51 +0100 Subject: [PATCH 0936/1197] Update docstring in all pairlists --- freqtrade/pairlist/AgeFilter.py | 2 +- freqtrade/pairlist/IPairList.py | 2 +- freqtrade/pairlist/PrecisionFilter.py | 2 +- freqtrade/pairlist/PriceFilter.py | 2 +- freqtrade/pairlist/ShuffleFilter.py | 2 +- freqtrade/pairlist/SpreadFilter.py | 2 +- freqtrade/pairlist/StaticPairList.py | 2 +- freqtrade/pairlist/VolumePairList.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index 19cf1c090..20635a9ed 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -37,7 +37,7 @@ class AgeFilter(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return True diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index 6b5bd11e7..c869e499b 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -68,7 +68,7 @@ class IPairList(ABC): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py index cf853397b..29e32fd44 100644 --- a/freqtrade/pairlist/PrecisionFilter.py +++ b/freqtrade/pairlist/PrecisionFilter.py @@ -32,7 +32,7 @@ class PrecisionFilter(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return True diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/pairlist/PriceFilter.py index 8cd57ee1d..bef1c0a15 100644 --- a/freqtrade/pairlist/PriceFilter.py +++ b/freqtrade/pairlist/PriceFilter.py @@ -35,7 +35,7 @@ class PriceFilter(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return True diff --git a/freqtrade/pairlist/ShuffleFilter.py b/freqtrade/pairlist/ShuffleFilter.py index eb4f6dcc3..28778db7b 100644 --- a/freqtrade/pairlist/ShuffleFilter.py +++ b/freqtrade/pairlist/ShuffleFilter.py @@ -25,7 +25,7 @@ class ShuffleFilter(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return False diff --git a/freqtrade/pairlist/SpreadFilter.py b/freqtrade/pairlist/SpreadFilter.py index 2527a3131..a636b90bd 100644 --- a/freqtrade/pairlist/SpreadFilter.py +++ b/freqtrade/pairlist/SpreadFilter.py @@ -24,7 +24,7 @@ class SpreadFilter(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return True diff --git a/freqtrade/pairlist/StaticPairList.py b/freqtrade/pairlist/StaticPairList.py index 3b6440763..2879cb364 100644 --- a/freqtrade/pairlist/StaticPairList.py +++ b/freqtrade/pairlist/StaticPairList.py @@ -30,7 +30,7 @@ class StaticPairList(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return False diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 44e5c52d7..7d3c2c653 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -49,7 +49,7 @@ class VolumePairList(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return True From ceb50a78071c7295b486de887161875126bb3f72 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 07:57:23 +0100 Subject: [PATCH 0937/1197] use exception handler when downloading data closes #3992 --- freqtrade/data/history/history_utils.py | 12 +++++------- tests/data/test_history.py | 5 +---- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 17b510b92..3b8b5a2f0 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -214,10 +214,9 @@ def _download_pair_history(datadir: Path, data_handler.ohlcv_store(pair, timeframe, data=data) return True - except Exception as e: - logger.error( - f'Failed to download history data for pair: "{pair}", timeframe: {timeframe}. ' - f'Error: {e}' + except Exception: + logger.exception( + f'Failed to download history data for pair: "{pair}", timeframe: {timeframe}.' ) return False @@ -304,10 +303,9 @@ def _download_trades_history(exchange: Exchange, logger.info(f"New Amount of trades: {len(trades)}") return True - except Exception as e: - logger.error( + except Exception: + logger.exception( f'Failed to download historic trades for pair: "{pair}". ' - f'Error: {e}' ) return False diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 905798041..99b22adda 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -312,10 +312,7 @@ def test_download_backtesting_data_exception(ohlcv_history, mocker, caplog, # clean files freshly downloaded _clean_test_file(file1_1) _clean_test_file(file1_5) - assert log_has( - 'Failed to download history data for pair: "MEME/BTC", timeframe: 1m. ' - 'Error: File Error', caplog - ) + assert log_has('Failed to download history data for pair: "MEME/BTC", timeframe: 1m.', caplog) def test_load_partial_missing(testdatadir, caplog) -> None: From 99b67348b206a75556d35f3994ec293b3d543dac Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 14:30:58 +0100 Subject: [PATCH 0938/1197] Add test for double-logging --- tests/test_configuration.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 9594b6413..47d393860 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -678,6 +678,9 @@ def test_set_loggers_syslog(mocker): assert [x for x in logger.handlers if type(x) == logging.handlers.SysLogHandler] assert [x for x in logger.handlers if type(x) == logging.StreamHandler] assert [x for x in logger.handlers if type(x) == logging.handlers.BufferingHandler] + # setting up logging again should NOT cause the loggers to be added a second time. + setup_logging(config) + assert len(logger.handlers) == 3 # reset handlers to not break pytest logger.handlers = orig_handlers From 0104c9fde68601e694969006029cd1b17e8c015c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 14:31:34 +0100 Subject: [PATCH 0939/1197] Fix double logging --- freqtrade/loggers.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py index 169cd2610..fbb05d879 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers.py @@ -37,6 +37,13 @@ def _set_loggers(verbosity: int = 0, api_verbosity: str = 'info') -> None: ) +def get_existing_handlers(handlertype): + """ + Returns Existing handler or None (if the handler has not yet been added to the root handlers). + """ + return next((h for h in logging.root.handlers if isinstance(h, handlertype)), None) + + def setup_logging_pre() -> None: """ Early setup for logging. @@ -71,18 +78,24 @@ def setup_logging(config: Dict[str, Any]) -> None: # config['logfilename']), which defaults to '/dev/log', applicable for most # of the systems. address = (s[1], int(s[2])) if len(s) > 2 else s[1] if len(s) > 1 else '/dev/log' - handler = SysLogHandler(address=address) + handler_sl = get_existing_handlers(SysLogHandler) + if handler_sl: + logging.root.removeHandler(handler_sl) + handler_sl = SysLogHandler(address=address) # No datetime field for logging into syslog, to allow syslog # to perform reduction of repeating messages if this is set in the # syslog config. The messages should be equal for this. - handler.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s')) - logging.root.addHandler(handler) + handler_sl.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s')) + logging.root.addHandler(handler_sl) elif s[0] == 'journald': try: from systemd.journal import JournaldLogHandler except ImportError: raise OperationalException("You need the systemd python package be installed in " "order to use logging to journald.") + handler_jd = get_existing_handlers(JournaldLogHandler) + if handler_jd: + logging.root.removeHandler(handler_jd) handler_jd = JournaldLogHandler() # No datetime field for logging into journald, to allow syslog # to perform reduction of repeating messages if this is set in the @@ -90,6 +103,9 @@ def setup_logging(config: Dict[str, Any]) -> None: handler_jd.setFormatter(Formatter('%(name)s - %(levelname)s - %(message)s')) logging.root.addHandler(handler_jd) else: + handler_rf = get_existing_handlers(RotatingFileHandler) + if handler_rf: + logging.root.removeHandler(handler_rf) handler_rf = RotatingFileHandler(logfile, maxBytes=1024 * 1024 * 10, # 10Mb backupCount=10) From b9980330a5469aa99160c8e40815a0c8707e0482 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 14:53:25 +0100 Subject: [PATCH 0940/1197] Add explicit test for FileHandler --- tests/test_configuration.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 47d393860..3501f1f3d 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -663,7 +663,7 @@ def test_set_loggers() -> None: @pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") -def test_set_loggers_syslog(mocker): +def test_set_loggers_syslog(): logger = logging.getLogger() orig_handlers = logger.handlers logger.handlers = [] @@ -685,6 +685,30 @@ def test_set_loggers_syslog(mocker): logger.handlers = orig_handlers +def test_set_loggers_Filehandler(tmpdir): + logger = logging.getLogger() + orig_handlers = logger.handlers + logger.handlers = [] + logfile = Path(tmpdir) / 'ft_logfile.log' + config = {'verbosity': 2, + 'logfile': str(logfile), + } + + setup_logging_pre() + setup_logging(config) + assert len(logger.handlers) == 3 + assert [x for x in logger.handlers if type(x) == logging.handlers.RotatingFileHandler] + assert [x for x in logger.handlers if type(x) == logging.StreamHandler] + assert [x for x in logger.handlers if type(x) == logging.handlers.BufferingHandler] + # setting up logging again should NOT cause the loggers to be added a second time. + setup_logging(config) + assert len(logger.handlers) == 3 + # reset handlers to not break pytest + if logfile.exists: + logfile.unlink() + logger.handlers = orig_handlers + + @pytest.mark.skip(reason="systemd is not installed on every system, so we're not testing this.") def test_set_loggers_journald(mocker): logger = logging.getLogger() From 46389e343bc0314427823023604c55bf212686e2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 15:10:17 +0100 Subject: [PATCH 0941/1197] Skip filehandler test on windows - as that causes a permission-error --- tests/test_configuration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 3501f1f3d..e6c91a96e 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -685,6 +685,7 @@ def test_set_loggers_syslog(): logger.handlers = orig_handlers +@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") def test_set_loggers_Filehandler(tmpdir): logger = logging.getLogger() orig_handlers = logger.handlers From 8f1d2ff0701bcf34609cec0c52e25697e3a8c65f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Nov 2020 19:47:27 +0100 Subject: [PATCH 0942/1197] Renamd volatilityFilter to RangeStabilityFilter --- config_full.json.example | 4 +-- docs/includes/pairlists.md | 22 ++++++------- freqtrade/constants.py | 2 +- ...ilityfilter.py => rangestabilityfilter.py} | 22 ++++++------- tests/pairlist/test_pairlist.py | 32 +++++++++---------- 5 files changed, 41 insertions(+), 41 deletions(-) rename freqtrade/pairlist/{volatilityfilter.py => rangestabilityfilter.py} (77%) diff --git a/config_full.json.example b/config_full.json.example index 0d82b9a2b..365b6180b 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -69,8 +69,8 @@ {"method": "PriceFilter", "low_price_ratio": 0.01, "min_price": 0.00000010}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}, { - "method": "VolatilityFilter", - "volatility_over_days": 10, + "method": "RangeStabilityFilter", + "lookback_days": 10, "min_volatility": 0.01, "refresh_period": 1440 } diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 7cd2369b1..149e784bd 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -19,7 +19,7 @@ Inactive markets are always removed from the resulting pairlist. Explicitly blac * [`PriceFilter`](#pricefilter) * [`ShuffleFilter`](#shufflefilter) * [`SpreadFilter`](#spreadfilter) -* [`VolatilityFilter`](#volatilityfilter) +* [`RangeStabilityFilter`](#rangestabilityfilter) !!! Tip "Testing pairlists" Pairlist configurations can be quite tricky to get right. Best use the [`test-pairlist`](utils.md#test-pairlist) utility sub-command to test your configuration quickly. @@ -119,26 +119,26 @@ Example: If `DOGE/BTC` maximum bid is 0.00000026 and minimum ask is 0.00000027, the ratio is calculated as: `1 - bid/ask ~= 0.037` which is `> 0.005` and this pair will be filtered out. -#### VolatilityFilter +#### RangeStabilityFilter -Removes pairs where the difference between lowest low and highest high over `volatility_over_days` days is below `min_volatility`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. +Removes pairs where the difference between lowest low and highest high over `lookback_days` days is below `min_rate_of_change`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. In the below example: -If volatility over the last 10 days is <1%, remove the pair from the whitelist. +If the trading range over the last 10 days is <1%, remove the pair from the whitelist. ```json "pairlists": [ { - "method": "VolatilityFilter", - "volatility_over_days": 10, - "min_volatility": 0.01, + "method": "RangeStabilityFilter", + "lookback_days": 10, + "min_rate_of_change": 0.01, "refresh_period": 1440 } ] ``` !!! Tip - This Filter can be used to automatically remove stable coin pairs, which have a very low volatility, and are therefore extremely difficult to trade with profit. + This Filter can be used to automatically remove stable coin pairs, which have a very low trading range, and are therefore extremely difficult to trade with profit. ### Full example of Pairlist Handlers @@ -160,9 +160,9 @@ The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, {"method": "PriceFilter", "low_price_ratio": 0.01}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}, { - "method": "VolatilityFilter", - "volatility_over_days": 10, - "min_volatility": 0.01, + "method": "RangeStabilityFilter", + "lookback_days": 10, + "min_rate_of_change": 0.01, "refresh_period": 1440 }, {"method": "ShuffleFilter", "seed": 42} diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 55d802587..2022556d2 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -25,7 +25,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PrecisionFilter', 'PriceFilter', - 'ShuffleFilter', 'SpreadFilter', 'VolatilityFilter'] + 'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S' diff --git a/freqtrade/pairlist/volatilityfilter.py b/freqtrade/pairlist/rangestabilityfilter.py similarity index 77% rename from freqtrade/pairlist/volatilityfilter.py rename to freqtrade/pairlist/rangestabilityfilter.py index 14ac0c617..f428bb113 100644 --- a/freqtrade/pairlist/volatilityfilter.py +++ b/freqtrade/pairlist/rangestabilityfilter.py @@ -15,23 +15,23 @@ from freqtrade.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) -class VolatilityFilter(IPairList): +class RangeStabilityFilter(IPairList): def __init__(self, exchange, pairlistmanager, config: Dict[str, Any], pairlistconfig: Dict[str, Any], pairlist_pos: int) -> None: super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) - self._days = pairlistconfig.get('volatility_over_days', 10) - self._min_volatility = pairlistconfig.get('min_volatility', 0.01) + self._days = pairlistconfig.get('lookback_days', 10) + self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01) self._refresh_period = pairlistconfig.get('refresh_period', 1440) self._pair_cache: TTLCache = TTLCache(maxsize=100, ttl=self._refresh_period) if self._days < 1: - raise OperationalException("VolatilityFilter requires volatility_over_days to be >= 1") + raise OperationalException("RangeStabilityFilter requires lookback_days to be >= 1") if self._days > exchange.ohlcv_candle_limit: - raise OperationalException("VolatilityFilter requires volatility_over_days to not " + raise OperationalException("RangeStabilityFilter requires lookback_days to not " "exceed exchange max request size " f"({exchange.ohlcv_candle_limit})") @@ -48,12 +48,12 @@ class VolatilityFilter(IPairList): """ Short whitelist method description - used for startup-messages """ - return (f"{self.name} - Filtering pairs with volatility below {self._min_volatility} " - f"over the last {plural(self._days, 'day')}.") + return (f"{self.name} - Filtering pairs with rate of change below " + f"{self._min_rate_of_change} over the last {plural(self._days, 'day')}.") def _validate_pair(self, ticker: Dict) -> bool: """ - Validate volatility + Validate trading range :param ticker: ticker dict as returned from ccxt.load_markets() :return: True if the pair can stay, False if it should be removed """ @@ -75,14 +75,14 @@ class VolatilityFilter(IPairList): highest_high = daily_candles['high'].max() lowest_low = daily_candles['low'].min() pct_change = ((highest_high - lowest_low) / lowest_low) if lowest_low > 0 else 0 - if pct_change >= self._min_volatility: + if pct_change >= self._min_rate_of_change: result = True else: self.log_on_refresh(logger.info, f"Removed {pair} from whitelist, " - f"because volatility over {plural(self._days, 'day')} is " + f"because rate of change over {plural(self._days, 'day')} is " f"{pct_change:.3f}, which is below the " - f"threshold of {self._min_volatility}.") + f"threshold of {self._min_rate_of_change}.") result = False self._pair_cache[pair] = result diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index e9df5d3f4..d696e6d02 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -341,8 +341,8 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): {"method": "PriceFilter", "low_price_ratio": 0.02}], "USDT", ['ETH/USDT', 'NANO/USDT']), ([{"method": "StaticPairList"}, - {"method": "VolatilityFilter", "volatility_over_days": 10, - "min_volatility": 0.01, "refresh_period": 1440}], + {"method": "RangeStabilityFilter", "lookback_days": 10, + "min_rate_of_change": 0.01, "refresh_period": 1440}], "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), ]) def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, @@ -586,9 +586,9 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count -def test_volatilityfilter_checks(mocker, default_conf, markets, tickers): +def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers): default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, - {'method': 'VolatilityFilter', 'volatility_over_days': 99999}] + {'method': 'RangeStabilityFilter', 'lookback_days': 99999}] mocker.patch.multiple('freqtrade.exchange.Exchange', markets=PropertyMock(return_value=markets), @@ -597,27 +597,27 @@ def test_volatilityfilter_checks(mocker, default_conf, markets, tickers): ) with pytest.raises(OperationalException, - match=r'VolatilityFilter requires volatility_over_days to not exceed ' + match=r'RangeStabilityFilter requires lookback_days to not exceed ' r'exchange max request size \([0-9]+\)'): get_patched_freqtradebot(mocker, default_conf) default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, - {'method': 'VolatilityFilter', 'volatility_over_days': 0}] + {'method': 'RangeStabilityFilter', 'lookback_days': 0}] with pytest.raises(OperationalException, - match='VolatilityFilter requires volatility_over_days to be >= 1'): + match='RangeStabilityFilter requires lookback_days to be >= 1'): get_patched_freqtradebot(mocker, default_conf) -@pytest.mark.parametrize('min_volatility,expected_length', [ +@pytest.mark.parametrize('min_rate_of_change,expected_length', [ (0.01, 5), - (0.05, 0), # Setting volatility to 5% removes all pairs from the whitelist. + (0.05, 0), # Setting rate_of_change to 5% removes all pairs from the whitelist. ]) -def test_volatilityfilter_caching(mocker, markets, default_conf, tickers, ohlcv_history_list, - min_volatility, expected_length): +def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, ohlcv_history_list, + min_rate_of_change, expected_length): default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, - {'method': 'VolatilityFilter', 'volatility_over_days': 2, - 'min_volatility': min_volatility}] + {'method': 'RangeStabilityFilter', 'lookback_days': 2, + 'min_rate_of_change': min_rate_of_change}] mocker.patch.multiple('freqtrade.exchange.Exchange', markets=PropertyMock(return_value=markets), @@ -677,9 +677,9 @@ def test_volatilityfilter_caching(mocker, markets, default_conf, tickers, ohlcv_ None, "PriceFilter requires max_price to be >= 0" ), # OperationalException expected - ({"method": "VolatilityFilter", "volatility_over_days": 10, "min_volatility": 0.01}, - "[{'VolatilityFilter': 'VolatilityFilter - Filtering pairs with volatility below 0.01 " - "over the last days.'}]", + ({"method": "RangeStabilityFilter", "lookback_days": 10, "min_rate_of_change": 0.01}, + "[{'RangeStabilityFilter': 'RangeStabilityFilter - Filtering pairs with rate of change below " + "0.01 over the last days.'}]", None ), ]) From 0d349cb3550bfbbfa6af7915a53789074a76d6a0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Nov 2020 19:59:18 +0100 Subject: [PATCH 0943/1197] Small finetuning --- config_full.json.example | 2 +- freqtrade/exchange/exchange.py | 2 +- freqtrade/pairlist/rangestabilityfilter.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 365b6180b..5ee2a1faf 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -71,7 +71,7 @@ { "method": "RangeStabilityFilter", "lookback_days": 10, - "min_volatility": 0.01, + "min_rate_of_change": 0.01, "refresh_period": 1440 } ], diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2f52c512f..18f4fbff5 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -689,7 +689,7 @@ class Exchange: since_ms: int) -> DataFrame: """ Minimal wrapper around get_historic_ohlcv - converting the result into a dataframe - :param pair: Pair to download + :param pair: Pair to download :param timeframe: Timeframe to get data for :param since_ms: Timestamp in milliseconds to get history from :return: OHLCV DataFrame diff --git a/freqtrade/pairlist/rangestabilityfilter.py b/freqtrade/pairlist/rangestabilityfilter.py index f428bb113..798d192bd 100644 --- a/freqtrade/pairlist/rangestabilityfilter.py +++ b/freqtrade/pairlist/rangestabilityfilter.py @@ -1,5 +1,5 @@ """ -Minimum age (days listed) pair list filter +Rate of change pairlist filter """ import logging from typing import Any, Dict From 8ae604d473df16f769c9cf43d4b37f3eec9f26cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Nov 2020 20:05:06 +0100 Subject: [PATCH 0944/1197] Ensure we're not running off of empty dataframes --- freqtrade/pairlist/rangestabilityfilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/pairlist/rangestabilityfilter.py b/freqtrade/pairlist/rangestabilityfilter.py index 798d192bd..b460ff477 100644 --- a/freqtrade/pairlist/rangestabilityfilter.py +++ b/freqtrade/pairlist/rangestabilityfilter.py @@ -71,7 +71,7 @@ class RangeStabilityFilter(IPairList): timeframe='1d', since_ms=since_ms) result = False - if daily_candles is not None: + if daily_candles is not None and not daily_candles.empty: highest_high = daily_candles['high'].max() lowest_low = daily_candles['low'].min() pct_change = ((highest_high - lowest_low) / lowest_low) if lowest_low > 0 else 0 From 6810192992df1e9f7943728c65c6c02e675c2d92 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Nov 2020 20:25:18 +0100 Subject: [PATCH 0945/1197] Update docstring for new filter --- freqtrade/pairlist/VolumePairList.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 44e5c52d7..7d3c2c653 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -49,7 +49,7 @@ class VolumePairList(IPairList): def needstickers(self) -> bool: """ Boolean property defining if tickers are necessary. - If no Pairlist requires tickers, an empty List is passed + If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ return True From c14c0f60a1b8a2fd52c501b5355713b92e3ba100 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 16:27:27 +0100 Subject: [PATCH 0946/1197] Add Support for kraken stoploss-limit --- docs/exchanges.md | 8 ++++---- docs/stoploss.md | 12 +++++++++--- freqtrade/exchange/kraken.py | 11 ++++++++--- tests/exchange/test_kraken.py | 26 +++++++++++++++----------- 4 files changed, 36 insertions(+), 21 deletions(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index 5d7505795..d877e6da2 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -23,7 +23,8 @@ Binance has been split into 3, and users must use the correct ccxt exchange ID f ## Kraken !!! Tip "Stoploss on Exchange" - Kraken supports `stoploss_on_exchange`. It provides great advantages, so we recommend to benefit from it. + Kraken supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it. + You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type to use. ### Historic Kraken data @@ -75,8 +76,7 @@ print(res) !!! Tip "Stoploss on Exchange" FTX supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it. - You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide. - + You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type of stoploss shall be used. ### Using subaccounts @@ -99,10 +99,10 @@ To use subaccounts with FTX, you need to edit the configuration and add the foll Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys. - ## Random notes for other exchanges * The Ocean (exchange id: `theocean`) exchange uses Web3 functionality and requires `web3` python package to be installed: + ```shell $ pip3 install web3 ``` diff --git a/docs/stoploss.md b/docs/stoploss.md index fa888cd47..7993da401 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -23,11 +23,12 @@ These modes can be configured with these values: ``` !!! Note - Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market) and FTX (stop limit and stop-market) as of now. - Do not set too low stoploss value if using stop loss on exchange! - If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work + Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market, stop-loss-limit) and FTX (stop limit and stop-market) as of now. + Do not set too low/tight stoploss value if using stop loss on exchange! + If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work. ### stoploss_on_exchange and stoploss_on_exchange_limit_ratio + Enable or Disable stop loss on exchange. If the stoploss is *on exchange* it means a stoploss limit order is placed on the exchange immediately after buy order happens successfully. This will protect you against sudden crashes in market as the order will be in the queue immediately and if market goes down then the order has more chance of being fulfilled. @@ -40,13 +41,18 @@ Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the limit order For example, assuming the stoploss is on exchange, and trailing stoploss is enabled, and the market is going up, then the bot automatically cancels the previous stoploss order and puts a new one with a stop value higher than the previous stoploss order. +!!! Note + If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new stoploss order. + ### stoploss_on_exchange_interval + In case of stoploss on exchange there is another parameter called `stoploss_on_exchange_interval`. This configures the interval in seconds at which the bot will check the stoploss and update it if necessary. The bot cannot do these every 5 seconds (at each iteration), otherwise it would get banned by the exchange. So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute). This same logic will reapply a stoploss order on the exchange should you cancel it accidentally. ### emergencysell + `emergencysell` is an optional value, which defaults to `market` and is used when creating stop loss on exchange orders fails. The below is the default which is used if not changed in strategy or configuration file. diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 5b7aa5c5b..d66793845 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -77,8 +77,15 @@ class Kraken(Exchange): Creates a stoploss market order. Stoploss market orders is the only stoploss type supported by kraken. """ + params = self._params.copy() - ordertype = "stop-loss" + if order_types.get('stoploss', 'market') == 'limit': + ordertype = "stop-loss-limit" + limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) + limit_rate = stop_price * limit_price_pct + params['price2'] = self.price_to_precision(pair, limit_rate) + else: + ordertype = "stop-loss" stop_price = self.price_to_precision(pair, stop_price) @@ -88,8 +95,6 @@ class Kraken(Exchange): return dry_order try: - params = self._params.copy() - amount = self.amount_to_precision(pair, amount) order = self._api.create_order(symbol=pair, type=ordertype, side='sell', diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 31b79a202..3803658eb 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -10,6 +10,7 @@ from tests.exchange.test_exchange import ccxt_exceptionhandlers STOPLOSS_ORDERTYPE = 'stop-loss' +STOPLOSS_LIMIT_ORDERTYPE = 'stop-loss-limit' def test_buy_kraken_trading_agreement(default_conf, mocker): @@ -156,7 +157,8 @@ def test_get_balances_prod(default_conf, mocker): "get_balances", "fetch_balance") -def test_stoploss_order_kraken(default_conf, mocker): +@pytest.mark.parametrize('ordertype', ['market', 'limit']) +def test_stoploss_order_kraken(default_conf, mocker, ordertype): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) @@ -173,24 +175,26 @@ def test_stoploss_order_kraken(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - # stoploss_on_exchange_limit_ratio is irrelevant for kraken market orders - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, - order_types={'stoploss_on_exchange_limit_ratio': 1.05}) - assert api_mock.create_order.call_count == 1 - - api_mock.create_order.reset_mock() - - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, + order_types={'stoploss': ordertype, + 'stoploss_on_exchange_limit_ratio': 0.99 + }) assert 'id' in order assert 'info' in order assert order['id'] == order_id assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' - assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE + if ordertype == 'limit': + assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_LIMIT_ORDERTYPE + assert api_mock.create_order.call_args_list[0][1]['params'] == { + 'trading_agreement': 'agree', 'price2': 217.8} + else: + assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE + assert api_mock.create_order.call_args_list[0][1]['params'] == { + 'trading_agreement': 'agree'} assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 assert api_mock.create_order.call_args_list[0][1]['price'] == 220 - assert api_mock.create_order.call_args_list[0][1]['params'] == {'trading_agreement': 'agree'} # test exception handling with pytest.raises(DependencyException): From d0d9921b42d5e4e10e57d3307d976c1f987c7e6e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 16:27:41 +0100 Subject: [PATCH 0947/1197] Reorder mkdocs sequence --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 8d1ce1cfe..2cc0c9fcb 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -20,13 +20,13 @@ nav: - Hyperopt: hyperopt.md - Edge Positioning: edge.md - Utility Subcommands: utils.md - - Exchange-specific Notes: exchanges.md - FAQ: faq.md - Data Analysis: - Jupyter Notebooks: data-analysis.md - Strategy analysis: strategy_analysis_example.md - Plotting: plotting.md - SQL Cheatsheet: sql_cheatsheet.md + - Exchange-specific Notes: exchanges.md - Advanced Post-installation Tasks: advanced-setup.md - Advanced Strategy: strategy-advanced.md - Advanced Hyperopt: advanced-hyperopt.md From 1d56c87a34850453e88500ee6028ec5e222b3d3f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 21:39:12 +0100 Subject: [PATCH 0948/1197] Fully support kraken limit stoploss --- freqtrade/exchange/exchange.py | 2 +- freqtrade/persistence/models.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 18f4fbff5..611ce4abd 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -524,7 +524,7 @@ class Exchange: 'rate': self.get_fee(pair) } }) - if closed_order["type"] in ["stop_loss_limit"]: + if closed_order["type"] in ["stop_loss_limit", "stop-loss-limit"]: closed_order["info"].update({"stopPrice": closed_order["price"]}) self._dry_run_open_orders[closed_order["id"]] = closed_order diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 8160ffbbf..6027908da 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -397,7 +397,7 @@ class Trade(_DECL_BASE): if self.is_open: logger.info(f'{order_type.upper()}_SELL has been fulfilled for {self}.') self.close(safe_value_fallback(order, 'average', 'price')) - elif order_type in ('stop_loss_limit', 'stop-loss', 'stop'): + elif order_type in ('stop_loss_limit', 'stop-loss', 'stop-loss-limit', 'stop'): self.stoploss_order_id = None self.close_rate_requested = self.stop_loss if self.is_open: From 0b68402c1094c89ead68bd4908eec47383005835 Mon Sep 17 00:00:00 2001 From: hoeckxer Date: Thu, 26 Nov 2020 10:24:48 +0100 Subject: [PATCH 0949/1197] Fixed a small typo in the pairlist documentation Signed-off-by: hoeckxer --- docs/includes/pairlists.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 149e784bd..5bb02470d 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -60,7 +60,7 @@ The `refresh_period` setting allows to define the period (in seconds), at which "method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume", - "refresh_period": 1800, + "refresh_period": 1800 }], ``` From dddbc799f9b1c8d686a33de95a6f620322a56ec7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 26 Nov 2020 19:40:08 +0100 Subject: [PATCH 0950/1197] have kraken stoploss-limit support trailing stop --- freqtrade/exchange/kraken.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index d66793845..4e4713052 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -69,7 +69,8 @@ class Kraken(Exchange): Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. """ - return order['type'] == 'stop-loss' and stop_loss > float(order['price']) + return (order['type'] in ('stop-loss', 'stop-loss-limit') + and stop_loss > float(order['price'])) @retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: From 98118f5e956d02c3f646ccb8feff1992e118cc22 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Thu, 26 Nov 2020 18:46:36 -0600 Subject: [PATCH 0951/1197] Fix parameter name Correct which parameter name was referred to within the 2nd Note under "Amend last stake amount" --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 56ba13414..2e8f6555f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -177,7 +177,7 @@ In the example above this would mean: This option only applies with [Static stake amount](#static-stake-amount) - since [Dynamic stake amount](#dynamic-stake-amount) divides the balances evenly. !!! Note - The minimum last stake amount can be configured using `amend_last_stake_amount` - which defaults to 0.5 (50%). This means that the minimum stake amount that's ever used is `stake_amount * 0.5`. This avoids very low stake amounts, that are close to the minimum tradable amount for the pair and can be refused by the exchange. + The minimum last stake amount can be configured using `last_stake_amount_min_ratio` - which defaults to 0.5 (50%). This means that the minimum stake amount that's ever used is `stake_amount * 0.5`. This avoids very low stake amounts, that are close to the minimum tradable amount for the pair and can be refused by the exchange. #### Static stake amount From fce31447edf2c0466cef8de13e0f8d0a26d71e61 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Thu, 26 Nov 2020 19:38:20 -0600 Subject: [PATCH 0952/1197] Prevent unintended LaTeX rendering --- docs/stoploss.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index 7993da401..14b04c7e0 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -140,7 +140,7 @@ For example, simplified math: * the stop loss would get triggered once the asset drops below 90$ * assuming the asset now increases to 102$ * the stop loss will now be -2% of 102$ = 99.96$ (99.96$ stop loss will be locked in and will follow asset price increasements with -2%) -* now the asset drops in value to 101$, the stop loss will still be 99.96$ and would trigger at 99.96$ +* now the asset drops in value to 101$, the stop loss will still be 99.96$ and would trigger at 99.96$ The 0.02 would translate to a -2% stop loss. Before this, `stoploss` is used for the trailing stoploss. From 31449987c08780afc384d524fd2bf9098995ae5b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 07:35:12 +0100 Subject: [PATCH 0953/1197] Fix mkdocs rendering --- docs/stoploss.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/stoploss.md b/docs/stoploss.md index 14b04c7e0..1e21fc50d 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -36,8 +36,8 @@ If `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the st `stoploss` defines the stop-price where the limit order is placed - and limit should be slightly below this. If an exchange supports both limit and market stoploss orders, then the value of `stoploss` will be used to determine the stoploss type. -Calculation example: we bought the asset at 100$. -Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the limit order fill can happen between 95$ and 94.05$. +Calculation example: we bought the asset at 100\$. +Stop-price is 95\$, then limit would be `95 * 0.99 = 94.05$` - so the limit order fill can happen between 95$ and 94.05$. For example, assuming the stoploss is on exchange, and trailing stoploss is enabled, and the market is going up, then the bot automatically cancels the previous stoploss order and puts a new one with a stop value higher than the previous stoploss order. @@ -90,6 +90,7 @@ Example of stop loss: ``` For example, simplified math: + * the bot buys an asset at a price of 100$ * the stop loss is defined at -10% * the stop loss would get triggered once the asset drops below 90$ @@ -113,7 +114,7 @@ For example, simplified math: * the stop loss would get triggered once the asset drops below 90$ * assuming the asset now increases to 102$ * the stop loss will now be -10% of 102$ = 91.8$ -* now the asset drops in value to 101$, the stop loss will still be 91.8$ and would trigger at 91.8$. +* now the asset drops in value to 101\$, the stop loss will still be 91.8$ and would trigger at 91.8$. In summary: The stoploss will be adjusted to be always be -10% of the highest observed price. @@ -139,8 +140,8 @@ For example, simplified math: * the stop loss is defined at -10% * the stop loss would get triggered once the asset drops below 90$ * assuming the asset now increases to 102$ -* the stop loss will now be -2% of 102$ = 99.96$ (99.96$ stop loss will be locked in and will follow asset price increasements with -2%) -* now the asset drops in value to 101$, the stop loss will still be 99.96$ and would trigger at 99.96$ +* the stop loss will now be -2% of 102$ = 99.96$ (99.96$ stop loss will be locked in and will follow asset price increments with -2%) +* now the asset drops in value to 101\$, the stop loss will still be 99.96$ and would trigger at 99.96$ The 0.02 would translate to a -2% stop loss. Before this, `stoploss` is used for the trailing stoploss. @@ -157,7 +158,7 @@ This option can be used with or without `trailing_stop_positive`, but uses `trai trailing_only_offset_is_reached = True ``` -Configuration (offset is buyprice + 3%): +Configuration (offset is buy-price + 3%): ``` python stoploss = -0.10 @@ -175,7 +176,7 @@ For example, simplified math: * stoploss will remain at 90$ unless asset increases to or above our configured offset * assuming the asset now increases to 103$ (where we have the offset configured) * the stop loss will now be -2% of 103$ = 100.94$ -* now the asset drops in value to 101$, the stop loss will still be 100.94$ and would trigger at 100.94$ +* now the asset drops in value to 101\$, the stop loss will still be 100.94$ and would trigger at 100.94$ !!! Tip Make sure to have this value (`trailing_stop_positive_offset`) lower than minimal ROI, otherwise minimal ROI will apply first and sell the trade. From 81d08c4deff25a9bd8b24e6d6847bdc04ccdace0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 08:16:11 +0100 Subject: [PATCH 0954/1197] Add detailed backtest test verifying the ROI / trailing stop collision --- freqtrade/strategy/interface.py | 12 +++--- tests/optimize/test_backtest_detail.py | 55 ++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 44a281ebe..172264b10 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -475,24 +475,24 @@ class IStrategy(ABC): stoplossflag = self.stop_loss_reached(current_rate=current_rate, trade=trade, current_time=date, current_profit=current_profit, force_stoploss=force_stoploss, high=high) - + # Set current rate to high for backtesting sell current_rate = high or rate current_profit = trade.calc_profit_ratio(current_rate) config_ask_strategy = self.config.get('ask_strategy', {}) - + roi_reached = self.min_roi_reached(trade=trade, current_profit=current_profit, - current_time=date) - + current_time=date) + if stoplossflag.sell_flag: - + # When backtesting, in the case of trailing_stop_loss, # make sure we don't make a profit higher than ROI. if stoplossflag.sell_type == SellType.TRAILING_STOP_LOSS and roi_reached: logger.debug(f"{trade.pair} - Required profit reached. sell_flag=True, " f"sell_type=SellType.ROI") return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) - + logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, " f"sell_type={stoplossflag.sell_type}") return stoplossflag diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index a5de64fe4..f3a2d8b96 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -328,6 +328,58 @@ tc20 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] ) +# Test 21: trailing_stop ROI collision. +# Roi should trigger before Trailing stop - otherwise Trailing stop profits can be > ROI +# which cannot happen in reality +# stop-loss: 10%, ROI: 4%, Trailing stop adjusted at the sell candle +tc21 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5050, 4950, 5100, 6172, 0, 0], + [2, 5100, 5251, 4650, 5100, 6172, 0, 0], + [3, 4850, 5050, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.04}, profit_perc=0.04, trailing_stop=True, + trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)] +) + +# Test 22: trailing_stop Raises in candle 2 - but ROI applies at the same time. +# applying a positive trailing stop of 3% - ROI should apply before trailing stop. +# stop-loss: 10%, ROI: 4%, stoploss adjusted candle 2 +tc22 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5050, 4950, 5100, 6172, 0, 0], + [2, 5100, 5251, 5100, 5100, 6172, 0, 0], + [3, 4850, 5050, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.04}, profit_perc=0.04, trailing_stop=True, + trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=2)] +) + +# Test 23: trailing_stop Raises in candle 2 (does not trigger) +# applying a positive trailing stop of 3% since stop_positive_offset is reached. +# ROI is changed after this to 4%, dropping ROI below trailing_stop_positive, causing a sell +# in the candle after the raised stoploss candle with ROI reason. +# Stoploss would trigger in this candle too, but it's no longer relevant. +# stop-loss: 10%, ROI: 4%, stoploss adjusted candle 2, ROI adjusted in candle 3 (causing the sell) +tc23 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5050, 4950, 5000, 6172, 1, 0], + [1, 5000, 5050, 4950, 5100, 6172, 0, 0], + [2, 5100, 5251, 5100, 5100, 6172, 0, 0], + [3, 4850, 5251, 4650, 4750, 6172, 0, 0], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.1, "119": 0.03}, profit_perc=0.03, trailing_stop=True, + trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.05, + trailing_stop_positive=0.03, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] +) + TESTS = [ tc0, @@ -351,6 +403,9 @@ TESTS = [ tc18, tc19, tc20, + tc21, + tc22, + tc23, ] From 57461a59f3b416add4e2a5169d89b81cd4c12ea8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 08:30:17 +0100 Subject: [PATCH 0955/1197] Update backtesting documentation with new logic --- docs/backtesting.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/backtesting.md b/docs/backtesting.md index 84911568b..2121e3126 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -278,6 +278,7 @@ Since backtesting lacks some detailed information about what happens within a ca - Trailing stoploss - High happens first - adjusting stoploss - Low uses the adjusted stoploss (so sells with large high-low difference are backtested correctly) + - ROI applies before trailing-stop, ensuring profits are "top-capped" at ROI if both ROI and trailing stop applies - Sell-reason does not explain if a trade was positive or negative, just what triggered the sell (this can look odd if negative ROI values are used) - Stoploss (and trailing stoploss) is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` and/or `trailing_stop` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes. From 4aa6ebee049a924b5fd5c931f14360af917eb3e4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 09:17:25 +0100 Subject: [PATCH 0956/1197] Add more tests for #2422 --- tests/optimize/test_backtest_detail.py | 64 ++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index f3a2d8b96..720ed8c13 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -380,6 +380,66 @@ tc23 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] ) +# Test 24: Sell with signal sell in candle 3 (stoploss also triggers on this candle) +# Stoploss at 1%. +# Stoploss wins over Sell-signal (because sell-signal is acted on in the next candle) +tc24 = 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], # enter trade (signal on last candle) + [2, 4987, 5012, 4986, 4600, 6172, 0, 0], + [3, 5010, 5000, 4855, 5010, 6172, 0, 1], # Triggers stoploss + sellsignal + [4, 5010, 4987, 4977, 4995, 6172, 0, 0], + [5, 4995, 4995, 4995, 4950, 6172, 0, 0]], + stop_loss=-0.01, roi={"0": 1}, profit_perc=-0.01, use_sell_signal=True, + trades=[BTrade(sell_reason=SellType.STOP_LOSS, open_tick=1, close_tick=3)] +) + +# Test 25: Sell with signal sell in candle 3 (stoploss also triggers on this candle) +# Stoploss at 1%. +# Sell-signal wins over stoploss +tc25 = 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], # enter trade (signal on last candle) + [2, 4987, 5012, 4986, 4600, 6172, 0, 0], + [3, 5010, 5000, 4986, 5010, 6172, 0, 1], + [4, 5010, 4987, 4855, 4995, 6172, 0, 0], # Triggers stoploss + sellsignal acted on + [5, 4995, 4995, 4995, 4950, 6172, 0, 0]], + stop_loss=-0.01, roi={"0": 1}, profit_perc=0.002, use_sell_signal=True, + trades=[BTrade(sell_reason=SellType.SELL_SIGNAL, open_tick=1, close_tick=4)] +) + +# Test 26: Sell with signal sell in candle 3 (ROI at signal candle) +# Stoploss at 10% (irrelevant), ROI at 5% (will trigger) +# Sell-signal wins over stoploss +tc26 = 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], # enter trade (signal on last candle) + [2, 4987, 5012, 4986, 4600, 6172, 0, 0], + [3, 5010, 5251, 4986, 5010, 6172, 0, 1], # Triggers ROI, sell-signal + [4, 5010, 4987, 4855, 4995, 6172, 0, 0], + [5, 4995, 4995, 4995, 4950, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.05}, profit_perc=0.05, use_sell_signal=True, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] +) + +# Test 27: Sell with signal sell in candle 3 (ROI at signal candle) +# Stoploss at 10% (irrelevant), ROI at 5% (will trigger) - Wins over Sell-signal +# TODO: figure out if sell-signal should win over ROI +# Sell-signal wins over stoploss +tc27 = 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], # enter trade (signal on last candle) + [2, 4987, 5012, 4986, 4600, 6172, 0, 0], + [3, 5010, 5012, 4986, 5010, 6172, 0, 1], # sell-signal + [4, 5010, 5251, 4855, 4995, 6172, 0, 0], # Triggers ROI, sell-signal acted on + [5, 4995, 4995, 4995, 4950, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.05}, profit_perc=0.05, use_sell_signal=True, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=4)] +) TESTS = [ tc0, @@ -406,6 +466,10 @@ TESTS = [ tc21, tc22, tc23, + tc24, + tc25, + tc26, + tc27, ] From fefb4b23d0603683800790de198f1894143449a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 09:18:03 +0100 Subject: [PATCH 0957/1197] revise logic in should_sell --- freqtrade/strategy/interface.py | 53 ++++++++++++++------------------- tests/test_freqtradebot.py | 2 +- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 172264b10..81f4e7651 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -481,46 +481,39 @@ class IStrategy(ABC): current_profit = trade.calc_profit_ratio(current_rate) config_ask_strategy = self.config.get('ask_strategy', {}) - roi_reached = self.min_roi_reached(trade=trade, current_profit=current_profit, - current_time=date) + # if buy signal and ignore_roi is set, we don't need to evaluate min_roi. + roi_reached = (not (buy and config_ask_strategy.get('ignore_roi_if_buy_signal', False)) + and self.min_roi_reached(trade=trade, current_profit=current_profit, + current_time=date)) - if stoplossflag.sell_flag: + if config_ask_strategy.get('sell_profit_only', False) and trade.calc_profit(rate=rate) <= 0: + # Negative profits and sell_profit_only - ignore sell signal + sell_signal = False + else: + sell_signal = sell and not buy and config_ask_strategy.get('use_sell_signal', True) + # TODO: return here if sell-signal should be favored over ROI - # When backtesting, in the case of trailing_stop_loss, - # make sure we don't make a profit higher than ROI. - if stoplossflag.sell_type == SellType.TRAILING_STOP_LOSS and roi_reached: - logger.debug(f"{trade.pair} - Required profit reached. sell_flag=True, " - f"sell_type=SellType.ROI") - return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) - - logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, " - f"sell_type={stoplossflag.sell_type}") - return stoplossflag - - if buy and config_ask_strategy.get('ignore_roi_if_buy_signal', False): - # This one is noisy, commented out - # logger.debug(f"{trade.pair} - Buy signal still active. sell_flag=False") - return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) - - # Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee) - if roi_reached: + # Start evaluations + # Sequence: + # ROI (if not stoploss) + # Sell-signal + # Stoploss + if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS: logger.debug(f"{trade.pair} - Required profit reached. sell_flag=True, " f"sell_type=SellType.ROI") return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) - if config_ask_strategy.get('sell_profit_only', False): - # This one is noisy, commented out - # logger.debug(f"{trade.pair} - Checking if trade is profitable...") - if trade.calc_profit(rate=rate) <= 0: - # This one is noisy, commented out - # logger.debug(f"{trade.pair} - Trade is not profitable. sell_flag=False") - return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) - - if sell and not buy and config_ask_strategy.get('use_sell_signal', True): + if sell_signal: logger.debug(f"{trade.pair} - Sell signal received. sell_flag=True, " f"sell_type=SellType.SELL_SIGNAL") return SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL) + if stoplossflag.sell_flag: + + logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, " + f"sell_type={stoplossflag.sell_type}") + return stoplossflag + # This one is noisy, commented out... # logger.debug(f"{trade.pair} - No sell signal. sell_flag=False") return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 1f5b3ecaa..64dfb016e 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3556,7 +3556,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_b # Test if buy-signal is absent patch_get_signal(freqtrade, value=(False, True)) assert freqtrade.handle_trade(trade) is True - assert trade.sell_reason == SellType.STOP_LOSS.value + assert trade.sell_reason == SellType.SELL_SIGNAL.value def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, fee, caplog, mocker): From c69ce28b76449a1785cc41bc1491d66c766e8f9d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 09:26:58 +0100 Subject: [PATCH 0958/1197] Update backtest assumption documentation --- docs/backtesting.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 2121e3126..953198ddd 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -268,19 +268,24 @@ It contains some useful key metrics about performance of your strategy on backte Since backtesting lacks some detailed information about what happens within a candle, it needs to take a few assumptions: - 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 +- Sell-signal sells happen at open-price of the consecutive candle +- Sell-signal is favored over Stoploss, because sell-signals are assumed to trigger on candle's open - 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 +- Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes +- Low happens before high for stoploss, protecting capital first - Trailing stoploss - High happens first - adjusting stoploss - Low uses the adjusted stoploss (so sells with large high-low difference are backtested correctly) - ROI applies before trailing-stop, ensuring profits are "top-capped" at ROI if both ROI and trailing stop applies - Sell-reason does not explain if a trade was positive or negative, just what triggered the sell (this can look odd if negative ROI values are used) -- Stoploss (and trailing stoploss) is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` and/or `trailing_stop` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes. +- Evaluation sequence (if multiple signals happen on the same candle) + - ROI (if not stoploss) + - Sell-signal + - Stoploss Taking these assumptions, backtesting tries to mirror real trading as closely as possible. However, backtesting will **never** replace running a strategy in dry-run mode. Also, keep in mind that past results don't guarantee future success. From 46ec6f498c7cf95c677955051ce17e3770cfb6ed Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Fri, 27 Nov 2020 12:51:44 -0600 Subject: [PATCH 0959/1197] Correct link Fix prior redirection to a non-working link: https://www.freqtrade.io/en/latest/telegram-usage/configuration/#understand-forcebuy_enable --- docs/telegram-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 09cf21223..f4bd0a12a 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -207,7 +207,7 @@ Return a summary of your profit/loss and performance. Note that for this to work, `forcebuy_enable` needs to be set to true. -[More details](configuration.md/#understand-forcebuy_enable) +[More details](configuration.md#understand-forcebuy_enable) ### /performance From 95c3c45ec95c3c4daa4c190f62bff3f867d0375b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 20:24:04 +0100 Subject: [PATCH 0960/1197] Remove long deprecated settings that moved from experimental to ask_strategy --- .../configuration/deprecated_settings.py | 30 ++++++++--- freqtrade/constants.py | 3 -- tests/test_configuration.py | 50 +++++++++++++++++-- 3 files changed, 70 insertions(+), 13 deletions(-) diff --git a/freqtrade/configuration/deprecated_settings.py b/freqtrade/configuration/deprecated_settings.py index 03ed41ab8..6873ab405 100644 --- a/freqtrade/configuration/deprecated_settings.py +++ b/freqtrade/configuration/deprecated_settings.py @@ -26,6 +26,24 @@ def check_conflicting_settings(config: Dict[str, Any], ) +def process_removed_setting(config: Dict[str, Any], + section1: str, name1: str, + section2: str, name2: str) -> None: + """ + :param section1: Removed section + :param name1: Removed setting name + :param section2: new section for this key + :param name2: new setting name + """ + section1_config = config.get(section1, {}) + if name1 in section1_config: + raise OperationalException( + f"Setting `{section1}.{name1}` has been moved to `{section2}.{name2}. " + f"Please delete it from your configuration and use the `{section2}.{name2}` " + "setting instead." + ) + + def process_deprecated_setting(config: Dict[str, Any], section1: str, name1: str, section2: str, name2: str) -> None: @@ -51,12 +69,12 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None: check_conflicting_settings(config, 'ask_strategy', 'ignore_roi_if_buy_signal', 'experimental', 'ignore_roi_if_buy_signal') - process_deprecated_setting(config, 'ask_strategy', 'use_sell_signal', - 'experimental', 'use_sell_signal') - process_deprecated_setting(config, 'ask_strategy', 'sell_profit_only', - 'experimental', 'sell_profit_only') - process_deprecated_setting(config, 'ask_strategy', 'ignore_roi_if_buy_signal', - 'experimental', 'ignore_roi_if_buy_signal') + process_removed_setting(config, 'experimental', 'use_sell_signal', + 'ask_strategy', 'use_sell_signal') + process_removed_setting(config, 'experimental', 'sell_profit_only', + 'ask_strategy', 'sell_profit_only') + process_removed_setting(config, 'experimental', 'ignore_roi_if_buy_signal', + 'ask_strategy', 'ignore_roi_if_buy_signal') if (config.get('edge', {}).get('enabled', False) and 'capital_available_percentage' in config.get('edge', {})): diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 2022556d2..3e523a49e 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -182,9 +182,6 @@ CONF_SCHEMA = { 'experimental': { 'type': 'object', 'properties': { - 'use_sell_signal': {'type': 'boolean'}, - 'sell_profit_only': {'type': 'boolean'}, - 'ignore_roi_if_buy_signal': {'type': 'boolean'}, 'block_bad_exchanges': {'type': 'boolean'} } }, diff --git a/tests/test_configuration.py b/tests/test_configuration.py index e6c91a96e..6c895a00b 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -16,6 +16,7 @@ from freqtrade.configuration import (Configuration, check_exchange, remove_crede from freqtrade.configuration.config_validation import validate_config_schema from freqtrade.configuration.deprecated_settings import (check_conflicting_settings, process_deprecated_setting, + process_removed_setting, process_temporary_deprecated_settings) from freqtrade.configuration.load_config import load_config_file, log_config_error_range from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL @@ -1061,13 +1062,11 @@ def test_pairlist_resolving_fallback(mocker): assert config['datadir'] == Path.cwd() / "user_data/data/binance" +@pytest.mark.skip(reason='Currently no deprecated / moved sections') +# The below is kept as a sample for the future. @pytest.mark.parametrize("setting", [ ("ask_strategy", "use_sell_signal", True, "experimental", "use_sell_signal", False), - ("ask_strategy", "sell_profit_only", False, - "experimental", "sell_profit_only", True), - ("ask_strategy", "ignore_roi_if_buy_signal", False, - "experimental", "ignore_roi_if_buy_signal", True), ]) def test_process_temporary_deprecated_settings(mocker, default_conf, setting, caplog): patched_configuration_load_config_file(mocker, default_conf) @@ -1097,6 +1096,25 @@ def test_process_temporary_deprecated_settings(mocker, default_conf, setting, ca assert default_conf[setting[0]][setting[1]] == setting[5] +@pytest.mark.parametrize("setting", [ + ("experimental", "use_sell_signal", False), + ("experimental", "sell_profit_only", True), + ("experimental", "ignore_roi_if_buy_signal", True), + ]) +def test_process_removed_settings(mocker, default_conf, setting, caplog): + patched_configuration_load_config_file(mocker, default_conf) + + # Create sections for new and deprecated settings + # (they may not exist in the config) + default_conf[setting[0]] = {} + # Assign removed setting + default_conf[setting[0]][setting[1]] = setting[2] + + # New and deprecated settings are conflicting ones + with pytest.raises(OperationalException, + match=r'Setting .* has been moved'): + process_temporary_deprecated_settings(default_conf) + def test_process_deprecated_setting_edge(mocker, edge_conf, caplog): patched_configuration_load_config_file(mocker, edge_conf) edge_conf.update({'edge': { @@ -1196,6 +1214,30 @@ def test_process_deprecated_setting(mocker, default_conf, caplog): assert default_conf['sectionA']['new_setting'] == 'valA' +def test_process_removed_setting(mocker, default_conf, caplog): + patched_configuration_load_config_file(mocker, default_conf) + + # Create sections for new and deprecated settings + # (they may not exist in the config) + default_conf['sectionA'] = {} + default_conf['sectionB'] = {} + # Assign new setting + default_conf['sectionB']['somesetting'] = 'valA' + + # Only new setting exists (nothing should happen) + process_removed_setting(default_conf, + 'sectionA', 'somesetting', + 'sectionB', 'somesetting') + # Assign removed setting + default_conf['sectionA']['somesetting'] = 'valB' + + with pytest.raises(OperationalException, + match=r"Setting .* has been moved"): + process_removed_setting(default_conf, + 'sectionA', 'somesetting', + 'sectionB', 'somesetting') + + def test_process_deprecated_ticker_interval(mocker, default_conf, caplog): message = "DEPRECATED: Please use 'timeframe' instead of 'ticker_interval." config = deepcopy(default_conf) From af1b3721fb736409cb8107912b664b07d4d7be30 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 20:28:17 +0100 Subject: [PATCH 0961/1197] remove duplicate settings check --- freqtrade/configuration/deprecated_settings.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/freqtrade/configuration/deprecated_settings.py b/freqtrade/configuration/deprecated_settings.py index 6873ab405..6b2a20c8c 100644 --- a/freqtrade/configuration/deprecated_settings.py +++ b/freqtrade/configuration/deprecated_settings.py @@ -62,12 +62,11 @@ def process_deprecated_setting(config: Dict[str, Any], def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None: - check_conflicting_settings(config, 'ask_strategy', 'use_sell_signal', - 'experimental', 'use_sell_signal') - check_conflicting_settings(config, 'ask_strategy', 'sell_profit_only', - 'experimental', 'sell_profit_only') - check_conflicting_settings(config, 'ask_strategy', 'ignore_roi_if_buy_signal', - 'experimental', 'ignore_roi_if_buy_signal') + # Kept for future deprecated / moved settings + # check_conflicting_settings(config, 'ask_strategy', 'use_sell_signal', + # 'experimental', 'use_sell_signal') + # process_deprecated_setting(config, 'ask_strategy', 'use_sell_signal', + # 'experimental', 'use_sell_signal') process_removed_setting(config, 'experimental', 'use_sell_signal', 'ask_strategy', 'use_sell_signal') From 89573348b6ed83ac2e8f16f0888d41c767f1f337 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Fri, 27 Nov 2020 20:37:52 -0600 Subject: [PATCH 0962/1197] Fix link --- docs/telegram-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 09cf21223..f4bd0a12a 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -207,7 +207,7 @@ Return a summary of your profit/loss and performance. Note that for this to work, `forcebuy_enable` needs to be set to true. -[More details](configuration.md/#understand-forcebuy_enable) +[More details](configuration.md#understand-forcebuy_enable) ### /performance From 7cbd89657f1f477451d91c962f1fad260858385c Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Fri, 27 Nov 2020 21:24:40 -0600 Subject: [PATCH 0963/1197] Initial step towards implementing proposed code --- freqtrade/constants.py | 2 +- freqtrade/pairlist/PerformanceFilter.py | 61 +++++++++++++++++++++++++ tests/pairlist/test_pairlist.py | 22 ++++++++- 3 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 freqtrade/pairlist/PerformanceFilter.py diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 3271dda39..f47301fa6 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -25,7 +25,7 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PrecisionFilter', 'PriceFilter', - 'ShuffleFilter', 'SpreadFilter'] + 'ShuffleFilter', 'SpreadFilter', 'PerformanceFilter'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S' diff --git a/freqtrade/pairlist/PerformanceFilter.py b/freqtrade/pairlist/PerformanceFilter.py new file mode 100644 index 000000000..e689ba0bc --- /dev/null +++ b/freqtrade/pairlist/PerformanceFilter.py @@ -0,0 +1,61 @@ +""" +Performance pair list filter +""" +import logging +import random +from typing import Any, Dict, List + +import pandas as pd +from pandas import DataFrame, Series + +from freqtrade.pairlist.IPairList import IPairList + +from freqtrade.persistence import Trade +from datetime import timedelta, datetime, timezone + +logger = logging.getLogger(__name__) + +class PerformanceFilter(IPairList): + + def __init__(self, exchange, pairlistmanager, + config: Dict[str, Any], pairlistconfig: Dict[str, Any], + pairlist_pos: int) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + + + @property + def needstickers(self) -> bool: + """ + Boolean property defining if tickers are necessary. + If no Pairlist requries tickers, an empty List is passed + as tickers argument to filter_pairlist + """ + return False + + def short_desc(self) -> str: + """ + Short whitelist method description - used for startup-messages + """ + return f"{self.name} - Sorting pairs by performance." + + def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: + """ + Filters and sorts pairlist and returns the whitelist again. + Called on each bot iteration - please use internal caching if necessary + :param pairlist: pairlist to filter or sort + :param tickers: Tickers (from exchange.get_tickers()). May be cached. + :return: new whitelist + """ + + # Get the trading performance for pairs from database + perf = pd.DataFrame(Trade.get_overall_performance()) + # update pairlist with values from performance dataframe + # set initial value for pairs with no trades to 0 + # and sort the list using performance and count + list_df = pd.DataFrame({'pair':pairlist}) + sorted_df = list_df.join(perf.set_index('pair'), on='pair')\ + .fillna(0).sort_values(by=['profit', 'count'], ascending=False) + pairlist = sorted_df['pair'].tolist() + + + return pairlist \ No newline at end of file diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 1f05bef1e..2643a0bd8 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -246,7 +246,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): {"method": "PrecisionFilter"}, {"method": "PriceFilter", "low_price_ratio": 0.03}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}, - {"method": "ShuffleFilter"}], + {"method": "ShuffleFilter"}, {"method": "PerformanceFilter"}], "ETH", []), # AgeFilter and VolumePairList (require 2 days only, all should pass age test) ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, @@ -302,6 +302,18 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "ShuffleFilter"}], "USDT", 3), # whitelist_result is integer -- check only length of randomized pairlist + # PerformanceFilter + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "PerformanceFilter", "seed": 77}], + "USDT", ['ADADOUBLE/USDT', 'ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT']), + # PerformanceFilter, other seed + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "PerformanceFilter", "seed": 42}], + "USDT", ['ADAHALF/USDT', 'NANO/USDT', 'ADADOUBLE/USDT', 'ETH/USDT']), + # PerformanceFilter, no seed + ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, + {"method": "PerformanceFilter"}], + "USDT", 3), # whitelist_result is integer -- check only length of randomized pairlist # AgeFilter only ([{"method": "AgeFilter", "min_days_listed": 2}], "BTC", 'filter_at_the_beginning'), # OperationalException expected @@ -326,6 +338,13 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # ShuffleFilter only ([{"method": "ShuffleFilter", "seed": 42}], "BTC", 'filter_at_the_beginning'), # OperationalException expected + # PrecisionFilter after StaticPairList + ([{"method": "StaticPairList"}, + {"method": "PrecisionFilter", "seed": 42}], + "BTC", ['TKN/BTC', 'ETH/BTC', 'HOT/BTC']), + # PrecisionFilter only + ([{"method": "PrecisionFilter", "seed": 42}], + "BTC", 'filter_at_the_beginning'), # OperationalException expected # SpreadFilter after StaticPairList ([{"method": "StaticPairList"}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}], @@ -379,6 +398,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t assert isinstance(whitelist, list) # Verify length of pairlist matches (used for ShuffleFilter without seed) + # TBD if this applies to PerformanceFilter if type(whitelist_result) is list: assert whitelist == whitelist_result else: From 05686998bbc497f56d7407cc96d713686c4f6d85 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Fri, 27 Nov 2020 21:26:42 -0600 Subject: [PATCH 0964/1197] Add starter entry in documentation --- docs/includes/pairlists.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index e6a9fc1a8..f8b33b27d 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -15,6 +15,7 @@ Inactive markets are always removed from the resulting pairlist. Explicitly blac * [`StaticPairList`](#static-pair-list) (default, if not configured differently) * [`VolumePairList`](#volume-pair-list) * [`AgeFilter`](#agefilter) +* [`PerformanceFilter`](#performancefilter) * [`PrecisionFilter`](#precisionfilter) * [`PriceFilter`](#pricefilter) * [`ShuffleFilter`](#shufflefilter) @@ -73,6 +74,10 @@ be caught out buying before the pair has finished dropping in price. This filter allows freqtrade to ignore pairs until they have been listed for at least `min_days_listed` days. +#### PerformanceFilter + +Lorem ipsum. + #### PrecisionFilter Filters low-value coins which would not allow setting stoplosses. From c34150552f348245cac68611a27d4b26eabc5f8a Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Fri, 27 Nov 2020 21:36:55 -0600 Subject: [PATCH 0965/1197] Revert unrelated change --- docs/telegram-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index f4bd0a12a..09cf21223 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -207,7 +207,7 @@ Return a summary of your profit/loss and performance. Note that for this to work, `forcebuy_enable` needs to be set to true. -[More details](configuration.md#understand-forcebuy_enable) +[More details](configuration.md/#understand-forcebuy_enable) ### /performance From 335735062835636d1bce627b9117030a5595f69c Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Fri, 27 Nov 2020 22:00:36 -0600 Subject: [PATCH 0966/1197] Revert unintended change --- docs/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index ec2d27174..9b15c9685 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -242,4 +242,4 @@ If this file is inexistent, then you're probably on a different version of MacOS ----- Now you have an environment ready, the next step is -[Bot Configuration](configuration.md). \ No newline at end of file +[Bot Configuration](configuration.md). From 380cca225239397f04d503fee36c189ee06014aa Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Fri, 27 Nov 2020 22:00:48 -0600 Subject: [PATCH 0967/1197] Remove unused imports --- freqtrade/pairlist/PerformanceFilter.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/freqtrade/pairlist/PerformanceFilter.py b/freqtrade/pairlist/PerformanceFilter.py index e689ba0bc..a2f2eb489 100644 --- a/freqtrade/pairlist/PerformanceFilter.py +++ b/freqtrade/pairlist/PerformanceFilter.py @@ -2,16 +2,13 @@ Performance pair list filter """ import logging -import random from typing import Any, Dict, List import pandas as pd -from pandas import DataFrame, Series from freqtrade.pairlist.IPairList import IPairList from freqtrade.persistence import Trade -from datetime import timedelta, datetime, timezone logger = logging.getLogger(__name__) From afb795b6f53de9ec00ccdbe2b4b128659831ad81 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Fri, 27 Nov 2020 22:08:23 -0600 Subject: [PATCH 0968/1197] Remove unnecessary test PerforamnceFilter doesn't use seeds, so no need to provide different ones. --- tests/pairlist/test_pairlist.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 2643a0bd8..64468fc05 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -302,14 +302,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "ShuffleFilter"}], "USDT", 3), # whitelist_result is integer -- check only length of randomized pairlist - # PerformanceFilter + # PerformanceFilter, unneeded seed provided ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "PerformanceFilter", "seed": 77}], "USDT", ['ADADOUBLE/USDT', 'ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT']), - # PerformanceFilter, other seed - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, - {"method": "PerformanceFilter", "seed": 42}], - "USDT", ['ADAHALF/USDT', 'NANO/USDT', 'ADADOUBLE/USDT', 'ETH/USDT']), # PerformanceFilter, no seed ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "PerformanceFilter"}], From 91b4c80d35611bbcf812a3a46dd5860e0ec949d2 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Fri, 27 Nov 2020 22:18:49 -0600 Subject: [PATCH 0969/1197] Remove unused parameters --- freqtrade/pairlist/PerformanceFilter.py | 2 -- tests/pairlist/test_pairlist.py | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/freqtrade/pairlist/PerformanceFilter.py b/freqtrade/pairlist/PerformanceFilter.py index a2f2eb489..d4bd5936d 100644 --- a/freqtrade/pairlist/PerformanceFilter.py +++ b/freqtrade/pairlist/PerformanceFilter.py @@ -43,7 +43,6 @@ class PerformanceFilter(IPairList): :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: new whitelist """ - # Get the trading performance for pairs from database perf = pd.DataFrame(Trade.get_overall_performance()) # update pairlist with values from performance dataframe @@ -53,6 +52,5 @@ class PerformanceFilter(IPairList): sorted_df = list_df.join(perf.set_index('pair'), on='pair')\ .fillna(0).sort_values(by=['profit', 'count'], ascending=False) pairlist = sorted_df['pair'].tolist() - return pairlist \ No newline at end of file diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 64468fc05..9814aea3e 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -425,7 +425,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t assert not log_has(logmsg, caplog) -def test_PrecisionFilter_error(mocker, whitelist_conf, tickers) -> None: +def test_PrecisionFilter_error(mocker, whitelist_conf) -> None: whitelist_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}] del whitelist_conf['stoploss'] @@ -498,7 +498,7 @@ def test__whitelist_for_active_markets(mocker, whitelist_conf, markets, pairlist @pytest.mark.parametrize("pairlist", AVAILABLE_PAIRLISTS) -def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, markets, pairlist, tickers): +def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, pairlist, tickers): whitelist_conf['pairlists'][0]['method'] = pairlist mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) @@ -514,7 +514,7 @@ def test__whitelist_for_active_markets_empty(mocker, whitelist_conf, markets, pa pairlist_handler._whitelist_for_active_markets(['ETH/BTC']) -def test_volumepairlist_invalid_sortvalue(mocker, markets, whitelist_conf): +def test_volumepairlist_invalid_sortvalue(mocker, whitelist_conf): whitelist_conf['pairlists'][0].update({"sort_key": "asdf"}) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) @@ -652,7 +652,7 @@ def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) -def test_pairlistmanager_no_pairlist(mocker, markets, whitelist_conf, caplog): +def test_pairlistmanager_no_pairlist(mocker, whitelist_conf): mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) whitelist_conf['pairlists'] = [] From 9538fa1d723cca40a862a265e24fc89e3f559c06 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 00:24:48 -0600 Subject: [PATCH 0970/1197] Tweak main parameterized block for PerformanceFilter Remove randomized exception that was geared toward ShuffleFilter. Remove case involvoing seed, also geared toward ShuffleFilter. Mock get_overall_performance(). --- tests/pairlist/test_pairlist.py | 46 ++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 9814aea3e..71d65d236 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -302,14 +302,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "ShuffleFilter"}], "USDT", 3), # whitelist_result is integer -- check only length of randomized pairlist - # PerformanceFilter, unneeded seed provided - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, - {"method": "PerformanceFilter", "seed": 77}], - "USDT", ['ADADOUBLE/USDT', 'ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT']), - # PerformanceFilter, no seed + # PerformanceFilter ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "PerformanceFilter"}], - "USDT", 3), # whitelist_result is integer -- check only length of randomized pairlist + "USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT', 'ADADOUBLE/USDT']), # AgeFilter only ([{"method": "AgeFilter", "min_days_listed": 2}], "BTC", 'filter_at_the_beginning'), # OperationalException expected @@ -381,6 +377,11 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), ) + # Provide for PerformanceFilter's dependency + mocker.patch.multiple('freqtrade.persistence.Trade', + get_overall_performance=MagicMock(return_value=[{'pair':'ETH/BTC','profit':5,'count':3}]), + ) + # Set whitelist_result to None if pairlist is invalid and should produce exception if whitelist_result == 'filter_at_the_beginning': with pytest.raises(OperationalException, @@ -394,7 +395,6 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t assert isinstance(whitelist, list) # Verify length of pairlist matches (used for ShuffleFilter without seed) - # TBD if this applies to PerformanceFilter if type(whitelist_result) is list: assert whitelist == whitelist_result else: @@ -544,7 +544,7 @@ def test_volumepairlist_caching(mocker, markets, whitelist_conf, tickers): assert freqtrade.pairlists._pairlist_handlers[0]._last_refresh == lrf -def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tickers, caplog): +def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tickers): default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, {'method': 'AgeFilter', 'min_days_listed': -1}] @@ -559,7 +559,7 @@ def test_agefilter_min_days_listed_too_small(mocker, default_conf, markets, tick get_patched_freqtradebot(mocker, default_conf) -def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tickers, caplog): +def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tickers): default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, {'method': 'AgeFilter', 'min_days_listed': 99999}] @@ -660,3 +660,31 @@ def test_pairlistmanager_no_pairlist(mocker, whitelist_conf): with pytest.raises(OperationalException, match=r"No Pairlist Handlers defined"): get_patched_freqtradebot(mocker, whitelist_conf) + + +@pytest.mark.parametrize("pairlists,base_currency,overall_performance,expected", [ + # Happy path, descening order, all values filled + ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}],'BTC',[{'pair':'ETH/BTC','profit':5,'count':3}, {'pair':'ETC/BTC','profit':4,'count':2}],['ETC/BTC']), +]) +def test_performance_filter(mocker, whitelist_conf, base_currency, pairlists, overall_performance, expected, tickers, markets, ohlcv_history_list): + whitelist_conf['pairlists'] = pairlists + whitelist_conf['stake_currency'] = base_currency + + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + + freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) + mocker.patch.multiple('freqtrade.exchange.Exchange', + get_tickers=tickers, + markets=PropertyMock(return_value=markets) + ) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), + ) + + mocker.patch.multiple('freqtrade.persistence.Trade', + get_overall_performance=MagicMock(return_value=overall_performance), + ) + freqtrade.pairlists.refresh_pairlist() + whitelist = freqtrade.pairlists.whitelist + assert whitelist == expected From 4600bb807c41782420806c62f819fa56b393c6b9 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 00:38:06 -0600 Subject: [PATCH 0971/1197] Existing tests pass. --- tests/pairlist/test_pairlist.py | 56 ++++++++++++++++----------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 71d65d236..86e4616e0 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -330,12 +330,12 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # ShuffleFilter only ([{"method": "ShuffleFilter", "seed": 42}], "BTC", 'filter_at_the_beginning'), # OperationalException expected - # PrecisionFilter after StaticPairList + # PerformanceFilter after StaticPairList ([{"method": "StaticPairList"}, - {"method": "PrecisionFilter", "seed": 42}], - "BTC", ['TKN/BTC', 'ETH/BTC', 'HOT/BTC']), - # PrecisionFilter only - ([{"method": "PrecisionFilter", "seed": 42}], + {"method": "PerformanceFilter", "seed": 42}], + "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), # Order matches order of appearance in whitelist_conf > exchange > pair_whitelist + # PerformanceFilter only + ([{"method": "PerformanceFilter", "seed": 42}], "BTC", 'filter_at_the_beginning'), # OperationalException expected # SpreadFilter after StaticPairList ([{"method": "StaticPairList"}, @@ -662,29 +662,29 @@ def test_pairlistmanager_no_pairlist(mocker, whitelist_conf): get_patched_freqtradebot(mocker, whitelist_conf) -@pytest.mark.parametrize("pairlists,base_currency,overall_performance,expected", [ - # Happy path, descening order, all values filled - ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}],'BTC',[{'pair':'ETH/BTC','profit':5,'count':3}, {'pair':'ETC/BTC','profit':4,'count':2}],['ETC/BTC']), -]) -def test_performance_filter(mocker, whitelist_conf, base_currency, pairlists, overall_performance, expected, tickers, markets, ohlcv_history_list): - whitelist_conf['pairlists'] = pairlists - whitelist_conf['stake_currency'] = base_currency +# @pytest.mark.parametrize("pairlists,base_currency,overall_performance,expected", [ +# # Happy path, descening order, all values filled +# ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}],'BTC',[{'pair':'ETH/BTC','profit':5,'count':3}, {'pair':'ETC/BTC','profit':4,'count':2}],['ETC/BTC']), +# ]) +# def test_performance_filter(mocker, whitelist_conf, base_currency, pairlists, overall_performance, expected, tickers, markets, ohlcv_history_list): +# whitelist_conf['pairlists'] = pairlists +# whitelist_conf['stake_currency'] = base_currency - mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) +# mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) - freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) - mocker.patch.multiple('freqtrade.exchange.Exchange', - get_tickers=tickers, - markets=PropertyMock(return_value=markets) - ) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), - ) +# freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) +# mocker.patch.multiple('freqtrade.exchange.Exchange', +# get_tickers=tickers, +# markets=PropertyMock(return_value=markets) +# ) +# mocker.patch.multiple( +# 'freqtrade.exchange.Exchange', +# get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), +# ) - mocker.patch.multiple('freqtrade.persistence.Trade', - get_overall_performance=MagicMock(return_value=overall_performance), - ) - freqtrade.pairlists.refresh_pairlist() - whitelist = freqtrade.pairlists.whitelist - assert whitelist == expected +# mocker.patch.multiple('freqtrade.persistence.Trade', +# get_overall_performance=MagicMock(return_value=overall_performance), +# ) +# freqtrade.pairlists.refresh_pairlist() +# whitelist = freqtrade.pairlists.whitelist +# assert whitelist == expected From 26855800a3b0229c1ea3a3c0f9d97268e9de57f7 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 00:39:18 -0600 Subject: [PATCH 0972/1197] Remove unused seed --- tests/pairlist/test_pairlist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 86e4616e0..ae80a3975 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -332,10 +332,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): "BTC", 'filter_at_the_beginning'), # OperationalException expected # PerformanceFilter after StaticPairList ([{"method": "StaticPairList"}, - {"method": "PerformanceFilter", "seed": 42}], + {"method": "PerformanceFilter"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), # Order matches order of appearance in whitelist_conf > exchange > pair_whitelist # PerformanceFilter only - ([{"method": "PerformanceFilter", "seed": 42}], + ([{"method": "PerformanceFilter"}], "BTC", 'filter_at_the_beginning'), # OperationalException expected # SpreadFilter after StaticPairList ([{"method": "StaticPairList"}, From 662ec3207310dd0c269e1a5bc4554eefec1891a6 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 01:15:36 -0600 Subject: [PATCH 0973/1197] Add test cases --- freqtrade/pairlist/PerformanceFilter.py | 4 +- tests/pairlist/test_pairlist.py | 65 ++++++++++++++++--------- 2 files changed, 44 insertions(+), 25 deletions(-) diff --git a/freqtrade/pairlist/PerformanceFilter.py b/freqtrade/pairlist/PerformanceFilter.py index d4bd5936d..b2889dc6b 100644 --- a/freqtrade/pairlist/PerformanceFilter.py +++ b/freqtrade/pairlist/PerformanceFilter.py @@ -45,10 +45,10 @@ class PerformanceFilter(IPairList): """ # Get the trading performance for pairs from database perf = pd.DataFrame(Trade.get_overall_performance()) - # update pairlist with values from performance dataframe + # get pairlist from performance dataframe values + list_df = pd.DataFrame({'pair':pairlist}) # set initial value for pairs with no trades to 0 # and sort the list using performance and count - list_df = pd.DataFrame({'pair':pairlist}) sorted_df = list_df.join(perf.set_index('pair'), on='pair')\ .fillna(0).sort_values(by=['profit', 'count'], ascending=False) pairlist = sorted_df['pair'].tolist() diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index ae80a3975..a99651727 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -662,29 +662,48 @@ def test_pairlistmanager_no_pairlist(mocker, whitelist_conf): get_patched_freqtradebot(mocker, whitelist_conf) -# @pytest.mark.parametrize("pairlists,base_currency,overall_performance,expected", [ -# # Happy path, descening order, all values filled -# ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}],'BTC',[{'pair':'ETH/BTC','profit':5,'count':3}, {'pair':'ETC/BTC','profit':4,'count':2}],['ETC/BTC']), -# ]) -# def test_performance_filter(mocker, whitelist_conf, base_currency, pairlists, overall_performance, expected, tickers, markets, ohlcv_history_list): -# whitelist_conf['pairlists'] = pairlists -# whitelist_conf['stake_currency'] = base_currency +@pytest.mark.parametrize("pairlists,pair_allowlist,overall_performance,allowlist_result", [ + # Happy path, descending order, all values filled + ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}], + ['ETH/BTC','TKN/BTC'], + [{'pair':'TKN/BTC','profit':5,'count':3}, {'pair':'ETH/BTC','profit':4,'count':2}], + ['TKN/BTC','ETH/BTC']), + # Performance data outside allow list ignored + ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}], + ['ETH/BTC','TKN/BTC'], + [{'pair':'OTHER/BTC','profit':5,'count':3}, {'pair':'ETH/BTC','profit':4,'count':2}], + ['ETH/BTC','TKN/BTC']), + # Partial performance data missing and sorted between positive and negative profit + ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}], + ['ETH/BTC','TKN/BTC','LTC/BTC'], + [{'pair':'ETH/BTC','profit':-5,'count':100}, {'pair':'TKN/BTC','profit':4,'count':2}], + ['TKN/BTC','LTC/BTC','ETH/BTC']), + # Tie in performance data broken by count + ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}], + ['ETH/BTC','TKN/BTC','LTC/BTC'], + [{'pair':'LTC/BTC','profit':-5,'count':101}, {'pair':'TKN/BTC','profit':-5,'count':2}, {'pair':'ETH/BTC','profit':-5,'count':100}, ], + ['LTC/BTC','ETH/BTC','TKN/BTC']), +]) +def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, overall_performance, allowlist_result, tickers, markets, ohlcv_history_list): + allowlist_conf = whitelist_conf + allowlist_conf['pairlists'] = pairlists + allowlist_conf['exchange']['pair_whitelist'] = pair_allowlist -# mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) -# freqtrade = get_patched_freqtradebot(mocker, whitelist_conf) -# mocker.patch.multiple('freqtrade.exchange.Exchange', -# get_tickers=tickers, -# markets=PropertyMock(return_value=markets) -# ) -# mocker.patch.multiple( -# 'freqtrade.exchange.Exchange', -# get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), -# ) + freqtrade = get_patched_freqtradebot(mocker, allowlist_conf) + mocker.patch.multiple('freqtrade.exchange.Exchange', + get_tickers=tickers, + markets=PropertyMock(return_value=markets) + ) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), + ) -# mocker.patch.multiple('freqtrade.persistence.Trade', -# get_overall_performance=MagicMock(return_value=overall_performance), -# ) -# freqtrade.pairlists.refresh_pairlist() -# whitelist = freqtrade.pairlists.whitelist -# assert whitelist == expected + mocker.patch.multiple('freqtrade.persistence.Trade', + get_overall_performance=MagicMock(return_value=overall_performance), + ) + freqtrade.pairlists.refresh_pairlist() + allowlist = freqtrade.pairlists.whitelist + assert allowlist == allowlist_result From dbd50fdff64e5e72a052db687082b26794790f7b Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 01:22:03 -0600 Subject: [PATCH 0974/1197] Document filter. --- docs/includes/pairlists.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index f8b33b27d..50ef52653 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -76,7 +76,12 @@ This filter allows freqtrade to ignore pairs until they have been listed for at #### PerformanceFilter -Lorem ipsum. +Sorts pairs by performance, as follows: +1. Positive performance. +2. No closed trades yet. +3. Negative performance. + +Trade count is used as a tie breaker. #### PrecisionFilter From 966c6b308f182392c85a74568350de4ac5cd9ced Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 01:34:18 -0600 Subject: [PATCH 0975/1197] Satisfy linter. --- freqtrade/pairlist/PerformanceFilter.py | 6 +-- tests/pairlist/test_pairlist.py | 51 +++++++++++++------------ 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/freqtrade/pairlist/PerformanceFilter.py b/freqtrade/pairlist/PerformanceFilter.py index b2889dc6b..099b8d271 100644 --- a/freqtrade/pairlist/PerformanceFilter.py +++ b/freqtrade/pairlist/PerformanceFilter.py @@ -12,6 +12,7 @@ from freqtrade.persistence import Trade logger = logging.getLogger(__name__) + class PerformanceFilter(IPairList): def __init__(self, exchange, pairlistmanager, @@ -19,7 +20,6 @@ class PerformanceFilter(IPairList): pairlist_pos: int) -> None: super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) - @property def needstickers(self) -> bool: """ @@ -46,11 +46,11 @@ class PerformanceFilter(IPairList): # Get the trading performance for pairs from database perf = pd.DataFrame(Trade.get_overall_performance()) # get pairlist from performance dataframe values - list_df = pd.DataFrame({'pair':pairlist}) + list_df = pd.DataFrame({'pair': pairlist}) # set initial value for pairs with no trades to 0 # and sort the list using performance and count sorted_df = list_df.join(perf.set_index('pair'), on='pair')\ .fillna(0).sort_values(by=['profit', 'count'], ascending=False) pairlist = sorted_df['pair'].tolist() - return pairlist \ No newline at end of file + return pairlist diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 65a0fa835..9e2bab12c 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -729,27 +729,32 @@ def test_pairlistmanager_no_pairlist(mocker, whitelist_conf): @pytest.mark.parametrize("pairlists,pair_allowlist,overall_performance,allowlist_result", [ # Happy path, descending order, all values filled - ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}], - ['ETH/BTC','TKN/BTC'], - [{'pair':'TKN/BTC','profit':5,'count':3}, {'pair':'ETH/BTC','profit':4,'count':2}], - ['TKN/BTC','ETH/BTC']), + ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ['ETH/BTC', 'TKN/BTC'], + [{'pair': 'TKN/BTC', 'profit': 5, 'count': 3}, {'pair': 'ETH/BTC', 'profit': 4, 'count': 2}], + ['TKN/BTC', 'ETH/BTC']), # Performance data outside allow list ignored - ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}], - ['ETH/BTC','TKN/BTC'], - [{'pair':'OTHER/BTC','profit':5,'count':3}, {'pair':'ETH/BTC','profit':4,'count':2}], - ['ETH/BTC','TKN/BTC']), + ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ['ETH/BTC', 'TKN/BTC'], + [{'pair': 'OTHER/BTC', 'profit': 5, 'count': 3}, + {'pair': 'ETH/BTC', 'profit': 4, 'count': 2}], + ['ETH/BTC', 'TKN/BTC']), # Partial performance data missing and sorted between positive and negative profit - ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}], - ['ETH/BTC','TKN/BTC','LTC/BTC'], - [{'pair':'ETH/BTC','profit':-5,'count':100}, {'pair':'TKN/BTC','profit':4,'count':2}], - ['TKN/BTC','LTC/BTC','ETH/BTC']), + ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], + [{'pair': 'ETH/BTC', 'profit': -5, 'count': 100}, + {'pair': 'TKN/BTC', 'profit': 4, 'count': 2}], + ['TKN/BTC', 'LTC/BTC', 'ETH/BTC']), # Tie in performance data broken by count - ([{"method": "StaticPairList"},{"method": "PerformanceFilter"}], - ['ETH/BTC','TKN/BTC','LTC/BTC'], - [{'pair':'LTC/BTC','profit':-5,'count':101}, {'pair':'TKN/BTC','profit':-5,'count':2}, {'pair':'ETH/BTC','profit':-5,'count':100}, ], - ['LTC/BTC','ETH/BTC','TKN/BTC']), + ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], + [{'pair': 'LTC/BTC', 'profit': -5, 'count': 101}, + {'pair': 'TKN/BTC', 'profit': -5, 'count': 2}, + {'pair': 'ETH/BTC', 'profit': -5, 'count': 100}], + ['LTC/BTC', 'ETH/BTC', 'TKN/BTC']), ]) -def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, overall_performance, allowlist_result, tickers, markets, ohlcv_history_list): +def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, overall_performance, + allowlist_result, tickers, markets, ohlcv_history_list): allowlist_conf = whitelist_conf allowlist_conf['pairlists'] = pairlists allowlist_conf['exchange']['pair_whitelist'] = pair_allowlist @@ -761,14 +766,12 @@ def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, o get_tickers=tickers, markets=PropertyMock(return_value=markets) ) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), - ) - + mocker.patch.multiple('freqtrade.exchange.Exchange', + get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), + ) mocker.patch.multiple('freqtrade.persistence.Trade', - get_overall_performance=MagicMock(return_value=overall_performance), - ) + get_overall_performance=MagicMock(return_value=overall_performance), + ) freqtrade.pairlists.refresh_pairlist() allowlist = freqtrade.pairlists.whitelist assert allowlist == allowlist_result From fefa500963d7d4a8b88d740783190755605942ac Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 01:34:40 -0600 Subject: [PATCH 0976/1197] More lint --- tests/pairlist/test_pairlist.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 9e2bab12c..5e9847e3d 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -333,7 +333,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # PerformanceFilter after StaticPairList ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], - "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), # Order matches order of appearance in whitelist_conf > exchange > pair_whitelist + "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), # PerformanceFilter only ([{"method": "PerformanceFilter"}], "BTC", 'filter_at_the_beginning'), # OperationalException expected @@ -383,7 +383,8 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t # Provide for PerformanceFilter's dependency mocker.patch.multiple('freqtrade.persistence.Trade', - get_overall_performance=MagicMock(return_value=[{'pair':'ETH/BTC','profit':5,'count':3}]), + get_overall_performance=MagicMock( + return_value=[{'pair': 'ETH/BTC', 'profit': 5, 'count' :3}]), ) # Set whitelist_result to None if pairlist is invalid and should produce exception From ecce5265f5e5fa153261095c563e505c243fc0a7 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 01:43:19 -0600 Subject: [PATCH 0977/1197] Linting --- tests/pairlist/test_pairlist.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 5e9847e3d..a4df031c9 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -383,8 +383,8 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t # Provide for PerformanceFilter's dependency mocker.patch.multiple('freqtrade.persistence.Trade', - get_overall_performance=MagicMock( - return_value=[{'pair': 'ETH/BTC', 'profit': 5, 'count' :3}]), + get_overall_performance=MagicMock(return_value=\ + [{'pair': 'ETH/BTC', 'profit': 5, 'count': 3}]), ) # Set whitelist_result to None if pairlist is invalid and should produce exception @@ -737,7 +737,7 @@ def test_pairlistmanager_no_pairlist(mocker, whitelist_conf): # Performance data outside allow list ignored ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], ['ETH/BTC', 'TKN/BTC'], - [{'pair': 'OTHER/BTC', 'profit': 5, 'count': 3}, + [{'pair': 'OTHER/BTC', 'profit': 5, 'count': 3}, {'pair': 'ETH/BTC', 'profit': 4, 'count': 2}], ['ETH/BTC', 'TKN/BTC']), # Partial performance data missing and sorted between positive and negative profit @@ -769,7 +769,7 @@ def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, o ) mocker.patch.multiple('freqtrade.exchange.Exchange', get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), - ) + ) mocker.patch.multiple('freqtrade.persistence.Trade', get_overall_performance=MagicMock(return_value=overall_performance), ) From f448564073b2d7c487ccb2d75e749bfe949bd547 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 01:49:46 -0600 Subject: [PATCH 0978/1197] Lint --- tests/pairlist/test_pairlist.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index a4df031c9..d40cece41 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -383,9 +383,9 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t # Provide for PerformanceFilter's dependency mocker.patch.multiple('freqtrade.persistence.Trade', - get_overall_performance=MagicMock(return_value=\ - [{'pair': 'ETH/BTC', 'profit': 5, 'count': 3}]), - ) + get_overall_performance=MagicMock( + return_value=[{'pair': 'ETH/BTC', 'profit': 5, 'count': 3}]), + ) # Set whitelist_result to None if pairlist is invalid and should produce exception if whitelist_result == 'filter_at_the_beginning': From 37d2e476df19bfafa86c6405404d0bc578f270d9 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 01:59:30 -0600 Subject: [PATCH 0979/1197] isort imports --- freqtrade/pairlist/PerformanceFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/pairlist/PerformanceFilter.py b/freqtrade/pairlist/PerformanceFilter.py index 099b8d271..bd56a4607 100644 --- a/freqtrade/pairlist/PerformanceFilter.py +++ b/freqtrade/pairlist/PerformanceFilter.py @@ -7,9 +7,9 @@ from typing import Any, Dict, List import pandas as pd from freqtrade.pairlist.IPairList import IPairList - from freqtrade.persistence import Trade + logger = logging.getLogger(__name__) From 4cb331b5ad644f9d1aeee2909e158de25078a913 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Nov 2020 10:24:44 +0100 Subject: [PATCH 0980/1197] Remove non-needed parameters from tests --- tests/test_configuration.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 6c895a00b..167215f29 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1101,7 +1101,7 @@ def test_process_temporary_deprecated_settings(mocker, default_conf, setting, ca ("experimental", "sell_profit_only", True), ("experimental", "ignore_roi_if_buy_signal", True), ]) -def test_process_removed_settings(mocker, default_conf, setting, caplog): +def test_process_removed_settings(mocker, default_conf, setting): patched_configuration_load_config_file(mocker, default_conf) # Create sections for new and deprecated settings @@ -1115,7 +1115,8 @@ def test_process_removed_settings(mocker, default_conf, setting, caplog): match=r'Setting .* has been moved'): process_temporary_deprecated_settings(default_conf) -def test_process_deprecated_setting_edge(mocker, edge_conf, caplog): + +def test_process_deprecated_setting_edge(mocker, edge_conf): patched_configuration_load_config_file(mocker, edge_conf) edge_conf.update({'edge': { 'enabled': True, From a47d8dbe56c1b3813a15b125726f7dc66a35666b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Nov 2020 11:31:28 +0100 Subject: [PATCH 0981/1197] Small refactor, avoiding duplicate calculation of profits --- freqtrade/optimize/optimize_reports.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index fc04cbd93..6aef031d3 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -58,16 +58,19 @@ def _generate_result_line(result: DataFrame, max_open_trades: int, first_column: """ Generate one result dict, with "first_column" as key. """ + profit_sum = result['profit_percent'].sum() + profit_total = profit_sum / max_open_trades + return { 'key': first_column, 'trades': len(result), 'profit_mean': result['profit_percent'].mean() if len(result) > 0 else 0.0, 'profit_mean_pct': result['profit_percent'].mean() * 100.0 if len(result) > 0 else 0.0, - 'profit_sum': result['profit_percent'].sum(), - 'profit_sum_pct': result['profit_percent'].sum() * 100.0, + 'profit_sum': profit_sum, + 'profit_sum_pct': round(profit_sum * 100.0, 2), 'profit_total_abs': result['profit_abs'].sum(), - 'profit_total': result['profit_percent'].sum() / max_open_trades, - 'profit_total_pct': result['profit_percent'].sum() * 100.0 / max_open_trades, + 'profit_total': profit_total, + 'profit_total_pct': round(profit_total * 100.0, 2), 'duration_avg': str(timedelta( minutes=round(result['trade_duration'].mean())) ) if not result.empty else '0:00', @@ -122,8 +125,8 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List result = results.loc[results['sell_reason'] == reason] profit_mean = result['profit_percent'].mean() - profit_sum = result["profit_percent"].sum() - profit_percent_tot = result['profit_percent'].sum() / max_open_trades + profit_sum = result['profit_percent'].sum() + profit_total = profit_sum / max_open_trades tabular_data.append( { @@ -137,8 +140,8 @@ def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List 'profit_sum': profit_sum, 'profit_sum_pct': round(profit_sum * 100, 2), 'profit_total_abs': result['profit_abs'].sum(), - 'profit_total': profit_percent_tot, - 'profit_total_pct': round(profit_percent_tot * 100, 2), + 'profit_total': profit_total, + 'profit_total_pct': round(profit_total * 100, 2), } ) return tabular_data From ff286bd80cd82fa079ec51cc0592ba2f986c2ceb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Nov 2020 16:32:44 +0100 Subject: [PATCH 0982/1197] Slightly clarify hyperopt docs --- docs/hyperopt.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index fc7a0dd93..c42889831 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -173,6 +173,11 @@ one we call `trigger` and use it to decide which buy trigger we want to use. So let's write the buy strategy using these values: ```python + @staticmethod + def buy_strategy_generator(params: Dict[str, Any]) -> Callable: + """ + Define the buy strategy parameters to be used by Hyperopt. + """ def populate_buy_trend(dataframe: DataFrame) -> DataFrame: conditions = [] # GUARDS AND TRENDS From 56529180eb6e73dfcac8ebcab5f35e4c731e2a20 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Nov 2020 16:42:08 +0100 Subject: [PATCH 0983/1197] Further improve hyperopt docs --- docs/hyperopt.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index c42889831..f88d9cd4f 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -64,9 +64,9 @@ Depending on the space you want to optimize, only some of the below are required Optional in hyperopt - can also be loaded from a strategy (recommended): -* copy `populate_indicators` from your strategy - otherwise default-strategy will be used -* copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used -* copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used +* `populate_indicators` - fallback to create indicators +* `populate_buy_trend` - fallback if not optimizing for buy space. should come from strategy +* `populate_sell_trend` - fallback if not optimizing for sell space. should come from strategy !!! Note You always have to provide a strategy to Hyperopt, even if your custom Hyperopt class contains all methods. @@ -104,7 +104,7 @@ This command will create a new hyperopt file from a template, allowing you to ge There are two places you need to change in your hyperopt file to add a new buy hyperopt for testing: * Inside `indicator_space()` - the parameters hyperopt shall be optimizing. -* Inside `populate_buy_trend()` - applying the parameters. +* Within `buy_strategy_generator()` - populate the nested `populate_buy_trend()` to apply the parameters. There you have two different types of indicators: 1. `guards` and 2. `triggers`. @@ -128,7 +128,7 @@ Similar to the buy-signal above, sell-signals can also be optimized. Place the corresponding settings into the following methods * Inside `sell_indicator_space()` - the parameters hyperopt shall be optimizing. -* Inside `populate_sell_trend()` - applying the parameters. +* Within `sell_strategy_generator()` - populate the nested method `populate_sell_trend()` to apply the parameters. The configuration and rules are the same than for buy signals. To avoid naming collisions in the search-space, please prefix all sell-spaces with `sell-`. From e1d42ba78ce670a662420b7bb19337c57cc335ca Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 09:44:01 -0600 Subject: [PATCH 0984/1197] Alphabetize --- freqtrade/constants.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 20cc70d2e..9d0078d21 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -24,9 +24,9 @@ HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss', 'SharpeHyperOptLoss', 'SharpeHyperOptLossDaily', 'SortinoHyperOptLoss', 'SortinoHyperOptLossDaily'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', - 'AgeFilter', 'PrecisionFilter', 'PriceFilter', - 'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter', - 'PerformanceFilter'] + 'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', + 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', + 'SpreadFilter'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S' From 03c5714399d57af12cb18ffa7f4b6937ed3cd6b4 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 09:45:17 -0600 Subject: [PATCH 0985/1197] Use explicit merge without depending on library detail. Add no trades case. --- freqtrade/pairlist/PerformanceFilter.py | 9 +++++++-- tests/pairlist/test_pairlist.py | 8 +++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/freqtrade/pairlist/PerformanceFilter.py b/freqtrade/pairlist/PerformanceFilter.py index bd56a4607..2d360a346 100644 --- a/freqtrade/pairlist/PerformanceFilter.py +++ b/freqtrade/pairlist/PerformanceFilter.py @@ -44,12 +44,17 @@ class PerformanceFilter(IPairList): :return: new whitelist """ # Get the trading performance for pairs from database - perf = pd.DataFrame(Trade.get_overall_performance()) + performance = pd.DataFrame(Trade.get_overall_performance()) + + # Skip performance-based sorting if no performance data is available + if len(performance) == 0: + return pairlist + # get pairlist from performance dataframe values list_df = pd.DataFrame({'pair': pairlist}) # set initial value for pairs with no trades to 0 # and sort the list using performance and count - sorted_df = list_df.join(perf.set_index('pair'), on='pair')\ + sorted_df = list_df.merge(performance, on='pair', how='left')\ .fillna(0).sort_values(by=['profit', 'count'], ascending=False) pairlist = sorted_df['pair'].tolist() diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index d40cece41..c62ec81f3 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -383,8 +383,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t # Provide for PerformanceFilter's dependency mocker.patch.multiple('freqtrade.persistence.Trade', - get_overall_performance=MagicMock( - return_value=[{'pair': 'ETH/BTC', 'profit': 5, 'count': 3}]), + get_overall_performance=MagicMock(return_value=[]) ) # Set whitelist_result to None if pairlist is invalid and should produce exception @@ -729,7 +728,10 @@ def test_pairlistmanager_no_pairlist(mocker, whitelist_conf): @pytest.mark.parametrize("pairlists,pair_allowlist,overall_performance,allowlist_result", [ - # Happy path, descending order, all values filled + # No trades yet + ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], [], ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']), + # Happy path: Descending order, all values filled ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], ['ETH/BTC', 'TKN/BTC'], [{'pair': 'TKN/BTC', 'profit': 5, 'count': 3}, {'pair': 'ETH/BTC', 'profit': 4, 'count': 2}], From a00f852cf99106b314a05d425b7ab6fcad9d158d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Nov 2020 16:56:08 +0100 Subject: [PATCH 0986/1197] Add best / worst pair to summary statistics --- docs/backtesting.md | 12 ++++++------ freqtrade/optimize/optimize_reports.py | 15 +++++++++++---- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 277b11083..01624e5c2 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -165,8 +165,8 @@ A backtesting result will look like that: | Max open trades | 3 | | | | | Total trades | 429 | -| First trade | 2019-01-01 18:30:00 | -| First trade Pair | EOS/USDT | +| Best Pair | LSK/BTC - 26.26% | +| Worst Pair | ZEC/BTC - -10.18% | | Total Profit % | 152.41% | | Trades per day | 3.575 | | Best day | 25.27% | @@ -238,8 +238,8 @@ It contains some useful key metrics about performance of your strategy on backte | Max open trades | 3 | | | | | Total trades | 429 | -| First trade | 2019-01-01 18:30:00 | -| First trade Pair | EOS/USDT | +| Best Pair | LSK/BTC - 26.26% | +| Worst Pair | ZEC/BTC - -10.18% | | Total Profit % | 152.41% | | Trades per day | 3.575 | | Best day | 25.27% | @@ -258,8 +258,8 @@ It contains some useful key metrics about performance of your strategy on backte - `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option). - `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - to clearly see settings for this. - `Total trades`: Identical to the total trades of the backtest output table. -- `First trade`: First trade entered. -- `First trade pair`: Which pair was part of the first trade. +- `Best Pair`: Which pair performed best, and it's corresponding `Cum Profit %`. +- `Worst pair`: Which pair performed worst and it's corresponding `Cum Profit %`. - `Total Profit %`: Total profit per stake amount. Aligned to the TOTAL column of the first table. - `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). - `Best day` / `Worst day`: Best and worst day based on daily profit. diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 6aef031d3..589e0ba1c 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -256,13 +256,18 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], results=results.loc[results['open_at_end']], skip_nan=True) daily_stats = generate_daily_stats(results) - + best_pair = max([pair for pair in pair_results if pair['key'] != 'TOTAL'], + key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None + worst_pair = min([pair for pair in pair_results if pair['key'] != 'TOTAL'], + key=lambda x: x['profit_sum']) if len(pair_results) > 1 else None results['open_timestamp'] = results['open_date'].astype(int64) // 1e6 results['close_timestamp'] = results['close_date'].astype(int64) // 1e6 backtest_days = (max_date - min_date).days strat_stats = { 'trades': results.to_dict(orient='records'), + 'best_pair': best_pair, + 'worst_pair': worst_pair, 'results_per_pair': pair_results, 'sell_reason_summary': sell_reason_stats, 'left_open_trades': left_open_results, @@ -395,17 +400,19 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: def text_table_add_metrics(strat_results: Dict) -> str: if len(strat_results['trades']) > 0: - min_trade = min(strat_results['trades'], key=lambda x: x['open_date']) metrics = [ ('Backtesting from', strat_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)), ('Backtesting to', strat_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)), ('Max open trades', strat_results['max_open_trades']), ('', ''), # Empty line to improve readability ('Total trades', strat_results['total_trades']), - ('First trade', min_trade['open_date'].strftime(DATETIME_PRINT_FORMAT)), - ('First trade Pair', min_trade['pair']), ('Total Profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"), ('Trades per day', strat_results['trades_per_day']), + ('', ''), # Empty line to improve readability + ('Best Pair', f"{strat_results['best_pair']['key']} - " + f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"), + ('Worst Pair', f"{strat_results['worst_pair']['key']} - " + f"{round(strat_results['worst_pair']['profit_sum_pct'], 2)}%"), ('Best day', f"{round(strat_results['backtest_best_day'] * 100, 2)}%"), ('Worst day', f"{round(strat_results['backtest_worst_day'] * 100, 2)}%"), ('Days win/draw/lose', f"{strat_results['winning_days']} / " From 5d3f59df90d97f1d96e7b4e9734dcc722b6bc7a4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Nov 2020 17:45:56 +0100 Subject: [PATCH 0987/1197] Add best / worst trade --- docs/backtesting.md | 18 ++++++++++++------ freqtrade/optimize/optimize_reports.py | 5 +++++ 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 01624e5c2..42de9bdc3 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -165,10 +165,13 @@ A backtesting result will look like that: | Max open trades | 3 | | | | | Total trades | 429 | -| Best Pair | LSK/BTC - 26.26% | -| Worst Pair | ZEC/BTC - -10.18% | | Total Profit % | 152.41% | | Trades per day | 3.575 | +| | | +| Best Pair | LSK/BTC - 26.26% | +| Worst Pair | ZEC/BTC - -10.18% | +| Best Trade | LSK/BTC - 4.25% | +| Worst Trade | ZEC/BTC - -10.25% | | Best day | 25.27% | | Worst day | -30.67% | | Avg. Duration Winners | 4:23:00 | @@ -238,10 +241,13 @@ It contains some useful key metrics about performance of your strategy on backte | Max open trades | 3 | | | | | Total trades | 429 | -| Best Pair | LSK/BTC - 26.26% | -| Worst Pair | ZEC/BTC - -10.18% | | Total Profit % | 152.41% | | Trades per day | 3.575 | +| | | +| Best Pair | LSK/BTC - 26.26% | +| Worst Pair | ZEC/BTC - -10.18% | +| Best Trade | LSK/BTC - 4.25% | +| Worst Trade | ZEC/BTC - -10.25% | | Best day | 25.27% | | Worst day | -30.67% | | Avg. Duration Winners | 4:23:00 | @@ -258,10 +264,10 @@ It contains some useful key metrics about performance of your strategy on backte - `Backtesting from` / `Backtesting to`: Backtesting range (usually defined with the `--timerange` option). - `Max open trades`: Setting of `max_open_trades` (or `--max-open-trades`) - to clearly see settings for this. - `Total trades`: Identical to the total trades of the backtest output table. -- `Best Pair`: Which pair performed best, and it's corresponding `Cum Profit %`. -- `Worst pair`: Which pair performed worst and it's corresponding `Cum Profit %`. - `Total Profit %`: Total profit per stake amount. Aligned to the TOTAL column of the first table. - `Trades per day`: Total trades divided by the backtesting duration in days (this will give you information about how many trades to expect from the strategy). +- `Best Pair` / `Worst Pair`: Best and worst performing pair, and it's corresponding `Cum Profit %`. +- `Best Trade` / `Worst Trade`: Biggest winning trade and biggest losing trade - `Best day` / `Worst day`: Best and worst day based on daily profit. - `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades. - `Max Drawdown`: Maximum drawdown experienced. For example, the value of 50% means that from highest to subsequent lowest point, a 50% drop was experienced). diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 589e0ba1c..3e44a6067 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -400,6 +400,8 @@ def text_table_strategy(strategy_results, stake_currency: str) -> str: def text_table_add_metrics(strat_results: Dict) -> str: if len(strat_results['trades']) > 0: + best_trade = max(strat_results['trades'], key=lambda x: x['profit_percent']) + worst_trade = min(strat_results['trades'], key=lambda x: x['profit_percent']) metrics = [ ('Backtesting from', strat_results['backtest_start'].strftime(DATETIME_PRINT_FORMAT)), ('Backtesting to', strat_results['backtest_end'].strftime(DATETIME_PRINT_FORMAT)), @@ -413,6 +415,9 @@ def text_table_add_metrics(strat_results: Dict) -> str: f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"), ('Worst Pair', f"{strat_results['worst_pair']['key']} - " f"{round(strat_results['worst_pair']['profit_sum_pct'], 2)}%"), + ('Best trade', f"{best_trade['pair']} {round(best_trade['profit_percent'] * 100, 2)}%"), + ('Worst trade', f"{worst_trade['pair']} {round(worst_trade['profit_percent'] * 100, 2)}%"), + ('Best day', f"{round(strat_results['backtest_best_day'] * 100, 2)}%"), ('Worst day', f"{round(strat_results['backtest_worst_day'] * 100, 2)}%"), ('Days win/draw/lose', f"{strat_results['winning_days']} / " From e40d97e05e765c5946208b47b5519c47b79aa135 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Nov 2020 17:52:29 +0100 Subject: [PATCH 0988/1197] Small formatting improvements --- docs/backtesting.md | 16 ++++++++-------- freqtrade/optimize/optimize_reports.py | 7 ++++--- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 42de9bdc3..c841899a7 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -168,10 +168,10 @@ A backtesting result will look like that: | Total Profit % | 152.41% | | Trades per day | 3.575 | | | | -| Best Pair | LSK/BTC - 26.26% | -| Worst Pair | ZEC/BTC - -10.18% | -| Best Trade | LSK/BTC - 4.25% | -| Worst Trade | ZEC/BTC - -10.25% | +| Best Pair | LSK/BTC 26.26% | +| Worst Pair | ZEC/BTC -10.18% | +| Best Trade | LSK/BTC 4.25% | +| Worst Trade | ZEC/BTC -10.25% | | Best day | 25.27% | | Worst day | -30.67% | | Avg. Duration Winners | 4:23:00 | @@ -244,10 +244,10 @@ It contains some useful key metrics about performance of your strategy on backte | Total Profit % | 152.41% | | Trades per day | 3.575 | | | | -| Best Pair | LSK/BTC - 26.26% | -| Worst Pair | ZEC/BTC - -10.18% | -| Best Trade | LSK/BTC - 4.25% | -| Worst Trade | ZEC/BTC - -10.25% | +| Best Pair | LSK/BTC 26.26% | +| Worst Pair | ZEC/BTC -10.18% | +| Best Trade | LSK/BTC 4.25% | +| Worst Trade | ZEC/BTC -10.25% | | Best day | 25.27% | | Worst day | -30.67% | | Avg. Duration Winners | 4:23:00 | diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 3e44a6067..b3799856e 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -411,12 +411,13 @@ def text_table_add_metrics(strat_results: Dict) -> str: ('Total Profit %', f"{round(strat_results['profit_total'] * 100, 2)}%"), ('Trades per day', strat_results['trades_per_day']), ('', ''), # Empty line to improve readability - ('Best Pair', f"{strat_results['best_pair']['key']} - " + ('Best Pair', f"{strat_results['best_pair']['key']} " f"{round(strat_results['best_pair']['profit_sum_pct'], 2)}%"), - ('Worst Pair', f"{strat_results['worst_pair']['key']} - " + ('Worst Pair', f"{strat_results['worst_pair']['key']} " f"{round(strat_results['worst_pair']['profit_sum_pct'], 2)}%"), ('Best trade', f"{best_trade['pair']} {round(best_trade['profit_percent'] * 100, 2)}%"), - ('Worst trade', f"{worst_trade['pair']} {round(worst_trade['profit_percent'] * 100, 2)}%"), + ('Worst trade', f"{worst_trade['pair']} " + f"{round(worst_trade['profit_percent'] * 100, 2)}%"), ('Best day', f"{round(strat_results['backtest_best_day'] * 100, 2)}%"), ('Worst day', f"{round(strat_results['backtest_worst_day'] * 100, 2)}%"), From 6a74c57c3d5c4ab422ced69643459c70269981ab Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 11:33:25 -0600 Subject: [PATCH 0989/1197] Pair name-based sorting. Attempt at more rational string sorting. Change test to show not working as expected. --- freqtrade/pairlist/PerformanceFilter.py | 2 +- tests/pairlist/test_pairlist.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/pairlist/PerformanceFilter.py b/freqtrade/pairlist/PerformanceFilter.py index 2d360a346..5e1ec3c66 100644 --- a/freqtrade/pairlist/PerformanceFilter.py +++ b/freqtrade/pairlist/PerformanceFilter.py @@ -55,7 +55,7 @@ class PerformanceFilter(IPairList): # set initial value for pairs with no trades to 0 # and sort the list using performance and count sorted_df = list_df.merge(performance, on='pair', how='left')\ - .fillna(0).sort_values(by=['profit', 'count'], ascending=False) + .fillna(0).sort_values(by=['profit', 'count', 'pair'], ascending=False) pairlist = sorted_df['pair'].tolist() return pairlist diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index c62ec81f3..475691327 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -305,7 +305,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # PerformanceFilter ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "PerformanceFilter"}], - "USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT', 'ADADOUBLE/USDT']), + "USDT", ['ETH/USDT', 'NANO/USDT', 'ADADOUBLE/USDT', 'ADAHALF/USDT']), # AgeFilter only ([{"method": "AgeFilter", "min_days_listed": 2}], "BTC", 'filter_at_the_beginning'), # OperationalException expected From 323c0657f8a80b0af14d0ff18920f431f66af7a0 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 12:17:03 -0600 Subject: [PATCH 0990/1197] Sort by profit after sort by count/pair --- freqtrade/pairlist/PerformanceFilter.py | 21 +++++++++++++-------- tests/pairlist/test_pairlist.py | 17 ++++++++++++----- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/freqtrade/pairlist/PerformanceFilter.py b/freqtrade/pairlist/PerformanceFilter.py index 5e1ec3c66..cdc3c78ad 100644 --- a/freqtrade/pairlist/PerformanceFilter.py +++ b/freqtrade/pairlist/PerformanceFilter.py @@ -31,17 +31,17 @@ class PerformanceFilter(IPairList): def short_desc(self) -> str: """ - Short whitelist method description - used for startup-messages + Short allowlist method description - used for startup-messages """ return f"{self.name} - Sorting pairs by performance." def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: """ - Filters and sorts pairlist and returns the whitelist again. + Filters and sorts pairlist and returns the allowlist again. Called on each bot iteration - please use internal caching if necessary :param pairlist: pairlist to filter or sort :param tickers: Tickers (from exchange.get_tickers()). May be cached. - :return: new whitelist + :return: new allowlist """ # Get the trading performance for pairs from database performance = pd.DataFrame(Trade.get_overall_performance()) @@ -49,13 +49,18 @@ class PerformanceFilter(IPairList): # Skip performance-based sorting if no performance data is available if len(performance) == 0: return pairlist - - # get pairlist from performance dataframe values + + # Get pairlist from performance dataframe values list_df = pd.DataFrame({'pair': pairlist}) - # set initial value for pairs with no trades to 0 - # and sort the list using performance and count + + # Set initial value for pairs with no trades to 0 + # Sort the list using: + # - primarily performance (high to low) + # - then count (low to high, so as to favor same performance with fewer trades) + # - then pair name alphametically sorted_df = list_df.merge(performance, on='pair', how='left')\ - .fillna(0).sort_values(by=['profit', 'count', 'pair'], ascending=False) + .fillna(0).sort_values(by=['count', 'pair'], ascending=True)\ + .sort_values(by=['profit'], ascending=False) pairlist = sorted_df['pair'].tolist() return pairlist diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 475691327..244f92d8b 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -748,13 +748,20 @@ def test_pairlistmanager_no_pairlist(mocker, whitelist_conf): [{'pair': 'ETH/BTC', 'profit': -5, 'count': 100}, {'pair': 'TKN/BTC', 'profit': 4, 'count': 2}], ['TKN/BTC', 'LTC/BTC', 'ETH/BTC']), - # Tie in performance data broken by count + # Tie in performance data broken by count (ascending) ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], ['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], - [{'pair': 'LTC/BTC', 'profit': -5, 'count': 101}, - {'pair': 'TKN/BTC', 'profit': -5, 'count': 2}, - {'pair': 'ETH/BTC', 'profit': -5, 'count': 100}], - ['LTC/BTC', 'ETH/BTC', 'TKN/BTC']), + [{'pair': 'LTC/BTC', 'profit': -5.01, 'count': 101}, + {'pair': 'TKN/BTC', 'profit': -5.01, 'count': 2}, + {'pair': 'ETH/BTC', 'profit': -5.01, 'count': 100}], + ['TKN/BTC', 'ETH/BTC', 'LTC/BTC']), + # Tie in performance and count, broken by alphabetical sort + ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], + ['ETH/BTC', 'TKN/BTC', 'LTC/BTC'], + [{'pair': 'LTC/BTC', 'profit': -5.01, 'count': 1}, + {'pair': 'TKN/BTC', 'profit': -5.01, 'count': 1}, + {'pair': 'ETH/BTC', 'profit': -5.01, 'count': 1}], + ['ETH/BTC', 'LTC/BTC', 'TKN/BTC']), ]) def test_performance_filter(mocker, whitelist_conf, pairlists, pair_allowlist, overall_performance, allowlist_result, tickers, markets, ohlcv_history_list): From d6c93919246a7e1e250f2934b3da7417a292118b Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 12:18:23 -0600 Subject: [PATCH 0991/1197] Restoring expectation --- tests/pairlist/test_pairlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 244f92d8b..4b4f51b37 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -305,7 +305,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # PerformanceFilter ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "PerformanceFilter"}], - "USDT", ['ETH/USDT', 'NANO/USDT', 'ADADOUBLE/USDT', 'ADAHALF/USDT']), + "USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT', 'ADADOUBLE/USDT']), # AgeFilter only ([{"method": "AgeFilter", "min_days_listed": 2}], "BTC", 'filter_at_the_beginning'), # OperationalException expected From e7a035eefe3bdcaeca5688051b778f7b48788505 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 12:29:31 -0600 Subject: [PATCH 0992/1197] Lint --- freqtrade/pairlist/PerformanceFilter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/pairlist/PerformanceFilter.py b/freqtrade/pairlist/PerformanceFilter.py index cdc3c78ad..92a97099e 100644 --- a/freqtrade/pairlist/PerformanceFilter.py +++ b/freqtrade/pairlist/PerformanceFilter.py @@ -49,7 +49,7 @@ class PerformanceFilter(IPairList): # Skip performance-based sorting if no performance data is available if len(performance) == 0: return pairlist - + # Get pairlist from performance dataframe values list_df = pd.DataFrame({'pair': pairlist}) @@ -60,7 +60,7 @@ class PerformanceFilter(IPairList): # - then pair name alphametically sorted_df = list_df.merge(performance, on='pair', how='left')\ .fillna(0).sort_values(by=['count', 'pair'], ascending=True)\ - .sort_values(by=['profit'], ascending=False) + .sort_values(by=['profit'], ascending=False) pairlist = sorted_df['pair'].tolist() return pairlist From 4b6f5b92b59e21896035c2f47475a3fcdf07c377 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 12:47:36 -0600 Subject: [PATCH 0993/1197] Remove non-pertinent test case --- tests/pairlist/test_pairlist.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 4b4f51b37..1d2f16b45 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -302,10 +302,6 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, {"method": "ShuffleFilter"}], "USDT", 3), # whitelist_result is integer -- check only length of randomized pairlist - # PerformanceFilter - ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}, - {"method": "PerformanceFilter"}], - "USDT", ['ETH/USDT', 'NANO/USDT', 'ADAHALF/USDT', 'ADADOUBLE/USDT']), # AgeFilter only ([{"method": "AgeFilter", "min_days_listed": 2}], "BTC", 'filter_at_the_beginning'), # OperationalException expected From 1791495475e1e0bbab0b820a065b627b92f28f7d Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 16:50:44 -0600 Subject: [PATCH 0994/1197] Trigger another run of tests --- tests/pairlist/test_pairlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 1d2f16b45..884be3c24 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -332,7 +332,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), # PerformanceFilter only ([{"method": "PerformanceFilter"}], - "BTC", 'filter_at_the_beginning'), # OperationalException expected + "BTC", 'filter_at_the_beginning'), # OperationalException expected # SpreadFilter after StaticPairList ([{"method": "StaticPairList"}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}], From 90070f0dc54cd2ea92268ce75240ff3972523666 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sat, 28 Nov 2020 17:17:40 -0600 Subject: [PATCH 0995/1197] Force test rerun --- tests/pairlist/test_pairlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 884be3c24..1d2f16b45 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -332,7 +332,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), # PerformanceFilter only ([{"method": "PerformanceFilter"}], - "BTC", 'filter_at_the_beginning'), # OperationalException expected + "BTC", 'filter_at_the_beginning'), # OperationalException expected # SpreadFilter after StaticPairList ([{"method": "StaticPairList"}, {"method": "SpreadFilter", "max_spread_ratio": 0.005}], From 5f8e67d2b25c40454343414653e02b48fd4518d8 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sun, 29 Nov 2020 05:05:54 -0600 Subject: [PATCH 0996/1197] Update docs/includes/pairlists.md Co-authored-by: Matthias --- docs/includes/pairlists.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index a1bbebbf7..844f1d70a 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -77,7 +77,7 @@ This filter allows freqtrade to ignore pairs until they have been listed for at #### PerformanceFilter -Sorts pairs by performance, as follows: +Sorts pairs by past trade performance, as follows: 1. Positive performance. 2. No closed trades yet. 3. Negative performance. From 99abe52043ab7b7f54e79fd75c1c80831eafebf5 Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sun, 29 Nov 2020 10:30:02 -0600 Subject: [PATCH 0997/1197] Trigger CI --- tests/pairlist/test_pairlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 1d2f16b45..1f434ae34 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -326,7 +326,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # ShuffleFilter only ([{"method": "ShuffleFilter", "seed": 42}], "BTC", 'filter_at_the_beginning'), # OperationalException expected - # PerformanceFilter after StaticPairList + # PerformanceFilter after StaticPairList ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), From b7de18608d8f977578e56d3c83b88fbbbb459f3e Mon Sep 17 00:00:00 2001 From: Leif Segen Date: Sun, 29 Nov 2020 10:30:43 -0600 Subject: [PATCH 0998/1197] Trigger CI --- tests/pairlist/test_pairlist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 1f434ae34..1d2f16b45 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -326,7 +326,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): # ShuffleFilter only ([{"method": "ShuffleFilter", "seed": 42}], "BTC", 'filter_at_the_beginning'), # OperationalException expected - # PerformanceFilter after StaticPairList + # PerformanceFilter after StaticPairList ([{"method": "StaticPairList"}, {"method": "PerformanceFilter"}], "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), From f17c7f0609f66f33b4e14fc79c9f56fd6665e394 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Nov 2020 05:42:00 +0000 Subject: [PATCH 0999/1197] Bump plotly from 4.12.0 to 4.13.0 Bumps [plotly](https://github.com/plotly/plotly.py) from 4.12.0 to 4.13.0. - [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.12.0...v4.13.0) Signed-off-by: dependabot[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index bd40bc0b5..1c3b03133 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.12.0 +plotly==4.13.0 From 275cfb3a9c8bfa69be15a14b3b38252df86f061a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Nov 2020 05:42:12 +0000 Subject: [PATCH 1000/1197] Bump ccxt from 1.38.13 to 1.38.55 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.38.13 to 1.38.55. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.38.13...1.38.55) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7490688d4..f72e30480 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.4 pandas==1.1.4 -ccxt==1.38.13 +ccxt==1.38.55 aiohttp==3.7.3 SQLAlchemy==1.3.20 python-telegram-bot==13.0 From 14d44b2cd6b8c9dc5884ee61b3432a0df43d131b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Nov 2020 08:02:09 +0000 Subject: [PATCH 1001/1197] Bump python-telegram-bot from 13.0 to 13.1 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 13.0 to 13.1. - [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases) - [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst) - [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v13.0...v13.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f72e30480..f59754f93 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ pandas==1.1.4 ccxt==1.38.55 aiohttp==3.7.3 SQLAlchemy==1.3.20 -python-telegram-bot==13.0 +python-telegram-bot==13.1 arrow==0.17.0 cachetools==4.1.1 requests==2.25.0 From 202ca88e2311bda492757351cef667630fa498de Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Nov 2020 17:37:19 +0100 Subject: [PATCH 1002/1197] Changes to pi steup --- docs/installation.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index 9b15c9685..b6197c905 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -105,13 +105,17 @@ OS Specific steps are listed first, the [Common](#common) section below is neces ``` === "RaspberryPi/Raspbian" - The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/) from at least September 2019. + The following assumes the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/). This image comes with python3.7 preinstalled, making it easy to get freqtrade up and running. Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied. + ``` bash sudo apt-get install python3-venv libatlas-base-dev + # Use pywheels.org to speed up installation + sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > sudo tee /etc/pip.conf + git clone https://github.com/freqtrade/freqtrade.git cd freqtrade @@ -120,6 +124,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces !!! Note "Installation duration" Depending on your internet speed and the Raspberry Pi version, installation can take multiple hours to complete. + Due to this, we recommend to use the prebuild docker-image for Raspberry, by following the [Docker quickstart documentation](docker_quickstart.md) !!! Note The above does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`. From 95b24ba8a931ab8a7d440548f820a40342fb694e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Nov 2020 20:56:14 +0100 Subject: [PATCH 1003/1197] Update setup.sh with some specifics --- docs/installation.md | 4 ++-- setup.sh | 28 ++++++++++++++++++++-------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index b6197c905..4a2450ea2 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -90,13 +90,13 @@ Each time you open a new terminal, you must run `source .env/bin/activate`. ## Custom Installation -We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros. +We've included/collected install instructions for Ubuntu, MacOS, and Windows. These are guidelines and your success may vary with other distros. OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems. !!! Note Python3.6 or higher and the corresponding pip are assumed to be available. -=== "Ubuntu 16.04" +=== "Ubuntu/Debian" #### Install necessary dependencies ```bash diff --git a/setup.sh b/setup.sh index 049a6a77e..83ba42d9b 100755 --- a/setup.sh +++ b/setup.sh @@ -61,13 +61,25 @@ function updateenv() { read -p "Do you want to install dependencies for dev [y/N]? " if [[ $REPLY =~ ^[Yy]$ ]] then - ${PYTHON} -m pip install --upgrade -r requirements-dev.txt + REQUIREMENTS=requirements-dev.txt else - ${PYTHON} -m pip install --upgrade -r requirements.txt - echo "Dev dependencies ignored." + REQUIREMENTS=requirements.txt + fi + SYS_ARCH=$(uname -m) + if [ "${SYS_ARCH}" == "armv7l" ]; then + echo "Detected Raspberry, installing cython." + ${PYTHON} -m pip install --upgrade cython + fi + ${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} + if [ $? -ne 0 ]; then + echo "Failed installing dependencies" + exit 1 fi - ${PYTHON} -m pip install -e . + if [ $? -ne 0 ]; then + echo "Failed installing Freqtrade" + exit 1 + fi echo "pip install completed" echo } @@ -134,11 +146,11 @@ function reset() { git fetch -a - if [ "1" == $(git branch -vv |grep -c "* develop") ] + if [ "1" == $(git branch -vv | grep -c "* develop") ] then echo "- Hard resetting of 'develop' branch." git reset --hard origin/develop - elif [ "1" == $(git branch -vv |grep -c "* stable") ] + elif [ "1" == $(git branch -vv | grep -c "* stable") ] then echo "- Hard resetting of 'stable' branch." git reset --hard origin/stable @@ -149,7 +161,7 @@ function reset() { fi if [ -d ".env" ]; then - echo "- Delete your previous virtual env" + echo "- Deleting your previous virtual env" rm -rf .env fi echo @@ -253,7 +265,7 @@ function install() { echo "Run the bot !" echo "-------------------------" echo "You can now use the bot by executing 'source .env/bin/activate; freqtrade '." - echo "You can see the list of available bot subcommands by executing 'source .env/bin/activate; freqtrade --help'." + echo "You can see the list of available bot sub-commands by executing 'source .env/bin/activate; freqtrade --help'." echo "You verify that freqtrade is installed successfully by running 'source .env/bin/activate; freqtrade --version'." } From 5f70d1f9a7bc58969cee2c3fa40b981047d22a9a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Nov 2020 21:10:11 +0100 Subject: [PATCH 1004/1197] Ask for hyperopt installation during setup closes #2871 --- docs/installation.md | 2 +- setup.sh | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index 4a2450ea2..5cc0e03f4 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -114,7 +114,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces ``` bash sudo apt-get install python3-venv libatlas-base-dev # Use pywheels.org to speed up installation - sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > sudo tee /etc/pip.conf + sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > tee /etc/pip.conf git clone https://github.com/freqtrade/freqtrade.git cd freqtrade diff --git a/setup.sh b/setup.sh index 83ba42d9b..8896331e3 100755 --- a/setup.sh +++ b/setup.sh @@ -56,6 +56,7 @@ function updateenv() { exit 1 fi source .env/bin/activate + SYS_ARCH=$(uname -m) echo "pip install in-progress. Please wait..." ${PYTHON} -m pip install --upgrade pip read -p "Do you want to install dependencies for dev [y/N]? " @@ -65,12 +66,21 @@ function updateenv() { else REQUIREMENTS=requirements.txt fi - SYS_ARCH=$(uname -m) + REQUIREMENTS_HYPEROPT="" + if [ "${SYS_ARCH}" != "armv7l" ]; then + # Is not Raspberry + read -p "Do you want to install hyperopt dependencies for dev [y/N]? " + if [[ $REPLY =~ ^[Yy]$ ]] + then + REQUIREMENTS_HYPEROPT="-r requirements-hyperopt.txt" + fi + fi + if [ "${SYS_ARCH}" == "armv7l" ]; then echo "Detected Raspberry, installing cython." ${PYTHON} -m pip install --upgrade cython fi - ${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} + ${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} ${REQUIREMENTS_HYPEROPT} if [ $? -ne 0 ]; then echo "Failed installing dependencies" exit 1 From cec771b59396658d7d89241e869da9459411a4b0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Nov 2020 21:17:50 +0100 Subject: [PATCH 1005/1197] Ask for plotting dependency installation --- setup.sh | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/setup.sh b/setup.sh index 8896331e3..af5e70691 100755 --- a/setup.sh +++ b/setup.sh @@ -67,20 +67,25 @@ function updateenv() { REQUIREMENTS=requirements.txt fi REQUIREMENTS_HYPEROPT="" - if [ "${SYS_ARCH}" != "armv7l" ]; then + REQUIREMENTS_PLOT="" + read -p "Do you want to install plotting dependencies (plotly) [y/N]? " + if [[ $REPLY =~ ^[Yy]$ ]] + then + REQUIREMENTS_PLOT="-r requirements-plot.txt" + fi + if [ "${SYS_ARCH}" == "armv7l" ]; then + echo "Detected Raspberry, installing cython, skipping hyperopt installation." + ${PYTHON} -m pip install --upgrade cython + else # Is not Raspberry - read -p "Do you want to install hyperopt dependencies for dev [y/N]? " + read -p "Do you want to install hyperopt dependencies [y/N]? " if [[ $REPLY =~ ^[Yy]$ ]] then REQUIREMENTS_HYPEROPT="-r requirements-hyperopt.txt" fi fi - if [ "${SYS_ARCH}" == "armv7l" ]; then - echo "Detected Raspberry, installing cython." - ${PYTHON} -m pip install --upgrade cython - fi - ${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} ${REQUIREMENTS_HYPEROPT} + ${PYTHON} -m pip install --upgrade -r ${REQUIREMENTS} ${REQUIREMENTS_HYPEROPT} ${REQUIREMENTS_PLOT} if [ $? -ne 0 ]; then echo "Failed installing dependencies" exit 1 From 36b7edc342cac2680c7408eb17cf412b3416b863 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Dec 2020 19:55:20 +0100 Subject: [PATCH 1006/1197] Update typing errors --- freqtrade/rpc/api_server.py | 2 +- freqtrade/rpc/rpc.py | 2 +- freqtrade/rpc/telegram.py | 48 +++++++++++++++++++++---------------- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 384d7c6c2..8c2c203e6 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -470,7 +470,7 @@ class ApiServer(RPC): @require_login @rpc_catch_errors - def _trades_delete(self, tradeid): + def _trades_delete(self, tradeid: int): """ Handler for DELETE /trades/ endpoint. Removes the trade from the database (tries to cancel open orders first!) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e608a2274..9ac271ba0 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -542,7 +542,7 @@ class RPC: else: return None - def _rpc_delete(self, trade_id: str) -> Dict[str, Union[str, int]]: + def _rpc_delete(self, trade_id: int) -> Dict[str, Union[str, int]]: """ Handler for delete . Delete the given trade and close eventually existing open orders. diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 31d5bbfbd..7239eab02 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -5,11 +5,11 @@ This module manage Telegram communication """ import json import logging -from typing import Any, Callable, Dict +from typing import Any, Callable, Dict, List, Union import arrow from tabulate import tabulate -from telegram import ParseMode, ReplyKeyboardMarkup, Update +from telegram import KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update from telegram.error import NetworkError, TelegramError from telegram.ext import CallbackContext, CommandHandler, Updater from telegram.utils.helpers import escape_markdown @@ -71,7 +71,7 @@ class Telegram(RPC): """ super().__init__(freqtrade) - self._updater: Updater = None + self._updater: Updater self._config = freqtrade.config self._init() if self._config.get('fiat_display_currency', None): @@ -231,7 +231,7 @@ class Telegram(RPC): :return: None """ - if 'table' in context.args: + if context.args and 'table' in context.args: self._status_table(update, context) return @@ -305,7 +305,7 @@ class Telegram(RPC): stake_cur = self._config['stake_currency'] fiat_disp_cur = self._config.get('fiat_display_currency', '') try: - timescale = int(context.args[0]) + timescale = int(context.args[0]) if context.args else 0 except (TypeError, ValueError, IndexError): timescale = 7 try: @@ -485,7 +485,10 @@ class Telegram(RPC): :return: None """ - trade_id = context.args[0] if len(context.args) > 0 else None + trade_id = context.args[0] if context.args and len(context.args) > 0 else None + if not trade_id: + self._send_msg("You must specify a trade-id or 'all'.") + return try: msg = self._rpc_forcesell(trade_id) self._send_msg('Forcesell Result: `{result}`'.format(**msg)) @@ -502,13 +505,13 @@ class Telegram(RPC): :param update: message update :return: None """ - - pair = context.args[0] - price = float(context.args[1]) if len(context.args) > 1 else None - try: - self._rpc_forcebuy(pair, price) - except RPCException as e: - self._send_msg(str(e)) + if context.args: + pair = context.args[0] + price = float(context.args[1]) if len(context.args) > 1 else None + try: + self._rpc_forcebuy(pair, price) + except RPCException as e: + self._send_msg(str(e)) @authorized_only def _trades(self, update: Update, context: CallbackContext) -> None: @@ -521,7 +524,7 @@ class Telegram(RPC): """ stake_cur = self._config['stake_currency'] try: - nrecent = int(context.args[0]) + nrecent = int(context.args[0]) if context.args else 10 except (TypeError, ValueError, IndexError): nrecent = 10 try: @@ -554,9 +557,10 @@ class Telegram(RPC): :param update: message update :return: None """ - - trade_id = context.args[0] if len(context.args) > 0 else None try: + if not context.args or len(context.args) == 0: + raise RPCException("Trade-id not set.") + trade_id = int(context.args[0]) msg = self._rpc_delete(trade_id) self._send_msg(( '`{result_msg}`\n' @@ -676,7 +680,7 @@ class Telegram(RPC): """ try: try: - limit = int(context.args[0]) + limit = int(context.args[0]) if context.args else 10 except (TypeError, ValueError, IndexError): limit = 10 logs = self._rpc_get_logs(limit)['logs'] @@ -802,7 +806,7 @@ class Telegram(RPC): f"*Current state:* `{val['state']}`" ) - def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN, + def _send_msg(self, msg: str, parse_mode: str = ParseMode.MARKDOWN, disable_notification: bool = False) -> None: """ Send given markdown message @@ -812,9 +816,11 @@ class Telegram(RPC): :return: None """ - keyboard = [['/daily', '/profit', '/balance'], - ['/status', '/status table', '/performance'], - ['/count', '/start', '/stop', '/help']] + keyboard: List[List[Union[str, KeyboardButton]]] = [ + ['/daily', '/profit', '/balance'], + ['/status', '/status table', '/performance'], + ['/count', '/start', '/stop', '/help'] + ] reply_markup = ReplyKeyboardMarkup(keyboard) From 5dfa1807a3c99499f3304d6046eeed96f8cc6825 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Dec 2020 19:57:43 +0100 Subject: [PATCH 1007/1197] Fix tests after small updates --- tests/rpc/test_rpc_telegram.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index ace44a34a..8264ab3df 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -58,7 +58,6 @@ def test__init__(default_conf, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) telegram = Telegram(get_patched_freqtradebot(mocker, default_conf)) - assert telegram._updater is None assert telegram._config == default_conf @@ -881,7 +880,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: context.args = [] telegram._forcesell(update=update, context=context) assert msg_mock.call_count == 1 - assert 'invalid argument' in msg_mock.call_args_list[0][0][0] + assert "You must specify a trade-id or 'all'." in msg_mock.call_args_list[0][0][0] # Invalid argument msg_mock.reset_mock() @@ -1251,7 +1250,7 @@ def test_telegram_delete_trade(mocker, update, default_conf, fee): context.args = [] telegram._delete_trade(update=update, context=context) - assert "invalid argument" in msg_mock.call_args_list[0][0][0] + assert "Trade-id not set." in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() create_mock_trades(fee) From d6cc3d737453fb6e6f0aad81d07bd63ddca2cbb3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Dec 2020 19:58:06 +0100 Subject: [PATCH 1008/1197] Improve FAQ related to question in #4023 --- docs/faq.md | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index aa33218fb..3cf5a74ca 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -2,7 +2,7 @@ ## Beginner Tips & Tricks -* When you work with your strategy & hyperopt file you should use a proper code editor like vscode or Pycharm. A good code editor will provide syntax highlighting as well as line numbers, making it easy to find syntax errors (most likely, pointed out by Freqtrade during startup). +* When you work with your strategy & hyperopt file you should use a proper code editor like VScode or Pycharm. A good code editor will provide syntax highlighting as well as line numbers, making it easy to find syntax errors (most likely, pointed out by Freqtrade during startup). ## Freqtrade common issues @@ -17,7 +17,7 @@ This could have the following reasons: * The installation did not work correctly. * Please check the [Installation documentation](installation.md). -### I have waited 5 minutes, why hasn't the bot made any trades yet?! +### I have waited 5 minutes, why hasn't the bot made any trades yet? * Depending on the buy strategy, the amount of whitelisted coins, the situation of the market etc, it can take up to hours to find good entry @@ -47,9 +47,9 @@ like pauses. You can stop your bot, adjust settings and start it again. That's great. We have a nice backtesting and hyperoptimization setup. See the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands). -### Is there a setting to only SELL the coins being held and not perform anymore BUYS? +### Is there a setting to only Buys the coins being held and not perform anymore Buys? -You can use the `/forcesell all` command from Telegram. +You can use the `/stopbuy` to prevent future buys, followed `/forcesell all` (sell all open trades) command from Telegram. ### I want to run multiple bots on the same machine @@ -59,7 +59,7 @@ Please look at the [advanced setup documentation Page](advanced-setup.md#running This message is just a warning that the latest candles had missing candles in them. Depending on the exchange, this can indicate that the pair didn't have a trade for the timeframe you are using - and the exchange does only return candles with volume. -On low volume pairs, this is a rather common occurance. +On low volume pairs, this is a rather common occurrence. If this happens for all pairs in the pairlist, this might indicate a recent exchange downtime. Please check your exchange's public channels for details. @@ -130,7 +130,7 @@ On Windows, the `--logfile` option is also supported by Freqtrade and you can us ### How many epoch do I need to get a good Hyperopt result? Per default Hyperopt called without the `-e`/`--epochs` command line option will only -run 100 epochs, means 100 evals of your triggers, guards, ... Too few +run 100 epochs, means 100 evaluations of your triggers, guards, ... Too few to find a great result (unless if you are very lucky), so you probably have to run it for 10.000 or more. But it will take an eternity to compute. @@ -140,7 +140,7 @@ Since hyperopt uses Bayesian search, running for too many epochs may not produce It's therefore recommended to run between 500-1000 epochs over and over until you hit at least 10.000 epochs in total (or are satisfied with the result). You can best judge by looking at the results - if the bot keeps discovering better strategies, it's best to keep on going. ```bash -freqtrade hyperopt --hyperop SampleHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy SampleStrategy -e 1000 +freqtrade hyperopt --hyperopt SampleHyperopt --hyperopt-loss SharpeHyperOptLossDaily --strategy SampleStrategy -e 1000 ``` ### Why does it take a long time to run hyperopt? @@ -151,21 +151,21 @@ freqtrade hyperopt --hyperop SampleHyperopt --hyperopt-loss SharpeHyperOptLossDa This answer was written during the release 0.15.1, when we had: -- 8 triggers -- 9 guards: let's say we evaluate even 10 values from each -- 1 stoploss calculation: let's say we want 10 values from that too to be evaluated +* 8 triggers +* 9 guards: let's say we evaluate even 10 values from each +* 1 stoploss calculation: let's say we want 10 values from that too to be evaluated The following calculation is still very rough and not very precise but it will give the idea. With only these triggers and guards there is -already 8\*10^9\*10 evaluations. A roughly total of 80 billion evals. -Did you run 100 000 evals? Congrats, you've done roughly 1 / 100 000 th +already 8\*10^9\*10 evaluations. A roughly total of 80 billion evaluations. +Did you run 100 000 evaluations? Congrats, you've done roughly 1 / 100 000 th of the search space, assuming that the bot never tests the same parameters more than once. * The time it takes to run 1000 hyperopt epochs depends on things like: The available cpu, hard-disk, ram, timeframe, timerange, indicator settings, indicator count, amount of coins that hyperopt test strategies on and the resulting trade count - which can be 650 trades in a year or 10.0000 trades depending if the strategy aims for big profits by trading rarely or for many low profit trades. Example: 4% profit 650 times vs 0,3% profit a trade 10.000 times in a year. If we assume you set the --timerange to 365 days. -Example: +Example: `freqtrade --config config.json --strategy SampleStrategy --hyperopt SampleHyperopt -e 1000 --timerange 20190601-20200601` ## Edge module From c1fffb9925ea7d5f5fb661ac4a63f6eaff4b7754 Mon Sep 17 00:00:00 2001 From: Samaoo Date: Tue, 1 Dec 2020 21:38:54 +0100 Subject: [PATCH 1009/1197] Update faq.md --- docs/faq.md | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/docs/faq.md b/docs/faq.md index 3cf5a74ca..e3a7895e3 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -2,15 +2,15 @@ ## Beginner Tips & Tricks -* When you work with your strategy & hyperopt file you should use a proper code editor like VScode or Pycharm. A good code editor will provide syntax highlighting as well as line numbers, making it easy to find syntax errors (most likely, pointed out by Freqtrade during startup). +* When you work with your strategy & hyperopt file you should use a proper code editor like VSCode or PyCharm. A good code editor will provide syntax highlighting as well as line numbers, making it easy to find syntax errors (most likely pointed out by Freqtrade during startup). ## Freqtrade common issues ### The bot does not start -Running the bot with `freqtrade trade --config config.json` does show the output `freqtrade: command not found`. +Running the bot with `freqtrade trade --config config.json` shows the output `freqtrade: command not found`. -This could have the following reasons: +This could be caused by the following reasons: * The virtual environment is not active * run `source .env/bin/activate` to activate the virtual environment @@ -20,12 +20,12 @@ This could have the following reasons: ### I have waited 5 minutes, why hasn't the bot made any trades yet? * Depending on the buy strategy, the amount of whitelisted coins, the -situation of the market etc, it can take up to hours to find good entry +situation of the market etc, it can take up to hours to find a good entry position for a trade. Be patient! -* Or it may because of a configuration error? Best check the logs, it's usually telling you if the bot is simply not getting buy signals (only heartbeat messages), or if there is something wrong (errors / exceptions in the log). +* It may be because of a configuration error. It's best check the logs, they usually tell you if the bot is simply not getting buy signals (only heartbeat messages), or if there is something wrong (errors / exceptions in the log). -### I have made 12 trades already, why is my total profit negative?! +### I have made 12 trades already, why is my total profit negative? I understand your disappointment but unfortunately 12 trades is just not enough to say anything. If you run backtesting, you can see that our @@ -36,20 +36,18 @@ of course constantly aim to improve the bot but it will _always_ be a gamble, which should leave you with modest wins on monthly basis but you can't say much from few trades. -### I’d like to change the stake amount. Can I just stop the bot with /stop and then change the config.json and run it again? +### I’d like to make changes to the config. Can I do that without having to kill the bot? -Not quite. Trades are persisted to a database but the configuration is -currently only read when the bot is killed and restarted. `/stop` more -like pauses. You can stop your bot, adjust settings and start it again. +Yes. You can edit your config, use the `/stop` command in Telegram, followed by `/reload_config` and the bot will run with the new config. ### I want to improve the bot with a new strategy That's great. We have a nice backtesting and hyperoptimization setup. See the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands). -### Is there a setting to only Buys the coins being held and not perform anymore Buys? +### Is there a setting to only SELL the coins being held and not perform anymore BUYS? -You can use the `/stopbuy` to prevent future buys, followed `/forcesell all` (sell all open trades) command from Telegram. +You can use the `/stopbuy` command in Telegram to prevent future buys, followed by `/forcesell all` (sell all open trades). ### I want to run multiple bots on the same machine @@ -73,7 +71,7 @@ Read [the Bittrex section about restricted markets](exchanges.md#restricted-mark ### I'm getting the "Exchange Bittrex does not support market orders." message and cannot run my strategy -As the message says, Bittrex does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Probably your strategy was written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Bittrex). +As the message says, Bittrex does not support market orders and you have one of the [order types](configuration.md/#understand-order_types) set to "market". Your strategy was probably written with other exchanges in mind and sets "market" orders for "stoploss" orders, which is correct and preferable for most of the exchanges supporting market orders (but not for Bittrex). To fix it for Bittrex, redefine order types in the strategy to use "limit" instead of "market": @@ -85,7 +83,7 @@ To fix it for Bittrex, redefine order types in the strategy to use "limit" inste } ``` -Same fix should be done in the configuration file, if order types are defined in your custom config rather than in the strategy. +The same fix should be applied in the configuration file, if order types are defined in your custom config rather than in the strategy. ### How do I search the bot logs for something? @@ -127,7 +125,7 @@ On Windows, the `--logfile` option is also supported by Freqtrade and you can us ## Hyperopt module -### How many epoch do I need to get a good Hyperopt result? +### How many epochs do I need to get a good Hyperopt result? Per default Hyperopt called without the `-e`/`--epochs` command line option will only run 100 epochs, means 100 evaluations of your triggers, guards, ... Too few From 4bc24ece41c61100fbd352166f588f401345cf55 Mon Sep 17 00:00:00 2001 From: Samaoo Date: Tue, 1 Dec 2020 21:49:50 +0100 Subject: [PATCH 1010/1197] Update faq.md --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index e3a7895e3..1940b4e6c 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -12,7 +12,7 @@ Running the bot with `freqtrade trade --config config.json` shows the output `fr This could be caused by the following reasons: -* The virtual environment is not active +* The virtual environment is not active. * run `source .env/bin/activate` to activate the virtual environment * The installation did not work correctly. * Please check the [Installation documentation](installation.md). From 3c4fe66d86e2eeb88f0a06791890478c1e3e4405 Mon Sep 17 00:00:00 2001 From: Samaoo Date: Tue, 1 Dec 2020 21:50:51 +0100 Subject: [PATCH 1011/1197] Update faq.md --- docs/faq.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/faq.md b/docs/faq.md index 1940b4e6c..337b87ec8 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -13,7 +13,7 @@ Running the bot with `freqtrade trade --config config.json` shows the output `fr This could be caused by the following reasons: * The virtual environment is not active. - * run `source .env/bin/activate` to activate the virtual environment + * Run `source .env/bin/activate` to activate the virtual environment. * The installation did not work correctly. * Please check the [Installation documentation](installation.md). From d039ce1fb3c84bc5535877f1afb04840a9d7c6cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Dec 2020 06:46:18 +0100 Subject: [PATCH 1012/1197] Update available columns for hyperopt closes #4025 --- docs/advanced-hyperopt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index 59ebc16b5..1ace61769 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -77,7 +77,7 @@ Currently, the arguments are: * `results`: DataFrame containing the result The following columns are available in results (corresponds to the output-file of backtesting when used with `--export trades`): - `pair, profit_percent, profit_abs, open_time, close_time, open_index, close_index, trade_duration, open_at_end, open_rate, close_rate, sell_reason` + `pair, profit_percent, profit_abs, open_date, open_rate, open_fee, close_date, close_rate, close_fee, amount, trade_duration, open_at_end, sell_reason` * `trade_count`: Amount of trades (identical to `len(results)`) * `min_date`: Start date of the hyperopting TimeFrame * `min_date`: End date of the hyperopting TimeFrame From c09c23eab15b920f757199fabebab9699d37ff4e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Dec 2020 07:51:59 +0100 Subject: [PATCH 1013/1197] Make sure non-int telegram values don't crash the bot --- tests/rpc/test_rpc_telegram.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 8264ab3df..33010484d 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1222,8 +1222,14 @@ def test_telegram_trades(mocker, update, default_conf, fee): telegram._trades(update=update, context=context) assert "0 recent trades:" in msg_mock.call_args_list[0][0][0] assert "
    " not in msg_mock.call_args_list[0][0][0]
    -
         msg_mock.reset_mock()
    +
    +    context.args = ['hello']
    +    telegram._trades(update=update, context=context)
    +    assert "0 recent trades:" in msg_mock.call_args_list[0][0][0]
    +    assert "
    " not in msg_mock.call_args_list[0][0][0]
    +    msg_mock.reset_mock()
    +
         create_mock_trades(fee)
     
         context = MagicMock()
    
    From 9b4a81c0a4111e5779e07487ed570d717719d505 Mon Sep 17 00:00:00 2001
    From: Samaoo 
    Date: Wed, 2 Dec 2020 08:40:49 +0100
    Subject: [PATCH 1014/1197] Update faq.md
    
    ---
     docs/faq.md | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/docs/faq.md b/docs/faq.md
    index 337b87ec8..b424cd31d 100644
    --- a/docs/faq.md
    +++ b/docs/faq.md
    @@ -23,7 +23,7 @@ This could be caused by the following reasons:
     situation of the market etc, it can take up to hours to find a good entry
     position for a trade. Be patient!
     
    -* It may be because of a configuration error. It's best check the logs, they usually tell you if the bot is simply not getting buy signals (only heartbeat messages), or if there is something wrong (errors / exceptions in the log).
    +* It may be because of a configuration error. It's best to check the logs, they usually tell you if the bot is simply not getting buy signals (only heartbeat messages), or if there is something wrong (errors / exceptions in the log).
     
     ### I have made 12 trades already, why is my total profit negative?
     
    
    From 2fbbeb970bb768de7c7efb3ed6b1ca2b0c922363 Mon Sep 17 00:00:00 2001
    From: Matthias 
    Date: Fri, 4 Dec 2020 07:42:16 +0100
    Subject: [PATCH 1015/1197] Gracefully handle cases where no buy price was
     found
    
    closes #4030
    ---
     freqtrade/freqtradebot.py  | 3 +++
     tests/test_freqtradebot.py | 6 ++++++
     2 files changed, 9 insertions(+)
    
    diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
    index 7416d8236..c8d281852 100644
    --- a/freqtrade/freqtradebot.py
    +++ b/freqtrade/freqtradebot.py
    @@ -616,6 +616,9 @@ class FreqtradeBot:
                 # Calculate price
                 buy_limit_requested = self.get_buy_rate(pair, True)
     
    +        if not buy_limit_requested:
    +            raise PricingError('Could not determine buy price.')
    +
             min_stake_amount = self._get_min_pair_stake_amount(pair, buy_limit_requested)
             if min_stake_amount is not None and min_stake_amount > stake_amount:
                 logger.warning(
    diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
    index 64dfb016e..6adef510f 100644
    --- a/tests/test_freqtradebot.py
    +++ b/tests/test_freqtradebot.py
    @@ -1074,6 +1074,12 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order
         mocker.patch('freqtrade.exchange.Exchange.buy', MagicMock(return_value=limit_buy_order))
         assert not freqtrade.execute_buy(pair, stake_amount)
     
    +    # Fail to get price...
    +    mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_buy_rate', MagicMock(return_value=0.0))
    +
    +    with pytest.raises(PricingError, match="Could not determine buy price."):
    +        freqtrade.execute_buy(pair, stake_amount)
    +
     
     def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) -> None:
         freqtrade = get_patched_freqtradebot(mocker, default_conf)
    
    From 7f453033a4928fd50eefc74bb737e03631f84bc8 Mon Sep 17 00:00:00 2001
    From: Samaoo 
    Date: Fri, 4 Dec 2020 16:53:41 +0100
    Subject: [PATCH 1016/1197] Update edge.md
    
    ---
     docs/edge.md | 20 ++++++++++----------
     1 file changed, 10 insertions(+), 10 deletions(-)
    
    diff --git a/docs/edge.md b/docs/edge.md
    index 7442f1927..fd6d2cf7d 100644
    --- a/docs/edge.md
    +++ b/docs/edge.md
    @@ -23,8 +23,8 @@ The Edge Positioning module seeks to improve a strategy's winning probability an
     We raise the following question[^1]:
     
     !!! Question "Which trade is a better option?"
    -    a) A trade with 80% of chance of losing $100 and 20% chance of winning $200
    - b) A trade with 100% of chance of losing $30 + a) A trade with 80% of chance of losing 100\$ and 20% chance of winning 200\$
    + b) A trade with 100% of chance of losing 30\$ ???+ Info "Answer" The expected value of *a)* is smaller than the expected value of *b)*.
    @@ -34,8 +34,8 @@ We raise the following question[^1]: Another way to look at it is to ask a similar question: !!! Question "Which trade is a better option?" - a) A trade with 80% of chance of winning 100 and 20% chance of losing $200
    - b) A trade with 100% of chance of winning $30 + a) A trade with 80% of chance of winning 100\$ and 20% chance of losing 200\$
    + b) A trade with 100% of chance of winning 30\$ Edge positioning tries to answer the hard questions about risk/reward and position size automatically, seeking to minimizes the chances of losing of a given strategy. @@ -82,7 +82,7 @@ Risk Reward Ratio ($R$) is a formula used to measure the expected gains of a giv $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ ???+ Example "Worked example of $R$ calculation" - Let's say that you think that the price of *stonecoin* today is $10.0. You believe that, because they will start mining stonecoin, it will go up to $15.0 tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to $0 tomorrow. You are planning to invest $100, which will give you 10 shares (100 / 10). + Let's say that you think that the price of *stonecoin* today is 10.0\$. You believe that, because they will start mining stonecoin, it will go up to 15.0\$ tomorrow. There is the risk that the stone is too hard, and the GPUs can't mine it, so the price might go to 0\$ tomorrow. You are planning to invest 100\$, which will give you 10 shares (100 / 10). Your potential profit is calculated as: @@ -92,9 +92,9 @@ $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ &= 50 \end{aligned}$ - Since the price might go to $0, the $100 dollars invested could turn into 0. + Since the price might go to 0\$, the 100\$ dollars invested could turn into 0. - We do however use a stoploss of 15% - so in the worst case, we'll sell 15% below entry price (or at 8.5$). + We do however use a stoploss of 15% - so in the worst case, we'll sell 15% below entry price (or at 8.5$\). $\begin{aligned} \text{potential_loss} &= (\text{entry_price} - \text{stoploss}) * \frac{\text{investment}}{\text{entry_price}} \\ @@ -109,7 +109,7 @@ $$ R = \frac{\text{potential_profit}}{\text{potential_loss}} $$ &= \frac{50}{15}\\ &= 3.33 \end{aligned}$
    - What it effectively means is that the strategy have the potential to make 3.33$ for each $1 invested. + What it effectively means is that the strategy have the potential to make 3.33\$ for each 1\$ invested. On a long horizon, that is, on many trades, we can calculate the risk reward by dividing the strategy' average profit on winning trades by the strategy' average loss on losing trades. We can calculate the average profit, $\mu_{win}$, as follows: @@ -141,7 +141,7 @@ $$E = R * W - L$$ $E = R * W - L = 5 * 0.28 - 0.72 = 0.68$
    -The expectancy worked out in the example above means that, on average, this strategy' trades will return 1.68 times the size of its losses. Said another way, the strategy makes $1.68 for every $1 it loses, on average. +The expectancy worked out in the example above means that, on average, this strategy' trades will return 1.68 times the size of its losses. Said another way, the strategy makes 1.68\$ for every 1\$ it loses, on average. This is important for two reasons: First, it may seem obvious, but you know right away that you have a positive return. Second, you now have a number you can compare to other candidate systems to make decisions about which ones you employ. @@ -222,7 +222,7 @@ Edge module has following configuration options: | `stoploss_range_max` | Maximum stoploss.
    *Defaults to `-0.10`.*
    **Datatype:** Float | `stoploss_range_step` | As an example if this is set to -0.01 then Edge will test the strategy for `[-0.01, -0,02, -0,03 ..., -0.09, -0.10]` ranges.
    **Note** than having a smaller step means having a bigger range which could lead to slow calculation.
    If you set this parameter to -0.001, you then slow down the Edge calculation by a factor of 10.
    *Defaults to `-0.001`.*
    **Datatype:** Float | `minimum_winrate` | It filters out pairs which don't have at least minimum_winrate.
    This comes handy if you want to be conservative and don't comprise win rate in favour of risk reward ratio.
    *Defaults to `0.60`.*
    **Datatype:** Float -| `minimum_expectancy` | It filters out pairs which have the expectancy lower than this number.
    Having an expectancy of 0.20 means if you put 10$ on a trade you expect a 12$ return.
    *Defaults to `0.20`.*
    **Datatype:** Float +| `minimum_expectancy` | It filters out pairs which have the expectancy lower than this number.
    Having an expectancy of 0.20 means if you put 10\$ on a trade you expect a 12\$ return.
    *Defaults to `0.20`.*
    **Datatype:** Float | `min_trade_number` | When calculating *W*, *R* and *E* (expectancy) against historical data, you always want to have a minimum number of trades. The more this number is the more Edge is reliable.
    Having a win rate of 100% on a single trade doesn't mean anything at all. But having a win rate of 70% over past 100 trades means clearly something.
    *Defaults to `10` (it is highly recommended not to decrease this number).*
    **Datatype:** Integer | `max_trade_duration_minute` | Edge will filter out trades with long duration. If a trade is profitable after 1 month, it is hard to evaluate the strategy based on it. But if most of trades are profitable and they have maximum duration of 30 minutes, then it is clearly a good sign.
    **NOTICE:** While configuring this value, you should take into consideration your timeframe. As an example filtering out trades having duration less than one day for a strategy which has 4h interval does not make sense. Default value is set assuming your strategy interval is relatively small (1m or 5m, etc.).
    *Defaults to `1440` (one day).*
    **Datatype:** Integer | `remove_pumps` | Edge will remove sudden pumps in a given market while going through historical data. However, given that pumps happen very often in crypto markets, we recommend you keep this off.
    *Defaults to `false`.*
    **Datatype:** Boolean From 71e46794b44ca1315c9f78ef98c33aa52cc76fed Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 4 Dec 2020 19:59:26 +0100 Subject: [PATCH 1017/1197] Add updating documentation closes #4036 --- docs/updating.md | 31 +++++++++++++++++++++++++++++++ mkdocs.yml | 1 + 2 files changed, 32 insertions(+) create mode 100644 docs/updating.md diff --git a/docs/updating.md b/docs/updating.md new file mode 100644 index 000000000..b23ce32dc --- /dev/null +++ b/docs/updating.md @@ -0,0 +1,31 @@ +# How to update + +To update your freqtrade installation, please use one of the below methods, corresponding to your installation method. + +## docker-compose + +!!! Note "Legacy installations using the `master` image" + We're switching from master to stable for the release Images - please adjust your docker-file and replace `freqtradeorg/freqtrade:master` with `freqtradeorg/freqtrade:stable` + +``` bash +docker-compose pull +docker-compose up -d +``` + +## Installation via setup script + +``` bash +./setup.sh --update +``` + +!!! Note + Make sure to run this command with your virtual environment disabled! + +## Plain native installation + +Please ensure that you're also updating dependencies - otherwise things might break without you noticing. + +``` bash +git pull +pip install -U -r requirements.txt +``` diff --git a/mkdocs.yml b/mkdocs.yml index 2cc0c9fcb..c791386ae 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -31,6 +31,7 @@ nav: - Advanced Strategy: strategy-advanced.md - Advanced Hyperopt: advanced-hyperopt.md - Sandbox Testing: sandbox-testing.md + - Updating Freqtrade: updating.md - Deprecated Features: deprecated.md - Contributors Guide: developer.md theme: From 058d40a72c389ab90c643fe1cede812c4c5038b2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Dec 2020 08:16:40 +0100 Subject: [PATCH 1018/1197] Fix telegram /daily command without arguments --- freqtrade/rpc/telegram.py | 2 +- tests/rpc/test_rpc_telegram.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 7239eab02..91306bf85 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -305,7 +305,7 @@ class Telegram(RPC): stake_cur = self._config['stake_currency'] fiat_disp_cur = self._config.get('fiat_display_currency', '') try: - timescale = int(context.args[0]) if context.args else 0 + timescale = int(context.args[0]) if context.args else 7 except (TypeError, ValueError, IndexError): timescale = 7 try: diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 33010484d..72d263bff 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -337,6 +337,18 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] + # Reset msg_mock + msg_mock.reset_mock() + context.args = [] + telegram._daily(update=update, context=context) + assert msg_mock.call_count == 1 + assert 'Daily' in msg_mock.call_args_list[0][0][0] + assert str(datetime.utcnow().date()) in msg_mock.call_args_list[0][0][0] + assert str(' 0.00006217 BTC') in msg_mock.call_args_list[0][0][0] + assert str(' 0.933 USD') in msg_mock.call_args_list[0][0][0] + assert str(' 1 trade') in msg_mock.call_args_list[0][0][0] + assert str(' 0 trade') in msg_mock.call_args_list[0][0][0] + # Reset msg_mock msg_mock.reset_mock() freqtradebot.config['max_open_trades'] = 2 From c556d1b37e6e6367e1c05362522e494df274275b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Dec 2020 14:06:46 +0100 Subject: [PATCH 1019/1197] Make /stats working --- freqtrade/rpc/rpc.py | 4 ++++ freqtrade/rpc/telegram.py | 21 +++++++++++---------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 9ac271ba0..e17ee6b4f 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -275,6 +275,10 @@ class RPC: "trades_count": len(output) } + def _rpc_stats(self): + trades = trades = Trade.get_trades([Trade.is_open.is_(False)]) + return trades + def _rpc_trade_statistics( self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: """ Returns cumulative profit statistics """ diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 074a6367f..29d2c6a01 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -782,22 +782,22 @@ class Telegram(RPC): """ # TODO: self._send_msg(...) def trade_win_loss(trade): - if trade['profit_abs'] > 0: + if trade.close_profit_abs > 0: return 'Wins' - elif trade['profit_abs'] < 0: + elif trade.close_profit_abs < 0: return 'Losses' else: return 'Draws' - trades = self._rpc_trade_history(-1) - trades_closed = [trade for trade in trades if not trade['is_open']] + trades = self._rpc_stats() + trades_closed = [trade for trade in trades if not trade.is_open] # Sell reason sell_reasons = {} for trade in trades_closed: - if trade['sell_reason'] not in sell_reasons: - sell_reasons[trade['sell_reason']] = {'Wins': 0, 'Losses': 0, 'Draws': 0} - sell_reasons[trade['sell_reason']][trade_win_loss(trade)] += 1 + if trade.sell_reason not in sell_reasons: + sell_reasons[trade.sell_reason] = {'Wins': 0, 'Losses': 0, 'Draws': 0} + sell_reasons[trade.sell_reason][trade_win_loss(trade)] += 1 sell_reasons_tabulate = [] for reason, count in sell_reasons.items(): sell_reasons_tabulate.append([ @@ -814,8 +814,8 @@ class Telegram(RPC): # Duration dur: Dict[str, List[int]] = {'Wins': [], 'Draws': [], 'Losses': []} for trade in trades_closed: - if trade['close_date'] is not None and trade['open_date'] is not None: - trade_dur = arrow.get(trade['close_date']) - arrow.get(trade['open_date']) + if trade.close_date is not None and trade.open_date is not None: + trade_dur = (trade.close_date - trade.open_date).total_seconds() dur[trade_win_loss(trade)].append(trade_dur) wins_dur = sum(dur['Wins']) / len(dur['Wins']) if len(dur['Wins']) > 0 else 'N/A' draws_dur = sum(dur['Draws']) / len(dur['Draws']) if len(dur['Draws']) > 0 else 'N/A' @@ -824,8 +824,9 @@ class Telegram(RPC): [['Wins', str(wins_dur)], ['Draws', str(draws_dur)], ['Losses', str(losses_dur)]], headers=['', 'Duration'] ) + msg = (f"""```{sell_reasons_msg}```\n```{duration_msg}```""") - self._send_msg('\n'.join([sell_reasons_msg, duration_msg])) + self._send_msg(msg, ParseMode.MARKDOWN) @authorized_only def _show_config(self, update: Update, context: CallbackContext) -> None: From 143423145cacec63868390045c1e911390dba327 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Dec 2020 14:38:42 +0100 Subject: [PATCH 1020/1197] Refactor most of the logic to rpc.py this way /stats can be used by other RPC methods too --- freqtrade/rpc/rpc.py | 31 +++++++++++++++++++- freqtrade/rpc/telegram.py | 59 ++++++++++++++++----------------------- 2 files changed, 54 insertions(+), 36 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e17ee6b4f..d7a59390d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -276,8 +276,37 @@ class RPC: } def _rpc_stats(self): + """ + Generate generic stats for trades in database + """ + def trade_win_loss(trade): + if trade.close_profit_abs > 0: + return 'Wins' + elif trade.close_profit_abs < 0: + return 'Losses' + else: + return 'Draws' trades = trades = Trade.get_trades([Trade.is_open.is_(False)]) - return trades + # Sell reason + sell_reasons = {} + for trade in trades: + if trade.sell_reason not in sell_reasons: + sell_reasons[trade.sell_reason] = {'Wins': 0, 'Losses': 0, 'Draws': 0} + sell_reasons[trade.sell_reason][trade_win_loss(trade)] += 1 + + # Duration + dur: Dict[str, List[int]] = {'Wins': [], 'Draws': [], 'Losses': []} + for trade in trades: + if trade.close_date is not None and trade.open_date is not None: + trade_dur = (trade.close_date - trade.open_date).total_seconds() + dur[trade_win_loss(trade)].append(trade_dur) + + wins_dur = sum(dur['Wins']) / len(dur['Wins']) if len(dur['Wins']) > 0 else 'N/A' + draws_dur = sum(dur['Draws']) / len(dur['Draws']) if len(dur['Draws']) > 0 else 'N/A' + losses_dur = sum(dur['Losses']) / len(dur['Losses']) if len(dur['Losses']) > 0 else 'N/A' + + durations = {'wins': wins_dur, 'draws': draws_dur, 'losses': losses_dur} + return sell_reasons, durations def _rpc_trade_statistics( self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 29d2c6a01..7c7007f86 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -3,6 +3,7 @@ """ This module manage Telegram communication """ +from datetime import timedelta import json import logging from typing import Any, Callable, Dict, List, Union @@ -775,56 +776,44 @@ class Telegram(RPC): def _stats(self, update: Update, context: CallbackContext) -> None: """ Handler for /stats - https://github.com/freqtrade/freqtrade/issues/3783 Show stats of recent trades - :param update: message update :return: None """ - # TODO: self._send_msg(...) - def trade_win_loss(trade): - if trade.close_profit_abs > 0: - return 'Wins' - elif trade.close_profit_abs < 0: - return 'Losses' - else: - return 'Draws' + sell_reasons, durations = self._rpc_stats() - trades = self._rpc_stats() - trades_closed = [trade for trade in trades if not trade.is_open] - - # Sell reason - sell_reasons = {} - for trade in trades_closed: - if trade.sell_reason not in sell_reasons: - sell_reasons[trade.sell_reason] = {'Wins': 0, 'Losses': 0, 'Draws': 0} - sell_reasons[trade.sell_reason][trade_win_loss(trade)] += 1 sell_reasons_tabulate = [] + reason_map = { + 'roi': 'ROI', + 'stop_loss': 'Stoploss', + 'trailing_stop_loss': 'Trail. Stop', + 'stoploss_on_exchange': 'Stoploss', + 'sell_signal': 'Sell Signal', + 'force_sell': 'Forcesell', + 'emergency_sell': 'Emergency Sell', + } for reason, count in sell_reasons.items(): sell_reasons_tabulate.append([ - reason, sum(count.values()), + reason_map.get(reason, reason), + sum(count.values()), count['Wins'], - count['Draws'], + # count['Draws'], count['Losses'] ]) sell_reasons_msg = tabulate( sell_reasons_tabulate, - headers=['Sell Reason', 'Sells', 'Wins', 'Draws', 'Losses'] + headers=['Sell Reason', 'Sells', 'Wins', 'Losses'] ) - # Duration - dur: Dict[str, List[int]] = {'Wins': [], 'Draws': [], 'Losses': []} - for trade in trades_closed: - if trade.close_date is not None and trade.open_date is not None: - trade_dur = (trade.close_date - trade.open_date).total_seconds() - dur[trade_win_loss(trade)].append(trade_dur) - wins_dur = sum(dur['Wins']) / len(dur['Wins']) if len(dur['Wins']) > 0 else 'N/A' - draws_dur = sum(dur['Draws']) / len(dur['Draws']) if len(dur['Draws']) > 0 else 'N/A' - losses_dur = sum(dur['Losses']) / len(dur['Losses']) if len(dur['Losses']) > 0 else 'N/A' - duration_msg = tabulate( - [['Wins', str(wins_dur)], ['Draws', str(draws_dur)], ['Losses', str(losses_dur)]], - headers=['', 'Duration'] + duration_msg = tabulate([ + ['Wins', str(timedelta(seconds=durations['wins'])) + if durations['wins'] != 'N/A' else 'N/A'], + # ['Draws', str(timedelta(seconds=durations['draws']))], + ['Losses', str(timedelta(seconds=durations['losses'])) + if durations['losses'] != 'N/A' else 'N/A'] + ], + headers=['', 'Avg. Duration'] ) - msg = (f"""```{sell_reasons_msg}```\n```{duration_msg}```""") + msg = (f"""```\n{sell_reasons_msg}```\n```\n{duration_msg}```""") self._send_msg(msg, ParseMode.MARKDOWN) From aa27c9ace2fa3ac9b83780de5f1d0e4a9bd70fbf Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Dec 2020 14:39:50 +0100 Subject: [PATCH 1021/1197] Reorder methods in telegram /stats is closely related to /profit --- freqtrade/rpc/telegram.py | 90 +++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 7c7007f86..76d9292b4 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -390,6 +390,51 @@ class Telegram(RPC): f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`") self._send_msg(markdown_msg) + @authorized_only + def _stats(self, update: Update, context: CallbackContext) -> None: + """ + Handler for /stats + Show stats of recent trades + :return: None + """ + sell_reasons, durations = self._rpc_stats() + + sell_reasons_tabulate = [] + reason_map = { + 'roi': 'ROI', + 'stop_loss': 'Stoploss', + 'trailing_stop_loss': 'Trail. Stop', + 'stoploss_on_exchange': 'Stoploss', + 'sell_signal': 'Sell Signal', + 'force_sell': 'Forcesell', + 'emergency_sell': 'Emergency Sell', + } + for reason, count in sell_reasons.items(): + sell_reasons_tabulate.append([ + reason_map.get(reason, reason), + sum(count.values()), + count['Wins'], + # count['Draws'], + count['Losses'] + ]) + sell_reasons_msg = tabulate( + sell_reasons_tabulate, + headers=['Sell Reason', 'Sells', 'Wins', 'Losses'] + ) + + duration_msg = tabulate([ + ['Wins', str(timedelta(seconds=durations['wins'])) + if durations['wins'] != 'N/A' else 'N/A'], + # ['Draws', str(timedelta(seconds=durations['draws']))], + ['Losses', str(timedelta(seconds=durations['losses'])) + if durations['losses'] != 'N/A' else 'N/A'] + ], + headers=['', 'Avg. Duration'] + ) + msg = (f"""```\n{sell_reasons_msg}```\n```\n{duration_msg}```""") + + self._send_msg(msg, ParseMode.MARKDOWN) + @authorized_only def _balance(self, update: Update, context: CallbackContext) -> None: """ Handler for /balance """ @@ -772,51 +817,6 @@ class Telegram(RPC): """ self._send_msg('*Version:* `{}`'.format(__version__)) - @authorized_only - def _stats(self, update: Update, context: CallbackContext) -> None: - """ - Handler for /stats - Show stats of recent trades - :return: None - """ - sell_reasons, durations = self._rpc_stats() - - sell_reasons_tabulate = [] - reason_map = { - 'roi': 'ROI', - 'stop_loss': 'Stoploss', - 'trailing_stop_loss': 'Trail. Stop', - 'stoploss_on_exchange': 'Stoploss', - 'sell_signal': 'Sell Signal', - 'force_sell': 'Forcesell', - 'emergency_sell': 'Emergency Sell', - } - for reason, count in sell_reasons.items(): - sell_reasons_tabulate.append([ - reason_map.get(reason, reason), - sum(count.values()), - count['Wins'], - # count['Draws'], - count['Losses'] - ]) - sell_reasons_msg = tabulate( - sell_reasons_tabulate, - headers=['Sell Reason', 'Sells', 'Wins', 'Losses'] - ) - - duration_msg = tabulate([ - ['Wins', str(timedelta(seconds=durations['wins'])) - if durations['wins'] != 'N/A' else 'N/A'], - # ['Draws', str(timedelta(seconds=durations['draws']))], - ['Losses', str(timedelta(seconds=durations['losses'])) - if durations['losses'] != 'N/A' else 'N/A'] - ], - headers=['', 'Avg. Duration'] - ) - msg = (f"""```\n{sell_reasons_msg}```\n```\n{duration_msg}```""") - - self._send_msg(msg, ParseMode.MARKDOWN) - @authorized_only def _show_config(self, update: Update, context: CallbackContext) -> None: """ From 245c19f5e9aff5a797e0f1d71924d552b1f86a1c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Dec 2020 14:48:56 +0100 Subject: [PATCH 1022/1197] Add simple test for /stats call --- freqtrade/rpc/rpc.py | 4 ++-- tests/conftest_trades.py | 2 ++ tests/rpc/test_rpc_telegram.py | 35 ++++++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index d7a59390d..c4b4117ff 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -280,9 +280,9 @@ class RPC: Generate generic stats for trades in database """ def trade_win_loss(trade): - if trade.close_profit_abs > 0: + if trade.close_profit > 0: return 'Wins' - elif trade.close_profit_abs < 0: + elif trade.close_profit < 0: return 'Losses' else: return 'Draws' diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 78388f022..fac822b2b 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -82,6 +82,7 @@ def mock_trade_2(fee): is_open=False, open_order_id='dry_run_sell_12345', strategy='DefaultStrategy', + sell_reason='sell_signal' ) o = Order.parse_from_ccxt_object(mock_order_2(), 'ETC/BTC', 'buy') trade.orders.append(o) @@ -134,6 +135,7 @@ def mock_trade_3(fee): close_profit=0.01, exchange='bittrex', is_open=False, + sell_reason='roi' ) o = Order.parse_from_ccxt_object(mock_order_3(), 'XRP/BTC', 'buy') trade.orders.append(o) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 73a549860..725c1411e 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -469,6 +469,41 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0] +def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee, + limit_buy_order, limit_sell_order, mocker) -> None: + mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker, + get_fee=fee, + ) + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + patch_get_signal(freqtradebot, (True, False)) + telegram = Telegram(freqtradebot) + + telegram._stats(update=update, context=MagicMock()) + assert msg_mock.call_count == 1 + # assert 'No trades yet.' in msg_mock.call_args_list[0][0][0] + msg_mock.reset_mock() + + # Create some test data + create_mock_trades(fee) + + telegram._stats(update=update, context=MagicMock()) + assert msg_mock.call_count == 1 + assert 'Sell Reason' in msg_mock.call_args_list[-1][0][0] + assert 'ROI' in msg_mock.call_args_list[-1][0][0] + assert 'Avg. Duration' in msg_mock.call_args_list[-1][0][0] + msg_mock.reset_mock() + + 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) From 51fbd0698c63f0ab909e45532d4dc3e64cee35a2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 6 Dec 2020 19:57:48 +0100 Subject: [PATCH 1023/1197] Move get_logs to be static method --- freqtrade/rpc/api_server.py | 2 +- freqtrade/rpc/rpc.py | 3 ++- freqtrade/rpc/telegram.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 8c2c203e6..e8eaef933 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -388,7 +388,7 @@ class ApiServer(RPC): limit: Only get a certain number of records """ limit = int(request.args.get('limit', 0)) or None - return jsonify(self._rpc_get_logs(limit)) + return jsonify(RPC._rpc_get_logs(limit)) @require_login @rpc_catch_errors diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 9ac271ba0..42f26fe74 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -645,7 +645,8 @@ class RPC: } return res - def _rpc_get_logs(self, limit: Optional[int]) -> Dict[str, Any]: + @staticmethod + def _rpc_get_logs(limit: Optional[int]) -> Dict[str, Any]: """Returns the last X logs""" if limit: buffer = bufferHandler.buffer[-limit:] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 91306bf85..26e87e654 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -683,7 +683,7 @@ class Telegram(RPC): limit = int(context.args[0]) if context.args else 10 except (TypeError, ValueError, IndexError): limit = 10 - logs = self._rpc_get_logs(limit)['logs'] + logs = RPC._rpc_get_logs(limit)['logs'] msgs = '' msg_template = "*{}* {}: {} \\- `{}`" for logrec in logs: From 0c0eb8236d24f84efaad4b51bd768ff23ea462f6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Dec 2020 05:48:23 +0000 Subject: [PATCH 1024/1197] Bump mkdocs-material from 6.1.6 to 6.1.7 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 6.1.6 to 6.1.7. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/6.1.6...6.1.7) Signed-off-by: dependabot[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 87bc6dfdd..2b133cb07 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==6.1.6 +mkdocs-material==6.1.7 mdx_truly_sane_lists==1.2 pymdown-extensions==8.0.1 From 647e6509a477b9b07b7e9f9d73ee3ad756d2b499 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Dec 2020 05:48:43 +0000 Subject: [PATCH 1025/1197] Bump ccxt from 1.38.55 to 1.38.87 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.38.55 to 1.38.87. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.38.55...1.38.87) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f59754f93..105839f0d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.4 pandas==1.1.4 -ccxt==1.38.55 +ccxt==1.38.87 aiohttp==3.7.3 SQLAlchemy==1.3.20 python-telegram-bot==13.1 From b6b9c8e5cc401bb5f876d74515b652cb5a5e6537 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Oct 2020 07:11:08 +0200 Subject: [PATCH 1026/1197] Move "slow-log" to it's own mixin --- freqtrade/mixins/__init__.py | 2 ++ freqtrade/mixins/logging_mixin.py | 34 +++++++++++++++++++++++++++++++ freqtrade/pairlist/IPairList.py | 25 +++-------------------- 3 files changed, 39 insertions(+), 22 deletions(-) create mode 100644 freqtrade/mixins/__init__.py create mode 100644 freqtrade/mixins/logging_mixin.py diff --git a/freqtrade/mixins/__init__.py b/freqtrade/mixins/__init__.py new file mode 100644 index 000000000..f4a640fa3 --- /dev/null +++ b/freqtrade/mixins/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa: F401 +from freqtrade.mixins.logging_mixin import LoggingMixin diff --git a/freqtrade/mixins/logging_mixin.py b/freqtrade/mixins/logging_mixin.py new file mode 100644 index 000000000..4e19e45a4 --- /dev/null +++ b/freqtrade/mixins/logging_mixin.py @@ -0,0 +1,34 @@ + + +from cachetools import TTLCache, cached + + +class LoggingMixin(): + """ + Logging Mixin + Shows similar messages only once every `refresh_period`. + """ + def __init__(self, logger, refresh_period: int = 3600): + """ + :param refresh_period: in seconds - Show identical messages in this intervals + """ + self.logger = logger + self.refresh_period = refresh_period + self._log_cache: TTLCache = TTLCache(maxsize=1024, ttl=self.refresh_period) + + def log_on_refresh(self, logmethod, message: str) -> None: + """ + Logs message - not more often than "refresh_period" to avoid log spamming + Logs the log-message as debug as well to simplify debugging. + :param logmethod: Function that'll be called. Most likely `logger.info`. + :param message: String containing the message to be sent to the function. + :return: None. + """ + @cached(cache=self._log_cache) + def _log_on_refresh(message: str): + logmethod(message) + + # Log as debug first + self.logger.debug(message) + # Call hidden function. + _log_on_refresh(message) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index c869e499b..5f29241ce 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -6,16 +6,15 @@ from abc import ABC, abstractmethod, abstractproperty from copy import deepcopy from typing import Any, Dict, List -from cachetools import TTLCache, cached - from freqtrade.exceptions import OperationalException from freqtrade.exchange import market_is_active +from freqtrade.mixins import LoggingMixin logger = logging.getLogger(__name__) -class IPairList(ABC): +class IPairList(LoggingMixin, ABC): def __init__(self, exchange, pairlistmanager, config: Dict[str, Any], pairlistconfig: Dict[str, Any], @@ -36,7 +35,7 @@ class IPairList(ABC): self._pairlist_pos = pairlist_pos self.refresh_period = self._pairlistconfig.get('refresh_period', 1800) self._last_refresh = 0 - self._log_cache: TTLCache = TTLCache(maxsize=1024, ttl=self.refresh_period) + LoggingMixin.__init__(self, logger, self.refresh_period) @property def name(self) -> str: @@ -46,24 +45,6 @@ class IPairList(ABC): """ return self.__class__.__name__ - def log_on_refresh(self, logmethod, message: str) -> None: - """ - Logs message - not more often than "refresh_period" to avoid log spamming - Logs the log-message as debug as well to simplify debugging. - :param logmethod: Function that'll be called. Most likely `logger.info`. - :param message: String containing the message to be sent to the function. - :return: None. - """ - - @cached(cache=self._log_cache) - def _log_on_refresh(message: str): - logmethod(message) - - # Log as debug first - logger.debug(message) - # Call hidden function. - _log_on_refresh(message) - @abstractproperty def needstickers(self) -> bool: """ From a0bd2ce837bb61a4e335a1980239f536101e3a70 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Oct 2020 08:06:29 +0200 Subject: [PATCH 1027/1197] Add first version of protection manager --- freqtrade/plugins/__init__.py | 0 freqtrade/plugins/protectionmanager.py | 51 ++++++++++++++++++++ freqtrade/plugins/protections/__init__.py | 2 + freqtrade/plugins/protections/iprotection.py | 24 +++++++++ freqtrade/resolvers/__init__.py | 1 + freqtrade/resolvers/protection_resolver.py | 44 +++++++++++++++++ 6 files changed, 122 insertions(+) create mode 100644 freqtrade/plugins/__init__.py create mode 100644 freqtrade/plugins/protectionmanager.py create mode 100644 freqtrade/plugins/protections/__init__.py create mode 100644 freqtrade/plugins/protections/iprotection.py create mode 100644 freqtrade/resolvers/protection_resolver.py diff --git a/freqtrade/plugins/__init__.py b/freqtrade/plugins/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py new file mode 100644 index 000000000..ff64ca789 --- /dev/null +++ b/freqtrade/plugins/protectionmanager.py @@ -0,0 +1,51 @@ +""" +Protection manager class +""" +import logging +from typing import Dict, List + +from freqtrade.exceptions import OperationalException +from freqtrade.plugins.protections import IProtection +from freqtrade.resolvers import ProtectionResolver + + +logger = logging.getLogger(__name__) + + +class ProtectionManager(): + + def __init__(self, exchange, config: dict) -> None: + self._exchange = exchange + self._config = config + + self._protection_handlers: List[IProtection] = [] + self._tickers_needed = False + for protection_handler_config in self._config.get('protections', None): + if 'method' not in protection_handler_config: + logger.warning(f"No method found in {protection_handler_config}, ignoring.") + continue + protection_handler = ProtectionResolver.load_protection( + protection_handler_config['method'], + exchange=exchange, + protectionmanager=self, + config=config, + protection_config=protection_handler_config, + ) + self._tickers_needed |= protection_handler.needstickers + self._protection_handlers.append(protection_handler) + + if not self._protection_handlers: + raise OperationalException("No protection Handlers defined") + + @property + def name_list(self) -> List[str]: + """ + Get list of loaded Protection Handler names + """ + return [p.name for p in self._protection_handlers] + + def short_desc(self) -> List[Dict]: + """ + List of short_desc for each Pairlist Handler + """ + return [{p.name: p.short_desc()} for p in self._pairlist_handlers] diff --git a/freqtrade/plugins/protections/__init__.py b/freqtrade/plugins/protections/__init__.py new file mode 100644 index 000000000..5ecae7888 --- /dev/null +++ b/freqtrade/plugins/protections/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa: F401 +from freqtrade.plugins.protections.iprotection import IProtection diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py new file mode 100644 index 000000000..b10856f70 --- /dev/null +++ b/freqtrade/plugins/protections/iprotection.py @@ -0,0 +1,24 @@ + +import logging +from abc import ABC, abstractmethod +from typing import Any, Dict + + +logger = logging.getLogger(__name__) + + +class IProtection(ABC): + + def __init__(self, config: Dict[str, Any]) -> None: + self._config = config + + @property + def name(self) -> str: + return self.__class__.__name__ + + @abstractmethod + def short_desc(self) -> str: + """ + Short method description - used for startup-messages + -> Please overwrite in subclasses + """ diff --git a/freqtrade/resolvers/__init__.py b/freqtrade/resolvers/__init__.py index b42ec4931..ef24bf481 100644 --- a/freqtrade/resolvers/__init__.py +++ b/freqtrade/resolvers/__init__.py @@ -6,6 +6,7 @@ from freqtrade.resolvers.exchange_resolver import ExchangeResolver # Don't import HyperoptResolver to avoid loading the whole Optimize tree # from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.resolvers.pairlist_resolver import PairListResolver +from freqtrade.resolvers.protection_resolver import ProtectionResolver from freqtrade.resolvers.strategy_resolver import StrategyResolver diff --git a/freqtrade/resolvers/protection_resolver.py b/freqtrade/resolvers/protection_resolver.py new file mode 100644 index 000000000..9a85104c3 --- /dev/null +++ b/freqtrade/resolvers/protection_resolver.py @@ -0,0 +1,44 @@ +# pragma pylint: disable=attribute-defined-outside-init + +""" +This module load custom pairlists +""" +import logging +from pathlib import Path +from typing import Dict + +from freqtrade.plugins.protections import IProtection +from freqtrade.resolvers import IResolver + + +logger = logging.getLogger(__name__) + + +class ProtectionResolver(IResolver): + """ + This class contains all the logic to load custom PairList class + """ + object_type = IProtection + object_type_str = "Protection" + user_subdir = None + initial_search_path = Path(__file__).parent.parent.joinpath('plugins/protections').resolve() + + @staticmethod + def load_protection(protection_name: str, exchange, protectionmanager, + config: Dict, protection_config: Dict) -> IProtection: + """ + Load the protection with protection_name + :param protection_name: Classname of the pairlist + :param exchange: Initialized exchange class + :param protectionmanager: Initialized protection manager + :param config: configuration dictionary + :param protection_config: Configuration dedicated to this pairlist + :return: initialized Protection class + """ + return ProtectionResolver.load_object(protection_name, config, + kwargs={'exchange': exchange, + 'pairlistmanager': protectionmanager, + 'config': config, + 'pairlistconfig': protection_config, + }, + ) From 3447f1ae531733eabd620b15b849fb48e204ae6d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Oct 2020 07:40:44 +0200 Subject: [PATCH 1028/1197] Implement first stop method --- config_full.json.example | 7 +++ docs/includes/protections.md | 36 ++++++++++++ freqtrade/plugins/protectionmanager.py | 5 +- freqtrade/plugins/protections/iprotection.py | 11 +++- .../plugins/protections/stoploss_guard.py | 55 +++++++++++++++++++ freqtrade/resolvers/protection_resolver.py | 11 +--- 6 files changed, 112 insertions(+), 13 deletions(-) create mode 100644 docs/includes/protections.md create mode 100644 freqtrade/plugins/protections/stoploss_guard.py diff --git a/config_full.json.example b/config_full.json.example index 5ee2a1faf..96aa82d5f 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -75,6 +75,13 @@ "refresh_period": 1440 } ], + "protections": [ + { + "method": "StoplossGuard", + "lookback_period": 60, + "trade_limit": 4 + } + ], "exchange": { "name": "bittrex", "sandbox": false, diff --git a/docs/includes/protections.md b/docs/includes/protections.md new file mode 100644 index 000000000..078ba0c2b --- /dev/null +++ b/docs/includes/protections.md @@ -0,0 +1,36 @@ +## Protections + +Protections will protect your strategy from unexpected events and market conditions. + +### Available Protection Handlers + +* [`StoplossGuard`](#stoploss-guard) (default, if not configured differently) + +#### Stoploss Guard + +`StoplossGuard` selects all trades within a `lookback_period` (in minutes), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case it will stop trading until this condition is no longer true. + +```json +"protections": [{ + "method": "StoplossGuard", + "lookback_period": 60, + "trade_limit": 4 +}], +``` + +!!! Note + `StoplossGuard` considers all trades with the results `"stop_loss"` and `"trailing_stop_loss"` if the result was negative. + +### Full example of Protections + +The below example stops trading if more than 4 stoploss occur within a 1 hour (60 minute) limit. + +```json +"protections": [ + { + "method": "StoplossGuard", + "lookback_period": 60, + "trade_limit": 4 + } + ], +``` diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index ff64ca789..5185c93f0 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -14,8 +14,7 @@ logger = logging.getLogger(__name__) class ProtectionManager(): - def __init__(self, exchange, config: dict) -> None: - self._exchange = exchange + def __init__(self, config: dict) -> None: self._config = config self._protection_handlers: List[IProtection] = [] @@ -26,8 +25,6 @@ class ProtectionManager(): continue protection_handler = ProtectionResolver.load_protection( protection_handler_config['method'], - exchange=exchange, - protectionmanager=self, config=config, protection_config=protection_handler_config, ) diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index b10856f70..75d1fb3ad 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -1,6 +1,7 @@ import logging from abc import ABC, abstractmethod +from datetime import datetime from typing import Any, Dict @@ -9,8 +10,9 @@ logger = logging.getLogger(__name__) class IProtection(ABC): - def __init__(self, config: Dict[str, Any]) -> None: + def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: self._config = config + self._protection_config = protection_config @property def name(self) -> str: @@ -22,3 +24,10 @@ class IProtection(ABC): Short method description - used for startup-messages -> Please overwrite in subclasses """ + + @abstractmethod + def stop_trade_enters_global(self, date_now: datetime) -> bool: + """ + Stops trading (position entering) for all pairs + This must evaluate to true for the whole period of the "cooldown period". + """ diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py new file mode 100644 index 000000000..3418dd1da --- /dev/null +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -0,0 +1,55 @@ + +import logging +from datetime import datetime, timedelta +from typing import Any, Dict + +from sqlalchemy import or_, and_ + +from freqtrade.persistence import Trade +from freqtrade.plugins.protections import IProtection +from freqtrade.strategy.interface import SellType + + +logger = logging.getLogger(__name__) + + +class StoplossGuard(IProtection): + + def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: + super().__init__(config, protection_config) + self._lookback_period = protection_config.get('lookback_period', 60) + self._trade_limit = protection_config.get('trade_limit', 10) + + def short_desc(self) -> str: + """ + Short method description - used for startup-messages + """ + return f"{self.name} - Frequent Stoploss Guard" + + def _stoploss_guard(self, date_now: datetime, pair: str = None) -> bool: + """ + Evaluate recent trades + """ + look_back_until = date_now - timedelta(minutes=self._lookback_period) + filters = [ + Trade.is_open.is_(False), + Trade.close_date > look_back_until, + or_(Trade.sell_reason == SellType.STOP_LOSS.value, + and_(Trade.sell_reason == SellType.TRAILING_STOP_LOSS.value, + Trade.close_profit < 0)) + ] + if pair: + filters.append(Trade.pair == pair) + trades = Trade.get_trades(filters).all() + + if len(trades) > self.trade_limit: + return True + + return False + + def stop_trade_enters_global(self, date_now: datetime) -> bool: + """ + Stops trading (position entering) for all pairs + This must evaluate to true for the whole period of the "cooldown period". + """ + return self._stoploss_guard(date_now, pair=None) diff --git a/freqtrade/resolvers/protection_resolver.py b/freqtrade/resolvers/protection_resolver.py index 9a85104c3..928bd4633 100644 --- a/freqtrade/resolvers/protection_resolver.py +++ b/freqtrade/resolvers/protection_resolver.py @@ -24,21 +24,16 @@ class ProtectionResolver(IResolver): initial_search_path = Path(__file__).parent.parent.joinpath('plugins/protections').resolve() @staticmethod - def load_protection(protection_name: str, exchange, protectionmanager, - config: Dict, protection_config: Dict) -> IProtection: + def load_protection(protection_name: str, config: Dict, protection_config: Dict) -> IProtection: """ Load the protection with protection_name :param protection_name: Classname of the pairlist - :param exchange: Initialized exchange class - :param protectionmanager: Initialized protection manager :param config: configuration dictionary :param protection_config: Configuration dedicated to this pairlist :return: initialized Protection class """ return ProtectionResolver.load_object(protection_name, config, - kwargs={'exchange': exchange, - 'pairlistmanager': protectionmanager, - 'config': config, - 'pairlistconfig': protection_config, + kwargs={'config': config, + 'protection_config': protection_config, }, ) From 04878c3ce1806536dcb46d9bbdc1dd38b32f88fc Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Oct 2020 07:41:41 +0200 Subject: [PATCH 1029/1197] Rename test directory for pairlist --- tests/{pairlist => plugins}/__init__.py | 0 tests/{pairlist => plugins}/test_pairlist.py | 0 tests/{pairlist => plugins}/test_pairlocks.py | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename tests/{pairlist => plugins}/__init__.py (100%) rename tests/{pairlist => plugins}/test_pairlist.py (100%) rename tests/{pairlist => plugins}/test_pairlocks.py (100%) diff --git a/tests/pairlist/__init__.py b/tests/plugins/__init__.py similarity index 100% rename from tests/pairlist/__init__.py rename to tests/plugins/__init__.py diff --git a/tests/pairlist/test_pairlist.py b/tests/plugins/test_pairlist.py similarity index 100% rename from tests/pairlist/test_pairlist.py rename to tests/plugins/test_pairlist.py diff --git a/tests/pairlist/test_pairlocks.py b/tests/plugins/test_pairlocks.py similarity index 100% rename from tests/pairlist/test_pairlocks.py rename to tests/plugins/test_pairlocks.py From 246b4a57a40f25750840e515d6f8119c2a5be291 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Oct 2020 19:24:09 +0200 Subject: [PATCH 1030/1197] add small note to pairlist dev docs --- docs/developer.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/developer.md b/docs/developer.md index c253f4460..662905d65 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -119,6 +119,9 @@ The base-class provides an instance of the exchange (`self._exchange`) the pairl self._pairlist_pos = pairlist_pos ``` +!!! Note + You'll need to register your pairlist in `constants.py` under the variable `AVAILABLE_PAIRLISTS` - otherwise it will not be selectable. + Now, let's step through the methods which require actions: #### Pairlist configuration From f39a534fc039795af1eb45761d998b221e9a1867 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Oct 2020 20:03:56 +0200 Subject: [PATCH 1031/1197] Implement global stop (First try) --- freqtrade/constants.py | 1 + freqtrade/freqtradebot.py | 5 ++++- freqtrade/plugins/__init__.py | 2 ++ freqtrade/plugins/protectionmanager.py | 20 +++++++++++++------ freqtrade/plugins/protections/iprotection.py | 2 +- .../plugins/protections/stoploss_guard.py | 2 +- 6 files changed, 23 insertions(+), 9 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 601e525c1..d070386d0 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -27,6 +27,7 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter'] +AVAILABLE_PROTECTIONS = ['StoplossGuard'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S' diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c8d281852..2dbd7f099 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -23,6 +23,7 @@ from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db +from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.state import State @@ -78,6 +79,8 @@ class FreqtradeBot: self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists) + self.protections = ProtectionManager(self.config) + # Attach Dataprovider to Strategy baseclass IStrategy.dp = self.dataprovider # Attach Wallets to Strategy baseclass @@ -178,7 +181,7 @@ class FreqtradeBot: self.exit_positions(trades) # Then looking for buy opportunities - if self.get_free_open_trades(): + if self.get_free_open_trades() and not self.protections.global_stop(): self.enter_positions() Trade.session.flush() diff --git a/freqtrade/plugins/__init__.py b/freqtrade/plugins/__init__.py index e69de29bb..96943268b 100644 --- a/freqtrade/plugins/__init__.py +++ b/freqtrade/plugins/__init__.py @@ -0,0 +1,2 @@ +# flake8: noqa: F401 +# from freqtrade.plugins.protectionmanager import ProtectionManager diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index 5185c93f0..31b0ca300 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -7,7 +7,7 @@ from typing import Dict, List from freqtrade.exceptions import OperationalException from freqtrade.plugins.protections import IProtection from freqtrade.resolvers import ProtectionResolver - +from datetime import datetime logger = logging.getLogger(__name__) @@ -18,8 +18,7 @@ class ProtectionManager(): self._config = config self._protection_handlers: List[IProtection] = [] - self._tickers_needed = False - for protection_handler_config in self._config.get('protections', None): + for protection_handler_config in self._config.get('protections', []): if 'method' not in protection_handler_config: logger.warning(f"No method found in {protection_handler_config}, ignoring.") continue @@ -28,11 +27,10 @@ class ProtectionManager(): config=config, protection_config=protection_handler_config, ) - self._tickers_needed |= protection_handler.needstickers self._protection_handlers.append(protection_handler) if not self._protection_handlers: - raise OperationalException("No protection Handlers defined") + logger.info("No protection Handlers defined.") @property def name_list(self) -> List[str]: @@ -45,4 +43,14 @@ class ProtectionManager(): """ List of short_desc for each Pairlist Handler """ - return [{p.name: p.short_desc()} for p in self._pairlist_handlers] + return [{p.name: p.short_desc()} for p in self._protection_handlers] + + def global_stop(self) -> bool: + now = datetime.utcnow() + for protection_handler in self._protection_handlers: + result = protection_handler.global_stop(now) + + # Early stopping - first positive result stops the application + if result: + return True + return False diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 75d1fb3ad..25bcee923 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -26,7 +26,7 @@ class IProtection(ABC): """ @abstractmethod - def stop_trade_enters_global(self, date_now: datetime) -> bool: + def global_stop(self, date_now: datetime) -> bool: """ Stops trading (position entering) for all pairs This must evaluate to true for the whole period of the "cooldown period". diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 3418dd1da..c6cddb01e 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -47,7 +47,7 @@ class StoplossGuard(IProtection): return False - def stop_trade_enters_global(self, date_now: datetime) -> bool: + def global_stop(self, date_now: datetime) -> bool: """ Stops trading (position entering) for all pairs This must evaluate to true for the whole period of the "cooldown period". From 816703b8e112d161727367664b7bbfdf2159b5d9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Oct 2020 07:38:00 +0200 Subject: [PATCH 1032/1197] Improve protections work --- freqtrade/constants.py | 11 ++++++++++- freqtrade/plugins/protectionmanager.py | 4 +++- freqtrade/plugins/protections/iprotection.py | 5 ++++- freqtrade/plugins/protections/stoploss_guard.py | 6 ++++-- 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index d070386d0..9a93bfae3 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -193,7 +193,16 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'method': {'type': 'string', 'enum': AVAILABLE_PAIRLISTS}, - 'config': {'type': 'object'} + }, + 'required': ['method'], + } + }, + 'protections': { + 'type': 'array', + 'items': { + 'type': 'object', + 'properties': { + 'method': {'type': 'string', 'enum': AVAILABLE_PROTECTIONS}, }, 'required': ['method'], } diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index 31b0ca300..dd6076ec1 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -2,12 +2,13 @@ Protection manager class """ import logging +from datetime import datetime from typing import Dict, List from freqtrade.exceptions import OperationalException from freqtrade.plugins.protections import IProtection from freqtrade.resolvers import ProtectionResolver -from datetime import datetime + logger = logging.getLogger(__name__) @@ -47,6 +48,7 @@ class ProtectionManager(): def global_stop(self) -> bool: now = datetime.utcnow() + for protection_handler in self._protection_handlers: result = protection_handler.global_stop(now) diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 25bcee923..ecb4cad09 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -4,15 +4,18 @@ from abc import ABC, abstractmethod from datetime import datetime from typing import Any, Dict +from freqtrade.mixins import LoggingMixin + logger = logging.getLogger(__name__) -class IProtection(ABC): +class IProtection(LoggingMixin, ABC): def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: self._config = config self._protection_config = protection_config + LoggingMixin.__init__(self, logger) @property def name(self) -> str: diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index c6cddb01e..3b0b8c773 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -3,7 +3,7 @@ import logging from datetime import datetime, timedelta from typing import Any, Dict -from sqlalchemy import or_, and_ +from sqlalchemy import and_, or_ from freqtrade.persistence import Trade from freqtrade.plugins.protections import IProtection @@ -42,7 +42,9 @@ class StoplossGuard(IProtection): filters.append(Trade.pair == pair) trades = Trade.get_trades(filters).all() - if len(trades) > self.trade_limit: + if len(trades) > self._trade_limit: + self.log_on_refresh(logger.info, f"Trading stopped due to {self._trade_limit} " + f"stoplosses within {self._lookback_period} minutes.") return True return False From 2b85e7eac3b8936d777eb751f15899afd4aa214f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Oct 2020 07:59:32 +0200 Subject: [PATCH 1033/1197] Add initial tests for StoplossGuard protection --- tests/plugins/test_protections.py | 79 +++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 tests/plugins/test_protections.py diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py new file mode 100644 index 000000000..2bb0886ee --- /dev/null +++ b/tests/plugins/test_protections.py @@ -0,0 +1,79 @@ +from freqtrade.strategy.interface import SellType +from unittest.mock import MagicMock, PropertyMock +import random +import pytest +from datetime import datetime, timedelta + +from freqtrade.constants import AVAILABLE_PROTECTIONS +from freqtrade.persistence import Trade +from tests.conftest import get_patched_freqtradebot, log_has_re + + +def generate_mock_trade(pair: str, fee: float, is_open: bool, + sell_reason: str = SellType.SELL_SIGNAL, + min_ago_open: int = None, min_ago_close: int = None, + ): + open_rate = random.random() + + trade = Trade( + pair=pair, + stake_amount=0.01, + fee_open=fee, + fee_close=fee, + open_date=datetime.utcnow() - timedelta(minutes=min_ago_open or 200), + close_date=datetime.utcnow() - timedelta(minutes=min_ago_close or 30), + open_rate=open_rate, + is_open=is_open, + amount=0.01 / open_rate, + exchange='bittrex', + ) + trade.recalc_open_trade_price() + if not is_open: + trade.close(open_rate * (1 - 0.9)) + trade.sell_reason = sell_reason + return trade + + +@pytest.mark.usefixtures("init_persistence") +def test_stoploss_guard(mocker, default_conf, fee, caplog): + default_conf['protections'] = [{ + "method": "StoplossGuard", + "lookback_period": 60, + "trade_limit": 2 + }] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + message = r"Trading stopped due to .*" + assert not freqtrade.protections.global_stop() + assert not log_has_re(message, caplog) + caplog.clear() + + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=200, min_ago_close=30, + )) + + assert not freqtrade.protections.global_stop() + assert not log_has_re(message, caplog) + caplog.clear() + # This trade does not count, as it's closed too long ago + Trade.session.add(generate_mock_trade( + 'BCH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=250, min_ago_close=100, + )) + + Trade.session.add(generate_mock_trade( + 'ETH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=240, min_ago_close=30, + )) + # 3 Trades closed - but the 2nd has been closed too long ago. + assert not freqtrade.protections.global_stop() + assert not log_has_re(message, caplog) + caplog.clear() + + Trade.session.add(generate_mock_trade( + 'LTC/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=180, min_ago_close=30, + )) + + assert freqtrade.protections.global_stop() + assert log_has_re(message, caplog) From 56975db2ed5387e8a14b5a17e290c0d45a0cdba6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Oct 2020 08:07:09 +0200 Subject: [PATCH 1034/1197] Add more tests --- config_full.json.example | 3 ++- freqtrade/plugins/protectionmanager.py | 13 ++++----- freqtrade/plugins/protections/__init__.py | 2 +- freqtrade/plugins/protections/iprotection.py | 6 +++-- .../plugins/protections/stoploss_guard.py | 27 ++++++++++++++----- tests/plugins/test_protections.py | 24 ++++++++++++++--- 6 files changed, 54 insertions(+), 21 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 96aa82d5f..eb20065ce 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -79,7 +79,8 @@ { "method": "StoplossGuard", "lookback_period": 60, - "trade_limit": 4 + "trade_limit": 4, + "stopduration": 60 } ], "exchange": { diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index dd6076ec1..c4822a323 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -2,10 +2,10 @@ Protection manager class """ import logging -from datetime import datetime +from datetime import datetime, timezone from typing import Dict, List -from freqtrade.exceptions import OperationalException +from freqtrade.persistence import PairLocks from freqtrade.plugins.protections import IProtection from freqtrade.resolvers import ProtectionResolver @@ -47,12 +47,13 @@ class ProtectionManager(): return [{p.name: p.short_desc()} for p in self._protection_handlers] def global_stop(self) -> bool: - now = datetime.utcnow() + now = datetime.now(timezone.utc) for protection_handler in self._protection_handlers: - result = protection_handler.global_stop(now) + result, until, reason = protection_handler.global_stop(now) - # Early stopping - first positive result stops the application - if result: + # Early stopping - first positive result blocks further trades + if result and until: + PairLocks.lock_pair('*', until, reason) return True return False diff --git a/freqtrade/plugins/protections/__init__.py b/freqtrade/plugins/protections/__init__.py index 5ecae7888..936355052 100644 --- a/freqtrade/plugins/protections/__init__.py +++ b/freqtrade/plugins/protections/__init__.py @@ -1,2 +1,2 @@ # flake8: noqa: F401 -from freqtrade.plugins.protections.iprotection import IProtection +from freqtrade.plugins.protections.iprotection import IProtection, ProtectionReturn diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index ecb4cad09..cadf01184 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -2,13 +2,15 @@ import logging from abc import ABC, abstractmethod from datetime import datetime -from typing import Any, Dict +from typing import Any, Dict, Optional, Tuple from freqtrade.mixins import LoggingMixin logger = logging.getLogger(__name__) +ProtectionReturn = Tuple[bool, Optional[datetime], Optional[str]] + class IProtection(LoggingMixin, ABC): @@ -29,7 +31,7 @@ class IProtection(LoggingMixin, ABC): """ @abstractmethod - def global_stop(self, date_now: datetime) -> bool: + def global_stop(self, date_now: datetime) -> ProtectionReturn: """ Stops trading (position entering) for all pairs This must evaluate to true for the whole period of the "cooldown period". diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 3b0b8c773..db3655a38 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -1,12 +1,12 @@ import logging from datetime import datetime, timedelta -from typing import Any, Dict +from typing import Any, Dict, Tuple from sqlalchemy import and_, or_ from freqtrade.persistence import Trade -from freqtrade.plugins.protections import IProtection +from freqtrade.plugins.protections import IProtection, ProtectionReturn from freqtrade.strategy.interface import SellType @@ -17,16 +17,26 @@ class StoplossGuard(IProtection): def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: super().__init__(config, protection_config) + self._lookback_period = protection_config.get('lookback_period', 60) self._trade_limit = protection_config.get('trade_limit', 10) + self._stopduration = protection_config.get('stopduration', 60) + + def _reason(self) -> str: + """ + LockReason to use + """ + return (f'{self._trade_limit} stoplosses in {self._lookback_period} min, ' + f'locking for {self._stopduration} min.') def short_desc(self) -> str: """ Short method description - used for startup-messages """ - return f"{self.name} - Frequent Stoploss Guard" + return (f"{self.name} - Frequent Stoploss Guard, {self._trade_limit} stoplosses " + f"within {self._lookback_period} minutes.") - def _stoploss_guard(self, date_now: datetime, pair: str = None) -> bool: + def _stoploss_guard(self, date_now: datetime, pair: str = None) -> ProtectionReturn: """ Evaluate recent trades """ @@ -45,13 +55,16 @@ class StoplossGuard(IProtection): if len(trades) > self._trade_limit: self.log_on_refresh(logger.info, f"Trading stopped due to {self._trade_limit} " f"stoplosses within {self._lookback_period} minutes.") - return True + until = date_now + timedelta(minutes=self._stopduration) + return True, until, self._reason() - return False + return False, None, None - def global_stop(self, date_now: datetime) -> bool: + def global_stop(self, date_now: datetime) -> ProtectionReturn: """ Stops trading (position entering) for all pairs This must evaluate to true for the whole period of the "cooldown period". + :return: Tuple of [bool, until, reason]. + If true, all pairs will be locked with until """ return self._stoploss_guard(date_now, pair=None) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 2bb0886ee..d2815338e 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -1,11 +1,10 @@ -from freqtrade.strategy.interface import SellType -from unittest.mock import MagicMock, PropertyMock import random -import pytest from datetime import datetime, timedelta -from freqtrade.constants import AVAILABLE_PROTECTIONS +import pytest + from freqtrade.persistence import Trade +from freqtrade.strategy.interface import SellType from tests.conftest import get_patched_freqtradebot, log_has_re @@ -77,3 +76,20 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): assert freqtrade.protections.global_stop() assert log_has_re(message, caplog) + + +@pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [ + ({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2}, + "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " + "2 stoplosses within 60 minutes.'}]", + None + ), +]) +def test_protection_manager_desc(mocker, default_conf, protectionconf, + desc_expected, exception_expected): + + default_conf['protections'] = [protectionconf] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + + short_desc = str(freqtrade.protections.short_desc()) + assert short_desc == desc_expected From 05be33ccd4240ccf39a92f122637003cea530854 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Oct 2020 13:55:54 +0200 Subject: [PATCH 1035/1197] Simplify is_pair_locked --- freqtrade/persistence/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 6027908da..04d5a7695 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -688,7 +688,7 @@ class PairLock(_DECL_BASE): @staticmethod def query_pair_locks(pair: Optional[str], now: datetime) -> Query: """ - Get all locks for this pair + Get all currently active locks for this pair :param pair: Pair to check for. Returns all current locks if pair is empty :param now: Datetime object (generated via datetime.now(timezone.utc)). """ From ff7ba23477d819ec3e3e1b91edc87149ee68efbc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Oct 2020 14:46:13 +0200 Subject: [PATCH 1036/1197] Simplify enter_positions and add global pairlock check --- freqtrade/freqtradebot.py | 10 +++++++--- tests/test_freqtradebot.py | 28 +++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2dbd7f099..75ff07b17 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -180,8 +180,10 @@ class FreqtradeBot: # First process current opened trades (positions) self.exit_positions(trades) + # Evaluate if protections should apply + self.protections.global_stop() # Then looking for buy opportunities - if self.get_free_open_trades() and not self.protections.global_stop(): + if self.get_free_open_trades(): self.enter_positions() Trade.session.flush() @@ -361,6 +363,9 @@ class FreqtradeBot: logger.info("No currency pair in active pair whitelist, " "but checking to sell open trades.") return trades_created + if PairLocks.is_global_lock(): + logger.info("Global pairlock active. Not creating new trades.") + return trades_created # Create entity and execute trade for each pair from whitelist for pair in whitelist: try: @@ -369,8 +374,7 @@ class FreqtradeBot: logger.warning('Unable to create trade for %s: %s', pair, exception) if not trades_created: - logger.debug("Found no buy signals for whitelisted currencies. " - "Trying again...") + logger.debug("Found no buy signals for whitelisted currencies. Trying again...") return trades_created diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 6adef510f..94ed06cd9 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -15,7 +15,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie InvalidOrderException, OperationalException, PricingError, TemporaryError) from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.persistence import Order, Trade +from freqtrade.persistence import Order, PairLocks, Trade from freqtrade.persistence.models import PairLock from freqtrade.rpc import RPCMessageType from freqtrade.state import RunMode, State @@ -678,6 +678,32 @@ def test_enter_positions_no_pairs_in_whitelist(default_conf, ticker, limit_buy_o assert log_has("Active pair whitelist is empty.", caplog) +@pytest.mark.usefixtures("init_persistence") +def test_enter_positions_global_pairlock(default_conf, ticker, limit_buy_order, fee, + mocker, caplog) -> None: + patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker, + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + get_fee=fee, + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + n = freqtrade.enter_positions() + message = "Global pairlock active. Not creating new trades." + n = freqtrade.enter_positions() + # 0 trades, but it's not because of pairlock. + assert n == 0 + assert not log_has(message, caplog) + + PairLocks.lock_pair('*', arrow.utcnow().shift(minutes=20).datetime, 'Just because') + n = freqtrade.enter_positions() + assert n == 0 + assert log_has(message, caplog) + + def test_create_trade_no_signal(default_conf, fee, mocker) -> None: default_conf['dry_run'] = True From 2a66c33a4e2c6ae4d116321a2fd8b46638f34354 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Oct 2020 16:52:26 +0200 Subject: [PATCH 1037/1197] Add locks per pair --- config_full.json.example | 4 ++ freqtrade/constants.py | 2 +- freqtrade/plugins/protectionmanager.py | 9 +++ .../plugins/protections/cooldown_period.py | 68 +++++++++++++++++++ freqtrade/plugins/protections/iprotection.py | 9 +++ .../plugins/protections/stoploss_guard.py | 11 ++- 6 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 freqtrade/plugins/protections/cooldown_period.py diff --git a/config_full.json.example b/config_full.json.example index eb20065ce..839f99dbd 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -81,6 +81,10 @@ "lookback_period": 60, "trade_limit": 4, "stopduration": 60 + }, + { + "method": "CooldownPeriod", + "stopduration": 20 } ], "exchange": { diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 9a93bfae3..d06047f4c 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -27,7 +27,7 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter'] -AVAILABLE_PROTECTIONS = ['StoplossGuard'] +AVAILABLE_PROTECTIONS = ['StoplossGuard', 'CooldownPeriod'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S' diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index c4822a323..b0929af88 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -57,3 +57,12 @@ class ProtectionManager(): PairLocks.lock_pair('*', until, reason) return True return False + + def stop_per_pair(self, pair) -> bool: + now = datetime.now(timezone.utc) + for protection_handler in self._protection_handlers: + result, until, reason = protection_handler.stop_per_pair(pair, now) + if result and until: + PairLocks.lock_pair(pair, until, reason) + return True + return False diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py new file mode 100644 index 000000000..c6b6685b2 --- /dev/null +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -0,0 +1,68 @@ + +import logging +from datetime import datetime, timedelta +from typing import Any, Dict + + +from freqtrade.persistence import Trade +from freqtrade.plugins.protections import IProtection, ProtectionReturn + + +logger = logging.getLogger(__name__) + + +class CooldownPeriod(IProtection): + + def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: + super().__init__(config, protection_config) + + self._stopduration = protection_config.get('stopduration', 60) + + def _reason(self) -> str: + """ + LockReason to use + """ + return (f'Cooldown period for {self._stopduration} min.') + + def short_desc(self) -> str: + """ + Short method description - used for startup-messages + """ + return (f"{self.name} - Cooldown period.") + + def _cooldown_period(self, pair: str, date_now: datetime, ) -> ProtectionReturn: + """ + Get last trade for this pair + """ + look_back_until = date_now - timedelta(minutes=self._stopduration) + filters = [ + Trade.is_open.is_(False), + Trade.close_date > look_back_until, + Trade.pair == pair, + ] + trade = Trade.get_trades(filters).first() + if trade: + self.log_on_refresh(logger.info, f"Cooldown for {pair} for {self._stopduration}.") + until = trade.close_date + timedelta(minutes=self._stopduration) + return True, until, self._reason() + + return False, None, None + + def global_stop(self, date_now: datetime) -> ProtectionReturn: + """ + Stops trading (position entering) for all pairs + This must evaluate to true for the whole period of the "cooldown period". + :return: Tuple of [bool, until, reason]. + If true, all pairs will be locked with until + """ + # Not implemented for cooldown period. + return False, None, None + + def stop_per_pair(self, pair: str, date_now: datetime) -> ProtectionReturn: + """ + Stops trading (position entering) for this pair + This must evaluate to true for the whole period of the "cooldown period". + :return: Tuple of [bool, until, reason]. + If true, this pair will be locked with until + """ + return self._cooldown_period(pair, date_now) diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index cadf01184..5dbcf72f6 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -36,3 +36,12 @@ class IProtection(LoggingMixin, ABC): Stops trading (position entering) for all pairs This must evaluate to true for the whole period of the "cooldown period". """ + + @abstractmethod + def stop_per_pair(self, pair: str, date_now: datetime) -> ProtectionReturn: + """ + Stops trading (position entering) for this pair + This must evaluate to true for the whole period of the "cooldown period". + :return: Tuple of [bool, until, reason]. + If true, this pair will be locked with until + """ diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index db3655a38..18888b854 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -1,7 +1,7 @@ import logging from datetime import datetime, timedelta -from typing import Any, Dict, Tuple +from typing import Any, Dict from sqlalchemy import and_, or_ @@ -68,3 +68,12 @@ class StoplossGuard(IProtection): If true, all pairs will be locked with until """ return self._stoploss_guard(date_now, pair=None) + + def stop_per_pair(self, pair: str, date_now: datetime) -> ProtectionReturn: + """ + Stops trading (position entering) for this pair + This must evaluate to true for the whole period of the "cooldown period". + :return: Tuple of [bool, until, reason]. + If true, this pair will be locked with until + """ + return False, None, None From fe0afb98832e662fbec06decc951143e8e5c113b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Oct 2020 17:09:30 +0200 Subject: [PATCH 1038/1197] Implement calling of per-pair protection --- freqtrade/freqtradebot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 75ff07b17..7bfd64c2d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1415,6 +1415,7 @@ class FreqtradeBot: # Updating wallets when order is closed if not trade.is_open: + self.protections.stop_per_pair(trade.pair) self.wallets.update() return False From 8dbef6bbeab0662a5014082f7ab65e2abb63d1ac Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 24 Oct 2020 20:25:40 +0200 Subject: [PATCH 1039/1197] Add test for cooldown period --- .../plugins/protections/cooldown_period.py | 8 ++-- tests/plugins/test_protections.py | 43 ++++++++++++++++++- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index c6b6685b2..24f55419b 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -1,9 +1,8 @@ import logging -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from typing import Any, Dict - from freqtrade.persistence import Trade from freqtrade.plugins.protections import IProtection, ProtectionReturn @@ -28,7 +27,7 @@ class CooldownPeriod(IProtection): """ Short method description - used for startup-messages """ - return (f"{self.name} - Cooldown period.") + return (f"{self.name} - Cooldown period of {self._stopduration} min.") def _cooldown_period(self, pair: str, date_now: datetime, ) -> ProtectionReturn: """ @@ -43,7 +42,8 @@ class CooldownPeriod(IProtection): trade = Trade.get_trades(filters).first() if trade: self.log_on_refresh(logger.info, f"Cooldown for {pair} for {self._stopduration}.") - until = trade.close_date + timedelta(minutes=self._stopduration) + until = trade.close_date.replace( + tzinfo=timezone.utc) + timedelta(minutes=self._stopduration) return True, until, self._reason() return False, None, None diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index d2815338e..59ada7c1e 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta import pytest -from freqtrade.persistence import Trade +from freqtrade.persistence import PairLocks, Trade from freqtrade.strategy.interface import SellType from tests.conftest import get_patched_freqtradebot, log_has_re @@ -76,6 +76,43 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): assert freqtrade.protections.global_stop() assert log_has_re(message, caplog) + assert PairLocks.is_global_lock() + + +@pytest.mark.usefixtures("init_persistence") +def test_CooldownPeriod(mocker, default_conf, fee, caplog): + default_conf['protections'] = [{ + "method": "CooldownPeriod", + "stopduration": 60, + }] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + message = r"Trading stopped due to .*" + assert not freqtrade.protections.global_stop() + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + + assert not log_has_re(message, caplog) + caplog.clear() + + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=200, min_ago_close=30, + )) + + assert not freqtrade.protections.global_stop() + assert freqtrade.protections.stop_per_pair('XRP/BTC') + assert PairLocks.is_pair_locked('XRP/BTC') + assert not PairLocks.is_global_lock() + + Trade.session.add(generate_mock_trade( + 'ETH/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, + min_ago_open=205, min_ago_close=35, + )) + + assert not freqtrade.protections.global_stop() + assert not PairLocks.is_pair_locked('ETH/BTC') + assert freqtrade.protections.stop_per_pair('ETH/BTC') + assert PairLocks.is_pair_locked('ETH/BTC') + assert not PairLocks.is_global_lock() @pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [ @@ -84,6 +121,10 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): "2 stoplosses within 60 minutes.'}]", None ), + ({"method": "CooldownPeriod", "stopduration": 60}, + "[{'CooldownPeriod': 'CooldownPeriod - Cooldown period of 60 min.'}]", + None + ), ]) def test_protection_manager_desc(mocker, default_conf, protectionconf, desc_expected, exception_expected): From 9f6c2a583fff165fd56d935cdd93f47aba13cbb2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 Nov 2020 07:48:27 +0100 Subject: [PATCH 1040/1197] Better wording for config options --- freqtrade/constants.py | 5 ++++- .../plugins/protections/cooldown_period.py | 12 ++++++------ .../plugins/protections/stoploss_guard.py | 18 +++++++++--------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index d06047f4c..6319d1f62 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -203,8 +203,11 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'method': {'type': 'string', 'enum': AVAILABLE_PROTECTIONS}, + 'stop_duration': {'type': 'number', 'minimum': 0.0}, + 'trade_limit': {'type': 'number', 'integer': 1}, + 'lookback_period': {'type': 'number', 'integer': 1}, }, - 'required': ['method'], + 'required': ['method', 'trade_limit'], } }, 'telegram': { diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index 24f55419b..ed618f6d4 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -15,25 +15,25 @@ class CooldownPeriod(IProtection): def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: super().__init__(config, protection_config) - self._stopduration = protection_config.get('stopduration', 60) + self._stop_duration = protection_config.get('stop_duration', 60) def _reason(self) -> str: """ LockReason to use """ - return (f'Cooldown period for {self._stopduration} min.') + return (f'Cooldown period for {self._stop_duration} min.') def short_desc(self) -> str: """ Short method description - used for startup-messages """ - return (f"{self.name} - Cooldown period of {self._stopduration} min.") + return (f"{self.name} - Cooldown period of {self._stop_duration} min.") def _cooldown_period(self, pair: str, date_now: datetime, ) -> ProtectionReturn: """ Get last trade for this pair """ - look_back_until = date_now - timedelta(minutes=self._stopduration) + look_back_until = date_now - timedelta(minutes=self._stop_duration) filters = [ Trade.is_open.is_(False), Trade.close_date > look_back_until, @@ -41,9 +41,9 @@ class CooldownPeriod(IProtection): ] trade = Trade.get_trades(filters).first() if trade: - self.log_on_refresh(logger.info, f"Cooldown for {pair} for {self._stopduration}.") + self.log_on_refresh(logger.info, f"Cooldown for {pair} for {self._stop_duration}.") until = trade.close_date.replace( - tzinfo=timezone.utc) + timedelta(minutes=self._stopduration) + tzinfo=timezone.utc) + timedelta(minutes=self._stop_duration) return True, until, self._reason() return False, None, None diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 18888b854..408492063 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -20,14 +20,7 @@ class StoplossGuard(IProtection): self._lookback_period = protection_config.get('lookback_period', 60) self._trade_limit = protection_config.get('trade_limit', 10) - self._stopduration = protection_config.get('stopduration', 60) - - def _reason(self) -> str: - """ - LockReason to use - """ - return (f'{self._trade_limit} stoplosses in {self._lookback_period} min, ' - f'locking for {self._stopduration} min.') + self._stop_duration = protection_config.get('stop_duration', 60) def short_desc(self) -> str: """ @@ -36,6 +29,13 @@ class StoplossGuard(IProtection): return (f"{self.name} - Frequent Stoploss Guard, {self._trade_limit} stoplosses " f"within {self._lookback_period} minutes.") + def _reason(self) -> str: + """ + LockReason to use + """ + return (f'{self._trade_limit} stoplosses in {self._lookback_period} min, ' + f'locking for {self._stop_duration} min.') + def _stoploss_guard(self, date_now: datetime, pair: str = None) -> ProtectionReturn: """ Evaluate recent trades @@ -55,7 +55,7 @@ class StoplossGuard(IProtection): if len(trades) > self._trade_limit: self.log_on_refresh(logger.info, f"Trading stopped due to {self._trade_limit} " f"stoplosses within {self._lookback_period} minutes.") - until = date_now + timedelta(minutes=self._stopduration) + until = date_now + timedelta(minutes=self._stop_duration) return True, until, self._reason() return False, None, None From 00d4820bc108976b759170efa0127e1e7960b5fc Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 Nov 2020 07:49:30 +0100 Subject: [PATCH 1041/1197] Add low_profit_pairs --- docs/includes/protections.md | 15 ++++ freqtrade/constants.py | 2 +- .../plugins/protections/low_profit_pairs.py | 81 +++++++++++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 freqtrade/plugins/protections/low_profit_pairs.py diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 078ba0c2b..aa0ca0f97 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -21,6 +21,21 @@ Protections will protect your strategy from unexpected events and market conditi !!! Note `StoplossGuard` considers all trades with the results `"stop_loss"` and `"trailing_stop_loss"` if the result was negative. +#### Low Profit Pairs + +`LowProfitpairs` uses all trades for a pair within a `lookback_period` (in minutes) to determine the overall profit ratio. +If that ratio is below `required_profit`, that pair will be locked for `stop_duration` (in minutes). + +```json +"protections": [{ + "method": "LowProfitpairs", + "lookback_period": 60, + "trade_limit": 4, + "stop_duration": 60, + "required_profit": 0.02 +}], +``` + ### Full example of Protections The below example stops trading if more than 4 stoploss occur within a 1 hour (60 minute) limit. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 6319d1f62..812883da0 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -27,7 +27,7 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter'] -AVAILABLE_PROTECTIONS = ['StoplossGuard', 'CooldownPeriod'] +AVAILABLE_PROTECTIONS = ['StoplossGuard', 'CooldownPeriod', 'LowProfitpairs'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S' diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py new file mode 100644 index 000000000..739642de7 --- /dev/null +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -0,0 +1,81 @@ + +import logging +from datetime import datetime, timedelta +from typing import Any, Dict + + +from freqtrade.persistence import Trade +from freqtrade.plugins.protections import IProtection, ProtectionReturn + + +logger = logging.getLogger(__name__) + + +class LowProfitpairs(IProtection): + + def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: + super().__init__(config, protection_config) + + self._lookback_period = protection_config.get('lookback_period', 60) + self._trade_limit = protection_config.get('trade_limit', 1) + self._stop_duration = protection_config.get('stop_duration', 60) + self._required_profit = protection_config.get('required_profit', 0.0) + + def short_desc(self) -> str: + """ + Short method description - used for startup-messages + """ + return (f"{self.name} - Low Profit Protection, locks pairs with " + f"profit < {self._required_profit} within {self._lookback_period} minutes.") + + def _reason(self, profit: float) -> str: + """ + LockReason to use + """ + return (f'{profit} < {self._required_profit} in {self._lookback_period} min, ' + f'locking for {self._stop_duration} min.') + + def _low_profit(self, date_now: datetime, pair: str) -> ProtectionReturn: + """ + Evaluate recent trades for pair + """ + look_back_until = date_now - timedelta(minutes=self._lookback_period) + filters = [ + Trade.is_open.is_(False), + Trade.close_date > look_back_until, + ] + if pair: + filters.append(Trade.pair == pair) + trades = Trade.get_trades(filters).all() + if len(trades) < self._trade_limit: + # Not enough trades in the relevant period + return False, None, None + + profit = sum(trade.close_profit for trade in trades) + if profit < self._required_profit: + self.log_on_refresh( + logger.info, + f"Trading for {pair} stopped due to {profit} < {self._required_profit} " + f"within {self._lookback_period} minutes.") + until = date_now + timedelta(minutes=self._stop_duration) + return True, until, self._reason(profit) + + return False, None, None + + def global_stop(self, date_now: datetime) -> ProtectionReturn: + """ + Stops trading (position entering) for all pairs + This must evaluate to true for the whole period of the "cooldown period". + :return: Tuple of [bool, until, reason]. + If true, all pairs will be locked with until + """ + return False, None, None + + def stop_per_pair(self, pair: str, date_now: datetime) -> ProtectionReturn: + """ + Stops trading (position entering) for this pair + This must evaluate to true for the whole period of the "cooldown period". + :return: Tuple of [bool, until, reason]. + If true, this pair will be locked with until + """ + return self._low_profit(date_now, pair=None) From 1f703dc3419e8a6179363248f6443177b3f86942 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 Nov 2020 08:00:10 +0100 Subject: [PATCH 1042/1197] Improve protection documentation --- docs/includes/protections.md | 62 ++++++++++++++++++++++++++++++++---- freqtrade/constants.py | 2 +- 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index aa0ca0f97..8efb02b95 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -8,13 +8,14 @@ Protections will protect your strategy from unexpected events and market conditi #### Stoploss Guard -`StoplossGuard` selects all trades within a `lookback_period` (in minutes), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case it will stop trading until this condition is no longer true. +`StoplossGuard` selects all trades within a `lookback_period` (in minutes), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration`. ```json "protections": [{ "method": "StoplossGuard", "lookback_period": 60, - "trade_limit": 4 + "trade_limit": 4, + "stop_duration": 60 }], ``` @@ -27,25 +28,72 @@ Protections will protect your strategy from unexpected events and market conditi If that ratio is below `required_profit`, that pair will be locked for `stop_duration` (in minutes). ```json -"protections": [{ +"protections": [ + { "method": "LowProfitpairs", "lookback_period": 60, "trade_limit": 4, "stop_duration": 60, "required_profit": 0.02 -}], + } +], ``` -### Full example of Protections +#### Cooldown Period + +`CooldownPeriod` locks a pair for `stop_duration` (in minutes) after selling, avoiding a re-entry for this pair for `stop_duration` minutes. + -The below example stops trading if more than 4 stoploss occur within a 1 hour (60 minute) limit. ```json "protections": [ + { + "method": "CooldownPeriod", + "stop_duration": 60 + } +], +``` + +!!! Note: + This Protection applies only at pair-level, and will never lock all pairs globally. + +### Full example of Protections + +All protections can be combined at will, also with different parameters, creating a increasing wall for under-performing pairs. +All protections are evaluated in the sequence they are defined. + +The below example: + +* stops trading if more than 4 stoploss occur for all pairs within a 1 hour (60 minute) limit (`StoplossGuard`). +* Locks each pair after selling for an additional 10 minutes (`CooldownPeriod`), giving other pairs a chance to get filled. +* Locks all pairs that had 4 Trades within the last 6 hours with a combined profit ratio of below 0.02 (<2%). (`LowProfitpairs`) +* Locks all pairs for 120 minutes that had a profit of below 0.01 (<1%) within the last 24h (`60 * 24 = 1440`), a minimum of 7 trades + +```json +"protections": [ + { + "method": "CooldownPeriod", + "stop_duration": 10 + }, { "method": "StoplossGuard", "lookback_period": 60, - "trade_limit": 4 + "trade_limit": 4, + "stop_duration": 60 + }, + { + "method": "LowProfitpairs", + "lookback_period": 360, + "trade_limit": 4, + "stop_duration": 60, + "required_profit": 0.02 + }, + { + "method": "LowProfitpairs", + "lookback_period": 1440, + "trade_limit": 7, + "stop_duration": 120, + "required_profit": 0.01 } ], ``` diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 812883da0..3f6b6f440 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -207,7 +207,7 @@ CONF_SCHEMA = { 'trade_limit': {'type': 'number', 'integer': 1}, 'lookback_period': {'type': 'number', 'integer': 1}, }, - 'required': ['method', 'trade_limit'], + 'required': ['method'], } }, 'telegram': { From bb06365c503c6c9a9cf5c90f994b588d4568431f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 Nov 2020 20:54:24 +0100 Subject: [PATCH 1043/1197] Improve protection documentation --- docs/includes/protections.md | 29 ++++++++++++------- freqtrade/constants.py | 2 +- .../plugins/protections/low_profit_pairs.py | 2 +- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 8efb02b95..91b10cf65 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -2,35 +2,46 @@ Protections will protect your strategy from unexpected events and market conditions. +!!! Note + Not all Protections will work for all strategies, and parameters will need to be tuned for your strategy. + +!!! Tip + Each Protection can be configured multiple times with different parameters, to allow different levels of protection (short-term / long-term). + ### Available Protection Handlers -* [`StoplossGuard`](#stoploss-guard) (default, if not configured differently) +* [`StoplossGuard`](#stoploss-guard) Stop trading if a certain amount of stoploss occurred within a certain time window. +* [`LowProfitPairs`](#low-profit-pairs) Lock pairs with low profits +* [`CooldownPeriod`](#cooldown-period) Don't enter a trade right after selling a trade. #### Stoploss Guard `StoplossGuard` selects all trades within a `lookback_period` (in minutes), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration`. ```json -"protections": [{ +"protections": [ + { "method": "StoplossGuard", "lookback_period": 60, "trade_limit": 4, "stop_duration": 60 -}], + } +], ``` !!! Note `StoplossGuard` considers all trades with the results `"stop_loss"` and `"trailing_stop_loss"` if the result was negative. + `trade_limit` and `lookback_period` will need to be tuned for your strategy. #### Low Profit Pairs -`LowProfitpairs` uses all trades for a pair within a `lookback_period` (in minutes) to determine the overall profit ratio. +`LowProfitPairs` uses all trades for a pair within a `lookback_period` (in minutes) to determine the overall profit ratio. If that ratio is below `required_profit`, that pair will be locked for `stop_duration` (in minutes). ```json "protections": [ { - "method": "LowProfitpairs", + "method": "LowProfitPairs", "lookback_period": 60, "trade_limit": 4, "stop_duration": 60, @@ -43,8 +54,6 @@ If that ratio is below `required_profit`, that pair will be locked for `stop_dur `CooldownPeriod` locks a pair for `stop_duration` (in minutes) after selling, avoiding a re-entry for this pair for `stop_duration` minutes. - - ```json "protections": [ { @@ -66,7 +75,7 @@ The below example: * stops trading if more than 4 stoploss occur for all pairs within a 1 hour (60 minute) limit (`StoplossGuard`). * Locks each pair after selling for an additional 10 minutes (`CooldownPeriod`), giving other pairs a chance to get filled. -* Locks all pairs that had 4 Trades within the last 6 hours with a combined profit ratio of below 0.02 (<2%). (`LowProfitpairs`) +* Locks all pairs that had 4 Trades within the last 6 hours with a combined profit ratio of below 0.02 (<2%). (`LowProfitPairs`) * Locks all pairs for 120 minutes that had a profit of below 0.01 (<1%) within the last 24h (`60 * 24 = 1440`), a minimum of 7 trades ```json @@ -82,14 +91,14 @@ The below example: "stop_duration": 60 }, { - "method": "LowProfitpairs", + "method": "LowProfitPairs", "lookback_period": 360, "trade_limit": 4, "stop_duration": 60, "required_profit": 0.02 }, { - "method": "LowProfitpairs", + "method": "LowProfitPairs", "lookback_period": 1440, "trade_limit": 7, "stop_duration": 120, diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 3f6b6f440..bc8acc8b3 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -27,7 +27,7 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter'] -AVAILABLE_PROTECTIONS = ['StoplossGuard', 'CooldownPeriod', 'LowProfitpairs'] +AVAILABLE_PROTECTIONS = ['StoplossGuard', 'CooldownPeriod', 'LowProfitPairs'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S' diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index 739642de7..cbc0052ef 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -11,7 +11,7 @@ from freqtrade.plugins.protections import IProtection, ProtectionReturn logger = logging.getLogger(__name__) -class LowProfitpairs(IProtection): +class LowProfitPairs(IProtection): def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: super().__init__(config, protection_config) From 9484ee6690ae15bd8a7e769042db1337d8e2d710 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 Nov 2020 21:11:22 +0100 Subject: [PATCH 1044/1197] Test for low_profit_pairs --- .../plugins/protections/low_profit_pairs.py | 2 +- tests/plugins/test_protections.py | 55 +++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index cbc0052ef..dc5e1ba24 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -78,4 +78,4 @@ class LowProfitPairs(IProtection): :return: Tuple of [bool, until, reason]. If true, this pair will be locked with until """ - return self._low_profit(date_now, pair=None) + return self._low_profit(date_now, pair=pair) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 59ada7c1e..3417b1a56 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -115,6 +115,56 @@ def test_CooldownPeriod(mocker, default_conf, fee, caplog): assert not PairLocks.is_global_lock() +@pytest.mark.usefixtures("init_persistence") +def test_LowProfitPairs(mocker, default_conf, fee, caplog): + default_conf['protections'] = [{ + "method": "LowProfitPairs", + "lookback_period": 400, + "stopduration": 60, + "trade_limit": 2, + "required_profit": 0.0, + }] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + message = r"Trading stopped due to .*" + assert not freqtrade.protections.global_stop() + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + + assert not log_has_re(message, caplog) + caplog.clear() + + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=800, min_ago_close=450, + )) + + # Not locked with 1 trade + assert not freqtrade.protections.global_stop() + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + assert not PairLocks.is_pair_locked('XRP/BTC') + assert not PairLocks.is_global_lock() + + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=200, min_ago_close=120, + )) + + # Not locked with 1 trade (first trade is outside of lookback_period) + assert not freqtrade.protections.global_stop() + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + assert not PairLocks.is_pair_locked('XRP/BTC') + assert not PairLocks.is_global_lock() + + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=110, min_ago_close=20, + )) + + # Locks due to 2nd trade + assert not freqtrade.protections.global_stop() + assert freqtrade.protections.stop_per_pair('XRP/BTC') + assert PairLocks.is_pair_locked('XRP/BTC') + assert not PairLocks.is_global_lock() + @pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [ ({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2}, "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " @@ -125,6 +175,11 @@ def test_CooldownPeriod(mocker, default_conf, fee, caplog): "[{'CooldownPeriod': 'CooldownPeriod - Cooldown period of 60 min.'}]", None ), + ({"method": "LowProfitPairs", "stopduration": 60}, + "[{'LowProfitPairs': 'LowProfitPairs - Low Profit Protection, locks pairs with " + "profit < 0.0 within 60 minutes.'}]", + None + ), ]) def test_protection_manager_desc(mocker, default_conf, protectionconf, desc_expected, exception_expected): From 5133675988e3f8e609d0828606af1910f7e264f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Nov 2020 11:41:48 +0100 Subject: [PATCH 1045/1197] Apply all stops in the list, even if the first would apply already --- freqtrade/plugins/protectionmanager.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index b0929af88..64c7208ce 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -48,21 +48,22 @@ class ProtectionManager(): def global_stop(self) -> bool: now = datetime.now(timezone.utc) - + result = False for protection_handler in self._protection_handlers: result, until, reason = protection_handler.global_stop(now) # Early stopping - first positive result blocks further trades if result and until: PairLocks.lock_pair('*', until, reason) - return True - return False + result = True + return result def stop_per_pair(self, pair) -> bool: now = datetime.now(timezone.utc) + result = False for protection_handler in self._protection_handlers: result, until, reason = protection_handler.stop_per_pair(pair, now) if result and until: PairLocks.lock_pair(pair, until, reason) - return True - return False + result = True + return result From 47cd856fea54fbe12f46d25494430d7cf432b2f2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Nov 2020 16:18:45 +0100 Subject: [PATCH 1046/1197] Include protection documentation --- docs/configuration.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index 2e8f6555f..b70a85c04 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -91,6 +91,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `edge.*` | Please refer to [edge configuration document](edge.md) for detailed explanation. | `experimental.block_bad_exchanges` | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
    *Defaults to `true`.*
    **Datatype:** Boolean | `pairlists` | Define one or more pairlists to be used. [More information below](#pairlists-and-pairlist-handlers).
    *Defaults to `StaticPairList`.*
    **Datatype:** List of Dicts +| `protections` | Define one or more protections to be used. [More information below](#protections).
    **Datatype:** List of Dicts | `telegram.enabled` | Enable the usage of Telegram.
    **Datatype:** Boolean | `telegram.token` | Your Telegram bot token. Only required if `telegram.enabled` is `true`.
    **Keep it in secret, do not disclose publicly.**
    **Datatype:** String | `telegram.chat_id` | Your personal Telegram account id. Only required if `telegram.enabled` is `true`.
    **Keep it in secret, do not disclose publicly.**
    **Datatype:** String @@ -575,6 +576,7 @@ Assuming both buy and sell are using market orders, a configuration similar to t Obviously, if only one side is using limit orders, different pricing combinations can be used. --8<-- "includes/pairlists.md" +--8<-- "includes/protections.md" ## Switch to Dry-run mode From 59091ef2b774d5e9cc481e0047dbd8db34967156 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Nov 2020 19:43:12 +0100 Subject: [PATCH 1047/1197] Add helper method to calculate protection until --- freqtrade/freqtradebot.py | 3 +++ freqtrade/plugins/protectionmanager.py | 3 ++- .../plugins/protections/cooldown_period.py | 4 ++-- freqtrade/plugins/protections/iprotection.py | 19 +++++++++++++++++-- .../plugins/protections/low_profit_pairs.py | 4 ++-- .../plugins/protections/stoploss_guard.py | 2 +- 6 files changed, 27 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7bfd64c2d..f2ee4d7f0 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -182,6 +182,7 @@ class FreqtradeBot: # Evaluate if protections should apply self.protections.global_stop() + # Then looking for buy opportunities if self.get_free_open_trades(): self.enter_positions() @@ -1416,6 +1417,8 @@ class FreqtradeBot: # Updating wallets when order is closed if not trade.is_open: self.protections.stop_per_pair(trade.pair) + # Evaluate if protections should apply + # self.protections.global_stop() self.wallets.update() return False diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index 64c7208ce..a79447f02 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -54,7 +54,8 @@ class ProtectionManager(): # Early stopping - first positive result blocks further trades if result and until: - PairLocks.lock_pair('*', until, reason) + if not PairLocks.is_global_lock(until): + PairLocks.lock_pair('*', until, reason) result = True return result diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index ed618f6d4..56635984b 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -42,8 +42,8 @@ class CooldownPeriod(IProtection): trade = Trade.get_trades(filters).first() if trade: self.log_on_refresh(logger.info, f"Cooldown for {pair} for {self._stop_duration}.") - until = trade.close_date.replace( - tzinfo=timezone.utc) + timedelta(minutes=self._stop_duration) + until = self.calculate_lock_end([trade], self._stop_duration) + return True, until, self._reason() return False, None, None diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 5dbcf72f6..8048fccf0 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -1,10 +1,11 @@ import logging from abc import ABC, abstractmethod -from datetime import datetime -from typing import Any, Dict, Optional, Tuple +from datetime import datetime, timedelta, timezone +from typing import Any, Dict, List, Optional, Tuple from freqtrade.mixins import LoggingMixin +from freqtrade.persistence import Trade logger = logging.getLogger(__name__) @@ -45,3 +46,17 @@ class IProtection(LoggingMixin, ABC): :return: Tuple of [bool, until, reason]. If true, this pair will be locked with until """ + + @staticmethod + def calculate_lock_end(trades: List[Trade], stop_minutes: int) -> datetime: + """ + Get lock end time + """ + max_date: datetime = max([trade.close_date for trade in trades]) + # comming from Database, tzinfo is not set. + if max_date.tzinfo is None: + max_date = max_date.replace(tzinfo=timezone.utc) + + until = max_date + timedelta(minutes=stop_minutes) + + return until diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index dc5e1ba24..38d0886bb 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -3,7 +3,6 @@ import logging from datetime import datetime, timedelta from typing import Any, Dict - from freqtrade.persistence import Trade from freqtrade.plugins.protections import IProtection, ProtectionReturn @@ -57,7 +56,8 @@ class LowProfitPairs(IProtection): logger.info, f"Trading for {pair} stopped due to {profit} < {self._required_profit} " f"within {self._lookback_period} minutes.") - until = date_now + timedelta(minutes=self._stop_duration) + until = self.calculate_lock_end(trades, self._stop_duration) + return True, until, self._reason(profit) return False, None, None diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 408492063..6335172f8 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -55,7 +55,7 @@ class StoplossGuard(IProtection): if len(trades) > self._trade_limit: self.log_on_refresh(logger.info, f"Trading stopped due to {self._trade_limit} " f"stoplosses within {self._lookback_period} minutes.") - until = date_now + timedelta(minutes=self._stop_duration) + until = self.calculate_lock_end(trades, self._stop_duration) return True, until, self._reason() return False, None, None From fc97266dd47011aa49c85d35bb7f194711fd57d0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Nov 2020 07:21:59 +0100 Subject: [PATCH 1048/1197] Add "now" to lock_pair method --- freqtrade/persistence/pairlock_middleware.py | 13 +++++++++++-- freqtrade/plugins/protectionmanager.py | 2 +- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index 44fc228f6..38b5a5d63 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -22,10 +22,19 @@ class PairLocks(): timeframe: str = '' @staticmethod - def lock_pair(pair: str, until: datetime, reason: str = None) -> None: + def lock_pair(pair: str, until: datetime, reason: str = None, *, now: datetime = None) -> None: + """ + Create PairLock from now to "until". + Uses database by default, unless PairLocks.use_db is set to False, + in which case a list is maintained. + :param pair: pair to lock. use '*' to lock all pairs + :param until: End time of the lock. Will be rounded up to the next candle. + :param reason: Reason string that will be shown as reason for the lock + :param now: Current timestamp. Used to determine lock start time. + """ lock = PairLock( pair=pair, - lock_time=datetime.now(timezone.utc), + lock_time=now or datetime.now(timezone.utc), lock_end_time=timeframe_to_next_date(PairLocks.timeframe, until), reason=reason, active=True diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index a79447f02..33a51970c 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -55,7 +55,7 @@ class ProtectionManager(): # Early stopping - first positive result blocks further trades if result and until: if not PairLocks.is_global_lock(until): - PairLocks.lock_pair('*', until, reason) + PairLocks.lock_pair('*', until, reason, now=now) result = True return result From e29d918ea54af338131b518cbe8ffad012c506a6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Nov 2020 08:01:12 +0100 Subject: [PATCH 1049/1197] Avoid double-locks also in per pair locks --- freqtrade/plugins/protectionmanager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index 33a51970c..e58a50c80 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -65,6 +65,7 @@ class ProtectionManager(): for protection_handler in self._protection_handlers: result, until, reason = protection_handler.stop_per_pair(pair, now) if result and until: - PairLocks.lock_pair(pair, until, reason) + if not PairLocks.is_pair_locked(pair, until): + PairLocks.lock_pair(pair, until, reason, now=now) result = True return result From 2e5b9fd4b27bfbbb4c027f8e2e28d88e5677a9b5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Nov 2020 08:04:19 +0100 Subject: [PATCH 1050/1197] format profit in low_profit_pairs --- freqtrade/plugins/protections/low_profit_pairs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index 38d0886bb..cc827529f 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -54,7 +54,7 @@ class LowProfitPairs(IProtection): if profit < self._required_profit: self.log_on_refresh( logger.info, - f"Trading for {pair} stopped due to {profit} < {self._required_profit} " + f"Trading for {pair} stopped due to {profit:.2f} < {self._required_profit} " f"within {self._lookback_period} minutes.") until = self.calculate_lock_end(trades, self._stop_duration) From 8ebd6ad2003ca29ba5dac192ce0258cef9e6894d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 19 Nov 2020 19:45:22 +0100 Subject: [PATCH 1051/1197] Rename login-mixin log method --- freqtrade/mixins/logging_mixin.py | 6 +++--- freqtrade/pairlist/AgeFilter.py | 8 ++++---- freqtrade/pairlist/PrecisionFilter.py | 5 ++--- freqtrade/pairlist/PriceFilter.py | 18 +++++++++--------- freqtrade/pairlist/SpreadFilter.py | 6 +++--- freqtrade/pairlist/VolumePairList.py | 2 +- freqtrade/pairlist/rangestabilityfilter.py | 9 ++++----- .../plugins/protections/cooldown_period.py | 2 +- .../plugins/protections/low_profit_pairs.py | 2 +- .../plugins/protections/stoploss_guard.py | 4 ++-- tests/plugins/test_pairlist.py | 8 ++++---- 11 files changed, 34 insertions(+), 36 deletions(-) diff --git a/freqtrade/mixins/logging_mixin.py b/freqtrade/mixins/logging_mixin.py index 4e19e45a4..db2307ad3 100644 --- a/freqtrade/mixins/logging_mixin.py +++ b/freqtrade/mixins/logging_mixin.py @@ -16,7 +16,7 @@ class LoggingMixin(): self.refresh_period = refresh_period self._log_cache: TTLCache = TTLCache(maxsize=1024, ttl=self.refresh_period) - def log_on_refresh(self, logmethod, message: str) -> None: + def log_once(self, logmethod, message: str) -> None: """ Logs message - not more often than "refresh_period" to avoid log spamming Logs the log-message as debug as well to simplify debugging. @@ -25,10 +25,10 @@ class LoggingMixin(): :return: None. """ @cached(cache=self._log_cache) - def _log_on_refresh(message: str): + def _log_once(message: str): logmethod(message) # Log as debug first self.logger.debug(message) # Call hidden function. - _log_on_refresh(message) + _log_once(message) diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index e2a13c20a..dd63c1147 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -76,9 +76,9 @@ class AgeFilter(IPairList): self._symbolsChecked[ticker['symbol']] = int(arrow.utcnow().float_timestamp) * 1000 return True else: - self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " - f"because age {len(daily_candles)} is less than " - f"{self._min_days_listed} " - f"{plural(self._min_days_listed, 'day')}") + self.log_once(logger.info, + f"Removed {ticker['symbol']} from whitelist, because age " + f"{len(daily_candles)} is less than {self._min_days_listed} " + f"{plural(self._min_days_listed, 'day')}") return False return False diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py index 29e32fd44..a28d54205 100644 --- a/freqtrade/pairlist/PrecisionFilter.py +++ b/freqtrade/pairlist/PrecisionFilter.py @@ -59,9 +59,8 @@ class PrecisionFilter(IPairList): logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}") if sp <= stop_gap_price: - self.log_on_refresh(logger.info, - f"Removed {ticker['symbol']} from whitelist, " - f"because stop price {sp} would be <= stop limit {stop_gap_price}") + self.log_once(logger.info, f"Removed {ticker['symbol']} from whitelist, because " + f"stop price {sp} would be <= stop limit {stop_gap_price}") return False return True diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/pairlist/PriceFilter.py index bef1c0a15..a5d73b728 100644 --- a/freqtrade/pairlist/PriceFilter.py +++ b/freqtrade/pairlist/PriceFilter.py @@ -64,9 +64,9 @@ class PriceFilter(IPairList): :return: True if the pair can stay, false if it should be removed """ if ticker['last'] is None or ticker['last'] == 0: - self.log_on_refresh(logger.info, - f"Removed {ticker['symbol']} from whitelist, because " - "ticker['last'] is empty (Usually no trade in the last 24h).") + self.log_once(logger.info, + f"Removed {ticker['symbol']} from whitelist, because " + "ticker['last'] is empty (Usually no trade in the last 24h).") return False # Perform low_price_ratio check. @@ -74,22 +74,22 @@ class PriceFilter(IPairList): compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last']) changeperc = compare / ticker['last'] if changeperc > self._low_price_ratio: - self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " - f"because 1 unit is {changeperc * 100:.3f}%") + self.log_once(logger.info, f"Removed {ticker['symbol']} from whitelist, " + f"because 1 unit is {changeperc * 100:.3f}%") return False # Perform min_price check. if self._min_price != 0: if ticker['last'] < self._min_price: - self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " - f"because last price < {self._min_price:.8f}") + self.log_once(logger.info, f"Removed {ticker['symbol']} from whitelist, " + f"because last price < {self._min_price:.8f}") return False # Perform max_price check. if self._max_price != 0: if ticker['last'] > self._max_price: - self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " - f"because last price > {self._max_price:.8f}") + self.log_once(logger.info, f"Removed {ticker['symbol']} from whitelist, " + f"because last price > {self._max_price:.8f}") return False return True diff --git a/freqtrade/pairlist/SpreadFilter.py b/freqtrade/pairlist/SpreadFilter.py index a636b90bd..963ecb82a 100644 --- a/freqtrade/pairlist/SpreadFilter.py +++ b/freqtrade/pairlist/SpreadFilter.py @@ -45,9 +45,9 @@ class SpreadFilter(IPairList): if 'bid' in ticker and 'ask' in ticker: spread = 1 - ticker['bid'] / ticker['ask'] if spread > self._max_spread_ratio: - self.log_on_refresh(logger.info, f"Removed {ticker['symbol']} from whitelist, " - f"because spread {spread * 100:.3f}% >" - f"{self._max_spread_ratio * 100}%") + self.log_once(logger.info, + f"Removed {ticker['symbol']} from whitelist, because spread " + f"{spread * 100:.3f}% > {self._max_spread_ratio * 100}%") return False else: return True diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 7d3c2c653..24e1674fd 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -111,6 +111,6 @@ class VolumePairList(IPairList): # Limit pairlist to the requested number of pairs pairs = pairs[:self._number_pairs] - self.log_on_refresh(logger.info, f"Searching {self._number_pairs} pairs: {pairs}") + self.log_once(logger.info, f"Searching {self._number_pairs} pairs: {pairs}") return pairs diff --git a/freqtrade/pairlist/rangestabilityfilter.py b/freqtrade/pairlist/rangestabilityfilter.py index b460ff477..7a1b69a1a 100644 --- a/freqtrade/pairlist/rangestabilityfilter.py +++ b/freqtrade/pairlist/rangestabilityfilter.py @@ -78,11 +78,10 @@ class RangeStabilityFilter(IPairList): if pct_change >= self._min_rate_of_change: result = True else: - self.log_on_refresh(logger.info, - f"Removed {pair} from whitelist, " - f"because rate of change over {plural(self._days, 'day')} is " - f"{pct_change:.3f}, which is below the " - f"threshold of {self._min_rate_of_change}.") + self.log_once(logger.info, + f"Removed {pair} from whitelist, because rate of change " + f"over {plural(self._days, 'day')} is {pct_change:.3f}, " + f"which is below the threshold of {self._min_rate_of_change}.") result = False self._pair_cache[pair] = result diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index 56635984b..447ca4363 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -41,7 +41,7 @@ class CooldownPeriod(IProtection): ] trade = Trade.get_trades(filters).first() if trade: - self.log_on_refresh(logger.info, f"Cooldown for {pair} for {self._stop_duration}.") + self.log_once(logger.info, f"Cooldown for {pair} for {self._stop_duration}.") until = self.calculate_lock_end([trade], self._stop_duration) return True, until, self._reason() diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index cc827529f..96fb2b08e 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -52,7 +52,7 @@ class LowProfitPairs(IProtection): profit = sum(trade.close_profit for trade in trades) if profit < self._required_profit: - self.log_on_refresh( + self.log_once( logger.info, f"Trading for {pair} stopped due to {profit:.2f} < {self._required_profit} " f"within {self._lookback_period} minutes.") diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 6335172f8..8b6871915 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -53,8 +53,8 @@ class StoplossGuard(IProtection): trades = Trade.get_trades(filters).all() if len(trades) > self._trade_limit: - self.log_on_refresh(logger.info, f"Trading stopped due to {self._trade_limit} " - f"stoplosses within {self._lookback_period} minutes.") + self.log_once(logger.info, f"Trading stopped due to {self._trade_limit} " + f"stoplosses within {self._lookback_period} minutes.") until = self.calculate_lock_end(trades, self._stop_duration) return True, until, self._reason() diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 1d2f16b45..2f1617f6c 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -92,7 +92,7 @@ def static_pl_conf(whitelist_conf): return whitelist_conf -def test_log_on_refresh(mocker, static_pl_conf, markets, tickers): +def test_log_cached(mocker, static_pl_conf, markets, tickers): mocker.patch.multiple('freqtrade.exchange.Exchange', markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), @@ -102,14 +102,14 @@ def test_log_on_refresh(mocker, static_pl_conf, markets, tickers): logmock = MagicMock() # Assign starting whitelist pl = freqtrade.pairlists._pairlist_handlers[0] - pl.log_on_refresh(logmock, 'Hello world') + pl.log_once(logmock, 'Hello world') assert logmock.call_count == 1 - pl.log_on_refresh(logmock, 'Hello world') + pl.log_once(logmock, 'Hello world') assert logmock.call_count == 1 assert pl._log_cache.currsize == 1 assert ('Hello world',) in pl._log_cache._Cache__data - pl.log_on_refresh(logmock, 'Hello world2') + pl.log_once(logmock, 'Hello world2') assert logmock.call_count == 2 assert pl._log_cache.currsize == 2 From 2cd54a59333feafbf64a664fe151a199e91a8ce4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 19 Nov 2020 20:06:13 +0100 Subject: [PATCH 1052/1197] Allow disabling output from plugins --- freqtrade/mixins/logging_mixin.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/freqtrade/mixins/logging_mixin.py b/freqtrade/mixins/logging_mixin.py index db2307ad3..a8dec2da7 100644 --- a/freqtrade/mixins/logging_mixin.py +++ b/freqtrade/mixins/logging_mixin.py @@ -8,6 +8,9 @@ class LoggingMixin(): Logging Mixin Shows similar messages only once every `refresh_period`. """ + # Disable output completely + show_output = True + def __init__(self, logger, refresh_period: int = 3600): """ :param refresh_period: in seconds - Show identical messages in this intervals @@ -31,4 +34,5 @@ class LoggingMixin(): # Log as debug first self.logger.debug(message) # Call hidden function. - _log_once(message) + if self.show_output: + _log_once(message) From 5e3d2401f5957de2cbea427f671cb4b013c0e1b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 19 Nov 2020 20:34:29 +0100 Subject: [PATCH 1053/1197] Only call stop methods when they actually support this method --- freqtrade/plugins/protectionmanager.py | 24 ++++++++++--------- .../plugins/protections/cooldown_period.py | 5 ++++ freqtrade/plugins/protections/iprotection.py | 7 +++++- .../plugins/protections/low_profit_pairs.py | 5 ++++ .../plugins/protections/stoploss_guard.py | 9 +++++-- 5 files changed, 36 insertions(+), 14 deletions(-) diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index e58a50c80..d12f4ba80 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -50,22 +50,24 @@ class ProtectionManager(): now = datetime.now(timezone.utc) result = False for protection_handler in self._protection_handlers: - result, until, reason = protection_handler.global_stop(now) + if protection_handler.has_global_stop: + result, until, reason = protection_handler.global_stop(now) - # Early stopping - first positive result blocks further trades - if result and until: - if not PairLocks.is_global_lock(until): - PairLocks.lock_pair('*', until, reason, now=now) - result = True + # Early stopping - first positive result blocks further trades + if result and until: + if not PairLocks.is_global_lock(until): + PairLocks.lock_pair('*', until, reason, now=now) + result = True return result def stop_per_pair(self, pair) -> bool: now = datetime.now(timezone.utc) result = False for protection_handler in self._protection_handlers: - result, until, reason = protection_handler.stop_per_pair(pair, now) - if result and until: - if not PairLocks.is_pair_locked(pair, until): - PairLocks.lock_pair(pair, until, reason, now=now) - result = True + if protection_handler.has_local_stop: + result, until, reason = protection_handler.stop_per_pair(pair, now) + if result and until: + if not PairLocks.is_pair_locked(pair, until): + PairLocks.lock_pair(pair, until, reason, now=now) + result = True return result diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index 447ca4363..4fe0a4fdc 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -12,6 +12,11 @@ logger = logging.getLogger(__name__) class CooldownPeriod(IProtection): + # Can globally stop the bot + has_global_stop: bool = False + # Can stop trading for one pair + has_local_stop: bool = True + def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: super().__init__(config, protection_config) diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 8048fccf0..49fccb0e6 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -1,6 +1,6 @@ import logging -from abc import ABC, abstractmethod +from abc import ABC, abstractmethod, abstractproperty from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, Optional, Tuple @@ -15,6 +15,11 @@ ProtectionReturn = Tuple[bool, Optional[datetime], Optional[str]] class IProtection(LoggingMixin, ABC): + # Can globally stop the bot + has_global_stop: bool = False + # Can stop trading for one pair + has_local_stop: bool = False + def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: self._config = config self._protection_config = protection_config diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index 96fb2b08e..48efa3c9a 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -12,6 +12,11 @@ logger = logging.getLogger(__name__) class LowProfitPairs(IProtection): + # Can globally stop the bot + has_global_stop: bool = False + # Can stop trading for one pair + has_local_stop: bool = True + def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: super().__init__(config, protection_config) diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 8b6871915..51a2fded8 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -15,6 +15,11 @@ logger = logging.getLogger(__name__) class StoplossGuard(IProtection): + # Can globally stop the bot + has_global_stop: bool = True + # Can stop trading for one pair + has_local_stop: bool = True + def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: super().__init__(config, protection_config) @@ -67,7 +72,7 @@ class StoplossGuard(IProtection): :return: Tuple of [bool, until, reason]. If true, all pairs will be locked with until """ - return self._stoploss_guard(date_now, pair=None) + return self._stoploss_guard(date_now, None) def stop_per_pair(self, pair: str, date_now: datetime) -> ProtectionReturn: """ @@ -76,4 +81,4 @@ class StoplossGuard(IProtection): :return: Tuple of [bool, until, reason]. If true, this pair will be locked with until """ - return False, None, None + return self._stoploss_guard(date_now, pair) From be57ceb2526a57cfda4ec209fbbf0d8efc358381 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 21 Nov 2020 14:46:27 +0100 Subject: [PATCH 1054/1197] Remove confusing entry (in this branch of the if statement, candle_date is empty --- freqtrade/strategy/interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 81f4e7651..d14d3a35f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -312,7 +312,7 @@ class IStrategy(ABC): if not candle_date: # Simple call ... - return PairLocks.is_pair_locked(pair, candle_date) + return PairLocks.is_pair_locked(pair) else: lock_time = timeframe_to_next_date(self.timeframe, candle_date) return PairLocks.is_pair_locked(pair, lock_time) From 8d9c66a638af5fa57d11bc5dde8563ebf2ede984 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Nov 2020 11:41:09 +0100 Subject: [PATCH 1055/1197] Add LogginMixin to freqtradebot class to avoid over-logging --- freqtrade/freqtradebot.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f2ee4d7f0..24827a7e3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -19,8 +19,9 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, PricingError) -from freqtrade.exchange import timeframe_to_minutes +from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import safe_value_fallback, safe_value_fallback2 +from freqtrade.mixins import LoggingMixin from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db from freqtrade.plugins.protectionmanager import ProtectionManager @@ -35,7 +36,7 @@ from freqtrade.wallets import Wallets logger = logging.getLogger(__name__) -class FreqtradeBot: +class FreqtradeBot(LoggingMixin): """ Freqtrade is the main class of the bot. This is from here the bot start its logic. @@ -104,6 +105,7 @@ class FreqtradeBot: self.rpc: RPCManager = RPCManager(self) # Protect sell-logic from forcesell and viceversa self._sell_lock = Lock() + LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) def notify_status(self, msg: str) -> None: """ @@ -365,7 +367,7 @@ class FreqtradeBot: "but checking to sell open trades.") return trades_created if PairLocks.is_global_lock(): - logger.info("Global pairlock active. Not creating new trades.") + self.log_once(logger.info, "Global pairlock active. Not creating new trades.") return trades_created # Create entity and execute trade for each pair from whitelist for pair in whitelist: @@ -551,7 +553,7 @@ class FreqtradeBot: analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, self.strategy.timeframe) if self.strategy.is_pair_locked( pair, analyzed_df.iloc[-1]['date'] if len(analyzed_df) > 0 else None): - logger.info(f"Pair {pair} is currently locked.") + self.log_once(logger.info, f"Pair {pair} is currently locked.") return False # get_free_open_trades is checked before create_trade is called From 8f958ef7238dcd1fa3046ce7307873d87ad85a54 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Nov 2020 11:49:41 +0100 Subject: [PATCH 1056/1197] Improve login-mixin structure --- freqtrade/freqtradebot.py | 4 ++-- freqtrade/mixins/logging_mixin.py | 5 +++-- freqtrade/pairlist/AgeFilter.py | 5 ++--- freqtrade/pairlist/PrecisionFilter.py | 4 ++-- freqtrade/pairlist/PriceFilter.py | 18 +++++++++--------- freqtrade/pairlist/SpreadFilter.py | 6 +++--- freqtrade/pairlist/VolumePairList.py | 2 +- freqtrade/pairlist/rangestabilityfilter.py | 6 +++--- .../plugins/protections/cooldown_period.py | 4 ++-- freqtrade/plugins/protections/iprotection.py | 2 +- .../plugins/protections/low_profit_pairs.py | 3 +-- .../plugins/protections/stoploss_guard.py | 4 ++-- tests/plugins/test_pairlist.py | 6 +++--- tests/plugins/test_protections.py | 1 + 14 files changed, 35 insertions(+), 35 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 24827a7e3..265a8ce10 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -367,7 +367,7 @@ class FreqtradeBot(LoggingMixin): "but checking to sell open trades.") return trades_created if PairLocks.is_global_lock(): - self.log_once(logger.info, "Global pairlock active. Not creating new trades.") + self.log_once("Global pairlock active. Not creating new trades.", logger.info) return trades_created # Create entity and execute trade for each pair from whitelist for pair in whitelist: @@ -553,7 +553,7 @@ class FreqtradeBot(LoggingMixin): analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, self.strategy.timeframe) if self.strategy.is_pair_locked( pair, analyzed_df.iloc[-1]['date'] if len(analyzed_df) > 0 else None): - self.log_once(logger.info, f"Pair {pair} is currently locked.") + self.log_once(f"Pair {pair} is currently locked.", logger.info) return False # get_free_open_trades is checked before create_trade is called diff --git a/freqtrade/mixins/logging_mixin.py b/freqtrade/mixins/logging_mixin.py index a8dec2da7..e9921e1ec 100644 --- a/freqtrade/mixins/logging_mixin.py +++ b/freqtrade/mixins/logging_mixin.py @@ -1,5 +1,6 @@ +from typing import Callable from cachetools import TTLCache, cached @@ -19,12 +20,12 @@ class LoggingMixin(): self.refresh_period = refresh_period self._log_cache: TTLCache = TTLCache(maxsize=1024, ttl=self.refresh_period) - def log_once(self, logmethod, message: str) -> None: + def log_once(self, message: str, logmethod: Callable) -> None: """ Logs message - not more often than "refresh_period" to avoid log spamming Logs the log-message as debug as well to simplify debugging. - :param logmethod: Function that'll be called. Most likely `logger.info`. :param message: String containing the message to be sent to the function. + :param logmethod: Function that'll be called. Most likely `logger.info`. :return: None. """ @cached(cache=self._log_cache) diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index dd63c1147..ae2132637 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -76,9 +76,8 @@ class AgeFilter(IPairList): self._symbolsChecked[ticker['symbol']] = int(arrow.utcnow().float_timestamp) * 1000 return True else: - self.log_once(logger.info, - f"Removed {ticker['symbol']} from whitelist, because age " + self.log_once(f"Removed {ticker['symbol']} from whitelist, because age " f"{len(daily_candles)} is less than {self._min_days_listed} " - f"{plural(self._min_days_listed, 'day')}") + f"{plural(self._min_days_listed, 'day')}", logger.info) return False return False diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py index a28d54205..db05d5883 100644 --- a/freqtrade/pairlist/PrecisionFilter.py +++ b/freqtrade/pairlist/PrecisionFilter.py @@ -59,8 +59,8 @@ class PrecisionFilter(IPairList): logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}") if sp <= stop_gap_price: - self.log_once(logger.info, f"Removed {ticker['symbol']} from whitelist, because " - f"stop price {sp} would be <= stop limit {stop_gap_price}") + self.log_once(f"Removed {ticker['symbol']} from whitelist, because " + f"stop price {sp} would be <= stop limit {stop_gap_price}", logger.info) return False return True diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/pairlist/PriceFilter.py index a5d73b728..3686cd138 100644 --- a/freqtrade/pairlist/PriceFilter.py +++ b/freqtrade/pairlist/PriceFilter.py @@ -64,9 +64,9 @@ class PriceFilter(IPairList): :return: True if the pair can stay, false if it should be removed """ if ticker['last'] is None or ticker['last'] == 0: - self.log_once(logger.info, - f"Removed {ticker['symbol']} from whitelist, because " - "ticker['last'] is empty (Usually no trade in the last 24h).") + self.log_once(f"Removed {ticker['symbol']} from whitelist, because " + "ticker['last'] is empty (Usually no trade in the last 24h).", + logger.info) return False # Perform low_price_ratio check. @@ -74,22 +74,22 @@ class PriceFilter(IPairList): compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last']) changeperc = compare / ticker['last'] if changeperc > self._low_price_ratio: - self.log_once(logger.info, f"Removed {ticker['symbol']} from whitelist, " - f"because 1 unit is {changeperc * 100:.3f}%") + self.log_once(f"Removed {ticker['symbol']} from whitelist, " + f"because 1 unit is {changeperc * 100:.3f}%", logger.info) return False # Perform min_price check. if self._min_price != 0: if ticker['last'] < self._min_price: - self.log_once(logger.info, f"Removed {ticker['symbol']} from whitelist, " - f"because last price < {self._min_price:.8f}") + self.log_once(f"Removed {ticker['symbol']} from whitelist, " + f"because last price < {self._min_price:.8f}", logger.info) return False # Perform max_price check. if self._max_price != 0: if ticker['last'] > self._max_price: - self.log_once(logger.info, f"Removed {ticker['symbol']} from whitelist, " - f"because last price > {self._max_price:.8f}") + self.log_once(f"Removed {ticker['symbol']} from whitelist, " + f"because last price > {self._max_price:.8f}", logger.info) return False return True diff --git a/freqtrade/pairlist/SpreadFilter.py b/freqtrade/pairlist/SpreadFilter.py index 963ecb82a..6c4e9f12f 100644 --- a/freqtrade/pairlist/SpreadFilter.py +++ b/freqtrade/pairlist/SpreadFilter.py @@ -45,9 +45,9 @@ class SpreadFilter(IPairList): if 'bid' in ticker and 'ask' in ticker: spread = 1 - ticker['bid'] / ticker['ask'] if spread > self._max_spread_ratio: - self.log_once(logger.info, - f"Removed {ticker['symbol']} from whitelist, because spread " - f"{spread * 100:.3f}% > {self._max_spread_ratio * 100}%") + self.log_once(f"Removed {ticker['symbol']} from whitelist, because spread " + f"{spread * 100:.3f}% > {self._max_spread_ratio * 100}%", + logger.info) return False else: return True diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 24e1674fd..7056bc59d 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -111,6 +111,6 @@ class VolumePairList(IPairList): # Limit pairlist to the requested number of pairs pairs = pairs[:self._number_pairs] - self.log_once(logger.info, f"Searching {self._number_pairs} pairs: {pairs}") + self.log_once(f"Searching {self._number_pairs} pairs: {pairs}", logger.info) return pairs diff --git a/freqtrade/pairlist/rangestabilityfilter.py b/freqtrade/pairlist/rangestabilityfilter.py index 7a1b69a1a..756368355 100644 --- a/freqtrade/pairlist/rangestabilityfilter.py +++ b/freqtrade/pairlist/rangestabilityfilter.py @@ -78,10 +78,10 @@ class RangeStabilityFilter(IPairList): if pct_change >= self._min_rate_of_change: result = True else: - self.log_once(logger.info, - f"Removed {pair} from whitelist, because rate of change " + self.log_once(f"Removed {pair} from whitelist, because rate of change " f"over {plural(self._days, 'day')} is {pct_change:.3f}, " - f"which is below the threshold of {self._min_rate_of_change}.") + f"which is below the threshold of {self._min_rate_of_change}.", + logger.info) result = False self._pair_cache[pair] = result diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index 4fe0a4fdc..1abec7218 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -1,6 +1,6 @@ import logging -from datetime import datetime, timedelta, timezone +from datetime import datetime, timedelta from typing import Any, Dict from freqtrade.persistence import Trade @@ -46,7 +46,7 @@ class CooldownPeriod(IProtection): ] trade = Trade.get_trades(filters).first() if trade: - self.log_once(logger.info, f"Cooldown for {pair} for {self._stop_duration}.") + self.log_once(f"Cooldown for {pair} for {self._stop_duration}.", logger.info) until = self.calculate_lock_end([trade], self._stop_duration) return True, until, self._reason() diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 49fccb0e6..0f539bbd3 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -1,6 +1,6 @@ import logging -from abc import ABC, abstractmethod, abstractproperty +from abc import ABC, abstractmethod from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, Optional, Tuple diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index 48efa3c9a..c45ba3a39 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -58,9 +58,8 @@ class LowProfitPairs(IProtection): profit = sum(trade.close_profit for trade in trades) if profit < self._required_profit: self.log_once( - logger.info, f"Trading for {pair} stopped due to {profit:.2f} < {self._required_profit} " - f"within {self._lookback_period} minutes.") + f"within {self._lookback_period} minutes.", logger.info) until = self.calculate_lock_end(trades, self._stop_duration) return True, until, self._reason(profit) diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 51a2fded8..0645d366b 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -58,8 +58,8 @@ class StoplossGuard(IProtection): trades = Trade.get_trades(filters).all() if len(trades) > self._trade_limit: - self.log_once(logger.info, f"Trading stopped due to {self._trade_limit} " - f"stoplosses within {self._lookback_period} minutes.") + self.log_once(f"Trading stopped due to {self._trade_limit} " + f"stoplosses within {self._lookback_period} minutes.", logger.info) until = self.calculate_lock_end(trades, self._stop_duration) return True, until, self._reason() diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 2f1617f6c..c2a4a69d7 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -102,14 +102,14 @@ def test_log_cached(mocker, static_pl_conf, markets, tickers): logmock = MagicMock() # Assign starting whitelist pl = freqtrade.pairlists._pairlist_handlers[0] - pl.log_once(logmock, 'Hello world') + pl.log_once('Hello world', logmock) assert logmock.call_count == 1 - pl.log_once(logmock, 'Hello world') + pl.log_once('Hello world', logmock) assert logmock.call_count == 1 assert pl._log_cache.currsize == 1 assert ('Hello world',) in pl._log_cache._Cache__data - pl.log_once(logmock, 'Hello world2') + pl.log_once('Hello world2', logmock) assert logmock.call_count == 2 assert pl._log_cache.currsize == 2 diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 3417b1a56..1a22d08a2 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -165,6 +165,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): assert PairLocks.is_pair_locked('XRP/BTC') assert not PairLocks.is_global_lock() + @pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [ ({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2}, "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " From 32cde1cb7da970b3dde7874db35c57984f442409 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 10:48:54 +0100 Subject: [PATCH 1057/1197] Improve test for lowprofitpairs --- tests/plugins/test_protections.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 1a22d08a2..a02a0366c 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -11,6 +11,7 @@ from tests.conftest import get_patched_freqtradebot, log_has_re def generate_mock_trade(pair: str, fee: float, is_open: bool, sell_reason: str = SellType.SELL_SIGNAL, min_ago_open: int = None, min_ago_close: int = None, + profit_rate: float = 0.9 ): open_rate = random.random() @@ -28,8 +29,9 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool, ) trade.recalc_open_trade_price() if not is_open: - trade.close(open_rate * (1 - 0.9)) + trade.close(open_rate * profit_rate) trade.sell_reason = sell_reason + return trade @@ -134,7 +136,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): Trade.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, - min_ago_open=800, min_ago_close=450, + min_ago_open=800, min_ago_close=450, profit_rate=0.9, )) # Not locked with 1 trade @@ -145,7 +147,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): Trade.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, - min_ago_open=200, min_ago_close=120, + min_ago_open=200, min_ago_close=120, profit_rate=0.9, )) # Not locked with 1 trade (first trade is outside of lookback_period) @@ -154,9 +156,17 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): assert not PairLocks.is_pair_locked('XRP/BTC') assert not PairLocks.is_global_lock() + # Add positive trade + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, + min_ago_open=20, min_ago_close=10, profit_rate=1.15, + )) + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + assert not PairLocks.is_pair_locked('XRP/BTC') + Trade.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, - min_ago_open=110, min_ago_close=20, + min_ago_open=110, min_ago_close=20, profit_rate=0.8, )) # Locks due to 2nd trade @@ -166,6 +176,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): assert not PairLocks.is_global_lock() + @pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [ ({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2}, "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " From dcdf4a0503281c02598b2d171c0e5f06f7878e15 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 10:58:50 +0100 Subject: [PATCH 1058/1197] Improve tests --- tests/plugins/test_protections.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index a02a0366c..ce0ad7d5e 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -5,6 +5,7 @@ import pytest from freqtrade.persistence import PairLocks, Trade from freqtrade.strategy.interface import SellType +from freqtrade import constants from tests.conftest import get_patched_freqtradebot, log_has_re @@ -35,6 +36,19 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool, return trade +def test_protectionmanager(mocker, default_conf): + default_conf['protections'] = [{'method': protection} + for protection in constants.AVAILABLE_PROTECTIONS] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + + for handler in freqtrade.protections._protection_handlers: + assert handler.name in constants.AVAILABLE_PROTECTIONS + if not handler.has_global_stop: + assert handler.global_stop(datetime.utcnow()) == (False, None, None) + if not handler.has_local_stop: + assert handler.local_stop('XRP/BTC', datetime.utcnow()) == (False, None, None) + + @pytest.mark.usefixtures("init_persistence") def test_stoploss_guard(mocker, default_conf, fee, caplog): default_conf['protections'] = [{ @@ -176,7 +190,6 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): assert not PairLocks.is_global_lock() - @pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [ ({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2}, "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " From dce236467224f4d80ce7c6c90927796fdff54722 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 11:11:55 +0100 Subject: [PATCH 1059/1197] Add stoploss per pair support --- docs/includes/protections.md | 4 +- .../plugins/protections/stoploss_guard.py | 3 + tests/plugins/test_protections.py | 58 +++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 91b10cf65..644b98e64 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -17,6 +17,7 @@ Protections will protect your strategy from unexpected events and market conditi #### Stoploss Guard `StoplossGuard` selects all trades within a `lookback_period` (in minutes), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration`. +This applies across all pairs, unless `only_per_pair` is set to true, which will then only look at one pair at a time. ```json "protections": [ @@ -24,7 +25,8 @@ Protections will protect your strategy from unexpected events and market conditi "method": "StoplossGuard", "lookback_period": 60, "trade_limit": 4, - "stop_duration": 60 + "stop_duration": 60, + "only_per_pair": false } ], ``` diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 0645d366b..1ad839f3d 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -26,6 +26,7 @@ class StoplossGuard(IProtection): self._lookback_period = protection_config.get('lookback_period', 60) self._trade_limit = protection_config.get('trade_limit', 10) self._stop_duration = protection_config.get('stop_duration', 60) + self._disable_global_stop = protection_config.get('only_per_pair', False) def short_desc(self) -> str: """ @@ -72,6 +73,8 @@ class StoplossGuard(IProtection): :return: Tuple of [bool, until, reason]. If true, all pairs will be locked with until """ + if self._disable_global_stop: + return False, None, None return self._stoploss_guard(date_now, None) def stop_per_pair(self, pair: str, date_now: datetime) -> ProtectionReturn: diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index ce0ad7d5e..7eac737ef 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -95,6 +95,64 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): assert PairLocks.is_global_lock() +@pytest.mark.parametrize('only_per_pair', [False, True]) +@pytest.mark.usefixtures("init_persistence") +def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair): + default_conf['protections'] = [{ + "method": "StoplossGuard", + "lookback_period": 60, + "trade_limit": 1, + "only_per_pair": only_per_pair + }] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + message = r"Trading stopped due to .*" + pair = 'XRP/BTC' + assert not freqtrade.protections.stop_per_pair(pair) + assert not freqtrade.protections.global_stop() + assert not log_has_re(message, caplog) + caplog.clear() + + Trade.session.add(generate_mock_trade( + pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=200, min_ago_close=30, profit_rate=0.9, + )) + + assert not freqtrade.protections.stop_per_pair(pair) + assert not freqtrade.protections.global_stop() + assert not log_has_re(message, caplog) + caplog.clear() + # This trade does not count, as it's closed too long ago + Trade.session.add(generate_mock_trade( + pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=250, min_ago_close=100, profit_rate=0.9, + )) + # Trade does not count for per pair stop as it's the wrong pair. + Trade.session.add(generate_mock_trade( + 'ETH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=240, min_ago_close=30, profit_rate=0.9, + )) + # 3 Trades closed - but the 2nd has been closed too long ago. + assert not freqtrade.protections.stop_per_pair(pair) + assert freqtrade.protections.global_stop() != only_per_pair + if not only_per_pair: + assert log_has_re(message, caplog) + else: + assert not log_has_re(message, caplog) + + caplog.clear() + + # 2nd Trade that counts with correct pair + Trade.session.add(generate_mock_trade( + pair, fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=180, min_ago_close=30, profit_rate=0.9, + )) + + assert freqtrade.protections.stop_per_pair(pair) + assert freqtrade.protections.global_stop() != only_per_pair + assert PairLocks.is_pair_locked(pair) + assert PairLocks.is_global_lock() != only_per_pair + + @pytest.mark.usefixtures("init_persistence") def test_CooldownPeriod(mocker, default_conf, fee, caplog): default_conf['protections'] = [{ From 6d0f16920f47961f9bae3d3be0e316fbbf368bea Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 11:54:11 +0100 Subject: [PATCH 1060/1197] Get Longest lock logic --- freqtrade/freqtradebot.py | 20 +++++++++--- freqtrade/persistence/pairlock_middleware.py | 11 ++++++- tests/plugins/test_pairlocks.py | 32 ++++++++++++++++++++ tests/test_freqtradebot.py | 8 ++--- 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 265a8ce10..1e0f5fdf0 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -367,7 +367,13 @@ class FreqtradeBot(LoggingMixin): "but checking to sell open trades.") return trades_created if PairLocks.is_global_lock(): - self.log_once("Global pairlock active. Not creating new trades.", logger.info) + lock = PairLocks.get_pair_longest_lock('*') + if lock: + self.log_once(f"Global pairlock active until " + f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)}. " + "Not creating new trades.", logger.info) + else: + self.log_once("Global pairlock active. Not creating new trades.", logger.info) return trades_created # Create entity and execute trade for each pair from whitelist for pair in whitelist: @@ -551,9 +557,15 @@ class FreqtradeBot(LoggingMixin): logger.debug(f"create_trade for pair {pair}") analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, self.strategy.timeframe) - if self.strategy.is_pair_locked( - pair, analyzed_df.iloc[-1]['date'] if len(analyzed_df) > 0 else None): - self.log_once(f"Pair {pair} is currently locked.", logger.info) + nowtime = analyzed_df.iloc[-1]['date'] if len(analyzed_df) > 0 else None + if self.strategy.is_pair_locked(pair, nowtime): + lock = PairLocks.get_pair_longest_lock(pair, nowtime) + if lock: + self.log_once(f"Pair {pair} is still locked until " + f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)}.", + logger.info) + else: + self.log_once(f"Pair {pair} is still locked.", logger.info) return False # get_free_open_trades is checked before create_trade is called diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index 38b5a5d63..de804f025 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -46,7 +46,7 @@ class PairLocks(): PairLocks.locks.append(lock) @staticmethod - def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> List[PairLock]: + def get_pair_locks(pair: str, now: Optional[datetime] = None) -> List[PairLock]: """ Get all currently active locks for this pair :param pair: Pair to check for. Returns all current locks if pair is empty @@ -66,6 +66,15 @@ class PairLocks(): )] return locks + @staticmethod + def get_pair_longest_lock(pair: str, now: Optional[datetime] = None) -> Optional[PairLock]: + """ + Get the lock that expires the latest for the pair given. + """ + locks = PairLocks.get_pair_locks(pair, now) + locks = sorted(locks, key=lambda l: l.lock_end_time, reverse=True) + return locks[0] if locks else None + @staticmethod def unlock_pair(pair: str, now: Optional[datetime] = None) -> None: """ diff --git a/tests/plugins/test_pairlocks.py b/tests/plugins/test_pairlocks.py index 0b6b89717..db7d9f46f 100644 --- a/tests/plugins/test_pairlocks.py +++ b/tests/plugins/test_pairlocks.py @@ -80,3 +80,35 @@ def test_PairLocks(use_db): assert len(PairLock.query.all()) == 0 # Reset use-db variable PairLocks.use_db = True + + +@pytest.mark.parametrize('use_db', (False, True)) +@pytest.mark.usefixtures("init_persistence") +def test_PairLocks_getlongestlock(use_db): + PairLocks.timeframe = '5m' + # No lock should be present + if use_db: + assert len(PairLock.query.all()) == 0 + else: + PairLocks.use_db = False + + assert PairLocks.use_db == use_db + + pair = 'ETH/BTC' + assert not PairLocks.is_pair_locked(pair) + PairLocks.lock_pair(pair, arrow.utcnow().shift(minutes=4).datetime) + # ETH/BTC locked for 4 minutes + assert PairLocks.is_pair_locked(pair) + lock = PairLocks.get_pair_longest_lock(pair) + + assert lock.lock_end_time.replace(tzinfo=timezone.utc) > arrow.utcnow().shift(minutes=3) + assert lock.lock_end_time.replace(tzinfo=timezone.utc) < arrow.utcnow().shift(minutes=14) + + PairLocks.lock_pair(pair, arrow.utcnow().shift(minutes=15).datetime) + assert PairLocks.is_pair_locked(pair) + + lock = PairLocks.get_pair_longest_lock(pair) + # Must be longer than above + assert lock.lock_end_time.replace(tzinfo=timezone.utc) > arrow.utcnow().shift(minutes=14) + + PairLocks.use_db = True diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 94ed06cd9..142729f4d 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -692,16 +692,16 @@ def test_enter_positions_global_pairlock(default_conf, ticker, limit_buy_order, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) n = freqtrade.enter_positions() - message = "Global pairlock active. Not creating new trades." + message = r"Global pairlock active until.* Not creating new trades." n = freqtrade.enter_positions() # 0 trades, but it's not because of pairlock. assert n == 0 - assert not log_has(message, caplog) + assert not log_has_re(message, caplog) PairLocks.lock_pair('*', arrow.utcnow().shift(minutes=20).datetime, 'Just because') n = freqtrade.enter_positions() assert n == 0 - assert log_has(message, caplog) + assert log_has_re(message, caplog) def test_create_trade_no_signal(default_conf, fee, mocker) -> None: @@ -3289,7 +3289,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo caplog.clear() freqtrade.enter_positions() - assert log_has(f"Pair {trade.pair} is currently locked.", caplog) + assert log_has_re(f"Pair {trade.pair} is still locked.*", caplog) def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_buy_order_open, From 12e84bda1e1940333ba8fb649d289c0fd5303a98 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 10:29:45 +0100 Subject: [PATCH 1061/1197] Add developer docs for Protections --- docs/developer.md | 47 +++++++++++++++++++++++++++++++++++- docs/includes/protections.md | 2 +- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index 662905d65..86e9b1078 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -94,7 +94,7 @@ Below is an outline of exception inheritance hierarchy: +---+ StrategyError ``` -## Modules +## Plugins ### Pairlists @@ -173,6 +173,51 @@ In `VolumePairList`, this implements different methods of sorting, does early va return pairs ``` +### Protections + +Best read the [Protection documentation](configuration.md#protections) to understand protections. +This Guide is directed towards Developers who want to develop a new protection. + +No protection should use datetime directly, but use the provided `date_now` variable for date calculations. This preserves the ability to backtest protections. + +!!! Tip "Writing a new Protection" + Best copy one of the existing Protections to have a good example. + +#### Implementation of a new protection + +All Protection implementations must have `IProtection` as parent class. +For that reason, they must implement the following methods: + +* `short_desc()` +* `global_stop()` +* `stop_per_pair()`. + +`global_stop()` and `stop_per_pair()` must return a ProtectionReturn tuple, which consists of: + +* lock pair - boolean +* lock until - datetime - until when should the pair be locked (will be rounded up to the next new candle) +* reason - string, used for logging and storage in the database + +The `until` portion should be calculated using the provided `calculate_lock_end()` method. + +#### Global vs. local stops + +Protections can have 2 different ways to stop trading for a limited : + +* Per pair (local) +* For all Pairs (globally) + +##### Protections - per pair + +Protections that implement the per pair approach must set `has_local_stop=True`. +The method `stop_per_pair()` will be called once, whenever a sell order is closed, and the trade is therefore closed. + +##### Protections - global protection + +These Protections should do their evaluation across all pairs, and consequently will also lock all pairs from trading (called a global PairLock). +Global protection must set `has_global_stop=True` to be evaluated for global stops. +The method `global_stop()` will be called on every iteration, so they should not do too heavy calculations (or should cache the calculations across runs). + ## Implement a new Exchange (WIP) !!! Note diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 644b98e64..aaf5bbff4 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -1,6 +1,6 @@ ## Protections -Protections will protect your strategy from unexpected events and market conditions. +Protections will protect your strategy from unexpected events and market conditions by temporarily stop trading for either one pair, or for all pairs. !!! Note Not all Protections will work for all strategies, and parameters will need to be tuned for your strategy. From 4351a26b4cc84abfab5b0fd901c60918d488175e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 10:32:23 +0100 Subject: [PATCH 1062/1197] Move stop_duration to parent class avoids reimplementation and enhances standardization --- docs/developer.md | 3 +++ freqtrade/plugins/protections/cooldown_period.py | 2 -- freqtrade/plugins/protections/iprotection.py | 2 ++ freqtrade/plugins/protections/low_profit_pairs.py | 1 - freqtrade/plugins/protections/stoploss_guard.py | 1 - 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index 86e9b1078..ebfe8e013 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -200,6 +200,9 @@ For that reason, they must implement the following methods: The `until` portion should be calculated using the provided `calculate_lock_end()` method. +All Protections should use `"stop_duration"` to define how long a a pair (or all pairs) should be locked. +The content of this is made available as `self._stop_duration` to the each Protection. + #### Global vs. local stops Protections can have 2 different ways to stop trading for a limited : diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index 1abec7218..18a73ef5b 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -20,8 +20,6 @@ class CooldownPeriod(IProtection): def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: super().__init__(config, protection_config) - self._stop_duration = protection_config.get('stop_duration', 60) - def _reason(self) -> str: """ LockReason to use diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 0f539bbd3..2053ae741 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -23,6 +23,8 @@ class IProtection(LoggingMixin, ABC): def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: self._config = config self._protection_config = protection_config + self._stop_duration = protection_config.get('stop_duration', 60) + LoggingMixin.__init__(self, logger) @property diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index c45ba3a39..cd850ca0c 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -22,7 +22,6 @@ class LowProfitPairs(IProtection): self._lookback_period = protection_config.get('lookback_period', 60) self._trade_limit = protection_config.get('trade_limit', 1) - self._stop_duration = protection_config.get('stop_duration', 60) self._required_profit = protection_config.get('required_profit', 0.0) def short_desc(self) -> str: diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 1ad839f3d..65403d683 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -25,7 +25,6 @@ class StoplossGuard(IProtection): self._lookback_period = protection_config.get('lookback_period', 60) self._trade_limit = protection_config.get('trade_limit', 10) - self._stop_duration = protection_config.get('stop_duration', 60) self._disable_global_stop = protection_config.get('only_per_pair', False) def short_desc(self) -> str: From 397a15cb617ffb668007632cb6cb9cc3c8717639 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 17:14:28 +0100 Subject: [PATCH 1063/1197] Improve protection documentation --- docs/includes/protections.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index aaf5bbff4..9722e70aa 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -1,6 +1,7 @@ ## Protections Protections will protect your strategy from unexpected events and market conditions by temporarily stop trading for either one pair, or for all pairs. +All protection end times are rounded up to the next candle to avoid sudden, unexpected intra-candle buys. !!! Note Not all Protections will work for all strategies, and parameters will need to be tuned for your strategy. From ad746627b339d243b4bcd9a15a1a54d4a4cb3051 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 17:47:15 +0100 Subject: [PATCH 1064/1197] Fix lock-loop --- docs/developer.md | 4 ++-- freqtrade/freqtradebot.py | 6 +----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index ebfe8e013..6ea641edd 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -213,13 +213,13 @@ Protections can have 2 different ways to stop trading for a limited : ##### Protections - per pair Protections that implement the per pair approach must set `has_local_stop=True`. -The method `stop_per_pair()` will be called once, whenever a sell order is closed, and the trade is therefore closed. +The method `stop_per_pair()` will be called whenever a trade closed (sell order completed). ##### Protections - global protection These Protections should do their evaluation across all pairs, and consequently will also lock all pairs from trading (called a global PairLock). Global protection must set `has_global_stop=True` to be evaluated for global stops. -The method `global_stop()` will be called on every iteration, so they should not do too heavy calculations (or should cache the calculations across runs). +The method `global_stop()` will be called whenever a trade closed (sell order completed). ## Implement a new Exchange (WIP) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1e0f5fdf0..ecc824a86 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -182,9 +182,6 @@ class FreqtradeBot(LoggingMixin): # First process current opened trades (positions) self.exit_positions(trades) - # Evaluate if protections should apply - self.protections.global_stop() - # Then looking for buy opportunities if self.get_free_open_trades(): self.enter_positions() @@ -1431,8 +1428,7 @@ class FreqtradeBot(LoggingMixin): # Updating wallets when order is closed if not trade.is_open: self.protections.stop_per_pair(trade.pair) - # Evaluate if protections should apply - # self.protections.global_stop() + self.protections.global_stop() self.wallets.update() return False From 9947dcd1da1660efed3d676c4e537f9c5bd0d045 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 17:51:58 +0100 Subject: [PATCH 1065/1197] Beta feature warning --- docs/includes/protections.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 9722e70aa..716addb55 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -1,5 +1,8 @@ ## Protections +!!! Warning "Beta feature" + This feature is still in it's testing phase. Should you notice something you think is wrong please let us know via Discord, Slack or via Issue. + Protections will protect your strategy from unexpected events and market conditions by temporarily stop trading for either one pair, or for all pairs. All protection end times are rounded up to the next candle to avoid sudden, unexpected intra-candle buys. From 768d7fa1966e3694ac97daba57833db8d3c08809 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 18:02:34 +0100 Subject: [PATCH 1066/1197] Readd optional for get_pair_locks - it's necessary --- freqtrade/persistence/pairlock_middleware.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index de804f025..6ce91ee6b 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -46,7 +46,7 @@ class PairLocks(): PairLocks.locks.append(lock) @staticmethod - def get_pair_locks(pair: str, now: Optional[datetime] = None) -> List[PairLock]: + def get_pair_locks(pair: Optional[str], now: Optional[datetime] = None) -> List[PairLock]: """ Get all currently active locks for this pair :param pair: Pair to check for. Returns all current locks if pair is empty From 9d6f3a89ef26081ecab03c3455c9cfa4063ad856 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Nov 2020 11:36:16 +0100 Subject: [PATCH 1067/1197] Improve docs and fix typos --- docs/developer.md | 11 +++++++++++ freqtrade/constants.py | 4 ++-- tests/plugins/test_protections.py | 1 + 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index 6ea641edd..05b518184 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -94,6 +94,8 @@ Below is an outline of exception inheritance hierarchy: +---+ StrategyError ``` +--- + ## Plugins ### Pairlists @@ -203,6 +205,8 @@ The `until` portion should be calculated using the provided `calculate_lock_end( All Protections should use `"stop_duration"` to define how long a a pair (or all pairs) should be locked. The content of this is made available as `self._stop_duration` to the each Protection. +If your protection requires a look-back period, please use `"lookback_period"` to keep different protections aligned. + #### Global vs. local stops Protections can have 2 different ways to stop trading for a limited : @@ -221,6 +225,13 @@ These Protections should do their evaluation across all pairs, and consequently Global protection must set `has_global_stop=True` to be evaluated for global stops. The method `global_stop()` will be called whenever a trade closed (sell order completed). +##### Protections - calculating lock end time + +Protections should calculate the lock end time based on the last trade it considers. +This avoids relocking should the lookback-period be longer than the actual lock period. + +--- + ## Implement a new Exchange (WIP) !!! Note diff --git a/freqtrade/constants.py b/freqtrade/constants.py index bc8acc8b3..add9aae95 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -204,8 +204,8 @@ CONF_SCHEMA = { 'properties': { 'method': {'type': 'string', 'enum': AVAILABLE_PROTECTIONS}, 'stop_duration': {'type': 'number', 'minimum': 0.0}, - 'trade_limit': {'type': 'number', 'integer': 1}, - 'lookback_period': {'type': 'number', 'integer': 1}, + 'trade_limit': {'type': 'number', 'minimum': 1}, + 'lookback_period': {'type': 'number', 'minimum': 1}, }, 'required': ['method'], } diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 7eac737ef..24594c3ac 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -54,6 +54,7 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): default_conf['protections'] = [{ "method": "StoplossGuard", "lookback_period": 60, + "stop_duration": 40, "trade_limit": 2 }] freqtrade = get_patched_freqtradebot(mocker, default_conf) From 089c463cfb04404a8ea2592dddaf489ed78615cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Nov 2020 08:05:48 +0100 Subject: [PATCH 1068/1197] Introduce max_drawdown protection --- freqtrade/constants.py | 2 +- .../plugins/protections/cooldown_period.py | 2 - .../plugins/protections/low_profit_pairs.py | 2 - .../protections/max_drawdown_protection.py | 91 +++++++++++++++++++ .../plugins/protections/stoploss_guard.py | 2 - 5 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 freqtrade/plugins/protections/max_drawdown_protection.py diff --git a/freqtrade/constants.py b/freqtrade/constants.py index add9aae95..dfc21b678 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -27,7 +27,7 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'AgeFilter', 'PerformanceFilter', 'PrecisionFilter', 'PriceFilter', 'RangeStabilityFilter', 'ShuffleFilter', 'SpreadFilter'] -AVAILABLE_PROTECTIONS = ['StoplossGuard', 'CooldownPeriod', 'LowProfitPairs'] +AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] DRY_RUN_WALLET = 1000 DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S' diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index 18a73ef5b..7b37b2303 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -12,9 +12,7 @@ logger = logging.getLogger(__name__) class CooldownPeriod(IProtection): - # Can globally stop the bot has_global_stop: bool = False - # Can stop trading for one pair has_local_stop: bool = True def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index cd850ca0c..515f81521 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -12,9 +12,7 @@ logger = logging.getLogger(__name__) class LowProfitPairs(IProtection): - # Can globally stop the bot has_global_stop: bool = False - # Can stop trading for one pair has_local_stop: bool = True def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py new file mode 100644 index 000000000..e8a920908 --- /dev/null +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -0,0 +1,91 @@ + +import logging +from datetime import datetime, timedelta +from typing import Any, Dict + +import pandas as pd + +from freqtrade.data.btanalysis import calculate_max_drawdown +from freqtrade.persistence import Trade +from freqtrade.plugins.protections import IProtection, ProtectionReturn + + +logger = logging.getLogger(__name__) + + +class MaxDrawdown(IProtection): + + has_global_stop: bool = True + has_local_stop: bool = False + + def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: + super().__init__(config, protection_config) + + self._lookback_period = protection_config.get('lookback_period', 60) + self._trade_limit = protection_config.get('trade_limit', 1) + self._max_allowed_drawdown = protection_config.get('max_allowed_drawdown', 0.0) + # TODO: Implement checks to limit max_drawdown to sensible values + + def short_desc(self) -> str: + """ + Short method description - used for startup-messages + """ + return (f"{self.name} - Max drawdown protection, stop trading if drawdown is > " + f"{self._max_allowed_drawdown} within {self._lookback_period} minutes.") + + def _reason(self, drawdown: float) -> str: + """ + LockReason to use + """ + return (f'{drawdown} > {self._max_allowed_drawdown} in {self._lookback_period} min, ' + f'locking for {self._stop_duration} min.') + + def _max_drawdown(self, date_now: datetime, pair: str) -> ProtectionReturn: + """ + Evaluate recent trades for drawdown ... + """ + look_back_until = date_now - timedelta(minutes=self._lookback_period) + filters = [ + Trade.is_open.is_(False), + Trade.close_date > look_back_until, + ] + if pair: + filters.append(Trade.pair == pair) + trades = Trade.get_trades(filters).all() + + trades_df = pd.DataFrame(trades) + + if len(trades) < self._trade_limit: + # Not enough trades in the relevant period + return False, None, None + + # Drawdown is always positive + drawdown, _, _ = calculate_max_drawdown(trades_df) + + if drawdown > self._max_allowed_drawdown: + self.log_once( + f"Trading for {pair} stopped due to {drawdown:.2f} < {self._max_allowed_drawdown} " + f"within {self._lookback_period} minutes.", logger.info) + until = self.calculate_lock_end(trades, self._stop_duration) + + return True, until, self._reason(drawdown) + + return False, None, None + + def global_stop(self, date_now: datetime) -> ProtectionReturn: + """ + Stops trading (position entering) for all pairs + This must evaluate to true for the whole period of the "cooldown period". + :return: Tuple of [bool, until, reason]. + If true, all pairs will be locked with until + """ + return self._max_drawdown(date_now) + + def stop_per_pair(self, pair: str, date_now: datetime) -> ProtectionReturn: + """ + Stops trading (position entering) for this pair + This must evaluate to true for the whole period of the "cooldown period". + :return: Tuple of [bool, until, reason]. + If true, this pair will be locked with until + """ + return False, None, None diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 65403d683..b6f430085 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -15,9 +15,7 @@ logger = logging.getLogger(__name__) class StoplossGuard(IProtection): - # Can globally stop the bot has_global_stop: bool = True - # Can stop trading for one pair has_local_stop: bool = True def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: From f06b58dc91d946f4b929531ad4f01ba5754860db Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Nov 2020 19:07:39 +0100 Subject: [PATCH 1069/1197] Test MaxDrawdown desc --- .../protections/max_drawdown_protection.py | 12 ++-- tests/plugins/test_protections.py | 72 ++++++++++++++++++- 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index e8a920908..e5625733c 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -40,7 +40,7 @@ class MaxDrawdown(IProtection): return (f'{drawdown} > {self._max_allowed_drawdown} in {self._lookback_period} min, ' f'locking for {self._stop_duration} min.') - def _max_drawdown(self, date_now: datetime, pair: str) -> ProtectionReturn: + def _max_drawdown(self, date_now: datetime) -> ProtectionReturn: """ Evaluate recent trades for drawdown ... """ @@ -49,23 +49,21 @@ class MaxDrawdown(IProtection): Trade.is_open.is_(False), Trade.close_date > look_back_until, ] - if pair: - filters.append(Trade.pair == pair) trades = Trade.get_trades(filters).all() - trades_df = pd.DataFrame(trades) + trades_df = pd.DataFrame([trade.to_json() for trade in trades]) if len(trades) < self._trade_limit: # Not enough trades in the relevant period return False, None, None # Drawdown is always positive - drawdown, _, _ = calculate_max_drawdown(trades_df) + drawdown, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit') if drawdown > self._max_allowed_drawdown: self.log_once( - f"Trading for {pair} stopped due to {drawdown:.2f} < {self._max_allowed_drawdown} " - f"within {self._lookback_period} minutes.", logger.info) + f"Trading stopped due to Max Drawdown {drawdown:.2f} < {self._max_allowed_drawdown}" + f" within {self._lookback_period} minutes.", logger.info) until = self.calculate_lock_end(trades, self._stop_duration) return True, until, self._reason(drawdown) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 24594c3ac..e5bbec431 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -46,7 +46,7 @@ def test_protectionmanager(mocker, default_conf): if not handler.has_global_stop: assert handler.global_stop(datetime.utcnow()) == (False, None, None) if not handler.has_local_stop: - assert handler.local_stop('XRP/BTC', datetime.utcnow()) == (False, None, None) + assert handler.stop_per_pair('XRP/BTC', datetime.utcnow()) == (False, None, None) @pytest.mark.usefixtures("init_persistence") @@ -249,6 +249,71 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): assert not PairLocks.is_global_lock() +@pytest.mark.usefixtures("init_persistence") +def test_MaxDrawdown(mocker, default_conf, fee, caplog): + default_conf['protections'] = [{ + "method": "MaxDrawdown", + "lookback_period": 1000, + "stopduration": 60, + "trade_limit": 3, + "max_allowed_drawdown": 0.15 + }] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + message = r"Trading stopped due to Max.*" + + assert not freqtrade.protections.global_stop() + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + caplog.clear() + + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=1000, min_ago_close=900, profit_rate=1.1, + )) + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=500, min_ago_close=400, profit_rate=0.9, + )) + # Not locked with one trade + assert not freqtrade.protections.global_stop() + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + assert not PairLocks.is_pair_locked('XRP/BTC') + assert not PairLocks.is_global_lock() + + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=1200, min_ago_close=1100, profit_rate=0.5, + )) + + # Not locked with 1 trade (2nd trade is outside of lookback_period) + assert not freqtrade.protections.global_stop() + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + assert not PairLocks.is_pair_locked('XRP/BTC') + assert not PairLocks.is_global_lock() + assert not log_has_re(message, caplog) + + # Winning trade ... (should not lock, does not change drawdown!) + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, + min_ago_open=320, min_ago_close=410, profit_rate=1.5, + )) + assert not freqtrade.protections.global_stop() + assert not PairLocks.is_global_lock() + + caplog.clear() + + # Add additional negative trade, causing a loss of > 15% + Trade.session.add(generate_mock_trade( + 'XRP/BTC', fee.return_value, False, sell_reason=SellType.ROI.value, + min_ago_open=20, min_ago_close=10, profit_rate=0.8, + )) + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + # local lock not supported + assert not PairLocks.is_pair_locked('XRP/BTC') + assert freqtrade.protections.global_stop() + assert PairLocks.is_global_lock() + assert log_has_re(message, caplog) + + @pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [ ({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2}, "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " @@ -264,6 +329,11 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): "profit < 0.0 within 60 minutes.'}]", None ), + ({"method": "MaxDrawdown", "stopduration": 60}, + "[{'MaxDrawdown': 'MaxDrawdown - Max drawdown protection, stop trading if drawdown is > 0.0 " + "within 60 minutes.'}]", + None + ), ]) def test_protection_manager_desc(mocker, default_conf, protectionconf, desc_expected, exception_expected): From b36f333b2fed15bd864526b98a44a9604f96dc38 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Dec 2020 06:52:19 +0100 Subject: [PATCH 1070/1197] Add new protections to full sample, documentation --- config_full.json.example | 14 +++++++++ docs/includes/protections.md | 59 ++++++++++++++++++++++++++++-------- 2 files changed, 60 insertions(+), 13 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 839f99dbd..737015b41 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -85,6 +85,20 @@ { "method": "CooldownPeriod", "stopduration": 20 + }, + { + "method": "MaxDrawdown", + "lookback_period": 2000, + "trade_limit": 20, + "stop_duration": 10, + "max_allowed_drawdown": 0.2 + }, + { + "method": "LowProfitPairs", + "lookback_period": 360, + "trade_limit": 1, + "stop_duration": 2, + "required_profit": 0.02 } ], "exchange": { diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 716addb55..526c4d0a3 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -15,6 +15,7 @@ All protection end times are rounded up to the next candle to avoid sudden, unex ### Available Protection Handlers * [`StoplossGuard`](#stoploss-guard) Stop trading if a certain amount of stoploss occurred within a certain time window. +* [`MaxDrawdown`](#maxdrawdown) Stop trading if max-drawdown is reached. * [`LowProfitPairs`](#low-profit-pairs) Lock pairs with low profits * [`CooldownPeriod`](#cooldown-period) Don't enter a trade right after selling a trade. @@ -23,33 +24,56 @@ All protection end times are rounded up to the next candle to avoid sudden, unex `StoplossGuard` selects all trades within a `lookback_period` (in minutes), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration`. This applies across all pairs, unless `only_per_pair` is set to true, which will then only look at one pair at a time. +The below example stops trading for all pairs for 2 hours (120min) after the last trade if the bot hit stoploss 4 times within the last 24h. + ```json "protections": [ { "method": "StoplossGuard", - "lookback_period": 60, + "lookback_period": 1440, "trade_limit": 4, - "stop_duration": 60, + "stop_duration": 120, "only_per_pair": false } ], ``` !!! Note - `StoplossGuard` considers all trades with the results `"stop_loss"` and `"trailing_stop_loss"` if the result was negative. + `StoplossGuard` considers all trades with the results `"stop_loss"` and `"trailing_stop_loss"` if the resulting profit was negative. `trade_limit` and `lookback_period` will need to be tuned for your strategy. +#### MaxDrawdown + +`MaxDrawdown` uses all trades within `lookback_period` (in minutes) to determine the maximum drawdown. If the drawdown is below `max_allowed_drawdown`, trading will stop for `stop_duration` (in minutes) after the last trade - assuming that the bot needs some time to let markets recover. + +The below sample stops trading for 12 hours (720min) if max-drawdown is > 20% considering all trades within the last 2 days (2880min). + +```json +"protections": [ + { + "method": "MaxDrawdown", + "lookback_period": 2880, + "trade_limit": 20, + "stop_duration": 720, + "max_allowed_drawdown": 0.2 + }, +], + +``` + #### Low Profit Pairs `LowProfitPairs` uses all trades for a pair within a `lookback_period` (in minutes) to determine the overall profit ratio. If that ratio is below `required_profit`, that pair will be locked for `stop_duration` (in minutes). +The below example will stop trading a pair for 60 minutes if the pair does not have a required profit of 2% (and a minimum of 2 trades) within the last 6 hours (360min). + ```json "protections": [ { "method": "LowProfitPairs", - "lookback_period": 60, - "trade_limit": 4, + "lookback_period": 360, + "trade_limit": 2, "stop_duration": 60, "required_profit": 0.02 } @@ -79,10 +103,11 @@ All protections are evaluated in the sequence they are defined. The below example: -* stops trading if more than 4 stoploss occur for all pairs within a 1 hour (60 minute) limit (`StoplossGuard`). * Locks each pair after selling for an additional 10 minutes (`CooldownPeriod`), giving other pairs a chance to get filled. -* Locks all pairs that had 4 Trades within the last 6 hours with a combined profit ratio of below 0.02 (<2%). (`LowProfitPairs`) -* Locks all pairs for 120 minutes that had a profit of below 0.01 (<1%) within the last 24h (`60 * 24 = 1440`), a minimum of 7 trades +* Stops trading if the last 2 days had 20 trades, which caused a max-drawdown of more than 20%. (`MaxDrawdown`). +* Stops trading if more than 4 stoploss occur for all pairs within a 1 day (1440min) limit (`StoplossGuard`). +* Locks all pairs that had 4 Trades within the last 6 hours (`60 * 6 = 360`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`). +* Locks all pairs for 120 minutes that had a profit of below 0.01 (<1%) within the last 24h (`60 * 24 = 1440`), a minimum of 4 trades. ```json "protections": [ @@ -90,23 +115,31 @@ The below example: "method": "CooldownPeriod", "stop_duration": 10 }, + { + "method": "MaxDrawdown", + "lookback_period": 2880, + "trade_limit": 20, + "stop_duration": 720, + "max_allowed_drawdown": 0.2 + }, { "method": "StoplossGuard", - "lookback_period": 60, + "lookback_period": 1440, "trade_limit": 4, - "stop_duration": 60 + "stop_duration": 120, + "only_per_pair": false }, { "method": "LowProfitPairs", "lookback_period": 360, - "trade_limit": 4, + "trade_limit": 2, "stop_duration": 60, "required_profit": 0.02 }, - { + { "method": "LowProfitPairs", "lookback_period": 1440, - "trade_limit": 7, + "trade_limit": 4, "stop_duration": 120, "required_profit": 0.01 } From f13e9ce5edb993b17e964d4ae3bae87baae97b68 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 08:22:12 +0100 Subject: [PATCH 1071/1197] Improve docs --- docs/includes/protections.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 526c4d0a3..f5639565f 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -12,16 +12,21 @@ All protection end times are rounded up to the next candle to avoid sudden, unex !!! Tip Each Protection can be configured multiple times with different parameters, to allow different levels of protection (short-term / long-term). -### Available Protection Handlers +### Available Protections * [`StoplossGuard`](#stoploss-guard) Stop trading if a certain amount of stoploss occurred within a certain time window. * [`MaxDrawdown`](#maxdrawdown) Stop trading if max-drawdown is reached. * [`LowProfitPairs`](#low-profit-pairs) Lock pairs with low profits * [`CooldownPeriod`](#cooldown-period) Don't enter a trade right after selling a trade. +### Common settings to all Protections + +* `stop_duration` (minutes) - how long should protections be locked. +* `lookback_period` (minutes) - Only trades that completed after `current_time - lookback_period` will be considered (may be ignored by some Protections). + #### Stoploss Guard -`StoplossGuard` selects all trades within a `lookback_period` (in minutes), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration`. +`StoplossGuard` selects all trades within `lookback_period` (in minutes), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration`. This applies across all pairs, unless `only_per_pair` is set to true, which will then only look at one pair at a time. The below example stops trading for all pairs for 2 hours (120min) after the last trade if the bot hit stoploss 4 times within the last 24h. @@ -63,7 +68,7 @@ The below sample stops trading for 12 hours (720min) if max-drawdown is > 20% co #### Low Profit Pairs -`LowProfitPairs` uses all trades for a pair within a `lookback_period` (in minutes) to determine the overall profit ratio. +`LowProfitPairs` uses all trades for a pair within `lookback_period` (in minutes) to determine the overall profit ratio. If that ratio is below `required_profit`, that pair will be locked for `stop_duration` (in minutes). The below example will stop trading a pair for 60 minutes if the pair does not have a required profit of 2% (and a minimum of 2 trades) within the last 6 hours (360min). @@ -93,8 +98,9 @@ The below example will stop trading a pair for 60 minutes if the pair does not h ], ``` -!!! Note: +!!! Note This Protection applies only at pair-level, and will never lock all pairs globally. + This Protection does not consider `lookback_period` as it only looks at the latest trade. ### Full example of Protections From eb952d77be2c9eaff24cab9d42a5111d28fefd13 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 08:27:14 +0100 Subject: [PATCH 1072/1197] Move lookback_period to parent __init__ --- docs/includes/protections.md | 2 ++ freqtrade/plugins/protections/iprotection.py | 1 + freqtrade/plugins/protections/low_profit_pairs.py | 1 - freqtrade/plugins/protections/max_drawdown_protection.py | 1 - freqtrade/plugins/protections/stoploss_guard.py | 1 - 5 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index f5639565f..25d59a992 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -21,8 +21,10 @@ All protection end times are rounded up to the next candle to avoid sudden, unex ### Common settings to all Protections +* `method` - Protection name to use. * `stop_duration` (minutes) - how long should protections be locked. * `lookback_period` (minutes) - Only trades that completed after `current_time - lookback_period` will be considered (may be ignored by some Protections). +* `trade_limit` - How many trades are required at minimum (not used by all Protections). #### Stoploss Guard diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 2053ae741..60f83eea6 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -24,6 +24,7 @@ class IProtection(LoggingMixin, ABC): self._config = config self._protection_config = protection_config self._stop_duration = protection_config.get('stop_duration', 60) + self._lookback_period = protection_config.get('lookback_period', 60) LoggingMixin.__init__(self, logger) diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index 515f81521..70ef5b080 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -18,7 +18,6 @@ class LowProfitPairs(IProtection): def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: super().__init__(config, protection_config) - self._lookback_period = protection_config.get('lookback_period', 60) self._trade_limit = protection_config.get('trade_limit', 1) self._required_profit = protection_config.get('required_profit', 0.0) diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index e5625733c..2a83cdeba 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -21,7 +21,6 @@ class MaxDrawdown(IProtection): def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: super().__init__(config, protection_config) - self._lookback_period = protection_config.get('lookback_period', 60) self._trade_limit = protection_config.get('trade_limit', 1) self._max_allowed_drawdown = protection_config.get('max_allowed_drawdown', 0.0) # TODO: Implement checks to limit max_drawdown to sensible values diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index b6f430085..520607337 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -21,7 +21,6 @@ class StoplossGuard(IProtection): def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: super().__init__(config, protection_config) - self._lookback_period = protection_config.get('lookback_period', 60) self._trade_limit = protection_config.get('trade_limit', 10) self._disable_global_stop = protection_config.get('only_per_pair', False) From a93bb6853bbd6359deaa733adaa0491de5546fd9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 10:21:03 +0100 Subject: [PATCH 1073/1197] Document *candles settings, implement validations --- docs/includes/protections.md | 12 ++++++++---- freqtrade/configuration/config_validation.py | 20 ++++++++++++++++++++ freqtrade/constants.py | 2 ++ tests/plugins/test_protections.py | 15 ++++++++------- tests/test_configuration.py | 18 ++++++++++++++++++ 5 files changed, 56 insertions(+), 11 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 25d59a992..2f704d83f 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -21,10 +21,14 @@ All protection end times are rounded up to the next candle to avoid sudden, unex ### Common settings to all Protections -* `method` - Protection name to use. -* `stop_duration` (minutes) - how long should protections be locked. -* `lookback_period` (minutes) - Only trades that completed after `current_time - lookback_period` will be considered (may be ignored by some Protections). -* `trade_limit` - How many trades are required at minimum (not used by all Protections). +| Parameter| Description | +|------------|-------------| +| method | Protection name to use.
    **Datatype:** String, selected from [available Protections](#available-protections) +| stop_duration_candles | For how many candles should the lock be set?
    **Datatype:** Positive integer (in candles) +| stop_duration | how many minutes should protections be locked.
    Cannot be used together with `stop_duration_candles`.
    **Datatype:** Float (in minutes) +| `lookback_period_candles` | Only trades that completed within the last `lookback_period_candles` candles will be considered. This setting may be ignored by some Protections.
    **Datatype:** Positive integer (in candles). +| lookback_period | Only trades that completed after `current_time - lookback_period` will be considered.
    Cannot be used together with `lookback_period_candles`.
    This setting may be ignored by some Protections.
    **Datatype:** Float (in minutes) +| trade_limit | Number of trades required at minimum (not used by all Protections).
    **Datatype:** Positive integer #### Stoploss Guard diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index ab21bc686..a6435d0e6 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -74,6 +74,7 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None: _validate_trailing_stoploss(conf) _validate_edge(conf) _validate_whitelist(conf) + _validate_protections(conf) _validate_unlimited_amount(conf) # validate configuration before returning @@ -155,3 +156,22 @@ def _validate_whitelist(conf: Dict[str, Any]) -> None: if (pl.get('method') == 'StaticPairList' and not conf.get('exchange', {}).get('pair_whitelist')): raise OperationalException("StaticPairList requires pair_whitelist to be set.") + + +def _validate_protections(conf: Dict[str, Any]) -> None: + """ + Validate protection configuration validity + """ + + for prot in conf.get('protections', []): + if ('stop_duration' in prot and 'stop_duration_candles' in prot): + raise OperationalException( + "Protections must specify either `stop_duration` or `stop_duration_candles`.\n" + f"Please fix the protection {prot.get('method')}" + ) + + if ('lookback_period' in prot and 'lookback_period_candle' in prot): + raise OperationalException( + "Protections must specify either `lookback_period` or `lookback_period_candles`.\n" + f"Please fix the protection {prot.get('method')}" + ) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index dfc21b678..e7d7e80f6 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -204,8 +204,10 @@ CONF_SCHEMA = { 'properties': { 'method': {'type': 'string', 'enum': AVAILABLE_PROTECTIONS}, 'stop_duration': {'type': 'number', 'minimum': 0.0}, + 'stop_duration_candles': {'type': 'number', 'minimum': 0}, 'trade_limit': {'type': 'number', 'minimum': 1}, 'lookback_period': {'type': 'number', 'minimum': 1}, + 'lookback_period_candles': {'type': 'number', 'minimum': 1}, }, 'required': ['method'], } diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index e5bbec431..29ff4e069 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -103,6 +103,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair "method": "StoplossGuard", "lookback_period": 60, "trade_limit": 1, + "stop_duration": 60, "only_per_pair": only_per_pair }] freqtrade = get_patched_freqtradebot(mocker, default_conf) @@ -158,7 +159,7 @@ def test_stoploss_guard_perpair(mocker, default_conf, fee, caplog, only_per_pair def test_CooldownPeriod(mocker, default_conf, fee, caplog): default_conf['protections'] = [{ "method": "CooldownPeriod", - "stopduration": 60, + "stop_duration": 60, }] freqtrade = get_patched_freqtradebot(mocker, default_conf) message = r"Trading stopped due to .*" @@ -195,7 +196,7 @@ def test_LowProfitPairs(mocker, default_conf, fee, caplog): default_conf['protections'] = [{ "method": "LowProfitPairs", "lookback_period": 400, - "stopduration": 60, + "stop_duration": 60, "trade_limit": 2, "required_profit": 0.0, }] @@ -254,7 +255,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): default_conf['protections'] = [{ "method": "MaxDrawdown", "lookback_period": 1000, - "stopduration": 60, + "stop_duration": 60, "trade_limit": 3, "max_allowed_drawdown": 0.15 }] @@ -315,21 +316,21 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): @pytest.mark.parametrize("protectionconf,desc_expected,exception_expected", [ - ({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2}, + ({"method": "StoplossGuard", "lookback_period": 60, "trade_limit": 2, "stop_duration": 60}, "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " "2 stoplosses within 60 minutes.'}]", None ), - ({"method": "CooldownPeriod", "stopduration": 60}, + ({"method": "CooldownPeriod", "stop_duration": 60}, "[{'CooldownPeriod': 'CooldownPeriod - Cooldown period of 60 min.'}]", None ), - ({"method": "LowProfitPairs", "stopduration": 60}, + ({"method": "LowProfitPairs", "lookback_period": 60, "stop_duration": 60}, "[{'LowProfitPairs': 'LowProfitPairs - Low Profit Protection, locks pairs with " "profit < 0.0 within 60 minutes.'}]", None ), - ({"method": "MaxDrawdown", "stopduration": 60}, + ({"method": "MaxDrawdown", "lookback_period": 60, "stop_duration": 60}, "[{'MaxDrawdown': 'MaxDrawdown - Max drawdown protection, stop trading if drawdown is > 0.0 " "within 60 minutes.'}]", None diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 167215f29..283f6a0f9 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -879,6 +879,24 @@ def test_validate_whitelist(default_conf): validate_config_consistency(conf) +@pytest.mark.parametrize('protconf,expected', [ + ([], None), + ([{"method": "StoplossGuard", "lookback_period": 2000, "stop_duration_candles": 10}], None), + ([{"method": "StoplossGuard", "lookback_period_candle": 20, "stop_duration": 10}], None), + ([{"method": "StoplossGuard", "lookback_period_candle": 20, "lookback_period": 2000, + "stop_duration": 10}], r'Protections must specify either `lookback_period`.*'), + ([{"method": "StoplossGuard", "lookback_period": 20, "stop_duration": 10, + "stop_duration_candles": 10}], r'Protections must specify either `stop_duration`.*'), +]) +def test_validate_protections(default_conf, protconf, expected): + conf = deepcopy(default_conf) + conf['protections'] = protconf + if expected: + with pytest.raises(OperationalException, match=expected): + validate_config_consistency(conf) + else: + validate_config_consistency(conf) + def test_load_config_test_comments() -> None: """ From d4799e6aa3897db275920b48b615e7f6733c32ef Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 10:45:35 +0100 Subject: [PATCH 1074/1197] Implement *candle definitions --- docs/includes/protections.md | 57 +++++++++++--------- freqtrade/configuration/config_validation.py | 2 +- freqtrade/plugins/protections/iprotection.py | 12 ++++- tests/plugins/test_protections.py | 32 ++++++++++- tests/test_configuration.py | 5 +- 5 files changed, 76 insertions(+), 32 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 2f704d83f..210765176 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -30,20 +30,24 @@ All protection end times are rounded up to the next candle to avoid sudden, unex | lookback_period | Only trades that completed after `current_time - lookback_period` will be considered.
    Cannot be used together with `lookback_period_candles`.
    This setting may be ignored by some Protections.
    **Datatype:** Float (in minutes) | trade_limit | Number of trades required at minimum (not used by all Protections).
    **Datatype:** Positive integer +!!! Note "Durations" + Durations (`stop_duration*` and `lookback_period*` can be defined in either minutes or candles). + For more flexibility when testing different timeframes, all below examples will use the "candle" definition. + #### Stoploss Guard -`StoplossGuard` selects all trades within `lookback_period` (in minutes), and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration`. +`StoplossGuard` selects all trades within `lookback_period`, and determines if the amount of trades that resulted in stoploss are above `trade_limit` - in which case trading will stop for `stop_duration`. This applies across all pairs, unless `only_per_pair` is set to true, which will then only look at one pair at a time. -The below example stops trading for all pairs for 2 hours (120min) after the last trade if the bot hit stoploss 4 times within the last 24h. +The below example stops trading for all pairs for 4 candles after the last trade if the bot hit stoploss 4 times within the last 24 candles. ```json "protections": [ { "method": "StoplossGuard", - "lookback_period": 1440, + "lookback_period_candles": 24, "trade_limit": 4, - "stop_duration": 120, + "stop_duration_candles": 4, "only_per_pair": false } ], @@ -57,15 +61,15 @@ The below example stops trading for all pairs for 2 hours (120min) after the las `MaxDrawdown` uses all trades within `lookback_period` (in minutes) to determine the maximum drawdown. If the drawdown is below `max_allowed_drawdown`, trading will stop for `stop_duration` (in minutes) after the last trade - assuming that the bot needs some time to let markets recover. -The below sample stops trading for 12 hours (720min) if max-drawdown is > 20% considering all trades within the last 2 days (2880min). +The below sample stops trading for 12 candles if max-drawdown is > 20% considering all trades within the last 48 candles. ```json "protections": [ { "method": "MaxDrawdown", - "lookback_period": 2880, + "lookback_period_candles": 48, "trade_limit": 20, - "stop_duration": 720, + "stop_duration_candles": 12, "max_allowed_drawdown": 0.2 }, ], @@ -77,13 +81,13 @@ The below sample stops trading for 12 hours (720min) if max-drawdown is > 20% co `LowProfitPairs` uses all trades for a pair within `lookback_period` (in minutes) to determine the overall profit ratio. If that ratio is below `required_profit`, that pair will be locked for `stop_duration` (in minutes). -The below example will stop trading a pair for 60 minutes if the pair does not have a required profit of 2% (and a minimum of 2 trades) within the last 6 hours (360min). +The below example will stop trading a pair for 60 minutes if the pair does not have a required profit of 2% (and a minimum of 2 trades) within the last 6 candles. ```json "protections": [ { "method": "LowProfitPairs", - "lookback_period": 360, + "lookback_period_candles": 6, "trade_limit": 2, "stop_duration": 60, "required_profit": 0.02 @@ -95,11 +99,13 @@ The below example will stop trading a pair for 60 minutes if the pair does not h `CooldownPeriod` locks a pair for `stop_duration` (in minutes) after selling, avoiding a re-entry for this pair for `stop_duration` minutes. +The below example will stop trading a pair for 2 candles after closing a trade, allowing this pair to "cool down". + ```json "protections": [ { "method": "CooldownPeriod", - "stop_duration": 60 + "stop_duration_candle": 2 } ], ``` @@ -113,46 +119,47 @@ The below example will stop trading a pair for 60 minutes if the pair does not h All protections can be combined at will, also with different parameters, creating a increasing wall for under-performing pairs. All protections are evaluated in the sequence they are defined. -The below example: +The below example assumes a timeframe of 1 hour: -* Locks each pair after selling for an additional 10 minutes (`CooldownPeriod`), giving other pairs a chance to get filled. -* Stops trading if the last 2 days had 20 trades, which caused a max-drawdown of more than 20%. (`MaxDrawdown`). -* Stops trading if more than 4 stoploss occur for all pairs within a 1 day (1440min) limit (`StoplossGuard`). -* Locks all pairs that had 4 Trades within the last 6 hours (`60 * 6 = 360`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`). -* Locks all pairs for 120 minutes that had a profit of below 0.01 (<1%) within the last 24h (`60 * 24 = 1440`), a minimum of 4 trades. +* Locks each pair after selling for an additional 5 candles (`CooldownPeriod`), giving other pairs a chance to get filled. +* Stops trading for 4 hours (`4 * 1h candles`) if the last 2 days (`48 * 1h candles`) had 20 trades, which caused a max-drawdown of more than 20%. (`MaxDrawdown`). +* Stops trading if more than 4 stoploss occur for all pairs within a 1 day (`24 * 1h candles`) limit (`StoplossGuard`). +* Locks all pairs that had 4 Trades within the last 6 hours (`6 * 1h candles`) with a combined profit ratio of below 0.02 (<2%) (`LowProfitPairs`). +* Locks all pairs for 2 candles that had a profit of below 0.01 (<1%) within the last 24h (`24 * 1h candles`), a minimum of 4 trades. ```json +"timeframe": "1h", "protections": [ { "method": "CooldownPeriod", - "stop_duration": 10 + "stop_duration_candles": 5 }, { "method": "MaxDrawdown", - "lookback_period": 2880, + "lookback_period_candles": 48, "trade_limit": 20, - "stop_duration": 720, + "stop_duration_candles": 4, "max_allowed_drawdown": 0.2 }, { "method": "StoplossGuard", - "lookback_period": 1440, + "lookback_period_candles": 24, "trade_limit": 4, - "stop_duration": 120, + "stop_duration_candles": 2, "only_per_pair": false }, { "method": "LowProfitPairs", - "lookback_period": 360, + "lookback_period_candles": 6, "trade_limit": 2, - "stop_duration": 60, + "stop_duration_candles": 60, "required_profit": 0.02 }, { "method": "LowProfitPairs", - "lookback_period": 1440, + "lookback_period_candles": 24, "trade_limit": 4, - "stop_duration": 120, + "stop_duration_candles": 2, "required_profit": 0.01 } ], diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index a6435d0e6..b8829b80f 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -170,7 +170,7 @@ def _validate_protections(conf: Dict[str, Any]) -> None: f"Please fix the protection {prot.get('method')}" ) - if ('lookback_period' in prot and 'lookback_period_candle' in prot): + if ('lookback_period' in prot and 'lookback_period_candles' in prot): raise OperationalException( "Protections must specify either `lookback_period` or `lookback_period_candles`.\n" f"Please fix the protection {prot.get('method')}" diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 60f83eea6..7a5a87f47 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -4,6 +4,7 @@ from abc import ABC, abstractmethod from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, Optional, Tuple +from freqtrade.exchange import timeframe_to_minutes from freqtrade.mixins import LoggingMixin from freqtrade.persistence import Trade @@ -23,8 +24,15 @@ class IProtection(LoggingMixin, ABC): def __init__(self, config: Dict[str, Any], protection_config: Dict[str, Any]) -> None: self._config = config self._protection_config = protection_config - self._stop_duration = protection_config.get('stop_duration', 60) - self._lookback_period = protection_config.get('lookback_period', 60) + tf_in_min = timeframe_to_minutes(config['timeframe']) + if 'stop_duration_candles' in protection_config: + self._stop_duration = (tf_in_min * protection_config.get('stop_duration_candles')) + else: + self._stop_duration = protection_config.get('stop_duration', 60) + if 'lookback_period_candles' in protection_config: + self._lookback_period = tf_in_min * protection_config.get('lookback_period_candles', 60) + else: + self._lookback_period = protection_config.get('lookback_period', 60) LoggingMixin.__init__(self, logger) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 29ff4e069..819ae805e 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -3,9 +3,10 @@ from datetime import datetime, timedelta import pytest -from freqtrade.persistence import PairLocks, Trade -from freqtrade.strategy.interface import SellType from freqtrade import constants +from freqtrade.persistence import PairLocks, Trade +from freqtrade.plugins.protectionmanager import ProtectionManager +from freqtrade.strategy.interface import SellType from tests.conftest import get_patched_freqtradebot, log_has_re @@ -49,6 +50,33 @@ def test_protectionmanager(mocker, default_conf): assert handler.stop_per_pair('XRP/BTC', datetime.utcnow()) == (False, None, None) +@pytest.mark.parametrize('timeframe,expected,protconf', [ + ('1m', [20, 10], + [{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 10}]), + ('5m', [100, 15], + [{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 15}]), + ('1h', [1200, 40], + [{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 40}]), + ('1d', [1440, 5], + [{"method": "StoplossGuard", "lookback_period_candles": 1, "stop_duration": 5}]), + ('1m', [20, 5], + [{"method": "StoplossGuard", "lookback_period": 20, "stop_duration_candles": 5}]), + ('5m', [15, 25], + [{"method": "StoplossGuard", "lookback_period": 15, "stop_duration_candles": 5}]), + ('1h', [50, 600], + [{"method": "StoplossGuard", "lookback_period": 50, "stop_duration_candles": 10}]), + ('1h', [60, 540], + [{"method": "StoplossGuard", "lookback_period_candles": 1, "stop_duration_candles": 9}]), +]) +def test_protections_init(mocker, default_conf, timeframe, expected, protconf): + default_conf['timeframe'] = timeframe + default_conf['protections'] = protconf + man = ProtectionManager(default_conf) + assert len(man._protection_handlers) == len(protconf) + assert man._protection_handlers[0]._lookback_period == expected[0] + assert man._protection_handlers[0]._stop_duration == expected[1] + + @pytest.mark.usefixtures("init_persistence") def test_stoploss_guard(mocker, default_conf, fee, caplog): default_conf['protections'] = [{ diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 283f6a0f9..bebbc1508 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -879,11 +879,12 @@ def test_validate_whitelist(default_conf): validate_config_consistency(conf) + @pytest.mark.parametrize('protconf,expected', [ ([], None), ([{"method": "StoplossGuard", "lookback_period": 2000, "stop_duration_candles": 10}], None), - ([{"method": "StoplossGuard", "lookback_period_candle": 20, "stop_duration": 10}], None), - ([{"method": "StoplossGuard", "lookback_period_candle": 20, "lookback_period": 2000, + ([{"method": "StoplossGuard", "lookback_period_candles": 20, "stop_duration": 10}], None), + ([{"method": "StoplossGuard", "lookback_period_candles": 20, "lookback_period": 2000, "stop_duration": 10}], r'Protections must specify either `lookback_period`.*'), ([{"method": "StoplossGuard", "lookback_period": 20, "stop_duration": 10, "stop_duration_candles": 10}], r'Protections must specify either `stop_duration`.*'), From c993831a04c8a92af179eb35a7bd6983458d0b56 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 10:54:37 +0100 Subject: [PATCH 1075/1197] Add protections to startup messages --- freqtrade/freqtradebot.py | 2 +- freqtrade/rpc/rpc_manager.py | 7 ++++++- tests/rpc/test_rpc_manager.py | 10 +++++++--- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ecc824a86..9fc342056 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -137,7 +137,7 @@ class FreqtradeBot(LoggingMixin): Called on startup and after reloading the bot - triggers notifications and performs startup tasks """ - self.rpc.startup_messages(self.config, self.pairlists) + self.rpc.startup_messages(self.config, self.pairlists, self.protections) if not self.edge: # Adjust stoploss if it was changed Trade.stoploss_reinitialization(self.strategy.stoploss) diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index b97a5357b..ab5e09ddd 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -62,7 +62,7 @@ class RPCManager: except NotImplementedError: logger.error(f"Message type '{msg['type']}' not implemented by handler {mod.name}.") - def startup_messages(self, config: Dict[str, Any], pairlist) -> None: + def startup_messages(self, config: Dict[str, Any], pairlist, protections) -> None: if config['dry_run']: self.send_msg({ 'type': RPCMessageType.WARNING_NOTIFICATION, @@ -90,3 +90,8 @@ class RPCManager: 'status': f'Searching for {stake_currency} pairs to buy and sell ' f'based on {pairlist.short_desc()}' }) + if len(protections.name_list) > 0: + self.send_msg({ + 'type': RPCMessageType.STARTUP_NOTIFICATION, + 'status': f'Using Protections {protections.short_desc()}' + }) diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 4b715fc37..06706120f 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -137,7 +137,7 @@ def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None: freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) - rpc_manager.startup_messages(default_conf, freqtradebot.pairlists) + rpc_manager.startup_messages(default_conf, freqtradebot.pairlists, freqtradebot.protections) assert telegram_mock.call_count == 3 assert "*Exchange:* `bittrex`" in telegram_mock.call_args_list[1][0][0]['status'] @@ -147,10 +147,14 @@ def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None: default_conf['whitelist'] = {'method': 'VolumePairList', 'config': {'number_assets': 20} } + default_conf['protections'] = [{"method": "StoplossGuard", + "lookback_period": 60, "trade_limit": 2, "stop_duration": 60}] + freqtradebot = get_patched_freqtradebot(mocker, default_conf) - rpc_manager.startup_messages(default_conf, freqtradebot.pairlists) - assert telegram_mock.call_count == 3 + rpc_manager.startup_messages(default_conf, freqtradebot.pairlists, freqtradebot.protections) + assert telegram_mock.call_count == 4 assert "Dry run is enabled." in telegram_mock.call_args_list[0][0][0]['status'] + assert 'StoplossGuard' in telegram_mock.call_args_list[-1][0][0]['status'] def test_init_apiserver_disabled(mocker, default_conf, caplog) -> None: From 0e2a43ab4dbd49e74f1aaca0da2d19dd41aa1dd9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 11:08:54 +0100 Subject: [PATCH 1076/1197] Add duration_explanation functions --- .../plugins/protections/cooldown_period.py | 6 ++-- freqtrade/plugins/protections/iprotection.py | 33 +++++++++++++++++-- .../plugins/protections/low_profit_pairs.py | 6 ++-- .../protections/max_drawdown_protection.py | 8 ++--- .../plugins/protections/stoploss_guard.py | 2 +- tests/plugins/test_protections.py | 22 ++++++++++++- 6 files changed, 63 insertions(+), 14 deletions(-) diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index 7b37b2303..e5eae01dd 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -22,13 +22,13 @@ class CooldownPeriod(IProtection): """ LockReason to use """ - return (f'Cooldown period for {self._stop_duration} min.') + return (f'Cooldown period for {self.stop_duration_str}.') def short_desc(self) -> str: """ Short method description - used for startup-messages """ - return (f"{self.name} - Cooldown period of {self._stop_duration} min.") + return (f"{self.name} - Cooldown period of {self.stop_duration_str}.") def _cooldown_period(self, pair: str, date_now: datetime, ) -> ProtectionReturn: """ @@ -42,7 +42,7 @@ class CooldownPeriod(IProtection): ] trade = Trade.get_trades(filters).first() if trade: - self.log_once(f"Cooldown for {pair} for {self._stop_duration}.", logger.info) + self.log_once(f"Cooldown for {pair} for {self.stop_duration_str}.", logger.info) until = self.calculate_lock_end([trade], self._stop_duration) return True, until, self._reason() diff --git a/freqtrade/plugins/protections/iprotection.py b/freqtrade/plugins/protections/iprotection.py index 7a5a87f47..684bf6cd3 100644 --- a/freqtrade/plugins/protections/iprotection.py +++ b/freqtrade/plugins/protections/iprotection.py @@ -5,6 +5,7 @@ from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, Optional, Tuple from freqtrade.exchange import timeframe_to_minutes +from freqtrade.misc import plural from freqtrade.mixins import LoggingMixin from freqtrade.persistence import Trade @@ -26,12 +27,16 @@ class IProtection(LoggingMixin, ABC): self._protection_config = protection_config tf_in_min = timeframe_to_minutes(config['timeframe']) if 'stop_duration_candles' in protection_config: - self._stop_duration = (tf_in_min * protection_config.get('stop_duration_candles')) + self._stop_duration_candles = protection_config.get('stop_duration_candles', 1) + self._stop_duration = (tf_in_min * self._stop_duration_candles) else: + self._stop_duration_candles = None self._stop_duration = protection_config.get('stop_duration', 60) if 'lookback_period_candles' in protection_config: - self._lookback_period = tf_in_min * protection_config.get('lookback_period_candles', 60) + self._lookback_period_candles = protection_config.get('lookback_period_candles', 1) + self._lookback_period = tf_in_min * self._lookback_period_candles else: + self._lookback_period_candles = None self._lookback_period = protection_config.get('lookback_period', 60) LoggingMixin.__init__(self, logger) @@ -40,6 +45,30 @@ class IProtection(LoggingMixin, ABC): def name(self) -> str: return self.__class__.__name__ + @property + def stop_duration_str(self) -> str: + """ + Output configured stop duration in either candles or minutes + """ + if self._stop_duration_candles: + return (f"{self._stop_duration_candles} " + f"{plural(self._stop_duration_candles, 'candle', 'candles')}") + else: + return (f"{self._stop_duration} " + f"{plural(self._stop_duration, 'minute', 'minutes')}") + + @property + def lookback_period_str(self) -> str: + """ + Output configured lookback period in either candles or minutes + """ + if self._lookback_period_candles: + return (f"{self._lookback_period_candles} " + f"{plural(self._lookback_period_candles, 'candle', 'candles')}") + else: + return (f"{self._lookback_period} " + f"{plural(self._lookback_period, 'minute', 'minutes')}") + @abstractmethod def short_desc(self) -> str: """ diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index 70ef5b080..4721ea1a2 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -26,14 +26,14 @@ class LowProfitPairs(IProtection): Short method description - used for startup-messages """ return (f"{self.name} - Low Profit Protection, locks pairs with " - f"profit < {self._required_profit} within {self._lookback_period} minutes.") + f"profit < {self._required_profit} within {self.lookback_period_str}.") def _reason(self, profit: float) -> str: """ LockReason to use """ - return (f'{profit} < {self._required_profit} in {self._lookback_period} min, ' - f'locking for {self._stop_duration} min.') + return (f'{profit} < {self._required_profit} in {self.lookback_period_str}, ' + f'locking for {self.stop_duration_str}.') def _low_profit(self, date_now: datetime, pair: str) -> ProtectionReturn: """ diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index 2a83cdeba..e0c91243b 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -30,14 +30,14 @@ class MaxDrawdown(IProtection): Short method description - used for startup-messages """ return (f"{self.name} - Max drawdown protection, stop trading if drawdown is > " - f"{self._max_allowed_drawdown} within {self._lookback_period} minutes.") + f"{self._max_allowed_drawdown} within {self.lookback_period_str}.") def _reason(self, drawdown: float) -> str: """ LockReason to use """ - return (f'{drawdown} > {self._max_allowed_drawdown} in {self._lookback_period} min, ' - f'locking for {self._stop_duration} min.') + return (f'{drawdown} > {self._max_allowed_drawdown} in {self.lookback_period_str}, ' + f'locking for {self.stop_duration_str}.') def _max_drawdown(self, date_now: datetime) -> ProtectionReturn: """ @@ -62,7 +62,7 @@ class MaxDrawdown(IProtection): if drawdown > self._max_allowed_drawdown: self.log_once( f"Trading stopped due to Max Drawdown {drawdown:.2f} < {self._max_allowed_drawdown}" - f" within {self._lookback_period} minutes.", logger.info) + f" within {self.lookback_period_str}.", logger.info) until = self.calculate_lock_end(trades, self._stop_duration) return True, until, self._reason(drawdown) diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 520607337..7a13ead57 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -29,7 +29,7 @@ class StoplossGuard(IProtection): Short method description - used for startup-messages """ return (f"{self.name} - Frequent Stoploss Guard, {self._trade_limit} stoplosses " - f"within {self._lookback_period} minutes.") + f"within {self.lookback_period_str}.") def _reason(self) -> str: """ diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 819ae805e..22fe33e19 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -350,7 +350,7 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): None ), ({"method": "CooldownPeriod", "stop_duration": 60}, - "[{'CooldownPeriod': 'CooldownPeriod - Cooldown period of 60 min.'}]", + "[{'CooldownPeriod': 'CooldownPeriod - Cooldown period of 60 minutes.'}]", None ), ({"method": "LowProfitPairs", "lookback_period": 60, "stop_duration": 60}, @@ -363,6 +363,26 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): "within 60 minutes.'}]", None ), + ({"method": "StoplossGuard", "lookback_period_candles": 12, "trade_limit": 2, + "stop_duration": 60}, + "[{'StoplossGuard': 'StoplossGuard - Frequent Stoploss Guard, " + "2 stoplosses within 12 candles.'}]", + None + ), + ({"method": "CooldownPeriod", "stop_duration_candles": 5}, + "[{'CooldownPeriod': 'CooldownPeriod - Cooldown period of 5 candles.'}]", + None + ), + ({"method": "LowProfitPairs", "lookback_period_candles": 11, "stop_duration": 60}, + "[{'LowProfitPairs': 'LowProfitPairs - Low Profit Protection, locks pairs with " + "profit < 0.0 within 11 candles.'}]", + None + ), + ({"method": "MaxDrawdown", "lookback_period_candles": 20, "stop_duration": 60}, + "[{'MaxDrawdown': 'MaxDrawdown - Max drawdown protection, stop trading if drawdown is > 0.0 " + "within 20 candles.'}]", + None + ), ]) def test_protection_manager_desc(mocker, default_conf, protectionconf, desc_expected, exception_expected): From 64d6c7bb651765ff5ab1071879f5197e7fbba025 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 11:17:11 +0100 Subject: [PATCH 1077/1197] Update developer docs --- docs/developer.md | 6 ++++-- freqtrade/plugins/protectionmanager.py | 3 --- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index 05b518184..f1d658ab8 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -202,10 +202,10 @@ For that reason, they must implement the following methods: The `until` portion should be calculated using the provided `calculate_lock_end()` method. -All Protections should use `"stop_duration"` to define how long a a pair (or all pairs) should be locked. +All Protections should use `"stop_duration"` / `"stop_duration_candles"` to define how long a a pair (or all pairs) should be locked. The content of this is made available as `self._stop_duration` to the each Protection. -If your protection requires a look-back period, please use `"lookback_period"` to keep different protections aligned. +If your protection requires a look-back period, please use `"lookback_period"` / `"lockback_period_candles"` to keep all protections aligned. #### Global vs. local stops @@ -230,6 +230,8 @@ The method `global_stop()` will be called whenever a trade closed (sell order co Protections should calculate the lock end time based on the last trade it considers. This avoids relocking should the lookback-period be longer than the actual lock period. +The `IProtection` parent class provides a helper method for this in `calculate_lock_end()`. + --- ## Implement a new Exchange (WIP) diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index d12f4ba80..03a09cc58 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -20,9 +20,6 @@ class ProtectionManager(): self._protection_handlers: List[IProtection] = [] for protection_handler_config in self._config.get('protections', []): - if 'method' not in protection_handler_config: - logger.warning(f"No method found in {protection_handler_config}, ignoring.") - continue protection_handler = ProtectionResolver.load_protection( protection_handler_config['method'], config=config, From 3426e99b8b3ad3f93eac53f474c84fc20c461c8e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 11:37:57 +0100 Subject: [PATCH 1078/1197] Improve formatting of protection startup message --- freqtrade/rpc/rpc_manager.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index ab5e09ddd..c42878f99 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -91,7 +91,8 @@ class RPCManager: f'based on {pairlist.short_desc()}' }) if len(protections.name_list) > 0: + prots = '\n'.join([p for prot in protections.short_desc() for k, p in prot.items()]) self.send_msg({ 'type': RPCMessageType.STARTUP_NOTIFICATION, - 'status': f'Using Protections {protections.short_desc()}' + 'status': f'Using Protections: \n{prots}' }) From 98c88fa58e890630a2c9221d48a31c42423a28ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 Nov 2020 20:09:34 +0100 Subject: [PATCH 1079/1197] Prepare protections for backtesting --- freqtrade/persistence/models.py | 41 +++++++++++++++++++ freqtrade/plugins/protectionmanager.py | 12 +++--- .../plugins/protections/cooldown_period.py | 17 ++++---- .../plugins/protections/low_profit_pairs.py | 16 ++++---- .../protections/max_drawdown_protection.py | 7 +--- .../plugins/protections/stoploss_guard.py | 25 ++++++----- 6 files changed, 84 insertions(+), 34 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 04d5a7695..d262a6784 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -202,6 +202,10 @@ class Trade(_DECL_BASE): """ __tablename__ = 'trades' + use_db: bool = True + # Trades container for backtesting + trades: List['Trade'] = [] + id = Column(Integer, primary_key=True) orders = relationship("Order", order_by="Order.id", cascade="all, delete-orphan") @@ -562,6 +566,43 @@ class Trade(_DECL_BASE): else: return Trade.query + @staticmethod + def get_trades_proxy(*, pair: str = None, is_open: bool = None, + open_date: datetime = None, close_date: datetime = None, + ) -> List['Trade']: + """ + Helper function to query Trades. + Returns a List of trades, filtered on the parameters given. + In live mode, converts the filter to a database query and returns all rows + In Backtest mode, uses filters on Trade.trades to get the result. + + :return: unsorted List[Trade] + """ + if Trade.use_db: + trade_filter = [] + if pair: + trade_filter.append(Trade.pair == pair) + if open_date: + trade_filter.append(Trade.open_date > open_date) + if close_date: + trade_filter.append(Trade.close_date > close_date) + if is_open is not None: + trade_filter.append(Trade.is_open.is_(is_open)) + return Trade.get_trades(trade_filter).all() + else: + # Offline mode - without database + sel_trades = [trade for trade in Trade.trades] + if pair: + sel_trades = [trade for trade in sel_trades if trade.pair == pair] + if open_date: + sel_trades = [trade for trade in sel_trades if trade.open_date > open_date] + if close_date: + sel_trades = [trade for trade in sel_trades if trade.close_date + and trade.close_date > close_date] + if is_open is not None: + sel_trades = [trade for trade in sel_trades if trade.is_open == is_open] + return sel_trades + @staticmethod def get_open_trades() -> List[Any]: """ diff --git a/freqtrade/plugins/protectionmanager.py b/freqtrade/plugins/protectionmanager.py index 03a09cc58..a8edd4e4b 100644 --- a/freqtrade/plugins/protectionmanager.py +++ b/freqtrade/plugins/protectionmanager.py @@ -3,7 +3,7 @@ Protection manager class """ import logging from datetime import datetime, timezone -from typing import Dict, List +from typing import Dict, List, Optional from freqtrade.persistence import PairLocks from freqtrade.plugins.protections import IProtection @@ -43,8 +43,9 @@ class ProtectionManager(): """ return [{p.name: p.short_desc()} for p in self._protection_handlers] - def global_stop(self) -> bool: - now = datetime.now(timezone.utc) + def global_stop(self, now: Optional[datetime] = None) -> bool: + if not now: + now = datetime.now(timezone.utc) result = False for protection_handler in self._protection_handlers: if protection_handler.has_global_stop: @@ -57,8 +58,9 @@ class ProtectionManager(): result = True return result - def stop_per_pair(self, pair) -> bool: - now = datetime.now(timezone.utc) + def stop_per_pair(self, pair, now: Optional[datetime] = None) -> bool: + if not now: + now = datetime.now(timezone.utc) result = False for protection_handler in self._protection_handlers: if protection_handler.has_local_stop: diff --git a/freqtrade/plugins/protections/cooldown_period.py b/freqtrade/plugins/protections/cooldown_period.py index e5eae01dd..2d7d7b4c7 100644 --- a/freqtrade/plugins/protections/cooldown_period.py +++ b/freqtrade/plugins/protections/cooldown_period.py @@ -35,13 +35,16 @@ class CooldownPeriod(IProtection): Get last trade for this pair """ look_back_until = date_now - timedelta(minutes=self._stop_duration) - filters = [ - Trade.is_open.is_(False), - Trade.close_date > look_back_until, - Trade.pair == pair, - ] - trade = Trade.get_trades(filters).first() - if trade: + # filters = [ + # Trade.is_open.is_(False), + # Trade.close_date > look_back_until, + # Trade.pair == pair, + # ] + # trade = Trade.get_trades(filters).first() + trades = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) + if trades: + # Get latest trade + trade = sorted(trades, key=lambda t: t.close_date)[-1] self.log_once(f"Cooldown for {pair} for {self.stop_duration_str}.", logger.info) until = self.calculate_lock_end([trade], self._stop_duration) diff --git a/freqtrade/plugins/protections/low_profit_pairs.py b/freqtrade/plugins/protections/low_profit_pairs.py index 4721ea1a2..9d5ed35b4 100644 --- a/freqtrade/plugins/protections/low_profit_pairs.py +++ b/freqtrade/plugins/protections/low_profit_pairs.py @@ -40,13 +40,15 @@ class LowProfitPairs(IProtection): Evaluate recent trades for pair """ look_back_until = date_now - timedelta(minutes=self._lookback_period) - filters = [ - Trade.is_open.is_(False), - Trade.close_date > look_back_until, - ] - if pair: - filters.append(Trade.pair == pair) - trades = Trade.get_trades(filters).all() + # filters = [ + # Trade.is_open.is_(False), + # Trade.close_date > look_back_until, + # ] + # if pair: + # filters.append(Trade.pair == pair) + + trades = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) + # trades = Trade.get_trades(filters).all() if len(trades) < self._trade_limit: # Not enough trades in the relevant period return False, None, None diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index e0c91243b..f1c77d1d9 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -44,11 +44,8 @@ class MaxDrawdown(IProtection): Evaluate recent trades for drawdown ... """ look_back_until = date_now - timedelta(minutes=self._lookback_period) - filters = [ - Trade.is_open.is_(False), - Trade.close_date > look_back_until, - ] - trades = Trade.get_trades(filters).all() + + trades = Trade.get_trades_proxy(is_open=False, close_date=look_back_until) trades_df = pd.DataFrame([trade.to_json() for trade in trades]) diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 7a13ead57..4dbc71048 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -43,16 +43,21 @@ class StoplossGuard(IProtection): Evaluate recent trades """ look_back_until = date_now - timedelta(minutes=self._lookback_period) - filters = [ - Trade.is_open.is_(False), - Trade.close_date > look_back_until, - or_(Trade.sell_reason == SellType.STOP_LOSS.value, - and_(Trade.sell_reason == SellType.TRAILING_STOP_LOSS.value, - Trade.close_profit < 0)) - ] - if pair: - filters.append(Trade.pair == pair) - trades = Trade.get_trades(filters).all() + # filters = [ + # Trade.is_open.is_(False), + # Trade.close_date > look_back_until, + # or_(Trade.sell_reason == SellType.STOP_LOSS.value, + # and_(Trade.sell_reason == SellType.TRAILING_STOP_LOSS.value, + # Trade.close_profit < 0)) + # ] + # if pair: + # filters.append(Trade.pair == pair) + # trades = Trade.get_trades(filters).all() + + trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) + trades = [trade for trade in trades1 if trade.sell_reason == SellType.STOP_LOSS + or (trade.sell_reason == SellType.TRAILING_STOP_LOSS + and trade.close_profit < 0)] if len(trades) > self._trade_limit: self.log_once(f"Trading stopped due to {self._trade_limit} " From b606936eb70131856a347d54540a784e23035085 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 Nov 2020 20:17:47 +0100 Subject: [PATCH 1080/1197] Make changes to backtesting to incorporate protections --- freqtrade/optimize/backtesting.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 883f7338c..1d183152c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -21,7 +21,8 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, store_backtest_stats) from freqtrade.pairlist.pairlistmanager import PairListManager -from freqtrade.persistence import Trade +from freqtrade.persistence import PairLocks, Trade +from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType @@ -115,6 +116,11 @@ class Backtesting: else: self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0]) + Trade.use_db = False + PairLocks.timeframe = self.config['timeframe'] + PairLocks.use_db = False + self.protections = ProtectionManager(self.config) + # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) # Load one (first) strategy @@ -235,6 +241,10 @@ class Backtesting: trade_dur = int((sell_row[DATE_IDX] - trade.open_date).total_seconds() // 60) closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) + trade.close_date = sell_row[DATE_IDX] + trade.sell_reason = sell.sell_type + trade.close(closerate) + return BacktestResult(pair=trade.pair, profit_percent=trade.calc_profit_ratio(rate=closerate), profit_abs=trade.calc_profit(rate=closerate), @@ -261,6 +271,7 @@ class Backtesting: if len(open_trades[pair]) > 0: for trade in open_trades[pair]: sell_row = data[pair][-1] + trade_entry = BacktestResult(pair=trade.pair, profit_percent=trade.calc_profit_ratio( rate=sell_row[OPEN_IDX]), @@ -320,6 +331,8 @@ class Backtesting: while tmp <= end_date: open_trade_count_start = open_trade_count + self.protections.global_stop(tmp) + for i, pair in enumerate(data): if pair not in indexes: indexes[pair] = 0 @@ -342,7 +355,8 @@ class Backtesting: if ((position_stacking or len(open_trades[pair]) == 0) and (max_open_trades <= 0 or open_trade_count_start < max_open_trades) and tmp != end_date - and row[BUY_IDX] == 1 and row[SELL_IDX] != 1): + and row[BUY_IDX] == 1 and row[SELL_IDX] != 1 + and not PairLocks.is_pair_locked(pair, row[DATE_IDX])): # Enter trade trade = Trade( pair=pair, @@ -361,6 +375,7 @@ class Backtesting: open_trade_count += 1 # logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.") open_trades[pair].append(trade) + Trade.trades.append(trade) for trade in open_trades[pair]: # since indexes has been incremented before, we need to go one step back to @@ -372,6 +387,7 @@ class Backtesting: open_trade_count -= 1 open_trades[pair].remove(trade) trades.append(trade_entry) + self.protections.stop_per_pair(pair, row[DATE_IDX]) # Move time one configured time_interval ahead. tmp += timedelta(minutes=self.timeframe_min) From 9f34aebdaa4ebdbeee78130d03e35f55352551e5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 Nov 2020 20:21:32 +0100 Subject: [PATCH 1081/1197] Allow closing trades without message --- freqtrade/optimize/backtesting.py | 2 +- freqtrade/persistence/models.py | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 1d183152c..f80976a20 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -243,7 +243,7 @@ class Backtesting: trade.close_date = sell_row[DATE_IDX] trade.sell_reason = sell.sell_type - trade.close(closerate) + trade.close(closerate, show_msg=False) return BacktestResult(pair=trade.pair, profit_percent=trade.calc_profit_ratio(rate=closerate), diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index d262a6784..9b8f561b8 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -411,7 +411,7 @@ class Trade(_DECL_BASE): raise ValueError(f'Unknown order type: {order_type}') cleanup_db() - def close(self, rate: float) -> None: + def close(self, rate: float, *, show_msg: bool = False) -> None: """ Sets close_rate to the given rate, calculates total profit and marks trade as closed @@ -423,10 +423,11 @@ class Trade(_DECL_BASE): self.is_open = False self.sell_order_status = 'closed' self.open_order_id = None - logger.info( - 'Marking %s as closed as the trade is fulfilled and found no open orders for it.', - self - ) + if show_msg: + logger.info( + 'Marking %s as closed as the trade is fulfilled and found no open orders for it.', + self + ) def update_fee(self, fee_cost: float, fee_currency: Optional[str], fee_rate: Optional[float], side: str) -> None: From 32189d27c82db0a3239075c6259a1770cb78e5c1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 19 Nov 2020 20:05:56 +0100 Subject: [PATCH 1082/1197] Disable output from plugins in backtesting --- freqtrade/optimize/backtesting.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index f80976a20..7ead5ca24 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -25,6 +25,7 @@ from freqtrade.persistence import PairLocks, Trade from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType +from freqtrade.mixins import LoggingMixin logger = logging.getLogger(__name__) @@ -68,6 +69,8 @@ class Backtesting: """ def __init__(self, config: Dict[str, Any]) -> None: + + LoggingMixin.show_output = False self.config = config # Reset keys for backtesting From e2d15f40824cdc26dc55081eda1e5fa0493cbc06 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Nov 2020 20:29:29 +0100 Subject: [PATCH 1083/1197] Add parameter to enable protections for backtesting --- freqtrade/commands/arguments.py | 6 ++++-- freqtrade/commands/cli_options.py | 8 ++++++++ freqtrade/configuration/configuration.py | 3 +++ freqtrade/optimize/backtesting.py | 11 ++++++++--- freqtrade/optimize/hyperopt.py | 2 ++ 5 files changed, 25 insertions(+), 5 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index aa58ff585..a7ae969f4 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -20,11 +20,13 @@ ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv", "max_open_trades", "stake_amount", "fee"] ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", + "enable_protections", "strategy_list", "export", "exportfilename"] ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", - "position_stacking", "epochs", "spaces", - "use_max_market_positions", "print_all", + "position_stacking", "use_max_market_positions", + "enable_protections", + "epochs", "spaces", "print_all", "print_colorized", "print_json", "hyperopt_jobs", "hyperopt_random_state", "hyperopt_min_trades", "hyperopt_loss"] diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 619a300ae..668b4abf5 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -144,6 +144,14 @@ AVAILABLE_CLI_OPTIONS = { action='store_false', default=True, ), + "enable_protections": Arg( + '--enable-protections', '--enableprotections', + help='Enable protections for backtesting.' + 'Will slow backtesting down by a considerable amount, but will include ' + 'configured protections', + action='store_true', + default=False, + ), "strategy_list": Arg( '--strategy-list', help='Provide a space-separated list of strategies to backtest. ' diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 1ca3187fb..7bf3e6bf2 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -211,6 +211,9 @@ class Configuration: self._args_to_config(config, argname='position_stacking', logstring='Parameter --enable-position-stacking detected ...') + self._args_to_config( + config, argname='enable_protections', + logstring='Parameter --enable-protections detected, enabling Protections. ...') # Setting max_open_trades to infinite if -1 if config.get('max_open_trades') == -1: config['max_open_trades'] = float('inf') diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7ead5ca24..56cc426ac 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -297,7 +297,8 @@ class Backtesting: def backtest(self, processed: Dict, stake_amount: float, start_date: datetime, end_date: datetime, - max_open_trades: int = 0, position_stacking: bool = False) -> DataFrame: + max_open_trades: int = 0, position_stacking: bool = False, + enable_protections: bool = False) -> DataFrame: """ Implement backtesting functionality @@ -311,6 +312,7 @@ class Backtesting: :param end_date: backtesting timerange end datetime :param max_open_trades: maximum number of concurrent trades, <= 0 means unlimited :param position_stacking: do we allow position stacking? + :param enable_protections: Should protections be enabled? :return: DataFrame with trades (results of backtesting) """ logger.debug(f"Run backtest, stake_amount: {stake_amount}, " @@ -334,7 +336,8 @@ class Backtesting: while tmp <= end_date: open_trade_count_start = open_trade_count - self.protections.global_stop(tmp) + if enable_protections: + self.protections.global_stop(tmp) for i, pair in enumerate(data): if pair not in indexes: @@ -390,7 +393,8 @@ class Backtesting: open_trade_count -= 1 open_trades[pair].remove(trade) trades.append(trade_entry) - self.protections.stop_per_pair(pair, row[DATE_IDX]) + if enable_protections: + self.protections.stop_per_pair(pair, row[DATE_IDX]) # Move time one configured time_interval ahead. tmp += timedelta(minutes=self.timeframe_min) @@ -446,6 +450,7 @@ class Backtesting: end_date=max_date.datetime, max_open_trades=max_open_trades, position_stacking=position_stacking, + enable_protections=self.config.get('enable_protections'), ) all_results[self.strategy.get_strategy_name()] = { 'results': results, diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 7870ba1cf..2a2f5b472 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -542,6 +542,8 @@ class Hyperopt: end_date=max_date.datetime, max_open_trades=self.max_open_trades, position_stacking=self.position_stacking, + enable_protections=self.config.get('enable_protections', False), + ) return self._get_results_dict(backtesting_results, min_date, max_date, params_dict, params_details) From 946fb094553ab32854d6992f9ecc58e6db26ea42 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Nov 2020 06:51:54 +0100 Subject: [PATCH 1084/1197] Update help command output --- docs/bot-usage.md | 43 +++++++++++++++++++++++++----------- docs/includes/protections.md | 3 +++ 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 4d07435c7..5820b3cc7 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -213,9 +213,11 @@ Backtesting also uses the config specified via `-c/--config`. usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] [-i TIMEFRAME] - [--timerange TIMERANGE] [--max-open-trades INT] + [--timerange TIMERANGE] + [--data-format-ohlcv {json,jsongz,hdf5}] + [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] - [--eps] [--dmmp] + [--eps] [--dmmp] [--enable-protections] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--export EXPORT] [--export-filename PATH] @@ -226,6 +228,9 @@ optional arguments: `1d`). --timerange TIMERANGE Specify what timerange of data to use. + --data-format-ohlcv {json,jsongz,hdf5} + Storage format for downloaded candle (OHLCV) data. + (default: `None`). --max-open-trades INT Override the value of the `max_open_trades` configuration setting. @@ -241,6 +246,10 @@ optional arguments: Disable applying `max_open_trades` during backtest (same as setting `max_open_trades` to a very high number). + --enable-protections, --enableprotections + Enable protections for backtesting.Will slow + backtesting down by a considerable amount, but will + include configured protections --strategy-list STRATEGY_LIST [STRATEGY_LIST ...] Provide a space-separated list of strategies to backtest. Please note that ticker-interval needs to be @@ -296,13 +305,14 @@ to find optimal parameter values for your strategy. usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] [-i TIMEFRAME] [--timerange TIMERANGE] + [--data-format-ohlcv {json,jsongz,hdf5}] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] [--hyperopt NAME] [--hyperopt-path PATH] [--eps] - [-e INT] + [--dmmp] [--enable-protections] [-e INT] [--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]] - [--dmmp] [--print-all] [--no-color] [--print-json] - [-j JOBS] [--random-state INT] [--min-trades INT] + [--print-all] [--no-color] [--print-json] [-j JOBS] + [--random-state INT] [--min-trades INT] [--hyperopt-loss NAME] optional arguments: @@ -312,6 +322,9 @@ optional arguments: `1d`). --timerange TIMERANGE Specify what timerange of data to use. + --data-format-ohlcv {json,jsongz,hdf5} + Storage format for downloaded candle (OHLCV) data. + (default: `None`). --max-open-trades INT Override the value of the `max_open_trades` configuration setting. @@ -327,14 +340,18 @@ optional arguments: --eps, --enable-position-stacking Allow buying the same pair multiple times (position stacking). - -e INT, --epochs INT Specify number of epochs (default: 100). - --spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...] - Specify which parameters to hyperopt. Space-separated - list. --dmmp, --disable-max-market-positions Disable applying `max_open_trades` during backtest (same as setting `max_open_trades` to a very high number). + --enable-protections, --enableprotections + Enable protections for backtesting.Will slow + backtesting down by a considerable amount, but will + include configured protections + -e INT, --epochs INT Specify number of epochs (default: 100). + --spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...] + Specify which parameters to hyperopt. Space-separated + list. --print-all Print all results, not only the best ones. --no-color Disable colorization of hyperopt results. May be useful if you are redirecting output to a file. @@ -353,10 +370,10 @@ optional arguments: class (IHyperOptLoss). Different functions can generate completely different results, since the target for optimization is different. Built-in - Hyperopt-loss-functions are: ShortTradeDurHyperOptLoss, - OnlyProfitHyperOptLoss, SharpeHyperOptLoss, - SharpeHyperOptLossDaily, SortinoHyperOptLoss, - SortinoHyperOptLossDaily. + Hyperopt-loss-functions are: + ShortTradeDurHyperOptLoss, OnlyProfitHyperOptLoss, + SharpeHyperOptLoss, SharpeHyperOptLossDaily, + SortinoHyperOptLoss, SortinoHyperOptLossDaily Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 210765176..351cfcac3 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -12,6 +12,9 @@ All protection end times are rounded up to the next candle to avoid sudden, unex !!! Tip Each Protection can be configured multiple times with different parameters, to allow different levels of protection (short-term / long-term). +!!! Note "Backtesting" + Protections are supported by backtesting and hyperopt, but must be enabled by using the `--enable-protections` flag. + ### Available Protections * [`StoplossGuard`](#stoploss-guard) Stop trading if a certain amount of stoploss occurred within a certain time window. From a3f9cd2c26cfaf70033f99d4d4a1e8cffc5f9c54 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Nov 2020 07:38:09 +0100 Subject: [PATCH 1085/1197] Only load protections when necessary --- freqtrade/optimize/backtesting.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 56cc426ac..1819e5617 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -122,7 +122,8 @@ class Backtesting: Trade.use_db = False PairLocks.timeframe = self.config['timeframe'] PairLocks.use_db = False - self.protections = ProtectionManager(self.config) + if self.config.get('enable_protections', False): + self.protections = ProtectionManager(self.config) # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) @@ -450,7 +451,7 @@ class Backtesting: end_date=max_date.datetime, max_open_trades=max_open_trades, position_stacking=position_stacking, - enable_protections=self.config.get('enable_protections'), + enable_protections=self.config.get('enable_protections', False), ) all_results[self.strategy.get_strategy_name()] = { 'results': results, From 75a5161650072fc5592bcb63e6f11cc8e2aab07f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Nov 2020 09:53:13 +0100 Subject: [PATCH 1086/1197] Support multis-strategy backtests with protections --- freqtrade/optimize/backtesting.py | 14 ++++++++ freqtrade/persistence/models.py | 8 +++++ freqtrade/persistence/pairlock_middleware.py | 8 +++++ .../plugins/protections/stoploss_guard.py | 4 +-- tests/optimize/test_backtesting.py | 34 +++++++++++++++++-- 5 files changed, 64 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 1819e5617..e3f5e7671 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -120,8 +120,10 @@ class Backtesting: self.fee = self.exchange.get_fee(symbol=self.pairlists.whitelist[0]) Trade.use_db = False + Trade.reset_trades() PairLocks.timeframe = self.config['timeframe'] PairLocks.use_db = False + PairLocks.reset_locks() if self.config.get('enable_protections', False): self.protections = ProtectionManager(self.config) @@ -130,6 +132,11 @@ class Backtesting: # Load one (first) strategy self._set_strategy(self.strategylist[0]) + def __del__(self): + LoggingMixin.show_output = True + PairLocks.use_db = True + Trade.use_db = True + def _set_strategy(self, strategy): """ Load strategy into backtesting @@ -321,6 +328,13 @@ class Backtesting: f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}" ) trades = [] + PairLocks.use_db = False + Trade.use_db = False + if enable_protections: + # Reset persisted data - used for protections only + + PairLocks.reset_locks() + Trade.reset_trades() # Use dict of lists with data for performance # (looping lists is a lot faster than pandas DataFrames) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 9b8f561b8..07f4b5a4f 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -327,6 +327,14 @@ class Trade(_DECL_BASE): 'open_order_id': self.open_order_id, } + @staticmethod + def reset_trades() -> None: + """ + Resets all trades. Only active for backtesting mode. + """ + if not Trade.use_db: + Trade.trades = [] + def adjust_min_max_rates(self, current_price: float) -> None: """ Adjust the max_rate and min_rate. diff --git a/freqtrade/persistence/pairlock_middleware.py b/freqtrade/persistence/pairlock_middleware.py index 6ce91ee6b..8644146d8 100644 --- a/freqtrade/persistence/pairlock_middleware.py +++ b/freqtrade/persistence/pairlock_middleware.py @@ -21,6 +21,14 @@ class PairLocks(): timeframe: str = '' + @staticmethod + def reset_locks() -> None: + """ + Resets all locks. Only active for backtesting mode. + """ + if not PairLocks.use_db: + PairLocks.locks = [] + @staticmethod def lock_pair(pair: str, until: datetime, reason: str = None, *, now: datetime = None) -> None: """ diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 4dbc71048..71e74880c 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -55,8 +55,8 @@ class StoplossGuard(IProtection): # trades = Trade.get_trades(filters).all() trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until) - trades = [trade for trade in trades1 if trade.sell_reason == SellType.STOP_LOSS - or (trade.sell_reason == SellType.TRAILING_STOP_LOSS + trades = [trade for trade in trades1 if str(trade.sell_reason) == SellType.STOP_LOSS.value + or (str(trade.sell_reason) == SellType.TRAILING_STOP_LOSS.value and trade.close_profit < 0)] if len(trades) > self._trade_limit: diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 45cbea68e..15ad18bf9 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -95,6 +95,7 @@ def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None: end_date=max_date, max_open_trades=1, position_stacking=False, + enable_protections=config.get('enable_protections', False), ) # results :: assert len(results) == num_results @@ -532,10 +533,39 @@ def test_processed(default_conf, mocker, testdatadir) -> None: def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir) -> None: - # TODO: Evaluate usefullness of this, the patterns and buy-signls are unrealistic mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - tests = [['raise', 19], ['lower', 0], ['sine', 35]] + tests = [ + ['sine', 35], + ['raise', 19], + ['lower', 0], + ['sine', 35], + ['raise', 19] + ] + # While buy-signals are unrealistic, running backtesting + # over and over again should not cause different results + for [contour, numres] in tests: + simple_backtest(default_conf, contour, numres, mocker, testdatadir) + +def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatadir) -> None: + # TODO: Evaluate usefullness of this, the patterns and buy-signls are unrealistic + default_conf['protections'] = [ + { + "method": "CooldownPeriod", + "stop_duration": 3, + }] + + default_conf['enable_protections'] = True + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + tests = [ + ['sine', 9], + ['raise', 10], + ['lower', 0], + ['sine', 9], + ['raise', 10], + ] + # While buy-signals are unrealistic, running backtesting + # over and over again should not cause different results for [contour, numres] in tests: simple_backtest(default_conf, contour, numres, mocker, testdatadir) From bb51da82978efe7592ed7a14619ab74a91eef4df Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Nov 2020 17:38:15 +0100 Subject: [PATCH 1087/1197] Fix slow backtest due to protections --- 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 e3f5e7671..2684a249c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -173,6 +173,17 @@ class Backtesting: return data, timerange + def prepare_backtest(self, enable_protections): + """ + Backtesting setup method - called once for every call to "backtest()". + """ + PairLocks.use_db = False + Trade.use_db = False + if enable_protections: + # Reset persisted data - used for protections only + PairLocks.reset_locks() + Trade.reset_trades() + def _get_ohlcv_as_lists(self, processed: Dict[str, DataFrame]) -> Dict[str, Tuple]: """ Helper function to convert a processed dataframes into lists for performance reasons. @@ -328,13 +339,7 @@ class Backtesting: f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}" ) trades = [] - PairLocks.use_db = False - Trade.use_db = False - if enable_protections: - # Reset persisted data - used for protections only - - PairLocks.reset_locks() - Trade.reset_trades() + self.prepare_backtest(enable_protections) # Use dict of lists with data for performance # (looping lists is a lot faster than pandas DataFrames) @@ -351,9 +356,6 @@ class Backtesting: while tmp <= end_date: open_trade_count_start = open_trade_count - if enable_protections: - self.protections.global_stop(tmp) - for i, pair in enumerate(data): if pair not in indexes: indexes[pair] = 0 @@ -410,6 +412,7 @@ class Backtesting: trades.append(trade_entry) if enable_protections: self.protections.stop_per_pair(pair, row[DATE_IDX]) + self.protections.global_stop(tmp) # Move time one configured time_interval ahead. tmp += timedelta(minutes=self.timeframe_min) From 57a4044eb0a74209c206941ff9ceb3b763bcf713 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Nov 2020 11:37:10 +0100 Subject: [PATCH 1088/1197] Enhance test verifying that locks are not replaced --- tests/plugins/test_protections.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 22fe33e19..e997c5526 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -1,5 +1,5 @@ import random -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import pytest @@ -123,6 +123,12 @@ def test_stoploss_guard(mocker, default_conf, fee, caplog): assert log_has_re(message, caplog) assert PairLocks.is_global_lock() + # Test 5m after lock-period - this should try and relock the pair, but end-time + # should be the previous end-time + end_time = PairLocks.get_pair_longest_lock('*').lock_end_time + timedelta(minutes=5) + assert freqtrade.protections.global_stop(end_time) + assert not PairLocks.is_global_lock(end_time) + @pytest.mark.parametrize('only_per_pair', [False, True]) @pytest.mark.usefixtures("init_persistence") From 5849d07497f1d36bfa0380c18f241ba6feabc8e1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Dec 2020 06:51:59 +0100 Subject: [PATCH 1089/1197] Export locks as part of backtesting --- freqtrade/optimize/backtesting.py | 1 + freqtrade/optimize/optimize_reports.py | 1 + 2 files changed, 2 insertions(+) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 2684a249c..5bb7eaf74 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -473,6 +473,7 @@ class Backtesting: all_results[self.strategy.get_strategy_name()] = { 'results': results, 'config': self.strategy.config, + 'locks': PairLocks.locks, } stats = generate_backtest_stats(data, all_results, min_date=min_date, max_date=max_date) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index b3799856e..d029ecd13 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -266,6 +266,7 @@ def generate_backtest_stats(btdata: Dict[str, DataFrame], backtest_days = (max_date - min_date).days strat_stats = { 'trades': results.to_dict(orient='records'), + 'locks': [lock.to_json() for lock in content['locks']], 'best_pair': best_pair, 'worst_pair': worst_pair, 'results_per_pair': pair_results, From effc96e92b352f819571a74aab5bcd8db1669803 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Dec 2020 07:42:39 +0100 Subject: [PATCH 1090/1197] Improve tests for backtest protections --- docs/developer.md | 5 +- docs/includes/protections.md | 4 +- freqtrade/mixins/logging_mixin.py | 2 - freqtrade/plugins/__init__.py | 2 - .../plugins/protections/stoploss_guard.py | 2 - tests/optimize/test_backtesting.py | 48 +++++++++++-------- tests/optimize/test_optimize_reports.py | 3 +- tests/plugins/test_protections.py | 2 +- 8 files changed, 37 insertions(+), 31 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index f1d658ab8..48b021027 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -121,8 +121,8 @@ The base-class provides an instance of the exchange (`self._exchange`) the pairl self._pairlist_pos = pairlist_pos ``` -!!! Note - You'll need to register your pairlist in `constants.py` under the variable `AVAILABLE_PAIRLISTS` - otherwise it will not be selectable. +!!! Tip + Don't forget to register your pairlist in `constants.py` under the variable `AVAILABLE_PAIRLISTS` - otherwise it will not be selectable. Now, let's step through the methods which require actions: @@ -184,6 +184,7 @@ No protection should use datetime directly, but use the provided `date_now` vari !!! Tip "Writing a new Protection" Best copy one of the existing Protections to have a good example. + Don't forget to register your protection in `constants.py` under the variable `AVAILABLE_PROTECTIONS` - otherwise it will not be selectable. #### Implementation of a new protection diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 351cfcac3..a8caf55b1 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -1,7 +1,7 @@ ## Protections !!! Warning "Beta feature" - This feature is still in it's testing phase. Should you notice something you think is wrong please let us know via Discord, Slack or via Issue. + This feature is still in it's testing phase. Should you notice something you think is wrong please let us know via Discord, Slack or via Github Issue. Protections will protect your strategy from unexpected events and market conditions by temporarily stop trading for either one pair, or for all pairs. All protection end times are rounded up to the next candle to avoid sudden, unexpected intra-candle buys. @@ -13,7 +13,7 @@ All protection end times are rounded up to the next candle to avoid sudden, unex Each Protection can be configured multiple times with different parameters, to allow different levels of protection (short-term / long-term). !!! Note "Backtesting" - Protections are supported by backtesting and hyperopt, but must be enabled by using the `--enable-protections` flag. + Protections are supported by backtesting and hyperopt, but must be explicitly enabled by using the `--enable-protections` flag. ### Available Protections diff --git a/freqtrade/mixins/logging_mixin.py b/freqtrade/mixins/logging_mixin.py index e9921e1ec..2e1c20a52 100644 --- a/freqtrade/mixins/logging_mixin.py +++ b/freqtrade/mixins/logging_mixin.py @@ -1,5 +1,3 @@ - - from typing import Callable from cachetools import TTLCache, cached diff --git a/freqtrade/plugins/__init__.py b/freqtrade/plugins/__init__.py index 96943268b..e69de29bb 100644 --- a/freqtrade/plugins/__init__.py +++ b/freqtrade/plugins/__init__.py @@ -1,2 +0,0 @@ -# flake8: noqa: F401 -# from freqtrade.plugins.protectionmanager import ProtectionManager diff --git a/freqtrade/plugins/protections/stoploss_guard.py b/freqtrade/plugins/protections/stoploss_guard.py index 71e74880c..193907ddc 100644 --- a/freqtrade/plugins/protections/stoploss_guard.py +++ b/freqtrade/plugins/protections/stoploss_guard.py @@ -3,8 +3,6 @@ import logging from datetime import datetime, timedelta from typing import Any, Dict -from sqlalchemy import and_, or_ - from freqtrade.persistence import Trade from freqtrade.plugins.protections import IProtection, ProtectionReturn from freqtrade.strategy.interface import SellType diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 15ad18bf9..547e55db8 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -79,7 +79,7 @@ def load_data_test(what, testdatadir): fill_missing=True)} -def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None: +def simple_backtest(config, contour, mocker, testdatadir) -> None: patch_exchange(mocker) config['timeframe'] = '1m' backtesting = Backtesting(config) @@ -98,7 +98,7 @@ def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None: enable_protections=config.get('enable_protections', False), ) # results :: - assert len(results) == num_results + return results # FIX: fixturize this? @@ -532,23 +532,9 @@ def test_processed(default_conf, mocker, testdatadir) -> None: assert col in cols -def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir) -> None: - mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) - tests = [ - ['sine', 35], - ['raise', 19], - ['lower', 0], - ['sine', 35], - ['raise', 19] - ] - # While buy-signals are unrealistic, running backtesting - # over and over again should not cause different results - for [contour, numres] in tests: - simple_backtest(default_conf, contour, numres, mocker, testdatadir) - - def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatadir) -> None: - # TODO: Evaluate usefullness of this, the patterns and buy-signls are unrealistic + # While this test IS a copy of test_backtest_pricecontours, it's needed to ensure + # results do not carry-over to the next run, which is not given by using parametrize. default_conf['protections'] = [ { "method": "CooldownPeriod", @@ -567,7 +553,31 @@ def test_backtest_pricecontours_protections(default_conf, fee, mocker, testdatad # While buy-signals are unrealistic, running backtesting # over and over again should not cause different results for [contour, numres] in tests: - simple_backtest(default_conf, contour, numres, mocker, testdatadir) + assert len(simple_backtest(default_conf, contour, mocker, testdatadir)) == numres + + +@pytest.mark.parametrize('protections,contour,expected', [ + (None, 'sine', 35), + (None, 'raise', 19), + (None, 'lower', 0), + (None, 'sine', 35), + (None, 'raise', 19), + ([{"method": "CooldownPeriod", "stop_duration": 3}], 'sine', 9), + ([{"method": "CooldownPeriod", "stop_duration": 3}], 'raise', 10), + ([{"method": "CooldownPeriod", "stop_duration": 3}], 'lower', 0), + ([{"method": "CooldownPeriod", "stop_duration": 3}], 'sine', 9), + ([{"method": "CooldownPeriod", "stop_duration": 3}], 'raise', 10), +]) +def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir, + protections, contour, expected) -> None: + if protections: + default_conf['protections'] = protections + default_conf['enable_protections'] = True + + mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + # While buy-signals are unrealistic, running backtesting + # over and over again should not cause different results + assert len(simple_backtest(default_conf, contour, mocker, testdatadir)) == expected def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index d04929164..a0e1932ff 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -76,7 +76,8 @@ def test_generate_backtest_stats(default_conf, testdatadir): "sell_reason": [SellType.ROI, SellType.STOP_LOSS, SellType.ROI, SellType.FORCE_SELL] }), - 'config': default_conf} + 'config': default_conf, + 'locks': []} } timerange = TimeRange.parse_timerange('1510688220-1510700340') min_date = Arrow.fromtimestamp(1510688220) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index e997c5526..2ad03a97c 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -1,5 +1,5 @@ import random -from datetime import datetime, timedelta, timezone +from datetime import datetime, timedelta import pytest From e873cafdc49d46c2398550a77bd29dd61816a050 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 14:54:39 +0100 Subject: [PATCH 1091/1197] Beautify code a bit --- freqtrade/rpc/rpc.py | 20 ++++++++++---------- freqtrade/rpc/telegram.py | 17 +++++++---------- 2 files changed, 17 insertions(+), 20 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c4b4117ff..49e5bc2d2 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -275,38 +275,38 @@ class RPC: "trades_count": len(output) } - def _rpc_stats(self): + def _rpc_stats(self) -> Dict[str, Any]: """ Generate generic stats for trades in database """ def trade_win_loss(trade): if trade.close_profit > 0: - return 'Wins' + return 'wins' elif trade.close_profit < 0: - return 'Losses' + return 'losses' else: - return 'Draws' + return 'draws' trades = trades = Trade.get_trades([Trade.is_open.is_(False)]) # Sell reason sell_reasons = {} for trade in trades: if trade.sell_reason not in sell_reasons: - sell_reasons[trade.sell_reason] = {'Wins': 0, 'Losses': 0, 'Draws': 0} + sell_reasons[trade.sell_reason] = {'wins': 0, 'losses': 0, 'draws': 0} sell_reasons[trade.sell_reason][trade_win_loss(trade)] += 1 # Duration - dur: Dict[str, List[int]] = {'Wins': [], 'Draws': [], 'Losses': []} + dur: Dict[str, List[int]] = {'wins': [], 'draws': [], 'losses': []} for trade in trades: if trade.close_date is not None and trade.open_date is not None: trade_dur = (trade.close_date - trade.open_date).total_seconds() dur[trade_win_loss(trade)].append(trade_dur) - wins_dur = sum(dur['Wins']) / len(dur['Wins']) if len(dur['Wins']) > 0 else 'N/A' - draws_dur = sum(dur['Draws']) / len(dur['Draws']) if len(dur['Draws']) > 0 else 'N/A' - losses_dur = sum(dur['Losses']) / len(dur['Losses']) if len(dur['Losses']) > 0 else 'N/A' + wins_dur = sum(dur['wins']) / len(dur['wins']) if len(dur['wins']) > 0 else 'N/A' + draws_dur = sum(dur['draws']) / len(dur['draws']) if len(dur['draws']) > 0 else 'N/A' + losses_dur = sum(dur['losses']) / len(dur['losses']) if len(dur['losses']) > 0 else 'N/A' durations = {'wins': wins_dur, 'draws': draws_dur, 'losses': losses_dur} - return sell_reasons, durations + return {'sell_reasons': sell_reasons, 'durations': durations} def _rpc_trade_statistics( self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 76d9292b4..25965e05f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -3,9 +3,9 @@ """ This module manage Telegram communication """ -from datetime import timedelta import json import logging +from datetime import timedelta from typing import Any, Callable, Dict, List, Union import arrow @@ -395,9 +395,8 @@ class Telegram(RPC): """ Handler for /stats Show stats of recent trades - :return: None """ - sell_reasons, durations = self._rpc_stats() + stats = self._rpc_stats() sell_reasons_tabulate = [] reason_map = { @@ -409,26 +408,24 @@ class Telegram(RPC): 'force_sell': 'Forcesell', 'emergency_sell': 'Emergency Sell', } - for reason, count in sell_reasons.items(): + for reason, count in stats['sell_reasons'].items(): sell_reasons_tabulate.append([ reason_map.get(reason, reason), sum(count.values()), - count['Wins'], - # count['Draws'], - count['Losses'] + count['wins'], + count['losses'] ]) sell_reasons_msg = tabulate( sell_reasons_tabulate, headers=['Sell Reason', 'Sells', 'Wins', 'Losses'] ) - + durations = stats['durations'] duration_msg = tabulate([ ['Wins', str(timedelta(seconds=durations['wins'])) if durations['wins'] != 'N/A' else 'N/A'], - # ['Draws', str(timedelta(seconds=durations['draws']))], ['Losses', str(timedelta(seconds=durations['losses'])) if durations['losses'] != 'N/A' else 'N/A'] - ], + ], headers=['', 'Avg. Duration'] ) msg = (f"""```\n{sell_reasons_msg}```\n```\n{duration_msg}```""") From 81410fb4044c0d6238441ce86dcaed95d3d3e975 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 15:03:16 +0100 Subject: [PATCH 1092/1197] Document /stats for telegram --- docs/telegram-usage.md | 1 + freqtrade/rpc/telegram.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index f4bd0a12a..c940f59ac 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -113,6 +113,7 @@ official commands. You can ask at any moment for help with `/help`. | `/performance` | Show performance of each finished trade grouped by pair | `/balance` | Show account balance per currency | `/daily ` | Shows profit or loss per day, over the last n days (n defaults to 7) +| `/stats` | Shows Wins / losses by Sell reason as well as Avg. holding durations for buys and sells | `/whitelist` | Show the current whitelist | `/blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. | `/edge` | Show validated pairs by Edge if it is enabled. diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 25965e05f..b6c0a1f3f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -787,6 +787,8 @@ class Telegram(RPC): "*/delete :* `Instantly delete the given trade in the database`\n" "*/performance:* `Show performance of each finished trade grouped by pair`\n" "*/daily :* `Shows profit or loss per day, over the last n days`\n" + "*/stats:* `Shows Wins / losses by Sell reason as well as " + "Avg. holding durationsfor buys and sells.`\n" "*/count:* `Show number of active trades compared to allowed number of trades`\n" "*/locks:* `Show currently locked pairs`\n" "*/balance:* `Show account balance per currency`\n" From 3ab5514697c294a9ab5918dc44e408f2f88bb341 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 15:07:08 +0100 Subject: [PATCH 1093/1197] Add API endpoint for /stats --- docs/rest-api.md | 4 ++++ freqtrade/rpc/api_server.py | 14 ++++++++++++++ scripts/rest_client.py | 7 +++++++ tests/rpc/test_rpc_apiserver.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 54 insertions(+) diff --git a/docs/rest-api.md b/docs/rest-api.md index 7726ab875..9bb35ce91 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -127,6 +127,7 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `performance` | Show performance of each finished trade grouped by pair. | `balance` | Show account balance per currency. | `daily ` | Shows profit or loss per day, over the last n days (n defaults to 7). +| `stats` | Display a summary of profit / loss reasons as well as average holding times. | `whitelist` | Show the current whitelist. | `blacklist [pair]` | Show the current blacklist, or adds a pair to the blacklist. | `edge` | Show validated pairs by Edge if it is enabled. @@ -229,6 +230,9 @@ show_config start Start the bot if it's in the stopped state. +stats + Return the stats report (durations, sell-reasons). + status Get the status of open trades. diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index 8c2c203e6..c86aa1fa7 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -198,6 +198,8 @@ class ApiServer(RPC): self.app.add_url_rule(f'{BASE_URI}/logs', 'log', view_func=self._get_logs, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/profit', 'profit', view_func=self._profit, methods=['GET']) + self.app.add_url_rule(f'{BASE_URI}/stats', 'stats', + view_func=self._stats, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/performance', 'performance', view_func=self._performance, methods=['GET']) self.app.add_url_rule(f'{BASE_URI}/status', 'status', @@ -417,6 +419,18 @@ class ApiServer(RPC): return jsonify(stats) + @require_login + @rpc_catch_errors + def _stats(self): + """ + Handler for /stats. + Returns a Object with "durations" and "sell_reasons" as keys. + """ + + stats = self._rpc_stats() + + return jsonify(stats) + @require_login @rpc_catch_errors def _performance(self): diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 268e81397..2232b8421 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -139,6 +139,13 @@ class FtRestClient(): """ return self._get("profit") + def stats(self): + """Return the stats report (durations, sell-reasons). + + :return: json object + """ + return self._get("stats") + def performance(self): """Return the performance of the different coins. diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 0dc43474f..2daa32bc7 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -559,6 +559,35 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li } +@pytest.mark.usefixtures("init_persistence") +def test_api_stats(botclient, mocker, ticker, fee, markets,): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_balances=MagicMock(return_value=ticker), + fetch_ticker=ticker, + get_fee=fee, + markets=PropertyMock(return_value=markets) + ) + + rc = client_get(client, f"{BASE_URI}/stats") + assert_response(rc, 200) + assert 'durations' in rc.json + assert 'sell_reasons' in rc.json + + create_mock_trades(fee) + + rc = client_get(client, f"{BASE_URI}/stats") + assert_response(rc, 200) + assert 'durations' in rc.json + assert 'sell_reasons' in rc.json + + assert 'wins' in rc.json['durations'] + assert 'losses' in rc.json['durations'] + assert 'draws' in rc.json['durations'] + + def test_api_performance(botclient, mocker, ticker, fee): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) From f047297995a3e8875382a1445bbb53d6a1024810 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 15:45:02 +0100 Subject: [PATCH 1094/1197] Improve wording, fix bug --- docs/includes/protections.md | 2 +- freqtrade/mixins/logging_mixin.py | 1 + freqtrade/optimize/backtesting.py | 2 +- .../plugins/protections/max_drawdown_protection.py | 5 ++++- tests/plugins/test_protections.py | 12 ++++++++++++ 5 files changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index a8caf55b1..7378a590c 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -7,7 +7,7 @@ Protections will protect your strategy from unexpected events and market conditi All protection end times are rounded up to the next candle to avoid sudden, unexpected intra-candle buys. !!! Note - Not all Protections will work for all strategies, and parameters will need to be tuned for your strategy. + Not all Protections will work for all strategies, and parameters will need to be tuned for your strategy to improve performance. !!! Tip Each Protection can be configured multiple times with different parameters, to allow different levels of protection (short-term / long-term). diff --git a/freqtrade/mixins/logging_mixin.py b/freqtrade/mixins/logging_mixin.py index 2e1c20a52..06935d5f6 100644 --- a/freqtrade/mixins/logging_mixin.py +++ b/freqtrade/mixins/logging_mixin.py @@ -1,4 +1,5 @@ from typing import Callable + from cachetools import TTLCache, cached diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 5bb7eaf74..de9c52dad 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -18,6 +18,7 @@ from freqtrade.data.converter import trim_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds +from freqtrade.mixins import LoggingMixin from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, store_backtest_stats) from freqtrade.pairlist.pairlistmanager import PairListManager @@ -25,7 +26,6 @@ from freqtrade.persistence import PairLocks, Trade from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType -from freqtrade.mixins import LoggingMixin logger = logging.getLogger(__name__) diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index f1c77d1d9..d54e6699b 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -54,7 +54,10 @@ class MaxDrawdown(IProtection): return False, None, None # Drawdown is always positive - drawdown, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit') + try: + drawdown, _, _ = calculate_max_drawdown(trades_df, value_col='close_profit') + except ValueError: + return False, None, None if drawdown > self._max_allowed_drawdown: self.log_once( diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 2ad03a97c..82b6e4500 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -304,6 +304,18 @@ def test_MaxDrawdown(mocker, default_conf, fee, caplog): 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=1000, min_ago_close=900, profit_rate=1.1, )) + Trade.session.add(generate_mock_trade( + 'ETH/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=1000, min_ago_close=900, profit_rate=1.1, + )) + Trade.session.add(generate_mock_trade( + 'NEO/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, + min_ago_open=1000, min_ago_close=900, profit_rate=1.1, + )) + # No losing trade yet ... so max_drawdown will raise exception + assert not freqtrade.protections.global_stop() + assert not freqtrade.protections.stop_per_pair('XRP/BTC') + Trade.session.add(generate_mock_trade( 'XRP/BTC', fee.return_value, False, sell_reason=SellType.STOP_LOSS.value, min_ago_open=500, min_ago_close=400, profit_rate=0.9, From de2cc9708dc5d6e9f9e0bfa83d11e31a31442ca6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 16:01:29 +0100 Subject: [PATCH 1095/1197] Fix test leakage --- tests/plugins/test_pairlocks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/plugins/test_pairlocks.py b/tests/plugins/test_pairlocks.py index db7d9f46f..bd103b21e 100644 --- a/tests/plugins/test_pairlocks.py +++ b/tests/plugins/test_pairlocks.py @@ -79,6 +79,7 @@ def test_PairLocks(use_db): # Nothing was pushed to the database assert len(PairLock.query.all()) == 0 # Reset use-db variable + PairLocks.reset_locks() PairLocks.use_db = True @@ -111,4 +112,5 @@ def test_PairLocks_getlongestlock(use_db): # Must be longer than above assert lock.lock_end_time.replace(tzinfo=timezone.utc) > arrow.utcnow().shift(minutes=14) + PairLocks.reset_locks() PairLocks.use_db = True From b5289d5f0ed48ba5cbbb916ca30a388619bf62e7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 16:02:55 +0100 Subject: [PATCH 1096/1197] Update full config with correct protection keys --- config_full.json.example | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 737015b41..b6170bceb 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -78,26 +78,26 @@ "protections": [ { "method": "StoplossGuard", - "lookback_period": 60, + "lookback_period_candles": 60, "trade_limit": 4, - "stopduration": 60 + "stop_duration_candles": 60 }, { "method": "CooldownPeriod", - "stopduration": 20 + "stop_duration_candles": 20 }, { "method": "MaxDrawdown", - "lookback_period": 2000, + "lookback_period_candles": 200, "trade_limit": 20, - "stop_duration": 10, + "stop_duration_candles": 10, "max_allowed_drawdown": 0.2 }, { "method": "LowProfitPairs", - "lookback_period": 360, + "lookback_period_candles": 360, "trade_limit": 1, - "stop_duration": 2, + "stop_duration_candles": 2, "required_profit": 0.02 } ], From c37bc307e29d26d9db8ceec2dd3e920354dafca2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 16:07:00 +0100 Subject: [PATCH 1097/1197] Small finetunings to documentation --- docs/developer.md | 2 +- freqtrade/pairlist/pairlistmanager.py | 3 --- freqtrade/persistence/models.py | 2 +- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index 48b021027..dcbaa3ca9 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -229,7 +229,7 @@ The method `global_stop()` will be called whenever a trade closed (sell order co ##### Protections - calculating lock end time Protections should calculate the lock end time based on the last trade it considers. -This avoids relocking should the lookback-period be longer than the actual lock period. +This avoids re-locking should the lookback-period be longer than the actual lock period. The `IProtection` parent class provides a helper method for this in `calculate_lock_end()`. diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/pairlist/pairlistmanager.py index 89bab99be..810a22300 100644 --- a/freqtrade/pairlist/pairlistmanager.py +++ b/freqtrade/pairlist/pairlistmanager.py @@ -26,9 +26,6 @@ class PairListManager(): self._pairlist_handlers: List[IPairList] = [] self._tickers_needed = False for pairlist_handler_config in self._config.get('pairlists', None): - if 'method' not in pairlist_handler_config: - logger.warning(f"No method found in {pairlist_handler_config}, ignoring.") - continue pairlist_handler = PairListResolver.load_pairlist( pairlist_handler_config['method'], exchange=exchange, diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 07f4b5a4f..bcda6368a 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -419,7 +419,7 @@ class Trade(_DECL_BASE): raise ValueError(f'Unknown order type: {order_type}') cleanup_db() - def close(self, rate: float, *, show_msg: bool = False) -> None: + def close(self, rate: float, *, show_msg: bool = True) -> None: """ Sets close_rate to the given rate, calculates total profit and marks trade as closed From 82bc6973feab3937010e99cb2301cbad30651724 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 16:16:33 +0100 Subject: [PATCH 1098/1197] Add last key to config_full --- config_full.json.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config_full.json.example b/config_full.json.example index b6170bceb..e69e52469 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -80,7 +80,8 @@ "method": "StoplossGuard", "lookback_period_candles": 60, "trade_limit": 4, - "stop_duration_candles": 60 + "stop_duration_candles": 60, + "only_per_pair": false }, { "method": "CooldownPeriod", From f897b683c7ec3031cc0aeb6dd863c5891a6e86c6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 7 Dec 2020 19:22:14 +0100 Subject: [PATCH 1099/1197] Add seperate page describing plugins --- docs/plugins.md | 3 +++ mkdocs.yml | 1 + 2 files changed, 4 insertions(+) create mode 100644 docs/plugins.md diff --git a/docs/plugins.md b/docs/plugins.md new file mode 100644 index 000000000..1f785bbaa --- /dev/null +++ b/docs/plugins.md @@ -0,0 +1,3 @@ +# Plugins +--8<-- "includes/pairlists.md" +--8<-- "includes/protections.md" diff --git a/mkdocs.yml b/mkdocs.yml index c791386ae..a7ae0cc96 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -19,6 +19,7 @@ nav: - Backtesting: backtesting.md - Hyperopt: hyperopt.md - Edge Positioning: edge.md + - Plugins: plugins.md - Utility Subcommands: utils.md - FAQ: faq.md - Data Analysis: From 9725b8e17cebe927ea2a0faa5f43e302c4cc18dd Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Dec 2020 08:43:22 +0100 Subject: [PATCH 1100/1197] Update Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 68b37afe3..ea94822c9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9.0-slim-buster +FROM python:3.9.1-slim-buster RUN apt-get update \ && apt-get -y install curl build-essential libssl-dev sqlite3 \ From 118a22d0104cf6f8298f0f7c72639c450f0413b0 Mon Sep 17 00:00:00 2001 From: Samaoo Date: Tue, 8 Dec 2020 18:04:26 +0100 Subject: [PATCH 1101/1197] Update data-download.md --- docs/data-download.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data-download.md b/docs/data-download.md index e9c5c1865..2d77a8a17 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -8,7 +8,7 @@ If no additional parameter is specified, freqtrade will download data for `"1m"` Exchange and pairs will come from `config.json` (if specified using `-c/--config`). Otherwise `--exchange` becomes mandatory. -You can use a relative timerange (`--days 20`) or an absolute starting point (`--timerange 20200101`). For incremental downloads, the relative approach should be used. +You can use a relative timerange (`--days 20`) or an absolute starting point (`--timerange 20200101-`). For incremental downloads, the relative approach should be used. !!! Tip "Tip: Updating existing data" If you already have backtesting data available in your data-directory and would like to refresh this data up to today, use `--days xx` with a number slightly higher than the missing number of days. Freqtrade will keep the available data and only download the missing data. From d9a86158f4ef44b5f172e4b3af7c257fd7d5c85f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 8 Dec 2020 19:46:54 +0100 Subject: [PATCH 1102/1197] Add cmake to support raspberry 64bit installs --- docs/installation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/installation.md b/docs/installation.md index 5cc0e03f4..f00bf0836 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -112,7 +112,7 @@ OS Specific steps are listed first, the [Common](#common) section below is neces ``` bash - sudo apt-get install python3-venv libatlas-base-dev + sudo apt-get install python3-venv libatlas-base-dev cmake # Use pywheels.org to speed up installation sudo echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > tee /etc/pip.conf From e6b3e645340da773d20f3b96144872b4bd115c63 Mon Sep 17 00:00:00 2001 From: David Martinez Martin Date: Wed, 9 Dec 2020 03:27:59 +0100 Subject: [PATCH 1103/1197] Update dockerfile to multistage This change reduce the image size from 727Mb to 469Mb. --- Dockerfile | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2be65274e..8840a707a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,24 +1,40 @@ -FROM python:3.8.6-slim-buster +FROM python:3.8.6-slim-buster as base -RUN apt-get update \ - && apt-get -y install curl build-essential libssl-dev sqlite3 \ - && apt-get clean \ - && pip install --upgrade pip +# Setup env +ENV LANG C.UTF-8 +ENV LC_ALL C.UTF-8 +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONFAULTHANDLER 1 # Prepare environment RUN mkdir /freqtrade WORKDIR /freqtrade +# Install dependencies +FROM base as python-deps +RUN apt-get update \ + && apt-get -y install git curl build-essential libssl-dev \ + && apt-get clean \ + && pip install --upgrade pip + # Install TA-lib COPY build_helpers/* /tmp/ RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib* - ENV LD_LIBRARY_PATH /usr/local/lib # Install dependencies COPY requirements.txt requirements-hyperopt.txt /freqtrade/ -RUN pip install numpy --no-cache-dir \ - && pip install -r requirements-hyperopt.txt --no-cache-dir +RUN pip install --user --no-cache-dir numpy \ + && pip install --user --no-cache-dir -r requirements-hyperopt.txt + +# Copy dependencies to runtime-image +FROM base as runtime-image +COPY --from=python-deps /usr/local/lib /usr/local/lib +ENV LD_LIBRARY_PATH /usr/local/lib + +COPY --from=python-deps /root/.local /root/.local +ENV PATH=/root/.local/bin:$PATH + # Install and execute COPY . /freqtrade/ From f1af2972e2b23b627a99e5a3645031e489066470 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Dec 2020 07:52:58 +0100 Subject: [PATCH 1104/1197] Ensure non-defined attributes fail correctly Remove unnecessary check, as stoploss cannot be none (it's mandatory and a number) --- freqtrade/freqtradebot.py | 3 +-- freqtrade/resolvers/strategy_resolver.py | 27 +++++++++++++++++------- freqtrade/strategy/interface.py | 3 +-- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c8d281852..15aa3416c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -519,8 +519,7 @@ class FreqtradeBot: # reserve some percent defined in config (5% default) + stoploss amount_reserve_percent = 1.0 - self.config.get('amount_reserve_percent', constants.DEFAULT_AMOUNT_RESERVE_PERCENT) - if self.strategy.stoploss is not None: - amount_reserve_percent += self.strategy.stoploss + amount_reserve_percent += self.strategy.stoploss # it should not be more than 50% amount_reserve_percent = max(amount_reserve_percent, 0.5) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 63a3f784e..73af00fee 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -88,9 +88,6 @@ class StrategyResolver(IResolver): StrategyResolver._override_attribute_helper(strategy, config, attribute, default) - # Assign deprecated variable - to not break users code relying on this. - strategy.ticker_interval = strategy.timeframe - # Loop this list again to have output combined for attribute, _, subkey in attributes: if subkey and attribute in config[subkey]: @@ -98,11 +95,7 @@ class StrategyResolver(IResolver): elif attribute in config: logger.info("Strategy using %s: %s", attribute, config[attribute]) - # Sort and apply type conversions - strategy.minimal_roi = OrderedDict(sorted( - {int(key): value for (key, value) in strategy.minimal_roi.items()}.items(), - key=lambda t: t[0])) - strategy.stoploss = float(strategy.stoploss) + StrategyResolver._normalize_attributes(strategy) StrategyResolver._strategy_sanity_validations(strategy) return strategy @@ -131,6 +124,24 @@ class StrategyResolver(IResolver): setattr(strategy, attribute, default) config[attribute] = default + @staticmethod + def _normalize_attributes(strategy: IStrategy) -> IStrategy: + """ + Normalize attributes to have the correct type. + """ + # Assign deprecated variable - to not break users code relying on this. + if hasattr(strategy, 'timeframe'): + strategy.ticker_interval = strategy.timeframe + + # Sort and apply type conversions + if hasattr(strategy, 'minimal_roi'): + strategy.minimal_roi = OrderedDict(sorted( + {int(key): value for (key, value) in strategy.minimal_roi.items()}.items(), + key=lambda t: t[0])) + if hasattr(strategy, 'stoploss'): + strategy.stoploss = float(strategy.stoploss) + return strategy + @staticmethod def _strategy_sanity_validations(strategy): if not all(k in strategy.order_types for k in REQUIRED_ORDERTYPES): diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 81f4e7651..125211a85 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -551,8 +551,7 @@ class IStrategy(ABC): # evaluate if the stoploss was hit if stoploss is not on exchange # in Dry-Run, this handles stoploss logic as well, as the logic will not be different to # regular stoploss handling. - if ((self.stoploss is not None) and - (trade.stop_loss >= current_rate) and + if ((trade.stop_loss >= current_rate) and (not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])): sell_type = SellType.STOP_LOSS From 57080982566e295747da2286d87f94f012e812cc Mon Sep 17 00:00:00 2001 From: David Martinez Martin Date: Wed, 9 Dec 2020 10:34:38 +0100 Subject: [PATCH 1105/1197] Move ENV PATH to base image --- Dockerfile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 8840a707a..f85dfb0c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,6 +5,7 @@ ENV LANG C.UTF-8 ENV LC_ALL C.UTF-8 ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONFAULTHANDLER 1 +ENV PATH=/root/.local/bin:$PATH # Prepare environment RUN mkdir /freqtrade @@ -13,7 +14,7 @@ WORKDIR /freqtrade # Install dependencies FROM base as python-deps RUN apt-get update \ - && apt-get -y install git curl build-essential libssl-dev \ + && apt-get -y install curl build-essential libssl-dev \ && apt-get clean \ && pip install --upgrade pip @@ -33,7 +34,7 @@ COPY --from=python-deps /usr/local/lib /usr/local/lib ENV LD_LIBRARY_PATH /usr/local/lib COPY --from=python-deps /root/.local /root/.local -ENV PATH=/root/.local/bin:$PATH + # Install and execute From 25f8e0cc57b050c10ecab3cbf1d2b712008fd341 Mon Sep 17 00:00:00 2001 From: David Martinez Martin Date: Wed, 9 Dec 2020 11:28:45 +0100 Subject: [PATCH 1106/1197] Added git packages for future dependencies --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index f85dfb0c7..602e6a28c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ WORKDIR /freqtrade # Install dependencies FROM base as python-deps RUN apt-get update \ - && apt-get -y install curl build-essential libssl-dev \ + && apt-get -y install curl build-essential libssl-dev git \ && apt-get clean \ && pip install --upgrade pip From f5817063b75b5957d29783c3f5f1b9eda19d20fc Mon Sep 17 00:00:00 2001 From: Samaoo Date: Wed, 9 Dec 2020 15:53:38 +0100 Subject: [PATCH 1107/1197] Update backtesting.md --- docs/backtesting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 3058d1b57..1fc9f3d73 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -285,7 +285,7 @@ Since backtesting lacks some detailed information about what happens within a ca - 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 +- Stoploss sells happen exactly at stoploss price, even if low was lower, but the loss will be 0.32% lower than the stoploss price. - Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes - Low happens before high for stoploss, protecting capital first - Trailing stoploss From af53dfbfab71e61cc3c50200ed2bb26c283720b7 Mon Sep 17 00:00:00 2001 From: Samaoo Date: Wed, 9 Dec 2020 15:57:15 +0100 Subject: [PATCH 1108/1197] Update backtesting.md --- docs/backtesting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 1fc9f3d73..de54c4c91 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -285,7 +285,7 @@ Since backtesting lacks some detailed information about what happens within a ca - 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, but the loss will be 0.32% lower than the stoploss price. +- Stoploss sells happen exactly at stoploss price, even if low was lower, but the loss will be 0.32% lower than the stoploss price - Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes - Low happens before high for stoploss, protecting capital first - Trailing stoploss From 33f330256b02098458cbd18e28d970a735efb5e6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 9 Dec 2020 20:26:11 +0100 Subject: [PATCH 1109/1197] Reorder commands on telegram init --- freqtrade/rpc/telegram.py | 2 +- tests/conftest_trades.py | 10 ++++++++-- tests/rpc/test_rpc_telegram.py | 6 +++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index b6c0a1f3f..fa36cfee9 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -99,6 +99,7 @@ class Telegram(RPC): CommandHandler('trades', self._trades), CommandHandler('delete', self._delete_trade), CommandHandler('performance', self._performance), + CommandHandler('stats', self._stats), CommandHandler('daily', self._daily), CommandHandler('count', self._count), CommandHandler('locks', self._locks), @@ -111,7 +112,6 @@ class Telegram(RPC): CommandHandler('edge', self._edge), CommandHandler('help', self._help), CommandHandler('version', self._version), - CommandHandler('stats', self._stats), ] for handle in handles: self._updater.dispatcher.add_handler(handle) diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index fac822b2b..e84722041 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -1,3 +1,5 @@ +from datetime import datetime, timedelta, timezone + from freqtrade.persistence.models import Order, Trade @@ -82,7 +84,9 @@ def mock_trade_2(fee): is_open=False, open_order_id='dry_run_sell_12345', strategy='DefaultStrategy', - sell_reason='sell_signal' + sell_reason='sell_signal', + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), + close_date=datetime.now(tz=timezone.utc), ) o = Order.parse_from_ccxt_object(mock_order_2(), 'ETC/BTC', 'buy') trade.orders.append(o) @@ -135,7 +139,9 @@ def mock_trade_3(fee): close_profit=0.01, exchange='bittrex', is_open=False, - sell_reason='roi' + sell_reason='roi', + open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), + close_date=datetime.now(tz=timezone.utc), ) o = Order.parse_from_ccxt_object(mock_order_3(), 'XRP/BTC', 'buy') trade.orders.append(o) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 725c1411e..ecad05683 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -74,10 +74,10 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], " "['balance'], ['start'], ['stop'], ['forcesell'], ['forcebuy'], ['trades'], " - "['delete'], ['performance'], ['daily'], ['count'], ['locks'], " + "['delete'], ['performance'], ['stats'], ['daily'], ['count'], ['locks'], " "['reload_config', 'reload_conf'], ['show_config', 'show_conf'], ['stopbuy'], " - "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version'], " - "['stats']]") + "['whitelist'], ['blacklist'], ['logs'], ['edge'], ['help'], ['version']" + "]") assert log_has(message_str, caplog) From ca99d484fcd852561f8acbb3bd9cbb879ddc724d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Dec 2020 07:39:50 +0100 Subject: [PATCH 1110/1197] Refactor to use list comprehension --- freqtrade/rpc/telegram.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index fa36cfee9..c54000677 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -398,7 +398,6 @@ class Telegram(RPC): """ stats = self._rpc_stats() - sell_reasons_tabulate = [] reason_map = { 'roi': 'ROI', 'stop_loss': 'Stoploss', @@ -408,13 +407,14 @@ class Telegram(RPC): 'force_sell': 'Forcesell', 'emergency_sell': 'Emergency Sell', } - for reason, count in stats['sell_reasons'].items(): - sell_reasons_tabulate.append([ + sell_reasons_tabulate = [ + [ reason_map.get(reason, reason), sum(count.values()), count['wins'], count['losses'] - ]) + ] for reason, count in stats['sell_reasons'].items() + ] sell_reasons_msg = tabulate( sell_reasons_tabulate, headers=['Sell Reason', 'Sells', 'Wins', 'Losses'] From 201cc67e0503c98ad962ea1a0a53b4763fdedd81 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Dec 2020 19:21:20 +0100 Subject: [PATCH 1111/1197] Rename open_trade_price to "open_trade_value" --- freqtrade/freqtradebot.py | 2 +- freqtrade/persistence/models.py | 22 +++++++++++----------- tests/rpc/test_rpc.py | 4 ++-- tests/rpc/test_rpc_apiserver.py | 4 ++-- tests/test_persistence.py | 28 ++++++++++++++-------------- 5 files changed, 30 insertions(+), 30 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c8d281852..59da58e1b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1396,7 +1396,7 @@ class FreqtradeBot: abs_tol=constants.MATH_CLOSE_PREC): order['amount'] = new_amount order.pop('filled', None) - trade.recalc_open_trade_price() + trade.recalc_open_trade_value() except DependencyException as exception: logger.warning("Could not update trade amount: %s", exception) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 6027908da..67871f96b 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -217,7 +217,7 @@ class Trade(_DECL_BASE): fee_close_currency = Column(String, nullable=True) open_rate = Column(Float) open_rate_requested = Column(Float) - # open_trade_price - calculated via _calc_open_trade_price + # open_trade_price - calculated via _calc_open_trade_value open_trade_price = Column(Float) close_rate = Column(Float) close_rate_requested = Column(Float) @@ -252,7 +252,7 @@ class Trade(_DECL_BASE): def __init__(self, **kwargs): super().__init__(**kwargs) - self.recalc_open_trade_price() + self.recalc_open_trade_value() def __repr__(self): open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed' @@ -284,7 +284,7 @@ class Trade(_DECL_BASE): 'open_timestamp': int(self.open_date.replace(tzinfo=timezone.utc).timestamp() * 1000), 'open_rate': self.open_rate, 'open_rate_requested': self.open_rate_requested, - 'open_trade_price': round(self.open_trade_price, 8), + 'open_trade_value': round(self.open_trade_price, 8), 'close_date_hum': (arrow.get(self.close_date).humanize() if self.close_date else None), @@ -389,7 +389,7 @@ class Trade(_DECL_BASE): # Update open rate and actual amount self.open_rate = Decimal(safe_value_fallback(order, 'average', 'price')) self.amount = Decimal(safe_value_fallback(order, 'filled', 'amount')) - self.recalc_open_trade_price() + self.recalc_open_trade_value() if self.is_open: logger.info(f'{order_type.upper()}_BUY has been fulfilled for {self}.') self.open_order_id = None @@ -464,7 +464,7 @@ class Trade(_DECL_BASE): Trade.session.delete(self) Trade.session.flush() - def _calc_open_trade_price(self) -> float: + def _calc_open_trade_value(self) -> float: """ Calculate the open_rate including open_fee. :return: Price in of the open trade incl. Fees @@ -473,14 +473,14 @@ class Trade(_DECL_BASE): fees = buy_trade * Decimal(self.fee_open) return float(buy_trade + fees) - def recalc_open_trade_price(self) -> None: + def recalc_open_trade_value(self) -> None: """ - Recalculate open_trade_price. + Recalculate open_trade_value. 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_value() - def calc_close_trade_price(self, rate: Optional[float] = None, + def calc_close_trade_value(self, rate: Optional[float] = None, fee: Optional[float] = None) -> float: """ Calculate the close_rate including fee @@ -507,7 +507,7 @@ class Trade(_DECL_BASE): 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( + close_trade_price = self.calc_close_trade_value( rate=(rate or self.close_rate), fee=(fee or self.fee_close) ) @@ -523,7 +523,7 @@ class Trade(_DECL_BASE): :param fee: fee to use on the close rate (optional). :return: profit ratio as float """ - close_trade_price = self.calc_close_trade_price( + close_trade_price = self.calc_close_trade_value( rate=(rate or self.close_rate), fee=(fee or self.fee_close) ) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 47e0f763d..4b36f4b4e 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -62,7 +62,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'fee_close_cost': ANY, 'fee_close_currency': ANY, 'open_rate_requested': ANY, - 'open_trade_price': 0.0010025, + 'open_trade_value': 0.0010025, 'close_rate_requested': ANY, 'sell_reason': ANY, 'sell_order_status': ANY, @@ -127,7 +127,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'fee_close_cost': ANY, 'fee_close_currency': ANY, 'open_rate_requested': ANY, - 'open_trade_price': ANY, + 'open_trade_value': ANY, 'close_rate_requested': ANY, 'sell_reason': ANY, 'sell_order_status': ANY, diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 0dc43474f..8e5a66998 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -678,7 +678,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'min_rate': 1.098e-05, 'open_order_id': None, 'open_rate_requested': 1.098e-05, - 'open_trade_price': 0.0010025, + 'open_trade_value': 0.0010025, 'sell_reason': None, 'sell_order_status': None, 'strategy': 'DefaultStrategy', @@ -805,7 +805,7 @@ def test_api_forcebuy(botclient, mocker, fee): 'min_rate': None, 'open_order_id': '123456', 'open_rate_requested': None, - 'open_trade_price': 0.24605460, + 'open_trade_value': 0.24605460, 'sell_reason': None, 'sell_order_status': None, 'strategy': None, diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 41b99b34f..a7ac8ed94 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -177,10 +177,10 @@ 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_value() == 0.0010024999999225068 trade.update(limit_sell_order) - assert trade.calc_close_trade_price() == 0.0010646656050132426 + assert trade.calc_close_trade_value() == 0.0010646656050132426 # Profit in BTC assert trade.calc_profit() == 0.00006217 @@ -233,7 +233,7 @@ def test_calc_close_trade_price_exception(limit_buy_order, fee): trade.open_order_id = 'something' trade.update(limit_buy_order) - assert trade.calc_close_trade_price() == 0.0 + assert trade.calc_close_trade_value() == 0.0 @pytest.mark.usefixtures("init_persistence") @@ -277,7 +277,7 @@ def test_update_invalid_order(limit_buy_order): @pytest.mark.usefixtures("init_persistence") -def test_calc_open_trade_price(limit_buy_order, fee): +def test_calc_open_trade_value(limit_buy_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, @@ -291,10 +291,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_value() == 0.0010024999999225068 trade.fee_open = 0.003 # Get the open rate price with a custom fee rate - assert trade._calc_open_trade_price() == 0.001002999999922468 + assert trade._calc_open_trade_value() == 0.001002999999922468 @pytest.mark.usefixtures("init_persistence") @@ -312,14 +312,14 @@ def test_calc_close_trade_price(limit_buy_order, limit_sell_order, fee): trade.update(limit_buy_order) # Buy @ 0.00001099 # Get the close rate price with a custom close rate and a regular fee rate - assert trade.calc_close_trade_price(rate=0.00001234) == 0.0011200318470471794 + assert trade.calc_close_trade_value(rate=0.00001234) == 0.0011200318470471794 # Get the close rate price with a custom close rate and a custom fee rate - assert trade.calc_close_trade_price(rate=0.00001234, fee=0.003) == 0.0011194704275749754 + assert trade.calc_close_trade_value(rate=0.00001234, fee=0.003) == 0.0011194704275749754 # Test when we apply a Sell order, and ask price with a custom fee rate trade.update(limit_sell_order) - assert trade.calc_close_trade_price(fee=0.005) == 0.0010619972701635854 + assert trade.calc_close_trade_value(fee=0.005) == 0.0010619972701635854 @pytest.mark.usefixtures("init_persistence") @@ -499,7 +499,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 == trade._calc_open_trade_price() + assert trade.open_trade_price == trade._calc_open_trade_value() assert trade.close_profit_abs is None assert trade.fee_open_cost is None assert trade.fee_open_currency is None @@ -607,7 +607,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 for trades - backup: trades_bak2", caplog) - assert trade.open_trade_price == trade._calc_open_trade_price() + assert trade.open_trade_price == trade._calc_open_trade_value() assert trade.close_profit_abs is None assert log_has("Moving open orders to Orders table.", caplog) @@ -677,7 +677,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 trade.open_trade_price == trade._calc_open_trade_value() assert log_has("trying trades_bak0", caplog) assert log_has("Running database migration for trades - backup: trades_bak0", caplog) @@ -803,7 +803,7 @@ def test_to_json(default_conf, fee): 'close_timestamp': None, 'open_rate': 0.123, 'open_rate_requested': None, - 'open_trade_price': 15.1668225, + 'open_trade_value': 15.1668225, 'fee_close': 0.0025, 'fee_close_cost': None, 'fee_close_currency': None, @@ -896,7 +896,7 @@ def test_to_json(default_conf, fee): 'min_rate': None, 'open_order_id': None, 'open_rate_requested': None, - 'open_trade_price': 12.33075, + 'open_trade_value': 12.33075, 'sell_reason': None, 'sell_order_status': None, 'strategy': None, From 95fd3824daa02d6889b7eaf636a51397d37f0f59 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 10 Dec 2020 19:36:52 +0100 Subject: [PATCH 1112/1197] Finish renamal of open_trade_price to open_value --- freqtrade/persistence/migrations.py | 10 +++++----- freqtrade/persistence/models.py | 16 ++++++++-------- tests/test_persistence.py | 6 +++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 84f3ed7e6..ed976c2a9 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -53,11 +53,11 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col else: timeframe = get_column_def(cols, 'timeframe', 'null') - open_trade_price = get_column_def(cols, 'open_trade_price', + open_trade_value = get_column_def(cols, 'open_trade_value', f'amount * open_rate * (1 + {fee_open})') close_profit_abs = get_column_def( cols, 'close_profit_abs', - f"(amount * close_rate * (1 - {fee_close})) - {open_trade_price}") + f"(amount * close_rate * (1 - {fee_close})) - {open_trade_value}") sell_order_status = get_column_def(cols, 'sell_order_status', 'null') amount_requested = get_column_def(cols, 'amount_requested', 'amount') @@ -79,7 +79,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stoploss_order_id, stoploss_last_update, max_rate, min_rate, sell_reason, sell_order_status, strategy, - timeframe, open_trade_price, close_profit_abs + timeframe, open_trade_value, close_profit_abs ) select id, lower(exchange), case @@ -102,7 +102,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, {sell_order_status} sell_order_status, {strategy} strategy, {timeframe} timeframe, - {open_trade_price} open_trade_price, {close_profit_abs} close_profit_abs + {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs from {table_back_name} """) @@ -134,7 +134,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None: table_back_name = get_backup_name(tabs, 'trades_bak') # Check for latest column - if not has_column(cols, 'amount_requested'): + if not has_column(cols, 'open_trade_value'): logger.info(f'Running database migration for trades - backup: {table_back_name}') migrate_trades_table(decl_base, inspector, engine, table_back_name, cols) # Reread columns - the above recreated the table! diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 67871f96b..06dd785e8 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -217,8 +217,8 @@ class Trade(_DECL_BASE): fee_close_currency = Column(String, nullable=True) open_rate = Column(Float) open_rate_requested = Column(Float) - # open_trade_price - calculated via _calc_open_trade_value - open_trade_price = Column(Float) + # open_trade_value - calculated via _calc_open_trade_value + open_trade_value = Column(Float) close_rate = Column(Float) close_rate_requested = Column(Float) close_profit = Column(Float) @@ -284,7 +284,7 @@ class Trade(_DECL_BASE): 'open_timestamp': int(self.open_date.replace(tzinfo=timezone.utc).timestamp() * 1000), 'open_rate': self.open_rate, 'open_rate_requested': self.open_rate_requested, - 'open_trade_value': round(self.open_trade_price, 8), + 'open_trade_value': round(self.open_trade_value, 8), 'close_date_hum': (arrow.get(self.close_date).humanize() if self.close_date else None), @@ -478,7 +478,7 @@ class Trade(_DECL_BASE): Recalculate open_trade_value. Must be called whenever open_rate or fee_open is changed. """ - self.open_trade_price = self._calc_open_trade_value() + self.open_trade_value = self._calc_open_trade_value() def calc_close_trade_value(self, rate: Optional[float] = None, fee: Optional[float] = None) -> float: @@ -507,11 +507,11 @@ class Trade(_DECL_BASE): 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_value( + close_trade_value = self.calc_close_trade_value( rate=(rate or self.close_rate), fee=(fee or self.fee_close) ) - profit = close_trade_price - self.open_trade_price + profit = close_trade_value - self.open_trade_value return float(f"{profit:.8f}") def calc_profit_ratio(self, rate: Optional[float] = None, @@ -523,11 +523,11 @@ class Trade(_DECL_BASE): :param fee: fee to use on the close rate (optional). :return: profit ratio as float """ - close_trade_price = self.calc_close_trade_value( + close_trade_value = self.calc_close_trade_value( rate=(rate or self.close_rate), fee=(fee or self.fee_close) ) - profit_ratio = (close_trade_price / self.open_trade_price) - 1 + profit_ratio = (close_trade_value / self.open_trade_value) - 1 return float(f"{profit_ratio:.8f}") def select_order(self, order_side: str, is_open: Optional[bool]) -> Optional[Order]: diff --git a/tests/test_persistence.py b/tests/test_persistence.py index a7ac8ed94..7487b2ef5 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -499,7 +499,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 == trade._calc_open_trade_value() + assert trade.open_trade_value == trade._calc_open_trade_value() assert trade.close_profit_abs is None assert trade.fee_open_cost is None assert trade.fee_open_currency is None @@ -607,7 +607,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 for trades - backup: trades_bak2", caplog) - assert trade.open_trade_price == trade._calc_open_trade_value() + assert trade.open_trade_value == trade._calc_open_trade_value() assert trade.close_profit_abs is None assert log_has("Moving open orders to Orders table.", caplog) @@ -677,7 +677,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_value() + assert trade.open_trade_value == trade._calc_open_trade_value() assert log_has("trying trades_bak0", caplog) assert log_has("Running database migration for trades - backup: trades_bak0", caplog) From 6107878f4e573b6f0b00eb56f9d0704408a2a806 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 12 Dec 2020 07:08:29 +0100 Subject: [PATCH 1113/1197] Bump ccxt to 1.39.10 closes #4051 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 105839f0d..e0c5ac072 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.4 pandas==1.1.4 -ccxt==1.38.87 +ccxt==1.39.10 aiohttp==3.7.3 SQLAlchemy==1.3.20 python-telegram-bot==13.1 From b45c2fb1d015847ba0b3fbe1c8059d7267e99d6a Mon Sep 17 00:00:00 2001 From: Samaoo Date: Sat, 12 Dec 2020 10:27:17 +0100 Subject: [PATCH 1114/1197] Update backtesting.md --- docs/backtesting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index de54c4c91..27bfebe37 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -285,7 +285,7 @@ Since backtesting lacks some detailed information about what happens within a ca - 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, but the loss will be 0.32% lower than the stoploss price +- Stoploss sells happen exactly at stoploss price, even if low was lower, but the loss will be `2 * fees` higher than the stoploss price - Stoploss is evaluated before ROI within one candle. So you can often see more trades with the `stoploss` sell reason comparing to the results obtained with the same strategy in the Dry Run/Live Trade modes - Low happens before high for stoploss, protecting capital first - Trailing stoploss From 181b88dc753354b34ca65162541823e1bf7b5258 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 12 Dec 2020 10:52:27 +0100 Subject: [PATCH 1115/1197] Don't accept too high fees, assuming they are erroneous Forces fallback to "detection from trades" --- freqtrade/freqtradebot.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ada9889a6..c86fb616b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1448,13 +1448,16 @@ class FreqtradeBot: fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(order) logger.info(f"Fee for Trade {trade} [{order.get('side')}]: " f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}") - - trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', '')) - if trade_base_currency == fee_currency: - # Apply fee to amount - return self.apply_fee_conditional(trade, trade_base_currency, - amount=order_amount, fee_abs=fee_cost) - return order_amount + if fee_rate is None or fee_rate < 0.02: + # Reject all fees that report as > 2%. + # These are most likely caused by a parsing bug in ccxt + # due to multiple trades (https://github.com/ccxt/ccxt/issues/8025) + trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', '')) + if trade_base_currency == fee_currency: + # Apply fee to amount + return self.apply_fee_conditional(trade, trade_base_currency, + amount=order_amount, fee_abs=fee_cost) + return order_amount return self.fee_detection_from_trades(trade, order, order_amount) def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float) -> float: From 3ee7fe64ba3e957ff5110f07b06c3f7265060a1e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 12 Dec 2020 11:25:56 +0100 Subject: [PATCH 1116/1197] Clean up some tests --- tests/conftest.py | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 079a521ed..e2e4788b6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1588,16 +1588,7 @@ def fetch_trades_result(): @pytest.fixture(scope="function") def trades_for_order2(): - return [{'info': {'id': 34567, - 'orderId': 123456, - 'price': '0.24544100', - 'qty': '8.00000000', - 'commission': '0.00800000', - 'commissionAsset': 'LTC', - 'time': 1521663363189, - 'isBuyer': True, - 'isMaker': False, - 'isBestMatch': True}, + return [{'info': {}, 'timestamp': 1521663363189, 'datetime': '2018-03-21T20:16:03.189Z', 'symbol': 'LTC/ETH', @@ -1609,16 +1600,7 @@ def trades_for_order2(): 'cost': 1.963528, 'amount': 4.0, 'fee': {'cost': 0.004, 'currency': 'LTC'}}, - {'info': {'id': 34567, - 'orderId': 123456, - 'price': '0.24544100', - 'qty': '8.00000000', - 'commission': '0.00800000', - 'commissionAsset': 'LTC', - 'time': 1521663363189, - 'isBuyer': True, - 'isMaker': False, - 'isBestMatch': True}, + {'info': {}, 'timestamp': 1521663363189, 'datetime': '2018-03-21T20:16:03.189Z', 'symbol': 'LTC/ETH', From 14647fb5f08254d4b85c528f59db66c697c43d83 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 12 Dec 2020 11:43:47 +0100 Subject: [PATCH 1117/1197] Add tests for update fee --- tests/conftest.py | 8 +++++++ tests/test_freqtradebot.py | 44 +++++++++++++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index e2e4788b6..5d358f015 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1614,6 +1614,14 @@ def trades_for_order2(): 'fee': {'cost': 0.004, 'currency': 'LTC'}}] +@pytest.fixture(scope="function") +def trades_for_order3(trades_for_order2): + # Different fee currencies for each trade + trades_for_order = deepcopy(trades_for_order2) + trades_for_order[0]['fee'] = {'cost': 0.02, 'currency': 'BNB'} + return trades_for_order + + @pytest.fixture def buy_order_fee(): return { diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 6adef510f..459a09c0c 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3718,6 +3718,48 @@ def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, c 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.992).', caplog) + assert trade.fee_open == 0.001 + assert trade.fee_close == 0.001 + assert trade.fee_open_cost is not None + assert trade.fee_open_currency is not None + assert trade.fee_close_cost is None + assert trade.fee_close_currency is None + + +def test_get_real_amount_multi2(default_conf, trades_for_order3, buy_order_fee, caplog, fee, + mocker, markets): + # Different fee currency on both trades + mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order3) + amount = float(sum(x['amount'] for x in trades_for_order3)) + default_conf['stake_currency'] = 'ETH' + trade = Trade( + 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" + ) + # Fake markets entry to enable fee parsing + markets['BNB/ETH'] = markets['ETH/BTC'] + freqtrade = get_patched_freqtradebot(mocker, default_conf) + mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', + return_value={'ask': 0.19, 'last': 0.2}) + + # Amount is reduced by "fee" + assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.0005) + assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' + 'open_rate=0.24544100, open_since=closed) (from 8.0 to 7.996).', + caplog) + # Overall fee is average of both trade's fee + assert trade.fee_open == 0.001518575 + assert trade.fee_open_cost is not None + assert trade.fee_open_currency is not None + assert trade.fee_close_cost is None + assert trade.fee_close_currency is None + def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee, fee, caplog, mocker): @@ -4264,7 +4306,7 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee): freqtrade = get_patched_freqtradebot(mocker, default_conf) def patch_with_fee(order): - order.update({'fee': {'cost': 0.1, 'rate': 0.2, + order.update({'fee': {'cost': 0.1, 'rate': 0.01, 'currency': order['symbol'].split('/')[0]}}) return order From 8a2fbf65923283efd0db0c61c316cc5df193089f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Dec 2020 10:15:16 +0100 Subject: [PATCH 1118/1197] Small cleanup of protection stuff --- docs/includes/protections.md | 12 ++++++------ freqtrade/resolvers/protection_resolver.py | 2 -- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/includes/protections.md b/docs/includes/protections.md index 7378a590c..87db17fd8 100644 --- a/docs/includes/protections.md +++ b/docs/includes/protections.md @@ -26,12 +26,12 @@ All protection end times are rounded up to the next candle to avoid sudden, unex | Parameter| Description | |------------|-------------| -| method | Protection name to use.
    **Datatype:** String, selected from [available Protections](#available-protections) -| stop_duration_candles | For how many candles should the lock be set?
    **Datatype:** Positive integer (in candles) -| stop_duration | how many minutes should protections be locked.
    Cannot be used together with `stop_duration_candles`.
    **Datatype:** Float (in minutes) +| `method` | Protection name to use.
    **Datatype:** String, selected from [available Protections](#available-protections) +| `stop_duration_candles` | For how many candles should the lock be set?
    **Datatype:** Positive integer (in candles) +| `stop_duration` | how many minutes should protections be locked.
    Cannot be used together with `stop_duration_candles`.
    **Datatype:** Float (in minutes) | `lookback_period_candles` | Only trades that completed within the last `lookback_period_candles` candles will be considered. This setting may be ignored by some Protections.
    **Datatype:** Positive integer (in candles). -| lookback_period | Only trades that completed after `current_time - lookback_period` will be considered.
    Cannot be used together with `lookback_period_candles`.
    This setting may be ignored by some Protections.
    **Datatype:** Float (in minutes) -| trade_limit | Number of trades required at minimum (not used by all Protections).
    **Datatype:** Positive integer +| `lookback_period` | Only trades that completed after `current_time - lookback_period` will be considered.
    Cannot be used together with `lookback_period_candles`.
    This setting may be ignored by some Protections.
    **Datatype:** Float (in minutes) +| `trade_limit` | Number of trades required at minimum (not used by all Protections).
    **Datatype:** Positive integer !!! Note "Durations" Durations (`stop_duration*` and `lookback_period*` can be defined in either minutes or candles). @@ -108,7 +108,7 @@ The below example will stop trading a pair for 2 candles after closing a trade, "protections": [ { "method": "CooldownPeriod", - "stop_duration_candle": 2 + "stop_duration_candles": 2 } ], ``` diff --git a/freqtrade/resolvers/protection_resolver.py b/freqtrade/resolvers/protection_resolver.py index 928bd4633..c54ae1011 100644 --- a/freqtrade/resolvers/protection_resolver.py +++ b/freqtrade/resolvers/protection_resolver.py @@ -1,5 +1,3 @@ -# pragma pylint: disable=attribute-defined-outside-init - """ This module load custom pairlists """ From 9cd1be8f93f59b332d6317fc777033343ce39036 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Dec 2020 10:33:45 +0100 Subject: [PATCH 1119/1197] Update usage of open_trade_price to open_trade_value --- tests/plugins/test_protections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 82b6e4500..e36900a96 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -29,7 +29,7 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool, amount=0.01 / open_rate, exchange='bittrex', ) - trade.recalc_open_trade_price() + trade.recalc_open_trade_value() if not is_open: trade.close(open_rate * profit_rate) trade.sell_reason = sell_reason From 657b002a8197b2a949ac423f7207a379db5bd787 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Dec 2020 10:59:29 +0100 Subject: [PATCH 1120/1197] Explicitly check for False in fetch_ticker --- freqtrade/exchange/exchange.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 611ce4abd..7f763610c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -658,7 +658,8 @@ class Exchange: @retrier def fetch_ticker(self, pair: str) -> dict: try: - if pair not in self._api.markets or not self._api.markets[pair].get('active'): + if (pair not in self._api.markets or + self._api.markets[pair].get('active', False) is False): raise ExchangeError(f"Pair {pair} not available") data = self._api.fetch_ticker(pair) return data From a4bfd0b0aa6556e47484fc67a675170b9ddd760e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Dec 2020 11:25:42 +0100 Subject: [PATCH 1121/1197] Split linux and OSX builds into 2 seperate, parallel jobs --- .github/workflows/ci.yml | 102 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 94 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d48dec2d3..36a9fc374 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,12 +14,12 @@ on: - cron: '0 5 * * 4' jobs: - build: + build_linux: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ ubuntu-18.04, ubuntu-20.04, macos-latest ] + os: [ ubuntu-18.04, ubuntu-20.04 ] python-version: [3.7, 3.8, 3.9] steps: @@ -31,21 +31,105 @@ jobs: python-version: ${{ matrix.python-version }} - name: Cache_dependencies - uses: actions/cache@v1 + uses: actions/cache@v2 id: cache with: path: ~/dependencies/ key: ${{ runner.os }}-dependencies - name: pip cache (linux) - uses: actions/cache@preview + uses: actions/cache@v2 if: startsWith(matrix.os, 'ubuntu') with: path: ~/.cache/pip key: test-${{ matrix.os }}-${{ matrix.python-version }}-pip + - name: TA binary *nix + if: steps.cache.outputs.cache-hit != 'true' + run: | + cd build_helpers && ./install_ta-lib.sh ${HOME}/dependencies/; cd .. + + - name: Installation - *nix + run: | + python -m pip install --upgrade pip + export LD_LIBRARY_PATH=${HOME}/dependencies/lib:$LD_LIBRARY_PATH + export TA_LIBRARY_PATH=${HOME}/dependencies/lib + export TA_INCLUDE_PATH=${HOME}/dependencies/include + pip install -r requirements-dev.txt + pip install -e . + + - name: Tests + run: | + pytest --random-order --cov=freqtrade --cov-config=.coveragerc + + - name: Coveralls + if: (startsWith(matrix.os, 'ubuntu-20') && matrix.python-version == '3.8') + env: + # Coveralls token. Not used as secret due to github not providing secrets to forked repositories + COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu + run: | + # Allow failure for coveralls + coveralls -v || true + + - name: Backtesting + run: | + cp config.json.example config.json + freqtrade create-userdir --userdir user_data + freqtrade backtesting --datadir tests/testdata --strategy SampleStrategy + + - name: Hyperopt + run: | + cp config.json.example config.json + freqtrade create-userdir --userdir user_data + freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpt --hyperopt-loss SharpeHyperOptLossDaily --print-all + + - name: Flake8 + run: | + flake8 + + - name: Sort imports (isort) + run: | + isort --check . + + - name: Mypy + run: | + mypy freqtrade scripts + + - name: Slack Notification + uses: homoluctus/slatify@v1.8.0 + if: failure() && ( github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) + with: + type: ${{ job.status }} + job_name: '*Freqtrade CI ${{ matrix.os }}*' + mention: 'here' + mention_if: 'failure' + channel: '#notifications' + url: ${{ secrets.SLACK_WEBHOOK }} + + build_macos: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ macos-latest ] + python-version: [3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Cache_dependencies + uses: actions/cache@v2 + id: cache + with: + path: ~/dependencies/ + key: ${{ runner.os }}-dependencies + - name: pip cache (macOS) - uses: actions/cache@preview + uses: actions/cache@v2 if: startsWith(matrix.os, 'macOS') with: path: ~/Library/Caches/pip @@ -113,13 +197,14 @@ jobs: channel: '#notifications' url: ${{ secrets.SLACK_WEBHOOK }} + build_windows: runs-on: ${{ matrix.os }} strategy: matrix: os: [ windows-latest ] - python-version: [3.7, 3.8, 3.9] + python-version: [3.7, 3.8] steps: - uses: actions/checkout@v2 @@ -215,7 +300,7 @@ jobs: # Notify on slack only once - when CI completes (and after deploy) in case it's successfull notify-complete: - needs: [ build, build_windows, docs_check ] + needs: [ build_linux, build_macos, build_windows, docs_check ] runs-on: ubuntu-20.04 steps: - name: Slack Notification @@ -228,8 +313,9 @@ jobs: url: ${{ secrets.SLACK_WEBHOOK }} deploy: - needs: [ build, build_windows, docs_check ] + needs: [ build_linux, build_macos, build_windows, docs_check ] runs-on: ubuntu-20.04 + if: (github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'release') && github.repository == 'freqtrade/freqtrade' steps: - uses: actions/checkout@v2 From dad427461d88ea70697f42b91c136ba9284b8f6e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 13 Dec 2020 13:11:04 +0100 Subject: [PATCH 1122/1197] Downgrade dockerfile to 3.8.6 to avoid image bloat --- Dockerfile | 2 +- docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 445f909b0..602e6a28c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.9.1-slim-buster as base +FROM python:3.8.6-slim-buster as base # Setup env ENV LANG C.UTF-8 diff --git a/docker-compose.yml b/docker-compose.yml index a99aac3c7..7094500b4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,7 +9,7 @@ services: # Build step - only needed when additional dependencies are needed # build: # context: . - # dockerfile: "./Dockerfile.technical" + # dockerfile: "./docker/Dockerfile.technical" restart: unless-stopped container_name: freqtrade volumes: From 3bea9255e78bb04bf1fad25e165513b322acb0d0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Dec 2020 05:43:33 +0000 Subject: [PATCH 1123/1197] Bump cachetools from 4.1.1 to 4.2.0 Bumps [cachetools](https://github.com/tkem/cachetools) from 4.1.1 to 4.2.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/v4.1.1...v4.2.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e0c5ac072..a30c5e8b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,7 @@ aiohttp==3.7.3 SQLAlchemy==1.3.20 python-telegram-bot==13.1 arrow==0.17.0 -cachetools==4.1.1 +cachetools==4.2.0 requests==2.25.0 urllib3==1.26.2 wrapt==1.12.1 From 4cf16fa8d1e3299857903aa4486c2a4d5efe5102 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Dec 2020 05:43:33 +0000 Subject: [PATCH 1124/1197] Bump plotly from 4.13.0 to 4.14.1 Bumps [plotly](https://github.com/plotly/plotly.py) from 4.13.0 to 4.14.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.13.0...v4.14.1) Signed-off-by: dependabot[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index 1c3b03133..3e31a24ae 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.13.0 +plotly==4.14.1 From a3139dd9d416e26163cab5aedd392d0e674664e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Dec 2020 05:43:34 +0000 Subject: [PATCH 1125/1197] Bump flake8-tidy-imports from 4.1.0 to 4.2.0 Bumps [flake8-tidy-imports](https://github.com/adamchainz/flake8-tidy-imports) from 4.1.0 to 4.2.0. - [Release notes](https://github.com/adamchainz/flake8-tidy-imports/releases) - [Changelog](https://github.com/adamchainz/flake8-tidy-imports/blob/master/HISTORY.rst) - [Commits](https://github.com/adamchainz/flake8-tidy-imports/compare/4.1.0...4.2.0) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e681274c8..97bb7a12f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,7 +6,7 @@ coveralls==2.2.0 flake8==3.8.4 flake8-type-annotations==0.1.0 -flake8-tidy-imports==4.1.0 +flake8-tidy-imports==4.2.0 mypy==0.790 pytest==6.1.2 pytest-asyncio==0.14.0 From bdd895b8dacddf59edfc91d9356228075aa1ccb5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Dec 2020 05:43:42 +0000 Subject: [PATCH 1126/1197] Bump pandas from 1.1.4 to 1.1.5 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.1.4 to 1.1.5. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.1.4...v1.1.5) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e0c5ac072..47fd5006b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ numpy==1.19.4 -pandas==1.1.4 +pandas==1.1.5 ccxt==1.39.10 aiohttp==3.7.3 From 44f295110bc10875bc4b571d92d485aca5d59c8c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Dec 2020 05:43:46 +0000 Subject: [PATCH 1127/1197] Bump python-rapidjson from 0.9.4 to 1.0 Bumps [python-rapidjson](https://github.com/python-rapidjson/python-rapidjson) from 0.9.4 to 1.0. - [Release notes](https://github.com/python-rapidjson/python-rapidjson/releases) - [Changelog](https://github.com/python-rapidjson/python-rapidjson/blob/master/CHANGES.rst) - [Commits](https://github.com/python-rapidjson/python-rapidjson/compare/v0.9.4...v1.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e0c5ac072..ae09adabd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -22,7 +22,7 @@ blosc==1.9.2 py_find_1st==1.1.4 # Load ticker files 30% faster -python-rapidjson==0.9.4 +python-rapidjson==1.0 # Notify systemd sdnotify==0.3.2 From a9b586d338ca2f9bb86a0e03ff8398b8d241d39d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Dec 2020 05:44:16 +0000 Subject: [PATCH 1128/1197] Bump ccxt from 1.39.10 to 1.39.33 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.39.10 to 1.39.33. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.39.10...1.39.33) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e0c5ac072..358238ea6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.4 pandas==1.1.4 -ccxt==1.39.10 +ccxt==1.39.33 aiohttp==3.7.3 SQLAlchemy==1.3.20 python-telegram-bot==13.1 From 8965b8a18d7390929b38d33f829f5ffdf87772b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Dec 2020 08:27:36 +0000 Subject: [PATCH 1129/1197] Bump pytest from 6.1.2 to 6.2.0 Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.1.2 to 6.2.0. - [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/6.1.2...6.2.0) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 97bb7a12f..6d7570f67 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.8.4 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.0 mypy==0.790 -pytest==6.1.2 +pytest==6.2.0 pytest-asyncio==0.14.0 pytest-cov==2.10.1 pytest-mock==3.3.1 From ba869a330f5b8e707cfa1419c0a36ce31147c71c Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Dec 2020 19:05:41 +0100 Subject: [PATCH 1130/1197] Build 3.6 on github actions too --- .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 f259129d4..9239b83ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: os: [ ubuntu-18.04, ubuntu-20.04, macos-latest ] - python-version: [3.7, 3.8] + python-version: [3.6, 3.7, 3.8] steps: - uses: actions/checkout@v2 From 66d5271adae8db286ab67f9821638eabdc39e547 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Dec 2020 19:10:24 +0100 Subject: [PATCH 1131/1197] Don't build for 3.6 any longer --- .github/workflows/ci.yml | 2 +- .travis.yml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9239b83ee..f259129d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: strategy: matrix: os: [ ubuntu-18.04, ubuntu-20.04, macos-latest ] - python-version: [3.6, 3.7, 3.8] + python-version: [3.7, 3.8] steps: - uses: actions/checkout@v2 diff --git a/.travis.yml b/.travis.yml index 9b8448db5..b61efe678 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,10 @@ os: - linux -dist: xenial +dist: bionic language: python python: -- 3.6 +- 3.7 +- 3.8 services: - docker env: From 9f5c4ead15993e67902c094109b3163a3626eafb Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 14 Dec 2020 19:18:54 +0100 Subject: [PATCH 1132/1197] Remove support for 3.6 --- .readthedocs.yml | 4 ++-- .travis.yml | 1 - README.md | 4 ++-- docs/index.md | 4 ++-- docs/installation.md | 8 ++++---- environment.yml | 2 +- freqtrade/__main__.py | 2 +- freqtrade/main.py | 4 ++-- setup.py | 5 ++--- setup.sh | 19 ++++++++++--------- 10 files changed, 26 insertions(+), 27 deletions(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index dec7b44d7..446181452 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -4,5 +4,5 @@ build: image: latest python: - version: 3.6 - setup_py_install: false \ No newline at end of file + version: 3.8 + setup_py_install: false diff --git a/.travis.yml b/.travis.yml index b61efe678..94239e33f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ os: dist: bionic language: python python: -- 3.7 - 3.8 services: - docker diff --git a/README.md b/README.md index 8526b5c91..a9aee342f 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Please find the complete documentation on our [website](https://www.freqtrade.io ## Features -- [x] **Based on Python 3.6+**: For botting on any operating system - Windows, macOS and Linux. +- [x] **Based on Python 3.7+**: For botting on any operating system - Windows, macOS and Linux. - [x] **Persistence**: Persistence is achieved through sqlite. - [x] **Dry-run**: Run the bot without playing money. - [x] **Backtesting**: Run a simulation of your buy/sell strategy. @@ -187,7 +187,7 @@ To run this bot we recommend you a cloud instance with a minimum of: ### Software requirements -- [Python 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/) +- [Python 3.7.x](http://docs.python-guide.org/en/latest/starting/installation/) - [pip](https://pip.pypa.io/en/stable/installing/) - [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) - [TA-Lib](https://mrjbq7.github.io/ta-lib/install.html) diff --git a/docs/index.md b/docs/index.md index f63aeb6b8..e6882263b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,7 +14,7 @@ ## Introduction -Freqtrade is a crypto-currency algorithmic trading software developed in python (3.6+) and supported on Windows, macOS and Linux. +Freqtrade is a crypto-currency algorithmic trading software developed in python (3.7+) and supported on Windows, macOS and Linux. !!! Danger "DISCLAIMER" This software is for educational purposes only. Do not risk money which you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS. @@ -51,7 +51,7 @@ To run this bot we recommend you a linux cloud instance with a minimum of: Alternatively -- Python 3.6.x +- Python 3.7+ - pip (pip3) - git - TA-Lib diff --git a/docs/installation.md b/docs/installation.md index f00bf0836..be98c45a8 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -10,7 +10,7 @@ Please consider using the prebuilt [docker images](docker.md) to get started qui Click each one for install guide: -* [Python >= 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/) +* [Python >= 3.7.x](http://docs.python-guide.org/en/latest/starting/installation/) * [pip](https://pip.pypa.io/en/stable/installing/) * [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) * [virtualenv](https://virtualenv.pypa.io/en/stable/installation.html) (Recommended) @@ -34,7 +34,7 @@ The easiest way to install and run Freqtrade is to clone the bot Github reposito When cloning the repository the default working branch has the name `develop`. This branch contains all last features (can be considered as relatively stable, thanks to automated tests). The `stable` branch contains the code of the last release (done usually once per month on an approximately one week old snapshot of the `develop` branch to prevent packaging bugs, so potentially it's more stable). !!! Note - Python3.6 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository. + Python3.7 or higher and the corresponding `pip` are assumed to be available. The install-script will warn you and stop if that's not the case. `git` is also needed to clone the Freqtrade repository. This can be achieved with the following commands: @@ -63,7 +63,7 @@ usage: ** --install ** With this option, the script will install the bot and most dependencies: -You will need to have git and python3.6+ installed beforehand for this to work. +You will need to have git and python3.7+ installed beforehand for this to work. * Mandatory software as: `ta-lib` * Setup your virtualenv under `.env/` @@ -94,7 +94,7 @@ We've included/collected install instructions for Ubuntu, MacOS, and Windows. Th OS Specific steps are listed first, the [Common](#common) section below is necessary for all systems. !!! Note - Python3.6 or higher and the corresponding pip are assumed to be available. + Python3.7 or higher and the corresponding pip are assumed to be available. === "Ubuntu/Debian" #### Install necessary dependencies diff --git a/environment.yml b/environment.yml index 86ea03519..746c4b912 100644 --- a/environment.yml +++ b/environment.yml @@ -4,7 +4,7 @@ channels: - conda-forge dependencies: # Required for app - - python>=3.6 + - python>=3.7 - pip - wheel - numpy diff --git a/freqtrade/__main__.py b/freqtrade/__main__.py index 881a2f562..ab4c7a110 100644 --- a/freqtrade/__main__.py +++ b/freqtrade/__main__.py @@ -3,7 +3,7 @@ __main__.py for Freqtrade To launch Freqtrade as a module -> python -m freqtrade (with Python >= 3.6) +> python -m freqtrade (with Python >= 3.7) """ from freqtrade import main diff --git a/freqtrade/main.py b/freqtrade/main.py index 5f8d5d19d..84d4b24f8 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -9,8 +9,8 @@ from typing import Any, List # check min. python version -if sys.version_info < (3, 6): - sys.exit("Freqtrade requires Python version >= 3.6") +if sys.version_info < (3, 7): + sys.exit("Freqtrade requires Python version >= 3.7") from freqtrade.commands import Arguments from freqtrade.exceptions import FreqtradeException, OperationalException diff --git a/setup.py b/setup.py index b47427709..030980c96 100644 --- a/setup.py +++ b/setup.py @@ -3,9 +3,9 @@ from sys import version_info from setuptools import setup -if version_info.major == 3 and version_info.minor < 6 or \ +if version_info.major == 3 and version_info.minor < 7 or \ version_info.major < 3: - print('Your Python interpreter must be 3.6 or greater!') + print('Your Python interpreter must be 3.7 or greater!') exit(1) from pathlib import Path # noqa: E402 @@ -109,7 +109,6 @@ setup(name='freqtrade', 'Environment :: Console', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Operating System :: MacOS', diff --git a/setup.sh b/setup.sh index af5e70691..d1686d2de 100755 --- a/setup.sh +++ b/setup.sh @@ -25,6 +25,14 @@ function check_installed_python() { return fi + which python3.9 + if [ $? -eq 0 ]; then + echo "using Python 3.9" + PYTHON=python3.9 + check_installed_pip + return + fi + which python3.7 if [ $? -eq 0 ]; then echo "using Python 3.7" @@ -33,16 +41,9 @@ function check_installed_python() { return fi - which python3.6 - if [ $? -eq 0 ]; then - echo "using Python 3.6" - PYTHON=python3.6 - check_installed_pip - return - fi if [ -z ${PYTHON} ]; then - echo "No usable python found. Please make sure to have python3.6 or python3.7 installed" + echo "No usable python found. Please make sure to have python3.7 or greater installed" exit 1 fi } @@ -302,7 +303,7 @@ function help() { echo " -p,--plot Install dependencies for Plotting scripts." } -# Verify if 3.6 or 3.7 is installed +# Verify if 3.7 or 3.8 is installed check_installed_python case $* in From dc92808335745045f556b89e91df1f33cc7a7400 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Dec 2020 06:44:08 +0100 Subject: [PATCH 1133/1197] Change PI dockerfile to use staged build --- Dockerfile.armhf | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 0633008ea..822da00a5 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -1,25 +1,40 @@ -FROM --platform=linux/arm/v7 python:3.7.7-slim-buster +FROM --platform=linux/arm/v7 python:3.7.9-slim-buster as base +# Setup env +ENV LANG C.UTF-8 +ENV LC_ALL C.UTF-8 +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONFAULTHANDLER 1 +ENV PATH=/root/.local/bin:$PATH + +# Prepare environment +RUN mkdir /freqtrade +WORKDIR /freqtrade + +# Install dependencies +FROM base as python-deps RUN apt-get update \ && apt-get -y install curl build-essential libssl-dev libffi-dev libatlas3-base libgfortran5 sqlite3 \ && apt-get clean \ && pip install --upgrade pip \ && echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf -# Prepare environment -RUN mkdir /freqtrade -WORKDIR /freqtrade - # Install TA-lib COPY build_helpers/* /tmp/ RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib* - ENV LD_LIBRARY_PATH /usr/local/lib # Install dependencies COPY requirements.txt /freqtrade/ -RUN pip install numpy --no-cache-dir \ - && pip install -r requirements.txt --no-cache-dir +RUN pip install --user --no-cache-dir numpy \ + && pip install --user --no-cache-dir -r requirements.txt + +# Copy dependencies to runtime-image +FROM base as runtime-image +COPY --from=python-deps /usr/local/lib /usr/local/lib +ENV LD_LIBRARY_PATH /usr/local/lib + +COPY --from=python-deps /root/.local /root/.local # Install and execute COPY . /freqtrade/ From 39fec25ae09c6ff727d953497bd211b1b18e2857 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Dec 2020 08:22:45 +0100 Subject: [PATCH 1134/1197] add optional Cache arguments to refresh_pairs method --- freqtrade/exchange/exchange.py | 24 ++++++++++++++++-------- tests/exchange/test_exchange.py | 12 ++++++++---- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 7f763610c..b61049c4e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -733,13 +733,17 @@ class Exchange: logger.info("Downloaded data for %s with length %s.", pair, len(data)) return data - def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes) -> List[Tuple[str, List]]: + def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *, + since_ms: Optional[int] = None, cache: bool = True + ) -> Dict[str, DataFrame]: """ Refresh in-memory OHLCV asynchronously and set `_klines` with the result Loops asynchronously over pair_list and downloads all pairs async (semi-parallel). Only used in the dataprovider.refresh() method. :param pair_list: List of 2 element tuples containing pair, interval to refresh - :return: TODO: return value is only used in the tests, get rid of it + :param since_ms: time since when to download, in milliseconds + :param cache: Assign result to _klines. Usefull for one-off downloads like for pairlists + :return: Dict of [{(pair, timeframe): Dataframe}] """ logger.debug("Refreshing candle (OHLCV) data for %d pairs", len(pair_list)) @@ -749,7 +753,8 @@ class Exchange: for pair, timeframe in set(pair_list): if (not ((pair, timeframe) in self._klines) or self._now_is_time_to_refresh(pair, timeframe)): - input_coroutines.append(self._async_get_candle_history(pair, timeframe)) + input_coroutines.append(self._async_get_candle_history(pair, timeframe, + since_ms=since_ms)) else: logger.debug( "Using cached candle (OHLCV) data for pair %s, timeframe %s ...", @@ -759,6 +764,7 @@ class Exchange: results = asyncio.get_event_loop().run_until_complete( asyncio.gather(*input_coroutines, return_exceptions=True)) + results_df = {} # handle caching for res in results: if isinstance(res, Exception): @@ -770,11 +776,13 @@ class Exchange: if ticks: self._pairs_last_refresh_time[(pair, timeframe)] = ticks[-1][0] // 1000 # keeping parsed dataframe in cache - self._klines[(pair, timeframe)] = ohlcv_to_dataframe( - ticks, timeframe, pair=pair, fill_missing=True, - drop_incomplete=self._ohlcv_partial_candle) - - return results + ohlcv_df = ohlcv_to_dataframe( + ticks, timeframe, pair=pair, fill_missing=True, + drop_incomplete=self._ohlcv_partial_candle) + results_df[(pair, timeframe)] = ohlcv_df + if cache: + self._klines[(pair, timeframe)] = ohlcv_df + return results_df def _now_is_time_to_refresh(self, pair: str, timeframe: str) -> bool: # Timeframe in seconds diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 42681b367..d8a846124 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1385,6 +1385,12 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None: pairs = [('IOTA/ETH', '5m'), ('XRP/ETH', '5m')] # empty dicts assert not exchange._klines + exchange.refresh_latest_ohlcv(pairs, cache=False) + # No caching + assert not exchange._klines + assert exchange._api_async.fetch_ohlcv.call_count == 2 + exchange._api_async.fetch_ohlcv.reset_mock() + exchange.refresh_latest_ohlcv(pairs) assert log_has(f'Refreshing candle (OHLCV) data for {len(pairs)} pairs', caplog) @@ -1499,11 +1505,9 @@ def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog): assert exchange._klines assert exchange._api_async.fetch_ohlcv.call_count == 2 - assert type(res) is list - assert len(res) == 2 + assert type(res) is dict + assert len(res) == 1 # Test that each is in list at least once as order is not guaranteed - assert type(res[0]) is tuple or type(res[1]) is tuple - assert type(res[0]) is TypeError or type(res[1]) is TypeError assert log_has("Error loading ETH/BTC. Result was [[]].", caplog) assert log_has("Async code raised an exception: TypeError", caplog) From 69901c131405c4e8262ddf0452e4b13dea7baf41 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Dec 2020 08:36:42 +0100 Subject: [PATCH 1135/1197] Provide pair to _validate_pairs in pairlists --- freqtrade/pairlist/AgeFilter.py | 5 +++-- freqtrade/pairlist/IPairList.py | 5 +++-- freqtrade/pairlist/PrecisionFilter.py | 9 +++++---- freqtrade/pairlist/PriceFilter.py | 11 ++++++----- freqtrade/pairlist/SpreadFilter.py | 7 ++++--- freqtrade/pairlist/rangestabilityfilter.py | 8 ++++---- 6 files changed, 25 insertions(+), 20 deletions(-) diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index ae2132637..f909014ba 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -49,11 +49,12 @@ class AgeFilter(IPairList): return (f"{self.name} - Filtering pairs with age less than " f"{self._min_days_listed} {plural(self._min_days_listed, 'day')}.") - def _validate_pair(self, ticker: Dict) -> bool: + def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool: """ Validate age for the ticker + :param pair: Pair that's currently validated :param ticker: ticker dict as returned from ccxt.load_markets() - :return: True if the pair can stay, False if it should be removed + :return: True if the pair can stay, false if it should be removed """ # Check symbol in cache diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/pairlist/IPairList.py index 5f29241ce..865aa90d6 100644 --- a/freqtrade/pairlist/IPairList.py +++ b/freqtrade/pairlist/IPairList.py @@ -60,13 +60,14 @@ class IPairList(LoggingMixin, ABC): -> Please overwrite in subclasses """ - def _validate_pair(self, ticker) -> bool: + def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool: """ Check one pair against Pairlist Handler's specific conditions. Either implement it in the Pairlist Handler or override the generic filter_pairlist() method. + :param pair: Pair that's currently validated :param ticker: ticker dict as returned from ccxt.load_markets() :return: True if the pair can stay, false if it should be removed """ @@ -109,7 +110,7 @@ class IPairList(LoggingMixin, ABC): # Copy list since we're modifying this list for p in deepcopy(pairlist): # Filter out assets - if not self._validate_pair(tickers[p]): + if not self._validate_pair(p, tickers[p] if p in tickers else {}): pairlist.remove(p) return pairlist diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py index db05d5883..c0d2893a1 100644 --- a/freqtrade/pairlist/PrecisionFilter.py +++ b/freqtrade/pairlist/PrecisionFilter.py @@ -43,19 +43,20 @@ class PrecisionFilter(IPairList): """ return f"{self.name} - Filtering untradable pairs." - def _validate_pair(self, ticker: dict) -> bool: + def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool: """ Check if pair has enough room to add a stoploss to avoid "unsellable" buys of very low value pairs. + :param pair: Pair that's currently validated :param ticker: ticker dict as returned from ccxt.load_markets() - :return: True if the pair can stay, False if it should be removed + :return: True if the pair can stay, false if it should be removed """ stop_price = ticker['ask'] * self._stoploss # Adjust stop-prices to precision - sp = self._exchange.price_to_precision(ticker["symbol"], stop_price) + sp = self._exchange.price_to_precision(pair, stop_price) - stop_gap_price = self._exchange.price_to_precision(ticker["symbol"], stop_price * 0.99) + stop_gap_price = self._exchange.price_to_precision(pair, stop_price * 0.99) logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}") if sp <= stop_gap_price: diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/pairlist/PriceFilter.py index 3686cd138..20a260b46 100644 --- a/freqtrade/pairlist/PriceFilter.py +++ b/freqtrade/pairlist/PriceFilter.py @@ -57,31 +57,32 @@ class PriceFilter(IPairList): return f"{self.name} - No price filters configured." - def _validate_pair(self, ticker) -> bool: + def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool: """ Check if if one price-step (pip) is > than a certain barrier. + :param pair: Pair that's currently validated :param ticker: ticker dict as returned from ccxt.load_markets() :return: True if the pair can stay, false if it should be removed """ if ticker['last'] is None or ticker['last'] == 0: - self.log_once(f"Removed {ticker['symbol']} from whitelist, because " + self.log_once(f"Removed {pair} from whitelist, because " "ticker['last'] is empty (Usually no trade in the last 24h).", logger.info) return False # Perform low_price_ratio check. if self._low_price_ratio != 0: - compare = self._exchange.price_get_one_pip(ticker['symbol'], ticker['last']) + compare = self._exchange.price_get_one_pip(pair, ticker['last']) changeperc = compare / ticker['last'] if changeperc > self._low_price_ratio: - self.log_once(f"Removed {ticker['symbol']} from whitelist, " + self.log_once(f"Removed {pair} from whitelist, " f"because 1 unit is {changeperc * 100:.3f}%", logger.info) return False # Perform min_price check. if self._min_price != 0: if ticker['last'] < self._min_price: - self.log_once(f"Removed {ticker['symbol']} from whitelist, " + self.log_once(f"Removed {pair} from whitelist, " f"because last price < {self._min_price:.8f}", logger.info) return False diff --git a/freqtrade/pairlist/SpreadFilter.py b/freqtrade/pairlist/SpreadFilter.py index 6c4e9f12f..cbbfb9626 100644 --- a/freqtrade/pairlist/SpreadFilter.py +++ b/freqtrade/pairlist/SpreadFilter.py @@ -36,16 +36,17 @@ class SpreadFilter(IPairList): return (f"{self.name} - Filtering pairs with ask/bid diff above " f"{self._max_spread_ratio * 100}%.") - def _validate_pair(self, ticker: dict) -> bool: + def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool: """ Validate spread for the ticker + :param pair: Pair that's currently validated :param ticker: ticker dict as returned from ccxt.load_markets() - :return: True if the pair can stay, False if it should be removed + :return: True if the pair can stay, false if it should be removed """ if 'bid' in ticker and 'ask' in ticker: spread = 1 - ticker['bid'] / ticker['ask'] if spread > self._max_spread_ratio: - self.log_once(f"Removed {ticker['symbol']} from whitelist, because spread " + self.log_once(f"Removed {pair} from whitelist, because spread " f"{spread * 100:.3f}% > {self._max_spread_ratio * 100}%", logger.info) return False diff --git a/freqtrade/pairlist/rangestabilityfilter.py b/freqtrade/pairlist/rangestabilityfilter.py index 756368355..f1fecc59c 100644 --- a/freqtrade/pairlist/rangestabilityfilter.py +++ b/freqtrade/pairlist/rangestabilityfilter.py @@ -42,7 +42,7 @@ class RangeStabilityFilter(IPairList): If no Pairlist requires tickers, an empty List is passed as tickers argument to filter_pairlist """ - return True + return False def short_desc(self) -> str: """ @@ -51,13 +51,13 @@ class RangeStabilityFilter(IPairList): return (f"{self.name} - Filtering pairs with rate of change below " f"{self._min_rate_of_change} over the last {plural(self._days, 'day')}.") - def _validate_pair(self, ticker: Dict) -> bool: + def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool: """ Validate trading range + :param pair: Pair that's currently validated :param ticker: ticker dict as returned from ccxt.load_markets() - :return: True if the pair can stay, False if it should be removed + :return: True if the pair can stay, false if it should be removed """ - pair = ticker['symbol'] # Check symbol in cache if pair in self._pair_cache: return self._pair_cache[pair] From c8dde632272fb2bc83e28a931a345b1c2144586a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Dec 2020 09:05:27 +0100 Subject: [PATCH 1136/1197] Allow test-pairlist to run with verbosity --- freqtrade/commands/arguments.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index a7ae969f4..a6c8a245f 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -44,7 +44,8 @@ ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"] ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one_column", "print_csv", "base_currencies", "quote_currencies", "list_pairs_all"] -ARGS_TEST_PAIRLIST = ["config", "quote_currencies", "print_one_column", "list_pairs_print_json"] +ARGS_TEST_PAIRLIST = ["verbosity", "config", "quote_currencies", "print_one_column", + "list_pairs_print_json"] ARGS_CREATE_USERDIR = ["user_data_dir", "reset"] From 4c0edd0461b694f1065f7541f359f3fd648ebf13 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Dec 2020 08:02:53 +0100 Subject: [PATCH 1137/1197] Move dependencies to base image for RPI --- Dockerfile.armhf | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/Dockerfile.armhf b/Dockerfile.armhf index 822da00a5..b6f2e44e6 100644 --- a/Dockerfile.armhf +++ b/Dockerfile.armhf @@ -11,10 +11,13 @@ ENV PATH=/root/.local/bin:$PATH RUN mkdir /freqtrade WORKDIR /freqtrade +RUN apt-get update \ + && apt-get -y install libatlas3-base curl sqlite3 \ + && apt-get clean + # Install dependencies FROM base as python-deps -RUN apt-get update \ - && apt-get -y install curl build-essential libssl-dev libffi-dev libatlas3-base libgfortran5 sqlite3 \ +RUN apt-get -y install build-essential libssl-dev libffi-dev libgfortran5 \ && apt-get clean \ && pip install --upgrade pip \ && echo "[global]\nextra-index-url=https://www.piwheels.org/simple" > /etc/pip.conf From 3c85d5201fab2914d63ffc6f2d0cea96f033b91c Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Dec 2020 20:38:26 +0100 Subject: [PATCH 1138/1197] Use async to get candle data for pairlists --- freqtrade/pairlist/AgeFilter.py | 50 ++++++++++------------ freqtrade/pairlist/pairlistmanager.py | 4 +- freqtrade/pairlist/rangestabilityfilter.py | 41 +++++++++++++----- 3 files changed, 55 insertions(+), 40 deletions(-) diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index f909014ba..21e1b1a01 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -2,7 +2,7 @@ Minimum age (days listed) pair list filter """ import logging -from typing import Any, Dict +from typing import Any, Dict, List import arrow @@ -49,36 +49,32 @@ class AgeFilter(IPairList): return (f"{self.name} - Filtering pairs with age less than " f"{self._min_days_listed} {plural(self._min_days_listed, 'day')}.") - def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool: + def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: """ - Validate age for the ticker - :param pair: Pair that's currently validated - :param ticker: ticker dict as returned from ccxt.load_markets() - :return: True if the pair can stay, false if it should be removed + :param pairlist: pairlist to filter or sort + :param tickers: Tickers (from exchange.get_tickers()). May be cached. + :return: new allowlist """ - - # Check symbol in cache - if ticker['symbol'] in self._symbolsChecked: - return True + needed_pairs = [(p, '1d') for p in pairlist if p not in self._symbolsChecked] + if not needed_pairs: + return pairlist since_ms = int(arrow.utcnow() .floor('day') - .shift(days=-self._min_days_listed) + .shift(days=-self._min_days_listed - 1) .float_timestamp) * 1000 + candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, cache=False) + pairlist_new = [] + if self._enabled: + for p, _ in needed_pairs: - daily_candles = self._exchange.get_historic_ohlcv(pair=ticker['symbol'], - timeframe='1d', - since_ms=since_ms) - - if daily_candles is not None: - if len(daily_candles) > self._min_days_listed: - # We have fetched at least the minimum required number of daily candles - # Add to cache, store the time we last checked this symbol - self._symbolsChecked[ticker['symbol']] = int(arrow.utcnow().float_timestamp) * 1000 - return True - else: - self.log_once(f"Removed {ticker['symbol']} from whitelist, because age " - f"{len(daily_candles)} is less than {self._min_days_listed} " - f"{plural(self._min_days_listed, 'day')}", logger.info) - return False - return False + age = len(candles[(p, '1d')]) if (p, '1d') in candles else 0 + if age > self._min_days_listed: + pairlist_new.append(p) + self._symbolsChecked[p] = int(arrow.utcnow().float_timestamp) * 1000 + else: + self.log_once(f"Removed {p} from whitelist, because age " + f"{age} is less than {self._min_days_listed} " + f"{plural(self._min_days_listed, 'day')}", logger.info) + logger.info(f"Validated {len(pairlist_new)} pairs.") + return pairlist_new diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/pairlist/pairlistmanager.py index 810a22300..418cc9e92 100644 --- a/freqtrade/pairlist/pairlistmanager.py +++ b/freqtrade/pairlist/pairlistmanager.py @@ -3,7 +3,7 @@ PairList manager class """ import logging from copy import deepcopy -from typing import Dict, List +from typing import Any, Dict, List from cachetools import TTLCache, cached @@ -97,7 +97,7 @@ class PairListManager(): self._whitelist = pairlist - def _prepare_whitelist(self, pairlist: List[str], tickers) -> List[str]: + def _prepare_whitelist(self, pairlist: List[str], tickers: Dict[str, Any]) -> List[str]: """ Prepare sanitized pairlist for Pairlist Handlers that use tickers data - remove pairs that do not have ticker available diff --git a/freqtrade/pairlist/rangestabilityfilter.py b/freqtrade/pairlist/rangestabilityfilter.py index f1fecc59c..8efded9ee 100644 --- a/freqtrade/pairlist/rangestabilityfilter.py +++ b/freqtrade/pairlist/rangestabilityfilter.py @@ -1,8 +1,9 @@ """ Rate of change pairlist filter """ +from copy import deepcopy import logging -from typing import Any, Dict +from typing import Any, Dict, List import arrow from cachetools.ttl import TTLCache @@ -51,7 +52,33 @@ class RangeStabilityFilter(IPairList): return (f"{self.name} - Filtering pairs with rate of change below " f"{self._min_rate_of_change} over the last {plural(self._days, 'day')}.") - def _validate_pair(self, pair: str, ticker: Dict[str, Any]) -> bool: + def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: + """ + Validate trading range + :param pairlist: pairlist to filter or sort + :param tickers: Tickers (from exchange.get_tickers()). May be cached. + :return: new allowlist + """ + needed_pairs = [(p, '1d') for p in pairlist if p not in self._pair_cache] + + since_ms = int(arrow.utcnow() + .floor('day') + .shift(days=-self._days - 1) + .float_timestamp) * 1000 + # Get all candles + candles = {} + if needed_pairs: + candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, + cache=False) + + if self._enabled: + for p in deepcopy(pairlist): + daily_candles = candles[(p, '1d')] if (p, '1d') in candles else None + if not self._validate_pair_loc(p, daily_candles): + pairlist.remove(p) + return pairlist + + def _validate_pair_loc(self, pair: str, daily_candles: Dict[str, Any]) -> bool: """ Validate trading range :param pair: Pair that's currently validated @@ -62,14 +89,6 @@ class RangeStabilityFilter(IPairList): if pair in self._pair_cache: return self._pair_cache[pair] - since_ms = int(arrow.utcnow() - .floor('day') - .shift(days=-self._days) - .float_timestamp) * 1000 - - daily_candles = self._exchange.get_historic_ohlcv_as_df(pair=pair, - timeframe='1d', - since_ms=since_ms) result = False if daily_candles is not None and not daily_candles.empty: highest_high = daily_candles['high'].max() @@ -79,7 +98,7 @@ class RangeStabilityFilter(IPairList): result = True else: self.log_once(f"Removed {pair} from whitelist, because rate of change " - f"over {plural(self._days, 'day')} is {pct_change:.3f}, " + f"over {self._days} {plural(self._days, 'day')} is {pct_change:.3f}, " f"which is below the threshold of {self._min_rate_of_change}.", logger.info) result = False From 011ba1d9ae74aeaad21cd3ebf2704acc8fa6b3d4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Dec 2020 20:49:46 +0100 Subject: [PATCH 1139/1197] Adapt tests to use async methods --- tests/conftest.py | 2 +- tests/plugins/test_pairlist.py | 53 ++++++++++++++++++++++---------- tests/rpc/test_rpc_apiserver.py | 11 ++++--- tests/strategy/test_interface.py | 11 ++++--- 4 files changed, 51 insertions(+), 26 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 5d358f015..965980f7a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1084,7 +1084,7 @@ def ohlcv_history_list(): @pytest.fixture def ohlcv_history(ohlcv_history_list): return ohlcv_to_dataframe(ohlcv_history_list, "5m", pair="UNITTEST/BTC", - fill_missing=True) + fill_missing=True, drop_incomplete=False) @pytest.fixture diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index c2a4a69d7..171f0e037 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -353,11 +353,19 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), ]) def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers, - ohlcv_history_list, pairlists, base_currency, + ohlcv_history, pairlists, base_currency, whitelist_result, caplog) -> None: whitelist_conf['pairlists'] = pairlists whitelist_conf['stake_currency'] = base_currency + ohlcv_data = { + ('ETH/BTC', '1d'): ohlcv_history, + ('TKN/BTC', '1d'): ohlcv_history, + ('LTC/BTC', '1d'): ohlcv_history, + ('XRP/BTC', '1d'): ohlcv_history, + ('HOT/BTC', '1d'): ohlcv_history, + } + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) if whitelist_result == 'static_in_the_middle': @@ -374,7 +382,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), + refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data), ) # Provide for PerformanceFilter's dependency @@ -402,7 +410,7 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t for pairlist in pairlists: if pairlist['method'] == 'AgeFilter' and pairlist['min_days_listed'] and \ - len(ohlcv_history_list) <= pairlist['min_days_listed']: + len(ohlcv_history) <= pairlist['min_days_listed']: assert log_has_re(r'^Removed .* from whitelist, because age .* is less than ' r'.* day.*', caplog) if pairlist['method'] == 'PrecisionFilter' and whitelist_result: @@ -575,8 +583,13 @@ def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tick get_patched_freqtradebot(mocker, default_conf) -def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, ohlcv_history_list): - +def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, ohlcv_history): + ohlcv_data = { + ('ETH/BTC', '1d'): ohlcv_history, + ('TKN/BTC', '1d'): ohlcv_history, + ('LTC/BTC', '1d'): ohlcv_history, + ('XRP/BTC', '1d'): ohlcv_history, + } mocker.patch.multiple('freqtrade.exchange.Exchange', markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), @@ -584,18 +597,18 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), + refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data), ) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_agefilter) - assert freqtrade.exchange.get_historic_ohlcv.call_count == 0 + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 0 freqtrade.pairlists.refresh_pairlist() - assert freqtrade.exchange.get_historic_ohlcv.call_count > 0 + assert freqtrade.exchange.refresh_latest_ohlcv.call_count > 0 - previous_call_count = freqtrade.exchange.get_historic_ohlcv.call_count + previous_call_count = freqtrade.exchange.refresh_latest_ohlcv.call_count freqtrade.pairlists.refresh_pairlist() # Should not have increased since first call. - assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers): @@ -625,7 +638,7 @@ def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers): (0.01, 5), (0.05, 0), # Setting rate_of_change to 5% removes all pairs from the whitelist. ]) -def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, ohlcv_history_list, +def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, ohlcv_history, min_rate_of_change, expected_length): default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, {'method': 'RangeStabilityFilter', 'lookback_days': 2, @@ -636,22 +649,30 @@ def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, oh exchange_has=MagicMock(return_value=True), get_tickers=tickers ) + ohlcv_data = { + ('ETH/BTC', '1d'): ohlcv_history, + ('TKN/BTC', '1d'): ohlcv_history, + ('LTC/BTC', '1d'): ohlcv_history, + ('XRP/BTC', '1d'): ohlcv_history, + ('HOT/BTC', '1d'): ohlcv_history, + ('BLK/BTC', '1d'): ohlcv_history, + } mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_historic_ohlcv=MagicMock(return_value=ohlcv_history_list), + refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data), ) freqtrade = get_patched_freqtradebot(mocker, default_conf) - assert freqtrade.exchange.get_historic_ohlcv.call_count == 0 + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 0 freqtrade.pairlists.refresh_pairlist() assert len(freqtrade.pairlists.whitelist) == expected_length - assert freqtrade.exchange.get_historic_ohlcv.call_count > 0 + assert freqtrade.exchange.refresh_latest_ohlcv.call_count > 0 - previous_call_count = freqtrade.exchange.get_historic_ohlcv.call_count + previous_call_count = freqtrade.exchange.refresh_latest_ohlcv.call_count freqtrade.pairlists.refresh_pairlist() assert len(freqtrade.pairlists.whitelist) == expected_length # Should not have increased since first call. - assert freqtrade.exchange.get_historic_ohlcv.call_count == previous_call_count + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count @pytest.mark.parametrize("pairlistconfig,desc_expected,exception_expected", [ diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 137727e8f..a1f4f7c9d 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -870,7 +870,7 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets): def test_api_pair_candles(botclient, ohlcv_history): ftbot, client = botclient timeframe = '5m' - amount = 2 + amount = 3 # No pair rc = client_get(client, @@ -910,8 +910,8 @@ def test_api_pair_candles(botclient, ohlcv_history): assert 'data_stop_ts' in rc.json assert rc.json['data_start'] == '2017-11-26 08:50:00+00:00' assert rc.json['data_start_ts'] == 1511686200000 - assert rc.json['data_stop'] == '2017-11-26 08:55:00+00:00' - assert rc.json['data_stop_ts'] == 1511686500000 + assert rc.json['data_stop'] == '2017-11-26 09:00:00+00:00' + assert rc.json['data_stop_ts'] == 1511686800000 assert isinstance(rc.json['columns'], list) assert rc.json['columns'] == ['date', 'open', 'high', 'low', 'close', 'volume', 'sma', 'buy', 'sell', @@ -926,7 +926,10 @@ def test_api_pair_candles(botclient, ohlcv_history): [['2017-11-26 08:50:00', 8.794e-05, 8.948e-05, 8.794e-05, 8.88e-05, 0.0877869, None, 0, 0, 1511686200000, None, None], ['2017-11-26 08:55:00', 8.88e-05, 8.942e-05, 8.88e-05, - 8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0, 1511686500000, 8.88e-05, None] + 8.893e-05, 0.05874751, 8.886500000000001e-05, 1, 0, 1511686500000, 8.88e-05, None], + ['2017-11-26 09:00:00', 8.891e-05, 8.893e-05, 8.875e-05, 8.877e-05, + 0.7039405, 8.885000000000002e-05, 0, 0, 1511686800000, None, None] + ]) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 7cf9a0624..7d6a564b1 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -128,27 +128,28 @@ def test_assert_df_raise(default_conf, mocker, caplog, ohlcv_history): def test_assert_df(default_conf, mocker, ohlcv_history, caplog): + df_len = len(ohlcv_history) - 1 # Ensure it's running when passed correctly _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), - ohlcv_history.loc[1, 'close'], ohlcv_history.loc[1, 'date']) + ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[df_len, 'date']) with pytest.raises(StrategyError, match=r"Dataframe returned from strategy.*length\."): _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history) + 1, - ohlcv_history.loc[1, 'close'], ohlcv_history.loc[1, 'date']) + ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[df_len, 'date']) with pytest.raises(StrategyError, match=r"Dataframe returned from strategy.*last close price\."): _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), - ohlcv_history.loc[1, 'close'] + 0.01, ohlcv_history.loc[1, 'date']) + ohlcv_history.loc[df_len, 'close'] + 0.01, ohlcv_history.loc[df_len, 'date']) with pytest.raises(StrategyError, match=r"Dataframe returned from strategy.*last date\."): _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), - ohlcv_history.loc[1, 'close'], ohlcv_history.loc[0, 'date']) + ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date']) _STRATEGY.disable_dataframe_checks = True caplog.clear() _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), - ohlcv_history.loc[1, 'close'], ohlcv_history.loc[0, 'date']) + ohlcv_history.loc[2, 'close'], ohlcv_history.loc[0, 'date']) assert log_has_re(r"Dataframe returned from strategy.*last date\.", caplog) # reset to avoid problems in other tests due to test leakage _STRATEGY.disable_dataframe_checks = False From d1fda28d2ea91405a808c90f315ef3e1d0bbd212 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 15 Dec 2020 20:59:58 +0100 Subject: [PATCH 1140/1197] Fix typehints --- freqtrade/exchange/exchange.py | 2 +- freqtrade/pairlist/rangestabilityfilter.py | 7 ++++--- tests/strategy/test_interface.py | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b61049c4e..46c45b5e2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -735,7 +735,7 @@ class Exchange: def refresh_latest_ohlcv(self, pair_list: ListPairsWithTimeframes, *, since_ms: Optional[int] = None, cache: bool = True - ) -> Dict[str, DataFrame]: + ) -> Dict[Tuple[str, str], DataFrame]: """ Refresh in-memory OHLCV asynchronously and set `_klines` with the result Loops asynchronously over pair_list and downloads all pairs async (semi-parallel). diff --git a/freqtrade/pairlist/rangestabilityfilter.py b/freqtrade/pairlist/rangestabilityfilter.py index 8efded9ee..6efe1e2ae 100644 --- a/freqtrade/pairlist/rangestabilityfilter.py +++ b/freqtrade/pairlist/rangestabilityfilter.py @@ -1,12 +1,13 @@ """ Rate of change pairlist filter """ -from copy import deepcopy import logging -from typing import Any, Dict, List +from copy import deepcopy +from typing import Any, Dict, List, Optional import arrow from cachetools.ttl import TTLCache +from pandas import DataFrame from freqtrade.exceptions import OperationalException from freqtrade.misc import plural @@ -78,7 +79,7 @@ class RangeStabilityFilter(IPairList): pairlist.remove(p) return pairlist - def _validate_pair_loc(self, pair: str, daily_candles: Dict[str, Any]) -> bool: + def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool: """ Validate trading range :param pair: Pair that's currently validated diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 7d6a564b1..640849ba4 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -140,7 +140,8 @@ def test_assert_df(default_conf, mocker, ohlcv_history, caplog): with pytest.raises(StrategyError, match=r"Dataframe returned from strategy.*last close price\."): _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), - ohlcv_history.loc[df_len, 'close'] + 0.01, ohlcv_history.loc[df_len, 'date']) + ohlcv_history.loc[df_len, 'close'] + 0.01, + ohlcv_history.loc[df_len, 'date']) with pytest.raises(StrategyError, match=r"Dataframe returned from strategy.*last date\."): _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), From 266031a6beebf4c27a33e63d0086400aa94bb485 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 16 Dec 2020 19:24:47 +0100 Subject: [PATCH 1141/1197] Disallow PerformanceFilter for backtesting closes #4072 --- docs/includes/pairlists.md | 6 ++++++ freqtrade/optimize/backtesting.py | 2 ++ tests/optimize/test_backtesting.py | 5 +++++ 3 files changed, 13 insertions(+) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 844f1d70a..732dfa5bb 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -65,6 +65,9 @@ The `refresh_period` setting allows to define the period (in seconds), at which }], ``` +!!! Note + `VolumePairList` does not support backtesting mode. + #### AgeFilter Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`). @@ -84,6 +87,9 @@ Sorts pairs by past trade performance, as follows: Trade count is used as a tie breaker. +!!! Note + `PerformanceFilter` does not support backtesting mode. + #### PrecisionFilter Filters low-value coins which would not allow setting stoplosses. diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index de9c52dad..639904975 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -102,6 +102,8 @@ class Backtesting: self.pairlists = PairListManager(self.exchange, self.config) if 'VolumePairList' in self.pairlists.name_list: raise OperationalException("VolumePairList not allowed for backtesting.") + if 'PerformanceFilter' in self.pairlists.name_list: + raise OperationalException("PerformanceFilter not allowed for backtesting.") if len(self.strategylist) > 1 and 'PrecisionFilter' in self.pairlists.name_list: raise OperationalException( diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 547e55db8..971f8d048 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -430,6 +430,11 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti with pytest.raises(OperationalException, match='VolumePairList not allowed for backtesting.'): Backtesting(default_conf) + default_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PerformanceFilter"}] + with pytest.raises(OperationalException, + match='PerformanceFilter not allowed for backtesting.'): + Backtesting(default_conf) + default_conf['pairlists'] = [{"method": "StaticPairList"}, {"method": "PrecisionFilter"}, ] Backtesting(default_conf) From 4e7f914e92f5e52a450e8ab547ef0d2db057eda5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 17 Dec 2020 13:32:19 +0100 Subject: [PATCH 1142/1197] Improve test for AgeFilter, fix bug in Agefilter --- freqtrade/pairlist/AgeFilter.py | 45 +++++++++++++++++++++++---------- tests/plugins/test_pairlist.py | 8 +++--- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index 21e1b1a01..cc61fd4c3 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -2,9 +2,11 @@ Minimum age (days listed) pair list filter """ import logging -from typing import Any, Dict, List +from copy import deepcopy +from typing import Any, Dict, List, Optional import arrow +from pandas import DataFrame from freqtrade.exceptions import OperationalException from freqtrade.misc import plural @@ -64,17 +66,34 @@ class AgeFilter(IPairList): .shift(days=-self._min_days_listed - 1) .float_timestamp) * 1000 candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, cache=False) - pairlist_new = [] if self._enabled: - for p, _ in needed_pairs: + for p in deepcopy(pairlist): + daily_candles = candles[(p, '1d')] if (p, '1d') in candles else None + if not self._validate_pair_loc(p, daily_candles): + pairlist.remove(p) + logger.info(f"Validated {len(pairlist)} pairs.") + return pairlist - age = len(candles[(p, '1d')]) if (p, '1d') in candles else 0 - if age > self._min_days_listed: - pairlist_new.append(p) - self._symbolsChecked[p] = int(arrow.utcnow().float_timestamp) * 1000 - else: - self.log_once(f"Removed {p} from whitelist, because age " - f"{age} is less than {self._min_days_listed} " - f"{plural(self._min_days_listed, 'day')}", logger.info) - logger.info(f"Validated {len(pairlist_new)} pairs.") - return pairlist_new + def _validate_pair_loc(self, pair: str, daily_candles: Optional[DataFrame]) -> bool: + """ + Validate age for the ticker + :param pair: Pair that's currently validated + :param ticker: ticker dict as returned from ccxt.load_markets() + :return: True if the pair can stay, false if it should be removed + """ + # Check symbol in cache + if pair in self._symbolsChecked: + return True + + if daily_candles is not None: + if len(daily_candles) > self._min_days_listed: + # We have fetched at least the minimum required number of daily candles + # Add to cache, store the time we last checked this symbol + self._symbolsChecked[pair] = int(arrow.utcnow().float_timestamp) * 1000 + return True + else: + self.log_once(f"Removed {pair} from whitelist, because age " + f"{len(daily_candles)} is less than {self._min_days_listed} " + f"{plural(self._min_days_listed, 'day')}", logger.info) + return False + return False diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 171f0e037..c4b370e15 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -588,7 +588,6 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o ('ETH/BTC', '1d'): ohlcv_history, ('TKN/BTC', '1d'): ohlcv_history, ('LTC/BTC', '1d'): ohlcv_history, - ('XRP/BTC', '1d'): ohlcv_history, } mocker.patch.multiple('freqtrade.exchange.Exchange', markets=PropertyMock(return_value=markets), @@ -603,12 +602,15 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_agefilter) assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 0 freqtrade.pairlists.refresh_pairlist() + assert len(freqtrade.pairlists.whitelist) == 3 assert freqtrade.exchange.refresh_latest_ohlcv.call_count > 0 + # freqtrade.config['exchange']['pair_whitelist'].append('HOT/BTC') previous_call_count = freqtrade.exchange.refresh_latest_ohlcv.call_count freqtrade.pairlists.refresh_pairlist() - # Should not have increased since first call. - assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count + assert len(freqtrade.pairlists.whitelist) == 3 + # Called once for XRP/BTC + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count + 1 def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers): From ca9fd089918c9c584735af803edef8785460634f Mon Sep 17 00:00:00 2001 From: bigchakalaka <34461529+bigchakalaka@users.noreply.github.com> Date: Thu, 17 Dec 2020 21:40:54 +0100 Subject: [PATCH 1143/1197] Update strategy-customization.md --- docs/strategy-customization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index db007985f..ab64d3a67 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -147,7 +147,7 @@ Let's try to backtest 1 month (January 2019) of 5m candles using an example stra freqtrade backtesting --timerange 20190101-20190201 --timeframe 5m ``` -Assuming `startup_candle_count` is set to 100, backtesting knows it needs 100 candles to generate valid buy signals. It will load data from `20190101 - (100 * 5m)` - which is ~2019-12-31 15:30:00. +Assuming `startup_candle_count` is set to 100, backtesting knows it needs 100 candles to generate valid buy signals. It will load data from `20190101 - (100 * 5m)` - which is ~2018-12-31 15:30:00. If this data is available, indicators will be calculated with this extended timerange. The instable startup period (up to 2019-01-01 00:00:00) will then be removed before starting backtesting. !!! Note From 7d2395ddb7ada3c4e99e5de868cf143303771dd3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Dec 2020 11:44:50 +0100 Subject: [PATCH 1144/1197] Add limit parameter to fetch_ohlcv --- freqtrade/exchange/binance.py | 1 + freqtrade/exchange/exchange.py | 3 ++- freqtrade/exchange/kraken.py | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 099f282a2..26ec30a8a 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -18,6 +18,7 @@ class Binance(Exchange): _ft_has: Dict = { "stoploss_on_exchange": True, "order_time_in_force": ['gtc', 'fok', 'ioc'], + "ohlcv_candle_limit": 1000, "trades_pagination": "id", "trades_pagination_arg": "fromId", "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 46c45b5e2..6f495e605 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -807,7 +807,8 @@ class Exchange: ) data = await self._api_async.fetch_ohlcv(pair, timeframe=timeframe, - since=since_ms) + since=since_ms, + limit=self._ohlcv_candle_limit) # Some exchanges sort OHLCV in ASC order and others in DESC. # Ex: Bittrex returns the list of OHLCV in ASC order (oldest first, newest last) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 4e4713052..6dbb751e5 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -18,6 +18,7 @@ class Kraken(Exchange): _params: Dict = {"trading_agreement": "agree"} _ft_has: Dict = { "stoploss_on_exchange": True, + "ohlcv_candle_limit": 720, "trades_pagination": "id", "trades_pagination_arg": "since", } From bd0af1b300949c40f9b7c84c14af8ec55e6812f6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Dec 2020 19:38:12 +0100 Subject: [PATCH 1145/1197] Fix test warning --- tests/exchange/test_exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index d8a846124..a42ff52e4 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2153,7 +2153,7 @@ def test_get_fee(default_conf, mocker, exchange_name): def test_stoploss_order_unsupported_exchange(default_conf, mocker): - exchange = get_patched_exchange(mocker, default_conf, 'bittrex') + exchange = get_patched_exchange(mocker, default_conf, id='bittrex') with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) From d7daa86434e5647f27f633e4dcc963f95605d739 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Dec 2020 19:59:46 +0100 Subject: [PATCH 1146/1197] Add bybit subclass --- freqtrade/exchange/__init__.py | 1 + freqtrade/exchange/bybit.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 freqtrade/exchange/bybit.py diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 5b58d7a95..15ba7b9f6 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -6,6 +6,7 @@ from freqtrade.exchange.exchange import Exchange from freqtrade.exchange.bibox import Bibox from freqtrade.exchange.binance import Binance from freqtrade.exchange.bittrex import Bittrex +from freqtrade.exchange.bybit import Bybit from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, get_exchange_bad_reason, is_exchange_bad, is_exchange_known_ccxt, is_exchange_officially_supported, diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py new file mode 100644 index 000000000..4a44bb42d --- /dev/null +++ b/freqtrade/exchange/bybit.py @@ -0,0 +1,24 @@ +""" Bybit exchange subclass """ +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + + +logger = logging.getLogger(__name__) + + +class Bybit(Exchange): + """ + Bybit exchange class. Contains adjustments needed for Freqtrade to work + with this exchange. + + Please note that this exchange is not included in the list of exchanges + officially supported by the Freqtrade development team. So some features + may still not work as expected. + """ + + # fetchCurrencies API point requires authentication for Bybit, + _ft_has: Dict = { + "ohlcv_candle_limit": 200, + } From 8d3f096a9758c46437c28ed3458546da2be93729 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 20 Dec 2020 20:08:54 +0100 Subject: [PATCH 1147/1197] AgeFilter does not require tickers --- freqtrade/pairlist/AgeFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/pairlist/AgeFilter.py index cc61fd4c3..e3465bd82 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/pairlist/AgeFilter.py @@ -42,7 +42,7 @@ class AgeFilter(IPairList): If no Pairlist requires tickers, an empty Dict is passed as tickers argument to filter_pairlist """ - return True + return False def short_desc(self) -> str: """ From e92bcb00f6ead312525608dea96c758273ef8107 Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 20 Dec 2020 13:43:50 +0100 Subject: [PATCH 1148/1197] telegram: specify custom shortcut bottons (keyboard) in config.json --- freqtrade/rpc/telegram.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 1d36b7e4d..ea9a3c31d 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -5,6 +5,7 @@ This module manage Telegram communication """ import json import logging +from itertools import chain from datetime import timedelta from typing import Any, Callable, Dict, List, Union @@ -862,12 +863,39 @@ class Telegram(RPC): :return: None """ + # default / fallback shortcut buttons keyboard: List[List[Union[str, KeyboardButton]]] = [ ['/daily', '/profit', '/balance'], ['/status', '/status table', '/performance'], ['/count', '/start', '/stop', '/help'] ] + # do not allow commands with mandatory arguments and critical cmds + # like /forcesell and /forcebuy + valid_btns: List[str] = ['/start', '/stop', '/status', '/status table', + '/trades', '/profit', '/performance', '/daily', + '/stats', '/count', '/locks', '/balance', + '/stopbuy', '/reload_config', '/show_config', + '/logs', '/whitelist', '/blacklist', '/edge', + '/help', '/version'] + # custom shortcuts specified in config.json + shortcut_btns = self._config['telegram'].get('shortcut_btns', []) + if shortcut_btns: + # check for valid shortcuts + invalid_shortcut_btns = [b for b in chain.from_iterable(shortcut_btns) + if b not in valid_btns] + if len(invalid_shortcut_btns): + logger.warning('rpc.telegram: invalid shortcut_btns %s', + invalid_shortcut_btns) + logger.info('rpc.telegram: using default shortcut_btns %s', + keyboard) + else: + keyboard = shortcut_btns + logger.info( + 'rpc.telegram uses custom shortcut bottons specified in ' + + 'config.json %s', [btn for btn in keyboard] + ) + reply_markup = ReplyKeyboardMarkup(keyboard) try: From 5e6897b278a4b367993e08c79ed5562a40de21ae Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 20 Dec 2020 14:48:49 +0100 Subject: [PATCH 1149/1197] documentation for custom keyboard --- docs/telegram-usage.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index c940f59ac..11eb831ad 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -87,6 +87,35 @@ Example configuration showing the different settings: }, ``` +## Create a custom keyboard (command shortcuts buttons) +Telegram allows us to create a custom keyboard with buttons for commands. +The default custom keyboard looks like this. +```python +[ + ['/daily', '/profit', '/balance'], # row 1, 3 commands + ['/status', '/status table', '/performance'], # row 2, 3 commands + ['/count', '/start', '/stop', '/help'] # row 3, 4 commands +] +``` +### Usage +You can create your own keyboard in `config.json`: +``` json +"telegram": { + "enabled": true, + "token": "your_telegram_token", + "chat_id": "your_telegram_chat_id", + "shortcut_btns": [ + ["/daily", "/stats", "/balance", "/profit"], + ["/status table", "/performance"], + ["/reload_config", "/count", "/logs"] + ] + }, +``` +!! NOTE: Only a certain list of commands are allowed. Command arguments are not +supported! +### Supported Commands + `/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopbuy`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version` + ## Telegram commands Per default, the Telegram bot shows predefined commands. Some commands From 5b3ffd514138cef8f6c3160f13bdcbc31d8b73d4 Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 20 Dec 2020 15:23:40 +0100 Subject: [PATCH 1150/1197] better log msg, comments --- freqtrade/rpc/telegram.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ea9a3c31d..ec99e4aa9 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -872,6 +872,9 @@ class Telegram(RPC): # do not allow commands with mandatory arguments and critical cmds # like /forcesell and /forcebuy + # TODO: DRY! - its not good to list all valid cmds here. But this + # needs refacoring of the whole telegram module (same problem + # in _help()). valid_btns: List[str] = ['/start', '/stop', '/status', '/status table', '/trades', '/profit', '/performance', '/daily', '/stats', '/count', '/locks', '/balance', @@ -885,16 +888,13 @@ class Telegram(RPC): invalid_shortcut_btns = [b for b in chain.from_iterable(shortcut_btns) if b not in valid_btns] if len(invalid_shortcut_btns): - logger.warning('rpc.telegram: invalid shortcut_btns %s', - invalid_shortcut_btns) - logger.info('rpc.telegram: using default shortcut_btns %s', - keyboard) + logger.warning('rpc.telegram: invalid commands for custom ' + f'keyboard: {invalid_shortcut_btns}') + logger.info('rpc.telegram: using default keyboard.') else: keyboard = shortcut_btns - logger.info( - 'rpc.telegram uses custom shortcut bottons specified in ' + - 'config.json %s', [btn for btn in keyboard] - ) + logger.info('rpc.telegram using custom keyboard from ' + f'config.json: {[btn for btn in keyboard]}') reply_markup = ReplyKeyboardMarkup(keyboard) From bf920994868a30113c57b665e7739261f4a91ac8 Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 20 Dec 2020 16:35:54 +0100 Subject: [PATCH 1151/1197] test for custom keyboard --- tests/rpc/test_rpc_telegram.py | 48 +++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index ecad05683..62c60d8e6 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -10,7 +10,7 @@ from unittest.mock import ANY, MagicMock, PropertyMock import arrow import pytest -from telegram import Chat, Message, Update +from telegram import Chat, Message, Update, ReplyKeyboardMarkup from telegram.error import NetworkError from freqtrade import __version__ @@ -1729,3 +1729,49 @@ def test__send_msg_network_error(default_conf, mocker, caplog) -> None: # Bot should've tried to send it twice assert len(bot.method_calls) == 2 assert log_has('Telegram NetworkError: Oh snap! Trying one more time.', caplog) + + +def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: + mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) + bot = MagicMock() + bot.send_message = MagicMock() + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(freqtradebot) + telegram._updater = MagicMock() + telegram._updater.bot = bot + + invalid_keys_list = [['/not_valid', '/profit'], ['/daily'], ['/alsoinvalid']] + default_keys_list = [['/daily', '/profit', '/balance'], + ['/status', '/status table', '/performance'], + ['/count', '/start', '/stop', '/help']] + default_keyboard = ReplyKeyboardMarkup(default_keys_list) + + custom_keys_list = [['/daily', '/stats', '/balance', '/profit'], + ['/count', '/start', '/reload_config', '/help']] + custom_keyboard = ReplyKeyboardMarkup(custom_keys_list) + + # no shortcut_btns in config -> default keyboard + telegram._config['telegram']['enabled'] = True + telegram._send_msg('test') + used_keyboard = bot.send_message.call_args.kwargs['reply_markup'] + assert used_keyboard == default_keyboard + + # invalid shortcut_btns in config -> default keyboard + telegram._config['telegram']['enabled'] = True + telegram._config['telegram']['shortcut_btns'] = invalid_keys_list + telegram._send_msg('test') + used_keyboard = bot.send_message.call_args.kwargs['reply_markup'] + assert used_keyboard == default_keyboard + assert log_has("rpc.telegram: invalid commands for custom keyboard: " + "['/not_valid', '/alsoinvalid']", caplog) + assert log_has('rpc.telegram: using default keyboard.', caplog) + + # valid shortcut_btns in config -> custom keyboard + telegram._config['telegram']['enabled'] = True + telegram._config['telegram']['shortcut_btns'] = custom_keys_list + telegram._send_msg('test') + used_keyboard = bot.send_message.call_args.kwargs['reply_markup'] + assert used_keyboard == custom_keyboard + assert log_has("rpc.telegram using custom keyboard from config.json: " + "[['/daily', '/stats', '/balance', '/profit'], ['/count', " + "'/start', '/reload_config', '/help']]", caplog) From 621105df9aecdc7a278cc89f035a7521a0fb1e23 Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 20 Dec 2020 16:50:42 +0100 Subject: [PATCH 1152/1197] renaming shortcut_btns to keyboard --- docs/telegram-usage.md | 4 ++-- freqtrade/rpc/telegram.py | 14 +++++++------- tests/rpc/test_rpc_telegram.py | 10 +++++----- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 11eb831ad..950b4df1e 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -87,7 +87,7 @@ Example configuration showing the different settings: }, ``` -## Create a custom keyboard (command shortcuts buttons) +## Create a custom keyboard (command shortcut buttons) Telegram allows us to create a custom keyboard with buttons for commands. The default custom keyboard looks like this. ```python @@ -104,7 +104,7 @@ You can create your own keyboard in `config.json`: "enabled": true, "token": "your_telegram_token", "chat_id": "your_telegram_chat_id", - "shortcut_btns": [ + "keyboard": [ ["/daily", "/stats", "/balance", "/profit"], ["/status table", "/performance"], ["/reload_config", "/count", "/logs"] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ec99e4aa9..3260d0d4f 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -882,17 +882,17 @@ class Telegram(RPC): '/logs', '/whitelist', '/blacklist', '/edge', '/help', '/version'] # custom shortcuts specified in config.json - shortcut_btns = self._config['telegram'].get('shortcut_btns', []) - if shortcut_btns: + cust_keyboard = self._config['telegram'].get('keyboard', []) + if cust_keyboard: # check for valid shortcuts - invalid_shortcut_btns = [b for b in chain.from_iterable(shortcut_btns) - if b not in valid_btns] - if len(invalid_shortcut_btns): + invalid_keys = [b for b in chain.from_iterable(cust_keyboard) + if b not in valid_btns] + if len(invalid_keys): logger.warning('rpc.telegram: invalid commands for custom ' - f'keyboard: {invalid_shortcut_btns}') + f'keyboard: {invalid_keys}') logger.info('rpc.telegram: using default keyboard.') else: - keyboard = shortcut_btns + keyboard = cust_keyboard logger.info('rpc.telegram using custom keyboard from ' f'config.json: {[btn for btn in keyboard]}') diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 62c60d8e6..5bbc68639 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1750,15 +1750,15 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: ['/count', '/start', '/reload_config', '/help']] custom_keyboard = ReplyKeyboardMarkup(custom_keys_list) - # no shortcut_btns in config -> default keyboard + # no keyboard in config -> default keyboard telegram._config['telegram']['enabled'] = True telegram._send_msg('test') used_keyboard = bot.send_message.call_args.kwargs['reply_markup'] assert used_keyboard == default_keyboard - # invalid shortcut_btns in config -> default keyboard + # invalid keyboard in config -> default keyboard telegram._config['telegram']['enabled'] = True - telegram._config['telegram']['shortcut_btns'] = invalid_keys_list + telegram._config['telegram']['keyboard'] = invalid_keys_list telegram._send_msg('test') used_keyboard = bot.send_message.call_args.kwargs['reply_markup'] assert used_keyboard == default_keyboard @@ -1766,9 +1766,9 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: "['/not_valid', '/alsoinvalid']", caplog) assert log_has('rpc.telegram: using default keyboard.', caplog) - # valid shortcut_btns in config -> custom keyboard + # valid keyboard in config -> custom keyboard telegram._config['telegram']['enabled'] = True - telegram._config['telegram']['shortcut_btns'] = custom_keys_list + telegram._config['telegram']['keyboard'] = custom_keys_list telegram._send_msg('test') used_keyboard = bot.send_message.call_args.kwargs['reply_markup'] assert used_keyboard == custom_keyboard From 799e6be2eba9f0b385d3bed1d2f9665743f37029 Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 20 Dec 2020 17:13:03 +0100 Subject: [PATCH 1153/1197] fix tests --- tests/rpc/test_rpc_telegram.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 5bbc68639..5a88edf54 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1753,14 +1753,14 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: # no keyboard in config -> default keyboard telegram._config['telegram']['enabled'] = True telegram._send_msg('test') - used_keyboard = bot.send_message.call_args.kwargs['reply_markup'] + used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == default_keyboard # invalid keyboard in config -> default keyboard telegram._config['telegram']['enabled'] = True telegram._config['telegram']['keyboard'] = invalid_keys_list telegram._send_msg('test') - used_keyboard = bot.send_message.call_args.kwargs['reply_markup'] + used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == default_keyboard assert log_has("rpc.telegram: invalid commands for custom keyboard: " "['/not_valid', '/alsoinvalid']", caplog) @@ -1770,7 +1770,7 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: telegram._config['telegram']['enabled'] = True telegram._config['telegram']['keyboard'] = custom_keys_list telegram._send_msg('test') - used_keyboard = bot.send_message.call_args.kwargs['reply_markup'] + used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == custom_keyboard assert log_has("rpc.telegram using custom keyboard from config.json: " "[['/daily', '/stats', '/balance', '/profit'], ['/count', " From 6b44545d37bd197e53c56fdc99d0965c14608553 Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 20 Dec 2020 17:22:23 +0100 Subject: [PATCH 1154/1197] sort order imports --- freqtrade/rpc/telegram.py | 2 +- tests/rpc/test_rpc_telegram.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 3260d0d4f..466c58878 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -5,8 +5,8 @@ This module manage Telegram communication """ import json import logging -from itertools import chain from datetime import timedelta +from itertools import chain from typing import Any, Callable, Dict, List, Union import arrow diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 5a88edf54..ce3db7130 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -10,7 +10,7 @@ from unittest.mock import ANY, MagicMock, PropertyMock import arrow import pytest -from telegram import Chat, Message, Update, ReplyKeyboardMarkup +from telegram import Chat, Message, ReplyKeyboardMarkup, Update from telegram.error import NetworkError from freqtrade import __version__ From ecadfdd98ea7fcb22265d37a67a6494706047e8a Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 17 May 2020 12:24:04 +0200 Subject: [PATCH 1155/1197] fixed:advanced config. added. feature: fill area between traces by advanced configuration. --- freqtrade/plot/plotting.py | 40 +++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index f7d300593..ec0d53a0b 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -236,12 +236,20 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], :param plot_config: Dict of Dicts containing advanced plot configuration :return: plot_config - eventually with indicators 1 and 2 """ - if plot_config: if indicators1: - plot_config['main_plot'] = {ind: {} for ind in indicators1} + for ind in indicators1: + #add indicators with no advanced plot_config. + if ind not in plot_config['main_plot'].keys(): + plot_config['main_plot'][ind] = {} if indicators2: - plot_config['subplots'] = {'Other': {ind: {} for ind in indicators2}} + #'Other' key not provided in strategy.plot_config. + if 'Other' not in plot_config['subplots'].keys(): + plot_config['subplots'] = {'Other': plot_config['subplots']} + for ind in indicators2: + #add indicators with no advanced plot_config + if ind not in plot_config['subplots']['Other'].keys(): + plot_config['subplots']['Other'][ind] = {} if not plot_config: # If no indicators and no plot-config given, use defaults. @@ -255,6 +263,8 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], 'main_plot': {ind: {} for ind in indicators1}, 'subplots': {'Other': {ind: {} for ind in indicators2}}, } + + #!!!NON SENSE - isnt it? if 'main_plot' not in plot_config: plot_config['main_plot'] = {} @@ -280,6 +290,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra :return: Plotly figure """ plot_config = create_plotconfig(indicators1, indicators2, plot_config) + print(plot_config) rows = 2 + len(plot_config['subplots']) row_widths = [1 for _ in plot_config['subplots']] @@ -370,6 +381,29 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra del plot_config['main_plot']['bb_upperband'] del plot_config['main_plot']['bb_lowerband'] + #fill area betwenn traces i.e. for ichimoku + if 'fill_area' in plot_config.keys(): + for area in plot_config['fill_area']: + #!error: need exactly 2 trace + traces = area['traces'] + color = area['color'] + if traces[0] in data and traces[1] in data: + trace_b = go.Scatter( + x=data.date, + y=data.get(traces[0]), + showlegend=False, + line={'color': 'rgba(255,255,255,0)'}, + ) + trace_a = go.Scatter( + x=data.date, + y=data.get(traces[1]), + name=f'{traces[0]} * {traces[1]}', + fill="tonexty", + fillcolor=color, + line={'color': 'rgba(255,255,255,0)'}, + ) + fig.add_trace(trace_b) + fig.add_trace(trace_a) # Add indicators to main plot fig = add_indicators(fig=fig, row=1, indicators=plot_config['main_plot'], data=data) From 3fdfc06a1e6046d61bb420d7b52d2dd8d6427e4b Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 17 May 2020 12:43:38 +0200 Subject: [PATCH 1156/1197] label for fill_area added and documentation updated --- docs/plotting.md | 9 ++++++++- freqtrade/plot/plotting.py | 4 ++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/plotting.md b/docs/plotting.md index 09eb6ddb5..f794cdedc 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -168,6 +168,7 @@ Additional features when using plot_config include: * Specify colors per indicator * Specify additional subplots +* Specify idicator pairs to fill area in between The sample plot configuration below specifies fixed colors for the indicators. Otherwise consecutive plots may produce different colorschemes each time, making comparisons difficult. It also allows multiple subplots to display both MACD and RSI at the same time. @@ -194,7 +195,13 @@ Sample configuration with inline comments explaining the process: "RSI": { 'rsi': {'color': 'red'}, } - } + }, + 'fill_area': { + "Ichimoku Cloud": { + 'traces': ('senkou_a', 'senkou_b'), + 'color': 'rgba(0,176,246,0.2)', + }, + } } ``` diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index ec0d53a0b..2d0d01388 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -383,7 +383,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra #fill area betwenn traces i.e. for ichimoku if 'fill_area' in plot_config.keys(): - for area in plot_config['fill_area']: + for label, area in plot_config['fill_area'].items(): #!error: need exactly 2 trace traces = area['traces'] color = area['color'] @@ -397,7 +397,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra trace_a = go.Scatter( x=data.date, y=data.get(traces[1]), - name=f'{traces[0]} * {traces[1]}', + name=label, fill="tonexty", fillcolor=color, line={'color': 'rgba(255,255,255,0)'}, From daa1727e2bfd641058728eb8161d615e2db1c66a Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 17 May 2020 13:13:32 +0200 Subject: [PATCH 1157/1197] Exeption for fill_area.traces --- freqtrade/plot/plotting.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 2d0d01388..aed752b94 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -290,7 +290,6 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra :return: Plotly figure """ plot_config = create_plotconfig(indicators1, indicators2, plot_config) - print(plot_config) rows = 2 + len(plot_config['subplots']) row_widths = [1 for _ in plot_config['subplots']] @@ -384,8 +383,13 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra #fill area betwenn traces i.e. for ichimoku if 'fill_area' in plot_config.keys(): for label, area in plot_config['fill_area'].items(): - #!error: need exactly 2 trace traces = area['traces'] + if len(traces) != 2: + raise Exception( + f"plot_config.fill_area.traces = {traces}: " \ + + f"needs exactly 2 indicators. " \ + + f"{len(traces)} is given." + ) color = area['color'] if traces[0] in data and traces[1] in data: trace_b = go.Scatter( From fdd4b40c3468d6cf8145202eeb058c6c36a50855 Mon Sep 17 00:00:00 2001 From: Christof Date: Thu, 21 May 2020 08:28:58 +0200 Subject: [PATCH 1158/1197] fixed subplots, empty create plot_config if its not given by strategie --- freqtrade/plot/plotting.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index aed752b94..ea18e7102 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -237,19 +237,24 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], :return: plot_config - eventually with indicators 1 and 2 """ if plot_config: + #maybe main or sub is given, not both. + if 'main_plot' not in plot_config.keys(): + plot_config['main_plot'] = {} + + if 'subplots' not in plot_config.keys(): + plot_config['subplots'] = {} if indicators1: for ind in indicators1: - #add indicators with no advanced plot_config. + #add indicators with NO advanced plot_config, only! to be sure + #indicator colors given in advanced plot_config will not be + #overwritten. if ind not in plot_config['main_plot'].keys(): plot_config['main_plot'][ind] = {} if indicators2: - #'Other' key not provided in strategy.plot_config. - if 'Other' not in plot_config['subplots'].keys(): - plot_config['subplots'] = {'Other': plot_config['subplots']} - for ind in indicators2: - #add indicators with no advanced plot_config - if ind not in plot_config['subplots']['Other'].keys(): - plot_config['subplots']['Other'][ind] = {} + #add other indicators given on cmd line to advanced plot_config. + plot_config['subplots'].update( + {'Other' : {ind : {} for ind in indicators2}} + ) if not plot_config: # If no indicators and no plot-config given, use defaults. @@ -264,12 +269,6 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], 'subplots': {'Other': {ind: {} for ind in indicators2}}, } - #!!!NON SENSE - isnt it? - if 'main_plot' not in plot_config: - plot_config['main_plot'] = {} - - if 'subplots' not in plot_config: - plot_config['subplots'] = {} return plot_config @@ -290,7 +289,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra :return: Plotly figure """ plot_config = create_plotconfig(indicators1, indicators2, plot_config) - + print(plot_config) rows = 2 + len(plot_config['subplots']) row_widths = [1 for _ in plot_config['subplots']] # Define the graph From fb3d82ccb9221a8928a9a81618bfade526853820 Mon Sep 17 00:00:00 2001 From: Christof Date: Thu, 21 May 2020 08:32:12 +0200 Subject: [PATCH 1159/1197] cleanup --- freqtrade/plot/plotting.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index ea18e7102..02636d831 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -289,7 +289,6 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra :return: Plotly figure """ plot_config = create_plotconfig(indicators1, indicators2, plot_config) - print(plot_config) rows = 2 + len(plot_config['subplots']) row_widths = [1 for _ in plot_config['subplots']] # Define the graph From 4531c924da55ecab7eb6a704d695a84c33e0aa19 Mon Sep 17 00:00:00 2001 From: Christof Date: Mon, 25 May 2020 09:05:24 +0200 Subject: [PATCH 1160/1197] PEP8 --- freqtrade/plot/plotting.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 02636d831..bed0319a6 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -237,7 +237,7 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], :return: plot_config - eventually with indicators 1 and 2 """ if plot_config: - #maybe main or sub is given, not both. + # maybe main or sub is given, not both. if 'main_plot' not in plot_config.keys(): plot_config['main_plot'] = {} @@ -245,15 +245,15 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], plot_config['subplots'] = {} if indicators1: for ind in indicators1: - #add indicators with NO advanced plot_config, only! to be sure - #indicator colors given in advanced plot_config will not be - #overwritten. + # add indicators with NO advanced plot_config, only! to be sure + # indicator colors given in advanced plot_config will not be + # overwritten. if ind not in plot_config['main_plot'].keys(): plot_config['main_plot'][ind] = {} if indicators2: - #add other indicators given on cmd line to advanced plot_config. + # add other indicators given on cmd line to advanced plot_config. plot_config['subplots'].update( - {'Other' : {ind : {} for ind in indicators2}} + {'Other': {ind: {} for ind in indicators2}} ) if not plot_config: @@ -378,15 +378,15 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra del plot_config['main_plot']['bb_upperband'] del plot_config['main_plot']['bb_lowerband'] - #fill area betwenn traces i.e. for ichimoku + # fill area betwenn traces i.e. for ichimoku if 'fill_area' in plot_config.keys(): for label, area in plot_config['fill_area'].items(): traces = area['traces'] if len(traces) != 2: raise Exception( - f"plot_config.fill_area.traces = {traces}: " \ - + f"needs exactly 2 indicators. " \ - + f"{len(traces)} is given." + f"plot_config.fill_area.traces = {traces}: " + + f"needs exactly 2 indicators. " + + f"{len(traces)} is given." ) color = area['color'] if traces[0] in data and traces[1] in data: @@ -406,6 +406,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra ) fig.add_trace(trace_b) fig.add_trace(trace_a) + # Add indicators to main plot fig = add_indicators(fig=fig, row=1, indicators=plot_config['main_plot'], data=data) From cc39cf97ddee25768ed26f6b8b8afc8f8fd52857 Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 14:14:20 +0100 Subject: [PATCH 1161/1197] revert to former create_plotconfig behaviour --- freqtrade/plot/plotting.py | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index bed0319a6..7aa332501 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -226,7 +226,6 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: logger.warning("No trades found.") return fig - def create_plotconfig(indicators1: List[str], indicators2: List[str], plot_config: Dict[str, Dict]) -> Dict[str, Dict]: """ @@ -236,25 +235,12 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], :param plot_config: Dict of Dicts containing advanced plot configuration :return: plot_config - eventually with indicators 1 and 2 """ - if plot_config: - # maybe main or sub is given, not both. - if 'main_plot' not in plot_config.keys(): - plot_config['main_plot'] = {} - if 'subplots' not in plot_config.keys(): - plot_config['subplots'] = {} + if plot_config: if indicators1: - for ind in indicators1: - # add indicators with NO advanced plot_config, only! to be sure - # indicator colors given in advanced plot_config will not be - # overwritten. - if ind not in plot_config['main_plot'].keys(): - plot_config['main_plot'][ind] = {} + plot_config['main_plot'] = {ind: {} for ind in indicators1} if indicators2: - # add other indicators given on cmd line to advanced plot_config. - plot_config['subplots'].update( - {'Other': {ind: {} for ind in indicators2}} - ) + plot_config['subplots'] = {'Other': {ind: {} for ind in indicators2}} if not plot_config: # If no indicators and no plot-config given, use defaults. @@ -268,10 +254,13 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], 'main_plot': {ind: {} for ind in indicators1}, 'subplots': {'Other': {ind: {} for ind in indicators2}}, } + if 'main_plot' not in plot_config: + plot_config['main_plot'] = {} + if 'subplots' not in plot_config: + plot_config['subplots'] = {} return plot_config - def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, *, indicators1: List[str] = [], indicators2: List[str] = [], From 75e4758936ac99d3fc9f756730d6f678480c8035 Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 17:06:21 +0100 Subject: [PATCH 1162/1197] changed config params, added fill area in subplots --- freqtrade/plot/plotting.py | 113 +++++++++++++++++++------------------ 1 file changed, 59 insertions(+), 54 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 7aa332501..62a667e03 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -226,6 +226,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: logger.warning("No trades found.") return fig + def create_plotconfig(indicators1: List[str], indicators2: List[str], plot_config: Dict[str, Dict]) -> Dict[str, Dict]: """ @@ -261,6 +262,34 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], plot_config['subplots'] = {} return plot_config + +def add_filled_traces(fig, row: int, data: pd.DataFrame, indicator_a: str, + indicator_b: str, label: str = "", + fill_color: str = "rgba(0,176,246,0.2)") -> make_subplots: + """ Adds plots for two traces, which are filled between to fig. + :param fig: Plot figure to append to + :param row: row number for this plot + :param data: candlestick DataFrame + :param indicator_a: indicator name as populated in stragetie + :param indicator_b: indicator name as populated in stragetie + :param label: label for the filled area + :param fill_color: color to be used for the filled area + :return: fig with added filled_traces plot + """ + if indicator_a in data and indicator_b in data: + # TODO: Figure out why scattergl causes problems plotly/plotly.js#2284 + trace_a = go.Scatter(x=data.date, y=data[indicator_a], + showlegend=False, + line={'color': 'rgba(255,255,255,0)'}) + + trace_b = go.Scatter(x=data.date, y=data[indicator_b], name=label, + fill="tonexty", fillcolor=fill_color, + line={'color': 'rgba(255,255,255,0)'}) + fig.add_trace(trace_a, row, 1) + fig.add_trace(trace_b, row, 1) + return fig + + def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, *, indicators1: List[str] = [], indicators2: List[str] = [], @@ -344,61 +373,28 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra else: logger.warning("No sell-signals found.") - # TODO: Figure out why scattergl causes problems plotly/plotly.js#2284 - if 'bb_lowerband' in data and 'bb_upperband' in data: - bb_lower = go.Scatter( - x=data.date, - y=data.bb_lowerband, - showlegend=False, - line={'color': 'rgba(255,255,255,0)'}, - ) - bb_upper = go.Scatter( - x=data.date, - y=data.bb_upperband, - name='Bollinger Band', - fill="tonexty", - fillcolor="rgba(0,176,246,0.2)", - line={'color': 'rgba(255,255,255,0)'}, - ) - fig.add_trace(bb_lower, 1, 1) - fig.add_trace(bb_upper, 1, 1) - if ('bb_upperband' in plot_config['main_plot'] - and 'bb_lowerband' in plot_config['main_plot']): - del plot_config['main_plot']['bb_upperband'] - del plot_config['main_plot']['bb_lowerband'] - - # fill area betwenn traces i.e. for ichimoku - if 'fill_area' in plot_config.keys(): - for label, area in plot_config['fill_area'].items(): - traces = area['traces'] - if len(traces) != 2: - raise Exception( - f"plot_config.fill_area.traces = {traces}: " + - f"needs exactly 2 indicators. " + - f"{len(traces)} is given." - ) - color = area['color'] - if traces[0] in data and traces[1] in data: - trace_b = go.Scatter( - x=data.date, - y=data.get(traces[0]), - showlegend=False, - line={'color': 'rgba(255,255,255,0)'}, - ) - trace_a = go.Scatter( - x=data.date, - y=data.get(traces[1]), - name=label, - fill="tonexty", - fillcolor=color, - line={'color': 'rgba(255,255,255,0)'}, - ) - fig.add_trace(trace_b) - fig.add_trace(trace_a) + # Add Boilinger Bands + fig = add_filled_traces(fig, 1, data, 'bb_lowerband', 'bb_upperband', + label="Boillinger Band") + # prevent bb_lower and bb_upper from plotting + try: + del plot_config['main_plot']['bb_lowerband'] + del plot_config['main_plot']['bb_upperband'] + except KeyError: + pass # Add indicators to main plot fig = add_indicators(fig=fig, row=1, indicators=plot_config['main_plot'], data=data) + # fill area between indicators ( 'fill_to': 'other_indicator') + for indicator, ind_conf in plot_config['main_plot'].items(): + if 'fill_to' in ind_conf: + label = ind_conf.get('fill_label', '') + fill_color = ind_conf.get('fill_color', 'rgba(0,176,246,0.2)') + fig = add_filled_traces(fig, 1, data, indicator, + ind_conf['fill_to'], label=label, + fill_color=fill_color) + fig = plot_trades(fig, trades) # Volume goes to row 2 @@ -412,11 +408,20 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra fig.add_trace(volume, 2, 1) # Add indicators to separate row - for i, name in enumerate(plot_config['subplots']): - fig = add_indicators(fig=fig, row=3 + i, - indicators=plot_config['subplots'][name], + for i, label in enumerate(plot_config['subplots']): + sub_config = plot_config['subplots'][label] + row = 3 + i + fig = add_indicators(fig=fig, row=row, indicators=sub_config, data=data) + # fill area between indicators ( 'fill_to': 'other_indicator') + for indicator, ind_config in sub_config.items(): + if 'fill_to' in ind_config: + label = ind_config.get('fill_label', '') + fill_color = ind_config.get('fill_color', 'rgba(0,176,246,0.2)') + fig = add_filled_traces(fig, row, data, indicator, + ind_config['fill_to'], label=label, + fill_color=fill_color) return fig From d901a86165736df19eae7495f631de7f66b751d3 Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 17:19:32 +0100 Subject: [PATCH 1163/1197] typo --- 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 62a667e03..b77b76e1a 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -375,7 +375,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra # Add Boilinger Bands fig = add_filled_traces(fig, 1, data, 'bb_lowerband', 'bb_upperband', - label="Boillinger Band") + label="Bollinger Band") # prevent bb_lower and bb_upper from plotting try: del plot_config['main_plot']['bb_lowerband'] From 16baca5eeb2bca0ff79746d8dc420b6042428a7a Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 17:42:22 +0100 Subject: [PATCH 1164/1197] fixed: too complex warning --- freqtrade/plot/plotting.py | 46 +++++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index b77b76e1a..7d17585a1 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -263,10 +263,10 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], return plot_config -def add_filled_traces(fig, row: int, data: pd.DataFrame, indicator_a: str, +def plot_area_between(fig, row: int, data: pd.DataFrame, indicator_a: str, indicator_b: str, label: str = "", fill_color: str = "rgba(0,176,246,0.2)") -> make_subplots: - """ Adds plots for two traces, which are filled between to fig. + """ Plots the area between two traces and adds it to fig. :param fig: Plot figure to append to :param row: row number for this plot :param data: candlestick DataFrame @@ -290,6 +290,25 @@ def add_filled_traces(fig, row: int, data: pd.DataFrame, indicator_a: str, return fig +def add_areas(fig, row: int, data: pd.DataFrame, indicators) -> make_subplots: + """ Adds all areas (from plot_config) to fig. + :param fig: Plot figure to append to + :param row: row number for this plot + :param data: candlestick DataFrame + :param indicators: dict with indicators. ie.: plot_config['main_plot'] or + plot_config['subplots'][subplot_label] + :return: fig with added filled_traces plot + """ + for indicator, ind_conf in indicators.items(): + if 'fill_to' in ind_conf: + label = ind_conf.get('fill_label', '') + fill_color = ind_conf.get('fill_color', 'rgba(0,176,246,0.2)') + fig = plot_area_between(fig, row, data, indicator, + ind_conf['fill_to'], label=label, + fill_color=fill_color) + return fig + + def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, *, indicators1: List[str] = [], indicators2: List[str] = [], @@ -373,8 +392,8 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra else: logger.warning("No sell-signals found.") - # Add Boilinger Bands - fig = add_filled_traces(fig, 1, data, 'bb_lowerband', 'bb_upperband', + # Add Bollinger Bands + fig = plot_area_between(fig, 1, data, 'bb_lowerband', 'bb_upperband', label="Bollinger Band") # prevent bb_lower and bb_upper from plotting try: @@ -385,15 +404,8 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra # Add indicators to main plot fig = add_indicators(fig=fig, row=1, indicators=plot_config['main_plot'], data=data) - # fill area between indicators ( 'fill_to': 'other_indicator') - for indicator, ind_conf in plot_config['main_plot'].items(): - if 'fill_to' in ind_conf: - label = ind_conf.get('fill_label', '') - fill_color = ind_conf.get('fill_color', 'rgba(0,176,246,0.2)') - fig = add_filled_traces(fig, 1, data, indicator, - ind_conf['fill_to'], label=label, - fill_color=fill_color) + fig = add_areas(fig, 1, data, plot_config['main_plot']) fig = plot_trades(fig, trades) @@ -413,15 +425,9 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra row = 3 + i fig = add_indicators(fig=fig, row=row, indicators=sub_config, data=data) - # fill area between indicators ( 'fill_to': 'other_indicator') - for indicator, ind_config in sub_config.items(): - if 'fill_to' in ind_config: - label = ind_config.get('fill_label', '') - fill_color = ind_config.get('fill_color', 'rgba(0,176,246,0.2)') - fig = add_filled_traces(fig, row, data, indicator, - ind_config['fill_to'], label=label, - fill_color=fill_color) + fig = add_areas(fig, row, data, sub_config) + return fig From 5b2902fcbcdd9ff4a3ed9510f2f064becaa0f3e7 Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 17:48:08 +0100 Subject: [PATCH 1165/1197] cleanup --- freqtrade/plot/plotting.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 7d17585a1..0f8c99852 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -266,7 +266,7 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], def plot_area_between(fig, row: int, data: pd.DataFrame, indicator_a: str, indicator_b: str, label: str = "", fill_color: str = "rgba(0,176,246,0.2)") -> make_subplots: - """ Plots the area between two traces and adds it to fig. + """ Creates plot for the area between two traces and adds it to fig. :param fig: Plot figure to append to :param row: row number for this plot :param data: candlestick DataFrame @@ -277,21 +277,21 @@ def plot_area_between(fig, row: int, data: pd.DataFrame, indicator_a: str, :return: fig with added filled_traces plot """ if indicator_a in data and indicator_b in data: + line = {'color': 'rgba(255,255,255,0)'} # TODO: Figure out why scattergl causes problems plotly/plotly.js#2284 trace_a = go.Scatter(x=data.date, y=data[indicator_a], showlegend=False, - line={'color': 'rgba(255,255,255,0)'}) - + line=line) trace_b = go.Scatter(x=data.date, y=data[indicator_b], name=label, fill="tonexty", fillcolor=fill_color, - line={'color': 'rgba(255,255,255,0)'}) + line=line) fig.add_trace(trace_a, row, 1) fig.add_trace(trace_b, row, 1) return fig def add_areas(fig, row: int, data: pd.DataFrame, indicators) -> make_subplots: - """ Adds all areas (from plot_config) to fig. + """ Adds all area plots (specified in plot_config) to fig. :param fig: Plot figure to append to :param row: row number for this plot :param data: candlestick DataFrame From 8b248780231c9b5306a1f42ef0764402f0ffd12e Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 18:21:26 +0100 Subject: [PATCH 1166/1197] plot_config documentation for fill_to, fill_label, fill_color --- docs/plotting.md | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/docs/plotting.md b/docs/plotting.md index f794cdedc..30844606c 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -168,7 +168,7 @@ Additional features when using plot_config include: * Specify colors per indicator * Specify additional subplots -* Specify idicator pairs to fill area in between +* Specify indicator pairs to fill area in between The sample plot configuration below specifies fixed colors for the indicators. Otherwise consecutive plots may produce different colorschemes each time, making comparisons difficult. It also allows multiple subplots to display both MACD and RSI at the same time. @@ -184,29 +184,32 @@ Sample configuration with inline comments explaining the process: 'ema50': {'color': '#CCCCCC'}, # By omitting color, a random color is selected. 'sar': {}, + # fill area between senkou_a and senkou_b + 'senkou_a': { + 'color': 'green', #optional + 'fill_to': 'senkou_b', + 'fill_label': 'Ichimoku Cloud' #optional, + 'fill_color': 'rgba(255,76,46,0.2)', #optional + }, + # plot senkou_b, too. Not only the area to it. + 'senkou_b': {} }, 'subplots': { # Create subplot MACD "MACD": { - 'macd': {'color': 'blue'}, - 'macdsignal': {'color': 'orange'}, + 'macd': {'color': 'blue', 'fill_to': 'macdhist'}, + 'macdsignal': {'color': 'orange'} }, # Additional subplot RSI "RSI": { - 'rsi': {'color': 'red'}, + 'rsi': {'color': 'red'} } - }, - 'fill_area': { - "Ichimoku Cloud": { - 'traces': ('senkou_a', 'senkou_b'), - 'color': 'rgba(0,176,246,0.2)', - }, - } + } } -``` +``` !!! Note - The above configuration assumes that `ema10`, `ema50`, `macd`, `macdsignal` and `rsi` are columns in the DataFrame created by the strategy. + The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`, `macd`, `macdsignal` and `rsi` are columns in the DataFrame created by the strategy. ## Plot profit From 43091a26ce19ce4bdfadf012ff6e9ff83b559101 Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 20:32:13 +0100 Subject: [PATCH 1167/1197] simple tests --- freqtrade/plot/plotting.py | 21 +++++++++++++++----- tests/test_plotting.py | 40 +++++++++++++++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 0f8c99852..6e92bced8 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -301,11 +301,22 @@ def add_areas(fig, row: int, data: pd.DataFrame, indicators) -> make_subplots: """ for indicator, ind_conf in indicators.items(): if 'fill_to' in ind_conf: - label = ind_conf.get('fill_label', '') - fill_color = ind_conf.get('fill_color', 'rgba(0,176,246,0.2)') - fig = plot_area_between(fig, row, data, indicator, - ind_conf['fill_to'], label=label, - fill_color=fill_color) + indicator_b = ind_conf['fill_to'] + if indicator in data and indicator_b in data: + label = ind_conf.get('fill_label', '') + fill_color = ind_conf.get('fill_color', 'rgba(0,176,246,0.2)') + fig = plot_area_between(fig, row, data, indicator, indicator_b, + label=label, fill_color=fill_color) + elif indicator not in data: + logger.info( + 'Indicator "%s" ignored. Reason: This indicator is not ' + 'found in your strategy.', indicator + ) + elif indicator_b not in data: + logger.info( + 'fill_to: "%s" ignored. Reason: This indicator is not ' + 'in your strategy.', indicator_b + ) return fig diff --git a/tests/test_plotting.py b/tests/test_plotting.py index d3f97013d..96eff4c69 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -16,7 +16,8 @@ from freqtrade.exceptions import OperationalException from freqtrade.plot.plotting import (add_indicators, add_profit, create_plotconfig, generate_candlestick_graph, generate_plot_filename, generate_profit_graph, init_plotscript, load_and_plot_trades, - plot_profit, plot_trades, store_plot_file) + plot_profit, plot_trades, store_plot_file, + add_areas) from freqtrade.resolvers import StrategyResolver from tests.conftest import get_args, log_has, log_has_re, patch_exchange @@ -96,6 +97,42 @@ def test_add_indicators(default_conf, testdatadir, caplog): assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog) +def test_add_areas(default_conf, testdatadir, caplog): + pair = "UNITTEST/BTC" + timerange = TimeRange(None, 'line', 0, -1000) + + data = history.load_pair_history(pair=pair, timeframe='1m', + datadir=testdatadir, timerange=timerange) + indicators = {"macd": {"color": "red", + "fill_color": "black", + "fill_to": "macdhist", + "fill_label": "MACD Fill"}} + + default_conf.update({'strategy': 'DefaultStrategy'}) + strategy = StrategyResolver.load_strategy(default_conf) + + # Generate buy/sell signals and indicators + data = strategy.analyze_ticker(data, {'pair': pair}) + fig = generate_empty_figure() + + # indicator mentioned in fill_to does not exist + fig1 = add_areas(fig, 1, data, {'ema10': {'fill_to': 'no_fill_indicator'}}) + assert fig == fig1 + assert log_has_re(r'fill_to: "no_fill_indicator" ignored\..*', caplog) + + # indicator does not exist + fig2 = add_areas(fig, 1, data, {'no_indicator': {'fill_to': 'ema10'}}) + assert fig == fig2 + assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog) + + fig3 = add_areas(fig, 3, data, indicators) + figure = fig3.layout.figure + fill_macd = find_trace_in_fig_data(figure.data, "MACD Fill") + assert isinstance(fill_macd, go.Scatter) + assert fill_macd.yaxis == "y3" + assert fill_macd.fillcolor == "black" + + def test_plot_trades(testdatadir, caplog): fig1 = generate_empty_figure() # nothing happens when no trades are available @@ -136,6 +173,7 @@ def test_plot_trades(testdatadir, caplog): assert trade_sell_loss.text[5] == '-10.4%, stop_loss, 720 min' + def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, testdatadir, caplog): row_mock = mocker.patch('freqtrade.plot.plotting.add_indicators', MagicMock(side_effect=fig_generating_mock)) From f24626e13920377f0208292a4f5326ccf1eca023 Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 20:42:19 +0100 Subject: [PATCH 1168/1197] removed too many blank lines --- tests/test_plotting.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 96eff4c69..379889ea1 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -173,7 +173,6 @@ def test_plot_trades(testdatadir, caplog): assert trade_sell_loss.text[5] == '-10.4%, stop_loss, 720 min' - def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, testdatadir, caplog): row_mock = mocker.patch('freqtrade.plot.plotting.add_indicators', MagicMock(side_effect=fig_generating_mock)) From f120c8d6c7c33419b315ebde415f17c1ec242f87 Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 20:47:25 +0100 Subject: [PATCH 1169/1197] documentation --- docs/plotting.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/plotting.md b/docs/plotting.md index 30844606c..ed682e44b 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -209,7 +209,8 @@ Sample configuration with inline comments explaining the process: ``` !!! Note - The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`, `macd`, `macdsignal` and `rsi` are columns in the DataFrame created by the strategy. + The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`, + `macd`, `macdsignal`, `macdhist` and `rsi` are columns in the DataFrame created by the strategy. ## Plot profit From fabb31e1bc20a0040609ce180787a98f3d83870f Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 20:50:15 +0100 Subject: [PATCH 1170/1197] imports order --- tests/test_plotting.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 379889ea1..5bd9f4f9d 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -13,11 +13,10 @@ from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data from freqtrade.exceptions import OperationalException -from freqtrade.plot.plotting import (add_indicators, add_profit, create_plotconfig, +from freqtrade.plot.plotting import (add_areas, add_indicators, add_profit, create_plotconfig, generate_candlestick_graph, generate_plot_filename, generate_profit_graph, init_plotscript, load_and_plot_trades, - plot_profit, plot_trades, store_plot_file, - add_areas) + plot_profit, plot_trades, store_plot_file) from freqtrade.resolvers import StrategyResolver from tests.conftest import get_args, log_has, log_has_re, patch_exchange From c1b8ad723261d89157720d9bfe4c77e46c0bfe5f Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 21:37:52 +0100 Subject: [PATCH 1171/1197] renaming, comments, cleanups --- freqtrade/plot/plotting.py | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 6e92bced8..b4adf4049 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -263,10 +263,10 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], return plot_config -def plot_area_between(fig, row: int, data: pd.DataFrame, indicator_a: str, - indicator_b: str, label: str = "", - fill_color: str = "rgba(0,176,246,0.2)") -> make_subplots: - """ Creates plot for the area between two traces and adds it to fig. +def plot_area(fig, row: int, data: pd.DataFrame, indicator_a: str, + indicator_b: str, label: str = "", + fill_color: str = "rgba(0,176,246,0.2)") -> make_subplots: + """ Creates a plot for the area between two traces and adds it to fig. :param fig: Plot figure to append to :param row: row number for this plot :param data: candlestick DataFrame @@ -277,6 +277,7 @@ def plot_area_between(fig, row: int, data: pd.DataFrame, indicator_a: str, :return: fig with added filled_traces plot """ if indicator_a in data and indicator_b in data: + # make lines invisible to get the area plotted, only. line = {'color': 'rgba(255,255,255,0)'} # TODO: Figure out why scattergl causes problems plotly/plotly.js#2284 trace_a = go.Scatter(x=data.date, y=data[indicator_a], @@ -303,10 +304,11 @@ def add_areas(fig, row: int, data: pd.DataFrame, indicators) -> make_subplots: if 'fill_to' in ind_conf: indicator_b = ind_conf['fill_to'] if indicator in data and indicator_b in data: - label = ind_conf.get('fill_label', '') + label = ind_conf.get('fill_label', + f'{indicator}<>{indicator_b}') fill_color = ind_conf.get('fill_color', 'rgba(0,176,246,0.2)') - fig = plot_area_between(fig, row, data, indicator, indicator_b, - label=label, fill_color=fill_color) + fig = plot_area(fig, row, data, indicator, indicator_b, + label=label, fill_color=fill_color) elif indicator not in data: logger.info( 'Indicator "%s" ignored. Reason: This indicator is not ' @@ -402,25 +404,20 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra fig.add_trace(sells, 1, 1) else: logger.warning("No sell-signals found.") - # Add Bollinger Bands - fig = plot_area_between(fig, 1, data, 'bb_lowerband', 'bb_upperband', - label="Bollinger Band") + fig = plot_area(fig, 1, data, 'bb_lowerband', 'bb_upperband', + label="Bollinger Band") # prevent bb_lower and bb_upper from plotting try: del plot_config['main_plot']['bb_lowerband'] del plot_config['main_plot']['bb_upperband'] except KeyError: pass - - # Add indicators to main plot + # main plot goes to row 1 fig = add_indicators(fig=fig, row=1, indicators=plot_config['main_plot'], data=data) - # fill area between indicators ( 'fill_to': 'other_indicator') fig = add_areas(fig, 1, data, plot_config['main_plot']) - fig = plot_trades(fig, trades) - - # Volume goes to row 2 + # sub plot: Volume goes to row 2 volume = go.Bar( x=data['date'], y=data['volume'], @@ -429,8 +426,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra marker_line_color='DarkSlateGrey' ) fig.add_trace(volume, 2, 1) - - # Add indicators to separate row + # Add each sub plot to a separate row for i, label in enumerate(plot_config['subplots']): sub_config = plot_config['subplots'][label] row = 3 + i @@ -438,7 +434,6 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra data=data) # fill area between indicators ( 'fill_to': 'other_indicator') fig = add_areas(fig, row, data, sub_config) - return fig From 3cb559994e108af03239ed4bce3f1769d3cd05b1 Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 21:47:11 +0100 Subject: [PATCH 1172/1197] some more test --- tests/test_plotting.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 5bd9f4f9d..42847ca50 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -107,6 +107,10 @@ def test_add_areas(default_conf, testdatadir, caplog): "fill_to": "macdhist", "fill_label": "MACD Fill"}} + ind_no_label = {"macd": {"fill_color": "red", + "fill_to": "macdhist"}} + + ind_plain = {"macd": {"fill_to": "macdhist"}} default_conf.update({'strategy': 'DefaultStrategy'}) strategy = StrategyResolver.load_strategy(default_conf) @@ -124,6 +128,7 @@ def test_add_areas(default_conf, testdatadir, caplog): assert fig == fig2 assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog) + # everythin given in plot config, row 3 fig3 = add_areas(fig, 3, data, indicators) figure = fig3.layout.figure fill_macd = find_trace_in_fig_data(figure.data, "MACD Fill") @@ -131,6 +136,21 @@ def test_add_areas(default_conf, testdatadir, caplog): assert fill_macd.yaxis == "y3" assert fill_macd.fillcolor == "black" + # label missing, row 1 + fig4 = add_areas(fig, 1, data, ind_no_label) + figure = fig4.layout.figure + fill_macd = find_trace_in_fig_data(figure.data, "macd<>macdhist") + assert isinstance(fill_macd, go.Scatter) + assert fill_macd.yaxis == "y" + assert fill_macd.fillcolor == "red" + + # fit_to only + fig5 = add_areas(fig, 1, data, ind_plain) + figure = fig5.layout.figure + fill_macd = find_trace_in_fig_data(figure.data, "macd<>macdhist") + assert isinstance(fill_macd, go.Scatter) + assert fill_macd.yaxis == "y" + def test_plot_trades(testdatadir, caplog): fig1 = generate_empty_figure() From 18a24d75efcbdc19f162d9c4310ae257a5252a3a Mon Sep 17 00:00:00 2001 From: Christof Date: Sat, 19 Dec 2020 22:01:33 +0100 Subject: [PATCH 1173/1197] cleanup --- 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 b4adf4049..497218deb 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -426,7 +426,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra marker_line_color='DarkSlateGrey' ) fig.add_trace(volume, 2, 1) - # Add each sub plot to a separate row + # add each sub plot to a separate row for i, label in enumerate(plot_config['subplots']): sub_config = plot_config['subplots'][label] row = 3 + i From f39dde121ab5d836d1be616d19858c7f35fe1fe2 Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 20 Dec 2020 22:36:56 +0100 Subject: [PATCH 1174/1197] moved keyboard config validation to __inti__ --- freqtrade/rpc/telegram.py | 79 ++++++++++++++++++---------------- tests/rpc/test_rpc_telegram.py | 14 ++++-- 2 files changed, 52 insertions(+), 41 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 466c58878..1ba45e089 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -7,11 +7,11 @@ import json import logging from datetime import timedelta from itertools import chain -from typing import Any, Callable, Dict, List, Union +from typing import Any, Callable, Dict, List import arrow from tabulate import tabulate -from telegram import KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update +from telegram import ParseMode, ReplyKeyboardMarkup, Update from telegram.error import NetworkError, TelegramError from telegram.ext import CallbackContext, CommandHandler, Updater from telegram.utils.helpers import escape_markdown @@ -75,10 +75,48 @@ class Telegram(RPC): self._updater: Updater self._config = freqtrade.config + self._validate_keyboard() self._init() if self._config.get('fiat_display_currency', None): self._fiat_converter = CryptoToFiatConverter() + def _validate_keyboard(self) -> None: + """ + Validates the keyboard configuration from telegram config + section. + """ + self._keyboard: List[List[str]] = [ + ['/daily', '/profit', '/balance'], + ['/status', '/status table', '/performance'], + ['/count', '/start', '/stop', '/help'] + ] + # do not allow commands with mandatory arguments and critical cmds + # like /forcesell and /forcebuy + # TODO: DRY! - its not good to list all valid cmds here. But otherwise + # this needs refacoring of the whole telegram module (same + # problem in _help()). + valid_keys: List[str] = ['/start', '/stop', '/status', '/status table', + '/trades', '/profit', '/performance', '/daily', + '/stats', '/count', '/locks', '/balance', + '/stopbuy', '/reload_config', '/show_config', + '/logs', '/whitelist', '/blacklist', '/edge', + '/help', '/version'] + + # custom shortcuts specified in config.json + cust_keyboard = self._config['telegram'].get('keyboard', []) + if cust_keyboard: + # check for valid shortcuts + invalid_keys = [b for b in chain.from_iterable(cust_keyboard) + if b not in valid_keys] + if len(invalid_keys): + logger.warning('rpc.telegram: invalid commands for custom ' + f'keyboard: {invalid_keys}') + logger.info('rpc.telegram: using default keyboard.') + else: + self._keyboard = cust_keyboard + logger.info('rpc.telegram using custom keyboard from ' + f'config.json: {self._keyboard}') + def _init(self) -> None: """ Initializes this module with the given config, @@ -862,42 +900,7 @@ class Telegram(RPC): :param parse_mode: telegram parse mode :return: None """ - - # default / fallback shortcut buttons - keyboard: List[List[Union[str, KeyboardButton]]] = [ - ['/daily', '/profit', '/balance'], - ['/status', '/status table', '/performance'], - ['/count', '/start', '/stop', '/help'] - ] - - # do not allow commands with mandatory arguments and critical cmds - # like /forcesell and /forcebuy - # TODO: DRY! - its not good to list all valid cmds here. But this - # needs refacoring of the whole telegram module (same problem - # in _help()). - valid_btns: List[str] = ['/start', '/stop', '/status', '/status table', - '/trades', '/profit', '/performance', '/daily', - '/stats', '/count', '/locks', '/balance', - '/stopbuy', '/reload_config', '/show_config', - '/logs', '/whitelist', '/blacklist', '/edge', - '/help', '/version'] - # custom shortcuts specified in config.json - cust_keyboard = self._config['telegram'].get('keyboard', []) - if cust_keyboard: - # check for valid shortcuts - invalid_keys = [b for b in chain.from_iterable(cust_keyboard) - if b not in valid_btns] - if len(invalid_keys): - logger.warning('rpc.telegram: invalid commands for custom ' - f'keyboard: {invalid_keys}') - logger.info('rpc.telegram: using default keyboard.') - else: - keyboard = cust_keyboard - logger.info('rpc.telegram using custom keyboard from ' - f'config.json: {[btn for btn in keyboard]}') - - reply_markup = ReplyKeyboardMarkup(keyboard) - + reply_markup = ReplyKeyboardMarkup(self._keyboard) try: try: self._updater.bot.send_message( diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index ce3db7130..148eb6428 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1751,14 +1751,17 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: custom_keyboard = ReplyKeyboardMarkup(custom_keys_list) # no keyboard in config -> default keyboard - telegram._config['telegram']['enabled'] = True + # telegram._config['telegram']['enabled'] = True telegram._send_msg('test') used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == default_keyboard # invalid keyboard in config -> default keyboard - telegram._config['telegram']['enabled'] = True - telegram._config['telegram']['keyboard'] = invalid_keys_list + freqtradebot.config['telegram']['enabled'] = True + freqtradebot.config['telegram']['keyboard'] = invalid_keys_list + telegram = Telegram(freqtradebot) + telegram._updater = MagicMock() + telegram._updater.bot = bot telegram._send_msg('test') used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == default_keyboard @@ -1767,6 +1770,11 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: assert log_has('rpc.telegram: using default keyboard.', caplog) # valid keyboard in config -> custom keyboard + freqtradebot.config['telegram']['enabled'] = True + freqtradebot.config['telegram']['keyboard'] = custom_keys_list + telegram = Telegram(freqtradebot) + telegram._updater = MagicMock() + telegram._updater.bot = bot telegram._config['telegram']['enabled'] = True telegram._config['telegram']['keyboard'] = custom_keys_list telegram._send_msg('test') From 5423c21be0be73139e9ba4488a14ecb5eb53a2ca Mon Sep 17 00:00:00 2001 From: Christof Date: Sun, 20 Dec 2020 22:51:40 +0100 Subject: [PATCH 1175/1197] keyboard type --- freqtrade/rpc/telegram.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 1ba45e089..5be880bcc 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -7,11 +7,11 @@ import json import logging from datetime import timedelta from itertools import chain -from typing import Any, Callable, Dict, List +from typing import Any, Callable, Dict, List, Union import arrow from tabulate import tabulate -from telegram import ParseMode, ReplyKeyboardMarkup, Update +from telegram import KeyboardButton, ParseMode, ReplyKeyboardMarkup, Update from telegram.error import NetworkError, TelegramError from telegram.ext import CallbackContext, CommandHandler, Updater from telegram.utils.helpers import escape_markdown @@ -85,7 +85,7 @@ class Telegram(RPC): Validates the keyboard configuration from telegram config section. """ - self._keyboard: List[List[str]] = [ + self._keyboard: List[List[Union[str, KeyboardButton]]] = [ ['/daily', '/profit', '/balance'], ['/status', '/status table', '/performance'], ['/count', '/start', '/stop', '/help'] From e7e687c8ec84485f45dd8899d587a1560c640220 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Dec 2020 05:36:08 +0000 Subject: [PATCH 1176/1197] Bump requests from 2.25.0 to 2.25.1 Bumps [requests](https://github.com/psf/requests) from 2.25.0 to 2.25.1. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/master/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.25.0...v2.25.1) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2e3a1a36f..c221338a4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,7 @@ SQLAlchemy==1.3.20 python-telegram-bot==13.1 arrow==0.17.0 cachetools==4.2.0 -requests==2.25.0 +requests==2.25.1 urllib3==1.26.2 wrapt==1.12.1 jsonschema==3.2.0 From a1755364e1011080f9b443240fb45996a7e51684 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Dec 2020 05:36:09 +0000 Subject: [PATCH 1177/1197] Bump pytest from 6.2.0 to 6.2.1 Bumps [pytest](https://github.com/pytest-dev/pytest) from 6.2.0 to 6.2.1. - [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/6.2.0...6.2.1) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 6d7570f67..66cb5bd1b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.8.4 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.0 mypy==0.790 -pytest==6.2.0 +pytest==6.2.1 pytest-asyncio==0.14.0 pytest-cov==2.10.1 pytest-mock==3.3.1 From fe27206926b5fcd67c591dff6e0ed2b902454629 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Dec 2020 05:36:12 +0000 Subject: [PATCH 1178/1197] Bump questionary from 1.8.1 to 1.9.0 Bumps [questionary](https://github.com/tmbo/questionary) from 1.8.1 to 1.9.0. - [Release notes](https://github.com/tmbo/questionary/releases) - [Commits](https://github.com/tmbo/questionary/compare/1.8.1...1.9.0) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2e3a1a36f..f27544cff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -35,5 +35,5 @@ flask-cors==3.0.9 # Support for colorized terminal output colorama==0.4.4 # Building config files interactively -questionary==1.8.1 +questionary==1.9.0 prompt-toolkit==3.0.8 From 5716202e45f8651cc700854b6daa4c35c8aa9f4f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Dec 2020 05:36:24 +0000 Subject: [PATCH 1179/1197] Bump joblib from 0.17.0 to 1.0.0 Bumps [joblib](https://github.com/joblib/joblib) from 0.17.0 to 1.0.0. - [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.17.0...1.0.0) Signed-off-by: dependabot[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 7e480b8c9..c51062bf7 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -6,5 +6,5 @@ scipy==1.5.4 scikit-learn==0.23.2 scikit-optimize==0.8.1 filelock==3.0.12 -joblib==0.17.0 +joblib==1.0.0 progressbar2==3.53.1 From a2873096c8efa911c1a2b5655bac6d4e4f4fc5a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Dec 2020 05:36:24 +0000 Subject: [PATCH 1180/1197] Bump flake8-tidy-imports from 4.2.0 to 4.2.1 Bumps [flake8-tidy-imports](https://github.com/adamchainz/flake8-tidy-imports) from 4.2.0 to 4.2.1. - [Release notes](https://github.com/adamchainz/flake8-tidy-imports/releases) - [Changelog](https://github.com/adamchainz/flake8-tidy-imports/blob/master/HISTORY.rst) - [Commits](https://github.com/adamchainz/flake8-tidy-imports/compare/4.2.0...4.2.1) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 6d7570f67..6e936e91f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,7 +6,7 @@ coveralls==2.2.0 flake8==3.8.4 flake8-type-annotations==0.1.0 -flake8-tidy-imports==4.2.0 +flake8-tidy-imports==4.2.1 mypy==0.790 pytest==6.2.0 pytest-asyncio==0.14.0 From 3b67863914fa2b332ec9a80b2176ae4ee20b974f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Dec 2020 05:36:27 +0000 Subject: [PATCH 1181/1197] Bump ccxt from 1.39.33 to 1.39.52 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.39.33 to 1.39.52. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.39.33...1.39.52) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 2e3a1a36f..d5dac04fa 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.19.4 pandas==1.1.5 -ccxt==1.39.33 +ccxt==1.39.52 aiohttp==3.7.3 SQLAlchemy==1.3.20 python-telegram-bot==13.1 From 8eb01302001cad53a6c6d76023893153803e5eff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Dec 2020 08:18:53 +0000 Subject: [PATCH 1182/1197] Bump pytest-mock from 3.3.1 to 3.4.0 Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 3.3.1 to 3.4.0. - [Release notes](https://github.com/pytest-dev/pytest-mock/releases) - [Changelog](https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest-mock/compare/v3.3.1...v3.4.0) Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e2c8b61a1..a2da87430 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,7 +11,7 @@ mypy==0.790 pytest==6.2.1 pytest-asyncio==0.14.0 pytest-cov==2.10.1 -pytest-mock==3.3.1 +pytest-mock==3.4.0 pytest-random-order==1.0.4 isort==5.6.4 From d25fe58574731614c0e9b61afd72271370c5d40b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 21 Dec 2020 08:19:10 +0000 Subject: [PATCH 1183/1197] Bump sqlalchemy from 1.3.20 to 1.3.22 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.20 to 1.3.22. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 669bc3282..2c565fee5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ pandas==1.1.5 ccxt==1.39.52 aiohttp==3.7.3 -SQLAlchemy==1.3.20 +SQLAlchemy==1.3.22 python-telegram-bot==13.1 arrow==0.17.0 cachetools==4.2.0 From 277f3ff47b1ce955a44889158374aa5f24283455 Mon Sep 17 00:00:00 2001 From: Christof Date: Mon, 21 Dec 2020 09:52:10 +0100 Subject: [PATCH 1184/1197] tests: cleaup --- tests/rpc/test_rpc_telegram.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 148eb6428..26384a507 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1736,9 +1736,6 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: bot = MagicMock() bot.send_message = MagicMock() freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) - telegram._updater = MagicMock() - telegram._updater.bot = bot invalid_keys_list = [['/not_valid', '/profit'], ['/daily'], ['/alsoinvalid']] default_keys_list = [['/daily', '/profit', '/balance'], @@ -1750,8 +1747,15 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: ['/count', '/start', '/reload_config', '/help']] custom_keyboard = ReplyKeyboardMarkup(custom_keys_list) + def init_telegram(freqtradebot): + telegram = Telegram(freqtradebot) + telegram._updater = MagicMock() + telegram._updater.bot = bot + return telegram + # no keyboard in config -> default keyboard - # telegram._config['telegram']['enabled'] = True + freqtradebot.config['telegram']['enabled'] = True + telegram = init_telegram(freqtradebot) telegram._send_msg('test') used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == default_keyboard @@ -1759,9 +1763,7 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: # invalid keyboard in config -> default keyboard freqtradebot.config['telegram']['enabled'] = True freqtradebot.config['telegram']['keyboard'] = invalid_keys_list - telegram = Telegram(freqtradebot) - telegram._updater = MagicMock() - telegram._updater.bot = bot + telegram = init_telegram(freqtradebot) telegram._send_msg('test') used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == default_keyboard @@ -1772,11 +1774,7 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: # valid keyboard in config -> custom keyboard freqtradebot.config['telegram']['enabled'] = True freqtradebot.config['telegram']['keyboard'] = custom_keys_list - telegram = Telegram(freqtradebot) - telegram._updater = MagicMock() - telegram._updater.bot = bot - telegram._config['telegram']['enabled'] = True - telegram._config['telegram']['keyboard'] = custom_keys_list + telegram = init_telegram(freqtradebot) telegram._send_msg('test') used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == custom_keyboard From 2787ba080907092bc517e9cbdc9b7e5f0f1ffc30 Mon Sep 17 00:00:00 2001 From: Christof Date: Mon, 21 Dec 2020 10:03:27 +0100 Subject: [PATCH 1185/1197] added /locks to command list --- docs/telegram-usage.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index c940f59ac..965d16d87 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -106,6 +106,7 @@ official commands. You can ask at any moment for help with `/help`. | `/trades [limit]` | List all recently closed trades in a table format. | `/delete ` | Delete a specific trade from the Database. Tries to close open orders. Requires manual handling of this trade on the exchange. | `/count` | Displays number of trades used and available +| `/locks` | Show currently locked pairs. | `/profit` | Display a summary of your profit/loss from close trades and some stats about your performance | `/forcesell ` | Instantly sells the given trade (Ignoring `minimum_roi`). | `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`). From 78dff3d5103640aa0efccd610d847168de092d25 Mon Sep 17 00:00:00 2001 From: Christof Date: Mon, 21 Dec 2020 10:22:24 +0100 Subject: [PATCH 1186/1197] docs: Note syntax --- docs/telegram-usage.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 950b4df1e..da4a2e8dd 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -111,8 +111,8 @@ You can create your own keyboard in `config.json`: ] }, ``` -!! NOTE: Only a certain list of commands are allowed. Command arguments are not -supported! +!!! Note + Only a certain list of commands are allowed. Command arguments are not supported! ### Supported Commands `/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopbuy`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version` From 4dadfd199d8485c3920aab5e128d942c191a508f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 22 Dec 2020 07:36:53 +0100 Subject: [PATCH 1187/1197] Documentation syntax --- docs/telegram-usage.md | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index da4a2e8dd..c8d95d743 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -88,17 +88,22 @@ Example configuration showing the different settings: ``` ## Create a custom keyboard (command shortcut buttons) + Telegram allows us to create a custom keyboard with buttons for commands. The default custom keyboard looks like this. + ```python [ - ['/daily', '/profit', '/balance'], # row 1, 3 commands - ['/status', '/status table', '/performance'], # row 2, 3 commands - ['/count', '/start', '/stop', '/help'] # row 3, 4 commands + ["/daily", "/profit", "/balance"], # row 1, 3 commands + ["/status", "/status table", "/performance"], # row 2, 3 commands + ["/count", "/start", "/stop", "/help"] # row 3, 4 commands ] -``` +``` + ### Usage + You can create your own keyboard in `config.json`: + ``` json "telegram": { "enabled": true, @@ -107,14 +112,15 @@ You can create your own keyboard in `config.json`: "keyboard": [ ["/daily", "/stats", "/balance", "/profit"], ["/status table", "/performance"], - ["/reload_config", "/count", "/logs"] + ["/reload_config", "/count", "/logs"] ] }, ``` -!!! Note - Only a certain list of commands are allowed. Command arguments are not supported! -### Supported Commands - `/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopbuy`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version` + +!!! Note "Supported Commands" + Only the following commands are allowed. Command arguments are not supported! + + `/start`, `/stop`, `/status`, `/status table`, `/trades`, `/profit`, `/performance`, `/daily`, `/stats`, `/count`, `/locks`, `/balance`, `/stopbuy`, `/reload_config`, `/show_config`, `/logs`, `/whitelist`, `/blacklist`, `/edge`, `/help`, `/version` ## Telegram commands From be28b42bfa232274c7e3f974c3eee2f8878af14c Mon Sep 17 00:00:00 2001 From: Christof Date: Tue, 22 Dec 2020 12:34:21 +0100 Subject: [PATCH 1188/1197] Exception for invalid keyboard config --- freqtrade/rpc/telegram.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 5be880bcc..63a98e2b1 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -17,6 +17,7 @@ from telegram.ext import CallbackContext, CommandHandler, Updater from telegram.utils.helpers import escape_markdown from freqtrade.__init__ import __version__ +from freqtrade.exceptions import OperationalException from freqtrade.rpc import RPC, RPCException, RPCMessageType from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -75,12 +76,12 @@ class Telegram(RPC): self._updater: Updater self._config = freqtrade.config - self._validate_keyboard() + self._init_keyboard() self._init() if self._config.get('fiat_display_currency', None): self._fiat_converter = CryptoToFiatConverter() - def _validate_keyboard(self) -> None: + def _init_keyboard(self) -> None: """ Validates the keyboard configuration from telegram config section. @@ -102,19 +103,19 @@ class Telegram(RPC): '/logs', '/whitelist', '/blacklist', '/edge', '/help', '/version'] - # custom shortcuts specified in config.json + # custom keyboard specified in config.json cust_keyboard = self._config['telegram'].get('keyboard', []) if cust_keyboard: # check for valid shortcuts invalid_keys = [b for b in chain.from_iterable(cust_keyboard) if b not in valid_keys] if len(invalid_keys): - logger.warning('rpc.telegram: invalid commands for custom ' - f'keyboard: {invalid_keys}') - logger.info('rpc.telegram: using default keyboard.') + err_msg = ('invalid commands for custom keyboard: ' + f'{invalid_keys}') + raise OperationalException(err_msg) else: self._keyboard = cust_keyboard - logger.info('rpc.telegram using custom keyboard from ' + logger.info('using custom keyboard from ' f'config.json: {self._keyboard}') def _init(self) -> None: From cd1a8e2c42a0bccf6c98381f0835289416311502 Mon Sep 17 00:00:00 2001 From: Christof Date: Tue, 22 Dec 2020 12:39:27 +0100 Subject: [PATCH 1189/1197] better error msg --- freqtrade/rpc/telegram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 63a98e2b1..b520756a9 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -110,8 +110,8 @@ class Telegram(RPC): invalid_keys = [b for b in chain.from_iterable(cust_keyboard) if b not in valid_keys] if len(invalid_keys): - err_msg = ('invalid commands for custom keyboard: ' - f'{invalid_keys}') + err_msg = ('config.telegram.keyboard: invalid commands for ' + f'custom keyboard: {invalid_keys}') raise OperationalException(err_msg) else: self._keyboard = cust_keyboard From b1fe5940fa7207e2f0c356f362e199b9c7c9fb91 Mon Sep 17 00:00:00 2001 From: Christof Date: Tue, 22 Dec 2020 13:01:01 +0100 Subject: [PATCH 1190/1197] check for Exception and log msgs --- tests/rpc/test_rpc_telegram.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 26384a507..df8983324 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -16,6 +16,7 @@ from telegram.error import NetworkError from freqtrade import __version__ from freqtrade.constants import CANCEL_REASON from freqtrade.edge import PairInfo +from freqtrade.exceptions import OperationalException from freqtrade.freqtradebot import FreqtradeBot from freqtrade.loggers import setup_logging from freqtrade.persistence import PairLocks, Trade @@ -1763,13 +1764,10 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: # invalid keyboard in config -> default keyboard freqtradebot.config['telegram']['enabled'] = True freqtradebot.config['telegram']['keyboard'] = invalid_keys_list - telegram = init_telegram(freqtradebot) - telegram._send_msg('test') - used_keyboard = bot.send_message.call_args[1]['reply_markup'] - assert used_keyboard == default_keyboard - assert log_has("rpc.telegram: invalid commands for custom keyboard: " - "['/not_valid', '/alsoinvalid']", caplog) - assert log_has('rpc.telegram: using default keyboard.', caplog) + err_msg = re.escape("config.telegram.keyboard: invalid commands for " + "custom keyboard: ['/not_valid', '/alsoinvalid']") + with pytest.raises(OperationalException, match=err_msg): + telegram = init_telegram(freqtradebot) # valid keyboard in config -> custom keyboard freqtradebot.config['telegram']['enabled'] = True @@ -1778,6 +1776,6 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: telegram._send_msg('test') used_keyboard = bot.send_message.call_args[1]['reply_markup'] assert used_keyboard == custom_keyboard - assert log_has("rpc.telegram using custom keyboard from config.json: " + assert log_has("using custom keyboard from config.json: " "[['/daily', '/stats', '/balance', '/profit'], ['/count', " "'/start', '/reload_config', '/help']]", caplog) From 74bcd82c3d28f7ce6c12118cc63f8f0c689eefeb Mon Sep 17 00:00:00 2001 From: Christof Date: Wed, 23 Dec 2020 16:00:01 +0100 Subject: [PATCH 1191/1197] Exception msg --- freqtrade/rpc/telegram.py | 5 +++-- tests/rpc/test_rpc_telegram.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index b520756a9..e2985fbee 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -110,8 +110,9 @@ class Telegram(RPC): invalid_keys = [b for b in chain.from_iterable(cust_keyboard) if b not in valid_keys] if len(invalid_keys): - err_msg = ('config.telegram.keyboard: invalid commands for ' - f'custom keyboard: {invalid_keys}') + err_msg = ('config.telegram.keyboard: Invalid commands for ' + f'custom Telegram keyboard: {invalid_keys}' + f'\nvalid commands are: {valid_keys}') raise OperationalException(err_msg) else: self._keyboard = cust_keyboard diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index df8983324..b8c5d8858 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1764,8 +1764,9 @@ def test__send_msg_keyboard(default_conf, mocker, caplog) -> None: # invalid keyboard in config -> default keyboard freqtradebot.config['telegram']['enabled'] = True freqtradebot.config['telegram']['keyboard'] = invalid_keys_list - err_msg = re.escape("config.telegram.keyboard: invalid commands for " - "custom keyboard: ['/not_valid', '/alsoinvalid']") + err_msg = re.escape("config.telegram.keyboard: Invalid commands for custom " + "Telegram keyboard: ['/not_valid', '/alsoinvalid']" + "\nvalid commands are: ") + r"*" with pytest.raises(OperationalException, match=err_msg): telegram = init_telegram(freqtradebot) From 67193bca3dc6d43bdfdbbbb0a8c461b3569d12ba Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Dec 2020 16:54:35 +0100 Subject: [PATCH 1192/1197] Move pairlists to be a plugin submodule --- freqtrade/commands/pairlist_commands.py | 2 +- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 2 +- freqtrade/{ => plugins}/pairlist/AgeFilter.py | 2 +- freqtrade/{ => plugins}/pairlist/IPairList.py | 0 .../{ => plugins}/pairlist/PerformanceFilter.py | 2 +- .../{ => plugins}/pairlist/PrecisionFilter.py | 2 +- freqtrade/{ => plugins}/pairlist/PriceFilter.py | 2 +- .../{ => plugins}/pairlist/ShuffleFilter.py | 2 +- freqtrade/{ => plugins}/pairlist/SpreadFilter.py | 2 +- .../{ => plugins}/pairlist/StaticPairList.py | 2 +- .../{ => plugins}/pairlist/VolumePairList.py | 2 +- freqtrade/{ => plugins}/pairlist/__init__.py | 0 .../pairlist/rangestabilityfilter.py | 2 +- .../{pairlist => plugins}/pairlistmanager.py | 2 +- freqtrade/resolvers/pairlist_resolver.py | 4 ++-- tests/data/test_dataprovider.py | 2 +- tests/optimize/test_backtesting.py | 16 ++++++++-------- tests/plugins/test_pairlist.py | 4 ++-- 19 files changed, 26 insertions(+), 26 deletions(-) rename freqtrade/{ => plugins}/pairlist/AgeFilter.py (98%) rename freqtrade/{ => plugins}/pairlist/IPairList.py (100%) rename freqtrade/{ => plugins}/pairlist/PerformanceFilter.py (97%) rename freqtrade/{ => plugins}/pairlist/PrecisionFilter.py (97%) rename freqtrade/{ => plugins}/pairlist/PriceFilter.py (98%) rename freqtrade/{ => plugins}/pairlist/ShuffleFilter.py (96%) rename freqtrade/{ => plugins}/pairlist/SpreadFilter.py (96%) rename freqtrade/{ => plugins}/pairlist/StaticPairList.py (97%) rename freqtrade/{ => plugins}/pairlist/VolumePairList.py (98%) rename freqtrade/{ => plugins}/pairlist/__init__.py (100%) rename freqtrade/{ => plugins}/pairlist/rangestabilityfilter.py (98%) rename freqtrade/{pairlist => plugins}/pairlistmanager.py (98%) diff --git a/freqtrade/commands/pairlist_commands.py b/freqtrade/commands/pairlist_commands.py index e4ee80ca5..0661cd03c 100644 --- a/freqtrade/commands/pairlist_commands.py +++ b/freqtrade/commands/pairlist_commands.py @@ -15,7 +15,7 @@ def start_test_pairlist(args: Dict[str, Any]) -> None: """ Test Pairlist configuration """ - from freqtrade.pairlist.pairlistmanager import PairListManager + from freqtrade.plugins.pairlistmanager import PairListManager config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 08b806076..dc8994d74 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -22,7 +22,7 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.mixins import LoggingMixin -from freqtrade.pairlist.pairlistmanager import PairListManager +from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 639904975..49274f75e 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -21,7 +21,7 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.mixins import LoggingMixin from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, store_backtest_stats) -from freqtrade.pairlist.pairlistmanager import PairListManager +from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.persistence import PairLocks, Trade from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver diff --git a/freqtrade/pairlist/AgeFilter.py b/freqtrade/plugins/pairlist/AgeFilter.py similarity index 98% rename from freqtrade/pairlist/AgeFilter.py rename to freqtrade/plugins/pairlist/AgeFilter.py index e3465bd82..8c3a5d22f 100644 --- a/freqtrade/pairlist/AgeFilter.py +++ b/freqtrade/plugins/pairlist/AgeFilter.py @@ -10,7 +10,7 @@ from pandas import DataFrame from freqtrade.exceptions import OperationalException from freqtrade.misc import plural -from freqtrade.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) diff --git a/freqtrade/pairlist/IPairList.py b/freqtrade/plugins/pairlist/IPairList.py similarity index 100% rename from freqtrade/pairlist/IPairList.py rename to freqtrade/plugins/pairlist/IPairList.py diff --git a/freqtrade/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py similarity index 97% rename from freqtrade/pairlist/PerformanceFilter.py rename to freqtrade/plugins/pairlist/PerformanceFilter.py index 92a97099e..c99905af5 100644 --- a/freqtrade/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -6,7 +6,7 @@ from typing import Any, Dict, List import pandas as pd -from freqtrade.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList from freqtrade.persistence import Trade diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/plugins/pairlist/PrecisionFilter.py similarity index 97% rename from freqtrade/pairlist/PrecisionFilter.py rename to freqtrade/plugins/pairlist/PrecisionFilter.py index c0d2893a1..519337f29 100644 --- a/freqtrade/pairlist/PrecisionFilter.py +++ b/freqtrade/plugins/pairlist/PrecisionFilter.py @@ -5,7 +5,7 @@ import logging from typing import Any, Dict from freqtrade.exceptions import OperationalException -from freqtrade.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) diff --git a/freqtrade/pairlist/PriceFilter.py b/freqtrade/plugins/pairlist/PriceFilter.py similarity index 98% rename from freqtrade/pairlist/PriceFilter.py rename to freqtrade/plugins/pairlist/PriceFilter.py index 20a260b46..6558f196f 100644 --- a/freqtrade/pairlist/PriceFilter.py +++ b/freqtrade/plugins/pairlist/PriceFilter.py @@ -5,7 +5,7 @@ import logging from typing import Any, Dict from freqtrade.exceptions import OperationalException -from freqtrade.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) diff --git a/freqtrade/pairlist/ShuffleFilter.py b/freqtrade/plugins/pairlist/ShuffleFilter.py similarity index 96% rename from freqtrade/pairlist/ShuffleFilter.py rename to freqtrade/plugins/pairlist/ShuffleFilter.py index 28778db7b..4d3dd29e3 100644 --- a/freqtrade/pairlist/ShuffleFilter.py +++ b/freqtrade/plugins/pairlist/ShuffleFilter.py @@ -5,7 +5,7 @@ import logging import random from typing import Any, Dict, List -from freqtrade.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) diff --git a/freqtrade/pairlist/SpreadFilter.py b/freqtrade/plugins/pairlist/SpreadFilter.py similarity index 96% rename from freqtrade/pairlist/SpreadFilter.py rename to freqtrade/plugins/pairlist/SpreadFilter.py index cbbfb9626..2f3fe47e3 100644 --- a/freqtrade/pairlist/SpreadFilter.py +++ b/freqtrade/plugins/pairlist/SpreadFilter.py @@ -4,7 +4,7 @@ Spread pair list filter import logging from typing import Any, Dict -from freqtrade.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) diff --git a/freqtrade/pairlist/StaticPairList.py b/freqtrade/plugins/pairlist/StaticPairList.py similarity index 97% rename from freqtrade/pairlist/StaticPairList.py rename to freqtrade/plugins/pairlist/StaticPairList.py index 2879cb364..dd592e0ca 100644 --- a/freqtrade/pairlist/StaticPairList.py +++ b/freqtrade/plugins/pairlist/StaticPairList.py @@ -7,7 +7,7 @@ import logging from typing import Any, Dict, List from freqtrade.exceptions import OperationalException -from freqtrade.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) diff --git a/freqtrade/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py similarity index 98% rename from freqtrade/pairlist/VolumePairList.py rename to freqtrade/plugins/pairlist/VolumePairList.py index 7056bc59d..dd8fc64fd 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -8,7 +8,7 @@ from datetime import datetime from typing import Any, Dict, List from freqtrade.exceptions import OperationalException -from freqtrade.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) diff --git a/freqtrade/pairlist/__init__.py b/freqtrade/plugins/pairlist/__init__.py similarity index 100% rename from freqtrade/pairlist/__init__.py rename to freqtrade/plugins/pairlist/__init__.py diff --git a/freqtrade/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py similarity index 98% rename from freqtrade/pairlist/rangestabilityfilter.py rename to freqtrade/plugins/pairlist/rangestabilityfilter.py index 6efe1e2ae..f2e84930b 100644 --- a/freqtrade/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -11,7 +11,7 @@ from pandas import DataFrame from freqtrade.exceptions import OperationalException from freqtrade.misc import plural -from freqtrade.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py similarity index 98% rename from freqtrade/pairlist/pairlistmanager.py rename to freqtrade/plugins/pairlistmanager.py index 418cc9e92..b71f02898 100644 --- a/freqtrade/pairlist/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -9,7 +9,7 @@ from cachetools import TTLCache, cached from freqtrade.constants import ListPairsWithTimeframes from freqtrade.exceptions import OperationalException -from freqtrade.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList from freqtrade.resolvers import PairListResolver diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index 4df5da37c..72a3cc1dd 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -6,7 +6,7 @@ This module load custom pairlists import logging from pathlib import Path -from freqtrade.pairlist.IPairList import IPairList +from freqtrade.plugins.pairlist.IPairList import IPairList from freqtrade.resolvers import IResolver @@ -20,7 +20,7 @@ class PairListResolver(IResolver): object_type = IPairList object_type_str = "Pairlist" user_subdir = None - initial_search_path = Path(__file__).parent.parent.joinpath('pairlist').resolve() + initial_search_path = Path(__file__).parent.parent.joinpath('plugins/pairlist').resolve() @staticmethod def load_pairlist(pairlist_name: str, exchange, pairlistmanager, diff --git a/tests/data/test_dataprovider.py b/tests/data/test_dataprovider.py index a3c57a77b..ee2e551b6 100644 --- a/tests/data/test_dataprovider.py +++ b/tests/data/test_dataprovider.py @@ -6,7 +6,7 @@ from pandas import DataFrame from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import ExchangeError, OperationalException -from freqtrade.pairlist.pairlistmanager import PairListManager +from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.state import RunMode from tests.conftest import get_patched_exchange diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 971f8d048..376390664 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -341,7 +341,7 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats') mocker.patch('freqtrade.optimize.backtesting.show_backtest_results') - mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', + mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) default_conf['timeframe'] = '1m' @@ -372,7 +372,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> mocker.patch('freqtrade.data.history.get_timerange', get_timerange) patch_exchange(mocker) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') - mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', + mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) default_conf['timeframe'] = "1m" @@ -392,7 +392,7 @@ def test_backtesting_no_pair_left(default_conf, mocker, caplog, testdatadir) -> mocker.patch('freqtrade.data.history.get_timerange', get_timerange) patch_exchange(mocker) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') - mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', + mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=[])) default_conf['timeframe'] = "1m" @@ -415,9 +415,9 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti mocker.patch('freqtrade.data.history.get_timerange', get_timerange) patch_exchange(mocker) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') - mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', + mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['XRP/BTC'])) - mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.refresh_pairlist') + mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.refresh_pairlist') default_conf['ticker_interval'] = "1m" default_conf['datadir'] = testdatadir @@ -700,7 +700,7 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest') mocker.patch('freqtrade.optimize.backtesting.generate_backtest_stats') mocker.patch('freqtrade.optimize.backtesting.show_backtest_results') - mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', + mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) patched_configuration_load_config_file(mocker, default_conf) @@ -740,7 +740,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): patch_exchange(mocker) backtestmock = MagicMock(return_value=pd.DataFrame(columns=BT_DATA_COLUMNS + ['profit_abs'])) - mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', + mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) text_table_mock = MagicMock() @@ -837,7 +837,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] }), ]) - mocker.patch('freqtrade.pairlist.pairlistmanager.PairListManager.whitelist', + mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index c4b370e15..1795fc27f 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -6,7 +6,7 @@ import pytest from freqtrade.constants import AVAILABLE_PAIRLISTS from freqtrade.exceptions import OperationalException -from freqtrade.pairlist.pairlistmanager import PairListManager +from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.resolvers import PairListResolver from tests.conftest import get_patched_freqtradebot, log_has, log_has_re @@ -190,7 +190,7 @@ def test_refresh_pairlist_dynamic_2(mocker, shitcoinmarkets, tickers, whitelist_ ) # Remove caching of ticker data to emulate changing volume by the time of second call mocker.patch.multiple( - 'freqtrade.pairlist.pairlistmanager.PairListManager', + 'freqtrade.plugins.pairlistmanager.PairListManager', _get_cached_tickers=MagicMock(return_value=tickers_dict), ) freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_2) From f11fd2fee10231ecd1db5f286578f4e71702f44a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Dec 2020 17:00:02 +0100 Subject: [PATCH 1193/1197] Sort imports --- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 2 +- freqtrade/plugins/pairlist/PerformanceFilter.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index dc8994d74..d60b111f2 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -22,8 +22,8 @@ from freqtrade.exceptions import (DependencyException, ExchangeError, Insufficie from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import safe_value_fallback, safe_value_fallback2 from freqtrade.mixins import LoggingMixin -from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db +from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager, RPCMessageType diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 49274f75e..a689786ec 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -21,8 +21,8 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.mixins import LoggingMixin from freqtrade.optimize.optimize_reports import (generate_backtest_stats, show_backtest_results, store_backtest_stats) -from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.persistence import PairLocks, Trade +from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.plugins.protectionmanager import ProtectionManager from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index c99905af5..7d91bb77c 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -6,8 +6,8 @@ from typing import Any, Dict, List import pandas as pd -from freqtrade.plugins.pairlist.IPairList import IPairList from freqtrade.persistence import Trade +from freqtrade.plugins.pairlist.IPairList import IPairList logger = logging.getLogger(__name__) From 516e56bfaa638dbea867f7ed824255bfc2647f90 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Dec 2020 20:50:32 +0100 Subject: [PATCH 1194/1197] Move init of _config to apiserver parent --- freqtrade/rpc/api_server.py | 1 - freqtrade/rpc/rpc.py | 1 + freqtrade/rpc/telegram.py | 1 - freqtrade/rpc/webhook.py | 1 - 4 files changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/rpc/api_server.py b/freqtrade/rpc/api_server.py index d9bf4d14a..31e7f3ff2 100644 --- a/freqtrade/rpc/api_server.py +++ b/freqtrade/rpc/api_server.py @@ -98,7 +98,6 @@ class ApiServer(RPC): """ super().__init__(freqtrade) - self._config = freqtrade.config self.app = Flask(__name__) self._cors = CORS(self.app, resources={r"/api/*": { diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 2bc989e80..9b7d62b54 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -79,6 +79,7 @@ class RPC: :return: None """ self._freqtrade = freqtrade + self._config: Dict[str, Any] = freqtrade.config @property def name(self) -> str: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e2985fbee..dddba7457 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -75,7 +75,6 @@ class Telegram(RPC): super().__init__(freqtrade) self._updater: Updater - self._config = freqtrade.config self._init_keyboard() self._init() if self._config.get('fiat_display_currency', None): diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 21413f165..f4008a70f 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -25,7 +25,6 @@ class Webhook(RPC): """ super().__init__(freqtrade) - self._config = freqtrade.config self._url = self._config['webhook']['url'] def cleanup(self) -> None: From 4cbbb80bc3a1717642a73467d9351d8b7bf2dee9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Dec 2020 07:10:01 +0100 Subject: [PATCH 1195/1197] Refactor test_telegram to simplify tests --- tests/rpc/test_rpc_telegram.py | 169 +++++++++++++-------------------- 1 file changed, 65 insertions(+), 104 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index b8c5d8858..71782411b 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -54,11 +54,18 @@ class DummyCls(Telegram): raise Exception('test') -def test__init__(default_conf, mocker) -> None: +def get_telegram_testobject(mocker, default_conf): + ftbot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(ftbot) + + return telegram, ftbot + + +def test_telegram__init__(default_conf, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) - telegram = Telegram(get_patched_freqtradebot(mocker, default_conf)) + telegram, _ = get_telegram_testobject(mocker, default_conf) assert telegram._config == default_conf @@ -66,7 +73,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: start_polling = MagicMock() mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling)) - Telegram(get_patched_freqtradebot(mocker, default_conf)) + get_telegram_testobject(mocker, default_conf) assert start_polling.call_count == 0 # number of handles registered @@ -88,7 +95,7 @@ def test_cleanup(default_conf, mocker, ) -> None: updater_mock.stop = MagicMock() mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock) - telegram = Telegram(get_patched_freqtradebot(mocker, default_conf)) + telegram, _ = get_telegram_testobject(mocker, default_conf) telegram.cleanup() assert telegram._updater.stop.call_count == 1 @@ -180,8 +187,7 @@ def test_telegram_status(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -208,12 +214,10 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) - freqtradebot.state = State.STOPPED # Status is also enabled when stopped telegram._status(update=update, context=MagicMock()) @@ -257,10 +261,10 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: ) default_conf['stake_amount'] = 15.0 - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + + patch_get_signal(freqtradebot, (True, False)) freqtradebot.state = State.STOPPED # Status table is also enabled when stopped @@ -308,9 +312,9 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) # Create some test data freqtradebot.enter_positions() @@ -386,9 +390,8 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) # Try invalid data msg_mock.reset_mock() @@ -425,9 +428,8 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) telegram._profit(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -519,11 +521,9 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) - telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 @@ -548,11 +548,9 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) - freqtradebot.config['dry_run'] = False telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] @@ -570,11 +568,9 @@ def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) - telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 @@ -608,11 +604,9 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) - telegram._balance(update=update, context=MagicMock()) assert msg_mock.call_count > 1 # Test if wrap happens around 4000 - @@ -630,8 +624,7 @@ def test_start_handle(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.STOPPED assert freqtradebot.state == State.STOPPED @@ -648,8 +641,7 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING @@ -667,8 +659,7 @@ def test_stop_handle(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING @@ -686,8 +677,7 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.STOPPED assert freqtradebot.state == State.STOPPED @@ -705,8 +695,7 @@ def test_stopbuy_handle(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) assert freqtradebot.config['max_open_trades'] != 0 telegram._stopbuy(update=update, context=MagicMock()) @@ -724,8 +713,7 @@ def test_reload_config_handle(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING @@ -909,9 +897,8 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) # Trader is not running freqtradebot.state = State.STOPPED @@ -1014,9 +1001,8 @@ def test_performance_handle(default_conf, update, ticker, fee, fetch_ticker=ticker, get_fee=fee, ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) # Create some test data freqtradebot.enter_positions() @@ -1049,9 +1035,8 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: fetch_ticker=ticker, get_fee=fee, ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) freqtradebot.state = State.STOPPED telegram._count(update=update, context=MagicMock()) @@ -1085,9 +1070,8 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None fetch_ticker=ticker, get_fee=fee, ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) PairLocks.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason') PairLocks.lock_pair('XRP/BTC', arrow.utcnow().shift(minutes=20).datetime, 'deadbeef') @@ -1110,9 +1094,7 @@ def test_whitelist_static(default_conf, update, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - - telegram = Telegram(freqtradebot) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) telegram._whitelist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1131,9 +1113,7 @@ def test_whitelist_dynamic(default_conf, update, mocker) -> None: default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 4 }] - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - - telegram = Telegram(freqtradebot) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) telegram._whitelist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1148,9 +1128,7 @@ def test_blacklist_static(default_conf, update, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - - telegram = Telegram(freqtradebot) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) telegram._blacklist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1190,9 +1168,8 @@ def test_telegram_logs(default_conf, update, mocker) -> None: ) setup_logging(default_conf) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, _ = get_telegram_testobject(mocker, default_conf) - telegram = Telegram(freqtradebot) context = MagicMock() context.args = [] telegram._logs(update=update, context=context) @@ -1223,9 +1200,7 @@ def test_edge_disabled(default_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) telegram._edge(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1245,9 +1220,7 @@ def test_edge_enabled(edge_conf, update, mocker) -> None: _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, edge_conf) - - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, edge_conf) telegram._edge(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1263,8 +1236,8 @@ def test_telegram_trades(mocker, update, default_conf, fee): _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) + context = MagicMock() context.args = [] @@ -1299,8 +1272,7 @@ def test_telegram_delete_trade(mocker, update, default_conf, fee): _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) context = MagicMock() context.args = [] @@ -1325,9 +1297,7 @@ def test_help_handle(default_conf, update, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) telegram._help(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1341,8 +1311,7 @@ def test_version_handle(default_conf, update, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) telegram._version(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1357,8 +1326,8 @@ def test_show_config_handle(default_conf, update, mocker) -> None: _send_msg=msg_mock ) default_conf['runmode'] = RunMode.DRY_RUN - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) telegram._show_config(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1398,8 +1367,8 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: 'amount': 1333.3333333333335, 'open_date': arrow.utcnow().shift(hours=-1) } - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram.send_msg(msg) assert msg_mock.call_args[0][0] \ == '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC\n' \ @@ -1431,8 +1400,8 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram.send_msg({ 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, 'exchange': 'Bittrex', @@ -1450,8 +1419,8 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) + old_convamount = telegram._fiat_converter.convert_amount telegram._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram.send_msg({ @@ -1520,8 +1489,8 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) + old_convamount = telegram._fiat_converter.convert_amount telegram._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram.send_msg({ @@ -1554,8 +1523,7 @@ def test_send_msg_status_notification(default_conf, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ 'type': RPCMessageType.STATUS_NOTIFICATION, 'status': 'running' @@ -1570,8 +1538,7 @@ def test_warning_notification(default_conf, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ 'type': RPCMessageType.WARNING_NOTIFICATION, 'status': 'message' @@ -1586,8 +1553,7 @@ def test_startup_notification(default_conf, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ 'type': RPCMessageType.STARTUP_NOTIFICATION, 'status': '*Custom:* `Hello World`' @@ -1602,8 +1568,7 @@ def test_send_msg_unknown_type(default_conf, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) with pytest.raises(NotImplementedError, match=r'Unknown message type: None'): telegram.send_msg({ 'type': None, @@ -1618,8 +1583,8 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram.send_msg({ 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': 'Bittrex', @@ -1649,8 +1614,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ 'type': RPCMessageType.SELL_NOTIFICATION, 'exchange': 'Binance', @@ -1696,8 +1660,7 @@ def test__sell_emoji(default_conf, mocker, msg, expected): _init=MagicMock(), _send_msg=msg_mock ) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) assert telegram._get_sell_emoji(msg) == expected @@ -1705,8 +1668,7 @@ def test__sell_emoji(default_conf, mocker, msg, expected): def test__send_msg(default_conf, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) bot = MagicMock() - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) telegram._updater = MagicMock() telegram._updater.bot = bot @@ -1719,8 +1681,7 @@ def test__send_msg_network_error(default_conf, mocker, caplog) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) bot = MagicMock() bot.send_message = MagicMock(side_effect=NetworkError('Oh snap')) - freqtradebot = get_patched_freqtradebot(mocker, default_conf) - telegram = Telegram(freqtradebot) + telegram, _ = get_telegram_testobject(mocker, default_conf) telegram._updater = MagicMock() telegram._updater.bot = bot From be4a4be7a3a34d19f05d2f3932325afd8692ce82 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Dec 2020 07:29:26 +0100 Subject: [PATCH 1196/1197] Further simplify test_telegram --- tests/rpc/test_rpc_telegram.py | 373 ++++++--------------------------- 1 file changed, 69 insertions(+), 304 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 71782411b..dc8ff46c1 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -54,18 +54,25 @@ class DummyCls(Telegram): raise Exception('test') -def get_telegram_testobject(mocker, default_conf): +def get_telegram_testobject(mocker, default_conf, mock=True): + msg_mock = MagicMock() + if mock: + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) ftbot = get_patched_freqtradebot(mocker, default_conf) telegram = Telegram(ftbot) - return telegram, ftbot + return telegram, ftbot, msg_mock def test_telegram__init__(default_conf, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, _ = get_telegram_testobject(mocker, default_conf) assert telegram._config == default_conf @@ -73,7 +80,7 @@ def test_telegram_init(default_conf, mocker, caplog) -> None: start_polling = MagicMock() mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling)) - get_telegram_testobject(mocker, default_conf) + get_telegram_testobject(mocker, default_conf, mock=False) assert start_polling.call_count == 0 # number of handles registered @@ -95,7 +102,7 @@ def test_cleanup(default_conf, mocker, ) -> None: updater_mock.stop = MagicMock() mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False) telegram.cleanup() assert telegram._updater.stop.call_count == 1 @@ -152,11 +159,9 @@ def test_telegram_status(default_conf, update, mocker) -> None: default_conf['telegram']['enabled'] = False default_conf['telegram']['chat_id'] = "123" - msg_mock = MagicMock() status_table = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), _rpc_trade_status=MagicMock(return_value=[{ 'trade_id': 1, 'pair': 'ETH/BTC', @@ -184,10 +189,9 @@ def test_telegram_status(default_conf, update, mocker) -> None: 'is_open': True }]), _status_table=status_table, - _send_msg=msg_mock ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -205,16 +209,13 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: fetch_ticker=ticker, get_fee=fee, ) - msg_mock = MagicMock() status_table = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), _status_table=status_table, - _send_msg=msg_mock ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) @@ -253,16 +254,10 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: fetch_ticker=ticker, get_fee=fee, ) - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) default_conf['stake_amount'] = 15.0 - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) @@ -305,14 +300,8 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, fetch_ticker=ticker, get_fee=fee, ) - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) @@ -383,14 +372,8 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: 'freqtrade.exchange.Exchange', fetch_ticker=ticker ) - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) # Try invalid data @@ -421,14 +404,8 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, fetch_ticker=ticker, get_fee=fee, ) - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram._profit(update=update, context=MagicMock()) @@ -480,16 +457,8 @@ def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee, fetch_ticker=ticker, get_fee=fee, ) - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - - freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) telegram._stats(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -514,14 +483,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', side_effect=lambda a, b: f"{a}/{b}") - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram._balance(update=update, context=MagicMock()) @@ -541,14 +503,7 @@ 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() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) freqtradebot.config['dry_run'] = False @@ -561,14 +516,7 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None: def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None: mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={}) - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram._balance(update=update, context=MagicMock()) @@ -597,14 +545,7 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None 'value': 1000.0, }) - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) telegram._balance(update=update, context=MagicMock()) @@ -617,14 +558,8 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None def test_start_handle(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.STOPPED assert freqtradebot.state == State.STOPPED @@ -634,14 +569,8 @@ def test_start_handle(default_conf, update, mocker) -> None: def test_start_handle_already_running(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING @@ -652,14 +581,8 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None: def test_stop_handle(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING @@ -670,14 +593,8 @@ def test_stop_handle(default_conf, update, mocker) -> None: def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.STOPPED assert freqtradebot.state == State.STOPPED @@ -688,14 +605,8 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: def test_stopbuy_handle(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) assert freqtradebot.config['max_open_trades'] != 0 telegram._stopbuy(update=update, context=MagicMock()) @@ -706,14 +617,8 @@ def test_stopbuy_handle(default_conf, update, mocker) -> None: def test_reload_config_handle(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) freqtradebot.state = State.RUNNING assert freqtradebot.state == State.RUNNING @@ -890,14 +795,8 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) # Trader is not running @@ -990,18 +889,13 @@ def test_forcebuy_handle_exception(default_conf, update, markets, mocker) -> Non def test_performance_handle(default_conf, update, ticker, fee, limit_buy_order, limit_sell_order, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) + mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) # Create some test data @@ -1024,18 +918,12 @@ def test_performance_handle(default_conf, update, ticker, fee, def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) freqtradebot.state = State.STOPPED @@ -1059,18 +947,12 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) PairLocks.lock_pair('ETH/BTC', arrow.utcnow().shift(minutes=4).datetime, 'randreason') @@ -1088,13 +970,8 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None def test_whitelist_static(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram._whitelist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1103,17 +980,11 @@ def test_whitelist_static(default_conf, update, mocker) -> None: def test_whitelist_dynamic(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 4 }] - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram._whitelist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1122,13 +993,8 @@ def test_whitelist_dynamic(default_conf, update, mocker) -> None: def test_blacklist_static(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram._blacklist(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1160,15 +1026,13 @@ def test_blacklist_static(default_conf, update, mocker) -> None: def test_telegram_logs(default_conf, update, mocker) -> None: - msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', _init=MagicMock(), - _send_msg=msg_mock ) setup_logging(default_conf) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) context = MagicMock() context.args = [] @@ -1193,14 +1057,8 @@ def test_telegram_logs(default_conf, update, mocker) -> None: def test_edge_disabled(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram._edge(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1208,19 +1066,13 @@ def test_edge_disabled(default_conf, update, mocker) -> None: def test_edge_enabled(edge_conf, update, mocker) -> None: - msg_mock = MagicMock() mocker.patch('freqtrade.edge.Edge._cached_pairs', mocker.PropertyMock( return_value={ 'E/F': PairInfo(-0.01, 0.66, 3.71, 0.50, 1.71, 10, 60), } )) - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, edge_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, edge_conf) telegram._edge(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1229,14 +1081,8 @@ def test_edge_enabled(edge_conf, update, mocker) -> None: def test_telegram_trades(mocker, update, default_conf, fee): - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) context = MagicMock() context.args = [] @@ -1265,14 +1111,8 @@ def test_telegram_trades(mocker, update, default_conf, fee): def test_telegram_delete_trade(mocker, update, default_conf, fee): - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) context = MagicMock() context.args = [] @@ -1291,13 +1131,7 @@ def test_telegram_delete_trade(mocker, update, default_conf, fee): def test_help_handle(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram._help(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1305,13 +1139,8 @@ def test_help_handle(default_conf, update, mocker) -> None: def test_version_handle(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram._version(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1319,15 +1148,10 @@ def test_version_handle(default_conf, update, mocker) -> None: def test_show_config_handle(default_conf, update, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) + default_conf['runmode'] = RunMode.DRY_RUN - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram._show_config(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1347,12 +1171,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None: def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) + msg = { 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': 'Bittrex', @@ -1367,7 +1186,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: 'amount': 1333.3333333333335, 'open_date': arrow.utcnow().shift(hours=-1) } - telegram, freqtradebot = get_telegram_testobject(mocker, default_conf) + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg(msg) assert msg_mock.call_args[0][0] \ @@ -1394,13 +1213,8 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, @@ -1413,13 +1227,8 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: def test_send_msg_sell_notification(default_conf, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) old_convamount = telegram._fiat_converter.convert_amount telegram._fiat_converter.convert_amount = lambda a, b, c: -24.812 @@ -1483,13 +1292,8 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) old_convamount = telegram._fiat_converter.convert_amount telegram._fiat_converter.convert_amount = lambda a, b, c: -24.812 @@ -1517,13 +1321,8 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: def test_send_msg_status_notification(default_conf, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ 'type': RPCMessageType.STATUS_NOTIFICATION, 'status': 'running' @@ -1532,13 +1331,7 @@ def test_send_msg_status_notification(default_conf, mocker) -> None: def test_warning_notification(default_conf, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ 'type': RPCMessageType.WARNING_NOTIFICATION, 'status': 'message' @@ -1547,13 +1340,7 @@ def test_warning_notification(default_conf, mocker) -> None: def test_startup_notification(default_conf, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ 'type': RPCMessageType.STARTUP_NOTIFICATION, 'status': '*Custom:* `Hello World`' @@ -1562,13 +1349,7 @@ def test_startup_notification(default_conf, mocker) -> None: def test_send_msg_unknown_type(default_conf, mocker) -> None: - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, _ = get_telegram_testobject(mocker, default_conf) with pytest.raises(NotImplementedError, match=r'Unknown message type: None'): telegram.send_msg({ 'type': None, @@ -1577,13 +1358,7 @@ def test_send_msg_unknown_type(default_conf, mocker) -> None: def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: del default_conf['fiat_display_currency'] - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ 'type': RPCMessageType.BUY_NOTIFICATION, @@ -1608,13 +1383,8 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: del default_conf['fiat_display_currency'] - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) + telegram.send_msg({ 'type': RPCMessageType.SELL_NOTIFICATION, 'exchange': 'Binance', @@ -1654,13 +1424,8 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: ]) def test__sell_emoji(default_conf, mocker, msg, expected): del default_conf['fiat_display_currency'] - msg_mock = MagicMock() - mocker.patch.multiple( - 'freqtrade.rpc.telegram.Telegram', - _init=MagicMock(), - _send_msg=msg_mock - ) - telegram, _ = get_telegram_testobject(mocker, default_conf) + + telegram, _, _ = get_telegram_testobject(mocker, default_conf) assert telegram._get_sell_emoji(msg) == expected @@ -1668,7 +1433,7 @@ def test__sell_emoji(default_conf, mocker, msg, expected): def test__send_msg(default_conf, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) bot = MagicMock() - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False) telegram._updater = MagicMock() telegram._updater.bot = bot @@ -1681,7 +1446,7 @@ def test__send_msg_network_error(default_conf, mocker, caplog) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) bot = MagicMock() bot.send_message = MagicMock(side_effect=NetworkError('Oh snap')) - telegram, _ = get_telegram_testobject(mocker, default_conf) + telegram, _, _ = get_telegram_testobject(mocker, default_conf, mock=False) telegram._updater = MagicMock() telegram._updater.bot = bot From 5bf739b917d8a32872c8f1e5e32722f1c5a2d7ce Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Dec 2020 07:39:46 +0100 Subject: [PATCH 1197/1197] Simplify more telegram tests --- tests/rpc/test_rpc_telegram.py | 52 +++++++++++++--------------------- 1 file changed, 19 insertions(+), 33 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index dc8ff46c1..5040f35cf 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -6,7 +6,7 @@ import re from datetime import datetime from random import choice, randint from string import ascii_uppercase -from unittest.mock import ANY, MagicMock, PropertyMock +from unittest.mock import ANY, MagicMock import arrow import pytest @@ -631,7 +631,7 @@ def test_reload_config_handle(default_conf, update, mocker) -> None: def test_telegram_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) - rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) + msg_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) patch_exchange(mocker) patch_whitelist(mocker, default_conf) @@ -659,8 +659,8 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, context.args = ["1"] telegram._forcesell(update=update, context=context) - assert rpc_mock.call_count == 3 - last_msg = rpc_mock.call_args_list[-1][0][0] + assert msg_mock.call_count == 3 + last_msg = msg_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, @@ -686,7 +686,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, ticker_sell_down, mocker) -> None: mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) - rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) + msg_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) patch_exchange(mocker) patch_whitelist(mocker, default_conf) @@ -718,9 +718,9 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, context.args = ["1"] telegram._forcesell(update=update, context=context) - assert rpc_mock.call_count == 3 + assert msg_mock.call_count == 3 - last_msg = rpc_mock.call_args_list[-1][0][0] + last_msg = msg_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, @@ -746,7 +746,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None patch_exchange(mocker) mocker.patch('freqtrade.rpc.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) - rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) + msg_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) patch_whitelist(mocker, default_conf) mocker.patch.multiple( @@ -761,7 +761,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None # Create some test data freqtradebot.enter_positions() - rpc_mock.reset_mock() + msg_mock.reset_mock() # /forcesell all context = MagicMock() @@ -769,8 +769,8 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None telegram._forcesell(update=update, context=context) # Called for each trade 3 times - assert rpc_mock.call_count == 8 - msg = rpc_mock.call_args_list[1][0][0] + assert msg_mock.call_count == 8 + msg = msg_mock.call_args_list[1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, @@ -828,21 +828,14 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: assert 'invalid argument' in msg_mock.call_args_list[0][0][0] -def test_forcebuy_handle(default_conf, update, markets, mocker) -> None: +def test_forcebuy_handle(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) - mocker.patch('freqtrade.rpc.telegram.Telegram._send_msg', MagicMock()) - mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - markets=PropertyMock(markets), - ) + fbuy_mock = MagicMock(return_value=None) mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock) - freqtradebot = FreqtradeBot(default_conf) + telegram, freqtradebot, _ = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) # /forcebuy ETH/BTC context = MagicMock() @@ -867,24 +860,17 @@ def test_forcebuy_handle(default_conf, update, markets, mocker) -> None: assert fbuy_mock.call_args_list[0][0][1] == 0.055 -def test_forcebuy_handle_exception(default_conf, update, markets, mocker) -> None: +def test_forcebuy_handle_exception(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) - rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram._send_msg', MagicMock()) - mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - markets=PropertyMock(markets), - ) - freqtradebot = FreqtradeBot(default_conf) + + telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) - telegram = Telegram(freqtradebot) update.message.text = '/forcebuy ETH/Nonepair' telegram._forcebuy(update=update, context=MagicMock()) - assert rpc_mock.call_count == 1 - assert rpc_mock.call_args_list[0][0][0] == 'Forcebuy not enabled.' + assert msg_mock.call_count == 1 + assert msg_mock.call_args_list[0][0][0] == 'Forcebuy not enabled.' def test_performance_handle(default_conf, update, ticker, fee,

    $A|6t6{{N!4b!`2pVt8GJ0vYO| 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%0DKK9 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 diff --git a/build_helpers/TA_Lib-0.4.19-cp37-cp37m-win_amd64.whl b/build_helpers/TA_Lib-0.4.19-cp37-cp37m-win_amd64.whl new file mode 100644 index 0000000000000000000000000000000000000000..5adbda8a7e81d37f3c1d387baba2457bc7da5b8f GIT binary patch literal 482390 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 z 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%0DmI|B^Ur#glPP7( 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%0D`LhoO>< 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