From 9199fd5964e72f6ee35f973eb405731b5ebbae9b Mon Sep 17 00:00:00 2001 From: gaugau3000 Date: Thu, 28 Nov 2019 21:21:43 +0100 Subject: [PATCH 001/324] change doc into --- docs/index.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 206d635c6..6d9951a67 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,8 +12,7 @@ Follow @freqtrade ## Introduction -Freqtrade is a cryptocurrency trading bot written in Python. - +Freqtrade is a crypto-currency algorithmic trading software develop in python (3.6+) 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. From 0e9e6b34438105ebc1f8dafa657c32fed815525b Mon Sep 17 00:00:00 2001 From: gaugau3000 Date: Thu, 28 Nov 2019 21:22:40 +0100 Subject: [PATCH 002/324] refactor feature details doc --- docs/index.md | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/docs/index.md b/docs/index.md index 6d9951a67..d5eb271d9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,18 +22,13 @@ Freqtrade is a crypto-currency algorithmic trading software develop in python (3 ## Features - - Based on Python 3.6+: For botting on any operating system — Windows, macOS and Linux. - - Persistence: Persistence is achieved through sqlite database. - - Dry-run mode: Run the bot without playing money. - - Backtesting: Run a simulation of your buy/sell strategy with historical data. - - Strategy Optimization by machine learning: Use machine learning to optimize your buy/sell strategy parameters with real exchange data. - - Edge position sizing: Calculate your win rate, risk reward ratio, the best stoploss and adjust your position size before taking a position for each specific market. - - Whitelist crypto-currencies: Select which crypto-currency you want to trade or use dynamic whitelists based on market (pair) trade volume. - - Blacklist crypto-currencies: Select which crypto-currency you want to avoid. - - Manageable via Telegram or REST APi: Manage the bot with Telegram or via the builtin REST API. - - Display profit/loss in fiat: Display your profit/loss in any of 33 fiat currencies supported. - - Daily summary of profit/loss: Receive the daily summary of your profit/loss. - - Performance status report: Receive the performance status of your current trades. + 1. Download markets datas : download historical datas of the exchange and the markets your may want to trade with. + 2. Select markets : create your list or use an automatic one based on top traded volume (not available during backtesting). You can blacklist markets you don't want to trade. + 3. Backtest : Test your strategy on past datas (based on [ohcl](https://en.wikipedia.org/wiki/Open-high-low-close_chart) candles). + 4. Optimize : Find the best parameters for your strategy (use machine leaning) + 5. Run : Run the bot on exchange without playing money (dry-run) or with money (live). + 6. Run using edge (optionnal module) : the concept is to find the best historical [trade expectancy](https://www.freqtrade.io/en/latest/edge/#expectancy) by markets based on variation of the stop-loss and then allow/reject markets to trade (the sizing of the trade is based on a risk of a percentage of your capital) + 7. Control/Monitor/Analyse : use Telegram or a REST API (start/stop the bot, profit/loss, daily summary, current open trades results...). Futher analysis can be done as trades are saved (SQLite database) ## Requirements From 58d70b2079638244b51d84fd297c0e5dd8a77213 Mon Sep 17 00:00:00 2001 From: gaugau3000 Date: Fri, 29 Nov 2019 09:35:13 +0100 Subject: [PATCH 003/324] doc explicit optimization feature --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index d5eb271d9..5871479d9 100644 --- a/docs/index.md +++ b/docs/index.md @@ -25,7 +25,7 @@ Freqtrade is a crypto-currency algorithmic trading software develop in python (3 1. Download markets datas : download historical datas of the exchange and the markets your may want to trade with. 2. Select markets : create your list or use an automatic one based on top traded volume (not available during backtesting). You can blacklist markets you don't want to trade. 3. Backtest : Test your strategy on past datas (based on [ohcl](https://en.wikipedia.org/wiki/Open-high-low-close_chart) candles). - 4. Optimize : Find the best parameters for your strategy (use machine leaning) + 4. Optimize : Find the best parameters for your strategy (use machining learning). You can optimize buy, sell, take profit and stop-loss. 5. Run : Run the bot on exchange without playing money (dry-run) or with money (live). 6. Run using edge (optionnal module) : the concept is to find the best historical [trade expectancy](https://www.freqtrade.io/en/latest/edge/#expectancy) by markets based on variation of the stop-loss and then allow/reject markets to trade (the sizing of the trade is based on a risk of a percentage of your capital) 7. Control/Monitor/Analyse : use Telegram or a REST API (start/stop the bot, profit/loss, daily summary, current open trades results...). Futher analysis can be done as trades are saved (SQLite database) From 309186911590630cb01bdd503da840c9eca8518c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 7 Dec 2019 14:30:14 +0100 Subject: [PATCH 004/324] refactor get_close_rate out of get_sell_trade-entry --- freqtrade/optimize/backtesting.py | 44 +++++++++++++++++-------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index d9fb1f2d1..c7d25cb7b 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -261,6 +261,29 @@ class Backtesting: ticker[pair] = [x for x in ticker_data.itertuples()] return ticker + def _get_close_rate(self, sell_row, trade: Trade, sell, trade_dur) -> float: + # Special handling if high or low hit STOP_LOSS or ROI + if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): + # Set close_rate to stoploss + closerate = trade.stop_loss + elif sell.sell_type == (SellType.ROI): + roi = self.strategy.min_roi_reached_entry(trade_dur) + if roi is not None: + # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) + closerate = - (trade.open_rate * roi + trade.open_rate * + (1 + trade.fee_open)) / (trade.fee_close - 1) + + # Use the maximum between closerate and low as we + # cannot sell outside of a candle. + # Applies when using {"xx": -1} as roi to force sells after xx minutes + closerate = max(closerate, sell_row.low) + else: + # This should not be reached... + closerate = sell_row.open + else: + closerate = sell_row.open + return closerate + def _get_sell_trade_entry( self, pair: str, buy_row: DataFrame, partial_ticker: List, trade_count_lock: Dict, @@ -287,26 +310,7 @@ class Backtesting: sell_row.sell, low=sell_row.low, high=sell_row.high) if sell.sell_flag: trade_dur = int((sell_row.date - buy_row.date).total_seconds() // 60) - # Special handling if high or low hit STOP_LOSS or ROI - if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): - # Set close_rate to stoploss - closerate = trade.stop_loss - elif sell.sell_type == (SellType.ROI): - roi = self.strategy.min_roi_reached_entry(trade_dur) - if roi is not None: - # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) - closerate = - (trade.open_rate * roi + trade.open_rate * - (1 + trade.fee_open)) / (trade.fee_close - 1) - - # Use the maximum between closerate and low as we - # cannot sell outside of a candle. - # Applies when using {"xx": -1} as roi to force sells after xx minutes - closerate = max(closerate, sell_row.low) - else: - # This should not be reached... - closerate = sell_row.open - else: - closerate = sell_row.open + closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) return BacktestResult(pair=pair, profit_percent=trade.calc_profit_percent(rate=closerate), From 1e6f9f9fe20cab92711b06f929e5641991c8ea60 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 7 Dec 2019 15:13:49 +0100 Subject: [PATCH 005/324] Add testcase for negative ROI sell using open --- tests/optimize/test_backtest_detail.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 3f6cc8c9a..e269009b1 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -265,6 +265,22 @@ tc16 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] ) +# Test 17: Buy, hold for 120 mins, then forcesell using roi=-1 +# Causes negative profit even though sell-reason is ROI. +# stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration) +# Uses open as sell-rate (special case) - since the roi-time is a multiple of the ticker interval. +tc17 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5025, 4975, 4987, 6172, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0], + [2, 4987, 5300, 4950, 5050, 6172, 0, 0], + [3, 4980, 5000, 4940, 4962, 6172, 0, 0], # ForceSell on ROI (roi=-1) + [4, 4962, 4987, 4972, 4950, 6172, 0, 0], + [5, 4950, 4975, 4925, 4950, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10, "120": -1}, profit_perc=-0.004, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] +) + TESTS = [ tc0, tc1, @@ -283,6 +299,7 @@ TESTS = [ tc14, tc15, tc16, + tc17, ] From 3163cbdf8a56f2eafefa5736945087327752ed10 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 7 Dec 2019 15:18:12 +0100 Subject: [PATCH 006/324] Apply special case for negative ROI --- freqtrade/optimize/backtesting.py | 10 ++++++++-- freqtrade/strategy/interface.py | 8 ++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index c7d25cb7b..6e7d36368 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -267,7 +267,7 @@ class Backtesting: # Set close_rate to stoploss closerate = trade.stop_loss elif sell.sell_type == (SellType.ROI): - roi = self.strategy.min_roi_reached_entry(trade_dur) + roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur) if roi is not None: # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) closerate = - (trade.open_rate * roi + trade.open_rate * @@ -275,8 +275,14 @@ class Backtesting: # Use the maximum between closerate and low as we # cannot sell outside of a candle. - # Applies when using {"xx": -1} as roi to force sells after xx minutes + # Applies when a new ROI setting comes in place and the whole candle is above that. closerate = max(closerate, sell_row.low) + if roi == -1 and roi_entry % self.timeframe_mins == 0: + # When forceselling with ROI=-1, the roi-entry will always be "on the entry". + # If that entry is a multiple of the timeframe (so on open) + # - we'll use open instead of close + closerate = max(closerate, sell_row.open) + else: # This should not be reached... closerate = sell_row.open diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index e208138e7..2b3a6194f 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -394,7 +394,7 @@ class IStrategy(ABC): return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) - def min_roi_reached_entry(self, trade_dur: int) -> Optional[float]: + def min_roi_reached_entry(self, trade_dur: int) -> Tuple[Optional[int], Optional[float]]: """ Based on trade duration defines the ROI entry that may have been reached. :param trade_dur: trade duration in minutes @@ -403,9 +403,9 @@ class IStrategy(ABC): # Get highest entry in ROI dict where key <= trade-duration roi_list = list(filter(lambda x: x <= trade_dur, self.minimal_roi.keys())) if not roi_list: - return None + return None, None roi_entry = max(roi_list) - return self.minimal_roi[roi_entry] + return roi_entry, self.minimal_roi[roi_entry] def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool: """ @@ -415,7 +415,7 @@ class IStrategy(ABC): """ # Check if time matches and current rate is above threshold trade_dur = int((current_time.timestamp() - trade.open_date.timestamp()) // 60) - roi = self.min_roi_reached_entry(trade_dur) + _, roi = self.min_roi_reached_entry(trade_dur) if roi is None: return False else: From 189835b9635e54ce10c88ae02ba0ce4936c7749d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 7 Dec 2019 15:26:10 +0100 Subject: [PATCH 007/324] Add documentation for ROI-1 case --- docs/backtesting.md | 5 ++++- docs/configuration.md | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 68782bb9c..017289905 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -194,7 +194,10 @@ Since backtesting lacks some detailed information about what happens within a ca - Buys happen at open-price - Sell signal sells happen at open-price of the following candle - Low happens before high for stoploss, protecting capital first. -- ROI sells are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the sell will be at 2%) +- ROI + - sells are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the sell will be at 2%) + - sells are never "below the candle", so a ROI of 2% may result in a sell at 2.4% if low was at 2.4% profit + - Forcesells caused by `=-1` ROI entries use low as sell value, unless N falls on the candle open (e.g. `120: -1` for 1h candles) - Stoploss sells happen exactly at stoploss price, even if low was lower - Trailing stoploss - High happens first - adjusting stoploss diff --git a/docs/configuration.md b/docs/configuration.md index 5ad1a886e..5bebbcfcd 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -169,6 +169,9 @@ This parameter can be set in either Strategy or Configuration file. If you use i `minimal_roi` value from the strategy file. If it is not set in either Strategy or Configuration, a default of 1000% `{"0": 10}` is used, and minimal roi is disabled unless your trade generates 1000% profit. +!!! Note "Special case to forcesell after a specific time" + A special case presents using `"": -1` as ROI. This forces the bot to sell a trade after N Minutes, no matter if it's positive or negative, so represents a time-limited force-sell. + ### Understand stoploss Go to the [stoploss documentation](stoploss.md) for more details. From 45d12dbc83fd6a24f1d7b28bcf6bf5022053d454 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 7 Dec 2019 15:28:56 +0100 Subject: [PATCH 008/324] Avoid a few calculations during backtesting --- freqtrade/optimize/backtesting.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6e7d36368..7b5bd34b4 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -262,13 +262,22 @@ class Backtesting: return ticker def _get_close_rate(self, sell_row, trade: Trade, sell, trade_dur) -> float: + """ + Get close rate for backtesting result + """ # Special handling if high or low hit STOP_LOSS or ROI if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): # Set close_rate to stoploss - closerate = trade.stop_loss + return trade.stop_loss elif sell.sell_type == (SellType.ROI): roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur) if roi is not None: + if roi == -1 and roi_entry % self.timeframe_mins == 0: + # When forceselling with ROI=-1, the roi-entry will always be "on the entry". + # If that entry is a multiple of the timeframe (so on open) + # - we'll use open instead of close + return sell_row.open + # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) closerate = - (trade.open_rate * roi + trade.open_rate * (1 + trade.fee_open)) / (trade.fee_close - 1) @@ -276,19 +285,13 @@ class Backtesting: # Use the maximum between closerate and low as we # cannot sell outside of a candle. # Applies when a new ROI setting comes in place and the whole candle is above that. - closerate = max(closerate, sell_row.low) - if roi == -1 and roi_entry % self.timeframe_mins == 0: - # When forceselling with ROI=-1, the roi-entry will always be "on the entry". - # If that entry is a multiple of the timeframe (so on open) - # - we'll use open instead of close - closerate = max(closerate, sell_row.open) + return max(closerate, sell_row.low) else: # This should not be reached... - closerate = sell_row.open + return sell_row.open else: - closerate = sell_row.open - return closerate + return sell_row.open def _get_sell_trade_entry( self, pair: str, buy_row: DataFrame, From e4655c9b07ab82d214d1166fd2c7a656756c482a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2019 14:07:46 +0100 Subject: [PATCH 009/324] include trade-duration with sell-notification --- freqtrade/freqtradebot.py | 4 +++- freqtrade/rpc/telegram.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ec341ff0a..0595e0d35 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -962,7 +962,9 @@ class FreqtradeBot: 'current_rate': current_rate, 'profit_amount': profit_trade, 'profit_percent': profit_percent, - 'sell_reason': trade.sell_reason + 'sell_reason': trade.sell_reason, + 'open_date': trade.open_date, + 'close_date': trade.close_date or datetime.utcnow() } # For regular case, when the configuration exists diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 2ae22f472..859700019 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -9,7 +9,7 @@ from typing import Any, Callable, Dict from tabulate import tabulate from telegram import ParseMode, ReplyKeyboardMarkup, Update from telegram.error import NetworkError, TelegramError -from telegram.ext import CommandHandler, Updater, CallbackContext +from telegram.ext import CallbackContext, CommandHandler, Updater from freqtrade.__init__ import __version__ from freqtrade.rpc import RPC, RPCException, RPCMessageType @@ -144,6 +144,8 @@ class Telegram(RPC): elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: msg['amount'] = round(msg['amount'], 8) msg['profit_percent'] = round(msg['profit_percent'] * 100, 2) + msg['duration'] = msg['close_date'].replace( + microsecond=0) - msg['open_date'].replace(microsecond=0) message = ("*{exchange}:* Selling {pair}\n" "*Rate:* `{limit:.8f}`\n" @@ -151,6 +153,7 @@ class Telegram(RPC): "*Open Rate:* `{open_rate:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n" "*Sell Reason:* `{sell_reason}`\n" + "*Duration:* `{duration}`\n" "*Profit:* `{profit_percent:.2f}%`").format(**msg) # Check if all sell properties are available. From 88a24da272c032851058e6812d69008d441c56ac Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2019 14:10:04 +0100 Subject: [PATCH 010/324] Adapt tests to sending open / close date --- tests/rpc/test_rpc_telegram.py | 29 ++++++++++++++++++++++------- tests/test_freqtradebot.py | 18 +++++++++++++----- 2 files changed, 35 insertions(+), 12 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 367eb8366..32f8ad036 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 MagicMock, PropertyMock +from unittest.mock import ANY, MagicMock, PropertyMock import arrow import pytest @@ -736,7 +736,9 @@ def test_forcesell_handle(default_conf, update, ticker, fee, 'profit_percent': 0.0611052, 'stake_currency': 'BTC', 'fiat_currency': 'USD', - 'sell_reason': SellType.FORCE_SELL.value + 'sell_reason': SellType.FORCE_SELL.value, + 'open_date': ANY, + 'close_date': ANY, } == last_msg @@ -793,7 +795,9 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, 'profit_percent': -0.05478342, 'stake_currency': 'BTC', 'fiat_currency': 'USD', - 'sell_reason': SellType.FORCE_SELL.value + 'sell_reason': SellType.FORCE_SELL.value, + 'open_date': ANY, + 'close_date': ANY, } == last_msg @@ -839,7 +843,9 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None 'profit_percent': -0.00589291, 'stake_currency': 'BTC', 'fiat_currency': 'USD', - 'sell_reason': SellType.FORCE_SELL.value + 'sell_reason': SellType.FORCE_SELL.value, + 'open_date': ANY, + 'close_date': ANY, } == msg @@ -1224,7 +1230,9 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'profit_percent': -0.57405275, 'stake_currency': 'ETH', 'fiat_currency': 'USD', - 'sell_reason': SellType.STOP_LOSS.value + 'sell_reason': SellType.STOP_LOSS.value, + 'open_date': arrow.utcnow().shift(hours=-1), + 'close_date': arrow.utcnow(), }) assert msg_mock.call_args[0][0] \ == ('*Binance:* Selling KEY/ETH\n' @@ -1233,6 +1241,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' '*Sell Reason:* `stop_loss`\n' + '*Duration:* `1:00:00`\n' '*Profit:* `-57.41%`` (loss: -0.05746268 ETH`` / -24.812 USD)`') msg_mock.reset_mock() @@ -1249,7 +1258,9 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'profit_amount': -0.05746268, 'profit_percent': -0.57405275, 'stake_currency': 'ETH', - 'sell_reason': SellType.STOP_LOSS.value + 'sell_reason': SellType.STOP_LOSS.value, + 'open_date': arrow.utcnow().shift(hours=-2, minutes=-30), + 'close_date': arrow.utcnow(), }) assert msg_mock.call_args[0][0] \ == ('*Binance:* Selling KEY/ETH\n' @@ -1258,6 +1269,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' '*Sell Reason:* `stop_loss`\n' + '*Duration:* `2:30:00`\n' '*Profit:* `-57.41%`') # Reset singleton function to avoid random breaks telegram._fiat_converter.convert_amount = old_convamount @@ -1377,7 +1389,9 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: 'profit_percent': -0.57405275, 'stake_currency': 'ETH', 'fiat_currency': 'USD', - 'sell_reason': SellType.STOP_LOSS.value + 'sell_reason': SellType.STOP_LOSS.value, + 'open_date': arrow.utcnow().shift(hours=-2, minutes=-35, seconds=-3), + 'close_date': arrow.utcnow(), }) assert msg_mock.call_args[0][0] \ == '*Binance:* Selling KEY/ETH\n' \ @@ -1386,6 +1400,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: '*Open Rate:* `0.00007500`\n' \ '*Current Rate:* `0.00003201`\n' \ '*Sell Reason:* `stop_loss`\n' \ + '*Duration:* `2:35:03`\n' \ '*Profit:* `-57.41%`' diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index fe58ccf37..efab64a6a 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -5,7 +5,7 @@ import logging import time from copy import deepcopy from math import isclose -from unittest.mock import MagicMock, PropertyMock +from unittest.mock import ANY, MagicMock, PropertyMock import arrow import pytest @@ -2199,7 +2199,9 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N 'profit_percent': 0.0611052, 'stake_currency': 'BTC', 'fiat_currency': 'USD', - 'sell_reason': SellType.ROI.value + 'sell_reason': SellType.ROI.value, + 'open_date': ANY, + 'close_date': ANY, } == last_msg @@ -2246,7 +2248,9 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) 'profit_percent': -0.05478342, 'stake_currency': 'BTC', 'fiat_currency': 'USD', - 'sell_reason': SellType.STOP_LOSS.value + 'sell_reason': SellType.STOP_LOSS.value, + 'open_date': ANY, + 'close_date': ANY, } == last_msg @@ -2300,7 +2304,9 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe 'profit_percent': -0.01493766, 'stake_currency': 'BTC', 'fiat_currency': 'USD', - 'sell_reason': SellType.STOP_LOSS.value + 'sell_reason': SellType.STOP_LOSS.value, + 'open_date': ANY, + 'close_date': ANY, } == last_msg @@ -2497,7 +2503,9 @@ def test_execute_sell_market_order(default_conf, ticker, fee, 'profit_percent': 0.0611052, 'stake_currency': 'BTC', 'fiat_currency': 'USD', - 'sell_reason': SellType.ROI.value + 'sell_reason': SellType.ROI.value, + 'open_date': ANY, + 'close_date': ANY, } == last_msg From dc9fed4a5f0fabc775332522f5a45b1ed1a90f84 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 8 Dec 2019 14:10:26 +0100 Subject: [PATCH 011/324] Adjust documentation --- docs/webhook-config.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/webhook-config.md b/docs/webhook-config.md index 112f8a77e..9e0a34eae 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -63,6 +63,8 @@ Possible parameters are: * `fiat_currency` * `sell_reason` * `order_type` +* `open_date` +* `close_date` ### Webhookstatus From 25447329a01bc2e354aee0f7d5b00ee7d65aacc9 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 9 Dec 2019 07:25:44 +0000 Subject: [PATCH 012/324] Bump scikit-learn from 0.21.3 to 0.22 Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 0.21.3 to 0.22. - [Release notes](https://github.com/scikit-learn/scikit-learn/releases) - [Commits](https://github.com/scikit-learn/scikit-learn/compare/0.21.3...0.22) 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 96a22b42e..2317cdf3e 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -3,7 +3,7 @@ # Required for hyperopt scipy==1.3.3 -scikit-learn==0.21.3 +scikit-learn==0.22 scikit-optimize==0.5.2 filelock==3.0.12 joblib==0.14.0 From de33ec425086e1aacc093b6cea0694ca549db5cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 9 Dec 2019 16:52:12 +0100 Subject: [PATCH 013/324] use sell_row.open also when the active ROI value just changed --- freqtrade/optimize/backtesting.py | 19 ++++++---- tests/optimize/test_backtest_detail.py | 50 ++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 7b5bd34b4..4e86acdc6 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -273,19 +273,26 @@ class Backtesting: roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur) if roi is not None: if roi == -1 and roi_entry % self.timeframe_mins == 0: - # When forceselling with ROI=-1, the roi-entry will always be "on the entry". - # If that entry is a multiple of the timeframe (so on open) + # When forceselling with ROI=-1, the roi time will always be equal to trade_dur. + # If that entry is a multiple of the timeframe (so on candle open) # - we'll use open instead of close return sell_row.open # - (Expected abs profit + open_rate + open_fee) / (fee_close -1) - closerate = - (trade.open_rate * roi + trade.open_rate * - (1 + trade.fee_open)) / (trade.fee_close - 1) + close_rate = - (trade.open_rate * roi + trade.open_rate * + (1 + trade.fee_open)) / (trade.fee_close - 1) - # Use the maximum between closerate and low as we + if (trade_dur > 0 and trade_dur == roi_entry + and roi_entry % self.timeframe_mins == 0 + and sell_row.open > close_rate): + # new ROI entry came into effect. + # use Open rate if open_rate > calculated sell rate + return sell_row.open + + # Use the maximum between close_rate and low as we # cannot sell outside of a candle. # Applies when a new ROI setting comes in place and the whole candle is above that. - return max(closerate, sell_row.low) + return max(close_rate, sell_row.low) else: # This should not be reached... diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index e269009b1..eff236a5e 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -281,6 +281,53 @@ tc17 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] ) + +# Test 18: Buy, hold for 120 mins, then drop ROI to 1%, causing a sell in candle 3. +# stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration) +# uses open_rate as sell-price +tc18 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5025, 4975, 4987, 6172, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0], + [2, 4987, 5300, 4950, 5200, 6172, 0, 0], + [3, 5200, 5220, 4940, 4962, 6172, 0, 0], # Sell on ROI (sells on open) + [4, 4962, 4987, 4972, 4950, 6172, 0, 0], + [5, 4950, 4975, 4925, 4950, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10, "120": 0.01}, profit_perc=0.04, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] +) + +# Test 19: Buy, hold for 119 mins, then drop ROI to 1%, causing a sell in candle 3. +# stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration) +# uses calculated ROI (1%) as sell rate, otherwise identical to tc18 +tc19 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5025, 4975, 4987, 6172, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0], + [2, 4987, 5300, 4950, 5200, 6172, 0, 0], + [3, 5000, 5300, 4940, 4962, 6172, 0, 0], # Sell on ROI + [4, 4962, 4987, 4972, 4950, 6172, 0, 0], + [5, 4550, 4975, 4925, 4950, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10, "120": 0.01}, profit_perc=0.01, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] +) + +# Test 20: Buy, hold for 119 mins, then drop ROI to 1%, causing a sell in candle 3. +# stop-loss: 10%, ROI: 10% (should not apply), -100% after 100 minutes (limits trade duration) +# uses calculated ROI (1%) as sell rate, otherwise identical to tc18 +tc20 = BTContainer(data=[ + # D O H L C V B S + [0, 5000, 5025, 4975, 4987, 6172, 1, 0], + [1, 5000, 5025, 4975, 4987, 6172, 0, 0], + [2, 4987, 5300, 4950, 5200, 6172, 0, 0], + [3, 5200, 5300, 4940, 4962, 6172, 0, 0], # Sell on ROI + [4, 4962, 4987, 4972, 4950, 6172, 0, 0], + [5, 4550, 4975, 4925, 4950, 6172, 0, 0]], + stop_loss=-0.10, roi={"0": 0.10, "119": 0.01}, profit_perc=0.01, + trades=[BTrade(sell_reason=SellType.ROI, open_tick=1, close_tick=3)] +) + + TESTS = [ tc0, tc1, @@ -300,6 +347,9 @@ TESTS = [ tc15, tc16, tc17, + tc18, + tc19, + tc20, ] From 641e3fdf7a51e846dafd862bc52b071d0000f55c Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 10 Dec 2019 03:13:45 +0300 Subject: [PATCH 014/324] Fix generation of hyperopt trailing params --- freqtrade/optimize/hyperopt.py | 13 ++++++------- freqtrade/optimize/hyperopt_interface.py | 24 ++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index e7b30198f..fb2de779c 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -177,8 +177,7 @@ class Hyperopt: result['stoploss'] = {p.name: params.get(p.name) for p in self.hyperopt_space('stoploss')} if self.has_space('trailing'): - result['trailing'] = {p.name: params.get(p.name) - for p in self.hyperopt_space('trailing')} + result['trailing'] = self.custom_hyperopt.generate_trailing_params(params) return result @@ -353,13 +352,13 @@ class Hyperopt: self.backtesting.strategy.stoploss = params_dict['stoploss'] if self.has_space('trailing'): - self.backtesting.strategy.trailing_stop = params_dict['trailing_stop'] - self.backtesting.strategy.trailing_stop_positive = \ - params_dict['trailing_stop_positive'] + d = self.custom_hyperopt.generate_trailing_params(params_dict) + self.backtesting.strategy.trailing_stop = d['trailing_stop'] + self.backtesting.strategy.trailing_stop_positive = d['trailing_stop_positive'] self.backtesting.strategy.trailing_stop_positive_offset = \ - params_dict['trailing_stop_positive_offset'] + d['trailing_stop_positive_offset'] self.backtesting.strategy.trailing_only_offset_is_reached = \ - params_dict['trailing_only_offset_is_reached'] + d['trailing_only_offset_is_reached'] processed = load(self.tickerdata_pickle) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 958fb8d66..dbbdb8b91 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -174,6 +174,19 @@ class IHyperOpt(ABC): Real(-0.35, -0.02, name='stoploss'), ] + @staticmethod + def generate_trailing_params(params: Dict) -> Dict: + """ + Create dict with trailing stop parameters. + """ + return { + 'trailing_stop': params['trailing_stop'], + 'trailing_stop_positive': params['trailing_stop_positive'], + 'trailing_stop_positive_offset': (params['trailing_stop_positive'] + + params['trailing_stop_positive_offset_p1']), + 'trailing_only_offset_is_reached': params['trailing_only_offset_is_reached'], + } + @staticmethod def trailing_space() -> List[Dimension]: """ @@ -190,8 +203,15 @@ class IHyperOpt(ABC): # other 'trailing' hyperspace parameters. Categorical([True], name='trailing_stop'), - Real(0.02, 0.35, name='trailing_stop_positive'), - Real(0.01, 0.1, name='trailing_stop_positive_offset'), + Real(0.01, 0.35, name='trailing_stop_positive'), + + # 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive', + # so this intermediate parameter is used as the value of the difference between + # them. The value of the 'trailing_stop_positive_offset' is constructed in the + # generate_trailing_params() method. + # # This is similar to the hyperspace dimensions used for constructing the ROI tables. + Real(0.001, 0.1, name='trailing_stop_positive_offset_p1'), + Categorical([True, False], name='trailing_only_offset_is_reached'), ] From 3252654ed3c4016a2c8ecc6520d9c6a35ef19c25 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 10 Dec 2019 14:06:17 +0300 Subject: [PATCH 015/324] Test adjusted --- tests/optimize/test_hyperopt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 6a8daab8b..60b445323 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -636,7 +636,7 @@ def test_generate_optimizer(mocker, default_conf) -> None: 'stoploss': -0.4, 'trailing_stop': True, 'trailing_stop_positive': 0.02, - 'trailing_stop_positive_offset': 0.1, + 'trailing_stop_positive_offset_p1': 0.05, 'trailing_only_offset_is_reached': False, } response_expected = { @@ -670,7 +670,7 @@ def test_generate_optimizer(mocker, default_conf) -> None: 'trailing': {'trailing_only_offset_is_reached': False, 'trailing_stop': True, 'trailing_stop_positive': 0.02, - 'trailing_stop_positive_offset': 0.1}}, + 'trailing_stop_positive_offset': 0.07}}, 'params_dict': optimizer_param, 'results_metrics': {'avg_profit': 2.3117, 'duration': 100.0, From 3448f86263363c26ba8cbc5c3ac9fcf3fff80bea Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 10 Dec 2019 15:45:10 +0300 Subject: [PATCH 016/324] Suppress scikit-learn FutureWarnings from skopt imports --- freqtrade/optimize/hyperopt.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index a7b0e2bf6..c84240934 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -7,6 +7,7 @@ This module contains the hyperopt logic import locale import logging import sys +import warnings from collections import OrderedDict from operator import itemgetter from pathlib import Path @@ -19,8 +20,12 @@ from colorama import init as colorama_init from joblib import (Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects) from pandas import DataFrame -from skopt import Optimizer -from skopt.space import Dimension + +# Suppress scikit-learn FutureWarnings from skopt +with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=FutureWarning) + from skopt import Optimizer + from skopt.space import Dimension from freqtrade.data.history import get_timeframe, trim_dataframe from freqtrade.misc import plural, round_dict From 390db9503f5f354ca836a2e88fc0b85e6def700e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 10 Dec 2019 15:12:36 +0100 Subject: [PATCH 017/324] Show humanized and minutes version of duration --- freqtrade/rpc/telegram.py | 5 ++++- tests/rpc/test_rpc_telegram.py | 8 ++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 859700019..b8d80ceb1 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -12,6 +12,7 @@ from telegram.error import NetworkError, TelegramError from telegram.ext import CallbackContext, CommandHandler, Updater from freqtrade.__init__ import __version__ +from freqtrade.misc import plural from freqtrade.rpc import RPC, RPCException, RPCMessageType from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -146,6 +147,8 @@ class Telegram(RPC): msg['profit_percent'] = round(msg['profit_percent'] * 100, 2) msg['duration'] = msg['close_date'].replace( microsecond=0) - msg['open_date'].replace(microsecond=0) + duration_tmp = msg['duration'].total_seconds() // 60 + msg['duration_mins'] = f"{duration_tmp:.0f} {plural(duration_tmp, 'min')}" message = ("*{exchange}:* Selling {pair}\n" "*Rate:* `{limit:.8f}`\n" @@ -153,7 +156,7 @@ class Telegram(RPC): "*Open Rate:* `{open_rate:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n" "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration}`\n" + "*Duration:* `{duration} ({duration_mins})`\n" "*Profit:* `{profit_percent:.2f}%`").format(**msg) # Check if all sell properties are available. diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 32f8ad036..7c1dd7983 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1241,7 +1241,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' '*Sell Reason:* `stop_loss`\n' - '*Duration:* `1:00:00`\n' + '*Duration:* `1:00:00 (60 mins)`\n' '*Profit:* `-57.41%`` (loss: -0.05746268 ETH`` / -24.812 USD)`') msg_mock.reset_mock() @@ -1259,7 +1259,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'profit_percent': -0.57405275, 'stake_currency': 'ETH', 'sell_reason': SellType.STOP_LOSS.value, - 'open_date': arrow.utcnow().shift(hours=-2, minutes=-30), + 'open_date': arrow.utcnow().shift(days=-1, hours=-2, minutes=-30), 'close_date': arrow.utcnow(), }) assert msg_mock.call_args[0][0] \ @@ -1269,7 +1269,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' '*Sell Reason:* `stop_loss`\n' - '*Duration:* `2:30:00`\n' + '*Duration:* `1 day, 2:30:00 (1590 mins)`\n' '*Profit:* `-57.41%`') # Reset singleton function to avoid random breaks telegram._fiat_converter.convert_amount = old_convamount @@ -1400,7 +1400,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: '*Open Rate:* `0.00007500`\n' \ '*Current Rate:* `0.00003201`\n' \ '*Sell Reason:* `stop_loss`\n' \ - '*Duration:* `2:35:03`\n' \ + '*Duration:* `2:35:03 (155 mins)`\n' \ '*Profit:* `-57.41%`' From 3f9f29ba4ef752ce87e74960116e0f0b9d1d63d0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 10 Dec 2019 16:10:51 +0100 Subject: [PATCH 018/324] Fix Flake8 import error --- freqtrade/optimize/hyperopt.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index c84240934..a5be478bf 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -21,12 +21,6 @@ from joblib import (Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects) from pandas import DataFrame -# Suppress scikit-learn FutureWarnings from skopt -with warnings.catch_warnings(): - warnings.filterwarnings("ignore", category=FutureWarning) - from skopt import Optimizer - from skopt.space import Dimension - from freqtrade.data.history import get_timeframe, trim_dataframe from freqtrade.misc import plural, round_dict from freqtrade.optimize.backtesting import Backtesting @@ -36,6 +30,13 @@ from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4 from freqtrade.resolvers.hyperopt_resolver import (HyperOptLossResolver, HyperOptResolver) +# Suppress scikit-learn FutureWarnings from skopt +with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=FutureWarning) + from skopt import Optimizer + from skopt.space import Dimension + + logger = logging.getLogger(__name__) From 1058e5fb723d68cd6fc2a8b388b1307993991de1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 Dec 2019 06:48:40 +0100 Subject: [PATCH 019/324] No plural for min --- freqtrade/rpc/telegram.py | 6 ++---- tests/rpc/test_rpc_telegram.py | 6 +++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index b8d80ceb1..51736968b 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -12,7 +12,6 @@ from telegram.error import NetworkError, TelegramError from telegram.ext import CallbackContext, CommandHandler, Updater from freqtrade.__init__ import __version__ -from freqtrade.misc import plural from freqtrade.rpc import RPC, RPCException, RPCMessageType from freqtrade.rpc.fiat_convert import CryptoToFiatConverter @@ -147,8 +146,7 @@ class Telegram(RPC): msg['profit_percent'] = round(msg['profit_percent'] * 100, 2) msg['duration'] = msg['close_date'].replace( microsecond=0) - msg['open_date'].replace(microsecond=0) - duration_tmp = msg['duration'].total_seconds() // 60 - msg['duration_mins'] = f"{duration_tmp:.0f} {plural(duration_tmp, 'min')}" + msg['duration_min'] = msg['duration'].total_seconds() / 60 message = ("*{exchange}:* Selling {pair}\n" "*Rate:* `{limit:.8f}`\n" @@ -156,7 +154,7 @@ class Telegram(RPC): "*Open Rate:* `{open_rate:.8f}`\n" "*Current Rate:* `{current_rate:.8f}`\n" "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration} ({duration_mins})`\n" + "*Duration:* `{duration} ({duration_min:.1f} min)`\n" "*Profit:* `{profit_percent:.2f}%`").format(**msg) # Check if all sell properties are available. diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 7c1dd7983..7c4a8f0d6 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1241,7 +1241,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' '*Sell Reason:* `stop_loss`\n' - '*Duration:* `1:00:00 (60 mins)`\n' + '*Duration:* `1:00:00 (60.0 min)`\n' '*Profit:* `-57.41%`` (loss: -0.05746268 ETH`` / -24.812 USD)`') msg_mock.reset_mock() @@ -1269,7 +1269,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' '*Sell Reason:* `stop_loss`\n' - '*Duration:* `1 day, 2:30:00 (1590 mins)`\n' + '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' '*Profit:* `-57.41%`') # Reset singleton function to avoid random breaks telegram._fiat_converter.convert_amount = old_convamount @@ -1400,7 +1400,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: '*Open Rate:* `0.00007500`\n' \ '*Current Rate:* `0.00003201`\n' \ '*Sell Reason:* `stop_loss`\n' \ - '*Duration:* `2:35:03 (155 mins)`\n' \ + '*Duration:* `2:35:03 (155.1 min)`\n' \ '*Profit:* `-57.41%`' From 7c7ca1cb90f6f5468cf286920462b117a3b63f99 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 11 Dec 2019 07:12:37 +0100 Subject: [PATCH 020/324] Remove min (plural) from codebase --- freqtrade/data/history.py | 6 +- freqtrade/optimize/backtesting.py | 6 +- freqtrade/optimize/hyperopt.py | 2 +- freqtrade/optimize/hyperopt_interface.py | 6 +- tests/conftest.py | 127 ++++++++++++++++++++++- tests/optimize/test_backtest_detail.py | 2 +- tests/optimize/test_hyperopt.py | 2 +- 7 files changed, 138 insertions(+), 13 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 865893a66..ddcae89ab 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -463,7 +463,7 @@ def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow] def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime, - max_date: datetime, timeframe_mins: int) -> bool: + max_date: datetime, timeframe_min: int) -> bool: """ Validates preprocessed backtesting data for missing values and shows warnings about it that. @@ -471,10 +471,10 @@ def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime, :param pair: pair used for log output. :param min_date: start-date of the data :param max_date: end-date of the data - :param timeframe_mins: ticker Timeframe in minutes + :param timeframe_min: ticker Timeframe in minutes """ # total difference in minutes / timeframe-minutes - expected_frames = int((max_date - min_date).total_seconds() // 60 // timeframe_mins) + expected_frames = int((max_date - min_date).total_seconds() // 60 // timeframe_min) found_missing = False dflen = len(data) if dflen < expected_frames: diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index d9fb1f2d1..637115dfd 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -87,7 +87,7 @@ class Backtesting: raise OperationalException("Ticker-interval needs to be set in either configuration " "or as cli argument `--ticker-interval 5m`") self.timeframe = str(self.config.get('ticker_interval')) - self.timeframe_mins = timeframe_to_minutes(self.timeframe) + self.timeframe_min = timeframe_to_minutes(self.timeframe) # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) @@ -378,7 +378,7 @@ class Backtesting: lock_pair_until: Dict = {} # Indexes per pair, so some pairs are allowed to have a missing start. indexes: Dict = {} - tmp = start_date + timedelta(minutes=self.timeframe_mins) + tmp = start_date + timedelta(minutes=self.timeframe_min) # Loop timerange and get candle for each pair at that point in time while tmp < end_date: @@ -430,7 +430,7 @@ class Backtesting: lock_pair_until[pair] = end_date.datetime # Move time one configured time_interval ahead. - tmp += timedelta(minutes=self.timeframe_mins) + tmp += timedelta(minutes=self.timeframe_min) return DataFrame.from_records(trades, columns=BacktestResult._fields) def start(self) -> None: diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index e52cb8faf..597fd7f22 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -427,7 +427,7 @@ class Hyperopt: f"Avg profit {results_metrics['avg_profit']: 6.2f}%. " f"Total profit {results_metrics['total_profit']: 11.8f} {stake_cur} " f"({results_metrics['profit']: 7.2f}\N{GREEK CAPITAL LETTER SIGMA}%). " - f"Avg duration {results_metrics['duration']:5.1f} mins." + f"Avg duration {results_metrics['duration']:5.1f} min." ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8') def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer: diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 958fb8d66..2a88462e9 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -106,7 +106,7 @@ class IHyperOpt(ABC): roi_t_alpha = 1.0 roi_p_alpha = 1.0 - timeframe_mins = timeframe_to_minutes(IHyperOpt.ticker_interval) + timeframe_min = timeframe_to_minutes(IHyperOpt.ticker_interval) # We define here limits for the ROI space parameters automagically adapted to the # timeframe used by the bot: @@ -117,8 +117,8 @@ class IHyperOpt(ABC): # # The scaling is designed so that it maps exactly to the legacy Freqtrade roi_space() # method for the 5m ticker interval. - roi_t_scale = timeframe_mins / 5 - roi_p_scale = math.log1p(timeframe_mins) / math.log1p(5) + roi_t_scale = timeframe_min / 5 + roi_p_scale = math.log1p(timeframe_min) / math.log1p(5) roi_limits = { 'roi_t1_min': int(10 * roi_t_scale * roi_t_alpha), 'roi_t1_max': int(120 * roi_t_scale * roi_t_alpha), diff --git a/tests/conftest.py b/tests/conftest.py index 020c29eb1..82111528e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1418,4 +1418,129 @@ def open_trade(): @pytest.fixture def hyperopt_results(): - return [{'loss': 0.4366182531161519, 'params_dict': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, 'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, 'results_metrics': {'trade_count': 2, 'avg_profit': -1.254995, 'total_profit': -0.00125625, 'profit': -2.50999, 'duration': 3930.0}, 'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 mins.', 'total_profit': -0.00125625, 'current_epoch': 1, 'is_initial_point': True, 'is_best': True}, {'loss': 20.0, 'params_dict': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 334, 'roi_t2': 683, 'roi_t3': 140, 'roi_p1': 0.06403981740598495, 'roi_p2': 0.055519840060645045, 'roi_p3': 0.3253712811342459, 'stoploss': -0.338070047333259}, 'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, 'stoploss': {'stoploss': -0.338070047333259}}, 'results_metrics': {'trade_count': 1, 'avg_profit': 0.12357, 'total_profit': 6.185e-05, 'profit': 0.12357, 'duration': 1200.0}, 'results_explanation': ' 1 trades. Avg profit 0.12%. Total profit 0.00006185 BTC ( 0.12Σ%). Avg duration 1200.0 mins.', 'total_profit': 6.185e-05, 'current_epoch': 2, 'is_initial_point': True, 'is_best': False}, {'loss': 14.241196856510731, 'params_dict': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 889, 'roi_t2': 533, 'roi_t3': 263, 'roi_p1': 0.04759065393663096, 'roi_p2': 0.1488819964638463, 'roi_p3': 0.4102801822104605, 'stoploss': -0.05394588767607611}, 'params_details': {'buy': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.6067528326109377, 263: 0.19647265040047726, 796: 0.04759065393663096, 1685: 0}, 'stoploss': {'stoploss': -0.05394588767607611}}, 'results_metrics': {'trade_count': 621, 'avg_profit': -0.43883302093397747, 'total_profit': -0.13639474, 'profit': -272.515306, 'duration': 1691.207729468599}, 'results_explanation': ' 621 trades. Avg profit -0.44%. Total profit -0.13639474 BTC (-272.52Σ%). Avg duration 1691.2 mins.', 'total_profit': -0.13639474, 'current_epoch': 3, 'is_initial_point': True, 'is_best': False}, {'loss': 100000, 'params_dict': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1402, 'roi_t2': 676, 'roi_t3': 215, 'roi_p1': 0.06264755784937427, 'roi_p2': 0.14258587851894644, 'roi_p3': 0.20671291201040828, 'stoploss': -0.11818343570194478}, 'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan mins.', 'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_best': False}, {'loss': 0.22195522184191518, 'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, 'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3077646493708299, 444: 0.16227697603830155, 1045: 0.07280999507931168, 2314: 0}, 'stoploss': {'stoploss': -0.18181041180901014}}, 'results_metrics': {'trade_count': 14, 'avg_profit': -0.3539515, 'total_profit': -0.002480140000000001, 'profit': -4.955321, 'duration': 3402.8571428571427}, 'results_explanation': ' 14 trades. Avg profit -0.35%. Total profit -0.00248014 BTC ( -4.96Σ%). Avg duration 3402.9 mins.', 'total_profit': -0.002480140000000001, 'current_epoch': 5, 'is_initial_point': True, 'is_best': True}, {'loss': 0.545315889154162, 'params_dict': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower', 'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 319, 'roi_t2': 556, 'roi_t3': 216, 'roi_p1': 0.06251955472249589, 'roi_p2': 0.11659519602202795, 'roi_p3': 0.0953744132197762, 'stoploss': -0.024551752215582423}, 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.2744891639643, 216: 0.17911475074452382, 772: 0.06251955472249589, 1091: 0}, 'stoploss': {'stoploss': -0.024551752215582423}}, 'results_metrics': {'trade_count': 39, 'avg_profit': -0.21400679487179478, 'total_profit': -0.0041773, 'profit': -8.346264999999997, 'duration': 636.9230769230769}, 'results_explanation': ' 39 trades. Avg profit -0.21%. Total profit -0.00417730 BTC ( -8.35Σ%). Avg duration 636.9 mins.', 'total_profit': -0.0041773, 'current_epoch': 6, 'is_initial_point': True, 'is_best': False}, {'loss': 4.713497421432944, 'params_dict': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 771, 'roi_t2': 620, 'roi_t3': 145, 'roi_p1': 0.0586919200378493, 'roi_p2': 0.04984118697312542, 'roi_p3': 0.37521058680247044, 'stoploss': -0.14613268022709905}, 'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, 'stoploss': {'stoploss': -0.14613268022709905}}, 'results_metrics': {'trade_count': 318, 'avg_profit': -0.39833954716981146, 'total_profit': -0.06339929, 'profit': -126.67197600000004, 'duration': 3140.377358490566}, 'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 mins.', 'total_profit': -0.06339929, 'current_epoch': 7, 'is_initial_point': True, 'is_best': False}, {'loss': 20.0, 'params_dict': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal', 'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 1149, 'roi_t2': 375, 'roi_t3': 289, 'roi_p1': 0.05571820757172588, 'roi_p2': 0.0606240398618907, 'roi_p3': 0.1729012220156157, 'stoploss': -0.1588514289110401}, 'params_details': {'buy': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.2892434694492323, 289: 0.11634224743361658, 664: 0.05571820757172588, 1813: 0}, 'stoploss': {'stoploss': -0.1588514289110401}}, 'results_metrics': {'trade_count': 1, 'avg_profit': 0.0, 'total_profit': 0.0, 'profit': 0.0, 'duration': 5340.0}, 'results_explanation': ' 1 trades. Avg profit 0.00%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration 5340.0 mins.', 'total_profit': 0.0, 'current_epoch': 8, 'is_initial_point': True, 'is_best': False}, {'loss': 2.4731817780991223, 'params_dict': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1012, 'roi_t2': 584, 'roi_t3': 422, 'roi_p1': 0.036764323603472565, 'roi_p2': 0.10335480573205287, 'roi_p3': 0.10322347377503042, 'stoploss': -0.2780610808108503}, 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.2433426031105559, 422: 0.14011912933552545, 1006: 0.036764323603472565, 2018: 0}, 'stoploss': {'stoploss': -0.2780610808108503}}, 'results_metrics': {'trade_count': 229, 'avg_profit': -0.38433433624454144, 'total_profit': -0.044050070000000004, 'profit': -88.01256299999999, 'duration': 6505.676855895196}, 'results_explanation': ' 229 trades. Avg profit -0.38%. Total profit -0.04405007 BTC ( -88.01Σ%). Avg duration 6505.7 mins.', 'total_profit': -0.044050070000000004, 'current_epoch': 9, 'is_initial_point': True, 'is_best': False}, {'loss': -0.2604606005845212, 'params_dict': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 792, 'roi_t2': 464, 'roi_t3': 215, 'roi_p1': 0.04594053535385903, 'roi_p2': 0.09623192684243963, 'roi_p3': 0.04428219070850663, 'stoploss': -0.16992287161634415}, 'params_details': {'buy': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.18645465290480528, 215: 0.14217246219629864, 679: 0.04594053535385903, 1471: 0}, 'stoploss': {'stoploss': -0.16992287161634415}}, 'results_metrics': {'trade_count': 4, 'avg_profit': 0.1080385, 'total_profit': 0.00021629, 'profit': 0.432154, 'duration': 2850.0}, 'results_explanation': ' 4 trades. Avg profit 0.11%. Total profit 0.00021629 BTC ( 0.43Σ%). Avg duration 2850.0 mins.', 'total_profit': 0.00021629, 'current_epoch': 10, 'is_initial_point': True, 'is_best': True}, {'loss': 4.876465945994304, 'params_dict': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 579, 'roi_t2': 614, 'roi_t3': 273, 'roi_p1': 0.05307643172744114, 'roi_p2': 0.1352282078262871, 'roi_p3': 0.1913307406325751, 'stoploss': -0.25728526022513887}, 'params_details': {'buy': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3796353801863034, 273: 0.18830463955372825, 887: 0.05307643172744114, 1466: 0}, 'stoploss': {'stoploss': -0.25728526022513887}}, 'results_metrics': {'trade_count': 117, 'avg_profit': -1.2698609145299145, 'total_profit': -0.07436117, 'profit': -148.573727, 'duration': 4282.5641025641025}, 'results_explanation': ' 117 trades. Avg profit -1.27%. Total profit -0.07436117 BTC (-148.57Σ%). Avg duration 4282.6 mins.', 'total_profit': -0.07436117, 'current_epoch': 11, 'is_initial_point': True, 'is_best': False}, {'loss': 100000, 'params_dict': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1156, 'roi_t2': 581, 'roi_t3': 408, 'roi_p1': 0.06860454019988212, 'roi_p2': 0.12473718444931989, 'roi_p3': 0.2896360635226823, 'stoploss': -0.30889015124682806}, 'params_details': {'buy': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4829777881718843, 408: 0.19334172464920202, 989: 0.06860454019988212, 2145: 0}, 'stoploss': {'stoploss': -0.30889015124682806}}, 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan mins.', 'total_profit': 0, 'current_epoch': 12, 'is_initial_point': True, 'is_best': False}] # noqa: E501 + return [ + { + 'loss': 0.4366182531161519, + 'params_dict': { + 'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1190, 'roi_t2': 541, 'roi_t3': 408, 'roi_p1': 0.026035863879169705, 'roi_p2': 0.12508730043628782, 'roi_p3': 0.27766427921605896, 'stoploss': -0.2562930402099556}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 15, 'fastd-value': 20, 'adx-value': 25, 'rsi-value': 28, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 88, 'sell-fastd-value': 97, 'sell-adx-value': 51, 'sell-rsi-value': 67, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4287874435315165, 408: 0.15112316431545753, 949: 0.026035863879169705, 2139: 0}, 'stoploss': {'stoploss': -0.2562930402099556}}, # noqa: E501 + 'results_metrics': {'trade_count': 2, 'avg_profit': -1.254995, 'total_profit': -0.00125625, 'profit': -2.50999, 'duration': 3930.0}, # noqa: E501 + 'results_explanation': ' 2 trades. Avg profit -1.25%. Total profit -0.00125625 BTC ( -2.51Σ%). Avg duration 3930.0 min.', # noqa: E501 + 'total_profit': -0.00125625, + 'current_epoch': 1, + 'is_initial_point': True, + 'is_best': True + }, { + 'loss': 20.0, + 'params_dict': { + 'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 334, 'roi_t2': 683, 'roi_t3': 140, 'roi_p1': 0.06403981740598495, 'roi_p2': 0.055519840060645045, 'roi_p3': 0.3253712811342459, 'stoploss': -0.338070047333259}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 38, 'adx-value': 48, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, # noqa: E501 + 'sell': {'sell-mfi-value': 96, 'sell-fastd-value': 68, 'sell-adx-value': 63, 'sell-rsi-value': 81, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, # noqa: E501 + 'roi': {0: 0.4449309386008759, 140: 0.11955965746663, 823: 0.06403981740598495, 1157: 0}, # noqa: E501 + 'stoploss': {'stoploss': -0.338070047333259}}, + 'results_metrics': {'trade_count': 1, 'avg_profit': 0.12357, 'total_profit': 6.185e-05, 'profit': 0.12357, 'duration': 1200.0}, # noqa: E501 + 'results_explanation': ' 1 trades. Avg profit 0.12%. Total profit 0.00006185 BTC ( 0.12Σ%). Avg duration 1200.0 min.', # noqa: E501 + 'total_profit': 6.185e-05, + 'current_epoch': 2, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 14.241196856510731, + 'params_dict': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 889, 'roi_t2': 533, 'roi_t3': 263, 'roi_p1': 0.04759065393663096, 'roi_p2': 0.1488819964638463, 'roi_p3': 0.4102801822104605, 'stoploss': -0.05394588767607611}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 25, 'fastd-value': 16, 'adx-value': 29, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 98, 'sell-fastd-value': 72, 'sell-adx-value': 51, 'sell-rsi-value': 82, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.6067528326109377, 263: 0.19647265040047726, 796: 0.04759065393663096, 1685: 0}, 'stoploss': {'stoploss': -0.05394588767607611}}, # noqa: E501 + 'results_metrics': {'trade_count': 621, 'avg_profit': -0.43883302093397747, 'total_profit': -0.13639474, 'profit': -272.515306, 'duration': 1691.207729468599}, # noqa: E501 + 'results_explanation': ' 621 trades. Avg profit -0.44%. Total profit -0.13639474 BTC (-272.52Σ%). Avg duration 1691.2 min.', # noqa: E501 + 'total_profit': -0.13639474, + 'current_epoch': 3, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 100000, + 'params_dict': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1402, 'roi_t2': 676, 'roi_t3': 215, 'roi_p1': 0.06264755784937427, 'roi_p2': 0.14258587851894644, 'roi_p3': 0.20671291201040828, 'stoploss': -0.11818343570194478}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 35, 'adx-value': 39, 'rsi-value': 29, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 54, 'sell-adx-value': 63, 'sell-rsi-value': 93, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.411946348378729, 215: 0.2052334363683207, 891: 0.06264755784937427, 2293: 0}, 'stoploss': {'stoploss': -0.11818343570194478}}, # noqa: E501 + 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # noqa: E501 + 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 + 'total_profit': 0, 'current_epoch': 4, 'is_initial_point': True, 'is_best': False + }, { + 'loss': 0.22195522184191518, + 'params_dict': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 1269, 'roi_t2': 601, 'roi_t3': 444, 'roi_p1': 0.07280999507931168, 'roi_p2': 0.08946698095898986, 'roi_p3': 0.1454876733325284, 'stoploss': -0.18181041180901014}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 17, 'fastd-value': 21, 'adx-value': 38, 'rsi-value': 33, 'mfi-enabled': True, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 82, 'sell-adx-value': 78, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3077646493708299, 444: 0.16227697603830155, 1045: 0.07280999507931168, 2314: 0}, 'stoploss': {'stoploss': -0.18181041180901014}}, # noqa: E501 + 'results_metrics': {'trade_count': 14, 'avg_profit': -0.3539515, 'total_profit': -0.002480140000000001, 'profit': -4.955321, 'duration': 3402.8571428571427}, # noqa: E501 + 'results_explanation': ' 14 trades. Avg profit -0.35%. Total profit -0.00248014 BTC ( -4.96Σ%). Avg duration 3402.9 min.', # noqa: E501 + 'total_profit': -0.002480140000000001, + 'current_epoch': 5, + 'is_initial_point': True, + 'is_best': True + }, { + 'loss': 0.545315889154162, + 'params_dict': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower', 'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 319, 'roi_t2': 556, 'roi_t3': 216, 'roi_p1': 0.06251955472249589, 'roi_p2': 0.11659519602202795, 'roi_p3': 0.0953744132197762, 'stoploss': -0.024551752215582423}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 43, 'adx-value': 46, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 87, 'sell-fastd-value': 65, 'sell-adx-value': 94, 'sell-rsi-value': 63, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.2744891639643, 216: 0.17911475074452382, 772: 0.06251955472249589, 1091: 0}, 'stoploss': {'stoploss': -0.024551752215582423}}, # noqa: E501 + 'results_metrics': {'trade_count': 39, 'avg_profit': -0.21400679487179478, 'total_profit': -0.0041773, 'profit': -8.346264999999997, 'duration': 636.9230769230769}, # noqa: E501 + 'results_explanation': ' 39 trades. Avg profit -0.21%. Total profit -0.00417730 BTC ( -8.35Σ%). Avg duration 636.9 min.', # noqa: E501 + 'total_profit': -0.0041773, + 'current_epoch': 6, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 4.713497421432944, + 'params_dict': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 771, 'roi_t2': 620, 'roi_t3': 145, 'roi_p1': 0.0586919200378493, 'roi_p2': 0.04984118697312542, 'roi_p3': 0.37521058680247044, 'stoploss': -0.14613268022709905}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 13, 'fastd-value': 41, 'adx-value': 21, 'rsi-value': 29, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 99, 'sell-fastd-value': 60, 'sell-adx-value': 81, 'sell-rsi-value': 69, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': False, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.4837436938134452, 145: 0.10853310701097472, 765: 0.0586919200378493, 1536: 0}, # noqa: E501 + 'stoploss': {'stoploss': -0.14613268022709905}}, # noqa: E501 + 'results_metrics': {'trade_count': 318, 'avg_profit': -0.39833954716981146, 'total_profit': -0.06339929, 'profit': -126.67197600000004, 'duration': 3140.377358490566}, # noqa: E501 + 'results_explanation': ' 318 trades. Avg profit -0.40%. Total profit -0.06339929 BTC (-126.67Σ%). Avg duration 3140.4 min.', # noqa: E501 + 'total_profit': -0.06339929, + 'current_epoch': 7, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 20.0, # noqa: E501 + 'params_dict': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal', 'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 1149, 'roi_t2': 375, 'roi_t3': 289, 'roi_p1': 0.05571820757172588, 'roi_p2': 0.0606240398618907, 'roi_p3': 0.1729012220156157, 'stoploss': -0.1588514289110401}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 24, 'fastd-value': 43, 'adx-value': 33, 'rsi-value': 20, 'mfi-enabled': False, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': True, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 89, 'sell-fastd-value': 74, 'sell-adx-value': 70, 'sell-rsi-value': 70, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': False, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.2892434694492323, 289: 0.11634224743361658, 664: 0.05571820757172588, 1813: 0}, 'stoploss': {'stoploss': -0.1588514289110401}}, # noqa: E501 + 'results_metrics': {'trade_count': 1, 'avg_profit': 0.0, 'total_profit': 0.0, 'profit': 0.0, 'duration': 5340.0}, # noqa: E501 + 'results_explanation': ' 1 trades. Avg profit 0.00%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration 5340.0 min.', # noqa: E501 + 'total_profit': 0.0, + 'current_epoch': 8, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 2.4731817780991223, + 'params_dict': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1012, 'roi_t2': 584, 'roi_t3': 422, 'roi_p1': 0.036764323603472565, 'roi_p2': 0.10335480573205287, 'roi_p3': 0.10322347377503042, 'stoploss': -0.2780610808108503}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 22, 'fastd-value': 20, 'adx-value': 29, 'rsi-value': 40, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 65, 'sell-adx-value': 81, 'sell-rsi-value': 64, 'sell-mfi-enabled': True, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.2433426031105559, 422: 0.14011912933552545, 1006: 0.036764323603472565, 2018: 0}, 'stoploss': {'stoploss': -0.2780610808108503}}, # noqa: E501 + 'results_metrics': {'trade_count': 229, 'avg_profit': -0.38433433624454144, 'total_profit': -0.044050070000000004, 'profit': -88.01256299999999, 'duration': 6505.676855895196}, # noqa: E501 + 'results_explanation': ' 229 trades. Avg profit -0.38%. Total profit -0.04405007 BTC ( -88.01Σ%). Avg duration 6505.7 min.', # noqa: E501 + 'total_profit': -0.044050070000000004, # noqa: E501 + 'current_epoch': 9, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': -0.2604606005845212, # noqa: E501 + 'params_dict': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal', 'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal', 'roi_t1': 792, 'roi_t2': 464, 'roi_t3': 215, 'roi_p1': 0.04594053535385903, 'roi_p2': 0.09623192684243963, 'roi_p3': 0.04428219070850663, 'stoploss': -0.16992287161634415}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 23, 'fastd-value': 24, 'adx-value': 22, 'rsi-value': 24, 'mfi-enabled': False, 'fastd-enabled': False, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'macd_cross_signal'}, 'sell': {'sell-mfi-value': 97, 'sell-fastd-value': 70, 'sell-adx-value': 64, 'sell-rsi-value': 80, 'sell-mfi-enabled': False, 'sell-fastd-enabled': True, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-sar_reversal'}, 'roi': {0: 0.18645465290480528, 215: 0.14217246219629864, 679: 0.04594053535385903, 1471: 0}, 'stoploss': {'stoploss': -0.16992287161634415}}, # noqa: E501 + 'results_metrics': {'trade_count': 4, 'avg_profit': 0.1080385, 'total_profit': 0.00021629, 'profit': 0.432154, 'duration': 2850.0}, # noqa: E501 + 'results_explanation': ' 4 trades. Avg profit 0.11%. Total profit 0.00021629 BTC ( 0.43Σ%). Avg duration 2850.0 min.', # noqa: E501 + 'total_profit': 0.00021629, + 'current_epoch': 10, + 'is_initial_point': True, + 'is_best': True + }, { + 'loss': 4.876465945994304, # noqa: E501 + 'params_dict': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower', 'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal', 'roi_t1': 579, 'roi_t2': 614, 'roi_t3': 273, 'roi_p1': 0.05307643172744114, 'roi_p2': 0.1352282078262871, 'roi_p3': 0.1913307406325751, 'stoploss': -0.25728526022513887}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 20, 'fastd-value': 32, 'adx-value': 49, 'rsi-value': 23, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': False, 'rsi-enabled': False, 'trigger': 'bb_lower'}, 'sell': {'sell-mfi-value': 75, 'sell-fastd-value': 56, 'sell-adx-value': 61, 'sell-rsi-value': 62, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-macd_cross_signal'}, 'roi': {0: 0.3796353801863034, 273: 0.18830463955372825, 887: 0.05307643172744114, 1466: 0}, 'stoploss': {'stoploss': -0.25728526022513887}}, # noqa: E501 + 'results_metrics': {'trade_count': 117, 'avg_profit': -1.2698609145299145, 'total_profit': -0.07436117, 'profit': -148.573727, 'duration': 4282.5641025641025}, # noqa: E501 + 'results_explanation': ' 117 trades. Avg profit -1.27%. Total profit -0.07436117 BTC (-148.57Σ%). Avg duration 4282.6 min.', # noqa: E501 + 'total_profit': -0.07436117, + 'current_epoch': 11, + 'is_initial_point': True, + 'is_best': False + }, { + 'loss': 100000, + 'params_dict': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal', 'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper', 'roi_t1': 1156, 'roi_t2': 581, 'roi_t3': 408, 'roi_p1': 0.06860454019988212, 'roi_p2': 0.12473718444931989, 'roi_p3': 0.2896360635226823, 'stoploss': -0.30889015124682806}, # noqa: E501 + 'params_details': {'buy': {'mfi-value': 10, 'fastd-value': 36, 'adx-value': 31, 'rsi-value': 22, 'mfi-enabled': True, 'fastd-enabled': True, 'adx-enabled': True, 'rsi-enabled': False, 'trigger': 'sar_reversal'}, 'sell': {'sell-mfi-value': 80, 'sell-fastd-value': 71, 'sell-adx-value': 60, 'sell-rsi-value': 85, 'sell-mfi-enabled': False, 'sell-fastd-enabled': False, 'sell-adx-enabled': True, 'sell-rsi-enabled': True, 'sell-trigger': 'sell-bb_upper'}, 'roi': {0: 0.4829777881718843, 408: 0.19334172464920202, 989: 0.06860454019988212, 2145: 0}, 'stoploss': {'stoploss': -0.30889015124682806}}, # noqa: E501 + 'results_metrics': {'trade_count': 0, 'avg_profit': None, 'total_profit': 0, 'profit': 0.0, 'duration': None}, # noqa: E501 + 'results_explanation': ' 0 trades. Avg profit nan%. Total profit 0.00000000 BTC ( 0.00Σ%). Avg duration nan min.', # noqa: E501 + 'total_profit': 0, + 'current_epoch': 12, + 'is_initial_point': True, + 'is_best': False + } + ] diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 3f6cc8c9a..ad90a86dd 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -250,7 +250,7 @@ tc15 = BTContainer(data=[ BTrade(sell_reason=SellType.STOP_LOSS, open_tick=2, close_tick=2)] ) -# Test 16: Buy, hold for 65 mins, then forcesell using roi=-1 +# Test 16: Buy, hold for 65 min, then forcesell using roi=-1 # Causes negative profit even though sell-reason is ROI. # stop-loss: 10%, ROI: 10% (should not apply), -100% after 65 minutes (limits trade duration) tc16 = BTContainer(data=[ diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 6a8daab8b..89099ff91 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -642,7 +642,7 @@ def test_generate_optimizer(mocker, default_conf) -> None: response_expected = { 'loss': 1.9840569076926293, 'results_explanation': (' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC ' - '( 2.31\N{GREEK CAPITAL LETTER SIGMA}%). Avg duration 100.0 mins.' + '( 2.31\N{GREEK CAPITAL LETTER SIGMA}%). Avg duration 100.0 min.' ).encode(locale.getpreferredencoding(), 'replace').decode('utf-8'), 'params_details': {'buy': {'adx-enabled': False, 'adx-value': 0, From 6e778ad710746c22c4ef96888b18946382e92010 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 12 Dec 2019 03:12:28 +0300 Subject: [PATCH 021/324] Seed hyperopt random_state if not passed --- freqtrade/optimize/hyperopt.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index ceed704c2..b4eecce2a 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -6,6 +6,7 @@ This module contains the hyperopt logic import locale import logging +import random import sys import warnings from collections import OrderedDict @@ -436,7 +437,7 @@ class Hyperopt: acq_optimizer="auto", n_initial_points=INITIAL_POINTS, acq_optimizer_kwargs={'n_jobs': cpu_count}, - random_state=self.config.get('hyperopt_random_state', None), + random_state=self.random_state, ) def fix_optimizer_models_list(self): @@ -475,7 +476,13 @@ class Hyperopt: logger.info(f"Loaded {len(trials)} previous evaluations from disk.") return trials + def _set_random_state(self, random_state: Optional[int]) -> int: + return random_state or random.randint(1, 2**32 - 1) + def start(self) -> None: + self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None)) + logger.info(f"Using optimizer random state: {self.random_state}") + data, timerange = self.backtesting.load_bt_data() preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data) From 330b8cf8a143d684b7b8f65ca74d9e13178693a2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 12 Dec 2019 14:08:44 +0100 Subject: [PATCH 022/324] space before unit ... --- freqtrade/plot/plotting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 57a02dd6b..85089af9c 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -121,7 +121,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: ) # Create description for sell summarizing the trade desc = trades.apply(lambda row: f"{round(row['profitperc'], 3)}%, {row['sell_reason']}, " - f"{row['duration']}min", + f"{row['duration']} min", axis=1) trade_sells = go.Scatter( x=trades["close_time"], From 5db883906af22eeb086174d1fcfae149f6978ed4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 13 Dec 2019 06:52:33 +0100 Subject: [PATCH 023/324] Try to verify available amount on the exchange --- freqtrade/freqtradebot.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0595e0d35..ac73f6d65 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -889,6 +889,27 @@ class FreqtradeBot: # TODO: figure out how to handle partially complete sell orders return False + def _safe_sell_amount(self, pair: str, amount: float) -> float: + """ + Get sellable amount. + Should be trade.amount - but will fall back to the available amount if necessary. + This should cover cases where get_real_amount() was not able to update the amount + for whatever reason. + :param pair: pair - used for logging + :param amount: amount we expect to be available + :return: amount to sell + :raise: DependencyException: if available balance is not within 2% of the available amount. + """ + wallet_amount = self.wallets.get_free(pair) + logger.info(f"Amounts: {wallet_amount} - {amount}") + if wallet_amount > amount: + return amount + elif wallet_amount > amount * 0.98: + logger.info(f"{pair} - Falling back to wallet-amount.") + return wallet_amount + else: + raise DependencyException("Not enough amount to sell.") + def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> None: """ Executes a limit sell for the given trade and limit @@ -919,10 +940,12 @@ class FreqtradeBot: # Emergencysells (default to market!) ordertype = self.strategy.order_types.get("emergencysell", "market") + amount = self._safe_sell_amount(trade.pair, trade.amount) + # Execute sell and update trade record order = self.exchange.sell(pair=str(trade.pair), ordertype=ordertype, - amount=trade.amount, rate=limit, + amount=amount, rate=limit, time_in_force=self.strategy.order_time_in_force['sell'] ) From b69f5afaaf5f0401f63e4b4a3b2fe5c353251211 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 13 Dec 2019 06:59:10 +0100 Subject: [PATCH 024/324] Round amount to precision also for dry-runs --- freqtrade/exchange/exchange.py | 7 ++++--- tests/test_freqtradebot.py | 5 ++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 30868df07..fbe7cd29a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -379,15 +379,16 @@ class Exchange: def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, params: Dict = {}) -> Dict[str, Any]: order_id = f'dry_run_{side}_{randint(0, 10**6)}' + _amount = self.symbol_amount_prec(pair, amount) dry_order = { "id": order_id, 'pair': pair, 'price': rate, - 'amount': amount, - "cost": amount * rate, + 'amount': _amount, + "cost": _amount * rate, 'type': ordertype, 'side': side, - 'remaining': amount, + 'remaining': _amount, 'datetime': arrow.utcnow().isoformat(), 'status': "closed" if ordertype == "market" else "open", 'fee': None, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index efab64a6a..5e197da71 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -786,7 +786,10 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - pair = 'NOCLUE/BTC' + pair = 'BLK/BTC' + # Ensure the pair is not in the whitelist! + assert pair not in default_conf['exchange']['pair_whitelist'] + # create open trade not in whitelist Trade.session.add(Trade( pair=pair, From 04257d8ecc357dea23face544f8e979834a20e19 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 13 Dec 2019 07:06:54 +0100 Subject: [PATCH 025/324] Add tests for safe_sell_amount --- freqtrade/freqtradebot.py | 2 +- tests/test_freqtradebot.py | 76 +++++++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ac73f6d65..3ebe89a71 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -901,7 +901,7 @@ class FreqtradeBot: :raise: DependencyException: if available balance is not within 2% of the available amount. """ wallet_amount = self.wallets.get_free(pair) - logger.info(f"Amounts: {wallet_amount} - {amount}") + logger.debug(f"{pair} - Wallet: {wallet_amount} - Trade-amount: {amount}") if wallet_amount > amount: return amount elif wallet_amount > amount * 0.98: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index efab64a6a..9c2fd9ddc 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -883,7 +883,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: 'freqtrade.freqtradebot.FreqtradeBot', get_target_bid=get_bid, _get_min_pair_stake_amount=MagicMock(return_value=1) - ) + ) buy_mm = MagicMock(return_value={'id': limit_buy_order['id']}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -2314,6 +2314,7 @@ 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.wallets.Wallets.get_free', MagicMock(return_value=300)) sellmock = MagicMock() patch_exchange(mocker) mocker.patch.multiple( @@ -2591,7 +2592,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple( - sell_flag=False, sell_type=SellType.NONE)) + sell_flag=False, sell_type=SellType.NONE)) freqtrade.create_trades() trade = Trade.query.first() @@ -2631,6 +2632,77 @@ 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, + fee, mocker, caplog) -> None: + patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=MagicMock(return_value={ + 'bid': 0.00002172, + 'ask': 0.00002173, + 'last': 0.00002172 + }), + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + get_fee=fee, + ) + + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) + + freqtrade.create_trades() + + trade = Trade.query.first() + amnt = trade.amount + trade.update(limit_buy_order) + patch_get_signal(freqtrade, value=(False, True)) + mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=trade.amount * 0.985)) + + assert freqtrade.handle_trade(trade) is True + assert log_has_re(r'.*Falling back to wallet-amount.', caplog) + assert trade.amount != amnt + + +def test__safe_sell_amount(default_conf, caplog, mocker): + patch_RPCManager(mocker) + patch_exchange(mocker) + amount = 95.33 + amount_wallet = 95.29 + mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet)) + trade = Trade( + pair='LTC/ETH', + amount=amount, + exchange='binance', + open_rate=0.245441, + open_order_id="123456" + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + + assert freqtrade._safe_sell_amount(trade.pair, trade.amount) == amount_wallet + assert log_has_re(r'.*Falling back to wallet-amount.', caplog) + + +def test__safe_sell_amount_error(default_conf, caplog, mocker): + patch_RPCManager(mocker) + patch_exchange(mocker) + amount = 95.33 + amount_wallet = 91.29 + mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet)) + trade = Trade( + pair='LTC/ETH', + amount=amount, + exchange='binance', + open_rate=0.245441, + open_order_id="123456" + ) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + with pytest.raises(DependencyException, match=r"Not enough amount to sell."): + assert freqtrade._safe_sell_amount(trade.pair, trade.amount) + + def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) From 3bd873f3c6e6b954711cc63d95bb2dfe70a808dc Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Fri, 13 Dec 2019 13:59:18 +0300 Subject: [PATCH 026/324] Add notes on random-state to the docs --- docs/hyperopt.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 9c9e9fdef..16bd4130b 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -6,8 +6,12 @@ algorithms included in the `scikit-optimize` package to accomplish this. The search will burn all your CPU cores, make your laptop sound like a fighter jet and still take a long time. +In general, the search for best parameters starts with a few random combinations and then uses Bayesian search with a +ML regressor algorithm (currently ExtraTreesRegressor) to quickly find a combination of parameters in the search hyperspace +that minimizes the value of the [loss function](#loss-functions). + Hyperopt requires historic data to be available, just as backtesting does. -To learn how to get data for the pairs and exchange you're interrested in, head over to the [Data Downloading](data-download.md) section of the documentation. +To learn how to get data for the pairs and exchange you're interested in, head over to the [Data Downloading](data-download.md) section of the documentation. !!! Bug Hyperopt can crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133) @@ -170,10 +174,6 @@ with different value combinations. It will then use the given historical data an buys based on the buy signals generated with the above function and based on the results it will end with telling you which paramter combination produced the best profits. -The search for best parameters starts with a few random combinations and then uses a -regressor algorithm (currently ExtraTreesRegressor) to quickly find a parameter combination -that minimizes the value of the [loss function](#loss-functions). - The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. When you want to test an indicator that isn't used by the bot currently, remember to add it to the `populate_indicators()` method in your custom hyperopt file. @@ -284,6 +284,16 @@ number). You can also enable position stacking in the configuration file by explicitly setting `"position_stacking"=true`. +### Reproducible results + +The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with a leading asterisk sign at the Hyperop output. + +The initial state for generation of these random values (random state) is controlled by the value of the `--random-state` command line option. You can set it to some arbitrary value of your choice to obtain reproducible results. + +If you have not set this value explicitly in the command line options, Hyperopt seeds the random state with some random value for you. The random state value for each Hyperopt run is shown in the log, so you can copy and paste it into the `--random-state` command line option to repeat the set of the initial random epochs used. + +If you have not changed anything in the command line options, configuration, timerange, Strategy and Hyperopt classes, historical data and the Loss Function -- you should obtain same hyperoptimization results with same random state value used. + ## Understand the Hyperopt Result Once Hyperopt is completed you can use the result to create a new strategy. From 014c18ead2d7a2847a74fa1880a5ba9fac405c40 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 13 Dec 2019 20:27:06 +0100 Subject: [PATCH 027/324] Improve output from show_config when trailing_stop is active --- freqtrade/rpc/telegram.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 51736968b..2e736f11a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -587,12 +587,23 @@ class Telegram(RPC): :return: None """ val = self._rpc_show_config() + if val['trailing_stop']: + sl_info = ( + f"*Initial Stoploss:* `{val['stoploss']}`\n" + f"*Trailing stop positive:* `{val['trailing_stop_positive']}`\n" + f"*Trailing stop offset:* `{val['trailing_stop_positive_offset']}`\n" + f"*Only trail above offset:* `{val['trailing_only_offset_is_reached']}`\n" + ) + + else: + sl_info = f"*Stoploss:* `{val['stoploss']}`\n" + self._send_msg( f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n" f"*Exchange:* `{val['exchange']}`\n" f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n" f"*Minimum ROI:* `{val['minimal_roi']}`\n" - f"*{'Trailing ' if val['trailing_stop'] else ''}Stoploss:* `{val['stoploss']}`\n" + f"{sl_info}" f"*Ticker Interval:* `{val['ticker_interval']}`\n" f"*Strategy:* `{val['strategy']}`'" ) From e4cc5c479fe73888a01b77fd4da310b36482ca2b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 13 Dec 2019 20:27:39 +0100 Subject: [PATCH 028/324] Test new show_config branch --- tests/rpc/test_rpc_telegram.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 7c4a8f0d6..2ba1ccf4b 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1177,6 +1177,16 @@ def test_show_config_handle(default_conf, update, mocker) -> None: assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] assert '*Exchange:* `bittrex`' in msg_mock.call_args_list[0][0][0] assert '*Strategy:* `DefaultStrategy`' in msg_mock.call_args_list[0][0][0] + assert '*Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] + + msg_mock.reset_mock() + freqtradebot.config['trailing_stop'] = True + telegram._show_config(update=update, context=MagicMock()) + assert msg_mock.call_count == 1 + assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] + assert '*Exchange:* `bittrex`' in msg_mock.call_args_list[0][0][0] + assert '*Strategy:* `DefaultStrategy`' in msg_mock.call_args_list[0][0][0] + assert '*Initial Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] def test_send_msg_buy_notification(default_conf, mocker) -> None: From a48c0ad8680a591f48ddb582a740fa3d3acfdcb8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Dec 2019 12:52:33 +0100 Subject: [PATCH 029/324] Use first pair of pairlist to get fee Use this instead of hardcoded ETH/BTC - so backtesting works with exchanges without ETH/BTC pair --- freqtrade/edge/__init__.py | 2 +- freqtrade/optimize/backtesting.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index afd20cf61..4bc3023a4 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -80,7 +80,7 @@ class Edge: if config.get('fee'): self.fee = config['fee'] else: - self.fee = self.exchange.get_fee() + self.fee = self.exchange.get_fee(symbol=self.config['exchange']['pair_whitelist'][0]) def calculate(self) -> bool: pairs = self.config['exchange']['pair_whitelist'] diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 637115dfd..7ef38331e 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -65,7 +65,7 @@ class Backtesting: if config.get('fee'): self.fee = config['fee'] else: - self.fee = self.exchange.get_fee() + self.fee = self.exchange.get_fee(symbol=self.config['exchange']['pair_whitelist'][0]) if self.config.get('runmode') != RunMode.HYPEROPT: self.dataprovider = DataProvider(self.config, self.exchange) From 82ff878e3868c8f08666a8fa08b4b12474184718 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Sat, 14 Dec 2019 15:15:20 +0300 Subject: [PATCH 030/324] Fix typo in the docs --- docs/hyperopt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 16bd4130b..f399fe816 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -286,7 +286,7 @@ You can also enable position stacking in the configuration file by explicitly se ### Reproducible results -The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with a leading asterisk sign at the Hyperop output. +The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with a leading asterisk sign at the Hyperopt output. The initial state for generation of these random values (random state) is controlled by the value of the `--random-state` command line option. You can set it to some arbitrary value of your choice to obtain reproducible results. From f2266ea9f42184e39597c63d3a64b3cd3b0a97f2 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Sat, 14 Dec 2019 15:17:45 +0300 Subject: [PATCH 031/324] Use shorter range for seeded random-state --- freqtrade/optimize/hyperopt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index b4eecce2a..1d524acf8 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -477,7 +477,7 @@ class Hyperopt: return trials def _set_random_state(self, random_state: Optional[int]) -> int: - return random_state or random.randint(1, 2**32 - 1) + return random_state or random.randint(1, 2**16 - 1) def start(self) -> None: self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None)) From 2275a1539ee989c9b5e2b2666e805472a5c5e8bc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Dec 2019 13:22:42 +0100 Subject: [PATCH 032/324] Remove default symbol from get_fee() --- freqtrade/exchange/exchange.py | 2 +- tests/edge/test_edge.py | 2 +- tests/exchange/test_exchange.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index fbe7cd29a..860f59fba 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -921,7 +921,7 @@ class Exchange: raise OperationalException(e) from e @retrier - def get_fee(self, symbol='ETH/BTC', type='', side='', amount=1, + def get_fee(self, symbol, type='', side='', amount=1, price=1, taker_or_maker='maker') -> float: try: # validate that markets are loaded before trying to get fee diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index 001dc9591..bdb986d6d 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -334,7 +334,7 @@ def test_process_expectancy(mocker, edge_conf): edge_conf['edge']['min_trade_number'] = 2 freqtrade = get_patched_freqtradebot(mocker, edge_conf) - def get_fee(): + def get_fee(*args, **kwargs): return 0.001 freqtrade.exchange.get_fee = get_fee diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index a21a5f3ac..82b62d5b8 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1646,10 +1646,10 @@ def test_get_fee(default_conf, mocker, exchange_name): }) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - assert exchange.get_fee() == 0.025 + assert exchange.get_fee('ETH/BTC') == 0.025 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, - 'get_fee', 'calculate_fee') + 'get_fee', 'calculate_fee', symbol="ETH/BTC") def test_stoploss_limit_order_unsupported_exchange(default_conf, mocker): From f81c49ce6d9734852b3354964543e5793c4adb10 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 14 Dec 2019 19:53:20 +0100 Subject: [PATCH 033/324] Fix typo causing a trailing "tic" in /show_config output --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 2e736f11a..a3f88003a 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -605,7 +605,7 @@ class Telegram(RPC): f"*Minimum ROI:* `{val['minimal_roi']}`\n" f"{sl_info}" f"*Ticker Interval:* `{val['ticker_interval']}`\n" - f"*Strategy:* `{val['strategy']}`'" + f"*Strategy:* `{val['strategy']}`" ) def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: From 26ab1088903d4088881803ba9605b05d58f80b9c Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 15 Dec 2019 01:10:09 +0300 Subject: [PATCH 034/324] Fix mypy errors in develop --- freqtrade/optimize/backtesting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 4b0288881..064a2f6ba 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -272,7 +272,7 @@ class Backtesting: elif sell.sell_type == (SellType.ROI): roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur) if roi is not None: - if roi == -1 and roi_entry % self.timeframe_mins == 0: + if roi == -1 and roi_entry % self.timeframe_min == 0: # When forceselling with ROI=-1, the roi time will always be equal to trade_dur. # If that entry is a multiple of the timeframe (so on candle open) # - we'll use open instead of close @@ -283,7 +283,7 @@ class Backtesting: (1 + trade.fee_open)) / (trade.fee_close - 1) if (trade_dur > 0 and trade_dur == roi_entry - and roi_entry % self.timeframe_mins == 0 + and roi_entry % self.timeframe_min == 0 and sell_row.open > close_rate): # new ROI entry came into effect. # use Open rate if open_rate > calculated sell rate From 931d24b5a8c028b879518921c0c67f79865b9dcf Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Dec 2019 09:22:15 +0100 Subject: [PATCH 035/324] Have dry_run_wallet default to 1000 --- docs/configuration.md | 2 +- freqtrade/constants.py | 5 +++-- freqtrade/exchange/exchange.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 5ad1a886e..927432a46 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -47,7 +47,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `ticker_interval` | The ticker interval to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *String* | `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency).
***Datatype:*** *String* | `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode.
*Defaults to `true`.*
***Datatype:*** *Boolean* -| `dry_run_wallet` | Overrides the default amount of 999.9 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason.
***Datatype:*** *Float* +| `dry_run_wallet` | Overrides the default amount of 1000 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason.
***Datatype:*** *Float* | `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
***Datatype:*** *Boolean* | `minimal_roi` | **Required.** Set the threshold in percent the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Dict* | `stoploss` | **Required.** Value of the stoploss in percent used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Float (as ratio)* diff --git a/freqtrade/constants.py b/freqtrade/constants.py index f5e5969eb..5c7190b41 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -18,7 +18,7 @@ REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'PrecisionFilter', 'PriceFilter'] -DRY_RUN_WALLET = 999.9 +DRY_RUN_WALLET = 1000 MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons USERPATH_HYPEROPTS = 'hyperopts' @@ -75,7 +75,7 @@ CONF_SCHEMA = { }, 'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT}, 'dry_run': {'type': 'boolean'}, - 'dry_run_wallet': {'type': 'number'}, + 'dry_run_wallet': {'type': 'number', 'default': DRY_RUN_WALLET}, 'process_only_new_candles': {'type': 'boolean'}, 'minimal_roi': { 'type': 'object', @@ -275,6 +275,7 @@ CONF_SCHEMA = { 'stake_currency', 'stake_amount', 'dry_run', + 'dry_run_wallet', 'bid_strategy', 'unfilledtimeout', 'stoploss', diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index fbe7cd29a..a148f9dae 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -479,7 +479,7 @@ class Exchange: @retrier def get_balance(self, currency: str) -> float: if self._config['dry_run']: - return constants.DRY_RUN_WALLET + return self._config['dry_run_wallet'] # ccxt exception is already handled by get_balances balances = self.get_balances() From 52b212db64ae6d679f76f6e9c0af382f54b13751 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Dec 2019 09:37:08 +0100 Subject: [PATCH 036/324] Fix tests after changing dry_run_wallet amount --- tests/exchange/test_exchange.py | 1 + tests/test_configuration.py | 7 ++++--- tests/test_freqtradebot.py | 2 ++ 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index a21a5f3ac..774ad8cf2 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -876,6 +876,7 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): def test_get_balance_dry_run(default_conf, mocker): default_conf['dry_run'] = True + default_conf['dry_run_wallet'] = 999.9 exchange = get_patched_exchange(mocker, default_conf) assert exchange.get_balance(currency='BTC') == 999.9 diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 89ca74afa..292d53315 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -8,9 +8,9 @@ from pathlib import Path from unittest.mock import MagicMock import pytest -from jsonschema import Draft4Validator, ValidationError, validate +from jsonschema import ValidationError -from freqtrade import OperationalException, constants +from freqtrade import OperationalException from freqtrade.configuration import (Arguments, Configuration, check_exchange, remove_credentials, validate_config_consistency) @@ -718,7 +718,8 @@ def test_load_config_warn_forcebuy(default_conf, mocker, caplog) -> None: def test_validate_default_conf(default_conf) -> None: - validate(default_conf, constants.CONF_SCHEMA, Draft4Validator) + # Validate via our validator - we allow setting defaults! + validate_config_schema(default_conf) def test_validate_tsl(default_conf): diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5e197da71..a73fd6c61 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -211,6 +211,7 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None: patch_RPCManager(mocker) patch_exchange(mocker) patch_edge(mocker) + edge_conf['dry_run_wallet'] = 999.9 freqtrade = FreqtradeBot(edge_conf) assert freqtrade._get_trade_stake_amount('NEO/BTC') == (999.9 * 0.5 * 0.01) / 0.20 @@ -1338,6 +1339,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, patch_exchange(mocker) patch_edge(mocker) edge_conf['max_open_trades'] = float('inf') + edge_conf['dry_run_wallet'] = 999.9 mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=MagicMock(return_value={ From fda8f7e30599810a834e2442a484d714e2ba3463 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Dec 2019 09:38:18 +0100 Subject: [PATCH 037/324] Introuce WalletDry - supporting dry-run wallets --- freqtrade/freqtradebot.py | 13 ++++++++----- freqtrade/rpc/telegram.py | 6 ++++++ freqtrade/wallets.py | 41 ++++++++++++++++++++++++++++++--------- 3 files changed, 46 insertions(+), 14 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 0595e0d35..df9fd0b17 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -25,7 +25,7 @@ from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.state import State from freqtrade.strategy.interface import IStrategy, SellType -from freqtrade.wallets import Wallets +from freqtrade.wallets import Wallets, WalletsDry logger = logging.getLogger(__name__) @@ -62,7 +62,13 @@ class FreqtradeBot: self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange - self.wallets = Wallets(self.config, self.exchange) + persistence.init(self.config.get('db_url', None), + clean_open_orders=self.config.get('dry_run', False)) + + if self.config['dry_run']: + self.wallets = WalletsDry(self.config, self.exchange) + else: + self.wallets = Wallets(self.config, self.exchange) self.dataprovider = DataProvider(self.config, self.exchange) # Attach Dataprovider to Strategy baseclass @@ -78,9 +84,6 @@ class FreqtradeBot: self.active_pair_whitelist = self._refresh_whitelist() - persistence.init(self.config.get('db_url', None), - clean_open_orders=self.config.get('dry_run', False)) - # Set initial bot state from config initial_state = self.config.get('initial_state') self.state = State[initial_state.upper()] if initial_state else State.STOPPED diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 2e736f11a..4d7857f44 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -331,7 +331,13 @@ class Telegram(RPC): try: result = self._rpc_balance(self._config['stake_currency'], self._config.get('fiat_display_currency', '')) + output = '' + if self._config['dry_run']: + output += ( + f"Simulated balances - starting capital: " + f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" + ) for currency in result['currencies']: if currency['est_stake'] > 0.0001: curr_output = "*{currency}:*\n" \ diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index c674b5286..eb2603776 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -28,9 +28,6 @@ class Wallets: def get_free(self, currency) -> float: - if self._config['dry_run']: - return self._config.get('dry_run_wallet', constants.DRY_RUN_WALLET) - balance = self._wallets.get(currency) if balance and balance.free: return balance.free @@ -39,9 +36,6 @@ class Wallets: def get_used(self, currency) -> float: - if self._config['dry_run']: - return self._config.get('dry_run_wallet', constants.DRY_RUN_WALLET) - balance = self._wallets.get(currency) if balance and balance.used: return balance.used @@ -50,9 +44,6 @@ class Wallets: def get_total(self, currency) -> float: - if self._config['dry_run']: - return self._config.get('dry_run_wallet', constants.DRY_RUN_WALLET) - balance = self._wallets.get(currency) if balance and balance.total: return balance.total @@ -75,3 +66,35 @@ class Wallets: def get_all_balances(self) -> Dict[str, Any]: return self._wallets + + +class WalletsDry(Wallets): + + def __init__(self, config: dict, exchange: Exchange) -> None: + self.start_cap = config['dry_run_wallet'] + super().__init__(config, exchange) + + def update(self) -> None: + """ Update does not do anything in dry-mode...""" + from freqtrade.persistence import Trade + closed_trades = Trade.get_trades(Trade.is_open.is_(False)).all() + print(len(closed_trades)) + tot_profit = sum([trade.calc_profit() for trade in closed_trades]) + current_stake = self.start_cap + tot_profit + self._wallets[self._config['stake_currency']] = Wallet( + self._config['stake_currency'], + current_stake, + 0, + current_stake + ) + open_trades = Trade.get_trades(Trade.is_open.is_(True)).all() + + for trade in open_trades: + curr = trade.pair.split('/')[0] + trade.amount + self._wallets[curr] = Wallet( + curr, + trade.amount, + 0, + trade.amount + ) From f0bbc75038134d7debb9aed56d85eb2c2702c6b5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Dec 2019 09:48:35 +0100 Subject: [PATCH 038/324] Combine dry_run wallet into original Wallets class --- freqtrade/freqtradebot.py | 8 ++--- freqtrade/rpc/telegram.py | 2 +- freqtrade/wallets.py | 64 +++++++++++++++++++++------------------ 3 files changed, 39 insertions(+), 35 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index df9fd0b17..a242c11ae 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -25,7 +25,7 @@ from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.state import State from freqtrade.strategy.interface import IStrategy, SellType -from freqtrade.wallets import Wallets, WalletsDry +from freqtrade.wallets import Wallets logger = logging.getLogger(__name__) @@ -65,10 +65,8 @@ class FreqtradeBot: persistence.init(self.config.get('db_url', None), clean_open_orders=self.config.get('dry_run', False)) - if self.config['dry_run']: - self.wallets = WalletsDry(self.config, self.exchange) - else: - self.wallets = Wallets(self.config, self.exchange) + self.wallets = Wallets(self.config, self.exchange) + self.dataprovider = DataProvider(self.config, self.exchange) # Attach Dataprovider to Strategy baseclass diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 4d7857f44..e36b46ba7 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -335,7 +335,7 @@ class Telegram(RPC): output = '' if self._config['dry_run']: output += ( - f"Simulated balances - starting capital: " + f"*Warning:*Simulated balances in Dry Mode.\nStarting capital: " f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" ) for currency in result['currencies']: diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index eb2603776..f8dd0ee2f 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -4,7 +4,7 @@ import logging from typing import Dict, NamedTuple, Any from freqtrade.exchange import Exchange -from freqtrade import constants +from freqtrade.persistence import Trade logger = logging.getLogger(__name__) @@ -23,6 +23,7 @@ class Wallets: self._config = config self._exchange = exchange self._wallets: Dict[str, Wallet] = {} + self.start_cap = config['dry_run_wallet'] self.update() @@ -50,36 +51,12 @@ class Wallets: else: return 0 - def update(self) -> None: - - balances = self._exchange.get_balances() - - for currency in balances: - self._wallets[currency] = Wallet( - currency, - balances[currency].get('free', None), - balances[currency].get('used', None), - balances[currency].get('total', None) - ) - - logger.info('Wallets synced.') - - def get_all_balances(self) -> Dict[str, Any]: - return self._wallets - - -class WalletsDry(Wallets): - - def __init__(self, config: dict, exchange: Exchange) -> None: - self.start_cap = config['dry_run_wallet'] - super().__init__(config, exchange) - - def update(self) -> None: - """ Update does not do anything in dry-mode...""" - from freqtrade.persistence import Trade + def _update_dry(self) -> None: + """ Update from database in dry-run mode""" closed_trades = Trade.get_trades(Trade.is_open.is_(False)).all() - print(len(closed_trades)) + tot_profit = sum([trade.calc_profit() for trade in closed_trades]) + current_stake = self.start_cap + tot_profit self._wallets[self._config['stake_currency']] = Wallet( self._config['stake_currency'], @@ -98,3 +75,32 @@ class WalletsDry(Wallets): 0, trade.amount ) + + def _update_live(self) -> None: + + balances = self._exchange.get_balances() + + for currency in balances: + self._wallets[currency] = Wallet( + currency, + balances[currency].get('free', None), + balances[currency].get('used', None), + balances[currency].get('total', None) + ) + + def update(self) -> None: + if self._config['dry_run']: + self._update_dry() + else: + self._update_live() + logger.info('Wallets synced.') + + def get_all_balances(self) -> Dict[str, Any]: + return self._wallets + + +class WalletsDry(Wallets): + + def __init__(self, config: dict, exchange: Exchange) -> None: + + super().__init__(config, exchange) From 4463d58470aff647fe3769e52c2c90c12588ca98 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Dec 2019 09:49:56 +0100 Subject: [PATCH 039/324] Add release section about collapsible section --- docs/developer.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/developer.md b/docs/developer.md index d731f1768..fe37c140e 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -246,6 +246,17 @@ Determine if crucial bugfixes have been made between this commit and the current git log --oneline --no-decorate --no-merges master..new_release ``` +To keep the release-log short, best wrap the full git changelog into a collapsible details secction. + +```markdown +
+Expand full changelog + +... Full git changelog + +
+``` + ### Create github release / tag Once the PR against master is merged (best right after merging): From 5a5741878cecbadae067c8ee0c29b211161a3aeb Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Dec 2019 10:26:56 +0100 Subject: [PATCH 040/324] Improve dry-run calculations --- freqtrade/freqtradebot.py | 4 ++-- freqtrade/wallets.py | 7 +++---- tests/test_freqtradebot.py | 30 ++++++++++++++++++++++++++++++ tests/test_integration.py | 1 + 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a242c11ae..5c3ef64b1 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -232,8 +232,8 @@ class FreqtradeBot: # Check if stake_amount is fulfilled if available_amount < stake_amount: raise DependencyException( - f"Available balance({available_amount} {self.config['stake_currency']}) is " - f"lower than stake amount({stake_amount} {self.config['stake_currency']})" + f"Available balance ({available_amount} {self.config['stake_currency']}) is " + f"lower than stake amount ({stake_amount} {self.config['stake_currency']})" ) return stake_amount diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index f8dd0ee2f..9ee305aab 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -54,21 +54,20 @@ class Wallets: def _update_dry(self) -> None: """ Update from database in dry-run mode""" closed_trades = Trade.get_trades(Trade.is_open.is_(False)).all() - + open_trades = Trade.get_trades(Trade.is_open.is_(True)).all() tot_profit = sum([trade.calc_profit() for trade in closed_trades]) + tot_in_trades = sum([trade.stake_amount for trade in open_trades]) - current_stake = self.start_cap + tot_profit + current_stake = self.start_cap + tot_profit - tot_in_trades self._wallets[self._config['stake_currency']] = Wallet( self._config['stake_currency'], current_stake, 0, current_stake ) - open_trades = Trade.get_trades(Trade.is_open.is_(True)).all() for trade in open_trades: curr = trade.pair.split('/')[0] - trade.amount self._wallets[curr] = Wallet( curr, trade.amount, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index a73fd6c61..a60ea8c7c 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3485,3 +3485,33 @@ def test_process_i_am_alive(default_conf, mocker, caplog): ftbot.process() assert log_has_re(message, caplog) + + +@pytest.mark.usefixtures("init_persistence") +def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order): + default_conf['dry_run'] = True + # Initialize to 2 times stake amount + default_conf['dry_run_wallet'] = 0.002 + default_conf['max_open_trades'] = 2 + patch_exchange(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_ticker=ticker, + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + get_fee=fee, + ) + + bot = get_patched_freqtradebot(mocker, default_conf) + patch_get_signal(bot) + assert bot.wallets.get_free('BTC') == 0.002 + + bot.create_trades() + trades = Trade.query.all() + assert len(trades) == 2 + + bot.config['max_open_trades'] = 3 + with pytest.raises( + DependencyException, + match=r"Available balance \(0 BTC\) is lower than stake amount \(0.001 BTC\)"): + bot.create_trades() + diff --git a/tests/test_integration.py b/tests/test_integration.py index 228ed8468..728e96d55 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -71,6 +71,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, ) mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) wallets_mock = mocker.patch("freqtrade.wallets.Wallets.update", MagicMock()) + mocker.patch("freqtrade.wallets.Wallets.get_free", MagicMock(return_value=1)) freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade.strategy.order_types['stoploss_on_exchange'] = True From c741b67c3c1be52872ec12273504af0a68c1975f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Dec 2019 10:39:52 +0100 Subject: [PATCH 041/324] Adjust tests for dry_run wallet simulation --- freqtrade/exchange/exchange.py | 2 +- tests/rpc/test_rpc.py | 2 +- tests/rpc/test_rpc_apiserver.py | 1 + tests/rpc/test_rpc_telegram.py | 6 ++++-- tests/test_freqtradebot.py | 1 - tests/test_wallets.py | 3 ++- 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a148f9dae..2401c59b6 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -18,7 +18,7 @@ from ccxt.base.decimal_to_precision import ROUND_DOWN, ROUND_UP from pandas import DataFrame from freqtrade import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError, constants) + OperationalException, TemporaryError) from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async from freqtrade.misc import deep_merge_dicts diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 699f2d962..0a8c1cabd 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -398,7 +398,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): get_valid_pair_combination=MagicMock( side_effect=lambda a, b: f"{b}/{a}" if a == "USDT" else f"{a}/{b}") ) - + default_conf['dry_run'] = False freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 555fcdc81..ebb70bdf8 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -230,6 +230,7 @@ def test_api_stopbuy(botclient): def test_api_balance(botclient, mocker, rpc_balance): ftbot, client = botclient + ftbot.config['dry_run'] = False mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', side_effect=lambda a, b: f"{a}/{b}") diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 2ba1ccf4b..b02f11394 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -462,7 +462,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tickers) -> None: - + default_conf['dry_run'] = False mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', @@ -494,6 +494,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick def test_balance_handle_empty_response(default_conf, update, mocker) -> None: + default_conf['dry_run'] = False mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={}) msg_mock = MagicMock() @@ -533,7 +534,8 @@ def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 - assert "Running in Dry Run, balances are not available." in result + assert "*Warning:*Simulated balances in Dry Mode." in result + assert "Starting capital: `1000` BTC" in result def test_balance_handle_too_large_response(default_conf, update, mocker) -> None: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index a60ea8c7c..18f5a461a 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3514,4 +3514,3 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order) DependencyException, match=r"Available balance \(0 BTC\) is lower than stake amount \(0.001 BTC\)"): bot.create_trades() - diff --git a/tests/test_wallets.py b/tests/test_wallets.py index ae2810a2d..3177edc05 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -1,7 +1,8 @@ # pragma pylint: disable=missing-docstring -from tests.conftest import get_patched_freqtradebot from unittest.mock import MagicMock +from tests.conftest import get_patched_freqtradebot + def test_sync_wallet_at_boot(mocker, default_conf): default_conf['dry_run'] = False From 23d467eb0d827f6abd6641b786fe1e2a72da2e9c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Dec 2019 10:41:57 +0100 Subject: [PATCH 042/324] Show simulation note also in restserver --- freqtrade/rpc/rpc.py | 1 + freqtrade/rpc/telegram.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 4cebe646e..84b72fe18 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -348,6 +348,7 @@ class RPC: 'total': total, 'symbol': symbol, 'value': value, + 'note': 'Simulated balances' if self._freqtrade.config.get('dry_run', False) else '' } def _rpc_start(self) -> Dict[str, str]: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e36b46ba7..c1572bb39 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -335,7 +335,9 @@ class Telegram(RPC): output = '' if self._config['dry_run']: output += ( - f"*Warning:*Simulated balances in Dry Mode.\nStarting capital: " + f"*Warning:*Simulated balances in Dry Mode.\n" + "This mode is still experimental!\n" + "Starting capital: " f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" ) for currency in result['currencies']: From 56e13c8919319997e99835ba645c7c3c175779ad Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Dec 2019 10:55:15 +0100 Subject: [PATCH 043/324] Enhance documentation for dry-run wallet --- docs/configuration.md | 10 ++++++---- freqtrade/wallets.py | 7 ------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 927432a46..f0724abc4 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -47,7 +47,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `ticker_interval` | The ticker interval to use (e.g `1m`, `5m`, `15m`, `30m`, `1h` ...). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *String* | `fiat_display_currency` | Fiat currency used to show your profits. [More information below](#what-values-can-be-used-for-fiat_display_currency).
***Datatype:*** *String* | `dry_run` | **Required.** Define if the bot must be in Dry Run or production mode.
*Defaults to `true`.*
***Datatype:*** *Boolean* -| `dry_run_wallet` | Overrides the default amount of 1000 stake currency units in the wallet used by the bot running in the Dry Run mode if you need it for any reason.
***Datatype:*** *Float* +| `dry_run_wallet` | Define the starting amount in stake currency for the simulated wallet used by the bot running in the Dry Run mode.
*Defaults to `1000`.*
***Datatype:*** *Float* | `process_only_new_candles` | Enable processing of indicators only when new candles arrive. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `false`.*
***Datatype:*** *Boolean* | `minimal_roi` | **Required.** Set the threshold in percent the bot will use to sell a trade. [More information below](#understand-minimal_roi). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Dict* | `stoploss` | **Required.** Value of the stoploss in percent used by the bot. More details in the [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Float (as ratio)* @@ -498,8 +498,10 @@ creating trades on the exchange. } ``` -Once you will be happy with your bot performance running in the Dry-run mode, -you can switch it to production mode. +Once you will be happy with your bot performance running in the Dry-run mode, you can switch it to production mode. + +!!! Note + A simulated wallet is available during dry-run mode, and will assume a starting capital of `dry_run_wallet` (defaults to 1000). ## Switch to production mode @@ -529,7 +531,7 @@ you run it in production mode. ``` !!! Note - If you have an exchange API key yet, [see our tutorial](/pre-requisite). + If you have an exchange API key yet, [see our tutorial](installation.md#setup-your-exchange-account). You should also make sure to read the [Exchanges](exchanges.md) section of the documentation to be aware of potential configuration details specific to your exchange. diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 9ee305aab..4c3a0f657 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -96,10 +96,3 @@ class Wallets: def get_all_balances(self) -> Dict[str, Any]: return self._wallets - - -class WalletsDry(Wallets): - - def __init__(self, config: dict, exchange: Exchange) -> None: - - super().__init__(config, exchange) From b5b6458f128a13920e5b0d7bf3f1fe1b084ead22 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Dec 2019 10:57:27 +0100 Subject: [PATCH 044/324] Add note about unlimited stake amount --- docs/configuration.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index f0724abc4..9490927df 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -149,6 +149,9 @@ In this case a trade amount is calculated as: currency_balance / (max_open_trades - current_open_trades) ``` +!!! Note "When using Dry-Run Mode" + When using `"stake_amount" : "unlimited",` in combination with Dry-Run, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time. It is therefore important to set `dry_run_wallet` to a sensible value, otherwise it may simulate trades with 100 BTC (or more) at once - which may not correspond to your real available balance. + ### Understand minimal_roi The `minimal_roi` configuration parameter is a JSON object where the key is a duration From ce845ab092e38448cf5f04443d36c004d4feb2c5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 15 Dec 2019 11:03:40 +0100 Subject: [PATCH 045/324] Improve docstring for dry-run wallet method --- freqtrade/wallets.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 4c3a0f657..dd706438f 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -52,7 +52,12 @@ class Wallets: return 0 def _update_dry(self) -> None: - """ Update from database in dry-run mode""" + """ + Update from database in dry-run mode + - Apply apply profits of closed trades on top of stake amount + - Subtract currently tied up stake_amount in open trades + - update balances for currencies currently in trades + """ closed_trades = Trade.get_trades(Trade.is_open.is_(False)).all() open_trades = Trade.get_trades(Trade.is_open.is_(True)).all() tot_profit = sum([trade.calc_profit() for trade in closed_trades]) From 655672c9575132ef5d143afd4ee38679e9d8b352 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 Dec 2019 06:22:54 +0100 Subject: [PATCH 046/324] Enhance documentation Note --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 9490927df..2c8f7cea7 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -150,7 +150,7 @@ currency_balance / (max_open_trades - current_open_trades) ``` !!! Note "When using Dry-Run Mode" - When using `"stake_amount" : "unlimited",` in combination with Dry-Run, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time. It is therefore important to set `dry_run_wallet` to a sensible value, otherwise it may simulate trades with 100 BTC (or more) at once - which may not correspond to your real available balance. + When using `"stake_amount" : "unlimited",` in combination with Dry-Run, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time. It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency. ### Understand minimal_roi From e398c37526e3817ea3d452db15098240c7f70e80 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2019 07:29:42 +0000 Subject: [PATCH 047/324] Bump pytest from 5.3.1 to 5.3.2 Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.3.1 to 5.3.2. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/5.3.1...5.3.2) Signed-off-by: dependabot-preview[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index f073ece6e..fe5b4e369 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.7.9 flake8-type-annotations==0.1.0 flake8-tidy-imports==3.1.0 mypy==0.750 -pytest==5.3.1 +pytest==5.3.2 pytest-asyncio==0.10.0 pytest-cov==2.8.1 pytest-mock==1.13.0 From 33db37a915ab2f2194483256194963f2021ec56d Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2019 07:30:04 +0000 Subject: [PATCH 048/324] Bump joblib from 0.14.0 to 0.14.1 Bumps [joblib](https://github.com/joblib/joblib) from 0.14.0 to 0.14.1. - [Release notes](https://github.com/joblib/joblib/releases) - [Changelog](https://github.com/joblib/joblib/blob/master/CHANGES.rst) - [Commits](https://github.com/joblib/joblib/compare/0.14.0...0.14.1) Signed-off-by: dependabot-preview[bot] --- requirements-hyperopt.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 2317cdf3e..b2428e37d 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -6,4 +6,4 @@ scipy==1.3.3 scikit-learn==0.22 scikit-optimize==0.5.2 filelock==3.0.12 -joblib==0.14.0 +joblib==0.14.1 From c05af1b63c57d5eb2014a2d800a6a6e72e48d506 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2019 07:30:27 +0000 Subject: [PATCH 049/324] Bump plotly from 4.3.0 to 4.4.1 Bumps [plotly](https://github.com/plotly/plotly.py) from 4.3.0 to 4.4.1. - [Release notes](https://github.com/plotly/plotly.py/releases) - [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md) - [Commits](https://github.com/plotly/plotly.py/compare/v4.3.0...v4.4.1) Signed-off-by: dependabot-preview[bot] --- requirements-plot.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-plot.txt b/requirements-plot.txt index 87d5553b6..415d4b888 100644 --- a/requirements-plot.txt +++ b/requirements-plot.txt @@ -1,5 +1,5 @@ # Include all requirements to run the bot. -r requirements.txt -plotly==4.3.0 +plotly==4.4.1 From cc41cdbf2277f8fd793317ba9af690ba9f957b9e Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2019 07:30:46 +0000 Subject: [PATCH 050/324] Bump mkdocs-material from 4.5.1 to 4.6.0 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 4.5.1 to 4.6.0. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/4.5.1...4.6.0) Signed-off-by: dependabot-preview[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index ae77c0b06..3e53c15e3 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,2 @@ -mkdocs-material==4.5.1 +mkdocs-material==4.6.0 mdx_truly_sane_lists==1.2 From 05de60a7febdfe846baa6c0f8714a18dd9ab81cf Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2019 07:31:38 +0000 Subject: [PATCH 051/324] Bump cachetools from 3.1.1 to 4.0.0 Bumps [cachetools](https://github.com/tkem/cachetools) from 3.1.1 to 4.0.0. - [Release notes](https://github.com/tkem/cachetools/releases) - [Changelog](https://github.com/tkem/cachetools/blob/master/CHANGELOG.rst) - [Commits](https://github.com/tkem/cachetools/compare/v3.1.1...v4.0.0) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index f00425e5b..3371537b7 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -4,7 +4,7 @@ ccxt==1.20.46 SQLAlchemy==1.3.11 python-telegram-bot==12.2.0 arrow==0.15.4 -cachetools==3.1.1 +cachetools==4.0.0 requests==2.22.0 urllib3==1.25.7 wrapt==1.11.2 From 75e6acd6ed54624c25479a386ccb5ce559354d5e Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 16 Dec 2019 09:46:17 +0000 Subject: [PATCH 052/324] Bump ccxt from 1.20.46 to 1.20.84 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.20.46 to 1.20.84. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/1.20.46...1.20.84) Signed-off-by: dependabot-preview[bot] --- requirements-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-common.txt b/requirements-common.txt index 3371537b7..a6d9a6f5e 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.20.46 +ccxt==1.20.84 SQLAlchemy==1.3.11 python-telegram-bot==12.2.0 arrow==0.15.4 From 2af9ffa7f2cf3f7bdfce4a4b582df667d5bd6cc5 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 16 Dec 2019 21:43:33 +0300 Subject: [PATCH 053/324] Align refresh_backtest_ to each other --- freqtrade/data/history.py | 16 ++++++++-------- freqtrade/utils.py | 2 +- tests/data/test_history.py | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index ddcae89ab..03aaeb0cc 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -334,12 +334,12 @@ def download_pair_history(datadir: Path, def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes: List[str], - dl_path: Path, timerange: Optional[TimeRange] = None, + datadir: Path, timerange: Optional[TimeRange] = None, erase=False) -> List[str]: """ Refresh stored ohlcv data for backtesting and hyperopt operations. - Used by freqtrade download-data - :return: Pairs not available + Used by freqtrade download-data subcommand. + :return: List of pairs that are not available. """ pairs_not_available = [] for pair in pairs: @@ -349,14 +349,14 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes continue for timeframe in timeframes: - dl_file = pair_data_filename(dl_path, pair, timeframe) + dl_file = pair_data_filename(datadir, pair, timeframe) if erase and dl_file.exists(): logger.info( f'Deleting existing data for pair {pair}, interval {timeframe}.') dl_file.unlink() logger.info(f'Downloading pair {pair}, interval {timeframe}.') - download_pair_history(datadir=dl_path, exchange=exchange, + download_pair_history(datadir=datadir, exchange=exchange, pair=pair, timeframe=str(timeframe), timerange=timerange) return pairs_not_available @@ -407,9 +407,9 @@ def download_trades_history(datadir: Path, def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: Path, timerange: TimeRange, erase=False) -> List[str]: """ - Refresh stored trades data. - Used by freqtrade download-data - :return: Pairs not available + Refresh stored trades data for backtesting and hyperopt operations. + Used by freqtrade download-data subcommand. + :return: List of pairs that are not available. """ pairs_not_available = [] for pair in pairs: diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 230fcf268..9e01c7ea6 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -213,7 +213,7 @@ def start_download_data(args: Dict[str, Any]) -> None: else: pairs_not_available = refresh_backtest_ohlcv_data( exchange, pairs=config["pairs"], timeframes=config["timeframes"], - dl_path=Path(config['datadir']), timerange=timerange, erase=config.get("erase")) + datadir=Path(config['datadir']), timerange=timerange, erase=config.get("erase")) except KeyboardInterrupt: sys.exit("SIGINT received, aborting ...") diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 6d89ab7c5..586492703 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -580,7 +580,7 @@ def test_refresh_backtest_ohlcv_data(mocker, default_conf, markets, caplog, test ex = get_patched_exchange(mocker, default_conf) timerange = TimeRange.parse_timerange("20190101-20190102") refresh_backtest_ohlcv_data(exchange=ex, pairs=["ETH/BTC", "XRP/BTC"], - timeframes=["1m", "5m"], dl_path=testdatadir, + timeframes=["1m", "5m"], datadir=testdatadir, timerange=timerange, erase=True ) @@ -600,7 +600,7 @@ def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir): timerange = TimeRange.parse_timerange("20190101-20190102") unav_pairs = refresh_backtest_ohlcv_data(exchange=ex, pairs=["BTT/BTC", "LTC/USDT"], timeframes=["1m", "5m"], - dl_path=testdatadir, + datadir=testdatadir, timerange=timerange, erase=False ) From 4cd45b6535d4ed613718c7c5656701a81891c1ee Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 16 Dec 2019 21:57:03 +0300 Subject: [PATCH 054/324] Rename download_*_history as non-public --- freqtrade/data/history.py | 40 ++++++++++++++--------------- tests/data/test_history.py | 52 +++++++++++++++++++------------------- 2 files changed, 46 insertions(+), 46 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 03aaeb0cc..15c5ed451 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -155,11 +155,11 @@ def load_pair_history(pair: str, # The user forced the refresh of pairs if refresh_pairs: - download_pair_history(datadir=datadir, - exchange=exchange, - pair=pair, - timeframe=timeframe, - timerange=timerange) + _download_pair_history(datadir=datadir, + exchange=exchange, + pair=pair, + timeframe=timeframe, + timerange=timerange) pairdata = load_tickerdata_file(datadir, pair, timeframe, timerange=timerange_startup) @@ -277,11 +277,11 @@ def _load_cached_data_for_updating(datadir: Path, pair: str, timeframe: str, return (data, since_ms) -def download_pair_history(datadir: Path, - exchange: Optional[Exchange], - pair: str, - timeframe: str = '5m', - timerange: Optional[TimeRange] = None) -> bool: +def _download_pair_history(datadir: Path, + exchange: Optional[Exchange], + pair: str, + timeframe: str = '5m', + timerange: Optional[TimeRange] = None) -> bool: """ Download latest candles from the exchange for the pair and timeframe passed in parameters The data is downloaded starting from the last correct data that @@ -356,16 +356,16 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes dl_file.unlink() logger.info(f'Downloading pair {pair}, interval {timeframe}.') - download_pair_history(datadir=datadir, exchange=exchange, - pair=pair, timeframe=str(timeframe), - timerange=timerange) + _download_pair_history(datadir=datadir, exchange=exchange, + pair=pair, timeframe=str(timeframe), + timerange=timerange) return pairs_not_available -def download_trades_history(datadir: Path, - exchange: Exchange, - pair: str, - timerange: Optional[TimeRange] = None) -> bool: +def _download_trades_history(datadir: Path, + exchange: Exchange, + pair: str, + timerange: Optional[TimeRange] = None) -> bool: """ Download trade history from the exchange. Appends to previously downloaded trades data. @@ -425,9 +425,9 @@ def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: dl_file.unlink() logger.info(f'Downloading trades for pair {pair}.') - download_trades_history(datadir=datadir, exchange=exchange, - pair=pair, - timerange=timerange) + _download_trades_history(datadir=datadir, exchange=exchange, + pair=pair, + timerange=timerange) return pairs_not_available diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 586492703..c304aa97d 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -14,9 +14,9 @@ from freqtrade import OperationalException from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.history import (_load_cached_data_for_updating, + _download_pair_history, + _download_trades_history, convert_trades_to_ohlcv, - download_pair_history, - download_trades_history, load_tickerdata_file, pair_data_filename, pair_trades_filename, refresh_backtest_ohlcv_data, @@ -267,12 +267,12 @@ def test_download_pair_history(ticker_history_list, mocker, default_conf, testda assert not file1_1.is_file() assert not file2_1.is_file() - assert download_pair_history(datadir=testdatadir, exchange=exchange, - pair='MEME/BTC', - timeframe='1m') - assert download_pair_history(datadir=testdatadir, exchange=exchange, - pair='CFI/BTC', - timeframe='1m') + assert _download_pair_history(datadir=testdatadir, exchange=exchange, + pair='MEME/BTC', + timeframe='1m') + assert _download_pair_history(datadir=testdatadir, exchange=exchange, + pair='CFI/BTC', + timeframe='1m') assert not exchange._pairs_last_refresh_time assert file1_1.is_file() assert file2_1.is_file() @@ -284,12 +284,12 @@ def test_download_pair_history(ticker_history_list, mocker, default_conf, testda assert not file1_5.is_file() assert not file2_5.is_file() - assert download_pair_history(datadir=testdatadir, exchange=exchange, - pair='MEME/BTC', - timeframe='5m') - assert download_pair_history(datadir=testdatadir, exchange=exchange, - pair='CFI/BTC', - timeframe='5m') + assert _download_pair_history(datadir=testdatadir, exchange=exchange, + pair='MEME/BTC', + timeframe='5m') + assert _download_pair_history(datadir=testdatadir, exchange=exchange, + pair='CFI/BTC', + timeframe='5m') assert not exchange._pairs_last_refresh_time assert file1_5.is_file() assert file2_5.is_file() @@ -307,8 +307,8 @@ def test_download_pair_history2(mocker, default_conf, testdatadir) -> None: json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None) mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=tick) exchange = get_patched_exchange(mocker, default_conf) - download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='1m') - download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='3m') + _download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='1m') + _download_pair_history(testdatadir, exchange, pair="UNITTEST/BTC", timeframe='3m') assert json_dump_mock.call_count == 2 @@ -324,9 +324,9 @@ def test_download_backtesting_data_exception(ticker_history, mocker, caplog, _backup_file(file1_1) _backup_file(file1_5) - assert not download_pair_history(datadir=testdatadir, exchange=exchange, - pair='MEME/BTC', - timeframe='1m') + assert not _download_pair_history(datadir=testdatadir, exchange=exchange, + pair='MEME/BTC', + timeframe='1m') # clean files freshly downloaded _clean_test_file(file1_1) _clean_test_file(file1_5) @@ -570,7 +570,7 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No def test_refresh_backtest_ohlcv_data(mocker, default_conf, markets, caplog, testdatadir): - dl_mock = mocker.patch('freqtrade.data.history.download_pair_history', MagicMock()) + dl_mock = mocker.patch('freqtrade.data.history._download_pair_history', MagicMock()) mocker.patch( 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) ) @@ -591,7 +591,7 @@ def test_refresh_backtest_ohlcv_data(mocker, default_conf, markets, caplog, test def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir): - dl_mock = mocker.patch('freqtrade.data.history.download_pair_history', MagicMock()) + dl_mock = mocker.patch('freqtrade.data.history._download_pair_history', MagicMock()) ex = get_patched_exchange(mocker, default_conf) mocker.patch( @@ -611,7 +611,7 @@ def test_download_data_no_markets(mocker, default_conf, caplog, testdatadir): def test_refresh_backtest_trades_data(mocker, default_conf, markets, caplog, testdatadir): - dl_mock = mocker.patch('freqtrade.data.history.download_trades_history', MagicMock()) + dl_mock = mocker.patch('freqtrade.data.history._download_trades_history', MagicMock()) mocker.patch( 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) ) @@ -646,8 +646,8 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad assert not file1.is_file() - assert download_trades_history(datadir=testdatadir, exchange=exchange, - pair='ETH/BTC') + assert _download_trades_history(datadir=testdatadir, exchange=exchange, + pair='ETH/BTC') assert log_has("New Amount of trades: 5", caplog) assert file1.is_file() @@ -657,8 +657,8 @@ def test_download_trades_history(trades_history, mocker, default_conf, testdatad mocker.patch('freqtrade.exchange.Exchange.get_historic_trades', MagicMock(side_effect=ValueError)) - assert not download_trades_history(datadir=testdatadir, exchange=exchange, - pair='ETH/BTC') + assert not _download_trades_history(datadir=testdatadir, exchange=exchange, + pair='ETH/BTC') assert log_has_re('Failed to download historic trades for pair: "ETH/BTC".*', caplog) From fa968996ed980df63aa139180af0ec5799b74b26 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 16 Dec 2019 22:01:26 +0300 Subject: [PATCH 055/324] Remove useless check --- freqtrade/data/history.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 15c5ed451..5176a9b19 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -295,11 +295,6 @@ def _download_pair_history(datadir: Path, :param timerange: range of time to download :return: bool with success state """ - if not exchange: - raise OperationalException( - "Exchange needs to be initialized when downloading pair history data" - ) - try: logger.info( f'Download history data for pair: "{pair}", timeframe: {timeframe} ' From a6fc743d854e33ddd44fb11cea277ab1a6482f86 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 16 Dec 2019 22:12:26 +0300 Subject: [PATCH 056/324] Align code in _download_*_history() --- freqtrade/data/history.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 5176a9b19..79500c512 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -307,11 +307,12 @@ def _download_pair_history(datadir: Path, logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None') # Default since_ms to 30 days if nothing is given - new_data = exchange.get_historic_ohlcv(pair=pair, timeframe=timeframe, - since_ms=since_ms if since_ms - else + new_data = exchange.get_historic_ohlcv(pair=pair, + timeframe=timeframe, + since_ms=since_ms if since_ms else int(arrow.utcnow().shift( - days=-30).float_timestamp) * 1000) + days=-30).float_timestamp) * 1000 + ) data.extend(new_data) logger.debug("New Start: %s", misc.format_ms_time(data[0][0])) @@ -376,11 +377,11 @@ def _download_trades_history(datadir: Path, logger.debug("Current Start: %s", trades[0]['datetime'] if trades else 'None') logger.debug("Current End: %s", trades[-1]['datetime'] if trades else 'None') + # Default since_ms to 30 days if nothing is given new_trades = exchange.get_historic_trades(pair=pair, since=since if since else int(arrow.utcnow().shift( days=-30).float_timestamp) * 1000, - # until=xxx, from_id=from_id, ) trades.extend(new_trades[1]) From 9cea5cd4427cbabd877860d34bb3a8cad32a2149 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 16 Dec 2019 20:38:36 +0100 Subject: [PATCH 057/324] Add documentation about ohlcv_partial_candle --- docs/developer.md | 6 ++++-- docs/exchanges.md | 21 +++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/docs/developer.md b/docs/developer.md index fe37c140e..5b07aff03 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -183,17 +183,19 @@ raw = ct.fetch_ohlcv(pair, timeframe=timeframe) # convert to dataframe df1 = parse_ticker_dataframe(raw, timeframe, pair=pair, drop_incomplete=False) -print(df1["date"].tail(1)) +print(df1.tail(1)) print(datetime.utcnow()) ``` ``` output -19 2019-06-08 00:00:00+00:00 + date open high low close volume +499 2019-06-08 00:00:00+00:00 0.000007 0.000007 0.000007 0.000007 26264344.0 2019-06-09 12:30:27.873327 ``` The output will show the last entry from the Exchange as well as the current UTC date. If the day shows the same day, then the last candle can be assumed as incomplete and should be dropped (leave the setting `"ohlcv_partial_candle"` from the exchange-class untouched / True). Otherwise, set `"ohlcv_partial_candle"` to `False` to not drop Candles (shown in the example above). +Another way is to run this command multiple times in a row and observe if the volume is changing (while the date remains the same). ## Updating example notebooks diff --git a/docs/exchanges.md b/docs/exchanges.md index 5bd283a69..d836b4c32 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -61,3 +61,24 @@ print(res) ```shell $ pip3 install web3 ``` + +### Send incomplete candles to the strategy + +Most exchanges return incomplete candles via their ohlcv / klines interface. +By default, Freqtrade assumes that incomplete candles are returned and removes the last candle assuming it's an incomplete candle. + +Wether your exchange returns incomplete candles or not can be checked using [the helper script](developer.md#Incomplete-candles) from the Contributor documentation. + +If the exchange does return incomplete candles and you would like to have incomplete candles in your strategy, you can set the following parameter in the configuration file. + +``` json +{ + + "exchange": { + "_ft_has_params": {"ohlcv_partial_candle": false} + } +} +``` + +!!! Warning "Danger of repainting" + Changing this parameter makes the strategy responsible to avoid repainting and handle this accordingly. Doing this is therefore not recommended, and should only be performed by experienced users who are fully aware of the impact this setting has. From 0277cd82ea66cd019138f0f45caaeed3b0afc187 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 16 Dec 2019 23:25:57 +0300 Subject: [PATCH 058/324] Make mypy happy --- freqtrade/data/history.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 79500c512..2c01f9f48 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -295,6 +295,11 @@ def _download_pair_history(datadir: Path, :param timerange: range of time to download :return: bool with success state """ + if not exchange: + raise OperationalException( + "Exchange needs to be initialized when downloading pair history data" + ) + try: logger.info( f'Download history data for pair: "{pair}", timeframe: {timeframe} ' From 707c5668a52f1956a458cf2255b26ce89aea1a7e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 06:11:44 +0100 Subject: [PATCH 059/324] Fix typo --- docs/exchanges.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index d836b4c32..76fa81f4a 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -67,7 +67,7 @@ $ pip3 install web3 Most exchanges return incomplete candles via their ohlcv / klines interface. By default, Freqtrade assumes that incomplete candles are returned and removes the last candle assuming it's an incomplete candle. -Wether your exchange returns incomplete candles or not can be checked using [the helper script](developer.md#Incomplete-candles) from the Contributor documentation. +Whether your exchange returns incomplete candles or not can be checked using [the helper script](developer.md#Incomplete-candles) from the Contributor documentation. If the exchange does return incomplete candles and you would like to have incomplete candles in your strategy, you can set the following parameter in the configuration file. From 0b5354f13db399a21b06275aeb0d6edc3097ee2f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 06:58:10 +0100 Subject: [PATCH 060/324] Add required arguments to Trade method --- tests/strategy/test_interface.py | 3 ++ tests/test_freqtradebot.py | 73 +++++++++++++++++++++++--------- tests/test_persistence.py | 33 ++++++++++++--- 3 files changed, 82 insertions(+), 27 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 5519b1a34..605622b8f 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -125,6 +125,7 @@ def test_min_roi_reached(default_conf, fee) -> None: trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, @@ -162,6 +163,7 @@ def test_min_roi_reached2(default_conf, fee) -> None: trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, @@ -195,6 +197,7 @@ def test_min_roi_reached3(default_conf, fee) -> None: trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 18f5a461a..9dff73322 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1512,13 +1512,15 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=limit_buy_order['amount']) - trade = Trade() - # Mock session away - Trade.session = MagicMock() - trade.open_order_id = '123' - trade.open_fee = 0.001 + trade = Trade( + open_order_id=123, + fee_open=0.001, + fee_close=0.001, + open_rate=0.01, + open_date=arrow.utcnow().datetime, + amount=11, + ) # Add datetime explicitly since sqlalchemy defaults apply only once written to database - trade.open_date = arrow.utcnow().datetime freqtrade.update_trade_state(trade) # Test amount not modified by fee-logic assert not log_has_re(r'Applying fee to .*', caplog) @@ -1541,7 +1543,7 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No assert log_has_re('Found open order for.*', caplog) -def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, mocker): +def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, fee, mocker): mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) # get_order should not be called!! mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError)) @@ -1554,6 +1556,8 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_ amount=amount, exchange='binance', open_rate=0.245441, + fee_open=fee.return_value, + fee_close=fee.return_value, open_order_id="123456", is_open=True, ) @@ -1562,7 +1566,7 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_ assert trade.amount == limit_buy_order['amount'] -def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_order, +def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_order, fee, limit_buy_order, mocker, caplog): trades_for_order[0]['amount'] = limit_buy_order['amount'] + 1e-14 mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) @@ -1577,6 +1581,8 @@ def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_ amount=amount, exchange='binance', open_rate=0.245441, + fee_open=fee.return_value, + fee_close=fee.return_value, open_order_id="123456", is_open=True, open_date=arrow.utcnow().datetime, @@ -2972,7 +2978,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, assert trade.sell_reason == SellType.STOP_LOSS.value -def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, caplog, mocker): +def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, fee, caplog, mocker): mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) patch_RPCManager(mocker) patch_exchange(mocker) @@ -2982,6 +2988,8 @@ def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, ca amount=amount, exchange='binance', open_rate=0.245441, + fee_open=fee.return_value, + fee_close=fee.return_value, open_order_id="123456" ) freqtrade = FreqtradeBot(default_conf) @@ -2994,7 +3002,7 @@ def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, ca caplog) -def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker): +def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker, fee): mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) patch_RPCManager(mocker) @@ -3005,6 +3013,8 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker): amount=amount, exchange='binance', open_rate=0.245441, + fee_open=fee.return_value, + fee_close=fee.return_value, open_order_id="123456" ) freqtrade = FreqtradeBot(default_conf) @@ -3017,7 +3027,7 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker): caplog) -def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, mocker): +def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, fee, mocker): trades_for_order[0]['fee']['currency'] = 'ETH' patch_RPCManager(mocker) @@ -3028,6 +3038,8 @@ def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, mo pair='LTC/ETH', amount=amount, exchange='binance', + fee_open=fee.return_value, + fee_close=fee.return_value, open_rate=0.245441, open_order_id="123456" ) @@ -3038,7 +3050,8 @@ def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, mo assert freqtrade.get_real_amount(trade, buy_order_fee) == amount -def test_get_real_amount_no_currency_in_fee(default_conf, trades_for_order, buy_order_fee, mocker): +def test_get_real_amount_no_currency_in_fee(default_conf, trades_for_order, buy_order_fee, + fee, mocker): limit_buy_order = deepcopy(buy_order_fee) limit_buy_order['fee'] = {'cost': 0.004, 'currency': None} @@ -3052,6 +3065,8 @@ def test_get_real_amount_no_currency_in_fee(default_conf, trades_for_order, buy_ pair='LTC/ETH', amount=amount, exchange='binance', + fee_open=fee.return_value, + fee_close=fee.return_value, open_rate=0.245441, open_order_id="123456" ) @@ -3062,7 +3077,7 @@ def test_get_real_amount_no_currency_in_fee(default_conf, trades_for_order, buy_ assert freqtrade.get_real_amount(trade, limit_buy_order) == amount -def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, mocker): +def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, fee, mocker): trades_for_order[0]['fee']['currency'] = 'BNB' trades_for_order[0]['fee']['cost'] = 0.00094518 @@ -3074,6 +3089,8 @@ def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, mock pair='LTC/ETH', amount=amount, exchange='binance', + fee_open=fee.return_value, + fee_close=fee.return_value, open_rate=0.245441, open_order_id="123456" ) @@ -3084,7 +3101,7 @@ def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, mock assert freqtrade.get_real_amount(trade, buy_order_fee) == amount -def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, caplog, mocker): +def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, caplog, fee, mocker): patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order2) @@ -3093,6 +3110,8 @@ def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, c pair='LTC/ETH', amount=amount, exchange='binance', + fee_open=fee.return_value, + fee_close=fee.return_value, open_rate=0.245441, open_order_id="123456" ) @@ -3106,7 +3125,8 @@ def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, c caplog) -def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee, caplog, mocker): +def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee, fee, + caplog, mocker): limit_buy_order = deepcopy(buy_order_fee) limit_buy_order['fee'] = {'cost': 0.004, 'currency': 'LTC'} @@ -3119,6 +3139,8 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee pair='LTC/ETH', amount=amount, exchange='binance', + fee_open=fee.return_value, + fee_close=fee.return_value, open_rate=0.245441, open_order_id="123456" ) @@ -3132,7 +3154,7 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee caplog) -def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order_fee, mocker): +def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order_fee, fee, mocker): limit_buy_order = deepcopy(buy_order_fee) limit_buy_order['fee'] = {'cost': 0.004} @@ -3144,6 +3166,8 @@ def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order pair='LTC/ETH', amount=amount, exchange='binance', + fee_open=fee.return_value, + fee_close=fee.return_value, open_rate=0.245441, open_order_id="123456" ) @@ -3154,7 +3178,7 @@ def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order assert freqtrade.get_real_amount(trade, limit_buy_order) == amount -def test_get_real_amount_wrong_amount(default_conf, trades_for_order, buy_order_fee, mocker): +def test_get_real_amount_wrong_amount(default_conf, trades_for_order, buy_order_fee, fee, mocker): limit_buy_order = deepcopy(buy_order_fee) limit_buy_order['amount'] = limit_buy_order['amount'] - 0.001 @@ -3167,6 +3191,8 @@ def test_get_real_amount_wrong_amount(default_conf, trades_for_order, buy_order_ amount=amount, exchange='binance', open_rate=0.245441, + fee_open=fee.return_value, + fee_close=fee.return_value, open_order_id="123456" ) freqtrade = FreqtradeBot(default_conf) @@ -3177,7 +3203,7 @@ def test_get_real_amount_wrong_amount(default_conf, trades_for_order, buy_order_ freqtrade.get_real_amount(trade, limit_buy_order) -def test_get_real_amount_wrong_amount_rounding(default_conf, trades_for_order, buy_order_fee, +def test_get_real_amount_wrong_amount_rounding(default_conf, trades_for_order, buy_order_fee, fee, mocker): # Floats should not be compared directly. limit_buy_order = deepcopy(buy_order_fee) @@ -3191,6 +3217,8 @@ def test_get_real_amount_wrong_amount_rounding(default_conf, trades_for_order, b pair='LTC/ETH', amount=amount, exchange='binance', + fee_open=fee.return_value, + fee_close=fee.return_value, open_rate=0.245441, open_order_id="123456" ) @@ -3202,7 +3230,7 @@ def test_get_real_amount_wrong_amount_rounding(default_conf, trades_for_order, b abs_tol=MATH_CLOSE_PREC,) -def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee, mocker): +def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee, fee, mocker): # Remove "Currency" from fee dict trades_for_order[0]['fee'] = {'cost': 0.008} @@ -3215,6 +3243,9 @@ def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee, amount=amount, exchange='binance', open_rate=0.245441, + fee_open=fee.return_value, + fee_close=fee.return_value, + open_order_id="123456" ) freqtrade = FreqtradeBot(default_conf) @@ -3223,7 +3254,7 @@ def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee, assert freqtrade.get_real_amount(trade, buy_order_fee) == amount -def test_get_real_amount_open_trade(default_conf, mocker): +def test_get_real_amount_open_trade(default_conf, fee, mocker): patch_RPCManager(mocker) patch_exchange(mocker) amount = 12345 @@ -3232,6 +3263,8 @@ def test_get_real_amount_open_trade(default_conf, mocker): amount=amount, exchange='binance', open_rate=0.245441, + fee_open=fee.return_value, + fee_close=fee.return_value, open_order_id="123456" ) order = { diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 231a1d2e2..67c61172c 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -136,12 +136,13 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog): id=2, pair='ETH/BTC', stake_amount=0.001, + open_rate=0.01, + amount=5, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', ) assert trade.open_order_id is None - assert trade.open_rate is None assert trade.close_profit is None assert trade.close_date is None @@ -173,6 +174,8 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog): id=1, pair='ETH/BTC', stake_amount=0.001, + amount=5, + open_rate=0.01, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -205,6 +208,8 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + open_rate=0.01, + amount=5, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -212,7 +217,7 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): trade.open_order_id = 'something' trade.update(limit_buy_order) - assert trade.calc_open_trade_price() == 0.0010024999999225068 + assert trade._calc_open_trade_price() == 0.0010024999999225068 trade.update(limit_sell_order) assert trade.calc_close_trade_price() == 0.0010646656050132426 @@ -229,6 +234,8 @@ def test_calc_close_trade_price_exception(limit_buy_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + open_rate=0.1, + amount=5, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -244,13 +251,14 @@ def test_update_open_order(limit_buy_order): trade = Trade( pair='ETH/BTC', stake_amount=1.00, + open_rate=0.01, + amount=5, fee_open=0.1, fee_close=0.1, exchange='bittrex', ) assert trade.open_order_id is None - assert trade.open_rate is None assert trade.close_profit is None assert trade.close_date is None @@ -258,7 +266,6 @@ def test_update_open_order(limit_buy_order): trade.update(limit_buy_order) assert trade.open_order_id is None - assert trade.open_rate is None assert trade.close_profit is None assert trade.close_date is None @@ -268,6 +275,8 @@ def test_update_invalid_order(limit_buy_order): trade = Trade( pair='ETH/BTC', stake_amount=1.00, + amount=5, + open_rate=0.001, fee_open=0.1, fee_close=0.1, exchange='bittrex', @@ -282,6 +291,8 @@ def test_calc_open_trade_price(limit_buy_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, + open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -290,10 +301,10 @@ def test_calc_open_trade_price(limit_buy_order, fee): trade.update(limit_buy_order) # Buy @ 0.00001099 # Get the open rate price with the standard fee rate - assert trade.calc_open_trade_price() == 0.0010024999999225068 - + assert trade._calc_open_trade_price() == 0.0010024999999225068 + trade.fee_open = 0.003 # Get the open rate price with a custom fee rate - assert trade.calc_open_trade_price(fee=0.003) == 0.001002999999922468 + assert trade._calc_open_trade_price() == 0.001002999999922468 @pytest.mark.usefixtures("init_persistence") @@ -301,6 +312,8 @@ def test_calc_close_trade_price(limit_buy_order, limit_sell_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, + open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -324,6 +337,8 @@ def test_calc_profit(limit_buy_order, limit_sell_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, + open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -356,6 +371,8 @@ def test_calc_profit_percent(limit_buy_order, limit_sell_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, + open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -630,6 +647,7 @@ def test_adjust_stop_loss(fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', @@ -681,6 +699,7 @@ def test_adjust_min_max_rates(fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, + amount=5, fee_open=fee.return_value, fee_close=fee.return_value, exchange='bittrex', From 307ade6251a7ec3a5e0a914204c9cf7da07c6866 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 07:02:02 +0100 Subject: [PATCH 061/324] Cache open_trade_price --- freqtrade/persistence.py | 23 ++++++++++++++++------- tests/test_persistence.py | 2 ++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 735c740c3..e1a3ad713 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -86,7 +86,7 @@ def check_migrate(engine) -> None: logger.debug(f'trying {table_back_name}') # Check for latest column - if not has_column(cols, 'stop_loss_pct'): + if not has_column(cols, 'open_trade_price'): logger.info(f'Running database migration - backup available as {table_back_name}') fee_open = get_column_def(cols, 'fee_open', 'fee') @@ -104,6 +104,8 @@ def check_migrate(engine) -> None: sell_reason = get_column_def(cols, 'sell_reason', 'null') strategy = get_column_def(cols, 'strategy', 'null') ticker_interval = get_column_def(cols, 'ticker_interval', 'null') + open_trade_price = get_column_def(cols, 'open_trade_price', + f'amount * open_rate * (1 + {fee_open})') # Schema migration necessary engine.execute(f"alter table trades rename to {table_back_name}") @@ -121,7 +123,7 @@ def check_migrate(engine) -> None: stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stoploss_order_id, stoploss_last_update, max_rate, min_rate, sell_reason, strategy, - ticker_interval + ticker_interval, open_trade_price ) select id, lower(exchange), case @@ -140,7 +142,8 @@ def check_migrate(engine) -> None: {initial_stop_loss_pct} initial_stop_loss_pct, {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, - {strategy} strategy, {ticker_interval} ticker_interval + {strategy} strategy, {ticker_interval} ticker_interval, + {open_trade_price} open_trade_price from {table_back_name} """) @@ -182,6 +185,7 @@ class Trade(_DECL_BASE): fee_close = Column(Float, nullable=False, default=0.0) open_rate = Column(Float) open_rate_requested = Column(Float) + open_trade_price = Column(Float) close_rate = Column(Float) close_rate_requested = Column(Float) close_profit = Column(Float) @@ -210,6 +214,10 @@ class Trade(_DECL_BASE): strategy = Column(String, nullable=True) ticker_interval = Column(Integer, nullable=True) + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.open_trade_price = self._calc_open_trade_price() + def __repr__(self): open_since = self.open_date.strftime('%Y-%m-%d %H:%M:%S') if self.is_open else 'closed' @@ -302,6 +310,7 @@ class Trade(_DECL_BASE): # Update open rate and actual amount self.open_rate = Decimal(order['price']) self.amount = Decimal(order['amount']) + self.open_trade_price = self._calc_open_trade_price() logger.info('%s_BUY has been fulfilled for %s.', order_type.upper(), self) self.open_order_id = None elif order_type in ('market', 'limit') and order['side'] == 'sell': @@ -331,7 +340,7 @@ class Trade(_DECL_BASE): self ) - def calc_open_trade_price(self, fee: Optional[float] = None) -> float: + def _calc_open_trade_price(self) -> float: """ Calculate the open_rate including fee. :param fee: fee to use on the open rate (optional). @@ -339,7 +348,7 @@ class Trade(_DECL_BASE): :return: Price in of the open trade incl. Fees """ buy_trade = (Decimal(self.amount) * Decimal(self.open_rate)) - fees = buy_trade * Decimal(fee or self.fee_open) + fees = buy_trade * Decimal(self.fee_open) return float(buy_trade + fees) def calc_close_trade_price(self, rate: Optional[float] = None, @@ -369,7 +378,7 @@ class Trade(_DECL_BASE): If rate is not set self.close_rate will be used :return: profit in stake currency as float """ - open_trade_price = self.calc_open_trade_price() + open_trade_price = self._calc_open_trade_price() close_trade_price = self.calc_close_trade_price( rate=(rate or self.close_rate), fee=(fee or self.fee_close) @@ -386,7 +395,7 @@ class Trade(_DECL_BASE): :param fee: fee to use on the close rate (optional). :return: profit in percentage as float """ - open_trade_price = self.calc_open_trade_price() + open_trade_price = self._calc_open_trade_price() close_trade_price = self.calc_close_trade_price( rate=(rate or self.close_rate), fee=(fee or self.fee_close) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 67c61172c..2cb79de98 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -498,6 +498,7 @@ def test_migrate_old(mocker, default_conf, fee): assert trade.max_rate == 0.0 assert trade.stop_loss == 0.0 assert trade.initial_stop_loss == 0.0 + assert trade.open_trade_price == 0.26758131848350003 def test_migrate_new(mocker, default_conf, fee, caplog): @@ -580,6 +581,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert log_has("trying trades_bak1", caplog) assert log_has("trying trades_bak2", caplog) assert log_has("Running database migration - backup available as trades_bak2", caplog) + assert trade.open_trade_price == 0.26758131848350003 def test_migrate_mid_state(mocker, default_conf, fee, caplog): From 861a7834fc32556b7d59be02c872c66f7a49efb3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 07:08:36 +0100 Subject: [PATCH 062/324] Call calc_open_price() whenever necessary --- freqtrade/freqtradebot.py | 2 ++ freqtrade/persistence.py | 17 +++++++++++------ tests/test_persistence.py | 4 ++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5c3ef64b1..a7ca67dcf 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -555,6 +555,7 @@ class FreqtradeBot: order['amount'] = new_amount # Fee was applied, so set to 0 trade.fee_open = 0 + trade.recalc_open_trade_price() except DependencyException as exception: logger.warning("Could not update trade amount: %s", exception) @@ -850,6 +851,7 @@ class FreqtradeBot: trade.amount = new_amount # Fee was applied, so set to 0 trade.fee_open = 0 + trade.recalc_open_trade_price() except DependencyException as e: logger.warning("Could not update trade amount: %s", e) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index e1a3ad713..354f713bc 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -216,7 +216,7 @@ class Trade(_DECL_BASE): def __init__(self, **kwargs): super().__init__(**kwargs) - self.open_trade_price = self._calc_open_trade_price() + self.recalc_open_trade_price() def __repr__(self): open_since = self.open_date.strftime('%Y-%m-%d %H:%M:%S') if self.is_open else 'closed' @@ -310,7 +310,7 @@ class Trade(_DECL_BASE): # Update open rate and actual amount self.open_rate = Decimal(order['price']) self.amount = Decimal(order['amount']) - self.open_trade_price = self._calc_open_trade_price() + self.recalc_open_trade_price() logger.info('%s_BUY has been fulfilled for %s.', order_type.upper(), self) self.open_order_id = None elif order_type in ('market', 'limit') and order['side'] == 'sell': @@ -351,6 +351,13 @@ class Trade(_DECL_BASE): fees = buy_trade * Decimal(self.fee_open) return float(buy_trade + fees) + def recalc_open_trade_price(self) -> None: + """ + Recalculate open_trade_price. + Must be called whenever open_rate or fee_open is changed. + """ + self.open_trade_price - self._calc_open_trade_price() + def calc_close_trade_price(self, rate: Optional[float] = None, fee: Optional[float] = None) -> float: """ @@ -378,12 +385,11 @@ class Trade(_DECL_BASE): If rate is not set self.close_rate will be used :return: profit in stake currency as float """ - open_trade_price = self._calc_open_trade_price() close_trade_price = self.calc_close_trade_price( rate=(rate or self.close_rate), fee=(fee or self.fee_close) ) - profit = close_trade_price - open_trade_price + profit = close_trade_price - self.open_trade_price return float(f"{profit:.8f}") def calc_profit_percent(self, rate: Optional[float] = None, @@ -395,12 +401,11 @@ class Trade(_DECL_BASE): :param fee: fee to use on the close rate (optional). :return: profit in percentage as float """ - open_trade_price = self._calc_open_trade_price() close_trade_price = self.calc_close_trade_price( rate=(rate or self.close_rate), fee=(fee or self.fee_close) ) - profit_percent = (close_trade_price / open_trade_price) - 1 + profit_percent = (close_trade_price / self.open_trade_price) - 1 return float(f"{profit_percent:.8f}") @staticmethod diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 2cb79de98..06df73cf9 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -498,7 +498,7 @@ def test_migrate_old(mocker, default_conf, fee): assert trade.max_rate == 0.0 assert trade.stop_loss == 0.0 assert trade.initial_stop_loss == 0.0 - assert trade.open_trade_price == 0.26758131848350003 + assert trade.open_trade_price == trade._calc_open_trade_price() def test_migrate_new(mocker, default_conf, fee, caplog): @@ -581,7 +581,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert log_has("trying trades_bak1", caplog) assert log_has("trying trades_bak2", caplog) assert log_has("Running database migration - backup available as trades_bak2", caplog) - assert trade.open_trade_price == 0.26758131848350003 + assert trade.open_trade_price == trade._calc_open_trade_price() def test_migrate_mid_state(mocker, default_conf, fee, caplog): From 362a40db6f89be6bd0fafb77f5f51b6f55345c45 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 07:09:56 +0100 Subject: [PATCH 063/324] Update docstring --- freqtrade/persistence.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 354f713bc..21e1b82ce 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -342,9 +342,7 @@ class Trade(_DECL_BASE): def _calc_open_trade_price(self) -> float: """ - Calculate the open_rate including fee. - :param fee: fee to use on the open rate (optional). - If rate is not set self.fee will be used + Calculate the open_rate including open_fee. :return: Price in of the open trade incl. Fees """ buy_trade = (Decimal(self.amount) * Decimal(self.open_rate)) From cbd10309f514862c7bb87eab81be845b7214c134 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 07:13:08 +0100 Subject: [PATCH 064/324] Add mid-state test --- tests/test_persistence.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 06df73cf9..0bde2f673 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -641,6 +641,7 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog): assert trade.max_rate == 0.0 assert trade.stop_loss == 0.0 assert trade.initial_stop_loss == 0.0 + assert trade.open_trade_price == trade._calc_open_trade_price() assert log_has("trying trades_bak0", caplog) assert log_has("Running database migration - backup available as trades_bak0", caplog) From 539b5627fdd67f0e245cce67a9b4428f94d8253f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 08:31:44 +0100 Subject: [PATCH 065/324] Fix typo --- freqtrade/persistence.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 21e1b82ce..61dee0414 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -354,7 +354,7 @@ class Trade(_DECL_BASE): Recalculate open_trade_price. Must be called whenever open_rate or fee_open is changed. """ - self.open_trade_price - self._calc_open_trade_price() + self.open_trade_price = self._calc_open_trade_price() def calc_close_trade_price(self, rate: Optional[float] = None, fee: Optional[float] = None) -> float: From a2964afd42c4ccf84ff315e140e0064efd62d6a7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 08:53:30 +0100 Subject: [PATCH 066/324] Rename profit_percent to profit_ratio to be consistent --- freqtrade/data/btanalysis.py | 2 +- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 4 ++-- freqtrade/persistence.py | 21 +++++++++++---------- freqtrade/rpc/rpc.py | 8 ++++---- freqtrade/strategy/interface.py | 6 +++--- tests/rpc/test_rpc_apiserver.py | 4 ++-- tests/test_freqtradebot.py | 3 ++- tests/test_persistence.py | 12 ++++++------ 9 files changed, 32 insertions(+), 30 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 379c80060..2fc931a9b 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -108,7 +108,7 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame: trades = pd.DataFrame([(t.pair, t.open_date.replace(tzinfo=timezone.utc), t.close_date.replace(tzinfo=timezone.utc) if t.close_date else None, - t.calc_profit(), t.calc_profit_percent(), + t.calc_profit(), t.calc_profit_ratio(), t.open_rate, t.close_rate, t.amount, (round((t.close_date.timestamp() - t.open_date.timestamp()) / 60, 2) if t.close_date else None), diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a7ca67dcf..8ae027fa2 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -950,7 +950,7 @@ class FreqtradeBot: profit_trade = trade.calc_profit(rate=profit_rate) # Use cached ticker here - it was updated seconds ago. current_rate = self.get_sell_rate(trade.pair, False) - profit_percent = trade.calc_profit_percent(profit_rate) + profit_percent = trade.calc_profit_ratio(profit_rate) gain = "profit" if profit_percent > 0 else "loss" msg = { diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 064a2f6ba..fc60bd310 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -329,7 +329,7 @@ class Backtesting: closerate = self._get_close_rate(sell_row, trade, sell, trade_dur) return BacktestResult(pair=pair, - profit_percent=trade.calc_profit_percent(rate=closerate), + profit_percent=trade.calc_profit_ratio(rate=closerate), profit_abs=trade.calc_profit(rate=closerate), open_time=buy_row.date, close_time=sell_row.date, @@ -345,7 +345,7 @@ class Backtesting: # no sell condition found - trade stil open at end of backtest period sell_row = partial_ticker[-1] bt_res = BacktestResult(pair=pair, - profit_percent=trade.calc_profit_percent(rate=sell_row.open), + profit_percent=trade.calc_profit_ratio(rate=sell_row.open), profit_abs=trade.calc_profit(rate=sell_row.open), open_time=buy_row.date, close_time=sell_row.date, diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 61dee0414..10896baaa 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -185,6 +185,7 @@ class Trade(_DECL_BASE): fee_close = Column(Float, nullable=False, default=0.0) open_rate = Column(Float) open_rate_requested = Column(Float) + # open_trade_price - calcuated via _calc_open_trade_price open_trade_price = Column(Float) close_rate = Column(Float) close_rate_requested = Column(Float) @@ -331,7 +332,7 @@ class Trade(_DECL_BASE): and marks trade as closed """ self.close_rate = Decimal(rate) - self.close_profit = self.calc_profit_percent() + self.close_profit = self.calc_profit_ratio() self.close_date = datetime.utcnow() self.is_open = False self.open_order_id = None @@ -361,9 +362,9 @@ class Trade(_DECL_BASE): """ Calculate the close_rate including fee :param fee: fee to use on the close rate (optional). - If rate is not set self.fee will be used + If rate is not set self.fee will be used :param rate: rate to compare with (optional). - If rate is not set self.close_rate will be used + If rate is not set self.close_rate will be used :return: Price in BTC of the open trade """ if rate is None and not self.close_rate: @@ -378,9 +379,9 @@ class Trade(_DECL_BASE): """ Calculate the absolute profit in stake currency between Close and Open trade :param fee: fee to use on the close rate (optional). - If rate is not set self.fee will be used + If rate is not set self.fee will be used :param rate: close rate to compare with (optional). - If rate is not set self.close_rate will be used + If rate is not set self.close_rate will be used :return: profit in stake currency as float """ close_trade_price = self.calc_close_trade_price( @@ -390,14 +391,14 @@ class Trade(_DECL_BASE): profit = close_trade_price - self.open_trade_price return float(f"{profit:.8f}") - def calc_profit_percent(self, rate: Optional[float] = None, - fee: Optional[float] = None) -> float: + def calc_profit_ratio(self, rate: Optional[float] = None, + fee: Optional[float] = None) -> float: """ - Calculates the profit in percentage (including fee). + Calculates the profit as ratio (including fee). :param rate: rate to compare with (optional). - If rate is not set self.close_rate will be used + If rate is not set self.close_rate will be used :param fee: fee to use on the close rate (optional). - :return: profit in percentage as float + :return: profit ratio as float """ close_trade_price = self.calc_close_trade_price( rate=(rate or self.close_rate), diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 84b72fe18..3b4b7570a 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -123,7 +123,7 @@ class RPC: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) except DependencyException: current_rate = NAN - current_profit = trade.calc_profit_percent(current_rate) + current_profit = trade.calc_profit_ratio(current_rate) fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%' if trade.close_profit else None) trade_dict = trade.to_json() @@ -151,7 +151,7 @@ class RPC: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) except DependencyException: current_rate = NAN - trade_perc = (100 * trade.calc_profit_percent(current_rate)) + trade_perc = (100 * trade.calc_profit_ratio(current_rate)) trade_profit = trade.calc_profit(current_rate) profit_str = f'{trade_perc:.2f}%' if self._fiat_converter: @@ -240,7 +240,7 @@ class RPC: durations.append((trade.close_date - trade.open_date).total_seconds()) if not trade.is_open: - profit_percent = trade.calc_profit_percent() + profit_percent = trade.calc_profit_ratio() profit_closed_coin.append(trade.calc_profit()) profit_closed_perc.append(profit_percent) else: @@ -249,7 +249,7 @@ class RPC: current_rate = self._freqtrade.get_sell_rate(trade.pair, False) except DependencyException: current_rate = NAN - profit_percent = trade.calc_profit_percent(rate=current_rate) + profit_percent = trade.calc_profit_ratio(rate=current_rate) profit_all_coin.append( trade.calc_profit(rate=trade.close_rate or current_rate) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 2b3a6194f..985ff37de 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -296,7 +296,7 @@ class IStrategy(ABC): """ # Set current rate to low for backtesting sell current_rate = low or rate - current_profit = trade.calc_profit_percent(current_rate) + current_profit = trade.calc_profit_ratio(current_rate) trade.adjust_min_max_rates(high or current_rate) @@ -311,7 +311,7 @@ class IStrategy(ABC): # Set current rate to high for backtesting sell current_rate = high or rate - current_profit = trade.calc_profit_percent(current_rate) + current_profit = trade.calc_profit_ratio(current_rate) config_ask_strategy = self.config.get('ask_strategy', {}) if buy and config_ask_strategy.get('ignore_roi_if_buy_signal', False): @@ -360,7 +360,7 @@ class IStrategy(ABC): sl_offset = self.trailing_stop_positive_offset # Make sure current_profit is calculated using high for backtesting. - high_profit = current_profit if not high else trade.calc_profit_percent(high) + high_profit = current_profit if not high else trade.calc_profit_ratio(high) # Don't update stoploss if trailing_only_offset_is_reached is true. if not (self.trailing_only_offset_is_reached and high_profit < sl_offset): diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index ebb70bdf8..f1e3421c5 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -381,7 +381,7 @@ def test_api_performance(botclient, mocker, ticker, fee): close_rate=0.265441, ) - trade.close_profit = trade.calc_profit_percent() + trade.close_profit = trade.calc_profit_ratio() Trade.session.add(trade) trade = Trade( @@ -396,7 +396,7 @@ def test_api_performance(botclient, mocker, ticker, fee): fee_open=fee.return_value, close_rate=0.391 ) - trade.close_profit = trade.calc_profit_percent() + trade.close_profit = trade.calc_profit_ratio() Trade.session.add(trade) Trade.session.flush() diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 9dff73322..341bc021f 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1543,7 +1543,8 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No assert log_has_re('Found open order for.*', caplog) -def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, fee, mocker): +def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, fee, + mocker): mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) # get_order should not be called!! mocker.patch('freqtrade.exchange.Exchange.get_order', MagicMock(side_effect=ValueError)) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 0bde2f673..25ad8b6a7 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -226,7 +226,7 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): assert trade.calc_profit() == 0.00006217 # Profit in percent - assert trade.calc_profit_percent() == 0.06201058 + assert trade.calc_profit_ratio() == 0.06201058 @pytest.mark.usefixtures("init_persistence") @@ -367,7 +367,7 @@ def test_calc_profit(limit_buy_order, limit_sell_order, fee): @pytest.mark.usefixtures("init_persistence") -def test_calc_profit_percent(limit_buy_order, limit_sell_order, fee): +def test_calc_profit_ratio(limit_buy_order, limit_sell_order, fee): trade = Trade( pair='ETH/BTC', stake_amount=0.001, @@ -381,17 +381,17 @@ def test_calc_profit_percent(limit_buy_order, limit_sell_order, fee): trade.update(limit_buy_order) # Buy @ 0.00001099 # Get percent of profit with a custom rate (Higher than open rate) - assert trade.calc_profit_percent(rate=0.00001234) == 0.11723875 + assert trade.calc_profit_ratio(rate=0.00001234) == 0.11723875 # Get percent of profit with a custom rate (Lower than open rate) - assert trade.calc_profit_percent(rate=0.00000123) == -0.88863828 + assert trade.calc_profit_ratio(rate=0.00000123) == -0.88863828 # Test when we apply a Sell order. Sell higher than open rate @ 0.00001173 trade.update(limit_sell_order) - assert trade.calc_profit_percent() == 0.06201058 + assert trade.calc_profit_ratio() == 0.06201058 # Test with a custom fee rate on the close trade - assert trade.calc_profit_percent(fee=0.003) == 0.06147824 + assert trade.calc_profit_ratio(fee=0.003) == 0.06147824 @pytest.mark.usefixtures("init_persistence") From 86de88ed480bfd16ec8c875b79c3d4298b0be849 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 09:36:26 +0100 Subject: [PATCH 067/324] Align usage of history import in test --- tests/data/test_history.py | 100 +++++++++++++++---------------------- 1 file changed, 40 insertions(+), 60 deletions(-) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index c304aa97d..708702ab2 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -12,16 +12,17 @@ from pandas import DataFrame from freqtrade import OperationalException from freqtrade.configuration import TimeRange -from freqtrade.data import history -from freqtrade.data.history import (_load_cached_data_for_updating, - _download_pair_history, +from freqtrade.data.history import (_download_pair_history, _download_trades_history, - convert_trades_to_ohlcv, + _load_cached_data_for_updating, + convert_trades_to_ohlcv, get_timeframe, + load_data, load_pair_history, load_tickerdata_file, pair_data_filename, pair_trades_filename, refresh_backtest_ohlcv_data, refresh_backtest_trades_data, - trim_tickerlist) + trim_dataframe, trim_tickerlist, + validate_backtest_data) from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import file_dump_json from freqtrade.strategy.default_strategy import DefaultStrategy @@ -64,7 +65,7 @@ def _clean_test_file(file: Path) -> None: def test_load_data_30min_ticker(mocker, caplog, default_conf, testdatadir) -> None: - ld = history.load_pair_history(pair='UNITTEST/BTC', timeframe='30m', datadir=testdatadir) + ld = load_pair_history(pair='UNITTEST/BTC', timeframe='30m', datadir=testdatadir) assert isinstance(ld, DataFrame) assert not log_has( 'Download history data for pair: "UNITTEST/BTC", timeframe: 30m ' @@ -73,7 +74,7 @@ def test_load_data_30min_ticker(mocker, caplog, default_conf, testdatadir) -> No def test_load_data_7min_ticker(mocker, caplog, default_conf, testdatadir) -> None: - ld = history.load_pair_history(pair='UNITTEST/BTC', timeframe='7m', datadir=testdatadir) + ld = load_pair_history(pair='UNITTEST/BTC', timeframe='7m', datadir=testdatadir) assert isinstance(ld, DataFrame) assert ld.empty assert log_has( @@ -86,7 +87,7 @@ def test_load_data_1min_ticker(ticker_history, mocker, caplog, testdatadir) -> N mocker.patch('freqtrade.exchange.Exchange.get_historic_ohlcv', return_value=ticker_history) file = testdatadir / 'UNITTEST_BTC-1m.json' _backup_file(file, copy_file=True) - history.load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC']) + load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC']) assert file.is_file() assert not log_has( 'Download history data for pair: "UNITTEST/BTC", interval: 1m ' @@ -99,10 +100,9 @@ def test_load_data_startup_candles(mocker, caplog, default_conf, testdatadir) -> ltfmock = mocker.patch('freqtrade.data.history.load_tickerdata_file', MagicMock(return_value=None)) timerange = TimeRange('date', None, 1510639620, 0) - history.load_pair_history(pair='UNITTEST/BTC', timeframe='1m', - datadir=testdatadir, timerange=timerange, - startup_candles=20, - ) + load_pair_history(pair='UNITTEST/BTC', timeframe='1m', + datadir=testdatadir, timerange=timerange, + startup_candles=20,) assert ltfmock.call_count == 1 assert ltfmock.call_args_list[0][1]['timerange'] != timerange @@ -121,9 +121,7 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, _backup_file(file) # do not download a new pair if refresh_pairs isn't set - history.load_pair_history(datadir=testdatadir, - timeframe='1m', - pair='MEME/BTC') + load_pair_history(datadir=testdatadir, timeframe='1m', pair='MEME/BTC') assert not file.is_file() assert log_has( 'No history data for pair: "MEME/BTC", timeframe: 1m. ' @@ -131,22 +129,16 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, ) # download a new pair if refresh_pairs is set - history.load_pair_history(datadir=testdatadir, - timeframe='1m', - refresh_pairs=True, - exchange=exchange, - pair='MEME/BTC') + load_pair_history(datadir=testdatadir, timeframe='1m', + refresh_pairs=True, exchange=exchange, pair='MEME/BTC') assert file.is_file() assert log_has_re( 'Download history data for pair: "MEME/BTC", timeframe: 1m ' 'and store in .*', caplog ) with pytest.raises(OperationalException, match=r'Exchange needs to be initialized when.*'): - history.load_pair_history(datadir=testdatadir, - timeframe='1m', - refresh_pairs=True, - exchange=None, - pair='MEME/BTC') + load_pair_history(datadir=testdatadir, timeframe='1m', + refresh_pairs=True, exchange=None, pair='MEME/BTC') _clean_test_file(file) @@ -351,10 +343,8 @@ def test_load_partial_missing(testdatadir, caplog) -> None: # Make sure we start fresh - test missing data at start start = arrow.get('2018-01-01T00:00:00') end = arrow.get('2018-01-11T00:00:00') - tickerdata = history.load_data(testdatadir, '5m', ['UNITTEST/BTC'], - startup_candles=20, - timerange=TimeRange('date', 'date', - start.timestamp, end.timestamp)) + tickerdata = load_data(testdatadir, '5m', ['UNITTEST/BTC'], startup_candles=20, + timerange=TimeRange('date', 'date', start.timestamp, end.timestamp)) assert log_has( 'Using indicator startup period: 20 ...', caplog ) @@ -369,10 +359,8 @@ def test_load_partial_missing(testdatadir, caplog) -> None: caplog.clear() start = arrow.get('2018-01-10T00:00:00') end = arrow.get('2018-02-20T00:00:00') - tickerdata = history.load_data(datadir=testdatadir, timeframe='5m', - pairs=['UNITTEST/BTC'], - timerange=TimeRange('date', 'date', - start.timestamp, end.timestamp)) + tickerdata = load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], + timerange=TimeRange('date', 'date', start.timestamp, end.timestamp)) # timedifference in 5 minutes td = ((end - start).total_seconds() // 60 // 5) + 1 assert td != len(tickerdata['UNITTEST/BTC']) @@ -385,7 +373,7 @@ def test_load_partial_missing(testdatadir, caplog) -> None: def test_init(default_conf, mocker) -> None: exchange = get_patched_exchange(mocker, default_conf) - assert {} == history.load_data( + assert {} == load_data( datadir='', exchange=exchange, pairs=[], @@ -447,7 +435,7 @@ def test_trim_tickerlist(testdatadir) -> None: def test_trim_dataframe(testdatadir) -> None: - data = history.load_data( + data = load_data( datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'] @@ -458,7 +446,7 @@ def test_trim_dataframe(testdatadir) -> None: # Remove first 30 minutes (1800 s) tr = TimeRange('date', None, min_date + 1800, 0) - data_modify = history.trim_dataframe(data_modify, tr) + data_modify = trim_dataframe(data_modify, tr) assert not data_modify.equals(data) assert len(data_modify) < len(data) assert len(data_modify) == len(data) - 30 @@ -468,7 +456,7 @@ def test_trim_dataframe(testdatadir) -> None: data_modify = data.copy() # Remove last 30 minutes (1800 s) tr = TimeRange(None, 'date', 0, max_date - 1800) - data_modify = history.trim_dataframe(data_modify, tr) + data_modify = trim_dataframe(data_modify, tr) assert not data_modify.equals(data) assert len(data_modify) < len(data) assert len(data_modify) == len(data) - 30 @@ -478,7 +466,7 @@ def test_trim_dataframe(testdatadir) -> None: data_modify = data.copy() # Remove first 25 and last 30 minutes (1800 s) tr = TimeRange('date', 'date', min_date + 1500, max_date - 1800) - data_modify = history.trim_dataframe(data_modify, tr) + data_modify = trim_dataframe(data_modify, tr) assert not data_modify.equals(data) assert len(data_modify) < len(data) assert len(data_modify) == len(data) - 55 @@ -515,13 +503,13 @@ def test_get_timeframe(default_conf, mocker, testdatadir) -> None: strategy = DefaultStrategy(default_conf) data = strategy.tickerdata_to_dataframe( - history.load_data( + load_data( datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'] ) ) - min_date, max_date = history.get_timeframe(data) + min_date, max_date = get_timeframe(data) assert min_date.isoformat() == '2017-11-04T23:02:00+00:00' assert max_date.isoformat() == '2017-11-14T22:58:00+00:00' @@ -531,17 +519,17 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) strategy = DefaultStrategy(default_conf) data = strategy.tickerdata_to_dataframe( - history.load_data( + load_data( datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'], fill_up_missing=False ) ) - min_date, max_date = history.get_timeframe(data) + min_date, max_date = get_timeframe(data) caplog.clear() - assert history.validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC', - min_date, max_date, timeframe_to_minutes('1m')) + assert validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC', + min_date, max_date, timeframe_to_minutes('1m')) assert len(caplog.record_tuples) == 1 assert log_has( "UNITTEST/BTC has missing frames: expected 14396, got 13680, that's 716 missing values", @@ -554,7 +542,7 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No timerange = TimeRange('index', 'index', 200, 250) data = strategy.tickerdata_to_dataframe( - history.load_data( + load_data( datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], @@ -562,10 +550,10 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No ) ) - min_date, max_date = history.get_timeframe(data) + min_date, max_date = get_timeframe(data) caplog.clear() - assert not history.validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC', - min_date, max_date, timeframe_to_minutes('5m')) + assert not validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC', + min_date, max_date, timeframe_to_minutes('5m')) assert len(caplog.record_tuples) == 0 @@ -668,12 +656,8 @@ def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog): file1 = testdatadir / 'XRP_ETH-1m.json' file5 = testdatadir / 'XRP_ETH-5m.json' # Compare downloaded dataset with converted dataset - dfbak_1m = history.load_pair_history(datadir=testdatadir, - timeframe="1m", - pair=pair) - dfbak_5m = history.load_pair_history(datadir=testdatadir, - timeframe="5m", - pair=pair) + dfbak_1m = load_pair_history(datadir=testdatadir, timeframe="1m", pair=pair) + dfbak_5m = load_pair_history(datadir=testdatadir, timeframe="5m", pair=pair) _backup_file(file1, copy_file=True) _backup_file(file5) @@ -685,12 +669,8 @@ def test_convert_trades_to_ohlcv(mocker, default_conf, testdatadir, caplog): assert log_has("Deleting existing data for pair XRP/ETH, interval 1m.", caplog) # Load new data - df_1m = history.load_pair_history(datadir=testdatadir, - timeframe="1m", - pair=pair) - df_5m = history.load_pair_history(datadir=testdatadir, - timeframe="5m", - pair=pair) + df_1m = load_pair_history(datadir=testdatadir, timeframe="1m", pair=pair) + df_5m = load_pair_history(datadir=testdatadir, timeframe="5m", pair=pair) assert df_1m.equals(dfbak_1m) assert df_5m.equals(dfbak_5m) From e1c0c6af7dcf94fe7dc6a982059b6f1a6ccbd2de Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 10:51:49 +0100 Subject: [PATCH 068/324] fix random-seed to failing one --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6a111944..40a6fd075 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,7 +69,7 @@ jobs: COVERALLS_SERVICE_NAME: travis-ci TRAVIS: "true" run: | - pytest --random-order --cov=freqtrade --cov-config=.coveragerc + pytest --random-order --cov=freqtrade --cov-config=.coveragerc --random-order-seed=834267 # Allow failure for coveralls # Fake travis environment to get coveralls working correctly export TRAVIS_PULL_REQUEST="https://github.com/${GITHUB_REPOSITORY}/pull/$(cat $GITHUB_EVENT_PATH | jq -r .number)" From 2e2f084f662e02eebc6cb083deb1dbeac145e840 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 11:07:59 +0100 Subject: [PATCH 069/324] Try to clear caplog ... --- .github/workflows/ci.yml | 2 +- tests/test_utils.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 40a6fd075..f6a111944 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,7 +69,7 @@ jobs: COVERALLS_SERVICE_NAME: travis-ci TRAVIS: "true" run: | - pytest --random-order --cov=freqtrade --cov-config=.coveragerc --random-order-seed=834267 + pytest --random-order --cov=freqtrade --cov-config=.coveragerc # Allow failure for coveralls # Fake travis environment to get coveralls working correctly export TRAVIS_PULL_REQUEST="https://github.com/${GITHUB_REPOSITORY}/pull/$(cat $GITHUB_EVENT_PATH | jq -r .number)" diff --git a/tests/test_utils.py b/tests/test_utils.py index feba1ed59..40ca9ac02 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -444,6 +444,9 @@ def test_create_datadir_failed(caplog): def test_create_datadir(caplog, mocker): + # Ensure that caplog is empty before starting ... + # Should prevent random failures. + caplog.clear() cud = mocker.patch("freqtrade.utils.create_userdata_dir", MagicMock()) csf = mocker.patch("freqtrade.utils.copy_sample_files", MagicMock()) args = [ From 8513a5e2d68721e896e4d3337f8be162847d8c86 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 11:35:39 +0100 Subject: [PATCH 070/324] Fix failures in test_main --- .github/workflows/ci.yml | 2 +- tests/test_main.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6a111944..628f169c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,7 +139,7 @@ jobs: - name: Tests run: | - pytest --random-order --cov=freqtrade --cov-config=.coveragerc + pytest --random-order --cov=freqtrade --cov-config=.coveragerc --random-order-seed=781055 - name: Backtesting run: | diff --git a/tests/test_main.py b/tests/test_main.py index 4e97c375d..03e6a7ce9 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -79,6 +79,7 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None: mocker.patch('freqtrade.worker.Worker._worker', MagicMock(side_effect=KeyboardInterrupt)) patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) + mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) args = ['trade', '-c', 'config.json.example'] @@ -98,6 +99,7 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None: MagicMock(side_effect=OperationalException('Oh snap!')) ) patched_configuration_load_config_file(mocker, default_conf) + mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) @@ -120,6 +122,7 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None: OperationalException("Oh snap!")]) mocker.patch('freqtrade.worker.Worker._worker', worker_mock) patched_configuration_load_config_file(mocker, default_conf) + mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) reconfigure_mock = mocker.patch('freqtrade.worker.Worker._reconfigure', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) @@ -143,6 +146,7 @@ def test_reconfigure(mocker, default_conf) -> None: 'freqtrade.worker.Worker._worker', MagicMock(side_effect=OperationalException('Oh snap!')) ) + mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) From 60f89c8c019f6806b28b044e642f8cdab8c60cc9 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 17 Dec 2019 13:43:42 +0300 Subject: [PATCH 071/324] Split refresh from load_data/load_pair_history --- freqtrade/data/history.py | 86 ++++++++++++++++++++---------- freqtrade/edge/__init__.py | 12 ++++- tests/data/test_history.py | 28 +++++++--- tests/edge/test_edge.py | 7 ++- tests/optimize/test_backtesting.py | 4 +- 5 files changed, 97 insertions(+), 40 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 2c01f9f48..62195a005 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -128,39 +128,26 @@ def load_pair_history(pair: str, timeframe: str, datadir: Path, timerange: Optional[TimeRange] = None, - refresh_pairs: bool = False, - exchange: Optional[Exchange] = None, fill_up_missing: bool = True, drop_incomplete: bool = True, startup_candles: int = 0, ) -> DataFrame: """ - Loads cached ticker history for the given pair. + Load cached ticker history for the given pair. + :param pair: Pair to load data for :param timeframe: Ticker timeframe (e.g. "5m") :param datadir: Path to the data storage location. :param timerange: Limit data to be loaded to this timerange - :param refresh_pairs: Refresh pairs from exchange. - (Note: Requires exchange to be passed as well.) - :param exchange: Exchange object (needed when using "refresh_pairs") :param fill_up_missing: Fill missing values with "No action"-candles :param drop_incomplete: Drop last candle assuming it may be incomplete. :param startup_candles: Additional candles to load at the start of the period :return: DataFrame with ohlcv data, or empty DataFrame """ - timerange_startup = deepcopy(timerange) if startup_candles > 0 and timerange_startup: timerange_startup.subtract_start(timeframe_to_seconds(timeframe) * startup_candles) - # The user forced the refresh of pairs - if refresh_pairs: - _download_pair_history(datadir=datadir, - exchange=exchange, - pair=pair, - timeframe=timeframe, - timerange=timerange) - pairdata = load_tickerdata_file(datadir, pair, timeframe, timerange=timerange_startup) if pairdata: @@ -177,33 +164,53 @@ def load_pair_history(pair: str, return DataFrame() +def refresh_pair_history(pair: str, + timeframe: str, + datadir: Path, + exchange: Exchange, + timerange: Optional[TimeRange] = None, + startup_candles: int = 0, + ) -> None: + """ + Refresh cached ticker history for the given pair. + + :param pair: Pair to load data for + :param timeframe: Ticker timeframe (e.g. "5m") + :param datadir: Path to the data storage location. + :param timerange: Limit data to be loaded to this timerange + :param exchange: Exchange object + :param startup_candles: Additional candles to load at the start of the period + """ + timerange_startup = deepcopy(timerange) + if startup_candles > 0 and timerange_startup: + timerange_startup.subtract_start(timeframe_to_seconds(timeframe) * startup_candles) + + _download_pair_history(datadir=datadir, + exchange=exchange, + pair=pair, + timeframe=timeframe, + timerange=timerange) + + def load_data(datadir: Path, timeframe: str, pairs: List[str], - refresh_pairs: bool = False, - exchange: Optional[Exchange] = None, timerange: Optional[TimeRange] = None, fill_up_missing: bool = True, startup_candles: int = 0, fail_without_data: bool = False ) -> Dict[str, DataFrame]: """ - Loads ticker history data for a list of pairs + Load ticker history data for a list of pairs. + :param datadir: Path to the data storage location. :param timeframe: Ticker Timeframe (e.g. "5m") :param pairs: List of pairs to load - :param refresh_pairs: Refresh pairs from exchange. - (Note: Requires exchange to be passed as well.) - :param exchange: Exchange object (needed when using "refresh_pairs") :param timerange: Limit data to be loaded to this timerange :param fill_up_missing: Fill missing values with "No action"-candles :param startup_candles: Additional candles to load at the start of the period :param fail_without_data: Raise OperationalException if no data is found. :return: dict(:) - TODO: refresh_pairs is still used by edge to keep the data uptodate. - This should be replaced in the future. Instead, writing the current candles to disk - from dataprovider should be implemented, as this would avoid loading ohlcv data twice. - exchange and refresh_pairs are then not needed here nor in load_pair_history. """ result: Dict[str, DataFrame] = {} if startup_candles > 0 and timerange: @@ -212,8 +219,6 @@ def load_data(datadir: Path, for pair in pairs: hist = load_pair_history(pair=pair, timeframe=timeframe, datadir=datadir, timerange=timerange, - refresh_pairs=refresh_pairs, - exchange=exchange, fill_up_missing=fill_up_missing, startup_candles=startup_candles) if not hist.empty: @@ -224,6 +229,33 @@ def load_data(datadir: Path, return result +def refresh_data(datadir: Path, + timeframe: str, + pairs: List[str], + exchange: Exchange, + timerange: Optional[TimeRange] = None, + startup_candles: int = 0, + ) -> None: + """ + Refresh ticker history data for a list of pairs. + + :param datadir: Path to the data storage location. + :param timeframe: Ticker Timeframe (e.g. "5m") + :param pairs: List of pairs to load + :param exchange: Exchange object + :param timerange: Limit data to be loaded to this timerange + :param startup_candles: Additional candles to load at the start of the period + """ + if startup_candles > 0 and timerange: + logger.info(f'Using indicator startup period: {startup_candles} ...') + + for pair in pairs: + refresh_pair_history(pair=pair, timeframe=timeframe, + datadir=datadir, timerange=timerange, + exchange=exchange, + startup_candles=startup_candles) + + def pair_data_filename(datadir: Path, pair: str, timeframe: str) -> Path: pair_s = pair.replace("/", "_") filename = datadir.joinpath(f'{pair_s}-{timeframe}.json') diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 4bc3023a4..b52b2ffdc 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -94,12 +94,20 @@ class Edge: logger.info('Using stake_currency: %s ...', self.config['stake_currency']) logger.info('Using local backtesting data (using whitelist in given config) ...') + if self._refresh_pairs: + history.refresh_data( + datadir=Path(self.config['datadir']), + pairs=pairs, + exchange=self.exchange, + timeframe=self.strategy.ticker_interval, + timerange=self._timerange, + startup_candles=self.strategy.startup_candle_count, + ) + data = history.load_data( datadir=Path(self.config['datadir']), pairs=pairs, timeframe=self.strategy.ticker_interval, - refresh_pairs=self._refresh_pairs, - exchange=self.exchange, timerange=self._timerange, startup_candles=self.strategy.startup_candle_count, ) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 708702ab2..1b7c2c4a4 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -21,6 +21,7 @@ from freqtrade.data.history import (_download_pair_history, pair_trades_filename, refresh_backtest_ohlcv_data, refresh_backtest_trades_data, + refresh_data, refresh_pair_history, trim_dataframe, trim_tickerlist, validate_backtest_data) from freqtrade.exchange import timeframe_to_minutes @@ -129,16 +130,17 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, ) # download a new pair if refresh_pairs is set - load_pair_history(datadir=testdatadir, timeframe='1m', - refresh_pairs=True, exchange=exchange, pair='MEME/BTC') + refresh_pair_history(datadir=testdatadir, timeframe='1m', pair='MEME/BTC', + exchange=exchange) + load_pair_history(datadir=testdatadir, timeframe='1m', pair='MEME/BTC') assert file.is_file() assert log_has_re( 'Download history data for pair: "MEME/BTC", timeframe: 1m ' 'and store in .*', caplog ) with pytest.raises(OperationalException, match=r'Exchange needs to be initialized when.*'): - load_pair_history(datadir=testdatadir, timeframe='1m', - refresh_pairs=True, exchange=None, pair='MEME/BTC') + refresh_pair_history(datadir=testdatadir, timeframe='1m', pair='MEME/BTC', + exchange=None) _clean_test_file(file) @@ -372,12 +374,24 @@ def test_load_partial_missing(testdatadir, caplog) -> None: def test_init(default_conf, mocker) -> None: - exchange = get_patched_exchange(mocker, default_conf) assert {} == load_data( datadir='', - exchange=exchange, pairs=[], - refresh_pairs=True, + timeframe=default_conf['ticker_interval'] + ) + + +def test_init_with_refresh(default_conf, mocker) -> None: + exchange = get_patched_exchange(mocker, default_conf) + refresh_data( + datadir='', + pairs=[], + timeframe=default_conf['ticker_interval'], + exchange=exchange + ) + assert {} == load_data( + datadir='', + pairs=[], timeframe=default_conf['ticker_interval'] ) diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index bdb986d6d..3a866c0a8 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -255,8 +255,8 @@ def test_edge_heartbeat_calculate(mocker, edge_conf): assert edge.calculate() is False -def mocked_load_data(datadir, pairs=[], timeframe='0m', refresh_pairs=False, - timerange=None, exchange=None, *args, **kwargs): +def mocked_load_data(datadir, pairs=[], timeframe='0m', + timerange=None, *args, **kwargs): hz = 0.1 base = 0.001 @@ -290,6 +290,7 @@ def mocked_load_data(datadir, pairs=[], timeframe='0m', refresh_pairs=False, def test_edge_process_downloaded_data(mocker, edge_conf): freqtrade = get_patched_freqtradebot(mocker, edge_conf) mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + mocker.patch('freqtrade.data.history.refresh_data', MagicMock()) mocker.patch('freqtrade.data.history.load_data', mocked_load_data) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) @@ -301,6 +302,7 @@ def test_edge_process_downloaded_data(mocker, edge_conf): def test_edge_process_no_data(mocker, edge_conf, caplog): freqtrade = get_patched_freqtradebot(mocker, edge_conf) mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + mocker.patch('freqtrade.data.history.refresh_data', MagicMock()) mocker.patch('freqtrade.data.history.load_data', MagicMock(return_value={})) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) @@ -313,6 +315,7 @@ def test_edge_process_no_data(mocker, edge_conf, caplog): def test_edge_process_no_trades(mocker, edge_conf, caplog): freqtrade = get_patched_freqtradebot(mocker, edge_conf) mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) + mocker.patch('freqtrade.data.history.refresh_data', MagicMock()) mocker.patch('freqtrade.data.history.load_data', mocked_load_data) # Return empty mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', MagicMock(return_value=[])) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 5086891a6..38a95be7a 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -116,8 +116,8 @@ def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None: assert len(results) == num_results -def mocked_load_data(datadir, pairs=[], timeframe='0m', refresh_pairs=False, - timerange=None, exchange=None, live=False, *args, **kwargs): +def mocked_load_data(datadir, pairs=[], timeframe='0m', + timerange=None, *args, **kwargs): tickerdata = history.load_tickerdata_file(datadir, 'UNITTEST/BTC', '1m', timerange=timerange) pairdata = {'UNITTEST/BTC': parse_ticker_dataframe(tickerdata, '1m', pair="UNITTEST/BTC", fill_missing=True)} From bbb05b52862dbd04d169a6df010683207545b92b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 11:51:50 +0100 Subject: [PATCH 072/324] Remove fixed random order --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 628f169c2..f6a111944 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -139,7 +139,7 @@ jobs: - name: Tests run: | - pytest --random-order --cov=freqtrade --cov-config=.coveragerc --random-order-seed=781055 + pytest --random-order --cov=freqtrade --cov-config=.coveragerc - name: Backtesting run: | From b2796f99b6c9be25af7a86cf67a2740641879cdd Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 17 Dec 2019 14:06:21 +0300 Subject: [PATCH 073/324] Remove redundant refresh_pair_history --- freqtrade/data/history.py | 35 +++-------------------------------- tests/data/test_history.py | 10 +++++----- 2 files changed, 8 insertions(+), 37 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 62195a005..3b16e41a9 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -164,34 +164,6 @@ def load_pair_history(pair: str, return DataFrame() -def refresh_pair_history(pair: str, - timeframe: str, - datadir: Path, - exchange: Exchange, - timerange: Optional[TimeRange] = None, - startup_candles: int = 0, - ) -> None: - """ - Refresh cached ticker history for the given pair. - - :param pair: Pair to load data for - :param timeframe: Ticker timeframe (e.g. "5m") - :param datadir: Path to the data storage location. - :param timerange: Limit data to be loaded to this timerange - :param exchange: Exchange object - :param startup_candles: Additional candles to load at the start of the period - """ - timerange_startup = deepcopy(timerange) - if startup_candles > 0 and timerange_startup: - timerange_startup.subtract_start(timeframe_to_seconds(timeframe) * startup_candles) - - _download_pair_history(datadir=datadir, - exchange=exchange, - pair=pair, - timeframe=timeframe, - timerange=timerange) - - def load_data(datadir: Path, timeframe: str, pairs: List[str], @@ -250,10 +222,9 @@ def refresh_data(datadir: Path, logger.info(f'Using indicator startup period: {startup_candles} ...') for pair in pairs: - refresh_pair_history(pair=pair, timeframe=timeframe, - datadir=datadir, timerange=timerange, - exchange=exchange, - startup_candles=startup_candles) + _download_pair_history(pair=pair, timeframe=timeframe, + datadir=datadir, timerange=timerange, + exchange=exchange) def pair_data_filename(datadir: Path, pair: str, timeframe: str) -> Path: diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 1b7c2c4a4..c9b198b39 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -21,7 +21,7 @@ from freqtrade.data.history import (_download_pair_history, pair_trades_filename, refresh_backtest_ohlcv_data, refresh_backtest_trades_data, - refresh_data, refresh_pair_history, + refresh_data, trim_dataframe, trim_tickerlist, validate_backtest_data) from freqtrade.exchange import timeframe_to_minutes @@ -130,8 +130,8 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, ) # download a new pair if refresh_pairs is set - refresh_pair_history(datadir=testdatadir, timeframe='1m', pair='MEME/BTC', - exchange=exchange) + refresh_data(datadir=testdatadir, timeframe='1m', pairs=['MEME/BTC'], + exchange=exchange) load_pair_history(datadir=testdatadir, timeframe='1m', pair='MEME/BTC') assert file.is_file() assert log_has_re( @@ -139,8 +139,8 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, 'and store in .*', caplog ) with pytest.raises(OperationalException, match=r'Exchange needs to be initialized when.*'): - refresh_pair_history(datadir=testdatadir, timeframe='1m', pair='MEME/BTC', - exchange=None) + refresh_data(datadir=testdatadir, timeframe='1m', pairs=['MEME/BTC'], + exchange=None) _clean_test_file(file) From 153738961773fbe307aa557fa2ea257c9fe6ad10 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Tue, 17 Dec 2019 18:23:31 +0300 Subject: [PATCH 074/324] Remove startup_candles argument in refresh_data --- freqtrade/data/history.py | 5 ----- freqtrade/edge/__init__.py | 1 - 2 files changed, 6 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 3b16e41a9..64f530b53 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -206,7 +206,6 @@ def refresh_data(datadir: Path, pairs: List[str], exchange: Exchange, timerange: Optional[TimeRange] = None, - startup_candles: int = 0, ) -> None: """ Refresh ticker history data for a list of pairs. @@ -216,11 +215,7 @@ def refresh_data(datadir: Path, :param pairs: List of pairs to load :param exchange: Exchange object :param timerange: Limit data to be loaded to this timerange - :param startup_candles: Additional candles to load at the start of the period """ - if startup_candles > 0 and timerange: - logger.info(f'Using indicator startup period: {startup_candles} ...') - for pair in pairs: _download_pair_history(pair=pair, timeframe=timeframe, datadir=datadir, timerange=timerange, diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index b52b2ffdc..85029905b 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -101,7 +101,6 @@ class Edge: exchange=self.exchange, timeframe=self.strategy.ticker_interval, timerange=self._timerange, - startup_candles=self.strategy.startup_candle_count, ) data = history.load_data( From c5e6a34f25081498908ef0f44fc7366cf59374f8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 17 Dec 2019 19:30:04 +0100 Subject: [PATCH 075/324] Remove unnecessary parenteses --- freqtrade/persistence.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 10896baaa..993b68bc7 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -346,7 +346,7 @@ class Trade(_DECL_BASE): Calculate the open_rate including open_fee. :return: Price in of the open trade incl. Fees """ - buy_trade = (Decimal(self.amount) * Decimal(self.open_rate)) + buy_trade = Decimal(self.amount) * Decimal(self.open_rate) fees = buy_trade * Decimal(self.fee_open) return float(buy_trade + fees) @@ -370,7 +370,7 @@ class Trade(_DECL_BASE): if rate is None and not self.close_rate: return 0.0 - sell_trade = (Decimal(self.amount) * Decimal(rate or self.close_rate)) + sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) fees = sell_trade * Decimal(fee or self.fee_close) return float(sell_trade - fees) From cf4c3642ce1ea09c758b16a8774cabcea0674ec0 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Wed, 18 Dec 2019 01:06:03 +0300 Subject: [PATCH 076/324] Minor improvements in data.history --- freqtrade/data/history.py | 20 ++++++++------------ freqtrade/edge/__init__.py | 2 +- freqtrade/optimize/backtesting.py | 4 ++-- freqtrade/optimize/hyperopt.py | 6 +++--- tests/data/test_converter.py | 4 ++-- tests/data/test_history.py | 15 +++++---------- tests/optimize/test_backtest_detail.py | 4 ++-- tests/optimize/test_backtesting.py | 20 ++++++++++---------- tests/optimize/test_hyperopt.py | 22 +++++++++++----------- 9 files changed, 44 insertions(+), 53 deletions(-) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 64f530b53..4c5c0521f 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -68,7 +68,7 @@ def trim_dataframe(df: DataFrame, timerange: TimeRange, df_date_col: str = 'date def load_tickerdata_file(datadir: Path, pair: str, timeframe: str, - timerange: Optional[TimeRange] = None) -> Optional[list]: + timerange: Optional[TimeRange] = None) -> List[Dict]: """ Load a pair from file, either .json.gz or .json :return: tickerlist or None if unsuccessful @@ -276,7 +276,7 @@ def _load_cached_data_for_updating(datadir: Path, pair: str, timeframe: str, def _download_pair_history(datadir: Path, - exchange: Optional[Exchange], + exchange: Exchange, pair: str, timeframe: str = '5m', timerange: Optional[TimeRange] = None) -> bool: @@ -293,11 +293,6 @@ def _download_pair_history(datadir: Path, :param timerange: range of time to download :return: bool with success state """ - if not exchange: - raise OperationalException( - "Exchange needs to be initialized when downloading pair history data" - ) - try: logger.info( f'Download history data for pair: "{pair}", timeframe: {timeframe} ' @@ -447,18 +442,19 @@ def convert_trades_to_ohlcv(pairs: List[str], timeframes: List[str], store_tickerdata_file(datadir, pair, timeframe, data=ohlcv) -def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: +def get_timerange(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: """ - Get the maximum timeframe for the given backtest data + Get the maximum common timerange for the given backtest data. + :param data: dictionary with preprocessed backtesting data :return: tuple containing min_date, max_date """ - timeframe = [ + timeranges = [ (arrow.get(frame['date'].min()), arrow.get(frame['date'].max())) for frame in data.values() ] - return min(timeframe, key=operator.itemgetter(0))[0], \ - max(timeframe, key=operator.itemgetter(1))[1] + return (min(timeranges, key=operator.itemgetter(0))[0], + max(timeranges, key=operator.itemgetter(1))[1]) def validate_backtest_data(data: DataFrame, pair: str, min_date: datetime, diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 85029905b..e56071a98 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -120,7 +120,7 @@ class Edge: preprocessed = self.strategy.tickerdata_to_dataframe(data) # Print timeframe - min_date, max_date = history.get_timeframe(preprocessed) + min_date, max_date = history.get_timerange(preprocessed) logger.info( 'Measuring data from %s up to %s (%s days) ...', min_date.isoformat(), diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index fc60bd310..726257cdd 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -117,7 +117,7 @@ class Backtesting: fail_without_data=True, ) - min_date, max_date = history.get_timeframe(data) + min_date, max_date = history.get_timerange(data) logger.info( 'Loading data from %s up to %s (%s days)..', @@ -481,7 +481,7 @@ class Backtesting: # Trim startup period from analyzed dataframe for pair, df in preprocessed.items(): preprocessed[pair] = history.trim_dataframe(df, timerange) - min_date, max_date = history.get_timeframe(preprocessed) + min_date, max_date = history.get_timerange(preprocessed) logger.info( 'Backtesting with data from %s up to %s (%s days)..', diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index cdde2b722..521a4d790 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -23,7 +23,7 @@ from joblib import (Parallel, cpu_count, delayed, dump, load, from pandas import DataFrame from freqtrade import OperationalException -from freqtrade.data.history import get_timeframe, trim_dataframe +from freqtrade.data.history import get_timerange, trim_dataframe from freqtrade.misc import plural, round_dict from freqtrade.optimize.backtesting import Backtesting # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules @@ -369,7 +369,7 @@ class Hyperopt: processed = load(self.tickerdata_pickle) - min_date, max_date = get_timeframe(processed) + min_date, max_date = get_timerange(processed) backtesting_results = self.backtesting.backtest( { @@ -490,7 +490,7 @@ class Hyperopt: # Trim startup period from analyzed dataframe for pair, df in preprocessed.items(): preprocessed[pair] = trim_dataframe(df, timerange) - min_date, max_date = get_timeframe(data) + min_date, max_date = get_timerange(data) logger.info( 'Hyperopting with data from %s up to %s (%s days)..', diff --git a/tests/data/test_converter.py b/tests/data/test_converter.py index 8184167b3..414551c95 100644 --- a/tests/data/test_converter.py +++ b/tests/data/test_converter.py @@ -2,7 +2,7 @@ import logging from freqtrade.data.converter import parse_ticker_dataframe, ohlcv_fill_up_missing_data -from freqtrade.data.history import load_pair_history, validate_backtest_data, get_timeframe +from freqtrade.data.history import load_pair_history, validate_backtest_data, get_timerange from tests.conftest import log_has @@ -36,7 +36,7 @@ def test_ohlcv_fill_up_missing_data(testdatadir, caplog): f"{len(data)} - after: {len(data2)}", caplog) # Test fillup actually fixes invalid backtest data - min_date, max_date = get_timeframe({'UNITTEST/BTC': data}) + min_date, max_date = get_timerange({'UNITTEST/BTC': data}) assert validate_backtest_data(data, 'UNITTEST/BTC', min_date, max_date, 1) assert not validate_backtest_data(data2, 'UNITTEST/BTC', min_date, max_date, 1) diff --git a/tests/data/test_history.py b/tests/data/test_history.py index c9b198b39..7b3143db9 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -7,15 +7,13 @@ from shutil import copyfile from unittest.mock import MagicMock, PropertyMock import arrow -import pytest from pandas import DataFrame -from freqtrade import OperationalException from freqtrade.configuration import TimeRange from freqtrade.data.history import (_download_pair_history, _download_trades_history, _load_cached_data_for_updating, - convert_trades_to_ohlcv, get_timeframe, + convert_trades_to_ohlcv, get_timerange, load_data, load_pair_history, load_tickerdata_file, pair_data_filename, pair_trades_filename, @@ -138,9 +136,6 @@ def test_load_data_with_new_pair_1min(ticker_history_list, mocker, caplog, 'Download history data for pair: "MEME/BTC", timeframe: 1m ' 'and store in .*', caplog ) - with pytest.raises(OperationalException, match=r'Exchange needs to be initialized when.*'): - refresh_data(datadir=testdatadir, timeframe='1m', pairs=['MEME/BTC'], - exchange=None) _clean_test_file(file) @@ -512,7 +507,7 @@ def test_file_dump_json_tofile(testdatadir) -> None: _clean_test_file(file) -def test_get_timeframe(default_conf, mocker, testdatadir) -> None: +def test_get_timerange(default_conf, mocker, testdatadir) -> None: patch_exchange(mocker) strategy = DefaultStrategy(default_conf) @@ -523,7 +518,7 @@ def test_get_timeframe(default_conf, mocker, testdatadir) -> None: pairs=['UNITTEST/BTC'] ) ) - min_date, max_date = get_timeframe(data) + min_date, max_date = get_timerange(data) assert min_date.isoformat() == '2017-11-04T23:02:00+00:00' assert max_date.isoformat() == '2017-11-14T22:58:00+00:00' @@ -540,7 +535,7 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) fill_up_missing=False ) ) - min_date, max_date = get_timeframe(data) + min_date, max_date = get_timerange(data) caplog.clear() assert validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC', min_date, max_date, timeframe_to_minutes('1m')) @@ -564,7 +559,7 @@ def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> No ) ) - min_date, max_date = get_timeframe(data) + min_date, max_date = get_timerange(data) caplog.clear() assert not validate_backtest_data(data['UNITTEST/BTC'], 'UNITTEST/BTC', min_date, max_date, timeframe_to_minutes('5m')) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index b36c1b9c5..47cb9f353 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock import pytest -from freqtrade.data.history import get_timeframe +from freqtrade.data.history import get_timerange from freqtrade.optimize.backtesting import Backtesting from freqtrade.strategy.interface import SellType from tests.conftest import patch_exchange @@ -380,7 +380,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: pair = "UNITTEST/BTC" # Dummy data as we mock the analyze functions data_processed = {pair: frame.copy()} - min_date, max_date = get_timeframe({pair: frame}) + min_date, max_date = get_timerange({pair: frame}) results = backtesting.backtest( { 'stake_amount': default_conf['stake_amount'], diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 38a95be7a..0ea35e41f 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -16,7 +16,7 @@ from freqtrade.data import history from freqtrade.data.btanalysis import evaluate_result_multi from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.dataprovider import DataProvider -from freqtrade.data.history import get_timeframe +from freqtrade.data.history import get_timerange from freqtrade.optimize import setup_configuration, start_backtesting from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode @@ -100,7 +100,7 @@ def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None: data = load_data_test(contour, testdatadir) processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = get_timeframe(processed) + min_date, max_date = get_timerange(processed) assert isinstance(processed, dict) results = backtesting.backtest( { @@ -138,7 +138,7 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC', record= patch_exchange(mocker) backtesting = Backtesting(conf) processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = get_timeframe(processed) + min_date, max_date = get_timerange(processed) return { 'stake_amount': conf['stake_amount'], 'processed': processed, @@ -458,11 +458,11 @@ def test_generate_text_table_strategyn(default_conf, mocker): def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: - def get_timeframe(input1): + def get_timerange(input1): return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) mocker.patch('freqtrade.data.history.load_data', mocked_load_data) - mocker.patch('freqtrade.data.history.get_timeframe', get_timeframe) + mocker.patch('freqtrade.data.history.get_timerange', get_timerange) mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock()) patch_exchange(mocker) mocker.patch.multiple( @@ -491,11 +491,11 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> None: - def get_timeframe(input1): + def get_timerange(input1): return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) mocker.patch('freqtrade.data.history.load_pair_history', MagicMock(return_value=pd.DataFrame())) - mocker.patch('freqtrade.data.history.get_timeframe', get_timeframe) + mocker.patch('freqtrade.data.history.get_timerange', get_timerange) mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock()) patch_exchange(mocker) mocker.patch.multiple( @@ -525,7 +525,7 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None: data = history.load_data(datadir=testdatadir, timeframe='5m', pairs=['UNITTEST/BTC'], timerange=timerange) data_processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = get_timeframe(data_processed) + min_date, max_date = get_timerange(data_processed) results = backtesting.backtest( { 'stake_amount': default_conf['stake_amount'], @@ -581,7 +581,7 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker, testdatadir) - data = history.load_data(datadir=testdatadir, timeframe='1m', pairs=['UNITTEST/BTC'], timerange=timerange) processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = get_timeframe(processed) + min_date, max_date = get_timerange(processed) results = backtesting.backtest( { 'stake_amount': default_conf['stake_amount'], @@ -701,7 +701,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) backtesting.strategy.advise_sell = _trend_alternate_hold # Override data_processed = backtesting.strategy.tickerdata_to_dataframe(data) - min_date, max_date = get_timeframe(data_processed) + min_date, max_date = get_timerange(data_processed) backtest_conf = { 'stake_amount': default_conf['stake_amount'], 'processed': data_processed, diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index c3afa4911..29b8b5b16 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -251,7 +251,7 @@ def test_start_no_data(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.data.history.load_pair_history', MagicMock(return_value=pd.DataFrame)) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -427,7 +427,7 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None: mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -602,7 +602,7 @@ def test_generate_optimizer(mocker, default_conf) -> None: MagicMock(return_value=backtest_result) ) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(Arrow(2017, 12, 10), Arrow(2017, 12, 13))) ) patch_exchange(mocker) @@ -726,7 +726,7 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None: mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -769,7 +769,7 @@ def test_print_json_spaces_default(mocker, default_conf, caplog, capsys) -> None mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -811,7 +811,7 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) -> mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -851,7 +851,7 @@ def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys) mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -899,7 +899,7 @@ def test_simplified_interface_all_failed(mocker, default_conf, caplog, capsys) - mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -930,7 +930,7 @@ def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None: mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -977,7 +977,7 @@ def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) @@ -1030,7 +1030,7 @@ def test_simplified_interface_failed(mocker, default_conf, caplog, capsys, metho mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data', MagicMock(return_value=(MagicMock(), None))) mocker.patch( - 'freqtrade.optimize.hyperopt.get_timeframe', + 'freqtrade.optimize.hyperopt.get_timerange', MagicMock(return_value=(datetime(2017, 12, 10), datetime(2017, 12, 13))) ) From 21622ac3138713555784e1ee58cfe61e127fcb40 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Dec 2019 16:34:30 +0100 Subject: [PATCH 077/324] Rename get_ticker to fetch_ticker --- freqtrade/exchange/exchange.py | 2 +- freqtrade/freqtradebot.py | 6 +- tests/exchange/test_exchange.py | 14 +-- tests/rpc/test_rpc.py | 36 +++---- tests/rpc/test_rpc_apiserver.py | 12 +-- tests/rpc/test_rpc_telegram.py | 28 +++--- tests/test_freqtradebot.py | 170 ++++++++++++++++---------------- tests/test_integration.py | 4 +- 8 files changed, 136 insertions(+), 136 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e4e7aacce..384ec1fb7 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -524,7 +524,7 @@ class Exchange: raise OperationalException(e) from e @retrier - def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict: + def fetch_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict: if refresh or pair not in self._cached_ticker.keys(): try: if pair not in self._api.markets or not self._api.markets[pair].get('active'): diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 8ae027fa2..ea9b892d7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -192,7 +192,7 @@ class FreqtradeBot: else: if not tick: logger.info('Using Last Ask / Last Price') - ticker = self.exchange.get_ticker(pair) + ticker = self.exchange.fetch_ticker(pair) else: ticker = tick if ticker['ask'] < ticker['last']: @@ -570,7 +570,7 @@ class FreqtradeBot: """ Get sell rate - either using get-ticker bid or first bid based on orderbook The orderbook portion is only used for rpc messaging, which would otherwise fail - for BitMex (has no bid/ask in get_ticker) + for BitMex (has no bid/ask in fetch_ticker) or remain static in any other case since it's not updating. :return: Bid rate """ @@ -582,7 +582,7 @@ class FreqtradeBot: rate = order_book['bids'][0][0] else: - rate = self.exchange.get_ticker(pair, refresh)['bid'] + rate = self.exchange.fetch_ticker(pair, refresh)['bid'] return rate def handle_trade(self, trade: Trade) -> bool: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 629f99aa2..7c273da9f 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -977,7 +977,7 @@ def test_get_tickers(default_conf, mocker, exchange_name): @pytest.mark.parametrize("exchange_name", EXCHANGES) -def test_get_ticker(default_conf, mocker, exchange_name): +def test_fetch_ticker(default_conf, mocker, exchange_name): api_mock = MagicMock() tick = { 'symbol': 'ETH/BTC', @@ -989,7 +989,7 @@ def test_get_ticker(default_conf, mocker, exchange_name): api_mock.markets = {'ETH/BTC': {'active': True}} exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) # retrieve original ticker - ticker = exchange.get_ticker(pair='ETH/BTC') + ticker = exchange.fetch_ticker(pair='ETH/BTC') assert ticker['bid'] == 0.00001098 assert ticker['ask'] == 0.00001099 @@ -1006,7 +1006,7 @@ def test_get_ticker(default_conf, mocker, exchange_name): # if not caching the result we should get the same ticker # if not fetching a new result we should get the cached ticker - ticker = exchange.get_ticker(pair='ETH/BTC') + ticker = exchange.fetch_ticker(pair='ETH/BTC') assert api_mock.fetch_ticker.call_count == 1 assert ticker['bid'] == 0.5 @@ -1018,19 +1018,19 @@ def test_get_ticker(default_conf, mocker, exchange_name): # Test caching api_mock.fetch_ticker = MagicMock() - exchange.get_ticker(pair='ETH/BTC', refresh=False) + exchange.fetch_ticker(pair='ETH/BTC', refresh=False) assert api_mock.fetch_ticker.call_count == 0 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, - "get_ticker", "fetch_ticker", + "fetch_ticker", "fetch_ticker", pair='ETH/BTC', refresh=True) api_mock.fetch_ticker = MagicMock(return_value={}) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.get_ticker(pair='ETH/BTC', refresh=True) + exchange.fetch_ticker(pair='ETH/BTC', refresh=True) with pytest.raises(DependencyException, match=r'Pair XRP/ETH not available'): - exchange.get_ticker(pair='XRP/ETH', refresh=True) + exchange.fetch_ticker(pair='XRP/ETH', refresh=True) @pytest.mark.parametrize("exchange_name", EXCHANGES) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 0a8c1cabd..3b897572c 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -29,7 +29,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) @@ -65,7 +65,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'open_order': '(limit buy rem=0.00000000)' } == results[0] - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available"))) # invalidate ticker cache rpc._freqtrade.exchange._cached_ticker = {} @@ -104,7 +104,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) @@ -134,7 +134,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: assert 'ETH/BTC' == result[0][1] assert '-0.59% (-0.09)' == result[0][3] - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available"))) # invalidate ticker cache rpc._freqtrade.exchange._cached_ticker = {} @@ -149,7 +149,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, markets=PropertyMock(return_value=markets) ) @@ -201,7 +201,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) @@ -225,7 +225,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, # Update the ticker with a market going up mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_up + fetch_ticker=ticker_sell_up ) trade.update(limit_sell_order) trade.close_date = datetime.utcnow() @@ -239,7 +239,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, # Update the ticker with a market going up mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_up + fetch_ticker=ticker_sell_up ) trade.update(limit_sell_order) trade.close_date = datetime.utcnow() @@ -260,7 +260,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, assert prec_satoshi(stats['best_rate'], 6.2) # Test non-available pair - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available"))) # invalidate ticker cache rpc._freqtrade.exchange._cached_ticker = {} @@ -287,7 +287,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) @@ -306,7 +306,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, # Update the ticker with a market going up mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_up, + fetch_ticker=ticker_sell_up, get_fee=fee ) trade.update(limit_sell_order) @@ -439,7 +439,7 @@ def test_rpc_start(mocker, default_conf) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock() + fetch_ticker=MagicMock() ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) @@ -460,7 +460,7 @@ def test_rpc_stop(mocker, default_conf) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock() + fetch_ticker=MagicMock() ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) @@ -482,7 +482,7 @@ def test_rpc_stopbuy(mocker, default_conf) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock() + fetch_ticker=MagicMock() ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) @@ -502,7 +502,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: cancel_order_mock = MagicMock() mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, cancel_order=cancel_order_mock, get_order=MagicMock( return_value={ @@ -604,7 +604,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) @@ -637,7 +637,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) @@ -661,7 +661,7 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order) -> None mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, buy=buy_mm ) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index f1e3421c5..36bb81e41 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -256,7 +256,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets): mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, markets=PropertyMock(return_value=markets) ) @@ -292,7 +292,7 @@ def test_api_daily(botclient, mocker, ticker, fee, markets): mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, markets=PropertyMock(return_value=markets) ) @@ -308,7 +308,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, markets=PropertyMock(return_value=markets) ) @@ -323,7 +323,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, markets=PropertyMock(return_value=markets) ) @@ -413,7 +413,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, markets=PropertyMock(return_value=markets) ) @@ -541,7 +541,7 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets): mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, markets=PropertyMock(return_value=markets) ) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index b02f11394..ed2aad37e 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -150,7 +150,7 @@ def test_status(default_conf, update, mocker, fee, ticker,) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) msg_mock = MagicMock() @@ -204,7 +204,7 @@ def test_status(default_conf, update, mocker, fee, ticker,) -> None: def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) msg_mock = MagicMock() @@ -254,7 +254,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': 'mocked_order_id'}), get_fee=fee, ) @@ -307,7 +307,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) msg_mock = MagicMock() @@ -373,7 +373,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker + fetch_ticker=ticker ) msg_mock = MagicMock() mocker.patch.multiple( @@ -411,7 +411,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) msg_mock = MagicMock() @@ -443,7 +443,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, msg_mock.reset_mock() # Update the ticker with a market going up - mocker.patch('freqtrade.exchange.Exchange.get_ticker', ticker_sell_up) + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up) trade.update(limit_sell_order) trade.close_date = datetime.utcnow() @@ -700,7 +700,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee, patch_whitelist(mocker, default_conf) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) @@ -715,7 +715,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee, assert trade # Increase the price and sell it - mocker.patch('freqtrade.exchange.Exchange.get_ticker', ticker_sell_up) + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', ticker_sell_up) # /forcesell 1 context = MagicMock() @@ -755,7 +755,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) @@ -769,7 +769,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, # Decrease the price and sell it mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_down + fetch_ticker=ticker_sell_down ) trade = Trade.query.first() @@ -812,7 +812,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None patch_whitelist(mocker, default_conf) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) default_conf['max_open_trades'] = 4 @@ -963,7 +963,7 @@ def test_performance_handle(default_conf, update, ticker, fee, ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) @@ -998,7 +998,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': 'mocked_order_id'}), get_fee=fee, ) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 341bc021f..13f1277b9 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -157,7 +157,7 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, patch_wallet(mocker, free=default_conf['stake_amount']) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee ) @@ -232,7 +232,7 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf buy_price = limit_buy_order['price'] mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': buy_price * 0.79, 'ask': buy_price * 0.79, 'last': buy_price * 0.79 @@ -272,7 +272,7 @@ def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, buy_price = limit_buy_order['price'] mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': buy_price * 0.85, 'ask': buy_price * 0.85, 'last': buy_price * 0.85 @@ -304,7 +304,7 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, default_conf['max_open_trades'] = 2 mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) @@ -467,7 +467,7 @@ def test_create_trades(default_conf, ticker, limit_buy_order, fee, mocker) -> No patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) @@ -501,7 +501,7 @@ def test_create_trades_no_stake_amount(default_conf, ticker, limit_buy_order, patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) @@ -519,7 +519,7 @@ def test_create_trades_minimal_amount(default_conf, ticker, limit_buy_order, buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=buy_mock, get_fee=fee, ) @@ -539,7 +539,7 @@ def test_create_trades_too_small_stake_amount(default_conf, ticker, limit_buy_or buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=buy_mock, get_fee=fee, ) @@ -558,7 +558,7 @@ def test_create_trades_limit_reached(default_conf, ticker, limit_buy_order, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_balance=MagicMock(return_value=default_conf['stake_amount']), get_fee=fee, @@ -579,7 +579,7 @@ def test_create_trades_no_pairs_let(default_conf, ticker, limit_buy_order, fee, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) @@ -600,7 +600,7 @@ def test_create_trades_no_pairs_in_whitelist(default_conf, ticker, limit_buy_ord patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) @@ -639,7 +639,7 @@ def test_create_trades_multiple_trades(default_conf, ticker, default_conf['max_open_trades'] = max_open mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': "12355555"}), get_fee=fee, ) @@ -658,7 +658,7 @@ def test_create_trades_preopen(default_conf, ticker, fee, mocker) -> None: default_conf['max_open_trades'] = 4 mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': "12355555"}), get_fee=fee, ) @@ -684,7 +684,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_order=MagicMock(return_value=limit_buy_order), get_fee=fee, @@ -718,7 +718,7 @@ def test_process_exchange_failures(default_conf, ticker, mocker) -> None: patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(side_effect=TemporaryError) ) sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None) @@ -735,7 +735,7 @@ def test_process_operational_exception(default_conf, ticker, mocker) -> None: patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(side_effect=OperationalException) ) worker = Worker(args=None, config=default_conf) @@ -753,7 +753,7 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order, fee, mock patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_order=MagicMock(return_value=limit_buy_order), get_fee=fee, @@ -780,7 +780,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_order=MagicMock(return_value=limit_buy_order), get_fee=fee, @@ -830,7 +830,7 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None: refresh_mock = MagicMock() mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(side_effect=TemporaryError), refresh_latest_ohlcv=refresh_mock, ) @@ -853,7 +853,7 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None: def test_balance_fully_ask_side(mocker, default_conf) -> None: default_conf['bid_strategy']['ask_last_balance'] = 0.0 freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={'ask': 20, 'last': 10})) assert freqtrade.get_target_bid('ETH/BTC') == 20 @@ -862,7 +862,7 @@ def test_balance_fully_ask_side(mocker, default_conf) -> None: def test_balance_fully_last_side(mocker, default_conf) -> None: default_conf['bid_strategy']['ask_last_balance'] = 1.0 freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={'ask': 20, 'last': 10})) assert freqtrade.get_target_bid('ETH/BTC') == 10 @@ -871,7 +871,7 @@ def test_balance_fully_last_side(mocker, default_conf) -> None: def test_balance_bigger_last_ask(mocker, default_conf) -> None: default_conf['bid_strategy']['ask_last_balance'] = 1.0 freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={'ask': 5, 'last': 10})) assert freqtrade.get_target_bid('ETH/BTC') == 5 @@ -891,7 +891,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: buy_mm = MagicMock(return_value={'id': limit_buy_order['id']}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00001172, 'ask': 0.00001173, 'last': 0.00001172 @@ -1000,7 +1000,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00001172, 'ask': 0.00001173, 'last': 0.00001172 @@ -1100,7 +1100,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00001172, 'ask': 0.00001173, 'last': 0.00001172 @@ -1134,7 +1134,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, sell_mock = MagicMock(return_value={'id': limit_sell_order['id']}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00001172, 'ask': 0.00001173, 'last': 0.00001172 @@ -1177,7 +1177,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, patch_RPCManager(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00001172, 'ask': 0.00001173, 'last': 0.00001172 @@ -1231,7 +1231,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, assert freqtrade.handle_stoploss_on_exchange(trade) is False # price jumped 2x - mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(return_value={ + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 'bid': 0.00002344, 'ask': 0.00002346, 'last': 0.00002344 @@ -1271,7 +1271,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00001172, 'ask': 0.00001173, 'last': 0.00001172 @@ -1342,7 +1342,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, edge_conf['dry_run_wallet'] = 999.9 mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00001172, 'ask': 0.00001173, 'last': 0.00001172 @@ -1406,7 +1406,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_order_mock) # price goes down 5% - mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(return_value={ + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 'bid': 0.00001172 * 0.95, 'ask': 0.00001173 * 0.95, 'last': 0.00001172 * 0.95 @@ -1422,7 +1422,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, cancel_order_mock.assert_not_called() # price jumped 2x - mocker.patch('freqtrade.exchange.Exchange.get_ticker', MagicMock(return_value={ + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 'bid': 0.00002344, 'ask': 0.00002346, 'last': 0.00002344 @@ -1662,7 +1662,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mock patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00001172, 'ask': 0.00001173, 'last': 0.00001172 @@ -1701,7 +1701,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, fee, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) @@ -1754,7 +1754,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, patch_RPCManager(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) @@ -1786,7 +1786,7 @@ def test_handle_trade_use_sell_signal( patch_RPCManager(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) @@ -1814,7 +1814,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) @@ -1842,7 +1842,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, op patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_order=MagicMock(return_value=limit_buy_order_old), cancel_order=cancel_order_mock, get_fee=fee @@ -1869,7 +1869,7 @@ def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, o limit_buy_order_old.update({"status": "canceled"}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_order=MagicMock(return_value=limit_buy_order_old), cancel_order=cancel_order_mock, get_fee=fee @@ -1896,7 +1896,7 @@ def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_ord mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), - get_ticker=ticker, + fetch_ticker=ticker, get_order=MagicMock(side_effect=DependencyException), cancel_order=cancel_order_mock, get_fee=fee @@ -1921,7 +1921,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_order=MagicMock(return_value=limit_sell_order_old), cancel_order=cancel_order_mock ) @@ -1949,7 +1949,7 @@ def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_order=MagicMock(return_value=limit_sell_order_old), cancel_order=cancel_order_mock ) @@ -1976,7 +1976,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_order=MagicMock(return_value=limit_buy_order_old_partial), cancel_order=cancel_order_mock ) @@ -2003,7 +2003,7 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_order=MagicMock(return_value=limit_buy_order_old_partial), cancel_order=cancel_order_mock, get_trades_for_order=MagicMock(return_value=trades_for_order), @@ -2040,7 +2040,7 @@ def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_order=MagicMock(return_value=limit_buy_order_old_partial), cancel_order=cancel_order_mock, get_trades_for_order=MagicMock(return_value=trades_for_order), @@ -2084,7 +2084,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_order=MagicMock(side_effect=requests.exceptions.RequestException('Oh snap')), cancel_order=cancel_order_mock ) @@ -2174,7 +2174,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) patch_whitelist(mocker, default_conf) @@ -2190,7 +2190,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N # Increase the price and sell it mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_up + fetch_ticker=ticker_sell_up ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) @@ -2222,7 +2222,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) patch_whitelist(mocker, default_conf) @@ -2238,7 +2238,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) # Decrease the price and sell it mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_down + fetch_ticker=ticker_sell_down ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], @@ -2272,7 +2272,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) patch_whitelist(mocker, default_conf) @@ -2288,7 +2288,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe # Decrease the price and sell it mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_down + fetch_ticker=ticker_sell_down ) default_conf['dry_run'] = True @@ -2330,7 +2330,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, sell=sellmock ) @@ -2367,7 +2367,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke cancel_order = MagicMock(return_value=True) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, symbol_amount_prec=lambda s, x, y: y, symbol_price_prec=lambda s, x, y: y, @@ -2391,7 +2391,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke # Increase the price and sell it mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_up + fetch_ticker=ticker_sell_up ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], @@ -2410,7 +2410,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, symbol_amount_prec=lambda s, x, y: y, symbol_price_prec=lambda s, x, y: y, @@ -2474,7 +2474,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) patch_whitelist(mocker, default_conf) @@ -2490,7 +2490,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, # Increase the price and sell it mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_up + fetch_ticker=ticker_sell_up ) freqtrade.config['order_types']['sell'] = 'market' @@ -2528,7 +2528,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00002172, 'ask': 0.00002173, 'last': 0.00002172 @@ -2559,7 +2559,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00002172, 'ask': 0.00002173, 'last': 0.00002172 @@ -2588,7 +2588,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00000172, 'ask': 0.00000173, 'last': 0.00000172 @@ -2617,7 +2617,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocke patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.0000172, 'ask': 0.0000173, 'last': 0.0000172 @@ -2648,7 +2648,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -2663,7 +2663,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo # Decrease the price and sell it mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker_sell_down + fetch_ticker=ticker_sell_down ) freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], @@ -2684,7 +2684,7 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, mocker) -> patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.0000172, 'ask': 0.0000173, 'last': 0.0000172 @@ -2717,7 +2717,7 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00001099, 'ask': 0.00001099, 'last': 0.00001099 @@ -2736,7 +2736,7 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker) assert freqtrade.handle_trade(trade) is False # Raise ticker above buy price - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 'bid': 0.00001099 * 1.5, 'ask': 0.00001099 * 1.5, @@ -2747,7 +2747,7 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker) assert freqtrade.handle_trade(trade) is False # Price fell - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 'bid': 0.00001099 * 1.1, 'ask': 0.00001099 * 1.1, @@ -2771,7 +2771,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': buy_price - 0.000001, 'ask': buy_price - 0.000001, 'last': buy_price - 0.000001 @@ -2795,7 +2795,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, assert freqtrade.handle_trade(trade) is False # Raise ticker above buy price - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 'bid': buy_price + 0.000003, 'ask': buy_price + 0.000003, @@ -2807,7 +2807,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, assert log_has(f"ETH/BTC - Adjusting stoploss...", caplog) assert trade.stop_loss == 0.0000138501 - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 'bid': buy_price + 0.000002, 'ask': buy_price + 0.000002, @@ -2828,7 +2828,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': buy_price - 0.000001, 'ask': buy_price - 0.000001, 'last': buy_price - 0.000001 @@ -2852,7 +2852,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, assert freqtrade.handle_trade(trade) is False # Raise ticker above buy price - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 'bid': buy_price + 0.000003, 'ask': buy_price + 0.000003, @@ -2865,7 +2865,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, assert log_has(f"ETH/BTC - Adjusting stoploss...", caplog) assert trade.stop_loss == 0.0000138501 - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 'bid': buy_price + 0.000002, 'ask': buy_price + 0.000002, @@ -2889,7 +2889,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': buy_price, 'ask': buy_price, 'last': buy_price @@ -2916,7 +2916,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee, assert trade.stop_loss == 0.0000098910 # Raise ticker above buy price - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 'bid': buy_price + 0.0000004, 'ask': buy_price + 0.0000004, @@ -2930,7 +2930,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee, assert trade.stop_loss == 0.0000098910 # price rises above the offset (rises 12% when the offset is 5.5%) - mocker.patch('freqtrade.exchange.Exchange.get_ticker', + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ 'bid': buy_price + 0.0000014, 'ask': buy_price + 0.0000014, @@ -2950,7 +2950,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00000172, 'ask': 0.00000173, 'last': 0.00000172 @@ -3287,7 +3287,7 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, mocker.patch('freqtrade.exchange.Exchange.get_order_book', order_book_l2) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) @@ -3324,7 +3324,7 @@ def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_o mocker.patch('freqtrade.exchange.Exchange.get_order_book', order_book_l2) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) @@ -3347,7 +3347,7 @@ def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_order_book=order_book_l2, - get_ticker=ticker_mock, + fetch_ticker=ticker_mock, ) default_conf['exchange']['name'] = 'binance' @@ -3371,7 +3371,7 @@ def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_order_book=order_book_l2, - get_ticker=ticker_mock, + fetch_ticker=ticker_mock, ) default_conf['exchange']['name'] = 'binance' @@ -3421,7 +3421,7 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=MagicMock(return_value={ + fetch_ticker=MagicMock(return_value={ 'bid': 0.00001172, 'ask': 0.00001173, 'last': 0.00001172 @@ -3451,7 +3451,7 @@ def test_get_sell_rate(default_conf, mocker, ticker, order_book_l2) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_order_book=order_book_l2, - get_ticker=ticker, + fetch_ticker=ticker, ) pair = "ETH/BTC" @@ -3530,7 +3530,7 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) diff --git a/tests/test_integration.py b/tests/test_integration.py index 728e96d55..a80600cdc 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -55,7 +55,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, mocker.patch('freqtrade.exchange.Binance.stoploss_limit', stoploss_limit) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, symbol_amount_prec=lambda s, x, y: y, symbol_price_prec=lambda s, x, y: y, @@ -126,7 +126,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc )) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_ticker=ticker, + fetch_ticker=ticker, get_fee=fee, symbol_amount_prec=lambda s, x, y: y, symbol_price_prec=lambda s, x, y: y, From dc07037edfc2bebed2d3815c986a999d281c5b0e Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Dec 2019 16:38:57 +0100 Subject: [PATCH 078/324] Add documentation for price finding --- docs/configuration.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index 73534b6f1..bfa29529d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -393,6 +393,41 @@ The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT" ``` +## Prices used for orders + +Prices for regular orders can be controlled via the parameter structures `bid_strategy` for Buying, and `ask_strategy` for selling. +Prices are always retrieved right before an order is placed, either by querying the `fetch_ticker()` endpoint of the exchange (usually `/ticker`), or by using the orderbook. + +### Buy price with Orderbook enabled + +When buying with the orderbook enabled (`bid_strategy.use_order_book=True`) - Freqtrade will fetch the `bid_strategy.order_book_top` entries in the orderbook, and will then use the entry specified as `bid_strategy.order_book_top` on the `bids` side of the orderbook. 1 specifies the topmost entry in the Orderbook - while 2 would use the 2nd entry in the Orderbook. + +### Buy price without Orderbook + +When not using orderbook (`bid_strategy.use_order_book=False`), then Freqtrade will use the best `ask` price based on a call to `fetch_ticker()` if it's below the `last` traded price. +Otherwise, it'll use the following formula to calculate the rate: + +``` python +ticker['ask'] + ask_last_balance * (ticker['last'] - ticker['ask']) +``` + +This means - it uses the difference between last and ask (which must be negative, since ask was checked to be higher than last) and multiplies this with `bid_strategy.ask_last_balance` - lowering the price by `balance * (difference between last and ask). + +### Sell price with Orderbook enabled + +When selling with the Orderbook enabled (`ask_strategy.use_order_book=True`) - Freqtrade will fetch the `ask_strategy.order_book_max` entries in the orderbook. Freqtrade will then validate each of the orderbook steps between `ask_strategy.order_book_min` and `ask_strategy.order_book_max` on the `ask` side for a profitable sell-possibility and will place a sell order at the first profitable spot. + +The idea here is to place the sell-order early, to be ahead in the queue. +A fixed slot (mirroring `bid_strategy.order_book_top`) can be defined by setting `ask_strategy.order_book_min` and `ask_strategy.order_book_max` to the same number. + +!!! Warning "Orderbook and stoploss_on_exchange" + Using `ask_strategy.order_book_max` higher than 1 may increase the risk, since an eventual [stoploss on exchange](#understand-order_types) will be need to be cancelled as soon as the order is placed. + + +### Sell price without orderbook + +When not using orderbook (`ask_strategy.use_order_book=False`), then the best `bid` will be used as sell rate based on a call to `fetch_ticker()`. + ## Pairlists Pairlists define the list of pairs that the bot should trade. From d73ba71ec63d08d9ddc98c983f10aa7509cd9b0b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Dec 2019 16:41:54 +0100 Subject: [PATCH 079/324] Improve formatting of orderbook doc --- docs/configuration.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index bfa29529d..e4d29825e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -398,11 +398,13 @@ The valid values are: Prices for regular orders can be controlled via the parameter structures `bid_strategy` for Buying, and `ask_strategy` for selling. Prices are always retrieved right before an order is placed, either by querying the `fetch_ticker()` endpoint of the exchange (usually `/ticker`), or by using the orderbook. -### Buy price with Orderbook enabled +### Buy price + +#### Buy price with Orderbook enabled When buying with the orderbook enabled (`bid_strategy.use_order_book=True`) - Freqtrade will fetch the `bid_strategy.order_book_top` entries in the orderbook, and will then use the entry specified as `bid_strategy.order_book_top` on the `bids` side of the orderbook. 1 specifies the topmost entry in the Orderbook - while 2 would use the 2nd entry in the Orderbook. -### Buy price without Orderbook +#### Buy price without Orderbook When not using orderbook (`bid_strategy.use_order_book=False`), then Freqtrade will use the best `ask` price based on a call to `fetch_ticker()` if it's below the `last` traded price. Otherwise, it'll use the following formula to calculate the rate: @@ -413,7 +415,9 @@ ticker['ask'] + ask_last_balance * (ticker['last'] - ticker['ask']) This means - it uses the difference between last and ask (which must be negative, since ask was checked to be higher than last) and multiplies this with `bid_strategy.ask_last_balance` - lowering the price by `balance * (difference between last and ask). -### Sell price with Orderbook enabled +### Sell price + +#### Sell price with Orderbook enabled When selling with the Orderbook enabled (`ask_strategy.use_order_book=True`) - Freqtrade will fetch the `ask_strategy.order_book_max` entries in the orderbook. Freqtrade will then validate each of the orderbook steps between `ask_strategy.order_book_min` and `ask_strategy.order_book_max` on the `ask` side for a profitable sell-possibility and will place a sell order at the first profitable spot. @@ -424,7 +428,7 @@ A fixed slot (mirroring `bid_strategy.order_book_top`) can be defined by setting Using `ask_strategy.order_book_max` higher than 1 may increase the risk, since an eventual [stoploss on exchange](#understand-order_types) will be need to be cancelled as soon as the order is placed. -### Sell price without orderbook +#### Sell price without orderbook When not using orderbook (`ask_strategy.use_order_book=False`), then the best `bid` will be used as sell rate based on a call to `fetch_ticker()`. From 1c19856d26c8ea9ed48c293519f8f63a12cbb568 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Dec 2019 16:49:56 +0100 Subject: [PATCH 080/324] add section about depth_of_market --- docs/configuration.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index e4d29825e..5c748b0cb 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -400,6 +400,12 @@ Prices are always retrieved right before an order is placed, either by querying ### Buy price +#### Check depth of market + +When enabling `bid_strategy.check_depth_of_market=True`, then buy signals will be filtered based on the orderbook size. + +#TODO: Finish this section + #### Buy price with Orderbook enabled When buying with the orderbook enabled (`bid_strategy.use_order_book=True`) - Freqtrade will fetch the `bid_strategy.order_book_top` entries in the orderbook, and will then use the entry specified as `bid_strategy.order_book_top` on the `bids` side of the orderbook. 1 specifies the topmost entry in the Orderbook - while 2 would use the 2nd entry in the Orderbook. @@ -419,7 +425,7 @@ This means - it uses the difference between last and ask (which must be negative #### Sell price with Orderbook enabled -When selling with the Orderbook enabled (`ask_strategy.use_order_book=True`) - Freqtrade will fetch the `ask_strategy.order_book_max` entries in the orderbook. Freqtrade will then validate each of the orderbook steps between `ask_strategy.order_book_min` and `ask_strategy.order_book_max` on the `ask` side for a profitable sell-possibility and will place a sell order at the first profitable spot. +When selling with the Orderbook enabled (`ask_strategy.use_order_book=True`) - Freqtrade will fetch the `ask_strategy.order_book_max` entries in the orderbook. Freqtrade will then validate each of the orderbook steps between `ask_strategy.order_book_min` and `ask_strategy.order_book_max` on the `ask` side for a profitable sell-possibility based on the strategy configuration and will place a sell order at the first profitable spot. The idea here is to place the sell-order early, to be ahead in the queue. A fixed slot (mirroring `bid_strategy.order_book_top`) can be defined by setting `ask_strategy.order_book_min` and `ask_strategy.order_book_max` to the same number. From 11e787c8846027f14e6bc0872743dd5529cf57f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Dec 2019 19:27:54 +0100 Subject: [PATCH 081/324] Finish depth_of_market documentation piece --- docs/configuration.md | 45 ++++++++++++++++++------------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 5c748b0cb..ba592d436 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -57,12 +57,12 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `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.
***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.
***Datatype:*** *Integer* -| `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#understand-ask_last_balance). -| `bid_strategy.use_order_book` | Enable buying using the rates in Order Book Bids.
***Datatype:*** *Boolean* -| `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids. *Defaults to `1`.*
***Datatype:*** *Positive Integer* -| `bid_strategy. check_depth_of_market.enabled` | Do not buy if the difference of buy orders and sell orders is met in Order Book.
*Defaults to `false`.*
***Datatype:*** *Boolean* -| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher. *Defaults to `0`.*
***Datatype:*** *Float (as ratio)* -| `ask_strategy.use_order_book` | Enable selling of open trades using Order Book Asks.
***Datatype:*** *Boolean* +| `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#buy-price-without-orderbook). +| `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled).
***Datatype:*** *Boolean* +| `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids to buy. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in [Order Book Bids](#buy-price-with-orderbook-enabled).
*Defaults to `1`.*
***Datatype:*** *Positive Integer* +| `bid_strategy. check_depth_of_market.enabled` | Do not buy if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market).
*Defaults to `false`.*
***Datatype:*** *Boolean* +| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market)
*Defaults to `0`.*
***Datatype:*** *Float (as ratio)* +| `ask_strategy.use_order_book` | Enable selling of open trades using [Order Book Asks](#sell-price-with-orderbook-enabled).
***Datatype:*** *Boolean* | `ask_strategy.order_book_min` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
*Defaults to `1`.*
***Datatype:*** *Positive Integer* | `ask_strategy.order_book_max` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
*Defaults to `1`.*
***Datatype:*** *Positive Integer* | `ask_strategy.use_sell_signal` | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy).
*Defaults to `true`.*
***Datatype:*** *Boolean* @@ -88,9 +88,9 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `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* | `webhook.enabled` | Enable usage of Webhook notifications
***Datatype:*** *Boolean* | `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
***Datatype:*** *String* -| `webhook.webhookbuy` | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
***Datatype:*** *String* -| `webhook.webhooksell` | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
***Datatype:*** *String* -| `webhook.webhookstatus` | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
***Datatype:*** *String* +| `webhook.webhookbuy` | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
***Datatype:*** *String* +| `webhook.webhooksell` | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
***Datatype:*** *String* +| `webhook.webhookstatus` | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
***Datatype:*** *String* | `api_server.enabled` | Enable usage of API Server. See the [API Server documentation](rest-api.md) for more details.
***Datatype:*** *Boolean* | `api_server.listen_ip_address` | Bind IP address. See the [API Server documentation](rest-api.md) for more details.
***Datatype:*** *IPv4* | `api_server.listen_port` | Bind Port. See the [API Server documentation](rest-api.md) for more details.
***Datatype:*** *Integer between 1024 and 65535* @@ -207,13 +207,6 @@ before asking the strategy if we should buy or a sell an asset. After each wait every opened trade wether or not we should sell, and for all the remaining pairs (either the dynamic list of pairs or the static list of pairs) if we should buy. -### Understand ask_last_balance - -The `ask_last_balance` configuration parameter sets the bidding price. Value `0.0` will use `ask` price, `1.0` will -use the `last` price and values between those interpolate between ask and last -price. Using `ask` price will guarantee quick success in bid, but bot will also -end up paying more then would probably have been necessary. - ### 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. @@ -398,13 +391,15 @@ The valid values are: Prices for regular orders can be controlled via the parameter structures `bid_strategy` for Buying, and `ask_strategy` for selling. Prices are always retrieved right before an order is placed, either by querying the `fetch_ticker()` endpoint of the exchange (usually `/ticker`), or by using the orderbook. -### Buy price +### Buy price #### Check depth of market -When enabling `bid_strategy.check_depth_of_market=True`, then buy signals will be filtered based on the orderbook size. +When enabling `bid_strategy.check_depth_of_market.enabled=True`, then buy signals will be filtered based on the orderbook size for each size (sum of all amounts). +Orderbook bid size is then divided by Orderbook ask size - and the resulting delta is compared to `bid_strategy.check_depth_of_market.bids_to_ask_delta`, and a buy is only executed if the orderbook delta is bigger or equal to the configured delta. -#TODO: Finish this section +!!! Note: + A calculated delta below 1 means that sell order size is greater, while value greater than 1 means buy order size is higher #### Buy price with Orderbook enabled @@ -413,15 +408,13 @@ When buying with the orderbook enabled (`bid_strategy.use_order_book=True`) - Fr #### Buy price without Orderbook When not using orderbook (`bid_strategy.use_order_book=False`), then Freqtrade will use the best `ask` price based on a call to `fetch_ticker()` if it's below the `last` traded price. -Otherwise, it'll use the following formula to calculate the rate: +Otherwise, it'll calculate a rate between `ask` and `last` price. -``` python -ticker['ask'] + ask_last_balance * (ticker['last'] - ticker['ask']) -``` +The `bid_strategy.ask_last_balance` configuration parameter controls this. A value of `0.0` will use `ask` price, `1.0` will use the `last` price and values between those interpolate between ask and last +price. +Using `ask` price will guarantee quick success in bid, but bot will also end up paying more than what would have been necessary. -This means - it uses the difference between last and ask (which must be negative, since ask was checked to be higher than last) and multiplies this with `bid_strategy.ask_last_balance` - lowering the price by `balance * (difference between last and ask). - -### Sell price +### Sell price #### Sell price with Orderbook enabled From 1af962899d3f443b639d6953f406f145f1cab344 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Dec 2019 19:43:37 +0100 Subject: [PATCH 082/324] Fix note-box syntax error --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index ba592d436..8ae824277 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -398,7 +398,7 @@ Prices are always retrieved right before an order is placed, either by querying When enabling `bid_strategy.check_depth_of_market.enabled=True`, then buy signals will be filtered based on the orderbook size for each size (sum of all amounts). Orderbook bid size is then divided by Orderbook ask size - and the resulting delta is compared to `bid_strategy.check_depth_of_market.bids_to_ask_delta`, and a buy is only executed if the orderbook delta is bigger or equal to the configured delta. -!!! Note: +!!! Note A calculated delta below 1 means that sell order size is greater, while value greater than 1 means buy order size is higher #### Buy price with Orderbook enabled From e72c6a0d94d810b2976c4a6729291fe51602db9d Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Dec 2019 20:02:15 +0100 Subject: [PATCH 083/324] use only first part of the currency to get wallet-amount (!!) --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4a48bba04..5656268c8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -903,7 +903,7 @@ class FreqtradeBot: :return: amount to sell :raise: DependencyException: if available balance is not within 2% of the available amount. """ - wallet_amount = self.wallets.get_free(pair) + wallet_amount = self.wallets.get_free(pair.split('/')[0]) logger.debug(f"{pair} - Wallet: {wallet_amount} - Trade-amount: {amount}") if wallet_amount > amount: return amount From 6507a26cc160fdfe99a58fbd92a8e0a197f4edef Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Dec 2019 20:16:53 +0100 Subject: [PATCH 084/324] Fix some tests after merge --- freqtrade/freqtradebot.py | 2 +- tests/test_freqtradebot.py | 18 ++++++++++++++---- tests/test_integration.py | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5656268c8..a51ab0dbf 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -898,7 +898,7 @@ class FreqtradeBot: Should be trade.amount - but will fall back to the available amount if necessary. This should cover cases where get_real_amount() was not able to update the amount for whatever reason. - :param pair: pair - used for logging + :param pair: Pair we're trying to sell :param amount: amount we expect to be available :return: amount to sell :raise: DependencyException: if available balance is not within 2% of the available amount. diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 300d0ad32..cd5c92199 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1682,6 +1682,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mock time.sleep(0.01) # Race condition fix trade.update(limit_buy_order) assert trade.is_open is True + freqtrade.wallets.update() patch_get_signal(freqtrade, value=(False, True)) assert freqtrade.handle_trade(trade) is True @@ -2549,6 +2550,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, trade = Trade.query.first() trade.update(limit_buy_order) + freqtrade.wallets.update() patch_get_signal(freqtrade, value=(False, True)) assert freqtrade.handle_trade(trade) is True assert trade.sell_reason == SellType.SELL_SIGNAL.value @@ -2579,6 +2581,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, trade = Trade.query.first() trade.update(limit_buy_order) + freqtrade.wallets.update() patch_get_signal(freqtrade, value=(False, True)) assert freqtrade.handle_trade(trade) is True assert trade.sell_reason == SellType.SELL_SIGNAL.value @@ -2639,6 +2642,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocke trade = Trade.query.first() trade.update(limit_buy_order) + freqtrade.wallets.update() patch_get_signal(freqtrade, value=(False, True)) assert freqtrade.handle_trade(trade) is True assert trade.sell_reason == SellType.SELL_SIGNAL.value @@ -2676,7 +2680,7 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order, assert trade.amount != amnt -def test__safe_sell_amount(default_conf, caplog, mocker): +def test__safe_sell_amount(default_conf, fee, caplog, mocker): patch_RPCManager(mocker) patch_exchange(mocker) amount = 95.33 @@ -2687,7 +2691,9 @@ def test__safe_sell_amount(default_conf, caplog, mocker): amount=amount, exchange='binance', open_rate=0.245441, - open_order_id="123456" + open_order_id="123456", + fee_open=fee.return_value, + fee_close=fee.return_value, ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -2696,7 +2702,7 @@ def test__safe_sell_amount(default_conf, caplog, mocker): assert log_has_re(r'.*Falling back to wallet-amount.', caplog) -def test__safe_sell_amount_error(default_conf, caplog, mocker): +def test__safe_sell_amount_error(default_conf, fee, caplog, mocker): patch_RPCManager(mocker) patch_exchange(mocker) amount = 95.33 @@ -2707,7 +2713,9 @@ def test__safe_sell_amount_error(default_conf, caplog, mocker): amount=amount, exchange='binance', open_rate=0.245441, - open_order_id="123456" + open_order_id="123456", + fee_open=fee.return_value, + fee_close=fee.return_value, ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -2775,6 +2783,7 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, mocker) -> trade = Trade.query.first() trade.update(limit_buy_order) + freqtrade.wallets.update() patch_get_signal(freqtrade, value=(True, True)) assert freqtrade.handle_trade(trade) is False @@ -3512,6 +3521,7 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order time.sleep(0.01) # Race condition fix trade.update(limit_buy_order) + freqtrade.wallets.update() assert trade.is_open is True patch_get_signal(freqtrade, value=(False, True)) diff --git a/tests/test_integration.py b/tests/test_integration.py index 728e96d55..2daf13db6 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -71,7 +71,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, ) mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) wallets_mock = mocker.patch("freqtrade.wallets.Wallets.update", MagicMock()) - mocker.patch("freqtrade.wallets.Wallets.get_free", MagicMock(return_value=1)) + mocker.patch("freqtrade.wallets.Wallets.get_free", MagicMock(return_value=1000)) freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade.strategy.order_types['stoploss_on_exchange'] = True From 0c6b5e01fb4a617d86e1aa9105e1aba960fdd254 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Dec 2019 20:28:36 +0100 Subject: [PATCH 085/324] Try with github-token --- .github/workflows/ci.yml | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6a111944..0e86abc9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,19 +64,16 @@ jobs: pip install -e . - name: Tests - env: - COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} - COVERALLS_SERVICE_NAME: travis-ci - TRAVIS: "true" run: | pytest --random-order --cov=freqtrade --cov-config=.coveragerc + + - name: Coveralls + if: startsWith(matrix.os, 'ubuntu') + env: + COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | # Allow failure for coveralls - # Fake travis environment to get coveralls working correctly - export TRAVIS_PULL_REQUEST="https://github.com/${GITHUB_REPOSITORY}/pull/$(cat $GITHUB_EVENT_PATH | jq -r .number)" - export TRAVIS_BRANCH=${GITHUB_REF#"ref/heads"} - export CI_BRANCH=${GITHUB_REF#"ref/heads"} - echo "${TRAVIS_BRANCH}" - coveralls || true + coveralls -v || true - name: Backtesting run: | From 342f3f450b1a201b155994f61ce182cc31721db3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 18 Dec 2019 20:38:21 +0100 Subject: [PATCH 086/324] try with coveralls token in yml ... --- .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 0e86abc9b..53b2e5440 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -70,7 +70,8 @@ jobs: - name: Coveralls if: startsWith(matrix.os, 'ubuntu') env: - COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # 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 From fc5764f9df7c4341c9dd14560822ac4eec7fb6cc Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 19 Dec 2019 19:55:21 +0100 Subject: [PATCH 087/324] Edge small cleanup --- freqtrade/optimize/edge_cli.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index a667ebb92..3848623db 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -42,11 +42,9 @@ class EdgeCli: # Set refresh_pairs to false for edge-cli (it must be true for edge) self.edge._refresh_pairs = False - self.timerange = TimeRange.parse_timerange(None if self.config.get( + self.edge._timerange = TimeRange.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) - self.edge._timerange = self.timerange - def _generate_edge_table(self, results: dict) -> str: floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', '.d') From 95bd9e8e0b5785031e70bedd0295cc763b54d541 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 22 Dec 2019 00:17:51 +0300 Subject: [PATCH 088/324] No underscores in cli options --- docs/bot-usage.md | 40 ++++++++++++++---------- freqtrade/configuration/cli_options.py | 8 ++--- freqtrade/configuration/configuration.py | 4 +-- 3 files changed, 29 insertions(+), 23 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 25818aea6..86a990946 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -192,8 +192,8 @@ 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] - [--timerange TIMERANGE] [--max_open_trades INT] - [--stake_amount STAKE_AMOUNT] [--fee FLOAT] + [--timerange TIMERANGE] [--max-open-trades INT] + [--stake-amount STAKE_AMOUNT] [--fee FLOAT] [--eps] [--dmmp] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--export EXPORT] [--export-filename PATH] @@ -205,10 +205,12 @@ optional arguments: `1d`). --timerange TIMERANGE Specify what timerange of data to use. - --max_open_trades INT - Specify max_open_trades to use. - --stake_amount STAKE_AMOUNT - Specify stake_amount. + --max-open-trades INT + Override the value of the `max_open_trades` + configuration setting. + --stake-amount STAKE_AMOUNT + Override the value of the `stake_amount` configuration + setting. --fee FLOAT Specify fee ratio. Will be applied twice (on trade entry and exit). --eps, --enable-position-stacking @@ -270,8 +272,8 @@ to find optimal parameter values for your stategy. usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] [-i TICKER_INTERVAL] [--timerange TIMERANGE] - [--max_open_trades INT] - [--stake_amount STAKE_AMOUNT] [--fee FLOAT] + [--max-open-trades INT] + [--stake-amount STAKE_AMOUNT] [--fee FLOAT] [--hyperopt NAME] [--hyperopt-path PATH] [--eps] [-e INT] [--spaces {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] @@ -286,10 +288,12 @@ optional arguments: `1d`). --timerange TIMERANGE Specify what timerange of data to use. - --max_open_trades INT - Specify max_open_trades to use. - --stake_amount STAKE_AMOUNT - Specify stake_amount. + --max-open-trades INT + Override the value of the `max_open_trades` + configuration setting. + --stake-amount STAKE_AMOUNT + Override the value of the `stake_amount` configuration + setting. --fee FLOAT Specify fee ratio. Will be applied twice (on trade entry and exit). --hyperopt NAME Specify hyperopt class name which will be used by the @@ -360,7 +364,7 @@ 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] - [--max_open_trades INT] [--stake_amount STAKE_AMOUNT] + [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] [--stoplosses STOPLOSS_RANGE] optional arguments: @@ -370,10 +374,12 @@ optional arguments: `1d`). --timerange TIMERANGE Specify what timerange of data to use. - --max_open_trades INT - Specify max_open_trades to use. - --stake_amount STAKE_AMOUNT - Specify stake_amount. + --max-open-trades INT + Override the value of the `max_open_trades` + configuration setting. + --stake-amount STAKE_AMOUNT + Override the value of the `stake_amount` configuration + setting. --fee FLOAT Specify fee ratio. Will be applied twice (on trade entry and exit). --stoplosses STOPLOSS_RANGE diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index 30902dfe9..4b6429f20 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -118,14 +118,14 @@ AVAILABLE_CLI_OPTIONS = { help='Specify what timerange of data to use.', ), "max_open_trades": Arg( - '--max_open_trades', - help='Specify max_open_trades to use.', + '--max-open-trades', + help='Override the value of the `max_open_trades` configuration setting.', type=int, metavar='INT', ), "stake_amount": Arg( - '--stake_amount', - help='Specify stake_amount.', + '--stake-amount', + help='Override the value of the `stake_amount` configuration setting.', type=float, ), # Backtesting diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index e517a0558..001e89303 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -223,13 +223,13 @@ class Configuration: 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"]}) - logger.info('Parameter --max_open_trades detected, ' + 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: logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) self._args_to_config(config, argname='stake_amount', - logstring='Parameter --stake_amount detected, ' + logstring='Parameter --stake-amount detected, ' 'overriding stake_amount to: {} ...') self._args_to_config(config, argname='fee', From 9ec4368c6f9cee11cbc72ca5f518fcff57859b02 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Dec 2019 09:20:42 +0100 Subject: [PATCH 089/324] Add release documentation --- docs/developer.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/developer.md b/docs/developer.md index 5b07aff03..c679b8a49 100644 --- a/docs/developer.md +++ b/docs/developer.md @@ -266,4 +266,29 @@ Once the PR against master 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 the above changelog as release comment (as codeblock). +* Use the above changelog as release comment (as codeblock) + +### After-release + +* Update version in develop by postfixing that with `-dev` (`2019.6 -> 2019.6-dev`). +* Create a PR against develop to update that branch. + +## Releases + +### pypi + +To create a pypi release, please run the following commands: + +Additional requirement: `wheel`, `twine` (for uploading), account on pypi with proper permissions. + +``` bash +python setup.py sdist bdist_wheel + +# For pypi test (to check if some change to the installation did work) +twine upload --repository-url https://test.pypi.org/legacy/ dist/* + +# For production: +twine upload dist/* +``` + +Please don't push non-releases to the productive / real pypi instance. From c417877eb8d6ad8cc63f9b7585846837f872b701 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 5 Oct 2019 13:41:54 +0200 Subject: [PATCH 090/324] sort pytest dependencies --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3710bcdc0..29f9db10c 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,7 @@ setup(name='freqtrade', license='GPLv3', packages=['freqtrade'], setup_requires=['pytest-runner', 'numpy'], - tests_require=['pytest', 'pytest-mock', 'pytest-cov'], + tests_require=['pytest', 'pytest-asyncio', 'pytest-cov', 'pytest-mock', ], install_requires=[ # from requirements-common.txt 'ccxt>=1.18.1080', From 1a73159200278edc3de3f484811c0ecde6dbaaa9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 23 Oct 2019 06:43:22 +0200 Subject: [PATCH 091/324] Modify classifiers --- setup.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 29f9db10c..7d8d7b68d 100644 --- a/setup.py +++ b/setup.py @@ -99,8 +99,12 @@ setup(name='freqtrade', ], }, classifiers=[ - 'Programming Language :: Python :: 3.6', - 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', - 'Topic :: Office/Business :: Financial :: Investment', + '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', + 'Operating System :: MacOS', + 'Operating System :: Unix', + 'Topic :: Office/Business :: Financial :: Investment', ]) From 1ff0d0f1fa05137560475d7c209768fb30d3aefc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Dec 2019 09:35:06 +0100 Subject: [PATCH 092/324] Add unfilledtimeout to strategy overrides --- docs/configuration.md | 5 +++-- freqtrade/resolvers/strategy_resolver.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 73534b6f1..846c4510d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -55,8 +55,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). [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_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.
***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.
***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. [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* | `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#understand-ask_last_balance). | `bid_strategy.use_order_book` | Enable buying using the rates in Order Book Bids.
***Datatype:*** *Boolean* | `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids. *Defaults to `1`.*
***Datatype:*** *Positive Integer* @@ -124,6 +124,7 @@ Values set in the configuration file always overwrite values set in the strategy * `order_time_in_force` * `stake_currency` * `stake_amount` +* `unfilledtimeout` * `use_sell_signal` (ask_strategy) * `sell_profit_only` (ask_strategy) * `ignore_roi_if_buy_signal` (ask_strategy) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 9a76b9b74..a2d14fbf3 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -61,6 +61,7 @@ class StrategyResolver(IResolver): ("stake_currency", None, False), ("stake_amount", None, False), ("startup_candle_count", None, False), + ("unfilledtimeout", None, False), ("use_sell_signal", True, True), ("sell_profit_only", False, True), ("ignore_roi_if_buy_signal", False, True), From 9835312033a300dce9bae1e92381b5bcffbdfa48 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Dec 2019 09:46:00 +0100 Subject: [PATCH 093/324] Improve pair_lock handling --- freqtrade/strategy/interface.py | 15 ++++++++++++++- tests/strategy/test_interface.py | 13 +++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 985ff37de..4f2e990d2 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -168,11 +168,24 @@ class IStrategy(ABC): """ Locks pair until a given timestamp happens. Locked pairs are not analyzed, and are prevented from opening new trades. + Locks can only count up (allowing users to lock pairs for a longer period of time). + To remove a lock from a pair, use `unlock_pair()` :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)` """ - self._pair_locked_until[pair] = until + if pair not in self._pair_locked_until or self._pair_locked_until[pair] < until: + self._pair_locked_until[pair] = until + + def unlock_pair(self, pair) -> None: + """ + Unlocks a pair previously locked using lock_pair. + Not used by freqtrade itself, but intended to be used if users lock pairs + 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] def is_pair_locked(self, pair: str) -> bool: """ diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 605622b8f..89c38bda1 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -302,6 +302,19 @@ 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) + + # Unlocking a pair that's not locked should not raise an error + strategy.unlock_pair(pair) + + # Unlock original pair + pair = 'ETH/BTC' + strategy.unlock_pair(pair) + assert not strategy.is_pair_locked(pair) From 89b4f45fe353773cafe09fb9a18a941a7b1cfb9b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Dec 2019 09:47:37 +0100 Subject: [PATCH 094/324] Remove section about strategy template - use new-strategy intead --- docs/strategy-customization.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 4efca7d2f..c4a477f80 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -479,11 +479,6 @@ 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). -### Where can i find a strategy template? - -The strategy template is located in the file -[user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_strategy.py). - ### Specify custom strategy location If you want to use a strategy from a different directory you can pass `--strategy-path` From a71deeda94f4c55ae1a8f30d10d034ffb0acbcd4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Dec 2019 09:55:40 +0100 Subject: [PATCH 095/324] Document lock-pair implementation --- docs/strategy-customization.md | 43 ++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index c4a477f80..011f64d70 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -455,6 +455,49 @@ 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 + +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 + +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 should be a time in the future, after which trading will be reenabled for that pair. + +Locks can also be lifted manually, by calling `self.unlock_pair(pair)`. + +!!! Note + Locked pairs are not persisted, so a restart of the bot, or calling `/reload_conf` will reset locked pairs. + +!!! Warning + Locking pairs is not functional during backtesting. + +##### Pair locking example + +``` python +from freqtrade.persistence import Trade +from datetime import timedelta, datetime, timezone +# Put the above lines a the top of the strategy file, next to all the other imports +# -------- + +# Within populate indicators (or populate_buy): +if self.config['runmode'] in ('live', 'dry_run'): + # fetch closed trades for the last 2 days + trades = Trade.get_trades([Trade.pair == metadata['pair'], + Trade.open_date > datetime.utcnow() - timedelta(days=2), + Trade.is_open == False, + ]).all() + # Analyze the conditions you'd like to lock the pair .... will probably be different for every strategy + sumprofit = sum(trade.close_profit for trade in trades) + if sumprofit < 0: + # Lock pair for 2 days + self.lock_pair(metadata['pair'], until=datetime.now(timezone.utc) + timedelta(days=2)) +``` + ### Print created dataframe To inspect the created dataframe, you can issue a print-statement in either `populate_buy_trend()` or `populate_sell_trend()`. From 43c25c8a328edf307dc979be0ae88a3cd3d508f8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Dec 2019 09:59:25 +0100 Subject: [PATCH 096/324] add documentation for is_pair_locked --- docs/strategy-customization.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 011f64d70..129939b25 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -466,10 +466,12 @@ 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 should be a time in the future, after which trading will be reenabled for that pair. +`until` must be a datetime object in the future, after which trading will be reenabled for that pair. 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. From ffd7034c005ead82a79e4a60e20e23e225953141 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Dec 2019 10:16:16 +0100 Subject: [PATCH 097/324] Persist dry-run trade per default --- freqtrade/constants.py | 2 +- tests/test_persistence.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 5c7190b41..d7c6249d5 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -10,7 +10,7 @@ HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss' DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' -DEFAULT_DB_DRYRUN_URL = 'sqlite://' +DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite' UNLIMITED_STAKE_AMOUNT = 'unlimited' DEFAULT_AMOUNT_RESERVE_PERCENT = 0.05 REQUIRED_ORDERTIF = ['buy', 'sell'] diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 25ad8b6a7..b9a636e1a 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -100,7 +100,7 @@ def test_init_dryrun_db(default_conf, mocker): init(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://' + assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.dryrun.sqlite' @pytest.mark.usefixtures("init_persistence") From dc567f99d67e5a82026fd3a5ff3002323ac51b24 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Dec 2019 10:16:49 +0100 Subject: [PATCH 098/324] Update documentation to new handling of dry-mode database --- docs/bot-usage.md | 8 +++-- docs/configuration.md | 2 +- docs/docker.md | 3 +- docs/plotting.md | 72 +++++++++++++++++-------------------------- 4 files changed, 37 insertions(+), 48 deletions(-) diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 86a990946..e856755d2 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -45,14 +45,17 @@ optional arguments: -h, --help show this help message and exit --db-url PATH Override trades database URL, this is useful in custom deployments (default: `sqlite:///tradesv3.sqlite` for - Live Run mode, `sqlite://` for Dry Run). + Live Run mode, `sqlite:///tradesv3.dryrun.sqlite` for + Dry Run). --sd-notify Notify systemd service manager. --dry-run Enforce dry-run for trading (removes Exchange secrets and simulates trades). Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). - --logfile FILE Log to the file specified. + --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`). @@ -68,6 +71,7 @@ Strategy arguments: Specify strategy class name which will be used by the bot. --strategy-path PATH Specify additional strategy lookup path. + ``` ### How to specify which configuration file be used? diff --git a/docs/configuration.md b/docs/configuration.md index 73534b6f1..fbd4c9815 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -96,7 +96,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `api_server.listen_port` | Bind Port. See the [API Server documentation](rest-api.md) for more details.
***Datatype:*** *Integer between 1024 and 65535* | `api_server.username` | Username for API server. See the [API Server documentation](rest-api.md) for more details. **Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* | `api_server.password` | Password for API server. See the [API Server documentation](rest-api.md) for more details. **Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* -| `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `true`, and to `sqlite:///tradesv3.sqlite` for production instances.
***Datatype:*** *String, SQLAlchemy connect string* +| `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite:///tradesv3.dryrun.sqlite` if `dry_run` is `true`, and to `sqlite:///tradesv3.sqlite` for production instances.
***Datatype:*** *String, SQLAlchemy connect string* | `initial_state` | Defines the initial application state. More information below.
*Defaults to `stopped`.*
***Datatype:*** *Enum, either `stopped` or `running`* | `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below.
***Datatype:*** *Boolean* | `strategy` | **Required** Defines Strategy class to use. Recommended to be set via `--strategy NAME`.
***Datatype:*** *ClassName* diff --git a/docs/docker.md b/docs/docker.md index ff5bf7f25..d1684abc5 100644 --- a/docs/docker.md +++ b/docs/docker.md @@ -164,8 +164,7 @@ docker run -d \ ``` !!! Note - db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used. - To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite` + 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. diff --git a/docs/plotting.md b/docs/plotting.md index 982a5cd65..ba737562f 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -23,58 +23,43 @@ The `freqtrade plot-dataframe` subcommand shows an interactive graph with three Possible arguments: ``` -usage: freqtrade plot-dataframe [-h] [-v] [--logfile FILE] [-V] [-c PATH] - [-d PATH] [--userdir PATH] [-s NAME] - [--strategy-path PATH] [-p PAIRS [PAIRS ...]] - [--indicators1 INDICATORS1 [INDICATORS1 ...]] - [--indicators2 INDICATORS2 [INDICATORS2 ...]] - [--plot-limit INT] [--db-url PATH] - [--trade-source {DB,file}] [--export EXPORT] - [--export-filename PATH] - [--timerange TIMERANGE] [-i TICKER_INTERVAL] +usage: freqtrade plot-dataframe [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] + [--strategy-path PATH] [-p PAIRS [PAIRS ...]] [--indicators1 INDICATORS1 [INDICATORS1 ...]] + [--indicators2 INDICATORS2 [INDICATORS2 ...]] [--plot-limit INT] [--db-url PATH] + [--trade-source {DB,file}] [--export EXPORT] [--export-filename PATH] [--timerange TIMERANGE] + [-i TICKER_INTERVAL] 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. --indicators1 INDICATORS1 [INDICATORS1 ...] - Set indicators from your strategy you want in the - first row of the graph. Space-separated list. Example: + Set indicators from your strategy you want in the first row of the graph. Space-separated list. Example: `ema3 ema5`. Default: `['sma', 'ema3', 'ema5']`. --indicators2 INDICATORS2 [INDICATORS2 ...] - Set indicators from your strategy you want in the - third row of the graph. Space-separated list. Example: + Set indicators from your strategy you want in the third row of the graph. Space-separated list. Example: `fastd fastk`. Default: `['macd', 'macdsignal']`. - --plot-limit INT Specify tick limit for plotting. Notice: too high - values cause huge files. Default: 750. - --db-url PATH Override trades database URL, this is useful in custom - deployments (default: `sqlite:///tradesv3.sqlite` for - Live Run mode, `sqlite://` for Dry Run). + --plot-limit INT Specify tick limit for plotting. Notice: too high values cause huge files. Default: 750. + --db-url PATH Override trades database URL, this is useful in custom deployments (default: `sqlite:///tradesv3.sqlite` + for Live Run mode, `sqlite:///tradesv3.dryrun.sqlite` for Dry Run). --trade-source {DB,file} - Specify the source for trades (Can be DB or file - (backtest file)) Default: file - --export EXPORT Export backtest results, argument are: trades. - Example: `--export=trades` + Specify the source for trades (Can be DB or file (backtest file)) Default: file + --export EXPORT Export backtest results, argument are: trades. Example: `--export=trades` --export-filename PATH - Save backtest results to the file with this filename - (default: `user_data/backtest_results/backtest- - result.json`). Requires `--export` to be set as well. - Example: `--export-filename=user_data/backtest_results - /backtest_today.json` + Save backtest results to the file with this filename. Requires `--export` to be set as well. Example: + `--export-filename=user_data/backtest_results/backtest_today.json` --timerange TIMERANGE Specify what timerange of data to use. -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL - Specify ticker interval (`1m`, `5m`, `30m`, `1h`, - `1d`). + Specify ticker interval (`1m`, `5m`, `30m`, `1h`, `1d`). Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). - --logfile FILE Log to the file specified. + --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 + Specify configuration file (default: `config.json`). 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. @@ -83,8 +68,7 @@ Common arguments: Strategy arguments: -s NAME, --strategy NAME - Specify strategy class name (default: - `DefaultStrategy`). + Specify strategy class name which will be used by the bot. --strategy-path PATH Specify additional strategy lookup path. ``` @@ -173,14 +157,14 @@ optional arguments: --export EXPORT Export backtest results, argument are: trades. Example: `--export=trades` --export-filename PATH - Save backtest results to the file with this filename - (default: `user_data/backtest_results/backtest- - result.json`). Requires `--export` to be set as well. - Example: `--export-filename=user_data/backtest_results - /backtest_today.json` + Save backtest results to the file with this filename. + Requires `--export` to be set as well. Example: + `--export-filename=user_data/backtest_results/backtest + _today.json` --db-url PATH Override trades database URL, this is useful in custom deployments (default: `sqlite:///tradesv3.sqlite` for - Live Run mode, `sqlite://` for Dry Run). + Live Run mode, `sqlite:///tradesv3.dryrun.sqlite` for + Dry Run). --trade-source {DB,file} Specify the source for trades (Can be DB or file (backtest file)) Default: file @@ -190,7 +174,9 @@ optional arguments: Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). - --logfile FILE Log to the file specified. + --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`). From 2195ae59d6227de123a85a1a547be0d956b49656 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Dec 2019 12:49:01 +0100 Subject: [PATCH 099/324] Use different time offsets to avoid confusion --- docs/strategy-customization.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 129939b25..d59b097d7 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -476,7 +476,7 @@ To verify if a pair is currently locked, use `self.is_pair_locked(pair)`. Locked pairs are not persisted, so a restart of the bot, or calling `/reload_conf` will reset locked pairs. !!! Warning - Locking pairs is not functional during backtesting. + Locking pairs is not functioning during backtesting. ##### Pair locking example @@ -496,8 +496,8 @@ if self.config['runmode'] in ('live', 'dry_run'): # Analyze the conditions you'd like to lock the pair .... will probably be different for every strategy sumprofit = sum(trade.close_profit for trade in trades) if sumprofit < 0: - # Lock pair for 2 days - self.lock_pair(metadata['pair'], until=datetime.now(timezone.utc) + timedelta(days=2)) + # Lock pair for 12 hours + self.lock_pair(metadata['pair'], until=datetime.now(timezone.utc) + timedelta(hours=12)) ``` ### Print created dataframe From 76a93fabc7027f80b5a45b6365b36891d127d1aa Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2019 06:26:13 +0000 Subject: [PATCH 100/324] Bump python from 3.7.5-slim-stretch to 3.7.6-slim-stretch Bumps python from 3.7.5-slim-stretch to 3.7.6-slim-stretch. Signed-off-by: dependabot-preview[bot] --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index dc9b04403..f631d891d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7.5-slim-stretch +FROM python:3.7.6-slim-stretch RUN apt-get update \ && apt-get -y install curl build-essential libssl-dev \ From 8f17b81329ea59ab7ad45a89501b9f42f79b32f4 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2019 07:37:04 +0000 Subject: [PATCH 101/324] Bump ccxt from 1.20.84 to 1.21.12 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.20.84 to 1.21.12. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/1.20.84...1.21.12) 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 a6d9a6f5e..ed6d68742 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -1,6 +1,6 @@ # requirements without requirements installable via conda # mainly used for Raspberry pi installs -ccxt==1.20.84 +ccxt==1.21.12 SQLAlchemy==1.3.11 python-telegram-bot==12.2.0 arrow==0.15.4 From 20ad8a379d51f29d3e00a1f7c3a8cdfbf33ed64b Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2019 07:38:05 +0000 Subject: [PATCH 102/324] Bump numpy from 1.17.4 to 1.18.0 Bumps [numpy](https://github.com/numpy/numpy) from 1.17.4 to 1.18.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.17.4...v1.18.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 ebf27abd4..e0e2942b1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Load common requirements -r requirements-common.txt -numpy==1.17.4 +numpy==1.18.0 pandas==0.25.3 From 31a7e9feedb72675845f8e1e1cf2fb29e91d8bc7 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2019 07:38:10 +0000 Subject: [PATCH 103/324] Bump mypy from 0.750 to 0.761 Bumps [mypy](https://github.com/python/mypy) from 0.750 to 0.761. - [Release notes](https://github.com/python/mypy/releases) - [Commits](https://github.com/python/mypy/compare/v0.750...v0.761) 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 fe5b4e369..1357bba00 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,7 +7,7 @@ coveralls==1.9.2 flake8==3.7.9 flake8-type-annotations==0.1.0 flake8-tidy-imports==3.1.0 -mypy==0.750 +mypy==0.761 pytest==5.3.2 pytest-asyncio==0.10.0 pytest-cov==2.8.1 From 9cfbe98a237549bef2c3d3e87e2d6a933911b8b0 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2019 07:39:25 +0000 Subject: [PATCH 104/324] Bump scipy from 1.3.3 to 1.4.1 Bumps [scipy](https://github.com/scipy/scipy) from 1.3.3 to 1.4.1. - [Release notes](https://github.com/scipy/scipy/releases) - [Commits](https://github.com/scipy/scipy/compare/v1.3.3...v1.4.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 b2428e37d..9b408dbed 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.3.3 +scipy==1.4.1 scikit-learn==0.22 scikit-optimize==0.5.2 filelock==3.0.12 From 779278ed507d1f7f9070ee50f8349b720b499869 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 23 Dec 2019 08:28:05 +0000 Subject: [PATCH 105/324] Bump sqlalchemy from 1.3.11 to 1.3.12 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.11 to 1.3.12. - [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 ed6d68742..9d0fd4756 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.21.12 -SQLAlchemy==1.3.11 +SQLAlchemy==1.3.12 python-telegram-bot==12.2.0 arrow==0.15.4 cachetools==4.0.0 From 1c5f8070e547d3c1766779697e553efa47742d42 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Dec 2019 09:53:55 +0100 Subject: [PATCH 106/324] Refactor build_paths to staticmethod --- freqtrade/resolvers/iresolver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 3bad42fd9..0b986debb 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -17,7 +17,8 @@ class IResolver: This class contains all the logic to load custom classes """ - def build_search_paths(self, config, current_path: Path, user_subdir: Optional[str] = None, + @staticmethod + def build_search_paths(config, current_path: Path, user_subdir: Optional[str] = None, extra_dir: Optional[str] = None) -> List[Path]: abs_paths: List[Path] = [current_path] From 5fefa9e97c01b83accd9ddd8c44bad83f331304d Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Dec 2019 09:56:12 +0100 Subject: [PATCH 107/324] Convert PairlistResolver to static loader --- freqtrade/pairlist/pairlistmanager.py | 14 ++++---- freqtrade/resolvers/pairlist_resolver.py | 43 ++++++++++++++---------- tests/pairlist/test_pairlist.py | 2 +- 3 files changed, 34 insertions(+), 25 deletions(-) diff --git a/freqtrade/pairlist/pairlistmanager.py b/freqtrade/pairlist/pairlistmanager.py index fa5382c37..1530710d2 100644 --- a/freqtrade/pairlist/pairlistmanager.py +++ b/freqtrade/pairlist/pairlistmanager.py @@ -28,13 +28,13 @@ class PairListManager(): if 'method' not in pl: logger.warning(f"No method in {pl}") continue - pairl = PairListResolver(pl.get('method'), - exchange=exchange, - pairlistmanager=self, - config=config, - pairlistconfig=pl, - pairlist_pos=len(self._pairlists) - ).pairlist + pairl = PairListResolver.load_pairlist(pl.get('method'), + exchange=exchange, + pairlistmanager=self, + config=config, + pairlistconfig=pl, + pairlist_pos=len(self._pairlists) + ) self._tickers_needed = pairl.needstickers or self._tickers_needed self._pairlists.append(pairl) diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index d849f4ffb..5b5bcee3a 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -18,23 +18,32 @@ class PairListResolver(IResolver): This class contains all the logic to load custom PairList class """ - __slots__ = ['pairlist'] + __slots__ = [] - def __init__(self, pairlist_name: str, exchange, pairlistmanager, - config: dict, pairlistconfig: dict, pairlist_pos: int) -> None: + @staticmethod + def load_pairlist(pairlist_name: str, exchange, pairlistmanager, + config: dict, pairlistconfig: dict, pairlist_pos: int) -> IPairList: """ - Load the custom class from config parameter - :param config: configuration dictionary or None + Load the pairlist with pairlist_name + :param pairlist_name: Classname of the pairlist + :param exchange: Initialized exchange class + :param pairlistmanager: Initialized pairlist manager + :param config: configuration dictionary + :param pairlistconfig: Configuration dedicated to this pairlist + :param pairlist_pos: Position of the pairlist in the list of pairlists + :return: initialized Pairlist class """ - self.pairlist = self._load_pairlist(pairlist_name, config, - kwargs={'exchange': exchange, - 'pairlistmanager': pairlistmanager, - 'config': config, - 'pairlistconfig': pairlistconfig, - 'pairlist_pos': pairlist_pos}) - def _load_pairlist( - self, pairlist_name: str, config: dict, kwargs: dict) -> IPairList: + return PairListResolver._load_pairlist(pairlist_name, config, + kwargs={'exchange': exchange, + 'pairlistmanager': pairlistmanager, + 'config': config, + 'pairlistconfig': pairlistconfig, + 'pairlist_pos': pairlist_pos}) + + + @staticmethod + def _load_pairlist(pairlist_name: str, config: dict, kwargs: dict) -> IPairList: """ Search and loads the specified pairlist. :param pairlist_name: name of the module to import @@ -44,11 +53,11 @@ class PairListResolver(IResolver): """ current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve() - abs_paths = self.build_search_paths(config, current_path=current_path, - user_subdir=None, extra_dir=None) + abs_paths = IResolver.build_search_paths(config, current_path=current_path, + user_subdir=None, extra_dir=None) - pairlist = self._load_object(paths=abs_paths, object_type=IPairList, - object_name=pairlist_name, kwargs=kwargs) + pairlist = IResolver._load_object(paths=abs_paths, object_type=IPairList, + object_name=pairlist_name, kwargs=kwargs) if pairlist: return pairlist raise OperationalException( diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 43285cdb1..e7f098777 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -53,7 +53,7 @@ def test_load_pairlist_noexist(mocker, markets, default_conf): with pytest.raises(OperationalException, match=r"Impossible to load Pairlist 'NonexistingPairList'. " r"This class does not exist or contains Python code errors."): - PairListResolver('NonexistingPairList', bot.exchange, plm, default_conf, {}, 1) + PairListResolver.load_pairlist('NonexistingPairList', bot.exchange, plm, default_conf, {}, 1) def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf): From 560acb7cea6fa34bcf1eb0473cc373970fb97fd0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Dec 2019 10:03:18 +0100 Subject: [PATCH 108/324] Convert ExchangeResolver to static loader class --- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 2 +- freqtrade/resolvers/exchange_resolver.py | 20 +++++++++++--------- freqtrade/resolvers/pairlist_resolver.py | 3 --- freqtrade/utils.py | 8 ++++---- tests/conftest.py | 2 +- tests/exchange/test_exchange.py | 8 ++++---- tests/pairlist/test_pairlist.py | 3 ++- 8 files changed, 24 insertions(+), 24 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 8ae027fa2..001b7f02d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -60,7 +60,7 @@ class FreqtradeBot: # Check config consistency here since strategies can set certain options validate_config_consistency(config) - self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange + self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) persistence.init(self.config.get('db_url', None), clean_open_orders=self.config.get('dry_run', False)) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 726257cdd..bab997cb1 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -60,7 +60,7 @@ class Backtesting: # Reset keys for backtesting remove_credentials(self.config) self.strategylist: List[IStrategy] = [] - self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange + self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) if config.get('fee'): self.fee = config['fee'] diff --git a/freqtrade/resolvers/exchange_resolver.py b/freqtrade/resolvers/exchange_resolver.py index 60f37b1c9..e28a5cf80 100644 --- a/freqtrade/resolvers/exchange_resolver.py +++ b/freqtrade/resolvers/exchange_resolver.py @@ -15,9 +15,8 @@ class ExchangeResolver(IResolver): This class contains all the logic to load a custom exchange class """ - __slots__ = ['exchange'] - - def __init__(self, exchange_name: str, config: dict, validate: bool = True) -> None: + @staticmethod + def load_exchange(exchange_name: str, config: dict, validate: bool = True) -> Exchange: """ Load the custom class from config parameter :param config: configuration dictionary @@ -25,17 +24,20 @@ class ExchangeResolver(IResolver): # Map exchange name to avoid duplicate classes for identical exchanges exchange_name = MAP_EXCHANGE_CHILDCLASS.get(exchange_name, exchange_name) exchange_name = exchange_name.title() + exchange = None try: - self.exchange = self._load_exchange(exchange_name, kwargs={'config': config, - 'validate': validate}) + exchange = ExchangeResolver._load_exchange(exchange_name, + kwargs={'config': config, + 'validate': validate}) except ImportError: logger.info( f"No {exchange_name} specific subclass found. Using the generic class instead.") - if not hasattr(self, "exchange"): - self.exchange = Exchange(config, validate=validate) + if not exchange: + exchange = Exchange(config, validate=validate) + return exchange - def _load_exchange( - self, exchange_name: str, kwargs: dict) -> Exchange: + @staticmethod + def _load_exchange(exchange_name: str, kwargs: dict) -> Exchange: """ Loads the specified exchange. Only checks for exchanges exported in freqtrade.exchanges diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index 5b5bcee3a..611660ff4 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -18,8 +18,6 @@ class PairListResolver(IResolver): This class contains all the logic to load custom PairList class """ - __slots__ = [] - @staticmethod def load_pairlist(pairlist_name: str, exchange, pairlistmanager, config: dict, pairlistconfig: dict, pairlist_pos: int) -> IPairList: @@ -41,7 +39,6 @@ class PairListResolver(IResolver): 'pairlistconfig': pairlistconfig, 'pairlist_pos': pairlist_pos}) - @staticmethod def _load_pairlist(pairlist_name: str, config: dict, kwargs: dict) -> IPairList: """ diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 9e01c7ea6..18966c574 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -198,7 +198,7 @@ def start_download_data(args: Dict[str, Any]) -> None: pairs_not_available: List[str] = [] # Init exchange - exchange = ExchangeResolver(config['exchange']['name'], config).exchange + exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config) try: if config.get('download_trades'): @@ -233,7 +233,7 @@ def start_list_timeframes(args: Dict[str, Any]) -> None: config['ticker_interval'] = None # Init exchange - exchange = ExchangeResolver(config['exchange']['name'], config, validate=False).exchange + exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) if args['print_one_column']: print('\n'.join(exchange.timeframes)) @@ -252,7 +252,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) # Init exchange - exchange = ExchangeResolver(config['exchange']['name'], config, validate=False).exchange + exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) # By default only active pairs/markets are to be shown active_only = not args.get('list_pairs_all', False) @@ -333,7 +333,7 @@ def start_test_pairlist(args: Dict[str, Any]) -> None: from freqtrade.pairlist.pairlistmanager import PairListManager config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) - exchange = ExchangeResolver(config['exchange']['name'], config, validate=False).exchange + exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) quote_currencies = args.get('quote_currencies') if not quote_currencies: diff --git a/tests/conftest.py b/tests/conftest.py index 82111528e..501f89fff 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -77,7 +77,7 @@ def get_patched_exchange(mocker, config, api_mock=None, id='bittrex', patch_exchange(mocker, api_mock, id, mock_markets) config["exchange"]["name"] = id try: - exchange = ExchangeResolver(id, config).exchange + exchange = ExchangeResolver.load_exchange(id, config) except ImportError: exchange = Exchange(config) return exchange diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 629f99aa2..dccf7d74f 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -124,19 +124,19 @@ def test_exchange_resolver(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) - exchange = ExchangeResolver('Bittrex', default_conf).exchange + exchange = ExchangeResolver.load_exchange('Bittrex', 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('kraken', default_conf).exchange + exchange = ExchangeResolver.load_exchange('kraken', default_conf) assert isinstance(exchange, Exchange) assert isinstance(exchange, Kraken) assert not isinstance(exchange, Binance) assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog) - exchange = ExchangeResolver('binance', default_conf).exchange + exchange = ExchangeResolver.load_exchange('binance', default_conf) assert isinstance(exchange, Exchange) assert isinstance(exchange, Binance) assert not isinstance(exchange, Kraken) @@ -145,7 +145,7 @@ def test_exchange_resolver(default_conf, mocker, caplog): caplog) # Test mapping - exchange = ExchangeResolver('binanceus', default_conf).exchange + exchange = ExchangeResolver.load_exchange('binanceus', default_conf) assert isinstance(exchange, Exchange) assert isinstance(exchange, Binance) assert not isinstance(exchange, Kraken) diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index e7f098777..21929de2b 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -53,7 +53,8 @@ def test_load_pairlist_noexist(mocker, markets, default_conf): with pytest.raises(OperationalException, match=r"Impossible to load Pairlist 'NonexistingPairList'. " r"This class does not exist or contains Python code errors."): - PairListResolver.load_pairlist('NonexistingPairList', bot.exchange, plm, default_conf, {}, 1) + PairListResolver.load_pairlist('NonexistingPairList', bot.exchange, plm, + default_conf, {}, 1) def test_refresh_market_pair_not_in_whitelist(mocker, markets, static_pl_conf): From 248ef5a0eac41a82febd07ef8b1e4f421f7cce38 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Dec 2019 10:06:19 +0100 Subject: [PATCH 109/324] Convert HyperoptResolver to static loader --- freqtrade/optimize/hyperopt.py | 2 +- freqtrade/resolvers/hyperopt_resolver.py | 29 +++++++++++++----------- tests/optimize/test_hyperopt.py | 6 ++--- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 521a4d790..a4a8f79d1 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -64,7 +64,7 @@ class Hyperopt: self.backtesting = Backtesting(self.config) - self.custom_hyperopt = HyperOptResolver(self.config).hyperopt + self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 05efa1164..a7f922c7b 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -20,11 +20,11 @@ class HyperOptResolver(IResolver): """ This class contains all the logic to load custom hyperopt class """ - __slots__ = ['hyperopt'] - def __init__(self, config: Dict) -> None: + @staticmethod + def load_hyperopt(config: Dict) -> IHyperOpt: """ - Load the custom class from config parameter + Load the custom hyperopt class from config parameter :param config: configuration dictionary """ if not config.get('hyperopt'): @@ -33,21 +33,23 @@ class HyperOptResolver(IResolver): hyperopt_name = config['hyperopt'] - self.hyperopt = self._load_hyperopt(hyperopt_name, config, - extra_dir=config.get('hyperopt_path')) + hyperopt = HyperOptResolver._load_hyperopt(hyperopt_name, config, + extra_dir=config.get('hyperopt_path')) - if not hasattr(self.hyperopt, 'populate_indicators'): + if not hasattr(hyperopt, 'populate_indicators'): logger.warning("Hyperopt class does not provide populate_indicators() method. " "Using populate_indicators from the strategy.") - if not hasattr(self.hyperopt, 'populate_buy_trend'): + 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.") - if not hasattr(self.hyperopt, 'populate_sell_trend'): + 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.") + return hyperopt + @staticmethod def _load_hyperopt( - self, hyperopt_name: str, config: Dict, extra_dir: Optional[str] = None) -> IHyperOpt: + hyperopt_name: str, config: Dict, extra_dir: Optional[str] = None) -> IHyperOpt: """ Search and loads the specified hyperopt. :param hyperopt_name: name of the module to import @@ -57,11 +59,12 @@ class HyperOptResolver(IResolver): """ current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() - abs_paths = self.build_search_paths(config, current_path=current_path, - user_subdir=USERPATH_HYPEROPTS, extra_dir=extra_dir) + abs_paths = IResolver.build_search_paths(config, current_path=current_path, + user_subdir=USERPATH_HYPEROPTS, + extra_dir=extra_dir) - hyperopt = self._load_object(paths=abs_paths, object_type=IHyperOpt, - object_name=hyperopt_name, kwargs={'config': config}) + hyperopt = IResolver._load_object(paths=abs_paths, object_type=IHyperOpt, + object_name=hyperopt_name, kwargs={'config': config}) if hyperopt: return hyperopt raise OperationalException( diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 29b8b5b16..37de32ab0 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -163,7 +163,7 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None: MagicMock(return_value=hyperopt(default_conf)) ) default_conf.update({'hyperopt': 'DefaultHyperOpt'}) - x = HyperOptResolver(default_conf).hyperopt + x = HyperOptResolver.load_hyperopt(default_conf) assert not hasattr(x, 'populate_indicators') assert not hasattr(x, 'populate_buy_trend') assert not hasattr(x, 'populate_sell_trend') @@ -180,7 +180,7 @@ def test_hyperoptresolver_wrongname(mocker, default_conf, caplog) -> None: default_conf.update({'hyperopt': "NonExistingHyperoptClass"}) with pytest.raises(OperationalException, match=r'Impossible to load Hyperopt.*'): - HyperOptResolver(default_conf).hyperopt + HyperOptResolver.load_hyperopt(default_conf) def test_hyperoptresolver_noname(default_conf): @@ -188,7 +188,7 @@ def test_hyperoptresolver_noname(default_conf): with pytest.raises(OperationalException, match="No Hyperopt set. Please use `--hyperopt` to specify " "the Hyperopt class to use."): - HyperOptResolver(default_conf) + HyperOptResolver.load_hyperopt(default_conf) def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None: From 6d5aca4f323dd06cac94cbf598173be6fb3ed645 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Dec 2019 10:09:08 +0100 Subject: [PATCH 110/324] Convert hyperoptloss resolver to static loader --- freqtrade/optimize/hyperopt.py | 2 +- freqtrade/resolvers/hyperopt_resolver.py | 26 +++++++++++++----------- tests/optimize/test_hyperopt.py | 14 ++++++------- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index a4a8f79d1..48f883ac5 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -66,7 +66,7 @@ class Hyperopt: self.custom_hyperopt = HyperOptResolver.load_hyperopt(self.config) - self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss + self.custom_hyperoptloss = HyperOptLossResolver.load_hyperoptloss(self.config) self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function self.trials_file = (self.config['user_data_dir'] / diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index a7f922c7b..0726b0627 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -77,9 +77,9 @@ class HyperOptLossResolver(IResolver): """ This class contains all the logic to load custom hyperopt loss class """ - __slots__ = ['hyperoptloss'] - def __init__(self, config: Dict) -> None: + @staticmethod + def load_hyperoptloss(config: Dict) -> IHyperOptLoss: """ Load the custom class from config parameter :param config: configuration dictionary @@ -89,20 +89,21 @@ class HyperOptLossResolver(IResolver): # default hyperopt loss hyperoptloss_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS - self.hyperoptloss = self._load_hyperoptloss( + hyperoptloss = HyperOptLossResolver._load_hyperoptloss( hyperoptloss_name, config, extra_dir=config.get('hyperopt_path')) # Assign ticker_interval to be used in hyperopt - self.hyperoptloss.__class__.ticker_interval = str(config['ticker_interval']) + hyperoptloss.__class__.ticker_interval = str(config['ticker_interval']) - if not hasattr(self.hyperoptloss, 'hyperopt_loss_function'): + if not hasattr(hyperoptloss, 'hyperopt_loss_function'): raise OperationalException( f"Found HyperoptLoss class {hyperoptloss_name} does not " "implement `hyperopt_loss_function`.") + return hyperoptloss - def _load_hyperoptloss( - self, hyper_loss_name: str, config: Dict, - extra_dir: Optional[str] = None) -> IHyperOptLoss: + @staticmethod + def _load_hyperoptloss(hyper_loss_name: str, config: Dict, + extra_dir: Optional[str] = None) -> IHyperOptLoss: """ Search and loads the specified hyperopt loss class. :param hyper_loss_name: name of the module to import @@ -112,11 +113,12 @@ class HyperOptLossResolver(IResolver): """ current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() - abs_paths = self.build_search_paths(config, current_path=current_path, - user_subdir=USERPATH_HYPEROPTS, extra_dir=extra_dir) + abs_paths = IResolver.build_search_paths(config, current_path=current_path, + user_subdir=USERPATH_HYPEROPTS, + extra_dir=extra_dir) - hyperoptloss = self._load_object(paths=abs_paths, object_type=IHyperOptLoss, - object_name=hyper_loss_name) + hyperoptloss = IResolver._load_object(paths=abs_paths, object_type=IHyperOptLoss, + object_name=hyper_loss_name) if hyperoptloss: return hyperoptloss diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 37de32ab0..9c6e73c53 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -198,7 +198,7 @@ def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None: 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver._load_hyperoptloss', MagicMock(return_value=hl) ) - x = HyperOptLossResolver(default_conf).hyperoptloss + x = HyperOptLossResolver.load_hyperoptloss(default_conf) assert hasattr(x, "hyperopt_loss_function") @@ -206,7 +206,7 @@ def test_hyperoptlossresolver_wrongname(mocker, default_conf, caplog) -> None: default_conf.update({'hyperopt_loss': "NonExistingLossClass"}) with pytest.raises(OperationalException, match=r'Impossible to load HyperoptLoss.*'): - HyperOptLossResolver(default_conf).hyperopt + HyperOptLossResolver.load_hyperoptloss(default_conf) def test_start_not_installed(mocker, default_conf, caplog, import_fails) -> None: @@ -286,7 +286,7 @@ def test_start_filelock(mocker, default_conf, caplog) -> None: def test_loss_calculation_prefer_correct_trade_count(default_conf, hyperopt_results) -> None: - hl = HyperOptLossResolver(default_conf).hyperoptloss + hl = HyperOptLossResolver.load_hyperoptloss(default_conf) correct = hl.hyperopt_loss_function(hyperopt_results, 600) over = hl.hyperopt_loss_function(hyperopt_results, 600 + 100) under = hl.hyperopt_loss_function(hyperopt_results, 600 - 100) @@ -298,7 +298,7 @@ def test_loss_calculation_prefer_shorter_trades(default_conf, hyperopt_results) resultsb = hyperopt_results.copy() resultsb.loc[1, 'trade_duration'] = 20 - hl = HyperOptLossResolver(default_conf).hyperoptloss + hl = HyperOptLossResolver.load_hyperoptloss(default_conf) longer = hl.hyperopt_loss_function(hyperopt_results, 100) shorter = hl.hyperopt_loss_function(resultsb, 100) assert shorter < longer @@ -310,7 +310,7 @@ def test_loss_calculation_has_limited_profit(default_conf, hyperopt_results) -> results_under = hyperopt_results.copy() results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 - hl = HyperOptLossResolver(default_conf).hyperoptloss + hl = HyperOptLossResolver.load_hyperoptloss(default_conf) correct = hl.hyperopt_loss_function(hyperopt_results, 600) over = hl.hyperopt_loss_function(results_over, 600) under = hl.hyperopt_loss_function(results_under, 600) @@ -325,7 +325,7 @@ def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> N results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 default_conf.update({'hyperopt_loss': 'SharpeHyperOptLoss'}) - hl = HyperOptLossResolver(default_conf).hyperoptloss + 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), @@ -343,7 +343,7 @@ def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2 default_conf.update({'hyperopt_loss': 'OnlyProfitHyperOptLoss'}) - hl = HyperOptLossResolver(default_conf).hyperoptloss + 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), From c6d22339788e9686552d171bf19cb00d41f7cb0a Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Dec 2019 10:23:48 +0100 Subject: [PATCH 111/324] Convert StrategyLoader to static loader --- docs/strategy_analysis_example.md | 6 +- freqtrade/freqtradebot.py | 2 +- freqtrade/optimize/backtesting.py | 4 +- freqtrade/optimize/edge_cli.py | 2 +- freqtrade/plot/plotting.py | 2 +- freqtrade/resolvers/strategy_resolver.py | 65 ++++---- .../templates/strategy_analysis_example.ipynb | 6 +- tests/strategy/test_strategy.py | 141 +++++++++--------- 8 files changed, 116 insertions(+), 112 deletions(-) diff --git a/docs/strategy_analysis_example.md b/docs/strategy_analysis_example.md index 9e61bda65..cc6b9805f 100644 --- a/docs/strategy_analysis_example.md +++ b/docs/strategy_analysis_example.md @@ -44,9 +44,9 @@ candles.head() ```python # Load strategy using values set above from freqtrade.resolvers import StrategyResolver -strategy = StrategyResolver({'strategy': strategy_name, - 'user_data_dir': user_data_dir, - 'strategy_path': strategy_location}).strategy +strategy = StrategyResolver.load_strategy({'strategy': strategy_name, + 'user_data_dir': user_data_dir, + 'strategy_path': strategy_location}) # Generate buy/sell signals using strategy df = strategy.analyze_ticker(candles, {'pair': pair}) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 001b7f02d..1b89cc75b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -55,7 +55,7 @@ class FreqtradeBot: self.heartbeat_interval = self.config.get('internals', {}).get('heartbeat_interval', 60) - self.strategy: IStrategy = StrategyResolver(self.config).strategy + self.strategy: IStrategy = StrategyResolver.load_strategy(self.config) # Check config consistency here since strategies can set certain options validate_config_consistency(config) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index bab997cb1..ffa112bd5 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -75,12 +75,12 @@ class Backtesting: for strat in list(self.config['strategy_list']): stratconf = deepcopy(self.config) stratconf['strategy'] = strat - self.strategylist.append(StrategyResolver(stratconf).strategy) + self.strategylist.append(StrategyResolver.load_strategy(stratconf)) validate_config_consistency(stratconf) else: # No strategy list specified, only one strategy - self.strategylist.append(StrategyResolver(self.config).strategy) + self.strategylist.append(StrategyResolver.load_strategy(self.config)) validate_config_consistency(self.config) if "ticker_interval" not in self.config: diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 3848623db..ea5cc663d 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -34,7 +34,7 @@ class EdgeCli: remove_credentials(self.config) self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT self.exchange = Exchange(self.config) - self.strategy = StrategyResolver(self.config).strategy + self.strategy = StrategyResolver.load_strategy(self.config) validate_config_consistency(self.config) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 85089af9c..7cd4ab854 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -340,7 +340,7 @@ def load_and_plot_trades(config: Dict[str, Any]): - Generate plot files :return: None """ - strategy = StrategyResolver(config).strategy + strategy = StrategyResolver.load_strategy(config) plot_elements = init_plotscript(config) trades = plot_elements['trades'] diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index a2d14fbf3..6d3fe5ff9 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -20,12 +20,11 @@ logger = logging.getLogger(__name__) class StrategyResolver(IResolver): """ - This class contains all the logic to load custom strategy class + This class contains the logic to load custom strategy class """ - __slots__ = ['strategy'] - - def __init__(self, config: Optional[Dict] = None) -> None: + @staticmethod + def load_strategy(config: Optional[Dict] = None) -> IStrategy: """ Load the custom class from config parameter :param config: configuration dictionary or None @@ -37,9 +36,9 @@ class StrategyResolver(IResolver): "the strategy class to use.") strategy_name = config['strategy'] - self.strategy: IStrategy = self._load_strategy(strategy_name, - config=config, - extra_dir=config.get('strategy_path')) + strategy: IStrategy = StrategyResolver._load_strategy( + strategy_name, config=config, + extra_dir=config.get('strategy_path')) # make sure ask_strategy dict is available if 'ask_strategy' not in config: @@ -68,9 +67,11 @@ class StrategyResolver(IResolver): ] for attribute, default, ask_strategy in attributes: if ask_strategy: - self._override_attribute_helper(config['ask_strategy'], attribute, default) + StrategyResolver._override_attribute_helper(strategy, config['ask_strategy'], + attribute, default) else: - self._override_attribute_helper(config, attribute, default) + StrategyResolver._override_attribute_helper(strategy, config, + attribute, default) # Loop this list again to have output combined for attribute, _, exp in attributes: @@ -80,14 +81,16 @@ class StrategyResolver(IResolver): logger.info("Strategy using %s: %s", attribute, config[attribute]) # Sort and apply type conversions - self.strategy.minimal_roi = OrderedDict(sorted( - {int(key): value for (key, value) in self.strategy.minimal_roi.items()}.items(), + strategy.minimal_roi = OrderedDict(sorted( + {int(key): value for (key, value) in strategy.minimal_roi.items()}.items(), key=lambda t: t[0])) - self.strategy.stoploss = float(self.strategy.stoploss) + strategy.stoploss = float(strategy.stoploss) - self._strategy_sanity_validations() + StrategyResolver._strategy_sanity_validations(strategy) + return strategy - def _override_attribute_helper(self, config, attribute: str, default): + @staticmethod + def _override_attribute_helper(strategy, config, attribute: str, default): """ Override attributes in the strategy. Prevalence: @@ -96,30 +99,32 @@ class StrategyResolver(IResolver): - default (if not None) """ if attribute in config: - setattr(self.strategy, attribute, config[attribute]) + setattr(strategy, attribute, config[attribute]) logger.info("Override strategy '%s' with value in config file: %s.", attribute, config[attribute]) - elif hasattr(self.strategy, attribute): - val = getattr(self.strategy, attribute) + elif hasattr(strategy, attribute): + val = getattr(strategy, attribute) # None's cannot exist in the config, so do not copy them if val is not None: config[attribute] = val # Explicitly check for None here as other "falsy" values are possible elif default is not None: - setattr(self.strategy, attribute, default) + setattr(strategy, attribute, default) config[attribute] = default - def _strategy_sanity_validations(self): - if not all(k in self.strategy.order_types for k in constants.REQUIRED_ORDERTYPES): - raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. " + @staticmethod + def _strategy_sanity_validations(strategy): + if not all(k in strategy.order_types for k in constants.REQUIRED_ORDERTYPES): + raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. " f"Order-types mapping is incomplete.") - if not all(k in self.strategy.order_time_in_force for k in constants.REQUIRED_ORDERTIF): - raise ImportError(f"Impossible to load Strategy '{self.strategy.__class__.__name__}'. " + if not all(k in strategy.order_time_in_force for k in constants.REQUIRED_ORDERTIF): + raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. " f"Order-time-in-force mapping is incomplete.") - def _load_strategy( - self, strategy_name: str, config: dict, extra_dir: Optional[str] = None) -> IStrategy: + @staticmethod + def _load_strategy(strategy_name: str, + config: dict, extra_dir: Optional[str] = None) -> IStrategy: """ Search and loads the specified strategy. :param strategy_name: name of the module to import @@ -129,9 +134,9 @@ class StrategyResolver(IResolver): """ current_path = Path(__file__).parent.parent.joinpath('strategy').resolve() - abs_paths = self.build_search_paths(config, current_path=current_path, - user_subdir=constants.USERPATH_STRATEGY, - extra_dir=extra_dir) + abs_paths = IResolver.build_search_paths(config, current_path=current_path, + user_subdir=constants.USERPATH_STRATEGY, + extra_dir=extra_dir) if ":" in strategy_name: logger.info("loading base64 encoded strategy") @@ -149,8 +154,8 @@ class StrategyResolver(IResolver): # register temp path with the bot abs_paths.insert(0, temp.resolve()) - strategy = self._load_object(paths=abs_paths, object_type=IStrategy, - object_name=strategy_name, kwargs={'config': config}) + strategy = IResolver._load_object(paths=abs_paths, object_type=IStrategy, + object_name=strategy_name, 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/freqtrade/templates/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb index 2876ea938..eea8fb85f 100644 --- a/freqtrade/templates/strategy_analysis_example.ipynb +++ b/freqtrade/templates/strategy_analysis_example.ipynb @@ -73,9 +73,9 @@ "source": [ "# Load strategy using values set above\n", "from freqtrade.resolvers import StrategyResolver\n", - "strategy = StrategyResolver({'strategy': strategy_name,\n", - " 'user_data_dir': user_data_dir,\n", - " 'strategy_path': strategy_location}).strategy\n", + "strategy = StrategyResolver.load_strategy({'strategy': strategy_name,\n", + " 'user_data_dir': user_data_dir,\n", + " 'strategy_path': strategy_location})\n", "\n", "# Generate buy/sell signals using strategy\n", "df = strategy.analyze_ticker(candles, {'pair': pair})\n", diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py index 963d36c76..116eec56b 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -39,8 +39,8 @@ def test_load_strategy(default_conf, result): default_conf.update({'strategy': 'SampleStrategy', 'strategy_path': str(Path(__file__).parents[2] / 'freqtrade/templates') }) - resolver = StrategyResolver(default_conf) - assert 'rsi' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) + strategy = StrategyResolver.load_strategy(default_conf) + assert 'rsi' in strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) def test_load_strategy_base64(result, caplog, default_conf): @@ -48,8 +48,8 @@ def test_load_strategy_base64(result, caplog, default_conf): encoded_string = urlsafe_b64encode(file.read()).decode("utf-8") default_conf.update({'strategy': 'SampleStrategy:{}'.format(encoded_string)}) - resolver = StrategyResolver(default_conf) - assert 'rsi' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) + strategy = StrategyResolver.load_strategy(default_conf) + assert 'rsi' in strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) # Make sure strategy was loaded from base64 (using temp directory)!! assert log_has_re(r"Using resolved strategy SampleStrategy from '" r".*(/|\\).*(/|\\)SampleStrategy\.py'\.\.\.", caplog) @@ -57,13 +57,12 @@ def test_load_strategy_base64(result, caplog, default_conf): def test_load_strategy_invalid_directory(result, caplog, default_conf): default_conf['strategy'] = 'DefaultStrategy' - resolver = StrategyResolver(default_conf) extra_dir = Path.cwd() / 'some/path' - resolver._load_strategy('DefaultStrategy', config=default_conf, extra_dir=extra_dir) + strategy = StrategyResolver._load_strategy('DefaultStrategy', config=default_conf, extra_dir=extra_dir) assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog) - assert 'rsi' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) + assert 'rsi' in strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) def test_load_not_found_strategy(default_conf): @@ -71,7 +70,7 @@ def test_load_not_found_strategy(default_conf): with pytest.raises(OperationalException, match=r"Impossible to load Strategy 'NotFoundStrategy'. " r"This class does not exist or contains Python code errors."): - StrategyResolver(default_conf) + StrategyResolver.load_strategy(default_conf) def test_load_strategy_noname(default_conf): @@ -79,30 +78,30 @@ def test_load_strategy_noname(default_conf): with pytest.raises(OperationalException, match="No strategy set. Please use `--strategy` to specify " "the strategy class to use."): - StrategyResolver(default_conf) + StrategyResolver.load_strategy(default_conf) def test_strategy(result, default_conf): default_conf.update({'strategy': 'DefaultStrategy'}) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) metadata = {'pair': 'ETH/BTC'} - assert resolver.strategy.minimal_roi[0] == 0.04 + assert strategy.minimal_roi[0] == 0.04 assert default_conf["minimal_roi"]['0'] == 0.04 - assert resolver.strategy.stoploss == -0.10 + assert strategy.stoploss == -0.10 assert default_conf['stoploss'] == -0.10 - assert resolver.strategy.ticker_interval == '5m' + assert strategy.ticker_interval == '5m' assert default_conf['ticker_interval'] == '5m' - df_indicators = resolver.strategy.advise_indicators(result, metadata=metadata) + df_indicators = strategy.advise_indicators(result, metadata=metadata) assert 'adx' in df_indicators - dataframe = resolver.strategy.advise_buy(df_indicators, metadata=metadata) + dataframe = strategy.advise_buy(df_indicators, metadata=metadata) assert 'buy' in dataframe.columns - dataframe = resolver.strategy.advise_sell(df_indicators, metadata=metadata) + dataframe = strategy.advise_sell(df_indicators, metadata=metadata) assert 'sell' in dataframe.columns @@ -114,9 +113,9 @@ def test_strategy_override_minimal_roi(caplog, default_conf): "0": 0.5 } }) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) - assert resolver.strategy.minimal_roi[0] == 0.5 + assert strategy.minimal_roi[0] == 0.5 assert log_has("Override strategy 'minimal_roi' with value in config file: {'0': 0.5}.", caplog) @@ -126,9 +125,9 @@ def test_strategy_override_stoploss(caplog, default_conf): 'strategy': 'DefaultStrategy', 'stoploss': -0.5 }) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) - assert resolver.strategy.stoploss == -0.5 + assert strategy.stoploss == -0.5 assert log_has("Override strategy 'stoploss' with value in config file: -0.5.", caplog) @@ -138,10 +137,10 @@ def test_strategy_override_trailing_stop(caplog, default_conf): 'strategy': 'DefaultStrategy', 'trailing_stop': True }) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) - assert resolver.strategy.trailing_stop - assert isinstance(resolver.strategy.trailing_stop, bool) + assert strategy.trailing_stop + assert isinstance(strategy.trailing_stop, bool) assert log_has("Override strategy 'trailing_stop' with value in config file: True.", caplog) @@ -153,13 +152,13 @@ def test_strategy_override_trailing_stop_positive(caplog, default_conf): 'trailing_stop_positive_offset': -0.2 }) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) - assert resolver.strategy.trailing_stop_positive == -0.1 + assert strategy.trailing_stop_positive == -0.1 assert log_has("Override strategy 'trailing_stop_positive' with value in config file: -0.1.", caplog) - assert resolver.strategy.trailing_stop_positive_offset == -0.2 + assert strategy.trailing_stop_positive_offset == -0.2 assert log_has("Override strategy 'trailing_stop_positive' with value in config file: -0.1.", caplog) @@ -172,10 +171,10 @@ def test_strategy_override_ticker_interval(caplog, default_conf): 'ticker_interval': 60, 'stake_currency': 'ETH' }) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) - assert resolver.strategy.ticker_interval == 60 - assert resolver.strategy.stake_currency == 'ETH' + assert strategy.ticker_interval == 60 + assert strategy.stake_currency == 'ETH' assert log_has("Override strategy 'ticker_interval' with value in config file: 60.", caplog) @@ -187,9 +186,9 @@ def test_strategy_override_process_only_new_candles(caplog, default_conf): 'strategy': 'DefaultStrategy', 'process_only_new_candles': True }) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) - assert resolver.strategy.process_only_new_candles + assert strategy.process_only_new_candles assert log_has("Override strategy 'process_only_new_candles' with value in config file: True.", caplog) @@ -207,11 +206,11 @@ def test_strategy_override_order_types(caplog, default_conf): 'strategy': 'DefaultStrategy', 'order_types': order_types }) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) - assert resolver.strategy.order_types + assert strategy.order_types for method in ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']: - assert resolver.strategy.order_types[method] == order_types[method] + assert strategy.order_types[method] == order_types[method] assert log_has("Override strategy 'order_types' with value in config file:" " {'buy': 'market', 'sell': 'limit', 'stoploss': 'limit'," @@ -225,7 +224,7 @@ def test_strategy_override_order_types(caplog, default_conf): with pytest.raises(ImportError, match=r"Impossible to load Strategy 'DefaultStrategy'. " r"Order-types mapping is incomplete."): - StrategyResolver(default_conf) + StrategyResolver.load_strategy(default_conf) def test_strategy_override_order_tif(caplog, default_conf): @@ -240,11 +239,11 @@ def test_strategy_override_order_tif(caplog, default_conf): 'strategy': 'DefaultStrategy', 'order_time_in_force': order_time_in_force }) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) - assert resolver.strategy.order_time_in_force + assert strategy.order_time_in_force for method in ['buy', 'sell']: - assert resolver.strategy.order_time_in_force[method] == order_time_in_force[method] + assert strategy.order_time_in_force[method] == order_time_in_force[method] assert log_has("Override strategy 'order_time_in_force' with value in config file:" " {'buy': 'fok', 'sell': 'gtc'}.", caplog) @@ -257,7 +256,7 @@ def test_strategy_override_order_tif(caplog, default_conf): with pytest.raises(ImportError, match=r"Impossible to load Strategy 'DefaultStrategy'. " r"Order-time-in-force mapping is incomplete."): - StrategyResolver(default_conf) + StrategyResolver.load_strategy(default_conf) def test_strategy_override_use_sell_signal(caplog, default_conf): @@ -265,9 +264,9 @@ def test_strategy_override_use_sell_signal(caplog, default_conf): default_conf.update({ 'strategy': 'DefaultStrategy', }) - resolver = StrategyResolver(default_conf) - assert resolver.strategy.use_sell_signal - assert isinstance(resolver.strategy.use_sell_signal, bool) + strategy = StrategyResolver.load_strategy(default_conf) + assert strategy.use_sell_signal + assert isinstance(strategy.use_sell_signal, bool) # must be inserted to configuration assert 'use_sell_signal' in default_conf['ask_strategy'] assert default_conf['ask_strategy']['use_sell_signal'] @@ -278,10 +277,10 @@ def test_strategy_override_use_sell_signal(caplog, default_conf): 'use_sell_signal': False, }, }) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) - assert not resolver.strategy.use_sell_signal - assert isinstance(resolver.strategy.use_sell_signal, bool) + assert not strategy.use_sell_signal + assert isinstance(strategy.use_sell_signal, bool) assert log_has("Override strategy 'use_sell_signal' with value in config file: False.", caplog) @@ -290,9 +289,9 @@ def test_strategy_override_use_sell_profit_only(caplog, default_conf): default_conf.update({ 'strategy': 'DefaultStrategy', }) - resolver = StrategyResolver(default_conf) - assert not resolver.strategy.sell_profit_only - assert isinstance(resolver.strategy.sell_profit_only, bool) + strategy = StrategyResolver.load_strategy(default_conf) + assert not strategy.sell_profit_only + assert isinstance(strategy.sell_profit_only, bool) # must be inserted to configuration assert 'sell_profit_only' in default_conf['ask_strategy'] assert not default_conf['ask_strategy']['sell_profit_only'] @@ -303,10 +302,10 @@ def test_strategy_override_use_sell_profit_only(caplog, default_conf): 'sell_profit_only': True, }, }) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) - assert resolver.strategy.sell_profit_only - assert isinstance(resolver.strategy.sell_profit_only, bool) + assert strategy.sell_profit_only + assert isinstance(strategy.sell_profit_only, bool) assert log_has("Override strategy 'sell_profit_only' with value in config file: True.", caplog) @@ -315,11 +314,11 @@ def test_deprecate_populate_indicators(result, default_conf): default_location = path.join(path.dirname(path.realpath(__file__))) default_conf.update({'strategy': 'TestStrategyLegacy', 'strategy_path': default_location}) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") - indicators = resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) + indicators = strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "deprecated - check out the Sample strategy to see the current function headers!" \ @@ -328,7 +327,7 @@ def test_deprecate_populate_indicators(result, default_conf): with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") - resolver.strategy.advise_buy(indicators, {'pair': 'ETH/BTC'}) + strategy.advise_buy(indicators, {'pair': 'ETH/BTC'}) assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "deprecated - check out the Sample strategy to see the current function headers!" \ @@ -337,7 +336,7 @@ def test_deprecate_populate_indicators(result, default_conf): with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. warnings.simplefilter("always") - resolver.strategy.advise_sell(indicators, {'pair': 'ETH_BTC'}) + strategy.advise_sell(indicators, {'pair': 'ETH_BTC'}) assert len(w) == 1 assert issubclass(w[-1].category, DeprecationWarning) assert "deprecated - check out the Sample strategy to see the current function headers!" \ @@ -349,47 +348,47 @@ def test_call_deprecated_function(result, monkeypatch, default_conf): default_location = path.join(path.dirname(path.realpath(__file__))) default_conf.update({'strategy': 'TestStrategyLegacy', 'strategy_path': default_location}) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) metadata = {'pair': 'ETH/BTC'} # Make sure we are using a legacy function - assert resolver.strategy._populate_fun_len == 2 - assert resolver.strategy._buy_fun_len == 2 - assert resolver.strategy._sell_fun_len == 2 - assert resolver.strategy.INTERFACE_VERSION == 1 + assert strategy._populate_fun_len == 2 + assert strategy._buy_fun_len == 2 + assert strategy._sell_fun_len == 2 + assert strategy.INTERFACE_VERSION == 1 - indicator_df = resolver.strategy.advise_indicators(result, metadata=metadata) + indicator_df = strategy.advise_indicators(result, metadata=metadata) assert isinstance(indicator_df, DataFrame) assert 'adx' in indicator_df.columns - buydf = resolver.strategy.advise_buy(result, metadata=metadata) + buydf = strategy.advise_buy(result, metadata=metadata) assert isinstance(buydf, DataFrame) assert 'buy' in buydf.columns - selldf = resolver.strategy.advise_sell(result, metadata=metadata) + selldf = strategy.advise_sell(result, metadata=metadata) assert isinstance(selldf, DataFrame) assert 'sell' in selldf def test_strategy_interface_versioning(result, monkeypatch, default_conf): default_conf.update({'strategy': 'DefaultStrategy'}) - resolver = StrategyResolver(default_conf) + strategy = StrategyResolver.load_strategy(default_conf) metadata = {'pair': 'ETH/BTC'} # Make sure we are using a legacy function - assert resolver.strategy._populate_fun_len == 3 - assert resolver.strategy._buy_fun_len == 3 - assert resolver.strategy._sell_fun_len == 3 - assert resolver.strategy.INTERFACE_VERSION == 2 + assert strategy._populate_fun_len == 3 + assert strategy._buy_fun_len == 3 + assert strategy._sell_fun_len == 3 + assert strategy.INTERFACE_VERSION == 2 - indicator_df = resolver.strategy.advise_indicators(result, metadata=metadata) + indicator_df = strategy.advise_indicators(result, metadata=metadata) assert isinstance(indicator_df, DataFrame) assert 'adx' in indicator_df.columns - buydf = resolver.strategy.advise_buy(result, metadata=metadata) + buydf = strategy.advise_buy(result, metadata=metadata) assert isinstance(buydf, DataFrame) assert 'buy' in buydf.columns - selldf = resolver.strategy.advise_sell(result, metadata=metadata) + selldf = strategy.advise_sell(result, metadata=metadata) assert isinstance(selldf, DataFrame) assert 'sell' in selldf From 90cabd7c21306f6f7fda068e40936e7f005ca825 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Dec 2019 10:46:35 +0100 Subject: [PATCH 112/324] Wrap line --- tests/strategy/test_strategy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py index 116eec56b..ce7ac1741 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -58,7 +58,8 @@ def test_load_strategy_base64(result, caplog, default_conf): def test_load_strategy_invalid_directory(result, caplog, default_conf): default_conf['strategy'] = 'DefaultStrategy' extra_dir = Path.cwd() / 'some/path' - strategy = StrategyResolver._load_strategy('DefaultStrategy', config=default_conf, extra_dir=extra_dir) + strategy = StrategyResolver._load_strategy('DefaultStrategy', config=default_conf, + extra_dir=extra_dir) assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog) From bb8acc61db3685f09e34169c0e4f8bb17fac8588 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Dec 2019 15:09:17 +0100 Subject: [PATCH 113/324] Convert datadir within config to Path (it's used as Path all the time!) --- freqtrade/configuration/configuration.py | 2 +- freqtrade/configuration/directory_operations.py | 4 ++-- freqtrade/data/dataprovider.py | 3 +-- freqtrade/utils.py | 9 ++++----- tests/test_configuration.py | 2 +- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 001e89303..f73b52c10 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -403,7 +403,7 @@ class Configuration: config['pairs'] = config.get('exchange', {}).get('pair_whitelist') else: # Fall back to /dl_path/pairs.json - pairs_file = Path(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/configuration/directory_operations.py b/freqtrade/configuration/directory_operations.py index 3dd76a025..556f27742 100644 --- a/freqtrade/configuration/directory_operations.py +++ b/freqtrade/configuration/directory_operations.py @@ -9,7 +9,7 @@ from freqtrade.constants import USER_DATA_FILES logger = logging.getLogger(__name__) -def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> str: +def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> Path: folder = Path(datadir) if datadir else Path(f"{config['user_data_dir']}/data") if not datadir: @@ -20,7 +20,7 @@ def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> str if not folder.is_dir(): folder.mkdir(parents=True) logger.info(f'Created data directory: {datadir}') - return str(folder) + return folder def create_userdata_dir(directory: str, create_dir=False) -> Path: diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 7b7159145..2964d1cb7 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -5,7 +5,6 @@ including Klines, tickers, historic data Common Interface for bot and strategy to access data. """ import logging -from pathlib import Path from typing import Any, Dict, List, Optional, Tuple from pandas import DataFrame @@ -65,7 +64,7 @@ class DataProvider: """ return load_pair_history(pair=pair, timeframe=timeframe or self._config['ticker_interval'], - datadir=Path(self._config['datadir']) + datadir=self._config['datadir'] ) def get_pair_dataframe(self, pair: str, timeframe: str = None) -> DataFrame: diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 9e01c7ea6..2ae9e1ecb 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -191,9 +191,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.") - dl_path = Path(config['datadir']) logger.info(f'About to download pairs: {config["pairs"]}, ' - f'intervals: {config["timeframes"]} to {dl_path}') + f'intervals: {config["timeframes"]} to {config["datadir"]}') pairs_not_available: List[str] = [] @@ -203,17 +202,17 @@ 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=Path(config['datadir']), + exchange, pairs=config["pairs"], datadir=config['datadir'], timerange=timerange, erase=config.get("erase")) # Convert downloaded trade data to different timeframes convert_trades_to_ohlcv( pairs=config["pairs"], timeframes=config["timeframes"], - datadir=Path(config['datadir']), timerange=timerange, erase=config.get("erase")) + datadir=config['datadir'], timerange=timerange, erase=config.get("erase")) else: pairs_not_available = refresh_backtest_ohlcv_data( exchange, pairs=config["pairs"], timeframes=config["timeframes"], - datadir=Path(config['datadir']), timerange=timerange, erase=config.get("erase")) + datadir=config['datadir'], timerange=timerange, erase=config.get("erase")) except KeyboardInterrupt: sys.exit("SIGINT received, aborting ...") diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 292d53315..6564417b3 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -977,7 +977,7 @@ def test_pairlist_resolving_fallback(mocker): assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] assert config['exchange']['name'] == 'binance' - assert config['datadir'] == str(Path.cwd() / "user_data/data/binance") + assert config['datadir'] == Path.cwd() / "user_data/data/binance" @pytest.mark.parametrize("setting", [ From ecbb77c17fa3130e2d3dbef2044a7c2d8447685b Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Dec 2019 15:13:55 +0100 Subject: [PATCH 114/324] Add forgotten option --- 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 85089af9c..62081194f 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -37,7 +37,7 @@ def init_plotscript(config): timerange = TimeRange.parse_timerange(config.get("timerange")) tickers = history.load_data( - datadir=Path(str(config.get("datadir"))), + datadir=config.get("datadir"), pairs=pairs, timeframe=config.get('ticker_interval', '5m'), timerange=timerange, From c6b9c8eca0722305f6d450cf7d9cada9cae216ea Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Dec 2019 19:32:31 +0100 Subject: [PATCH 115/324] Forgot to save --- freqtrade/edge/__init__.py | 4 ++-- freqtrade/optimize/backtesting.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index e56071a98..e1dfe8e25 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -96,7 +96,7 @@ class Edge: if self._refresh_pairs: history.refresh_data( - datadir=Path(self.config['datadir']), + datadir=self.config['datadir'], pairs=pairs, exchange=self.exchange, timeframe=self.strategy.ticker_interval, @@ -104,7 +104,7 @@ class Edge: ) data = history.load_data( - datadir=Path(self.config['datadir']), + datadir=self.config['datadir'], pairs=pairs, timeframe=self.strategy.ticker_interval, timerange=self._timerange, diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 726257cdd..630cd106f 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -109,7 +109,7 @@ class Backtesting: 'timerange') is None else str(self.config.get('timerange'))) data = history.load_data( - datadir=Path(self.config['datadir']), + datadir=self.config['datadir'], pairs=self.config['exchange']['pair_whitelist'], timeframe=self.timeframe, timerange=timerange, From 0ac5e5035c499e07569f63dca45ac11938a601c3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 23 Dec 2019 20:43:31 +0100 Subject: [PATCH 116/324] Remove unused import --- freqtrade/edge/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index e1dfe8e25..9ad2485ef 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -1,7 +1,6 @@ # pragma pylint: disable=W0603 """ Edge positioning package """ import logging -from pathlib import Path from typing import Any, Dict, NamedTuple import arrow From 690eb2a52b1883e3faff22a29a2f68dcaad4aa2f Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Tue, 24 Dec 2019 07:19:35 +0300 Subject: [PATCH 117/324] configuration.md reviewed --- docs/configuration.md | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 8ae824277..b3292ee74 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -388,48 +388,51 @@ The valid values are: ## Prices used for orders -Prices for regular orders can be controlled via the parameter structures `bid_strategy` for Buying, and `ask_strategy` for selling. -Prices are always retrieved right before an order is placed, either by querying the `fetch_ticker()` endpoint of the exchange (usually `/ticker`), or by using the orderbook. +Prices for regular orders can be controlled via the parameter structures `bid_strategy` for buying and `ask_strategy` for selling. +Prices are always retrieved right before an order is placed, either by querying the exchange tickers or by using the orderbook data. + +!!! 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. ### Buy price #### Check depth of market -When enabling `bid_strategy.check_depth_of_market.enabled=True`, then buy signals will be filtered based on the orderbook size for each size (sum of all amounts). -Orderbook bid size is then divided by Orderbook ask size - and the resulting delta is compared to `bid_strategy.check_depth_of_market.bids_to_ask_delta`, and a buy is only executed if the orderbook delta is bigger or equal to the configured delta. +When check depth of market is enabled (i.e. `bid_strategy.check_depth_of_market.enabled=True`), the buy signals are filtered based on the orderbook depth (sum of all amounts) for each orderbook side. + +Orderbook `bid` (buy) side depth is then divided by the orderbook `ask` (sell) side depth and the resulting delta is compared to the value of the `bid_strategy.check_depth_of_market.bids_to_ask_delta` parameter. The buy order is only executed if the orderbook delta is greater than or equal to the configured delta value. !!! Note - A calculated delta below 1 means that sell order size is greater, while value greater than 1 means buy order size is higher + A delta value below 1 means that `ask` (sell) orderbook side depth is greater than the depth of the `bid` (buy) orderbook side, while a value greater than 1 means opposite (depth of the buy side is higher than the depth of the sell side). #### Buy price with Orderbook enabled -When buying with the orderbook enabled (`bid_strategy.use_order_book=True`) - Freqtrade will fetch the `bid_strategy.order_book_top` entries in the orderbook, and will then use the entry specified as `bid_strategy.order_book_top` on the `bids` side of the orderbook. 1 specifies the topmost entry in the Orderbook - while 2 would use the 2nd entry in the Orderbook. +When buying with the orderbook enabled (`bid_strategy.use_order_book=True`), Freqtrade fetches the `bid_strategy.order_book_top` entries from the orderbook and then uses the entry specified as `bid_strategy.order_book_top` on the `bid` (buy) side of the orderbook. 1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on. -#### Buy price without Orderbook +#### Buy price without Orderbook enabled -When not using orderbook (`bid_strategy.use_order_book=False`), then Freqtrade will use the best `ask` price based on a call to `fetch_ticker()` if it's below the `last` traded price. -Otherwise, it'll calculate a rate between `ask` and `last` price. +When not using orderbook (`bid_strategy.use_order_book=False`), Freqtrade uses the best `ask` (sell) price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `ask` price is not below the `last` price), it calculates a rate between `ask` and `last` price. -The `bid_strategy.ask_last_balance` configuration parameter controls this. A value of `0.0` will use `ask` price, `1.0` will use the `last` price and values between those interpolate between ask and last -price. -Using `ask` price will guarantee quick success in bid, but bot will also end up paying more than what would have been necessary. +The `bid_strategy.ask_last_balance` configuration parameter controls this. A value of `0.0` will use `ask` price, while `1.0` will use the `last` price and values between those interpolate between ask and last price. + +Using `ask` price often guarantees quicker success in the bid, but the bot can also end up paying more than what would have been necessary. ### Sell price #### Sell price with Orderbook enabled -When selling with the Orderbook enabled (`ask_strategy.use_order_book=True`) - Freqtrade will fetch the `ask_strategy.order_book_max` entries in the orderbook. Freqtrade will then validate each of the orderbook steps between `ask_strategy.order_book_min` and `ask_strategy.order_book_max` on the `ask` side for a profitable sell-possibility based on the strategy configuration and will place a sell order at the first profitable spot. +When selling with the orderbook enabled (`ask_strategy.use_order_book=True`), Freqtrade fetches the `ask_strategy.order_book_max` entries in the orderbook. Then each of the orderbook steps between `ask_strategy.order_book_min` and `ask_strategy.order_book_max` on the `ask` orderbook side are validated for a profitable sell-possibility based on the strategy configuration and the sell order is placed at the first profitable spot. + +The idea here is to place the sell order early, to be ahead in the queue. -The idea here is to place the sell-order early, to be ahead in the queue. A fixed slot (mirroring `bid_strategy.order_book_top`) can be defined by setting `ask_strategy.order_book_min` and `ask_strategy.order_book_max` to the same number. !!! Warning "Orderbook and stoploss_on_exchange" - Using `ask_strategy.order_book_max` higher than 1 may increase the risk, since an eventual [stoploss on exchange](#understand-order_types) will be need to be cancelled as soon as the order is placed. + Using `ask_strategy.order_book_max` higher than 1 may increase the risk, since an eventual [stoploss on exchange](#understand-order_types) will be needed to be cancelled as soon as the order is placed. +#### Sell price without Orderbook enabled -#### Sell price without orderbook - -When not using orderbook (`ask_strategy.use_order_book=False`), then the best `bid` will be used as sell rate based on a call to `fetch_ticker()`. +When not using orderbook (`ask_strategy.use_order_book=False`), the `bid` price from the ticker will be used as the sell price. ## Pairlists From f487dac047bfb1d61a93c5f45d9a08ddfcdab5bc Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Dec 2019 06:27:11 +0100 Subject: [PATCH 118/324] FIx bug in dry-run wallets causing balances to stay there after trades are sold --- freqtrade/wallets.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index dd706438f..54c3f9138 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -58,13 +58,15 @@ class Wallets: - Subtract currently tied up stake_amount in open trades - update balances for currencies currently in trades """ + # Recreate _wallets to reset closed trade balances + _wallets = {} closed_trades = Trade.get_trades(Trade.is_open.is_(False)).all() open_trades = Trade.get_trades(Trade.is_open.is_(True)).all() tot_profit = sum([trade.calc_profit() for trade in closed_trades]) tot_in_trades = sum([trade.stake_amount for trade in open_trades]) current_stake = self.start_cap + tot_profit - tot_in_trades - self._wallets[self._config['stake_currency']] = Wallet( + _wallets[self._config['stake_currency']] = Wallet( self._config['stake_currency'], current_stake, 0, @@ -73,12 +75,13 @@ class Wallets: for trade in open_trades: curr = trade.pair.split('/')[0] - self._wallets[curr] = Wallet( + _wallets[curr] = Wallet( curr, trade.amount, 0, trade.amount ) + self._wallets = _wallets def _update_live(self) -> None: From 33cfeaf9b01a51fc16d3dd9a437a6ffc5c670955 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Dec 2019 06:31:05 +0100 Subject: [PATCH 119/324] Remove i.e. where it doesn't fit --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index b3292ee74..b1b03c721 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -398,7 +398,7 @@ Prices are always retrieved right before an order is placed, either by querying #### Check depth of market -When check depth of market is enabled (i.e. `bid_strategy.check_depth_of_market.enabled=True`), the buy signals are filtered based on the orderbook depth (sum of all amounts) for each orderbook side. +When check depth of market is enabled (`bid_strategy.check_depth_of_market.enabled=True`), the buy signals are filtered based on the orderbook depth (sum of all amounts) for each orderbook side. Orderbook `bid` (buy) side depth is then divided by the orderbook `ask` (sell) side depth and the resulting delta is compared to the value of the `bid_strategy.check_depth_of_market.bids_to_ask_delta` parameter. The buy order is only executed if the orderbook delta is greater than or equal to the configured delta value. From b8442d536a41e4b93872618f38239fadcaa20f28 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Dec 2019 06:47:25 +0100 Subject: [PATCH 120/324] Update integration test to also test dry-run-wallets --- tests/test_integration.py | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index 728e96d55..c5d08ec22 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -118,12 +118,10 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc default_conf['max_open_trades'] = 5 default_conf['forcebuy_enable'] = True default_conf['stake_amount'] = 'unlimited' + default_conf['dry_run_wallet'] = 1000 default_conf['exchange']['name'] = 'binance' default_conf['telegram']['enabled'] = True mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) - mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock( - side_effect=[1000, 800, 600, 400, 200] - )) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_ticker=ticker, @@ -138,6 +136,14 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc update_trade_state=MagicMock(), _notify_sell=MagicMock(), ) + should_sell_mock = MagicMock(side_effect=[ + SellCheckTuple(sell_flag=False, sell_type=SellType.NONE), + SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL), + SellCheckTuple(sell_flag=False, sell_type=SellType.NONE), + SellCheckTuple(sell_flag=False, sell_type=SellType.NONE), + SellCheckTuple(sell_flag=None, sell_type=SellType.NONE)] + ) + mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock) freqtrade = get_patched_freqtradebot(mocker, default_conf) rpc = RPC(freqtrade) @@ -158,3 +164,20 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc for trade in trades: assert trade.stake_amount == 200 + # Reset trade open order id's + trade.open_order_id = None + trades = Trade.get_open_trades() + assert len(trades) == 5 + bals = freqtrade.wallets.get_all_balances() + + freqtrade.process_maybe_execute_sells(trades) + trades = Trade.get_open_trades() + # One trade sold + assert len(trades) == 4 + # Validate that balance of sold trade is not in dry-run balances anymore. + bals2 = freqtrade.wallets.get_all_balances() + assert bals != bals2 + assert len(bals) == 6 + assert len(bals2) == 5 + assert 'LTC' in bals + assert 'LTC' not in bals2 From a105e5664a3d166a2c641a0da8ce10fea5f55d7b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Dec 2019 06:58:30 +0100 Subject: [PATCH 121/324] Align /balance output to show everything in stake currency the conversation to BTC does not make sense --- freqtrade/rpc/rpc.py | 3 ++- freqtrade/rpc/telegram.py | 4 ++-- tests/rpc/test_rpc_telegram.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 3b4b7570a..d6d442df5 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -341,13 +341,14 @@ class RPC: raise RPCException('All balances are zero.') symbol = fiat_display_currency - value = self._fiat_converter.convert_amount(total, 'BTC', + value = self._fiat_converter.convert_amount(total, stake_currency, symbol) if self._fiat_converter else 0 return { 'currencies': output, 'total': total, 'symbol': symbol, 'value': value, + 'stake': stake_currency, 'note': 'Simulated balances' if self._freqtrade.config.get('dry_run', False) else '' } diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index e0e2afd7b..e9ecdcff6 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -335,7 +335,7 @@ class Telegram(RPC): output = '' if self._config['dry_run']: output += ( - f"*Warning:*Simulated balances in Dry Mode.\n" + f"*Warning:* Simulated balances in Dry Mode.\n" "This mode is still experimental!\n" "Starting capital: " f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" @@ -358,7 +358,7 @@ class Telegram(RPC): output += curr_output output += "\n*Estimated Value*:\n" \ - "\t`BTC: {total: .8f}`\n" \ + "\t`{stake}: {total: .8f}`\n" \ "\t`{symbol}: {value: .2f}`\n".format(**result) self._send_msg(output) except RPCException as e: diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index b02f11394..8126ab64c 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -534,7 +534,7 @@ def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 - assert "*Warning:*Simulated balances in Dry Mode." in result + assert "*Warning:* Simulated balances in Dry Mode." in result assert "Starting capital: `1000` BTC" in result From 83ed0b38c1d2fdd4305ad0c7d332ef944b24ebea Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Dec 2019 07:10:57 +0100 Subject: [PATCH 122/324] Wordwrap before keep it secret --- docs/configuration.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 43964e882..2f61343db 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -38,8 +38,8 @@ The prevelance for all Options is as follows: Mandatory parameters are marked as **Required**, which means that they are required to be set in one of the possible ways. -| Command | Description | -|----------|-------------| +| Parameter | Description | +|------------|-------------| | `max_open_trades` | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades).
***Datatype:*** *Positive integer or -1.* | `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *String* | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#understand-stake_amount). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Positive float or `"unlimited"`.* @@ -72,9 +72,9 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `order_time_in_force` | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Dict* | `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
***Datatype:*** *String* | `exchange.sandbox` | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.
***Datatype:*** *Boolean* -| `exchange.key` | API key to use for the exchange. Only required when you are in production mode. **Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* -| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode. **Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* -| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests. **Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* +| `exchange.key` | API key to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* +| `exchange.secret` | API secret to use for the exchange. Only required when you are in production mode.
**Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* +| `exchange.password` | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests.
**Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* | `exchange.pair_whitelist` | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Not used by VolumePairList (see [below](#dynamic-pairlists)).
***Datatype:*** *List* | `exchange.pair_blacklist` | List of pairs the bot must absolutely avoid for trading and backtesting (see [below](#dynamic-pairlists)).
***Datatype:*** *List* | `exchange.ccxt_config` | Additional CCXT parameters passed to the regular 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* @@ -84,8 +84,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `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](#dynamic-pairlists).
*Defaults to `StaticPairList`.*
***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* +| `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* | `webhook.enabled` | Enable usage of Webhook notifications
***Datatype:*** *Boolean* | `webhook.url` | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
***Datatype:*** *String* | `webhook.webhookbuy` | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
***Datatype:*** *String* @@ -93,9 +93,9 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `webhook.webhookstatus` | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
***Datatype:*** *String* | `api_server.enabled` | Enable usage of API Server. See the [API Server documentation](rest-api.md) for more details.
***Datatype:*** *Boolean* | `api_server.listen_ip_address` | Bind IP address. See the [API Server documentation](rest-api.md) for more details.
***Datatype:*** *IPv4* -| `api_server.listen_port` | Bind Port. See the [API Server documentation](rest-api.md) for more details.
***Datatype:*** *Integer between 1024 and 65535* -| `api_server.username` | Username for API server. See the [API Server documentation](rest-api.md) for more details. **Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* -| `api_server.password` | Password for API server. See the [API Server documentation](rest-api.md) for more details. **Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* +| `api_server.listen_port` | Bind Port. See the [API Server documentation](rest-api.md) for more details.
***Datatype:*** *Integer between 1024 and 65535* +| `api_server.username` | Username for API server. See the [API Server documentation](rest-api.md) for more details.
**Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* +| `api_server.password` | Password for API server. See the [API Server documentation](rest-api.md) for more details.
**Keep it in secret, do not disclose publicly.**
***Datatype:*** *String* | `db_url` | Declares database URL to use. NOTE: This defaults to `sqlite:///tradesv3.dryrun.sqlite` if `dry_run` is `true`, and to `sqlite:///tradesv3.sqlite` for production instances.
***Datatype:*** *String, SQLAlchemy connect string* | `initial_state` | Defines the initial application state. More information below.
*Defaults to `stopped`.*
***Datatype:*** *Enum, either `stopped` or `running`* | `forcebuy_enable` | Enables the RPC Commands to force a buy. More information below.
***Datatype:*** *Boolean* From 48935d2932ca5b3317463bf78f3dda397eaf5126 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Dec 2019 07:25:18 +0100 Subject: [PATCH 123/324] Align edge documentation to configuration page --- docs/edge.md | 153 ++++++++++++++++++--------------------------------- 1 file changed, 54 insertions(+), 99 deletions(-) diff --git a/docs/edge.md b/docs/edge.md index c7b088476..769e48927 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -1,4 +1,4 @@ -# Edge positioning +# 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. @@ -9,6 +9,7 @@ This page explains how to use Edge Positioning module in your bot in order to en Edge does not consider anything else than buy/sell/stoploss signals. So trailing stoploss, ROI, and everything else are ignored in its calculation. ## 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. 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? @@ -22,43 +23,61 @@ Let's complicate it more: you win 80% of the time but only 2$, I win 20% of the The question is: How do you calculate that? How do you know if you wanna play? The answer comes to two factors: + - Win Rate - Risk Reward Ratio ### Win Rate + 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). - W = (Number of winning trades) / (Total number of trades) = (Number of winning trades) / N +``` +W = (Number of winning trades) / (Total number of trades) = (Number of winning trades) / N +``` Complementary Loss Rate (*L*) is defined as - L = (Number of losing trades) / (Total number of trades) = (Number of losing trades) / N +``` +L = (Number of losing trades) / (Total number of trades) = (Number of losing trades) / N +``` or, which is the same, as - L = 1 – W +``` +L = 1 – W +``` ### 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: - R = Profit / Loss +``` +R = Profit / 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: - Average profit = (Sum of profits) / (Number of winning trades) +``` +Average profit = (Sum of profits) / (Number of winning trades) - Average loss = (Sum of losses) / (Number of losing trades) +Average loss = (Sum of losses) / (Number of losing trades) - R = (Average profit) / (Average loss) +R = (Average profit) / (Average loss) +``` ### 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: - Expectancy Ratio = (Risk Reward Ratio X Win Rate) – Loss Rate = (R X W) – L +``` +Expectancy Ratio = (Risk Reward Ratio X Win Rate) – Loss Rate = (R X W) – L +``` So lets say your Win rate is 28% and your Risk Reward Ratio is 5: - Expectancy = (5 X 0.28) – 0.72 = 0.68 +``` +Expectancy = (5 X 0.28) – 0.72 = 0.68 +``` Superficially, this means that on average you expect this strategy’s trades to return .68 times the size of your loses. 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. @@ -69,6 +88,7 @@ You can also use this value to evaluate the effectiveness of modifications to th **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? + 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 | @@ -83,6 +103,7 @@ The goal here is to find the best stoploss for the strategy in order to have the 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: - Allowed capital at risk @@ -90,13 +111,17 @@ Edge also dictates the stake amount for each trade to the bot according to the f Allowed capital at risk is calculated as follows: - Allowed capital at risk = (Capital available_percentage) X (Allowed risk per trade) +``` +Allowed capital at risk = (Capital available_percentage) X (Allowed risk per trade) +``` Stoploss is calculated as described above against historical data. Your position size then will be: - Position size = (Allowed capital at risk) / Stoploss +``` +Position size = (Allowed capital at risk) / Stoploss +``` Example: @@ -115,100 +140,30 @@ Available capital doesn’t change before a position is sold. Let’s assume tha 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**. ## Configurations + Edge module has following configuration options: -#### enabled -If true, then Edge will run periodically. - -(defaults to false) - -#### process_throttle_secs -How often should Edge run in seconds? - -(defaults to 3600 so one hour) - -#### calculate_since_number_of_days -Number of days of data against which Edge calculates Win Rate, Risk Reward and Expectancy -Note that it downloads historical data so increasing this number would lead to slowing down the bot. - -(defaults to 7) - -#### capital_available_percentage -This is the percentage of the total capital on exchange in stake currency. - -As an example if you have 10 ETH available in your wallet on the exchange and this value is 0.5 (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers it as available capital. - -(defaults to 0.5) - -#### allowed_risk -Percentage of allowed risk per trade. - -(defaults to 0.01 so 1%) - -#### stoploss_range_min - -Minimum stoploss. - -(defaults to -0.01) - -#### stoploss_range_max - -Maximum stoploss. - -(defaults to -0.10) - -#### 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.01) - -#### 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) - -#### 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) - -#### 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) - -#### 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 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 1 day, i.e. to 60 * 24 = 1440 minutes) - -#### 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) +| Parameter | Description | +|------------|-------------| +| `enabled` | If true, then Edge will run periodically.
*Defaults to `false`.*
***Datatype:*** *Boolean* +| `process_throttle_secs` | How often should Edge run in seconds.
*Defaults to `3600` (once per hour).*
***Datatype:*** *Integer* +| `calculate_since_number_of_days` | Number of days of data against which Edge calculates Win Rate, Risk Reward and Expectancy.
**Note** that it downloads historical data so increasing this number would lead to slowing down the bot.
*Defaults to `7` (once per hour).*
***Datatype:*** *Integer* +| `capital_available_percentage` | This is the percentage of the total capital on exchange in stake currency.
As an example if you have 10 ETH available in your wallet on the exchange and this value is 0.5 (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers it as available capital.
*Defaults to `0.5` (once per hour).*
***Datatype:*** *Float* +| `allowed_risk` | Ratio of allowed risk per trade.
*Defaults to `0.01` (1%)).*
***Datatype:*** *Float* +| `stoploss_range_min` | Minimum stoploss.
*Defaults to `-0.01`.*
***Datatype:*** *Float* +| `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* +| `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 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* +| `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 You can run Edge independently in order to see in details the result. Here is an example: -```bash +``` bash freqtrade edge ``` From a68445692bfdde1d9407a8d3bc1f82d785cd3def Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 22 Dec 2019 16:08:33 +0100 Subject: [PATCH 124/324] Add first steps for list-strategies --- freqtrade/configuration/arguments.py | 19 ++++++++++++++++--- freqtrade/utils.py | 8 ++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 41c5c3957..5f7bc74f1 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -30,6 +30,8 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] +ARGS_LIST_STRATEGIES = ["strategy_path"] + ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"] ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"] @@ -62,7 +64,8 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop "print_json", "hyperopt_show_no_header"] NO_CONF_REQURIED = ["download-data", "list-timeframes", "list-markets", "list-pairs", - "hyperopt-list", "hyperopt-show", "plot-dataframe", "plot-profit"] + "list-strategies", "hyperopt-list", "hyperopt-show", "plot-dataframe", + "plot-profit"] NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-hyperopt", "new-strategy"] @@ -131,8 +134,9 @@ class Arguments: from freqtrade.utils import (start_create_userdir, start_download_data, start_hyperopt_list, start_hyperopt_show, start_list_exchanges, start_list_markets, - start_new_hyperopt, start_new_strategy, - start_list_timeframes, start_test_pairlist, start_trading) + start_list_strategies, start_new_hyperopt, + start_new_strategy, start_list_timeframes, + start_test_pairlist, start_trading) from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit subparsers = self.parser.add_subparsers(dest='command', @@ -185,6 +189,15 @@ class Arguments: build_hyperopt_cmd.set_defaults(func=start_new_hyperopt) self._build_args(optionlist=ARGS_BUILD_HYPEROPT, parser=build_hyperopt_cmd) + # Add list-strategies subcommand + list_strategies_cmd = subparsers.add_parser( + 'list-strategies', + help='Print available strategies.', + parents=[_common_parser], + ) + list_strategies_cmd.set_defaults(func=start_list_strategies) + self._build_args(optionlist=ARGS_LIST_STRATEGIES, parser=list_strategies_cmd) + # Add list-exchanges subcommand list_exchanges_cmd = subparsers.add_parser( 'list-exchanges', diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 18966c574..c5fc47a74 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -224,6 +224,14 @@ def start_download_data(args: Dict[str, Any]) -> None: f"on exchange {exchange.name}.") +def start_list_strategies(args: Dict[str, Any]) -> None: + """ + Print Strategies available in a folder + """ + config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) + print(config) + + def start_list_timeframes(args: Dict[str, Any]) -> None: """ Print ticker intervals (timeframes) available on Exchange From eb1040ddb7445a49168792871ab417360bdfa4a1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Dec 2019 13:34:37 +0100 Subject: [PATCH 125/324] Convert resolvers to classmethods --- freqtrade/resolvers/exchange_resolver.py | 1 + freqtrade/resolvers/hyperopt_resolver.py | 23 +++++++++------- freqtrade/resolvers/iresolver.py | 34 +++++++++++++----------- freqtrade/resolvers/pairlist_resolver.py | 6 +++-- freqtrade/resolvers/strategy_resolver.py | 12 +++++---- tests/strategy/test_strategy.py | 2 -- 6 files changed, 43 insertions(+), 35 deletions(-) diff --git a/freqtrade/resolvers/exchange_resolver.py b/freqtrade/resolvers/exchange_resolver.py index e28a5cf80..2b6a731a9 100644 --- a/freqtrade/resolvers/exchange_resolver.py +++ b/freqtrade/resolvers/exchange_resolver.py @@ -14,6 +14,7 @@ class ExchangeResolver(IResolver): """ This class contains all the logic to load a custom exchange class """ + object_type = Exchange @staticmethod def load_exchange(exchange_name: str, config: dict, validate: bool = True) -> Exchange: diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index 0726b0627..b9c750251 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -20,6 +20,7 @@ class HyperOptResolver(IResolver): """ This class contains all the logic to load custom hyperopt class """ + object_type = IHyperOpt @staticmethod def load_hyperopt(config: Dict) -> IHyperOpt: @@ -59,12 +60,13 @@ class HyperOptResolver(IResolver): """ current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() - abs_paths = IResolver.build_search_paths(config, current_path=current_path, - user_subdir=USERPATH_HYPEROPTS, - extra_dir=extra_dir) + abs_paths = HyperOptResolver.build_search_paths(config, current_path=current_path, + user_subdir=USERPATH_HYPEROPTS, + extra_dir=extra_dir) - hyperopt = IResolver._load_object(paths=abs_paths, object_type=IHyperOpt, - object_name=hyperopt_name, kwargs={'config': config}) + hyperopt = HyperOptResolver._load_object(paths=abs_paths, + object_name=hyperopt_name, + kwargs={'config': config}) if hyperopt: return hyperopt raise OperationalException( @@ -77,6 +79,7 @@ class HyperOptLossResolver(IResolver): """ This class contains all the logic to load custom hyperopt loss class """ + object_type = IHyperOptLoss @staticmethod def load_hyperoptloss(config: Dict) -> IHyperOptLoss: @@ -113,12 +116,12 @@ class HyperOptLossResolver(IResolver): """ current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() - abs_paths = IResolver.build_search_paths(config, current_path=current_path, - user_subdir=USERPATH_HYPEROPTS, - extra_dir=extra_dir) + abs_paths = HyperOptLossResolver.build_search_paths(config, current_path=current_path, + user_subdir=USERPATH_HYPEROPTS, + extra_dir=extra_dir) - hyperoptloss = IResolver._load_object(paths=abs_paths, object_type=IHyperOptLoss, - object_name=hyper_loss_name) + hyperoptloss = HyperOptLossResolver._load_object(paths=abs_paths, + object_name=hyper_loss_name) if hyperoptloss: return hyperoptloss diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 0b986debb..11937c1da 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -7,7 +7,7 @@ import importlib.util import inspect import logging from pathlib import Path -from typing import Any, List, Optional, Tuple, Union, Generator +from typing import Any, Generator, List, Optional, Tuple, Type, Union logger = logging.getLogger(__name__) @@ -16,6 +16,8 @@ class IResolver: """ This class contains all the logic to load custom classes """ + # Childclasses need to override this + object_type: Type[Any] @staticmethod def build_search_paths(config, current_path: Path, user_subdir: Optional[str] = None, @@ -32,12 +34,11 @@ class IResolver: return abs_paths - @staticmethod - def _get_valid_object(object_type, module_path: Path, + @classmethod + def _get_valid_object(cls, module_path: Path, object_name: str) -> Generator[Any, None, None]: """ Generator returning objects with matching object_type and object_name in the path given. - :param object_type: object_type (class) :param module_path: absolute path to the module :param object_name: Class name of the object :return: generator containing matching objects @@ -55,19 +56,21 @@ class IResolver: valid_objects_gen = ( obj for name, obj in inspect.getmembers(module, inspect.isclass) - if object_name == name and object_type in obj.__bases__ + if object_name == name and cls.object_type in obj.__bases__ ) return valid_objects_gen - @staticmethod - def _search_object(directory: Path, object_type, object_name: str, + @classmethod + def _search_object(cls, directory: Path, object_name: str, kwargs: dict = {}) -> Union[Tuple[Any, Path], Tuple[None, None]]: """ Search for the objectname in the given directory :param directory: relative or absolute directory path + :param object_name: ClassName of the object to load :return: object instance """ - logger.debug("Searching for %s %s in '%s'", object_type.__name__, object_name, directory) + logger.debug("Searching for %s %s in '%s'", + cls.object_type.__name__, object_name, directory) for entry in directory.iterdir(): # Only consider python files if not str(entry).endswith('.py'): @@ -75,14 +78,14 @@ class IResolver: continue module_path = entry.resolve() - obj = next(IResolver._get_valid_object(object_type, module_path, object_name), None) + obj = next(cls._get_valid_object(module_path, object_name), None) if obj: return (obj(**kwargs), module_path) return (None, None) - @staticmethod - def _load_object(paths: List[Path], object_type, object_name: str, + @classmethod + def _load_object(cls, paths: List[Path], object_name: str, kwargs: dict = {}) -> Optional[Any]: """ Try to load object from path list. @@ -90,13 +93,12 @@ class IResolver: for _path in paths: try: - (module, module_path) = IResolver._search_object(directory=_path, - object_type=object_type, - object_name=object_name, - kwargs=kwargs) + (module, module_path) = cls._search_object(directory=_path, + object_name=object_name, + kwargs=kwargs) if module: logger.info( - f"Using resolved {object_type.__name__.lower()[1:]} {object_name} " + f"Using resolved {cls.object_type.__name__.lower()[1:]} {object_name} " f"from '{module_path}'...") return module except FileNotFoundError: diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index 611660ff4..00ebc03aa 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -17,6 +17,7 @@ class PairListResolver(IResolver): """ This class contains all the logic to load custom PairList class """ + object_type = IPairList @staticmethod def load_pairlist(pairlist_name: str, exchange, pairlistmanager, @@ -53,8 +54,9 @@ class PairListResolver(IResolver): abs_paths = IResolver.build_search_paths(config, current_path=current_path, user_subdir=None, extra_dir=None) - pairlist = IResolver._load_object(paths=abs_paths, object_type=IPairList, - object_name=pairlist_name, kwargs=kwargs) + pairlist = PairListResolver._load_object(paths=abs_paths, + object_name=pairlist_name, + kwargs=kwargs) if pairlist: return pairlist raise OperationalException( diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 6d3fe5ff9..654103377 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -22,6 +22,7 @@ class StrategyResolver(IResolver): """ This class contains the logic to load custom strategy class """ + object_type = IStrategy @staticmethod def load_strategy(config: Optional[Dict] = None) -> IStrategy: @@ -134,9 +135,9 @@ class StrategyResolver(IResolver): """ current_path = Path(__file__).parent.parent.joinpath('strategy').resolve() - abs_paths = IResolver.build_search_paths(config, current_path=current_path, - user_subdir=constants.USERPATH_STRATEGY, - extra_dir=extra_dir) + abs_paths = StrategyResolver.build_search_paths(config, current_path=current_path, + user_subdir=constants.USERPATH_STRATEGY, + extra_dir=extra_dir) if ":" in strategy_name: logger.info("loading base64 encoded strategy") @@ -154,8 +155,9 @@ class StrategyResolver(IResolver): # register temp path with the bot abs_paths.insert(0, temp.resolve()) - strategy = IResolver._load_object(paths=abs_paths, object_type=IStrategy, - object_name=strategy_name, kwargs={'config': config}) + strategy = StrategyResolver._load_object(paths=abs_paths, + object_name=strategy_name, + 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/strategy/test_strategy.py b/tests/strategy/test_strategy.py index ce7ac1741..dba816621 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -20,7 +20,6 @@ def test_search_strategy(): s, _ = StrategyResolver._search_object( directory=default_location, - object_type=IStrategy, kwargs={'config': default_config}, object_name='DefaultStrategy' ) @@ -28,7 +27,6 @@ def test_search_strategy(): s, _ = StrategyResolver._search_object( directory=default_location, - object_type=IStrategy, kwargs={'config': default_config}, object_name='NotFoundStrategy' ) From 25e6d6a7bfdea9bd74b6f06375c4e11df2f903b1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Dec 2019 13:54:46 +0100 Subject: [PATCH 126/324] Combine load_object methods into one --- freqtrade/resolvers/hyperopt_resolver.py | 70 +++++------------------- freqtrade/resolvers/iresolver.py | 36 +++++++++++- freqtrade/resolvers/pairlist_resolver.py | 42 ++++---------- freqtrade/resolvers/strategy_resolver.py | 16 ++++-- tests/optimize/test_hyperopt.py | 4 +- 5 files changed, 68 insertions(+), 100 deletions(-) diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index b9c750251..c26fd09f2 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -5,7 +5,7 @@ This module load custom hyperopt """ import logging from pathlib import Path -from typing import Optional, Dict +from typing import Dict from freqtrade import OperationalException from freqtrade.constants import DEFAULT_HYPEROPT_LOSS, USERPATH_HYPEROPTS @@ -21,6 +21,9 @@ class HyperOptResolver(IResolver): This class contains all the logic to load custom hyperopt class """ object_type = IHyperOpt + object_type_str = "Hyperopt" + user_subdir = USERPATH_HYPEROPTS + initial_search_path = Path(__file__).parent.parent.joinpath('optimize').resolve() @staticmethod def load_hyperopt(config: Dict) -> IHyperOpt: @@ -34,8 +37,9 @@ class HyperOptResolver(IResolver): hyperopt_name = config['hyperopt'] - hyperopt = HyperOptResolver._load_hyperopt(hyperopt_name, config, - extra_dir=config.get('hyperopt_path')) + hyperopt = HyperOptResolver.load_object(hyperopt_name, config, + kwargs={'config': config}, + extra_dir=config.get('hyperopt_path')) if not hasattr(hyperopt, 'populate_indicators'): logger.warning("Hyperopt class does not provide populate_indicators() method. " @@ -48,38 +52,15 @@ class HyperOptResolver(IResolver): "Using populate_sell_trend from the strategy.") return hyperopt - @staticmethod - def _load_hyperopt( - hyperopt_name: str, config: Dict, extra_dir: Optional[str] = None) -> IHyperOpt: - """ - Search and loads the specified hyperopt. - :param hyperopt_name: name of the module to import - :param config: configuration dictionary - :param extra_dir: additional directory to search for the given hyperopt - :return: HyperOpt instance or None - """ - current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() - - abs_paths = HyperOptResolver.build_search_paths(config, current_path=current_path, - user_subdir=USERPATH_HYPEROPTS, - extra_dir=extra_dir) - - hyperopt = HyperOptResolver._load_object(paths=abs_paths, - object_name=hyperopt_name, - kwargs={'config': config}) - if hyperopt: - return hyperopt - raise OperationalException( - f"Impossible to load Hyperopt '{hyperopt_name}'. This class does not exist " - "or contains Python code errors." - ) - class HyperOptLossResolver(IResolver): """ This class contains all the logic to load custom hyperopt loss class """ object_type = IHyperOptLoss + object_type_str = "HyperoptLoss" + user_subdir = USERPATH_HYPEROPTS + initial_search_path = Path(__file__).parent.parent.joinpath('optimize').resolve() @staticmethod def load_hyperoptloss(config: Dict) -> IHyperOptLoss: @@ -92,8 +73,9 @@ class HyperOptLossResolver(IResolver): # default hyperopt loss hyperoptloss_name = config.get('hyperopt_loss') or DEFAULT_HYPEROPT_LOSS - hyperoptloss = HyperOptLossResolver._load_hyperoptloss( - hyperoptloss_name, config, extra_dir=config.get('hyperopt_path')) + hyperoptloss = HyperOptLossResolver.load_object(hyperoptloss_name, + config, kwargs={}, + extra_dir=config.get('hyperopt_path')) # Assign ticker_interval to be used in hyperopt hyperoptloss.__class__.ticker_interval = str(config['ticker_interval']) @@ -103,29 +85,3 @@ class HyperOptLossResolver(IResolver): f"Found HyperoptLoss class {hyperoptloss_name} does not " "implement `hyperopt_loss_function`.") return hyperoptloss - - @staticmethod - def _load_hyperoptloss(hyper_loss_name: str, config: Dict, - extra_dir: Optional[str] = None) -> IHyperOptLoss: - """ - Search and loads the specified hyperopt loss class. - :param hyper_loss_name: name of the module to import - :param config: configuration dictionary - :param extra_dir: additional directory to search for the given hyperopt - :return: HyperOptLoss instance or None - """ - current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() - - abs_paths = HyperOptLossResolver.build_search_paths(config, current_path=current_path, - user_subdir=USERPATH_HYPEROPTS, - extra_dir=extra_dir) - - hyperoptloss = HyperOptLossResolver._load_object(paths=abs_paths, - object_name=hyper_loss_name) - if hyperoptloss: - return hyperoptloss - - raise OperationalException( - f"Impossible to load HyperoptLoss '{hyper_loss_name}'. This class does not exist " - "or contains Python code errors." - ) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 11937c1da..0101e37a3 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -9,6 +9,8 @@ import logging from pathlib import Path from typing import Any, Generator, List, Optional, Tuple, Type, Union +from freqtrade import OperationalException + logger = logging.getLogger(__name__) @@ -18,12 +20,15 @@ class IResolver: """ # Childclasses need to override this object_type: Type[Any] + object_type_str: str + user_subdir: Optional[str] = None + initial_search_path: Path - @staticmethod - def build_search_paths(config, current_path: Path, user_subdir: Optional[str] = None, + @classmethod + def build_search_paths(cls, config, user_subdir: Optional[str] = None, extra_dir: Optional[str] = None) -> List[Path]: - abs_paths: List[Path] = [current_path] + abs_paths: List[Path] = [cls.initial_search_path] if user_subdir: abs_paths.insert(0, config['user_data_dir'].joinpath(user_subdir)) @@ -105,3 +110,28 @@ class IResolver: logger.warning('Path "%s" does not exist.', _path.resolve()) return None + + @classmethod + 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. + :param objectname: name of the module to import + :param config: configuration dictionary + :param extra_dir: additional directory to search for the given pairlist + :raises: OperationalException if the class is invalid or does not exist. + :return: Object instance or None + """ + + abs_paths = cls.build_search_paths(config, + 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 + raise OperationalException( + f"Impossible to load {cls.object_type_str} '{object_name}'. This class does not exist " + "or contains Python code errors." + ) diff --git a/freqtrade/resolvers/pairlist_resolver.py b/freqtrade/resolvers/pairlist_resolver.py index 00ebc03aa..77db74084 100644 --- a/freqtrade/resolvers/pairlist_resolver.py +++ b/freqtrade/resolvers/pairlist_resolver.py @@ -6,7 +6,6 @@ This module load custom pairlists import logging from pathlib import Path -from freqtrade import OperationalException from freqtrade.pairlist.IPairList import IPairList from freqtrade.resolvers import IResolver @@ -18,6 +17,9 @@ class PairListResolver(IResolver): This class contains all the logic to load custom PairList class """ object_type = IPairList + object_type_str = "Pairlist" + user_subdir = None + initial_search_path = Path(__file__).parent.parent.joinpath('pairlist').resolve() @staticmethod def load_pairlist(pairlist_name: str, exchange, pairlistmanager, @@ -32,34 +34,10 @@ class PairListResolver(IResolver): :param pairlist_pos: Position of the pairlist in the list of pairlists :return: initialized Pairlist class """ - - return PairListResolver._load_pairlist(pairlist_name, config, - kwargs={'exchange': exchange, - 'pairlistmanager': pairlistmanager, - 'config': config, - 'pairlistconfig': pairlistconfig, - 'pairlist_pos': pairlist_pos}) - - @staticmethod - def _load_pairlist(pairlist_name: str, config: dict, kwargs: dict) -> IPairList: - """ - Search and loads the specified pairlist. - :param pairlist_name: name of the module to import - :param config: configuration dictionary - :param extra_dir: additional directory to search for the given pairlist - :return: PairList instance or None - """ - current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve() - - abs_paths = IResolver.build_search_paths(config, current_path=current_path, - user_subdir=None, extra_dir=None) - - pairlist = PairListResolver._load_object(paths=abs_paths, - object_name=pairlist_name, - kwargs=kwargs) - if pairlist: - return pairlist - raise OperationalException( - f"Impossible to load Pairlist '{pairlist_name}'. This class does not exist " - "or contains Python code errors." - ) + return PairListResolver.load_object(pairlist_name, config, + kwargs={'exchange': exchange, + 'pairlistmanager': pairlistmanager, + 'config': config, + 'pairlistconfig': pairlistconfig, + 'pairlist_pos': pairlist_pos}, + ) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 654103377..4fd5c586a 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -11,7 +11,9 @@ from inspect import getfullargspec from pathlib import Path from typing import Dict, Optional -from freqtrade import constants, OperationalException +from freqtrade import OperationalException +from freqtrade.constants import (REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES, + USERPATH_STRATEGY) from freqtrade.resolvers import IResolver from freqtrade.strategy.interface import IStrategy @@ -23,6 +25,9 @@ class StrategyResolver(IResolver): This class contains the logic to load custom strategy class """ object_type = IStrategy + object_type_str = "Strategy" + user_subdir = USERPATH_STRATEGY + initial_search_path = Path(__file__).parent.parent.joinpath('strategy').resolve() @staticmethod def load_strategy(config: Optional[Dict] = None) -> IStrategy: @@ -115,11 +120,11 @@ class StrategyResolver(IResolver): @staticmethod def _strategy_sanity_validations(strategy): - if not all(k in strategy.order_types for k in constants.REQUIRED_ORDERTYPES): + if not all(k in strategy.order_types for k in REQUIRED_ORDERTYPES): raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. " f"Order-types mapping is incomplete.") - if not all(k in strategy.order_time_in_force for k in constants.REQUIRED_ORDERTIF): + if not all(k in strategy.order_time_in_force for k in REQUIRED_ORDERTIF): raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. " f"Order-time-in-force mapping is incomplete.") @@ -133,10 +138,9 @@ class StrategyResolver(IResolver): :param extra_dir: additional directory to search for the given strategy :return: Strategy instance or None """ - current_path = Path(__file__).parent.parent.joinpath('strategy').resolve() - abs_paths = StrategyResolver.build_search_paths(config, current_path=current_path, - user_subdir=constants.USERPATH_STRATEGY, + abs_paths = StrategyResolver.build_search_paths(config, + user_subdir=USERPATH_STRATEGY, extra_dir=extra_dir) if ":" in strategy_name: diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 9c6e73c53..fb492be35 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -159,7 +159,7 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None: delattr(hyperopt, 'populate_buy_trend') delattr(hyperopt, 'populate_sell_trend') mocker.patch( - 'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver._load_hyperopt', + 'freqtrade.resolvers.hyperopt_resolver.HyperOptResolver.load_object', MagicMock(return_value=hyperopt(default_conf)) ) default_conf.update({'hyperopt': 'DefaultHyperOpt'}) @@ -195,7 +195,7 @@ def test_hyperoptlossresolver(mocker, default_conf, caplog) -> None: hl = DefaultHyperOptLoss mocker.patch( - 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver._load_hyperoptloss', + 'freqtrade.resolvers.hyperopt_resolver.HyperOptLossResolver.load_object', MagicMock(return_value=hl) ) x = HyperOptLossResolver.load_hyperoptloss(default_conf) From 5a11ca86bb69b1619cbba78cce05a205f669a67d Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Dec 2019 14:01:28 +0100 Subject: [PATCH 127/324] Move instanciation out of search_object --- freqtrade/resolvers/iresolver.py | 13 ++++++------- tests/strategy/test_strategy.py | 5 +---- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 0101e37a3..bbdc8ca91 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -66,13 +66,13 @@ class IResolver: return valid_objects_gen @classmethod - def _search_object(cls, directory: Path, object_name: str, - kwargs: dict = {}) -> Union[Tuple[Any, Path], Tuple[None, None]]: + def _search_object(cls, directory: Path, object_name: str + ) -> Union[Tuple[Any, Path], Tuple[None, None]]: """ Search for the objectname in the given directory :param directory: relative or absolute directory path :param object_name: ClassName of the object to load - :return: object instance + :return: object class """ logger.debug("Searching for %s %s in '%s'", cls.object_type.__name__, object_name, directory) @@ -86,7 +86,7 @@ class IResolver: obj = next(cls._get_valid_object(module_path, object_name), None) if obj: - return (obj(**kwargs), module_path) + return (obj, module_path) return (None, None) @classmethod @@ -99,13 +99,12 @@ class IResolver: for _path in paths: try: (module, module_path) = cls._search_object(directory=_path, - object_name=object_name, - kwargs=kwargs) + object_name=object_name) if module: logger.info( f"Using resolved {cls.object_type.__name__.lower()[1:]} {object_name} " f"from '{module_path}'...") - return module + return module(**kwargs) except FileNotFoundError: logger.warning('Path "%s" does not exist.', _path.resolve()) diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py index dba816621..7085223c5 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -15,19 +15,16 @@ from tests.conftest import log_has, log_has_re def test_search_strategy(): - default_config = {} default_location = Path(__file__).parent.parent.joinpath('strategy').resolve() s, _ = StrategyResolver._search_object( directory=default_location, - kwargs={'config': default_config}, object_name='DefaultStrategy' ) - assert isinstance(s, IStrategy) + assert issubclass(s, IStrategy) s, _ = StrategyResolver._search_object( directory=default_location, - kwargs={'config': default_config}, object_name='NotFoundStrategy' ) assert s is None From 2ab989e274c5bfb278e8b6397380b1714d806a80 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Dec 2019 15:28:35 +0100 Subject: [PATCH 128/324] Cleanup some code and add option --- freqtrade/configuration/arguments.py | 2 +- freqtrade/resolvers/iresolver.py | 35 +++++++++++++++++++++++----- freqtrade/utils.py | 14 +++++++++-- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 5f7bc74f1..b2197619d 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -30,7 +30,7 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path", ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] -ARGS_LIST_STRATEGIES = ["strategy_path"] +ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column"] ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"] diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index bbdc8ca91..01ecbcb84 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -7,7 +7,7 @@ import importlib.util import inspect import logging from pathlib import Path -from typing import Any, Generator, List, Optional, Tuple, Type, Union +from typing import Any, Dict, Generator, List, Optional, Tuple, Type, Union from freqtrade import OperationalException @@ -41,7 +41,7 @@ class IResolver: @classmethod def _get_valid_object(cls, module_path: Path, - object_name: str) -> Generator[Any, None, None]: + object_name: Optional[str]) -> Generator[Any, None, None]: """ Generator returning objects with matching object_type and object_name in the path given. :param module_path: absolute path to the module @@ -51,7 +51,7 @@ class IResolver: # Generate spec based on absolute path # Pass object_name as first argument to have logging print a reasonable name. - spec = importlib.util.spec_from_file_location(object_name, str(module_path)) + spec = importlib.util.spec_from_file_location(object_name or "", str(module_path)) module = importlib.util.module_from_spec(spec) try: spec.loader.exec_module(module) # type: ignore # importlib does not use typehints @@ -61,7 +61,7 @@ class IResolver: valid_objects_gen = ( obj for name, obj in inspect.getmembers(module, inspect.isclass) - if object_name == name and cls.object_type in obj.__bases__ + if (object_name is None or object_name == name) and cls.object_type in obj.__bases__ ) return valid_objects_gen @@ -74,8 +74,7 @@ class IResolver: :param object_name: ClassName of the object to load :return: object class """ - logger.debug("Searching for %s %s in '%s'", - cls.object_type.__name__, object_name, directory) + logger.debug(f"Searching for {cls.object_type.__name__} {object_name} in '{directory}'") for entry in directory.iterdir(): # Only consider python files if not str(entry).endswith('.py'): @@ -134,3 +133,27 @@ class IResolver: f"Impossible to load {cls.object_type_str} '{object_name}'. This class does not exist " "or contains Python code errors." ) + + @classmethod + def search_all_objects(cls, directory: Path) -> List[Dict[str, Any]]: + """ + Searches a directory for valid objects + :param directory: Path to search + :return: List of dicts containing 'name', 'class' and 'location' entires + """ + logger.debug(f"Searching for {cls.object_type.__name__} '{directory}'") + objects = [] + for entry in directory.iterdir(): + # Only consider python files + if not str(entry).endswith('.py'): + logger.debug('Ignoring %s', entry) + continue + module_path = entry.resolve() + logger.info(f"Path {module_path}") + for obj in cls._get_valid_object(module_path, object_name=None): + objects.append( + {'name': obj.__name__, + 'class': obj, + 'location': entry, + }) + return objects diff --git a/freqtrade/utils.py b/freqtrade/utils.py index c5fc47a74..06a62172a 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -23,7 +23,7 @@ from freqtrade.data.history import (convert_trades_to_ohlcv, from freqtrade.exchange import (available_exchanges, ccxt_exchanges, market_is_active, symbol_is_pair) from freqtrade.misc import plural, render_template -from freqtrade.resolvers import ExchangeResolver +from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -229,7 +229,17 @@ def start_list_strategies(args: Dict[str, Any]) -> None: Print Strategies available in a folder """ config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) - print(config) + + directory = Path(config.get('strategy_path', config['user_data_dir'] / USERPATH_STRATEGY)) + strategies = StrategyResolver.search_all_objects(directory) + # Sort alphabetically + strategies = sorted(strategies, key=lambda x: x['name']) + strats_to_print = [{'name': s['name'], 'location': s['location']} for s in strategies] + + if args['print_one_column']: + print('\n'.join([s['name'] for s in strategies])) + else: + print(tabulate(strats_to_print, headers='keys', tablefmt='pipe')) def start_list_timeframes(args: Dict[str, Any]) -> None: From 27b86170778c70dd4a83d2c02d06a12452fb1783 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Dec 2019 15:35:38 +0100 Subject: [PATCH 129/324] Add tests --- tests/strategy/test_strategy.py | 8 +++++++ tests/test_utils.py | 42 +++++++++++++++++++++++++++++---- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py index 7085223c5..10b9f3466 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -30,6 +30,14 @@ def test_search_strategy(): assert s is None +def test_search_all_strategies(): + directory = Path(__file__).parent + strategies = StrategyResolver.search_all_objects(directory) + assert isinstance(strategies, list) + assert len(strategies) == 3 + assert isinstance(strategies[0], dict) + + def test_load_strategy(default_conf, result): default_conf.update({'strategy': 'SampleStrategy', 'strategy_path': str(Path(__file__).parents[2] / 'freqtrade/templates') diff --git a/tests/test_utils.py b/tests/test_utils.py index 40ca9ac02..b8be1ae61 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -7,11 +7,12 @@ import pytest from freqtrade import OperationalException from freqtrade.state import RunMode from freqtrade.utils import (setup_utils_configuration, start_create_userdir, - start_download_data, start_list_exchanges, - start_list_markets, start_list_timeframes, - start_new_hyperopt, start_new_strategy, - start_test_pairlist, start_trading, - start_hyperopt_list, start_hyperopt_show) + start_download_data, start_hyperopt_list, + start_hyperopt_show, start_list_exchanges, + start_list_markets, start_list_strategies, + start_list_timeframes, start_new_hyperopt, + start_new_strategy, start_test_pairlist, + start_trading) from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) @@ -630,6 +631,37 @@ def test_download_data_trades(mocker, caplog): assert convert_mock.call_count == 1 +def test_start_list_strategies(mocker, caplog, capsys): + + args = [ + "list-strategies", + "--strategy-path", + str(Path(__file__).parent / "strategy"), + "-1" + ] + pargs = get_args(args) + # pargs['config'] = None + start_list_strategies(pargs) + captured = capsys.readouterr() + assert "TestStrategyLegacy" in captured.out + assert "strategy/legacy_strategy.py" not in captured.out + assert "DefaultStrategy" in captured.out + + # Test regular output + args = [ + "list-strategies", + "--strategy-path", + str(Path(__file__).parent / "strategy"), + ] + pargs = get_args(args) + # pargs['config'] = None + start_list_strategies(pargs) + captured = capsys.readouterr() + assert "TestStrategyLegacy" in captured.out + assert "strategy/legacy_strategy.py" in captured.out + assert "DefaultStrategy" in captured.out + + def test_start_test_pairlist(mocker, caplog, markets, tickers, default_conf, capsys): mocker.patch.multiple('freqtrade.exchange.Exchange', markets=PropertyMock(return_value=markets), From 66f9ece0619554f072fbc20c251b35dd6bbc6f33 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Dec 2019 15:35:53 +0100 Subject: [PATCH 130/324] Add documentation for strategy-list --- docs/utils.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/utils.md b/docs/utils.md index a9fbfc7d5..6eb37a386 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -108,6 +108,44 @@ With custom user directory freqtrade new-hyperopt --userdir ~/.freqtrade/ --hyperopt AwesomeHyperopt ``` +## List Strategies + +Use the `list-strategies` subcommand to see all strategies in one particular folder. + +``` +freqtrade list-strategies --help +usage: freqtrade list-strategies [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [--strategy-path PATH] [-1] + +optional arguments: + -h, --help show this help message and exit + --strategy-path PATH Specify additional strategy lookup path. + -1, --one-column Print output in one column. + +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: `config.json`). 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: search default strategy folder within userdir + +``` bash +freqtrade list-strategies --user-data ~/.freqtrade/ +``` + +Example: search dedicated strategy path + +``` bash +freqtrade list-strategies --strategy-path ~/.freqtrade/strategies/ +``` + ## List Exchanges Use the `list-exchanges` subcommand to see the exchanges available for the bot. From 402c761a231edffe017ba93fdf821637e5ddab40 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Dec 2019 15:40:49 +0100 Subject: [PATCH 131/324] Change loglevel of Path output to debug --- docs/utils.md | 5 ++++- freqtrade/resolvers/iresolver.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/utils.md b/docs/utils.md index 6eb37a386..f7501ae9d 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -132,8 +132,11 @@ Common arguments: Path to directory with historical backtesting data. --userdir PATH, --user-data-dir PATH Path to userdata directory. - ``` + +!!! Warning + Using this command will try to load all python files from a folder. This can be a security risk if untrusted files reside in this folder, since all module-level code is executed. + Example: search default strategy folder within userdir ``` bash diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 01ecbcb84..e3c0d1ad0 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -149,7 +149,7 @@ class IResolver: logger.debug('Ignoring %s', entry) continue module_path = entry.resolve() - logger.info(f"Path {module_path}") + logger.debug(f"Path {module_path}") for obj in cls._get_valid_object(module_path, object_name=None): objects.append( {'name': obj.__name__, From ad75048678796a23e4dcf4a82464628a2860d6b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 24 Dec 2019 15:53:40 +0100 Subject: [PATCH 132/324] Fix testing with path in windows --- tests/test_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index b8be1ae61..185425efc 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -644,7 +644,7 @@ def test_start_list_strategies(mocker, caplog, capsys): start_list_strategies(pargs) captured = capsys.readouterr() assert "TestStrategyLegacy" in captured.out - assert "strategy/legacy_strategy.py" not in captured.out + assert str(Path("strategy/legacy_strategy.py")) not in captured.out assert "DefaultStrategy" in captured.out # Test regular output @@ -658,7 +658,7 @@ def test_start_list_strategies(mocker, caplog, capsys): start_list_strategies(pargs) captured = capsys.readouterr() assert "TestStrategyLegacy" in captured.out - assert "strategy/legacy_strategy.py" in captured.out + assert str(Path("strategy/legacy_strategy.py")) in captured.out assert "DefaultStrategy" in captured.out From e5aed098b568900f34e7f3cbe85b8b8d21d76254 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Dec 2019 09:39:29 +0100 Subject: [PATCH 133/324] Enhance backtest results with sell reason profit / loss table --- freqtrade/optimize/backtesting.py | 6 ++++-- tests/optimize/test_backtesting.py | 12 ++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ffa112bd5..21fb24b17 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -183,9 +183,11 @@ class Backtesting: Generate small table outlining Backtest results """ tabular_data = [] - headers = ['Sell Reason', 'Count'] + headers = ['Sell Reason', 'Count', 'Profit', 'Loss'] for reason, count in results['sell_reason'].value_counts().iteritems(): - tabular_data.append([reason.value, count]) + profit = len(results[(results['sell_reason'] == reason) & (results['profit_abs'] >= 0)]) + loss = len(results[(results['sell_reason'] == reason) & (results['profit_abs'] < 0)]) + tabular_data.append([reason.value, count, profit, loss]) return tabulate(tabular_data, headers=headers, tablefmt="pipe") def _generate_text_table_strategy(self, all_results: dict) -> str: diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 0ea35e41f..8a27c591f 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -394,8 +394,8 @@ def test_generate_text_table_sell_reason(default_conf, mocker): results = pd.DataFrame( { 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], - 'profit_percent': [0.1, 0.2, 0.3], - 'profit_abs': [0.2, 0.4, 0.5], + 'profit_percent': [0.1, 0.2, -0.3], + 'profit_abs': [0.2, 0.4, -0.5], 'trade_duration': [10, 30, 10], 'profit': [2, 0, 0], 'loss': [0, 0, 1], @@ -404,10 +404,10 @@ def test_generate_text_table_sell_reason(default_conf, mocker): ) result_str = ( - '| Sell Reason | Count |\n' - '|:--------------|--------:|\n' - '| roi | 2 |\n' - '| stop_loss | 1 |' + '| Sell Reason | Count | Profit | Loss |\n' + '|:--------------|--------:|---------:|-------:|\n' + '| roi | 2 | 2 | 0 |\n' + '| stop_loss | 1 | 0 | 1 |' ) assert backtesting._generate_text_table_sell_reason( data={'ETH/BTC': {}}, results=results) == result_str From 63f41cf1c692d69da8899fec520916d6e59699b1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Dec 2019 09:39:44 +0100 Subject: [PATCH 134/324] Update documentation with new result --- docs/backtesting.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 017289905..ac7c8e11a 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -137,12 +137,12 @@ A backtesting result will look like that: | ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 | 15 | | TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 243 | ========================================================= SELL REASON STATS ========================================================= -| Sell Reason | Count | -|:-------------------|--------:| -| trailing_stop_loss | 205 | -| stop_loss | 166 | -| sell_signal | 56 | -| force_sell | 2 | +| Sell Reason | Count | Profit | Loss | +|:-------------------|--------:|---------:|-------:| +| trailing_stop_loss | 205 | 150 | 55 | +| stop_loss | 166 | 0 | 166 | +| sell_signal | 56 | 36 | 20 | +| force_sell | 2 | 0 | 2 | ====================================================== LEFT OPEN TRADES REPORT ====================================================== | pair | buy count | avg profit % | cum profit % | tot profit BTC | tot profit % | avg duration | profit | loss | |:---------|------------:|---------------:|---------------:|-----------------:|---------------:|:---------------|---------:|-------:| @@ -154,6 +154,7 @@ A backtesting result will look like that: 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. From 98647b490c6aa311e1b1735ebb5e1cab96020ef1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 25 Dec 2019 19:27:08 +0100 Subject: [PATCH 135/324] Remove wrong "once per hour" listings --- docs/edge.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/edge.md b/docs/edge.md index 769e48927..e7909594e 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -147,8 +147,8 @@ Edge module has following configuration options: |------------|-------------| | `enabled` | If true, then Edge will run periodically.
*Defaults to `false`.*
***Datatype:*** *Boolean* | `process_throttle_secs` | How often should Edge run in seconds.
*Defaults to `3600` (once per hour).*
***Datatype:*** *Integer* -| `calculate_since_number_of_days` | Number of days of data against which Edge calculates Win Rate, Risk Reward and Expectancy.
**Note** that it downloads historical data so increasing this number would lead to slowing down the bot.
*Defaults to `7` (once per hour).*
***Datatype:*** *Integer* -| `capital_available_percentage` | This is the percentage of the total capital on exchange in stake currency.
As an example if you have 10 ETH available in your wallet on the exchange and this value is 0.5 (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers it as available capital.
*Defaults to `0.5` (once per hour).*
***Datatype:*** *Float* +| `calculate_since_number_of_days` | Number of days of data against which Edge calculates Win Rate, Risk Reward and Expectancy.
**Note** that it downloads historical data so increasing this number would lead to slowing down the bot.
*Defaults to `7`.*
***Datatype:*** *Integer* +| `capital_available_percentage` | This is the percentage of the total capital on exchange in stake currency.
As an example if you have 10 ETH available in your wallet on the exchange and this value is 0.5 (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers it as available capital.
*Defaults to `0.5`.*
***Datatype:*** *Float* | `allowed_risk` | Ratio of allowed risk per trade.
*Defaults to `0.01` (1%)).*
***Datatype:*** *Float* | `stoploss_range_min` | Minimum stoploss.
*Defaults to `-0.01`.*
***Datatype:*** *Float* | `stoploss_range_max` | Maximum stoploss.
*Defaults to `-0.10`.*
***Datatype:*** *Float* From cadde3ab6d32a207e805f7043598e2c8e0eef3fb Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Dec 2019 16:15:44 +0100 Subject: [PATCH 136/324] Check if markets.info is a dict before using it --- freqtrade/exchange/exchange.py | 10 +++++++++- tests/exchange/test_exchange.py | 3 ++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 384ec1fb7..01e84c06e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -278,7 +278,15 @@ class Exchange: raise OperationalException( f'Pair {pair} is not available on {self.name}. ' f'Please remove {pair} from your whitelist.') - elif self.markets[pair].get('info', {}).get('IsRestricted', False): + + # From ccxt Documentation: + # markets.info: An associative array of non-common market properties, + # including fees, rates, limits and other general market information. + # The internal info array is different for each particular market, + # its contents depend on the exchange. + # It can also be a string or similar ... so we need to verify that first. + elif (isinstance(self.markets[pair].get('info', None), dict) + and self.markets[pair].get('info', {}).get('IsRestricted', False)): # Warn users about restricted pairs in whitelist. # We cannot determine reliably if Users are affected. logger.warning(f"Pair {pair} is restricted for some users on this exchange." diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 8f335c16b..4190869ad 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -364,7 +364,8 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog): api_mock = MagicMock() type(api_mock).markets = PropertyMock(return_value={ 'ETH/BTC': {}, 'LTC/BTC': {}, 'NEO/BTC': {}, - 'XRP/BTC': {'info': {'IsRestricted': True}} + 'XRP/BTC': {'info': {'IsRestricted': True}}, + 'NEO/BTC': {'info': 'TestString'}, # info can also be a string ... }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) From e51ac2c97356b5d5d11ed55426a83967ca674ca5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 27 Dec 2019 16:22:41 +0100 Subject: [PATCH 137/324] Remove unavailable pair ... --- 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 4190869ad..2f95e4e01 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -363,7 +363,7 @@ def test_validate_pairs_exception(default_conf, mocker, caplog): def test_validate_pairs_restricted(default_conf, mocker, caplog): api_mock = MagicMock() type(api_mock).markets = PropertyMock(return_value={ - 'ETH/BTC': {}, 'LTC/BTC': {}, 'NEO/BTC': {}, + 'ETH/BTC': {}, 'LTC/BTC': {}, 'XRP/BTC': {'info': {'IsRestricted': True}}, 'NEO/BTC': {'info': 'TestString'}, # info can also be a string ... }) From 039dfc302ca2c8497aea27282f3ab20f5a0c4343 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 28 Dec 2019 01:34:31 +0300 Subject: [PATCH 138/324] No need to convert pair name --- freqtrade/freqtradebot.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e21d89cd3..a9ce14bef 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -351,7 +351,6 @@ class FreqtradeBot: :param pair: pair for which we want to create a LIMIT_BUY :return: None """ - pair_s = pair.replace('_', '/') stake_currency = self.config['stake_currency'] fiat_currency = self.config.get('fiat_display_currency', None) time_in_force = self.strategy.order_time_in_force['buy'] @@ -362,10 +361,10 @@ class FreqtradeBot: # Calculate amount buy_limit_requested = self.get_target_bid(pair) - min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit_requested) + 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( - f"Can't open a new trade for {pair_s}: stake amount " + f"Can't open a new trade for {pair}: stake amount " f"is too small ({stake_amount} < {min_stake_amount})" ) return False @@ -388,7 +387,7 @@ class FreqtradeBot: if float(order['filled']) == 0: logger.warning('Buy %s order with time in force %s for %s is %s by %s.' ' zero amount is fulfilled.', - order_tif, order_type, pair_s, order_status, self.exchange.name) + order_tif, order_type, pair, order_status, self.exchange.name) return False else: # the order is partially fulfilled @@ -396,7 +395,7 @@ class FreqtradeBot: # if the order is fulfilled fully or partially logger.warning('Buy %s order with time in force %s for %s is %s by %s.' ' %s amount fulfilled out of %s (%s remaining which is canceled).', - order_tif, order_type, pair_s, order_status, self.exchange.name, + order_tif, order_type, pair, order_status, self.exchange.name, order['filled'], order['amount'], order['remaining'] ) stake_amount = order['cost'] @@ -413,7 +412,7 @@ class FreqtradeBot: self.rpc.send_msg({ 'type': RPCMessageType.BUY_NOTIFICATION, 'exchange': self.exchange.name.capitalize(), - 'pair': pair_s, + 'pair': pair, 'limit': buy_limit_filled_price, 'order_type': order_type, 'stake_amount': stake_amount, From b6d1c5b17aca2e21d6cde9fd41513a5daf3aa0a2 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 28 Dec 2019 01:44:51 +0300 Subject: [PATCH 139/324] _get_trade_stake_amount() is not private --- freqtrade/freqtradebot.py | 4 ++-- freqtrade/rpc/rpc.py | 2 +- tests/test_freqtradebot.py | 18 +++++++++--------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a9ce14bef..9bd7e94fc 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -204,7 +204,7 @@ class FreqtradeBot: return used_rate - def _get_trade_stake_amount(self, pair) -> Optional[float]: + def get_trade_stake_amount(self, pair) -> Optional[float]: """ Check if stake amount can be fulfilled with the available balance for the stake currency @@ -309,7 +309,7 @@ class FreqtradeBot: self.dataprovider.ohlcv(_pair, self.strategy.ticker_interval)) if buy and not sell and len(Trade.get_open_trades()) < self.config['max_open_trades']: - stake_amount = self._get_trade_stake_amount(_pair) + stake_amount = self.get_trade_stake_amount(_pair) if not stake_amount: continue diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index d6d442df5..35c312743 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -462,7 +462,7 @@ class RPC: raise RPCException(f'position for {pair} already open - id: {trade.id}') # gen stake amount - stakeamount = self._freqtrade._get_trade_stake_amount(pair) + stakeamount = self._freqtrade.get_trade_stake_amount(pair) # execute buy if self._freqtrade.execute_buy(pair, stakeamount, price): diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 13f1277b9..1d6cce7fa 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -136,7 +136,7 @@ def test_get_trade_stake_amount(default_conf, ticker, mocker) -> None: freqtrade = FreqtradeBot(default_conf) - result = freqtrade._get_trade_stake_amount('ETH/BTC') + result = freqtrade.get_trade_stake_amount('ETH/BTC') assert result == default_conf['stake_amount'] @@ -147,7 +147,7 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: freqtrade = FreqtradeBot(default_conf) with pytest.raises(DependencyException, match=r'.*stake amount.*'): - freqtrade._get_trade_stake_amount('ETH/BTC') + freqtrade.get_trade_stake_amount('ETH/BTC') def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, @@ -170,25 +170,25 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, patch_get_signal(freqtrade) # no open trades, order amount should be 'balance / max_open_trades' - result = freqtrade._get_trade_stake_amount('ETH/BTC') + result = freqtrade.get_trade_stake_amount('ETH/BTC') assert result == default_conf['stake_amount'] / conf['max_open_trades'] # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' freqtrade.execute_buy('ETH/BTC', result) - result = freqtrade._get_trade_stake_amount('LTC/BTC') + result = freqtrade.get_trade_stake_amount('LTC/BTC') assert result == default_conf['stake_amount'] / (conf['max_open_trades'] - 1) # create 2 trades, order amount should be None freqtrade.execute_buy('LTC/BTC', result) - result = freqtrade._get_trade_stake_amount('XRP/BTC') + result = freqtrade.get_trade_stake_amount('XRP/BTC') assert result is None # set max_open_trades = None, so do not trade conf['max_open_trades'] = 0 freqtrade = FreqtradeBot(conf) - result = freqtrade._get_trade_stake_amount('NEO/BTC') + result = freqtrade.get_trade_stake_amount('NEO/BTC') assert result is None @@ -214,8 +214,8 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None: edge_conf['dry_run_wallet'] = 999.9 freqtrade = FreqtradeBot(edge_conf) - assert freqtrade._get_trade_stake_amount('NEO/BTC') == (999.9 * 0.5 * 0.01) / 0.20 - assert freqtrade._get_trade_stake_amount('LTC/BTC') == (999.9 * 0.5 * 0.01) / 0.21 + assert freqtrade.get_trade_stake_amount('NEO/BTC') == (999.9 * 0.5 * 0.01) / 0.20 + assert freqtrade.get_trade_stake_amount('LTC/BTC') == (999.9 * 0.5 * 0.01) / 0.21 def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf) -> None: @@ -570,7 +570,7 @@ def test_create_trades_limit_reached(default_conf, ticker, limit_buy_order, patch_get_signal(freqtrade) assert not freqtrade.create_trades() - assert freqtrade._get_trade_stake_amount('ETH/BTC') is None + assert freqtrade.get_trade_stake_amount('ETH/BTC') is None def test_create_trades_no_pairs_let(default_conf, ticker, limit_buy_order, fee, From 86f269304099489279934f5622205bdad159d01e Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 28 Dec 2019 01:54:12 +0300 Subject: [PATCH 140/324] cosmetics --- freqtrade/freqtradebot.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9bd7e94fc..7827f29af 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -299,17 +299,17 @@ class FreqtradeBot: buycount = 0 # running get_signal on historical data fetched - for _pair in whitelist: - if self.strategy.is_pair_locked(_pair): - logger.info(f"Pair {_pair} is currently locked.") + for pair in whitelist: + if self.strategy.is_pair_locked(pair): + logger.info(f"Pair {pair} is currently locked.") continue (buy, sell) = self.strategy.get_signal( - _pair, self.strategy.ticker_interval, - self.dataprovider.ohlcv(_pair, self.strategy.ticker_interval)) + pair, self.strategy.ticker_interval, + self.dataprovider.ohlcv(pair, self.strategy.ticker_interval)) if buy and not sell and len(Trade.get_open_trades()) < self.config['max_open_trades']: - stake_amount = self.get_trade_stake_amount(_pair) + stake_amount = self.get_trade_stake_amount(pair) if not stake_amount: continue @@ -320,11 +320,11 @@ class FreqtradeBot: get('check_depth_of_market', {}) if (bidstrat_check_depth_of_market.get('enabled', False)) and\ (bidstrat_check_depth_of_market.get('bids_to_ask_delta', 0) > 0): - if self._check_depth_of_market_buy(_pair, bidstrat_check_depth_of_market): - buycount += self.execute_buy(_pair, stake_amount) + if self._check_depth_of_market_buy(pair, bidstrat_check_depth_of_market): + buycount += self.execute_buy(pair, stake_amount) continue - buycount += self.execute_buy(_pair, stake_amount) + buycount += self.execute_buy(pair, stake_amount) return buycount > 0 From 243bcb23680f5989dce55c268240b074de9448fd Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 28 Dec 2019 02:25:43 +0300 Subject: [PATCH 141/324] Make _check_available_stake_amount() a separate method --- freqtrade/freqtradebot.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7827f29af..24d250ffe 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -206,9 +206,8 @@ class FreqtradeBot: def get_trade_stake_amount(self, pair) -> Optional[float]: """ - Check if stake amount can be fulfilled with the available balance - for the stake currency - :return: float: Stake Amount + Calculate stake amount for the trade + :return: float: Stake amount """ if self.edge: return self.edge.stake_amount( @@ -220,16 +219,24 @@ class FreqtradeBot: else: stake_amount = self.config['stake_amount'] - available_amount = self.wallets.get_free(self.config['stake_currency']) - if stake_amount == constants.UNLIMITED_STAKE_AMOUNT: open_trades = len(Trade.get_open_trades()) if open_trades >= self.config['max_open_trades']: logger.warning("Can't open a new trade: max number of trades is reached") return None + available_amount = self.wallets.get_free(self.config['stake_currency']) return available_amount / (self.config['max_open_trades'] - open_trades) - # Check if stake_amount is fulfilled + return self._check_available_stake_amount(stake_amount) + + def _check_available_stake_amount(self, stake_amount) -> float: + """ + Check if stake amount can be fulfilled with the available balance + for the stake currency + :return: float: Stake amount + """ + available_amount = self.wallets.get_free(self.config['stake_currency']) + if available_amount < stake_amount: raise DependencyException( f"Available balance ({available_amount} {self.config['stake_currency']}) is " From abaeab89aadee4f8a6ee3a29d381fa58261fe0b1 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 28 Dec 2019 02:36:32 +0300 Subject: [PATCH 142/324] Make _calculate_unlimited_stake_amount() a separate method --- freqtrade/freqtradebot.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 24d250ffe..7b111e2d4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -220,15 +220,22 @@ class FreqtradeBot: stake_amount = self.config['stake_amount'] if stake_amount == constants.UNLIMITED_STAKE_AMOUNT: - open_trades = len(Trade.get_open_trades()) - if open_trades >= self.config['max_open_trades']: - logger.warning("Can't open a new trade: max number of trades is reached") - return None - available_amount = self.wallets.get_free(self.config['stake_currency']) - return available_amount / (self.config['max_open_trades'] - open_trades) + return self._calculate_unlimited_stake_amount() return self._check_available_stake_amount(stake_amount) + def _calculate_unlimited_stake_amount(self) -> Optional[float]: + """ + Calculate stake amount for "unlimited" stake amount + :return: None if max number of trades reached + """ + open_trades = len(Trade.get_open_trades()) + if open_trades >= self.config['max_open_trades']: + logger.warning("Can't open a new trade: max number of trades is reached") + return None + available_amount = self.wallets.get_free(self.config['stake_currency']) + return available_amount / (self.config['max_open_trades'] - open_trades) + def _check_available_stake_amount(self, stake_amount) -> float: """ Check if stake amount can be fulfilled with the available balance From ef92fd775c0863530caae52e9b0b170c3fcc7b77 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 28 Dec 2019 02:53:41 +0300 Subject: [PATCH 143/324] Align behavior: check for available in all cases: edge, unlimited and fixed --- freqtrade/freqtradebot.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7b111e2d4..2f4803cc5 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -210,7 +210,7 @@ class FreqtradeBot: :return: float: Stake amount """ if self.edge: - return self.edge.stake_amount( + stake_amount = self.edge.stake_amount( pair, self.wallets.get_free(self.config['stake_currency']), self.wallets.get_total(self.config['stake_currency']), @@ -218,9 +218,8 @@ class FreqtradeBot: ) else: stake_amount = self.config['stake_amount'] - - if stake_amount == constants.UNLIMITED_STAKE_AMOUNT: - return self._calculate_unlimited_stake_amount() + if stake_amount == constants.UNLIMITED_STAKE_AMOUNT: + stake_amount = self._calculate_unlimited_stake_amount() return self._check_available_stake_amount(stake_amount) @@ -236,7 +235,7 @@ class FreqtradeBot: available_amount = self.wallets.get_free(self.config['stake_currency']) return available_amount / (self.config['max_open_trades'] - open_trades) - def _check_available_stake_amount(self, stake_amount) -> float: + def _check_available_stake_amount(self, stake_amount: Optional[float]) -> Optional[float]: """ Check if stake amount can be fulfilled with the available balance for the stake currency @@ -244,7 +243,7 @@ class FreqtradeBot: """ available_amount = self.wallets.get_free(self.config['stake_currency']) - if available_amount < stake_amount: + if stake_amount is not None and available_amount < stake_amount: raise DependencyException( f"Available balance ({available_amount} {self.config['stake_currency']}) is " f"lower than stake amount ({stake_amount} {self.config['stake_currency']})" From ed9cb4219df4305e5c3aa1133b45ca0637b9b6d4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 28 Dec 2019 02:58:23 +0300 Subject: [PATCH 144/324] Make mypy happy --- freqtrade/freqtradebot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2f4803cc5..4ec01cb60 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -209,6 +209,7 @@ class FreqtradeBot: Calculate stake amount for the trade :return: float: Stake amount """ + stake_amount: Optional[float] if self.edge: stake_amount = self.edge.stake_amount( pair, From 8eeabd2372b26987975a4dd772157e14de7a0044 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 28 Dec 2019 03:22:50 +0300 Subject: [PATCH 145/324] Move warning to create_trades() --- freqtrade/freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4ec01cb60..44a83db1a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -231,7 +231,6 @@ class FreqtradeBot: """ open_trades = len(Trade.get_open_trades()) if open_trades >= self.config['max_open_trades']: - logger.warning("Can't open a new trade: max number of trades is reached") return None available_amount = self.wallets.get_free(self.config['stake_currency']) return available_amount / (self.config['max_open_trades'] - open_trades) @@ -324,7 +323,8 @@ class FreqtradeBot: if buy and not sell and len(Trade.get_open_trades()) < self.config['max_open_trades']: stake_amount = self.get_trade_stake_amount(pair) - if not stake_amount: + if stake_amount is None: + logger.warning("Can't open a new trade: max number of trades is reached") continue logger.info(f"Buy signal found: about create a new trade with stake_amount: " From 3dbd83e35a7529215ef609e3090aa9d78475ecb4 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 28 Dec 2019 03:46:42 +0300 Subject: [PATCH 146/324] Introduce get_free_open_trades() method --- freqtrade/freqtradebot.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 44a83db1a..14d67b942 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -136,7 +136,7 @@ class FreqtradeBot: self.process_maybe_execute_sells(trades) # Then looking for buy opportunities - if len(trades) < self.config['max_open_trades']: + if self.get_free_open_trades(): self.process_maybe_execute_buys() # Check and handle any timed out open orders @@ -173,6 +173,14 @@ class FreqtradeBot: """ return [(pair, self.config['ticker_interval']) for pair in pairs] + def get_free_open_trades(self): + """ + Return the number of free open trades slots or 0 if + max number of open trades reached + """ + open_trades = len(Trade.get_open_trades()) + return max(0, self.config['max_open_trades'] - open_trades) + def get_target_bid(self, pair: str, tick: Dict = None) -> float: """ Calculates bid target between current ask price and last price @@ -229,11 +237,11 @@ class FreqtradeBot: Calculate stake amount for "unlimited" stake amount :return: None if max number of trades reached """ - open_trades = len(Trade.get_open_trades()) - if open_trades >= self.config['max_open_trades']: + free_open_trades = self.get_free_open_trades() + if not free_open_trades: return None available_amount = self.wallets.get_free(self.config['stake_currency']) - return available_amount / (self.config['max_open_trades'] - open_trades) + return available_amount / free_open_trades def _check_available_stake_amount(self, stake_amount: Optional[float]) -> Optional[float]: """ @@ -321,12 +329,12 @@ class FreqtradeBot: pair, self.strategy.ticker_interval, self.dataprovider.ohlcv(pair, self.strategy.ticker_interval)) - if buy and not sell and len(Trade.get_open_trades()) < self.config['max_open_trades']: - stake_amount = self.get_trade_stake_amount(pair) - if stake_amount is None: + if buy and not sell: + if not self.get_free_open_trades(): logger.warning("Can't open a new trade: max number of trades is reached") continue + stake_amount = self.get_trade_stake_amount(pair) logger.info(f"Buy signal found: about create a new trade with stake_amount: " f"{stake_amount} ...") From d6ca562b0378a289b160372d1a4a7df0d2da2d67 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 28 Dec 2019 04:05:03 +0300 Subject: [PATCH 147/324] Make mypy happy and handle hypothetical case when stake_amount == 0 --- freqtrade/freqtradebot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 14d67b942..9c18d2d95 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -335,6 +335,10 @@ class FreqtradeBot: continue stake_amount = self.get_trade_stake_amount(pair) + if not stake_amount: + logger.warning("Stake amount is 0, ignoring trade") + continue + logger.info(f"Buy signal found: about create a new trade with stake_amount: " f"{stake_amount} ...") From fc98cf00372b8b5f0d8e9cdd6ea8dc3b182f3f4a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Dec 2019 06:25:45 +0100 Subject: [PATCH 148/324] Address PR feedback - change output to show Filename only --- docs/utils.md | 8 ++++---- freqtrade/utils.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/utils.md b/docs/utils.md index f7501ae9d..18deeac54 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -110,7 +110,7 @@ freqtrade new-hyperopt --userdir ~/.freqtrade/ --hyperopt AwesomeHyperopt ## List Strategies -Use the `list-strategies` subcommand to see all strategies in one particular folder. +Use the `list-strategies` subcommand to see all strategies in one particular directory. ``` freqtrade list-strategies --help @@ -135,12 +135,12 @@ Common arguments: ``` !!! Warning - Using this command will try to load all python files from a folder. This can be a security risk if untrusted files reside in this folder, since all module-level code is executed. + Using this command will try to load all python files from a directory. This can be a security risk if untrusted files reside in this directory, since all module-level code is executed. -Example: search default strategy folder within userdir +Example: search default strategy directory within userdir ``` bash -freqtrade list-strategies --user-data ~/.freqtrade/ +freqtrade list-strategies --userdir ~/.freqtrade/ ``` Example: search dedicated strategy path diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 06a62172a..f6e251154 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -226,7 +226,7 @@ def start_download_data(args: Dict[str, Any]) -> None: def start_list_strategies(args: Dict[str, Any]) -> None: """ - Print Strategies available in a folder + Print Strategies available in a directory """ config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) @@ -234,7 +234,7 @@ def start_list_strategies(args: Dict[str, Any]) -> None: strategies = StrategyResolver.search_all_objects(directory) # Sort alphabetically strategies = sorted(strategies, key=lambda x: x['name']) - strats_to_print = [{'name': s['name'], 'location': s['location']} for s in strategies] + strats_to_print = [{'name': s['name'], 'location': s['location'].name} for s in strategies] if args['print_one_column']: print('\n'.join([s['name'] for s in strategies])) From b2fb28453f460f89bfaef57a2ac5edae90749585 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 28 Dec 2019 06:39:25 +0100 Subject: [PATCH 149/324] Fix tests after changing output --- tests/test_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 185425efc..4cf7b5f23 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -644,7 +644,7 @@ def test_start_list_strategies(mocker, caplog, capsys): start_list_strategies(pargs) captured = capsys.readouterr() assert "TestStrategyLegacy" in captured.out - assert str(Path("strategy/legacy_strategy.py")) not in captured.out + assert "legacy_strategy.py" not in captured.out assert "DefaultStrategy" in captured.out # Test regular output @@ -658,7 +658,7 @@ def test_start_list_strategies(mocker, caplog, capsys): start_list_strategies(pargs) captured = capsys.readouterr() assert "TestStrategyLegacy" in captured.out - assert str(Path("strategy/legacy_strategy.py")) in captured.out + assert "legacy_strategy.py" in captured.out assert "DefaultStrategy" in captured.out From 5c39ebd0a0d9dea455762086d78a3ea5cbf3c995 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 28 Dec 2019 13:59:40 +0300 Subject: [PATCH 150/324] Adjust logging --- freqtrade/freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9c18d2d95..1cc7f32f4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -331,12 +331,12 @@ class FreqtradeBot: if buy and not sell: if not self.get_free_open_trades(): - logger.warning("Can't open a new trade: max number of trades is reached") + logger.debug("Can't open a new trade: max number of trades is reached") continue stake_amount = self.get_trade_stake_amount(pair) if not stake_amount: - logger.warning("Stake amount is 0, ignoring trade") + logger.debug("Stake amount is 0, ignoring possible trade for {pair}.") continue logger.info(f"Buy signal found: about create a new trade with stake_amount: " From 762604300f26afb5712d0ac92f01a57048a63f9e Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 29 Dec 2019 04:17:49 +0300 Subject: [PATCH 151/324] Refactor create_trades() --- freqtrade/freqtradebot.py | 104 ++++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 50 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 90eb7beba..d15624e9f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -294,65 +294,48 @@ class FreqtradeBot: # See also #2575 at github. return max(min_stake_amounts) / amount_reserve_percent - def create_trades(self) -> bool: + def create_trade(self, pair: str) -> bool: """ - Checks the implemented trading strategy for buy-signals, using the active pair whitelist. - If a pair triggers the buy_signal a new trade record gets created. - Checks pairs as long as the open trade count is below `max_open_trades`. - :return: True if at least one trade has been created. + Check the implemented trading strategy for buy-signals. + If the pair triggers the buy_signal a new trade record gets created. + :return: True if a trade has been created. """ - whitelist = copy.deepcopy(self.active_pair_whitelist) + logger.debug(f"create_trade for pair {pair}") - if not whitelist: - logger.info("Active pair whitelist is empty.") + if self.strategy.is_pair_locked(pair): + logger.info(f"Pair {pair} is currently locked.") return False - # Remove currently opened and latest pairs from 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.") - return False - - buycount = 0 # running get_signal on historical data fetched - for pair in whitelist: - if self.strategy.is_pair_locked(pair): - logger.info(f"Pair {pair} is currently locked.") - continue + (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, - self.dataprovider.ohlcv(pair, self.strategy.ticker_interval)) + if buy and not sell: + if not self.get_free_open_trades(): + logger.debug("Can't open a new trade: max number of trades is reached") + return False - if buy and not sell: - if not self.get_free_open_trades(): - logger.debug("Can't open a new trade: max number of trades is reached") - continue + stake_amount = self.get_trade_stake_amount(pair) + if not stake_amount: + logger.debug("Stake amount is 0, ignoring possible trade for {pair}.") + return False - stake_amount = self.get_trade_stake_amount(pair) - if not stake_amount: - logger.debug("Stake amount is 0, ignoring possible trade for {pair}.") - continue + logger.info(f"Buy signal found: about create a new trade with stake_amount: " + f"{stake_amount} ...") - logger.info(f"Buy signal found: about create a new trade with stake_amount: " - f"{stake_amount} ...") + bidstrat_check_depth_of_market = self.config.get('bid_strategy', {}).\ + get('check_depth_of_market', {}) + if (bidstrat_check_depth_of_market.get('enabled', False)) and\ + (bidstrat_check_depth_of_market.get('bids_to_ask_delta', 0) > 0): + if self._check_depth_of_market_buy(pair, bidstrat_check_depth_of_market): + return self.execute_buy(pair, stake_amount) + else: + return False - bidstrat_check_depth_of_market = self.config.get('bid_strategy', {}).\ - get('check_depth_of_market', {}) - if (bidstrat_check_depth_of_market.get('enabled', False)) and\ - (bidstrat_check_depth_of_market.get('bids_to_ask_delta', 0) > 0): - if self._check_depth_of_market_buy(pair, bidstrat_check_depth_of_market): - buycount += self.execute_buy(pair, stake_amount) - continue - - buycount += self.execute_buy(pair, stake_amount) - - return buycount > 0 + return self.execute_buy(pair, stake_amount) + else: + return False def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool: """ @@ -479,10 +462,31 @@ class FreqtradeBot: """ Tries to execute buy orders for trades in a safe way """ + result = False try: - # Create entity and execute trade - if not self.create_trades(): + whitelist = copy.deepcopy(self.active_pair_whitelist) + + if not whitelist: + logger.info("Active pair whitelist is empty.") + else: + # Remove currently opened and latest pairs from 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: + if self.create_trade(pair): + result = True + + if not result: logger.debug('Found no buy signals for whitelisted currencies. Trying again...') + except DependencyException as exception: logger.warning('Unable to create trade: %s', exception) From ce84f74528de4ee06977967a859722b68a1f6b31 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 29 Dec 2019 04:38:28 +0300 Subject: [PATCH 152/324] Adjust tests --- tests/rpc/test_rpc.py | 22 ++--- tests/rpc/test_rpc_apiserver.py | 8 +- tests/rpc/test_rpc_telegram.py | 22 ++--- tests/test_freqtradebot.py | 151 ++++++++++++++++---------------- tests/test_integration.py | 4 +- 5 files changed, 105 insertions(+), 102 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 3b897572c..a9f73e98d 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -41,7 +41,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: with pytest.raises(RPCException, match=r'.*no active trade*'): rpc._rpc_trade_status() - freqtradebot.create_trades() + freqtradebot.process_maybe_execute_buys() results = rpc._rpc_trade_status() assert { 'trade_id': 1, @@ -116,7 +116,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: with pytest.raises(RPCException, match=r'.*no active order*'): rpc._rpc_status_table(default_conf['stake_currency'], 'USD') - freqtradebot.create_trades() + freqtradebot.process_maybe_execute_buys() result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert "Since" in headers @@ -162,7 +162,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, rpc = RPC(freqtradebot) rpc._fiat_converter = CryptoToFiatConverter() # Create some test data - freqtradebot.create_trades() + freqtradebot.process_maybe_execute_buys() trade = Trade.query.first() assert trade @@ -217,7 +217,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) # Create some test data - freqtradebot.create_trades() + freqtradebot.process_maybe_execute_buys() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) @@ -231,7 +231,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, trade.close_date = datetime.utcnow() trade.is_open = False - freqtradebot.create_trades() + freqtradebot.process_maybe_execute_buys() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) @@ -299,7 +299,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, rpc = RPC(freqtradebot) # Create some test data - freqtradebot.create_trades() + freqtradebot.process_maybe_execute_buys() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) @@ -529,7 +529,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.create_trades() + freqtradebot.process_maybe_execute_buys() msg = rpc._rpc_forcesell('all') assert msg == {'result': 'Created sell orders for all open trades.'} @@ -563,7 +563,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: assert cancel_order_mock.call_count == 1 assert trade.amount == filled_amount - freqtradebot.create_trades() + freqtradebot.process_maybe_execute_buys() trade = Trade.query.filter(Trade.id == '2').first() amount = trade.amount # make an limit-buy open trade, if there is no 'filled', don't sell it @@ -582,7 +582,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: assert cancel_order_mock.call_count == 2 assert trade.amount == amount - freqtradebot.create_trades() + freqtradebot.process_maybe_execute_buys() # make an limit-sell open trade mocker.patch( 'freqtrade.exchange.Exchange.get_order', @@ -613,7 +613,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, rpc = RPC(freqtradebot) # Create some test data - freqtradebot.create_trades() + freqtradebot.process_maybe_execute_buys() trade = Trade.query.first() assert trade @@ -649,7 +649,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee) -> None: assert counts["current"] == 0 # Create some test data - freqtradebot.create_trades() + freqtradebot.process_maybe_execute_buys() counts = rpc._rpc_count() assert counts["current"] == 1 diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 36bb81e41..94af85349 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -267,7 +267,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets): assert rc.json["max"] == 1.0 # Create some test data - ftbot.create_trades() + ftbot.process_maybe_execute_buys() rc = client_get(client, f"{BASE_URI}/count") assert_response(rc) assert rc.json["current"] == 1.0 @@ -333,7 +333,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li assert len(rc.json) == 1 assert rc.json == {"error": "Error querying _profit: no closed trade"} - ftbot.create_trades() + ftbot.process_maybe_execute_buys() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade @@ -422,7 +422,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): assert_response(rc, 200) assert rc.json == [] - ftbot.create_trades() + ftbot.process_maybe_execute_buys() rc = client_get(client, f"{BASE_URI}/status") assert_response(rc) assert len(rc.json) == 1 @@ -552,7 +552,7 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets): assert_response(rc, 502) assert rc.json == {"error": "Error querying _forcesell: invalid argument"} - ftbot.create_trades() + ftbot.process_maybe_execute_buys() rc = client_post(client, f"{BASE_URI}/forcesell", data='{"tradeid": "1"}') diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index f8b9ca8ab..d9a7e2f5b 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -189,7 +189,7 @@ def test_status(default_conf, update, mocker, fee, ticker,) -> None: # Create some test data for _ in range(3): - freqtradebot.create_trades() + freqtradebot.process_maybe_execute_buys() telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -236,7 +236,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: msg_mock.reset_mock() # Create some test data - freqtradebot.create_trades() + freqtradebot.process_maybe_execute_buys() # Trigger status while we have a fulfilled order for the open trade telegram._status(update=update, context=MagicMock()) @@ -285,7 +285,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: msg_mock.reset_mock() # Create some test data - freqtradebot.create_trades() + freqtradebot.process_maybe_execute_buys() telegram._status_table(update=update, context=MagicMock()) @@ -322,7 +322,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, telegram = Telegram(freqtradebot) # Create some test data - freqtradebot.create_trades() + freqtradebot.process_maybe_execute_buys() trade = Trade.query.first() assert trade @@ -352,7 +352,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, msg_mock.reset_mock() freqtradebot.config['max_open_trades'] = 2 # Add two other trades - freqtradebot.create_trades() + freqtradebot.process_maybe_execute_buys() trades = Trade.query.all() for trade in trades: @@ -431,7 +431,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, msg_mock.reset_mock() # Create some test data - freqtradebot.create_trades() + freqtradebot.process_maybe_execute_buys() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade @@ -709,7 +709,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee, telegram = Telegram(freqtradebot) # Create some test data - freqtradebot.create_trades() + freqtradebot.process_maybe_execute_buys() trade = Trade.query.first() assert trade @@ -764,7 +764,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, telegram = Telegram(freqtradebot) # Create some test data - freqtradebot.create_trades() + freqtradebot.process_maybe_execute_buys() # Decrease the price and sell it mocker.patch.multiple( @@ -821,7 +821,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None telegram = Telegram(freqtradebot) # Create some test data - freqtradebot.create_trades() + freqtradebot.process_maybe_execute_buys() rpc_mock.reset_mock() # /forcesell all @@ -971,7 +971,7 @@ def test_performance_handle(default_conf, update, ticker, fee, telegram = Telegram(freqtradebot) # Create some test data - freqtradebot.create_trades() + freqtradebot.process_maybe_execute_buys() trade = Trade.query.first() assert trade @@ -1014,7 +1014,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: freqtradebot.state = State.RUNNING # Create some test data - freqtradebot.create_trades() + freqtradebot.process_maybe_execute_buys() msg_mock.reset_mock() telegram._count(update=update, context=MagicMock()) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 2f8c786fd..be6a9b392 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -247,7 +247,7 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf freqtrade.active_pair_whitelist = ['NEO/BTC'] patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() trade.update(limit_buy_order) ############################################# @@ -287,7 +287,7 @@ def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, freqtrade.active_pair_whitelist = ['NEO/BTC'] patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() trade.update(limit_buy_order) ############################################# @@ -310,7 +310,7 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() assert trade is not None @@ -318,7 +318,7 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, assert trade.is_open assert trade.open_date is not None - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.order_by(Trade.id.desc()).first() assert trade is not None @@ -462,7 +462,7 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) / 0.9, 8) -def test_create_trades(default_conf, ticker, limit_buy_order, fee, mocker) -> None: +def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -476,7 +476,7 @@ def test_create_trades(default_conf, ticker, limit_buy_order, fee, mocker) -> No whitelist = deepcopy(default_conf['exchange']['pair_whitelist']) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trades() + freqtrade.create_trade('ETH/BTC') trade = Trade.query.first() assert trade is not None @@ -494,8 +494,8 @@ def test_create_trades(default_conf, ticker, limit_buy_order, fee, mocker) -> No assert whitelist == default_conf['exchange']['pair_whitelist'] -def test_create_trades_no_stake_amount(default_conf, ticker, limit_buy_order, - fee, mocker) -> None: +def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5) @@ -509,11 +509,11 @@ def test_create_trades_no_stake_amount(default_conf, ticker, limit_buy_order, patch_get_signal(freqtrade) with pytest.raises(DependencyException, match=r'.*stake amount.*'): - freqtrade.create_trades() + freqtrade.create_trade('ETH/BTC') -def test_create_trades_minimal_amount(default_conf, ticker, limit_buy_order, - fee, mocker) -> None: +def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) @@ -527,13 +527,13 @@ def test_create_trades_minimal_amount(default_conf, ticker, limit_buy_order, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trades() + 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'] -def test_create_trades_too_small_stake_amount(default_conf, ticker, limit_buy_order, - fee, mocker) -> None: +def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order, + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) @@ -549,11 +549,11 @@ def test_create_trades_too_small_stake_amount(default_conf, ticker, limit_buy_or patch_get_signal(freqtrade) - assert not freqtrade.create_trades() + assert not freqtrade.create_trade('ETH/BTC') -def test_create_trades_limit_reached(default_conf, ticker, limit_buy_order, - fee, markets, mocker) -> None: +def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order, + fee, markets, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -569,12 +569,13 @@ def test_create_trades_limit_reached(default_conf, ticker, limit_buy_order, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - assert not freqtrade.create_trades() + assert not freqtrade.create_trade('ETH/BTC') assert freqtrade.get_trade_stake_amount('ETH/BTC') is None -def test_create_trades_no_pairs_let(default_conf, ticker, limit_buy_order, fee, - mocker, caplog) -> None: +def test_process_maybe_execute_buys_no_pairs_let(default_conf, ticker, + limit_buy_order, fee, + mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -588,14 +589,15 @@ def test_create_trades_no_pairs_let(default_conf, ticker, limit_buy_order, fee, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - assert freqtrade.create_trades() - assert not freqtrade.create_trades() - assert log_has("No currency pair in active pair whitelist, " - "but checking to sell open trades.", caplog) + freqtrade.process_maybe_execute_buys() + assert not log_has_re(r"No currency pair in active pair whitelist.*", caplog) + freqtrade.process_maybe_execute_buys() + assert log_has_re(r"No currency pair in active pair whitelist.*", caplog) -def test_create_trades_no_pairs_in_whitelist(default_conf, ticker, limit_buy_order, fee, - mocker, caplog) -> None: +def test_process_maybe_execute_buys_no_pairs_in_whitelist(default_conf, ticker, + limit_buy_order, fee, + mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -608,11 +610,11 @@ def test_create_trades_no_pairs_in_whitelist(default_conf, ticker, limit_buy_ord freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - assert not freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() assert log_has("Active pair whitelist is empty.", caplog) -def test_create_trades_no_signal(default_conf, fee, mocker) -> None: +def test_create_trade_no_signal(default_conf, fee, mocker) -> None: default_conf['dry_run'] = True patch_RPCManager(mocker) @@ -628,7 +630,7 @@ def test_create_trades_no_signal(default_conf, fee, mocker) -> None: Trade.query = MagicMock() Trade.query.filter = MagicMock() - assert not freqtrade.create_trades() + assert not freqtrade.create_trade('ETH/BTC') @pytest.mark.parametrize("max_open", range(0, 5)) @@ -646,7 +648,7 @@ def test_create_trades_multiple_trades(default_conf, ticker, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trades = Trade.get_open_trades() assert len(trades) == max_open @@ -672,7 +674,8 @@ def test_create_trades_preopen(default_conf, ticker, fee, mocker) -> None: assert len(Trade.get_open_trades()) == 2 # Create 2 new trades using create_trades - assert freqtrade.create_trades() + assert freqtrade.create_trade('ETH/BTC') + assert freqtrade.create_trade('NEO/BTC') trades = Trade.get_open_trades() assert len(trades) == 4 @@ -1056,7 +1059,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, # should unset stoploss_order_id and return true # as a trade actually happened caplog.clear() - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() trade.is_open = True trade.open_order_id = None @@ -1114,7 +1117,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() trade.is_open = True trade.open_order_id = '12345' @@ -1149,7 +1152,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, patch_get_signal(freqtrade) freqtrade.strategy.order_types['stoploss_on_exchange'] = True - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() caplog.clear() freqtrade.create_stoploss_order(trade, 200, 199) @@ -1207,7 +1210,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, patch_get_signal(freqtrade) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() trade.is_open = True trade.open_order_id = None @@ -1295,7 +1298,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c # setting stoploss_on_exchange_interval to 60 seconds freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60 patch_get_signal(freqtrade) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() trade.is_open = True trade.open_order_id = None @@ -1376,7 +1379,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, freqtrade.active_pair_whitelist = freqtrade.edge.adjust(freqtrade.active_pair_whitelist) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() trade.is_open = True trade.open_order_id = None @@ -1444,7 +1447,7 @@ def test_process_maybe_execute_buys(mocker, default_conf, caplog) -> None: caplog.set_level(logging.DEBUG) freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trades', MagicMock(return_value=False)) + mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trade', MagicMock(return_value=False)) freqtrade.process_maybe_execute_buys() assert log_has('Found no buy signals for whitelisted currencies. Trying again...', caplog) @@ -1453,7 +1456,7 @@ def test_process_maybe_execute_buys_exception(mocker, default_conf, caplog) -> N freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch( - 'freqtrade.freqtradebot.FreqtradeBot.create_trades', + 'freqtrade.freqtradebot.FreqtradeBot.create_trade', MagicMock(side_effect=DependencyException) ) freqtrade.process_maybe_execute_buys() @@ -1674,7 +1677,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mock freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() assert trade @@ -1711,7 +1714,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, fee, patch_get_signal(freqtrade, value=(True, True)) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() # Buy and Sell triggering, so doing nothing ... trades = Trade.query.all() @@ -1720,7 +1723,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, fee, # Buy is triggering, so buying ... patch_get_signal(freqtrade, value=(True, False)) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trades = Trade.query.all() nb_trades = len(trades) assert nb_trades == 1 @@ -1764,7 +1767,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, patch_get_signal(freqtrade, value=(True, False)) freqtrade.strategy.min_roi_reached = MagicMock(return_value=True) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() trade.is_open = True @@ -1795,7 +1798,7 @@ def test_handle_trade_use_sell_signal( freqtrade = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() trade.is_open = True @@ -1823,7 +1826,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, patch_get_signal(freqtrade) # Create trade and sell it - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() assert trade @@ -2183,7 +2186,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() assert trade @@ -2231,7 +2234,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() assert trade @@ -2281,7 +2284,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() assert trade @@ -2339,7 +2342,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c freqtrade.strategy.order_types['stoploss_on_exchange'] = True patch_get_signal(freqtrade) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() Trade.session = MagicMock() @@ -2382,7 +2385,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() assert trade @@ -2432,7 +2435,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() trades = [trade] freqtrade.process_maybe_execute_sells(trades) @@ -2484,7 +2487,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() assert trade @@ -2546,7 +2549,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2577,7 +2580,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2608,7 +2611,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker patch_get_signal(freqtrade) freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple( sell_flag=False, sell_type=SellType.NONE)) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2638,7 +2641,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocke patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2667,7 +2670,7 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order, patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() amnt = trade.amount @@ -2735,7 +2738,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() assert trade @@ -2754,7 +2757,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo # reinit - should buy other pair. caplog.clear() - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() assert log_has(f"Pair {trade.pair} is currently locked.", caplog) @@ -2779,7 +2782,7 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, mocker) -> patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=True) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2812,7 +2815,7 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() assert freqtrade.handle_trade(trade) is False @@ -2867,7 +2870,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2924,7 +2927,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2987,7 +2990,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() trade.update(limit_buy_order) @@ -3046,7 +3049,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=True) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() trade.update(limit_buy_order) @@ -3377,7 +3380,7 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, whitelist = deepcopy(default_conf['exchange']['pair_whitelist']) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() assert trade is not None @@ -3412,7 +3415,7 @@ def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_o # Save state of current whitelist freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() assert trade is None @@ -3514,7 +3517,7 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trade = Trade.query.first() assert trade @@ -3604,7 +3607,7 @@ def test_process_i_am_alive(default_conf, mocker, caplog): @pytest.mark.usefixtures("init_persistence") -def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order): +def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order, caplog): default_conf['dry_run'] = True # Initialize to 2 times stake amount default_conf['dry_run_wallet'] = 0.002 @@ -3621,12 +3624,12 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order) patch_get_signal(bot) assert bot.wallets.get_free('BTC') == 0.002 - bot.create_trades() + bot.process_maybe_execute_buys() trades = Trade.query.all() assert len(trades) == 2 bot.config['max_open_trades'] = 3 - with pytest.raises( - DependencyException, - match=r"Available balance \(0 BTC\) is lower than stake amount \(0.001 BTC\)"): - bot.create_trades() + bot.process_maybe_execute_buys() + assert log_has_re(r"Unable to create trade: " + r"Available balance \(0 BTC\) is lower than stake amount \(0.001 BTC\)", + caplog) diff --git a/tests/test_integration.py b/tests/test_integration.py index 11dbca225..2c95bc6e3 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -80,7 +80,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, patch_get_signal(freqtrade) # Create some test data - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() wallets_mock.reset_mock() Trade.session = MagicMock() @@ -153,7 +153,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc patch_get_signal(freqtrade) # Create 4 trades - freqtrade.create_trades() + freqtrade.process_maybe_execute_buys() trades = Trade.query.all() assert len(trades) == 4 From 04f28ed9bcd337e6f0dbdddd0c1d51f62e67c613 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sun, 29 Dec 2019 05:03:10 +0300 Subject: [PATCH 153/324] Refactor try/except: handle DependencyException for each pair separately --- freqtrade/freqtradebot.py | 40 +++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d15624e9f..e47a4a85c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -463,32 +463,32 @@ class FreqtradeBot: Tries to execute buy orders for trades in a safe way """ result = False - try: - whitelist = copy.deepcopy(self.active_pair_whitelist) + + whitelist = copy.deepcopy(self.active_pair_whitelist) + if not whitelist: + logger.info("Active pair whitelist is empty.") + else: + # Remove currently opened and latest pairs from 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("Active pair whitelist is empty.") + logger.info("No currency pair in active pair whitelist, " + "but checking to sell open trades.") else: - # Remove currently opened and latest pairs from 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: + # Create entity and execute trade for each pair from whitelist + for pair in whitelist: + try: if self.create_trade(pair): result = True + except DependencyException as exception: + logger.warning('Unable to create trade: %s', exception) - if not result: - logger.debug('Found no buy signals for whitelisted currencies. Trying again...') - - except DependencyException as exception: - logger.warning('Unable to create trade: %s', exception) + if not result: + logger.debug("Found no buy signals for whitelisted currencies. " + "Trying again...") def process_maybe_execute_sells(self, trades: List[Any]) -> None: """ From d1c45cf3f869968523f5b84e7949efd8130e166d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Dec 2019 13:07:51 +0100 Subject: [PATCH 154/324] Update analysis documentation to include kernel installation --- docs/data-analysis.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 115ce1916..8786a1199 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -8,6 +8,27 @@ You can analyze the results of backtests and trading history easily using Jupyte * 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. +### Using virtual environment with System jupyter + +Sometimes it can be desired to use a system-wide installation of Jupyter notebook, and use a jupyter kernel from the virtual environment. +This prevents you from installing the full jupyter suite multiple times per system, and provides an easy way to switch between tasks (freqtrade / other analytics tasks). + +For this to work, first activate your virtual environment and run the following commands: + +``` bash +# Activate virtual environment +source .env/bin/activate + +pip install ipykernel +ipython kernel install --user --name=freqtrade +# Restart jupyter (lab / notebook) +# select kernel "freqtrade" in the notebook +``` + +!!! 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 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. From 304d15e23694d1dc3ac1f85760dbc3af9996710a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Dec 2019 19:35:42 +0100 Subject: [PATCH 155/324] Small corrections --- docs/data-analysis.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/data-analysis.md b/docs/data-analysis.md index 8786a1199..fc4693b17 100644 --- a/docs/data-analysis.md +++ b/docs/data-analysis.md @@ -8,7 +8,7 @@ You can analyze the results of backtests and trading history easily using Jupyte * 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. -### Using virtual environment with System jupyter +### Using virtual environment with system-wide Jupyter installation Sometimes it can be desired to use a system-wide installation of Jupyter notebook, and use a jupyter kernel from the virtual environment. This prevents you from installing the full jupyter suite multiple times per system, and provides an easy way to switch between tasks (freqtrade / other analytics tasks). @@ -26,7 +26,7 @@ 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 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). + 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 From df7ceb4ccbbfd35fe7eaec2d37dea273b89a4873 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 29 Dec 2019 19:51:47 +0100 Subject: [PATCH 156/324] Fix misinformation in /status table --- freqtrade/rpc/rpc.py | 2 +- tests/rpc/test_rpc.py | 2 +- tests/rpc/test_rpc_telegram.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 35c312743..0a79d350b 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -142,7 +142,7 @@ class RPC: def _rpc_status_table(self, stake_currency, fiat_display_currency: str) -> Tuple[List, List]: trades = Trade.get_open_trades() if not trades: - raise RPCException('no active order') + raise RPCException('no active trade') else: trades_list = [] for trade in trades: diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 3b897572c..c5bb0ca2c 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -113,7 +113,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: rpc = RPC(freqtradebot) freqtradebot.state = State.RUNNING - with pytest.raises(RPCException, match=r'.*no active order*'): + with pytest.raises(RPCException, match=r'.*no active trade*'): rpc._rpc_status_table(default_conf['stake_currency'], 'USD') freqtradebot.create_trades() diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index f8b9ca8ab..ddbc35bd5 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -275,13 +275,13 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: # Status table is also enabled when stopped telegram._status_table(update=update, context=MagicMock()) assert msg_mock.call_count == 1 - assert 'no active order' in msg_mock.call_args_list[0][0][0] + assert 'no active trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() freqtradebot.state = State.RUNNING telegram._status_table(update=update, context=MagicMock()) assert msg_mock.call_count == 1 - assert 'no active order' in msg_mock.call_args_list[0][0][0] + assert 'no active trade' in msg_mock.call_args_list[0][0][0] msg_mock.reset_mock() # Create some test data From 20a132651f460b3ab00f4e4032173b8c58eb7bba Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 30 Dec 2019 07:34:49 +0000 Subject: [PATCH 157/324] Bump ccxt from 1.21.12 to 1.21.23 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.21.12 to 1.21.23. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/1.21.12...1.21.23) 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 9d0fd4756..add1ea0fd 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.21.12 +ccxt==1.21.23 SQLAlchemy==1.3.12 python-telegram-bot==12.2.0 arrow==0.15.4 From de23f3928de5639a01efe424e5040354196c59e8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Dec 2019 09:56:42 +0100 Subject: [PATCH 158/324] Add trailing_only_offset to template and sample --- freqtrade/templates/base_strategy.py.j2 | 1 + freqtrade/templates/sample_strategy.py | 1 + 2 files changed, 2 insertions(+) diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index 73a4c7a5a..32573ec9e 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -47,6 +47,7 @@ class {{ strategy }}(IStrategy): # Trailing stoploss trailing_stop = False + # trailing_only_offset_is_reached = False # trailing_stop_positive = 0.01 # trailing_stop_positive_offset = 0.0 # Disabled / not configured diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 02bf24e7e..228e56b82 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -48,6 +48,7 @@ class SampleStrategy(IStrategy): # Trailing stoploss trailing_stop = False + # trailing_only_offset_is_reached = False # trailing_stop_positive = 0.01 # trailing_stop_positive_offset = 0.0 # Disabled / not configured From 4c9295fe2d657fdf373601ea8d2350fb1990ebc8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Dec 2019 14:00:34 +0100 Subject: [PATCH 159/324] Rename Bid-strategy helpervariable to something shorter avoids unnecessary wrapping... --- freqtrade/freqtradebot.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e47a4a85c..68091ed76 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -313,7 +313,7 @@ class FreqtradeBot: if buy and not sell: if not self.get_free_open_trades(): - logger.debug("Can't open a new trade: max number of trades is reached") + logger.debug("Can't open a new trade: max number of trades is reached.") return False stake_amount = self.get_trade_stake_amount(pair) @@ -324,11 +324,10 @@ class FreqtradeBot: logger.info(f"Buy signal found: about create a new trade with stake_amount: " f"{stake_amount} ...") - bidstrat_check_depth_of_market = self.config.get('bid_strategy', {}).\ - get('check_depth_of_market', {}) - if (bidstrat_check_depth_of_market.get('enabled', False)) and\ - (bidstrat_check_depth_of_market.get('bids_to_ask_delta', 0) > 0): - if self._check_depth_of_market_buy(pair, bidstrat_check_depth_of_market): + bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {}) + if ((bid_check_dom.get('enabled', False)) and + (bid_check_dom.get('bids_to_ask_delta', 0) > 0)): + if self._check_depth_of_market_buy(pair, bid_check_dom): return self.execute_buy(pair, stake_amount) else: return False From fb3a53b8af27c49eb4c8a26c271437a581dd187e Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Dec 2019 14:28:34 +0100 Subject: [PATCH 160/324] Use ExchangeResolver for edge_cli too --- freqtrade/optimize/edge_cli.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index ea5cc663d..4944f1dbb 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -12,8 +12,7 @@ from freqtrade import constants from freqtrade.configuration import (TimeRange, remove_credentials, validate_config_consistency) from freqtrade.edge import Edge -from freqtrade.exchange import Exchange -from freqtrade.resolvers import StrategyResolver +from freqtrade.resolvers import StrategyResolver, ExchangeResolver logger = logging.getLogger(__name__) @@ -33,7 +32,7 @@ class EdgeCli: # Reset keys for edge remove_credentials(self.config) self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT - self.exchange = Exchange(self.config) + self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) self.strategy = StrategyResolver.load_strategy(self.config) validate_config_consistency(self.config) From 20abf67779a1c9f778e6339df0abd0af63433154 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Dec 2019 14:29:36 +0100 Subject: [PATCH 161/324] Add Debug "code" for randomly failing test --- tests/test_utils.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index 4cf7b5f23..8545eb817 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -448,6 +448,9 @@ def test_create_datadir(caplog, mocker): # Ensure that caplog is empty before starting ... # Should prevent random failures. caplog.clear() + # Added assert here to analyze random test-failures ... + assert len(caplog.record_tuples) == 0 + cud = mocker.patch("freqtrade.utils.create_userdata_dir", MagicMock()) csf = mocker.patch("freqtrade.utils.copy_sample_files", MagicMock()) args = [ From 024aa3ab6b6e5e0ba11bf7cfe0035cb83423bbf3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Dec 2019 14:57:26 +0100 Subject: [PATCH 162/324] Move exceptions to seperate module --- freqtrade/__init__.py | 31 ------------------------------- freqtrade/exceptions.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 31 deletions(-) create mode 100644 freqtrade/exceptions.py diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 83fee0b0d..e1f65d4fe 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -11,34 +11,3 @@ if __version__ == 'develop': except Exception: # git not available, ignore pass - - -class DependencyException(Exception): - """ - Indicates that an assumed dependency is not met. - This could happen when there is currently not enough money on the account. - """ - - -class OperationalException(Exception): - """ - Requires manual intervention and will usually stop the bot. - This happens when an exchange returns an unexpected error during runtime - or given configuration is invalid. - """ - - -class InvalidOrderException(Exception): - """ - This is returned when the order is not valid. Example: - If stoploss on exchange order is hit, then trying to cancel the order - should return this exception. - """ - - -class TemporaryError(Exception): - """ - Temporary network or exchange related error. - This could happen when an exchange is congested, unavailable, or the user - has networking problems. Usually resolves itself after a time. - """ diff --git a/freqtrade/exceptions.py b/freqtrade/exceptions.py new file mode 100644 index 000000000..80d45dd86 --- /dev/null +++ b/freqtrade/exceptions.py @@ -0,0 +1,31 @@ + + +class DependencyException(Exception): + """ + Indicates that an assumed dependency is not met. + This could happen when there is currently not enough money on the account. + """ + + +class OperationalException(Exception): + """ + Requires manual intervention and will usually stop the bot. + This happens when an exchange returns an unexpected error during runtime + or given configuration is invalid. + """ + + +class InvalidOrderException(Exception): + """ + This is returned when the order is not valid. Example: + If stoploss on exchange order is hit, then trying to cancel the order + should return this exception. + """ + + +class TemporaryError(Exception): + """ + Temporary network or exchange related error. + This could happen when an exchange is congested, unavailable, or the user + has networking problems. Usually resolves itself after a time. + """ From 1ffda29fd2aeb95cffedfaded274c4d07e404436 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Dec 2019 15:02:17 +0100 Subject: [PATCH 163/324] Adjust improts to new exception location --- freqtrade/configuration/check_exchange.py | 4 ++-- freqtrade/configuration/config_validation.py | 3 ++- freqtrade/configuration/configuration.py | 5 +++-- freqtrade/configuration/deprecated_settings.py | 2 +- freqtrade/configuration/directory_operations.py | 2 +- freqtrade/configuration/load_config.py | 2 +- freqtrade/data/history.py | 6 ++++-- freqtrade/edge/__init__.py | 4 ++-- freqtrade/exchange/binance.py | 4 ++-- freqtrade/exchange/common.py | 2 +- freqtrade/exchange/exchange.py | 4 ++-- freqtrade/exchange/kraken.py | 2 +- freqtrade/freqtradebot.py | 6 +++--- freqtrade/loggers.py | 2 +- freqtrade/main.py | 2 +- freqtrade/optimize/__init__.py | 4 ++-- freqtrade/optimize/backtesting.py | 2 +- freqtrade/optimize/hyperopt.py | 6 +++--- freqtrade/optimize/hyperopt_interface.py | 6 ++---- freqtrade/pairlist/VolumePairList.py | 2 +- freqtrade/pairlist/pairlistmanager.py | 5 +++-- freqtrade/persistence.py | 2 +- freqtrade/plot/plot_utils.py | 2 +- freqtrade/resolvers/hyperopt_resolver.py | 2 +- freqtrade/resolvers/iresolver.py | 2 +- freqtrade/resolvers/strategy_resolver.py | 2 +- freqtrade/rpc/rpc.py | 2 +- freqtrade/utils.py | 2 +- freqtrade/worker.py | 4 ++-- tests/edge/test_edge.py | 2 +- tests/exchange/test_binance.py | 4 ++-- tests/exchange/test_exchange.py | 4 ++-- tests/optimize/test_backtesting.py | 4 ++-- tests/optimize/test_hyperopt.py | 2 +- tests/pairlist/test_pairlist.py | 2 +- tests/rpc/test_rpc.py | 4 ++-- tests/strategy/test_strategy.py | 2 +- tests/test_configuration.py | 2 +- tests/test_directory_operations.py | 2 +- tests/test_freqtradebot.py | 10 +++++----- tests/test_main.py | 2 +- tests/test_persistence.py | 3 ++- tests/test_plotting.py | 6 +++--- tests/test_utils.py | 2 +- 44 files changed, 74 insertions(+), 70 deletions(-) diff --git a/freqtrade/configuration/check_exchange.py b/freqtrade/configuration/check_exchange.py index c739de692..0076b1c5d 100644 --- a/freqtrade/configuration/check_exchange.py +++ b/freqtrade/configuration/check_exchange.py @@ -1,9 +1,9 @@ import logging from typing import Any, Dict -from freqtrade import OperationalException +from freqtrade.exceptions import OperationalException from freqtrade.exchange import (available_exchanges, get_exchange_bad_reason, - is_exchange_known_ccxt, is_exchange_bad, + is_exchange_bad, is_exchange_known_ccxt, is_exchange_officially_supported) from freqtrade.state import RunMode diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 068364884..43eead46a 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -4,7 +4,8 @@ from typing import Any, Dict from jsonschema import Draft4Validator, validators from jsonschema.exceptions import ValidationError, best_match -from freqtrade import constants, OperationalException +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 f73b52c10..a8b7638c8 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -7,15 +7,16 @@ from copy import deepcopy from pathlib import Path from typing import Any, Callable, Dict, List, Optional -from freqtrade import OperationalException, constants +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.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 RunMode, TRADING_MODES, NON_UTIL_MODES +from freqtrade.state import NON_UTIL_MODES, TRADING_MODES, RunMode logger = logging.getLogger(__name__) diff --git a/freqtrade/configuration/deprecated_settings.py b/freqtrade/configuration/deprecated_settings.py index b1e3535a3..260aae419 100644 --- a/freqtrade/configuration/deprecated_settings.py +++ b/freqtrade/configuration/deprecated_settings.py @@ -5,7 +5,7 @@ Functions to handle deprecated settings import logging from typing import Any, Dict -from freqtrade import OperationalException +from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) diff --git a/freqtrade/configuration/directory_operations.py b/freqtrade/configuration/directory_operations.py index 556f27742..43a209483 100644 --- a/freqtrade/configuration/directory_operations.py +++ b/freqtrade/configuration/directory_operations.py @@ -3,7 +3,7 @@ import shutil from pathlib import Path from typing import Any, Dict, Optional -from freqtrade import OperationalException +from freqtrade.exceptions import OperationalException from freqtrade.constants import USER_DATA_FILES logger = logging.getLogger(__name__) diff --git a/freqtrade/configuration/load_config.py b/freqtrade/configuration/load_config.py index 7a3ca1798..19179c6c3 100644 --- a/freqtrade/configuration/load_config.py +++ b/freqtrade/configuration/load_config.py @@ -6,7 +6,7 @@ import logging import sys from typing import Any, Dict -from freqtrade import OperationalException +from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) diff --git a/freqtrade/data/history.py b/freqtrade/data/history.py index 4c5c0521f..30d168f78 100644 --- a/freqtrade/data/history.py +++ b/freqtrade/data/history.py @@ -16,10 +16,12 @@ from typing import Any, Dict, List, Optional, Tuple import arrow from pandas import DataFrame -from freqtrade import OperationalException, misc +from freqtrade import misc from freqtrade.configuration import TimeRange from freqtrade.data.converter import parse_ticker_dataframe, trades_to_ohlcv -from freqtrade.exchange import Exchange, timeframe_to_minutes, timeframe_to_seconds +from freqtrade.exceptions import OperationalException +from freqtrade.exchange import (Exchange, timeframe_to_minutes, + timeframe_to_seconds) logger = logging.getLogger(__name__) diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 9ad2485ef..19d65d9d7 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -8,12 +8,12 @@ import numpy as np import utils_find_1st as utf1st from pandas import DataFrame -from freqtrade import constants, OperationalException +from freqtrade import constants from freqtrade.configuration import TimeRange from freqtrade.data import history +from freqtrade.exceptions import OperationalException from freqtrade.strategy.interface import SellType - logger = logging.getLogger(__name__) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index b5507981f..96f72fcf5 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -4,8 +4,8 @@ from typing import Dict import ccxt -from freqtrade import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +from freqtrade.exceptions import (DependencyException, InvalidOrderException, + OperationalException, TemporaryError) from freqtrade.exchange import Exchange logger = logging.getLogger(__name__) diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py index ed30b95c7..b38ed35a3 100644 --- a/freqtrade/exchange/common.py +++ b/freqtrade/exchange/common.py @@ -1,6 +1,6 @@ import logging -from freqtrade import DependencyException, TemporaryError +from freqtrade.exceptions import DependencyException, TemporaryError logger = logging.getLogger(__name__) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 01e84c06e..3ef32db62 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -17,9 +17,9 @@ import ccxt.async_support as ccxt_async from ccxt.base.decimal_to_precision import ROUND_DOWN, ROUND_UP from pandas import DataFrame -from freqtrade import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) from freqtrade.data.converter import parse_ticker_dataframe +from freqtrade.exceptions import (DependencyException, InvalidOrderException, + OperationalException, TemporaryError) from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async from freqtrade.misc import deep_merge_dicts diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index f548489bc..9bcd9cc1f 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -4,7 +4,7 @@ from typing import Dict import ccxt -from freqtrade import OperationalException, TemporaryError +from freqtrade.exceptions import OperationalException, TemporaryError from freqtrade.exchange import Exchange from freqtrade.exchange.exchange import retrier diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 90eb7beba..f33a8cd03 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -12,17 +12,17 @@ from typing import Any, Dict, List, Optional, Tuple import arrow from requests.exceptions import RequestException -from freqtrade import (DependencyException, InvalidOrderException, __version__, - constants, persistence) +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 from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date +from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.rpc import RPCManager, RPCMessageType -from freqtrade.pairlist.pairlistmanager import PairListManager from freqtrade.state import State from freqtrade.strategy.interface import IStrategy, SellType from freqtrade.wallets import Wallets diff --git a/freqtrade/loggers.py b/freqtrade/loggers.py index 27f16ecc3..c69388430 100644 --- a/freqtrade/loggers.py +++ b/freqtrade/loggers.py @@ -5,7 +5,7 @@ from logging import Formatter from logging.handlers import RotatingFileHandler, SysLogHandler from typing import Any, Dict, List -from freqtrade import OperationalException +from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) diff --git a/freqtrade/main.py b/freqtrade/main.py index 7afaeb1a2..62d2fc05d 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -13,8 +13,8 @@ if sys.version_info < (3, 6): import logging from typing import Any, List -from freqtrade import OperationalException from freqtrade.configuration import Arguments +from freqtrade.exceptions import OperationalException logger = logging.getLogger('freqtrade') diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 1f2f588ef..34760372f 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -1,11 +1,11 @@ import logging from typing import Any, Dict -from freqtrade import DependencyException, constants, OperationalException +from freqtrade import constants +from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.state import RunMode from freqtrade.utils import setup_utils_configuration - logger = logging.getLogger(__name__) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index a8fe90a06..9bd0327e0 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -12,11 +12,11 @@ from typing import Any, Dict, List, NamedTuple, Optional from pandas import DataFrame from tabulate import tabulate -from freqtrade import OperationalException from freqtrade.configuration import (TimeRange, remove_credentials, validate_config_consistency) from freqtrade.data import history from freqtrade.data.dataprovider import DataProvider +from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import file_dump_json from freqtrade.persistence import Trade diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 48f883ac5..a8f1a71ef 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -22,13 +22,13 @@ from joblib import (Parallel, cpu_count, delayed, dump, load, wrap_non_picklable_objects) from pandas import DataFrame -from freqtrade import OperationalException from freqtrade.data.history import get_timerange, trim_dataframe +from freqtrade.exceptions import OperationalException 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: F4 -from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4 +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) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 856f3eee7..d7d917c19 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -4,17 +4,15 @@ This module defines the interface to apply for hyperopt """ import logging import math - from abc import ABC -from typing import Dict, Any, Callable, List +from typing import Any, Callable, Dict, List from skopt.space import Categorical, Dimension, Integer, Real -from freqtrade import OperationalException +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/pairlist/VolumePairList.py b/freqtrade/pairlist/VolumePairList.py index 2df9ba691..4ac9935ba 100644 --- a/freqtrade/pairlist/VolumePairList.py +++ b/freqtrade/pairlist/VolumePairList.py @@ -8,7 +8,7 @@ import logging from datetime import datetime from typing import Dict, List -from freqtrade import OperationalException +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 1530710d2..55828c6ef 100644 --- a/freqtrade/pairlist/pairlistmanager.py +++ b/freqtrade/pairlist/pairlistmanager.py @@ -4,11 +4,12 @@ Static List provider Provides lists as configured in config.json """ -from cachetools import TTLCache, cached import logging from typing import Dict, List -from freqtrade import OperationalException +from cachetools import TTLCache, cached + +from freqtrade.exceptions import OperationalException from freqtrade.pairlist.IPairList import IPairList from freqtrade.resolvers import PairListResolver diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 993b68bc7..75116f1e3 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -16,7 +16,7 @@ from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.orm.session import sessionmaker from sqlalchemy.pool import StaticPool -from freqtrade import OperationalException +from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) diff --git a/freqtrade/plot/plot_utils.py b/freqtrade/plot/plot_utils.py index 8de0eb9e7..9eff08396 100644 --- a/freqtrade/plot/plot_utils.py +++ b/freqtrade/plot/plot_utils.py @@ -1,6 +1,6 @@ from typing import Any, Dict -from freqtrade import OperationalException +from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode from freqtrade.utils import setup_utils_configuration diff --git a/freqtrade/resolvers/hyperopt_resolver.py b/freqtrade/resolvers/hyperopt_resolver.py index c26fd09f2..ddf461252 100644 --- a/freqtrade/resolvers/hyperopt_resolver.py +++ b/freqtrade/resolvers/hyperopt_resolver.py @@ -7,8 +7,8 @@ import logging from pathlib import Path from typing import Dict -from freqtrade import OperationalException from freqtrade.constants import DEFAULT_HYPEROPT_LOSS, USERPATH_HYPEROPTS +from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt_interface import IHyperOpt from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss from freqtrade.resolvers import IResolver diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index e3c0d1ad0..5a844097c 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -9,7 +9,7 @@ import logging from pathlib import Path from typing import Any, Dict, Generator, List, Optional, Tuple, Type, Union -from freqtrade import OperationalException +from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) diff --git a/freqtrade/resolvers/strategy_resolver.py b/freqtrade/resolvers/strategy_resolver.py index 4fd5c586a..9e64f38df 100644 --- a/freqtrade/resolvers/strategy_resolver.py +++ b/freqtrade/resolvers/strategy_resolver.py @@ -11,9 +11,9 @@ from inspect import getfullargspec from pathlib import Path from typing import Dict, Optional -from freqtrade import OperationalException from freqtrade.constants import (REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES, USERPATH_STRATEGY) +from freqtrade.exceptions import OperationalException from freqtrade.resolvers import IResolver from freqtrade.strategy.interface import IStrategy diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 0a79d350b..c187dae4f 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 import DependencyException, TemporaryError +from freqtrade.exceptions import DependencyException, TemporaryError from freqtrade.misc import shorten_date from freqtrade.persistence import Trade from freqtrade.rpc.fiat_convert import CryptoToFiatConverter diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 5a5662e4b..45520ecf7 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -11,7 +11,6 @@ import rapidjson from colorama import init as colorama_init from tabulate import tabulate -from freqtrade import OperationalException from freqtrade.configuration import (Configuration, TimeRange, remove_credentials) from freqtrade.configuration.directory_operations import (copy_sample_files, @@ -20,6 +19,7 @@ from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGY 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 (available_exchanges, ccxt_exchanges, market_is_active, symbol_is_pair) from freqtrade.misc import plural, render_template diff --git a/freqtrade/worker.py b/freqtrade/worker.py index 8e4be9d43..22651d269 100755 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -8,9 +8,9 @@ from typing import Any, Callable, Dict, Optional import sdnotify -from freqtrade import (OperationalException, TemporaryError, __version__, - constants) +from freqtrade import __version__, constants from freqtrade.configuration import Configuration +from freqtrade.exceptions import OperationalException, TemporaryError from freqtrade.freqtradebot import FreqtradeBot from freqtrade.rpc import RPCMessageType from freqtrade.state import State diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index 3a866c0a8..ef1280fa4 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -10,7 +10,7 @@ import numpy as np import pytest from pandas import DataFrame, to_datetime -from freqtrade import OperationalException +from freqtrade.exceptions import OperationalException from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.strategy.interface import SellType diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 7720a7d2e..0a12c1cb1 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -4,8 +4,8 @@ from unittest.mock import MagicMock import ccxt import pytest -from freqtrade import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +from freqtrade.exceptions import (DependencyException, InvalidOrderException, + OperationalException, TemporaryError) from tests.conftest import get_patched_exchange diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 2f95e4e01..cb40bdbd9 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -11,8 +11,8 @@ import ccxt import pytest from pandas import DataFrame -from freqtrade import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError) +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, diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 8a27c591f..4e2fd01cf 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -10,13 +10,14 @@ import pandas as pd import pytest from arrow import Arrow -from freqtrade import DependencyException, OperationalException, constants +from freqtrade import constants from freqtrade.configuration import TimeRange from freqtrade.data import history from freqtrade.data.btanalysis import evaluate_result_multi from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange +from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.optimize import setup_configuration, start_backtesting from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode @@ -25,7 +26,6 @@ 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 fb492be35..473d760ac 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -9,7 +9,7 @@ import pytest from arrow import Arrow from filelock import Timeout -from freqtrade import OperationalException +from freqtrade.exceptions import OperationalException from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file from freqtrade.optimize import setup_configuration, start_hyperopt diff --git a/tests/pairlist/test_pairlist.py b/tests/pairlist/test_pairlist.py index 21929de2b..ac4cbc813 100644 --- a/tests/pairlist/test_pairlist.py +++ b/tests/pairlist/test_pairlist.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, PropertyMock import pytest -from freqtrade import OperationalException +from freqtrade.exceptions import OperationalException from freqtrade.constants import AVAILABLE_PAIRLISTS from freqtrade.resolvers import PairListResolver from freqtrade.pairlist.pairlistmanager import PairListManager diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index c5bb0ca2c..31632bd70 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -7,13 +7,13 @@ from unittest.mock import ANY, MagicMock, PropertyMock import pytest from numpy import isnan -from freqtrade import DependencyException, TemporaryError from freqtrade.edge import PairInfo +from freqtrade.exceptions import DependencyException, 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 patch_get_signal, get_patched_freqtradebot +from tests.conftest import get_patched_freqtradebot, patch_get_signal # Functions for recurrent object patching diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py index 10b9f3466..d3977ae44 100644 --- a/tests/strategy/test_strategy.py +++ b/tests/strategy/test_strategy.py @@ -8,7 +8,7 @@ from pathlib import Path import pytest from pandas import DataFrame -from freqtrade import OperationalException +from freqtrade.exceptions import OperationalException from freqtrade.resolvers import StrategyResolver from freqtrade.strategy.interface import IStrategy from tests.conftest import log_has, log_has_re diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 6564417b3..ee3d23131 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -10,7 +10,6 @@ from unittest.mock import MagicMock import pytest from jsonschema import ValidationError -from freqtrade import OperationalException from freqtrade.configuration import (Arguments, Configuration, check_exchange, remove_credentials, validate_config_consistency) @@ -20,6 +19,7 @@ from freqtrade.configuration.deprecated_settings import ( process_temporary_deprecated_settings) from freqtrade.configuration.load_config import load_config_file 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.state import RunMode from tests.conftest import (log_has, log_has_re, diff --git a/tests/test_directory_operations.py b/tests/test_directory_operations.py index db41e2da2..889338a64 100644 --- a/tests/test_directory_operations.py +++ b/tests/test_directory_operations.py @@ -4,10 +4,10 @@ from unittest.mock import MagicMock import pytest -from freqtrade import OperationalException 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 2f8c786fd..3f5b7bd54 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -11,9 +11,9 @@ import arrow import pytest import requests -from freqtrade import (DependencyException, InvalidOrderException, - OperationalException, TemporaryError, constants) -from freqtrade.constants import MATH_CLOSE_PREC +from freqtrade.constants import MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT +from freqtrade.exceptions import (DependencyException, InvalidOrderException, + OperationalException, TemporaryError) from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.rpc import RPCMessageType @@ -163,7 +163,7 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, ) conf = deepcopy(default_conf) - conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT + conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT conf['max_open_trades'] = 2 freqtrade = FreqtradeBot(conf) @@ -564,7 +564,7 @@ def test_create_trades_limit_reached(default_conf, ticker, limit_buy_order, get_fee=fee, ) default_conf['max_open_trades'] = 0 - default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT + default_conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) diff --git a/tests/test_main.py b/tests/test_main.py index 03e6a7ce9..83be01999 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -5,8 +5,8 @@ from unittest.mock import MagicMock, PropertyMock import pytest -from freqtrade import OperationalException from freqtrade.configuration import Arguments +from freqtrade.exceptions import OperationalException from freqtrade.freqtradebot import FreqtradeBot from freqtrade.main import main from freqtrade.state import State diff --git a/tests/test_persistence.py b/tests/test_persistence.py index b9a636e1a..6bd7971a7 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -6,7 +6,8 @@ import arrow import pytest from sqlalchemy import create_engine -from freqtrade import OperationalException, constants +from freqtrade import constants +from freqtrade.exceptions import OperationalException from freqtrade.persistence import Trade, clean_dry_run_db, init from tests.conftest import log_has diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 31502cafc..9934d2493 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -7,17 +7,17 @@ import plotly.graph_objects as go import pytest from plotly.subplots import make_subplots -from freqtrade import OperationalException 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.plot_utils import start_plot_dataframe, start_plot_profit from freqtrade.plot.plotting import (add_indicators, add_profit, - load_and_plot_trades, generate_candlestick_graph, generate_plot_filename, generate_profit_graph, init_plotscript, - plot_profit, plot_trades, store_plot_file) + load_and_plot_trades, plot_profit, + plot_trades, store_plot_file) from freqtrade.strategy.default_strategy import DefaultStrategy from tests.conftest import get_args, log_has, log_has_re diff --git a/tests/test_utils.py b/tests/test_utils.py index 4cf7b5f23..2e2499337 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, PropertyMock import pytest -from freqtrade import OperationalException +from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode from freqtrade.utils import (setup_utils_configuration, start_create_userdir, start_download_data, start_hyperopt_list, From 8e9a3e8fc8e5de762455ca7472c81e5ad15f8591 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 30 Dec 2019 15:11:07 +0100 Subject: [PATCH 164/324] Capture FtBaseException at the outermost level --- freqtrade/exceptions.py | 28 +++++++++++++++++----------- freqtrade/main.py | 4 ++-- tests/test_main.py | 4 ++-- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/freqtrade/exceptions.py b/freqtrade/exceptions.py index 80d45dd86..2f05ddb57 100644 --- a/freqtrade/exceptions.py +++ b/freqtrade/exceptions.py @@ -1,21 +1,27 @@ -class DependencyException(Exception): +class FreqtradeException(Exception): + """ + Freqtrade base exception. Handled at the outermost level. + All other exception types are subclasses of this exception type. + """ + + +class OperationalException(FreqtradeException): + """ + Requires manual intervention and will stop the bot. + Most of the time, this is caused by an invalid Configuration. + """ + + +class DependencyException(FreqtradeException): """ Indicates that an assumed dependency is not met. This could happen when there is currently not enough money on the account. """ -class OperationalException(Exception): - """ - Requires manual intervention and will usually stop the bot. - This happens when an exchange returns an unexpected error during runtime - or given configuration is invalid. - """ - - -class InvalidOrderException(Exception): +class InvalidOrderException(FreqtradeException): """ This is returned when the order is not valid. Example: If stoploss on exchange order is hit, then trying to cancel the order @@ -23,7 +29,7 @@ class InvalidOrderException(Exception): """ -class TemporaryError(Exception): +class TemporaryError(FreqtradeException): """ Temporary network or exchange related error. This could happen when an exchange is congested, unavailable, or the user diff --git a/freqtrade/main.py b/freqtrade/main.py index 62d2fc05d..811e29864 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -4,6 +4,7 @@ Main Freqtrade bot script. Read the documentation to know what cli arguments you need. """ +from freqtrade.exceptions import FreqtradeException, OperationalException import sys # check min. python version if sys.version_info < (3, 6): @@ -14,7 +15,6 @@ import logging from typing import Any, List from freqtrade.configuration import Arguments -from freqtrade.exceptions import OperationalException logger = logging.getLogger('freqtrade') @@ -50,7 +50,7 @@ def main(sysargv: List[str] = None) -> None: except KeyboardInterrupt: logger.info('SIGINT received, aborting ...') return_code = 0 - except OperationalException as e: + except FreqtradeException as e: logger.error(str(e)) return_code = 2 except Exception: diff --git a/tests/test_main.py b/tests/test_main.py index 83be01999..76b1bf658 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock, PropertyMock import pytest from freqtrade.configuration import Arguments -from freqtrade.exceptions import OperationalException +from freqtrade.exceptions import OperationalException, FreqtradeException from freqtrade.freqtradebot import FreqtradeBot from freqtrade.main import main from freqtrade.state import State @@ -96,7 +96,7 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None: mocker.patch('freqtrade.freqtradebot.FreqtradeBot.cleanup', MagicMock()) mocker.patch( 'freqtrade.worker.Worker._worker', - MagicMock(side_effect=OperationalException('Oh snap!')) + MagicMock(side_effect=FreqtradeException('Oh snap!')) ) patched_configuration_load_config_file(mocker, default_conf) mocker.patch('freqtrade.wallets.Wallets.update', MagicMock()) From 4d56e3b36e02d2fb8bd41bde5cbf0fabc0cd7a28 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 30 Dec 2019 20:54:32 +0300 Subject: [PATCH 165/324] Address some comments made in the review --- freqtrade/freqtradebot.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 68091ed76..3bfa74005 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -296,8 +296,11 @@ class FreqtradeBot: def create_trade(self, pair: str) -> bool: """ - Check the implemented trading strategy for buy-signals. - If the pair triggers the buy_signal a new trade record gets created. + Check the implemented trading strategy for buy signals. + + If the pair triggers the buy signal a new trade record gets created + and the buy-order opening the trade gets issued towards the exchange. + :return: True if a trade has been created. """ logger.debug(f"create_trade for pair {pair}") @@ -467,7 +470,7 @@ class FreqtradeBot: if not whitelist: logger.info("Active pair whitelist is empty.") else: - # Remove currently opened and latest pairs from whitelist + # 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) From b00406a7eb0e73f2b104598441958782d4c95497 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 30 Dec 2019 21:09:35 +0300 Subject: [PATCH 166/324] Make process_maybe_execute_*() returning integers --- freqtrade/freqtradebot.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 3bfa74005..e6f51e5fa 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -460,11 +460,11 @@ class FreqtradeBot: return True - def process_maybe_execute_buys(self) -> None: + def process_maybe_execute_buys(self) -> int: """ Tries to execute buy orders for trades in a safe way """ - result = False + trades_created = 0 whitelist = copy.deepcopy(self.active_pair_whitelist) if not whitelist: @@ -483,39 +483,42 @@ class FreqtradeBot: # Create entity and execute trade for each pair from whitelist for pair in whitelist: try: - if self.create_trade(pair): - result = True + trades_created += self.create_trade(pair) except DependencyException as exception: logger.warning('Unable to create trade: %s', exception) - if not result: + if not trades_created: logger.debug("Found no buy signals for whitelisted currencies. " "Trying again...") - def process_maybe_execute_sells(self, trades: List[Any]) -> None: + return trades_created + + def process_maybe_execute_sells(self, trades: List[Any]) -> int: """ Tries to execute sell orders for trades in a safe way """ - result = False + trades_closed = 0 for trade in trades: try: self.update_trade_state(trade) if (self.strategy.order_types.get('stoploss_on_exchange') and self.handle_stoploss_on_exchange(trade)): - result = True + trades_closed += 1 continue # Check if we can sell our current pair if trade.open_order_id is None and self.handle_trade(trade): - result = True + trades_closed += 1 except DependencyException as exception: logger.warning('Unable to sell trade: %s', exception) # Updating wallets if any trade occured - if result: + if trades_closed: self.wallets.update() + return trades_closed + def get_real_amount(self, trade: Trade, order: Dict, order_amount: float = None) -> float: """ Get real amount for the trade From 84918ad42481a7558bd935a1639b29de5f317931 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 30 Dec 2019 22:08:36 +0300 Subject: [PATCH 167/324] Rename process_maybe_execute_sells() --> exit_positions() --- freqtrade/freqtradebot.py | 8 ++++---- tests/test_freqtradebot.py | 22 ++++++++++++---------- tests/test_integration.py | 6 ++++-- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e6f51e5fa..695302bf9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -132,8 +132,8 @@ class FreqtradeBot: self.dataprovider.refresh(self._create_pair_whitelist(self.active_pair_whitelist), self.strategy.informative_pairs()) - # First process current opened trades - self.process_maybe_execute_sells(trades) + # First process current opened trades (positions) + self.exit_positions(trades) # Then looking for buy opportunities if self.get_free_open_trades(): @@ -493,9 +493,9 @@ class FreqtradeBot: return trades_created - def process_maybe_execute_sells(self, trades: List[Any]) -> int: + def exit_positions(self, trades: List[Any]) -> int: """ - Tries to execute sell orders for trades in a safe way + Tries to execute sell orders for open trades (positions) """ trades_closed = 0 for trade in trades: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index be6a9b392..22110dd1c 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -990,7 +990,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None trade.is_open = True trades = [trade] - freqtrade.process_maybe_execute_sells(trades) + freqtrade.exit_positions(trades) assert trade.stoploss_order_id == '13434334' assert stoploss_limit.call_count == 1 assert trade.is_open is True @@ -1463,7 +1463,7 @@ def test_process_maybe_execute_buys_exception(mocker, default_conf, caplog) -> N assert log_has('Unable to create trade: ', caplog) -def test_process_maybe_execute_sells(mocker, default_conf, limit_buy_order, caplog) -> None: +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)) @@ -1476,7 +1476,8 @@ def test_process_maybe_execute_sells(mocker, default_conf, limit_buy_order, capl trade.open_order_id = '123' trade.open_fee = 0.001 trades = [trade] - assert not freqtrade.process_maybe_execute_sells(trades) + n = freqtrade.exit_positions(trades) + assert n == 0 # Test amount not modified by fee-logic assert not log_has( 'Applying fee to amount for Trade {} from 90.99181073 to 90.81'.format(trade), caplog @@ -1484,11 +1485,11 @@ def test_process_maybe_execute_sells(mocker, default_conf, limit_buy_order, capl mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81) # test amount modified by fee-logic - assert not freqtrade.process_maybe_execute_sells(trades) + n = freqtrade.exit_positions(trades) + assert n == 0 -def test_process_maybe_execute_sells_exception(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) @@ -1502,7 +1503,8 @@ def test_process_maybe_execute_sells_exception(mocker, default_conf, 'freqtrade.freqtradebot.FreqtradeBot.update_trade_state', side_effect=DependencyException() ) - freqtrade.process_maybe_execute_sells(trades) + n = freqtrade.exit_positions(trades) + assert n == 0 assert log_has('Unable to sell trade: ', caplog) @@ -2391,7 +2393,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke assert trade trades = [trade] - freqtrade.process_maybe_execute_sells(trades) + freqtrade.exit_positions(trades) # Increase the price and sell it mocker.patch.multiple( @@ -2438,7 +2440,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f freqtrade.process_maybe_execute_buys() trade = Trade.query.first() trades = [trade] - freqtrade.process_maybe_execute_sells(trades) + freqtrade.exit_positions(trades) assert trade assert trade.stoploss_order_id == '123' assert trade.open_order_id is None @@ -2466,7 +2468,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f }) mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_limit_executed) - freqtrade.process_maybe_execute_sells(trades) + freqtrade.exit_positions(trades) assert trade.stoploss_order_id is None assert trade.is_open is False assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value diff --git a/tests/test_integration.py b/tests/test_integration.py index 2c95bc6e3..a73beeabe 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -90,7 +90,8 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, trade.stoploss_order_id = 3 trade.open_order_id = None - freqtrade.process_maybe_execute_sells(trades) + n = freqtrade.exit_positions(trades) + assert n == 2 assert should_sell_mock.call_count == 2 # Only order for 3rd trade needs to be cancelled @@ -170,7 +171,8 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc assert len(trades) == 5 bals = freqtrade.wallets.get_all_balances() - freqtrade.process_maybe_execute_sells(trades) + n = freqtrade.exit_positions(trades) + assert n == 1 trades = Trade.get_open_trades() # One trade sold assert len(trades) == 4 From fd7af587da29851de854fdd2be7a7d697cdcdbe2 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 30 Dec 2019 22:50:56 +0300 Subject: [PATCH 168/324] Rename process_maybe_execute_buys() --> enter_positions() --- freqtrade/freqtradebot.py | 6 +- tests/rpc/test_rpc.py | 22 +++--- tests/rpc/test_rpc_apiserver.py | 8 +-- tests/rpc/test_rpc_telegram.py | 25 +++---- tests/test_freqtradebot.py | 118 +++++++++++++++++--------------- tests/test_integration.py | 5 +- 6 files changed, 96 insertions(+), 88 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 695302bf9..c595150ac 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -137,7 +137,7 @@ class FreqtradeBot: # Then looking for buy opportunities if self.get_free_open_trades(): - self.process_maybe_execute_buys() + self.enter_positions() # Check and handle any timed out open orders self.check_handle_timedout() @@ -460,9 +460,9 @@ class FreqtradeBot: return True - def process_maybe_execute_buys(self) -> int: + def enter_positions(self) -> int: """ - Tries to execute buy orders for trades in a safe way + Tries to execute buy orders for new trades (positions) """ trades_created = 0 diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index a9f73e98d..3581e3d18 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -41,7 +41,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: with pytest.raises(RPCException, match=r'.*no active trade*'): rpc._rpc_trade_status() - freqtradebot.process_maybe_execute_buys() + freqtradebot.enter_positions() results = rpc._rpc_trade_status() assert { 'trade_id': 1, @@ -116,7 +116,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: with pytest.raises(RPCException, match=r'.*no active order*'): rpc._rpc_status_table(default_conf['stake_currency'], 'USD') - freqtradebot.process_maybe_execute_buys() + freqtradebot.enter_positions() result, headers = rpc._rpc_status_table(default_conf['stake_currency'], 'USD') assert "Since" in headers @@ -162,7 +162,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, rpc = RPC(freqtradebot) rpc._fiat_converter = CryptoToFiatConverter() # Create some test data - freqtradebot.process_maybe_execute_buys() + freqtradebot.enter_positions() trade = Trade.query.first() assert trade @@ -217,7 +217,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) # Create some test data - freqtradebot.process_maybe_execute_buys() + freqtradebot.enter_positions() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) @@ -231,7 +231,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, trade.close_date = datetime.utcnow() trade.is_open = False - freqtradebot.process_maybe_execute_buys() + freqtradebot.enter_positions() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) @@ -299,7 +299,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, rpc = RPC(freqtradebot) # Create some test data - freqtradebot.process_maybe_execute_buys() + freqtradebot.enter_positions() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) @@ -529,7 +529,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.process_maybe_execute_buys() + freqtradebot.enter_positions() msg = rpc._rpc_forcesell('all') assert msg == {'result': 'Created sell orders for all open trades.'} @@ -563,7 +563,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: assert cancel_order_mock.call_count == 1 assert trade.amount == filled_amount - freqtradebot.process_maybe_execute_buys() + freqtradebot.enter_positions() trade = Trade.query.filter(Trade.id == '2').first() amount = trade.amount # make an limit-buy open trade, if there is no 'filled', don't sell it @@ -582,7 +582,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: assert cancel_order_mock.call_count == 2 assert trade.amount == amount - freqtradebot.process_maybe_execute_buys() + freqtradebot.enter_positions() # make an limit-sell open trade mocker.patch( 'freqtrade.exchange.Exchange.get_order', @@ -613,7 +613,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, rpc = RPC(freqtradebot) # Create some test data - freqtradebot.process_maybe_execute_buys() + freqtradebot.enter_positions() trade = Trade.query.first() assert trade @@ -649,7 +649,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee) -> None: assert counts["current"] == 0 # Create some test data - freqtradebot.process_maybe_execute_buys() + freqtradebot.enter_positions() counts = rpc._rpc_count() assert counts["current"] == 1 diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 94af85349..25c971bf7 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -267,7 +267,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets): assert rc.json["max"] == 1.0 # Create some test data - ftbot.process_maybe_execute_buys() + ftbot.enter_positions() rc = client_get(client, f"{BASE_URI}/count") assert_response(rc) assert rc.json["current"] == 1.0 @@ -333,7 +333,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets, limit_buy_order, li assert len(rc.json) == 1 assert rc.json == {"error": "Error querying _profit: no closed trade"} - ftbot.process_maybe_execute_buys() + ftbot.enter_positions() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade @@ -422,7 +422,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): assert_response(rc, 200) assert rc.json == [] - ftbot.process_maybe_execute_buys() + ftbot.enter_positions() rc = client_get(client, f"{BASE_URI}/status") assert_response(rc) assert len(rc.json) == 1 @@ -552,7 +552,7 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets): assert_response(rc, 502) assert rc.json == {"error": "Error querying _forcesell: invalid argument"} - ftbot.process_maybe_execute_buys() + ftbot.enter_positions() rc = client_post(client, f"{BASE_URI}/forcesell", data='{"tradeid": "1"}') diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index d9a7e2f5b..44e51514e 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -188,8 +188,8 @@ def test_status(default_conf, update, mocker, fee, ticker,) -> None: telegram = Telegram(freqtradebot) # Create some test data - for _ in range(3): - freqtradebot.process_maybe_execute_buys() + n = freqtradebot.enter_positions() + assert n == 1 telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -236,7 +236,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: msg_mock.reset_mock() # Create some test data - freqtradebot.process_maybe_execute_buys() + freqtradebot.enter_positions() # Trigger status while we have a fulfilled order for the open trade telegram._status(update=update, context=MagicMock()) @@ -285,7 +285,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: msg_mock.reset_mock() # Create some test data - freqtradebot.process_maybe_execute_buys() + freqtradebot.enter_positions() telegram._status_table(update=update, context=MagicMock()) @@ -322,7 +322,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, telegram = Telegram(freqtradebot) # Create some test data - freqtradebot.process_maybe_execute_buys() + freqtradebot.enter_positions() trade = Trade.query.first() assert trade @@ -352,7 +352,8 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, msg_mock.reset_mock() freqtradebot.config['max_open_trades'] = 2 # Add two other trades - freqtradebot.process_maybe_execute_buys() + n = freqtradebot.enter_positions() + assert n == 2 trades = Trade.query.all() for trade in trades: @@ -431,7 +432,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, msg_mock.reset_mock() # Create some test data - freqtradebot.process_maybe_execute_buys() + freqtradebot.enter_positions() trade = Trade.query.first() # Simulate fulfilled LIMIT_BUY order for trade @@ -709,7 +710,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee, telegram = Telegram(freqtradebot) # Create some test data - freqtradebot.process_maybe_execute_buys() + freqtradebot.enter_positions() trade = Trade.query.first() assert trade @@ -764,7 +765,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, telegram = Telegram(freqtradebot) # Create some test data - freqtradebot.process_maybe_execute_buys() + freqtradebot.enter_positions() # Decrease the price and sell it mocker.patch.multiple( @@ -821,7 +822,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None telegram = Telegram(freqtradebot) # Create some test data - freqtradebot.process_maybe_execute_buys() + freqtradebot.enter_positions() rpc_mock.reset_mock() # /forcesell all @@ -971,7 +972,7 @@ def test_performance_handle(default_conf, update, ticker, fee, telegram = Telegram(freqtradebot) # Create some test data - freqtradebot.process_maybe_execute_buys() + freqtradebot.enter_positions() trade = Trade.query.first() assert trade @@ -1014,7 +1015,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: freqtradebot.state = State.RUNNING # Create some test data - freqtradebot.process_maybe_execute_buys() + freqtradebot.enter_positions() msg_mock.reset_mock() telegram._count(update=update, context=MagicMock()) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 22110dd1c..f99b14286 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -247,7 +247,7 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf freqtrade.active_pair_whitelist = ['NEO/BTC'] patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() trade.update(limit_buy_order) ############################################# @@ -287,7 +287,7 @@ def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, freqtrade.active_pair_whitelist = ['NEO/BTC'] patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() trade.update(limit_buy_order) ############################################# @@ -310,7 +310,7 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() assert trade is not None @@ -318,7 +318,7 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, assert trade.is_open assert trade.open_date is not None - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.order_by(Trade.id.desc()).first() assert trade is not None @@ -573,9 +573,8 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order, assert freqtrade.get_trade_stake_amount('ETH/BTC') is None -def test_process_maybe_execute_buys_no_pairs_let(default_conf, ticker, - limit_buy_order, fee, - mocker, caplog) -> None: +def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order, fee, + mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -589,15 +588,16 @@ def test_process_maybe_execute_buys_no_pairs_let(default_conf, ticker, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.process_maybe_execute_buys() + n = freqtrade.enter_positions() + assert n == 1 assert not log_has_re(r"No currency pair in active pair whitelist.*", caplog) - freqtrade.process_maybe_execute_buys() + n = freqtrade.enter_positions() + assert n == 0 assert log_has_re(r"No currency pair in active pair whitelist.*", caplog) -def test_process_maybe_execute_buys_no_pairs_in_whitelist(default_conf, ticker, - limit_buy_order, fee, - mocker, caplog) -> None: +def test_enter_positions_no_pairs_in_whitelist(default_conf, ticker, limit_buy_order, fee, + mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -610,7 +610,8 @@ def test_process_maybe_execute_buys_no_pairs_in_whitelist(default_conf, ticker, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.process_maybe_execute_buys() + n = freqtrade.enter_positions() + assert n == 0 assert log_has("Active pair whitelist is empty.", caplog) @@ -648,7 +649,8 @@ def test_create_trades_multiple_trades(default_conf, ticker, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.process_maybe_execute_buys() + n = freqtrade.enter_positions() + assert n == max_open trades = Trade.get_open_trades() assert len(trades) == max_open @@ -1059,7 +1061,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, # should unset stoploss_order_id and return true # as a trade actually happened caplog.clear() - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() trade.is_open = True trade.open_order_id = None @@ -1117,7 +1119,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() trade.is_open = True trade.open_order_id = '12345' @@ -1152,7 +1154,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, patch_get_signal(freqtrade) freqtrade.strategy.order_types['stoploss_on_exchange'] = True - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() caplog.clear() freqtrade.create_stoploss_order(trade, 200, 199) @@ -1210,7 +1212,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, patch_get_signal(freqtrade) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() trade.is_open = True trade.open_order_id = None @@ -1298,7 +1300,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c # setting stoploss_on_exchange_interval to 60 seconds freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60 patch_get_signal(freqtrade) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() trade.is_open = True trade.open_order_id = None @@ -1379,7 +1381,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, freqtrade.active_pair_whitelist = freqtrade.edge.adjust(freqtrade.active_pair_whitelist) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() trade.is_open = True trade.open_order_id = None @@ -1443,23 +1445,25 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, stop_price=0.00002344 * 0.99) -def test_process_maybe_execute_buys(mocker, default_conf, caplog) -> None: +def test_enter_positions(mocker, default_conf, caplog) -> None: caplog.set_level(logging.DEBUG) freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trade', MagicMock(return_value=False)) - freqtrade.process_maybe_execute_buys() + n = freqtrade.enter_positions() + assert n == 0 assert log_has('Found no buy signals for whitelisted currencies. Trying again...', caplog) -def test_process_maybe_execute_buys_exception(mocker, default_conf, caplog) -> None: +def test_enter_positions_exception(mocker, default_conf, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch( 'freqtrade.freqtradebot.FreqtradeBot.create_trade', MagicMock(side_effect=DependencyException) ) - freqtrade.process_maybe_execute_buys() + n = freqtrade.enter_positions() + assert n == 0 assert log_has('Unable to create trade: ', caplog) @@ -1679,7 +1683,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mock freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() assert trade @@ -1702,7 +1706,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mock assert trade.close_date is not None -def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, fee, mocker) -> None: +def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -1716,7 +1720,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, fee, patch_get_signal(freqtrade, value=(True, True)) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() # Buy and Sell triggering, so doing nothing ... trades = Trade.query.all() @@ -1725,7 +1729,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, fee, # Buy is triggering, so buying ... patch_get_signal(freqtrade, value=(True, False)) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trades = Trade.query.all() nb_trades = len(trades) assert nb_trades == 1 @@ -1769,7 +1773,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, patch_get_signal(freqtrade, value=(True, False)) freqtrade.strategy.min_roi_reached = MagicMock(return_value=True) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() trade.is_open = True @@ -1800,7 +1804,7 @@ def test_handle_trade_use_sell_signal( freqtrade = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() trade.is_open = True @@ -1828,7 +1832,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, patch_get_signal(freqtrade) # Create trade and sell it - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() assert trade @@ -2188,7 +2192,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N patch_get_signal(freqtrade) # Create some test data - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() assert trade @@ -2236,7 +2240,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) patch_get_signal(freqtrade) # Create some test data - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() assert trade @@ -2286,7 +2290,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe patch_get_signal(freqtrade) # Create some test data - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() assert trade @@ -2344,7 +2348,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c freqtrade.strategy.order_types['stoploss_on_exchange'] = True patch_get_signal(freqtrade) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() Trade.session = MagicMock() @@ -2387,7 +2391,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke patch_get_signal(freqtrade) # Create some test data - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() assert trade @@ -2437,7 +2441,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f patch_get_signal(freqtrade) # Create some test data - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() trades = [trade] freqtrade.exit_positions(trades) @@ -2489,7 +2493,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, patch_get_signal(freqtrade) # Create some test data - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() assert trade @@ -2551,7 +2555,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2582,7 +2586,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2613,7 +2617,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker patch_get_signal(freqtrade) freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple( sell_flag=False, sell_type=SellType.NONE)) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2643,7 +2647,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocke patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2672,7 +2676,7 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order, patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() amnt = trade.amount @@ -2740,7 +2744,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo patch_get_signal(freqtrade) # Create some test data - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() assert trade @@ -2759,7 +2763,7 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo # reinit - should buy other pair. caplog.clear() - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() assert log_has(f"Pair {trade.pair} is currently locked.", caplog) @@ -2784,7 +2788,7 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, mocker) -> patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=True) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2817,7 +2821,7 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() assert freqtrade.handle_trade(trade) is False @@ -2872,7 +2876,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2929,7 +2933,7 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() trade.update(limit_buy_order) @@ -2992,7 +2996,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, fee, freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() trade.update(limit_buy_order) @@ -3051,7 +3055,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = MagicMock(return_value=True) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() trade.update(limit_buy_order) @@ -3382,7 +3386,7 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, whitelist = deepcopy(default_conf['exchange']['pair_whitelist']) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() assert trade is not None @@ -3417,7 +3421,7 @@ def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_o # Save state of current whitelist freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() assert trade is None @@ -3519,7 +3523,7 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() trade = Trade.query.first() assert trade @@ -3626,12 +3630,14 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order, patch_get_signal(bot) assert bot.wallets.get_free('BTC') == 0.002 - bot.process_maybe_execute_buys() + n = bot.enter_positions() + assert n == 2 trades = Trade.query.all() assert len(trades) == 2 bot.config['max_open_trades'] = 3 - bot.process_maybe_execute_buys() + n = bot.enter_positions() + assert n == 0 assert log_has_re(r"Unable to create trade: " r"Available balance \(0 BTC\) is lower than stake amount \(0.001 BTC\)", caplog) diff --git a/tests/test_integration.py b/tests/test_integration.py index a73beeabe..1108969ad 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -80,7 +80,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, patch_get_signal(freqtrade) # Create some test data - freqtrade.process_maybe_execute_buys() + freqtrade.enter_positions() wallets_mock.reset_mock() Trade.session = MagicMock() @@ -154,7 +154,8 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc patch_get_signal(freqtrade) # Create 4 trades - freqtrade.process_maybe_execute_buys() + n = freqtrade.enter_positions() + assert n == 4 trades = Trade.query.all() assert len(trades) == 4 From a88464de3a5ffb4c6802040c6463d4079c5dc868 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 31 Dec 2019 07:01:58 +0100 Subject: [PATCH 169/324] Improve some test code --- tests/rpc/test_rpc_telegram.py | 10 ---------- tests/test_freqtradebot.py | 3 ++- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 44e51514e..9a39f2119 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -148,11 +148,6 @@ def test_status(default_conf, update, mocker, fee, ticker,) -> None: default_conf['telegram']['enabled'] = False default_conf['telegram']['chat_id'] = "123" - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - get_fee=fee, - ) msg_mock = MagicMock() status_table = MagicMock() mocker.patch.multiple( @@ -184,13 +179,8 @@ def test_status(default_conf, update, mocker, fee, ticker,) -> None: ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) - # Create some test data - n = freqtradebot.enter_positions() - assert n == 1 - telegram._status(update=update, context=MagicMock()) assert msg_mock.call_count == 1 diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index f99b14286..434633f5f 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1458,12 +1458,13 @@ def test_enter_positions(mocker, default_conf, caplog) -> None: def test_enter_positions_exception(mocker, default_conf, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch( + mock_ct = mocker.patch( 'freqtrade.freqtradebot.FreqtradeBot.create_trade', MagicMock(side_effect=DependencyException) ) n = freqtrade.enter_positions() assert n == 0 + assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist']) assert log_has('Unable to create trade: ', caplog) From 6ebb9017c7bc0b0d39d8f83d663a7218d776c48a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 31 Dec 2019 07:03:57 +0100 Subject: [PATCH 170/324] Improve test enter_positions --- tests/test_freqtradebot.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 434633f5f..812ba4e3e 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1449,10 +1449,12 @@ def test_enter_positions(mocker, default_conf, caplog) -> None: caplog.set_level(logging.DEBUG) freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trade', MagicMock(return_value=False)) + mock_ct = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trade', + MagicMock(return_value=False)) n = freqtrade.enter_positions() assert n == 0 assert log_has('Found no buy signals for whitelisted currencies. Trying again...', caplog) + assert mock_ct.call_count == 4 def test_enter_positions_exception(mocker, default_conf, caplog) -> None: From 9d518b9d29cdcf90ccdc8efc4117ccff0ba5c380 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 31 Dec 2019 07:05:21 +0100 Subject: [PATCH 171/324] Add comment and don't hardcode 4 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 812ba4e3e..6527554be 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1454,7 +1454,8 @@ def test_enter_positions(mocker, default_conf, caplog) -> None: n = freqtrade.enter_positions() assert n == 0 assert log_has('Found no buy signals for whitelisted currencies. Trying again...', caplog) - assert mock_ct.call_count == 4 + # create_trade should be called once for every pair in the whitelist. + assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist']) def test_enter_positions_exception(mocker, default_conf, caplog) -> None: From 26a2395aebd0b1ed39aaadb63e674dcf64a592bc Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 31 Dec 2019 07:11:09 +0100 Subject: [PATCH 172/324] Include Pair name in exception log message --- freqtrade/freqtradebot.py | 2 +- tests/test_freqtradebot.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c595150ac..a642e6ea7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -485,7 +485,7 @@ class FreqtradeBot: try: trades_created += self.create_trade(pair) except DependencyException as exception: - logger.warning('Unable to create trade: %s', 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/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 6527554be..cf395ef39 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1468,7 +1468,7 @@ def test_enter_positions_exception(mocker, default_conf, caplog) -> None: n = freqtrade.enter_positions() assert n == 0 assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist']) - assert log_has('Unable to create trade: ', caplog) + assert log_has('Unable to create trade for ETH/BTC: ', caplog) def test_exit_positions(mocker, default_conf, limit_buy_order, caplog) -> None: @@ -3642,6 +3642,6 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order, bot.config['max_open_trades'] = 3 n = bot.enter_positions() assert n == 0 - assert log_has_re(r"Unable to create trade: " + assert log_has_re(r"Unable to create trade for XRP/BTC: " r"Available balance \(0 BTC\) is lower than stake amount \(0.001 BTC\)", caplog) From 0ea44b014347d3541bb1b125ee1ebda88e8464f7 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 2 Jan 2020 02:36:59 +0300 Subject: [PATCH 173/324] Fix message in setup.sh --- setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index c4b6e074a..fb5102e12 100755 --- a/setup.sh +++ b/setup.sh @@ -263,7 +263,7 @@ function install() { echo "-------------------------" echo "Run the bot !" echo "-------------------------" - echo "You can now use the bot by executing 'source .env/bin/activate; freqtrade'." + echo "You can now use the bot by executing 'source .env/bin/activate; freqtrade trade'." } function plot() { From 4475110df85265cd2ba1172ebe3bf9df592addaf Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 2 Jan 2020 02:53:25 +0300 Subject: [PATCH 174/324] Cosmetics in freqtradebot --- freqtrade/freqtradebot.py | 73 ++++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 32 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c4991a31c..4a207b087 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -27,6 +27,7 @@ from freqtrade.state import State from freqtrade.strategy.interface import IStrategy, SellType from freqtrade.wallets import Wallets + logger = logging.getLogger(__name__) @@ -181,6 +182,43 @@ class FreqtradeBot: open_trades = len(Trade.get_open_trades()) return max(0, self.config['max_open_trades'] - open_trades) +# +# BUY / enter positions / open trades part +# + + def enter_positions(self) -> int: + """ + Tries to execute buy orders for new trades (positions) + """ + trades_created = 0 + + 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) + + 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 trades_created: + logger.debug("Found no buy signals for whitelisted currencies. " + "Trying again...") + + return trades_created + def get_target_bid(self, pair: str, tick: Dict = None) -> float: """ Calculates bid target between current ask price and last price @@ -460,38 +498,9 @@ class FreqtradeBot: return True - def enter_positions(self) -> int: - """ - Tries to execute buy orders for new trades (positions) - """ - trades_created = 0 - - 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) - - 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 trades_created: - logger.debug("Found no buy signals for whitelisted currencies. " - "Trying again...") - - return trades_created +# +# SELL / exit positions / close trades part +# def exit_positions(self, trades: List[Any]) -> int: """ From 21418e298828e78576fffae4b973fe5c3723079a Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 2 Jan 2020 03:16:18 +0300 Subject: [PATCH 175/324] Minor: fix comment --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4a207b087..774e001c8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -407,7 +407,7 @@ class FreqtradeBot: if price: buy_limit_requested = price else: - # Calculate amount + # Calculate price buy_limit_requested = self.get_target_bid(pair) min_stake_amount = self._get_min_pair_stake_amount(pair, buy_limit_requested) From 18a53f446762016b42b15d0adb90181c69a80027 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 07:26:43 +0100 Subject: [PATCH 176/324] Extract generate_text_table from backtesting class --- freqtrade/optimize/backtest_reports.py | 53 +++++++++++++++++++++++ freqtrade/optimize/backtesting.py | 60 ++++---------------------- tests/optimize/test_backtesting.py | 9 ++-- 3 files changed, 68 insertions(+), 54 deletions(-) create mode 100644 freqtrade/optimize/backtest_reports.py diff --git a/freqtrade/optimize/backtest_reports.py b/freqtrade/optimize/backtest_reports.py new file mode 100644 index 000000000..501d22228 --- /dev/null +++ b/freqtrade/optimize/backtest_reports.py @@ -0,0 +1,53 @@ +from datetime import timedelta +from typing import Dict + +from pandas import DataFrame +from tabulate import tabulate + + +def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_trades: int, + results: DataFrame, skip_nan: bool = False) -> str: + """ + Generates and returns a text table for the given backtest data and the results dataframe + :return: pretty printed table with tabulate as str + """ + + floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f') + tabular_data = [] + headers = ['pair', 'buy count', 'avg profit %', 'cum profit %', + f'tot profit {stake_currency}', 'tot profit %', 'avg duration', + 'profit', 'loss'] + for pair in data: + result = results[results.pair == pair] + if skip_nan and result.profit_abs.isnull().all(): + continue + + tabular_data.append([ + pair, + len(result.index), + result.profit_percent.mean() * 100.0, + result.profit_percent.sum() * 100.0, + result.profit_abs.sum(), + result.profit_percent.sum() * 100.0 / max_open_trades, + str(timedelta( + minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00', + len(result[result.profit_abs > 0]), + len(result[result.profit_abs < 0]) + ]) + + # Append Total + tabular_data.append([ + 'TOTAL', + len(results.index), + results.profit_percent.mean() * 100.0, + results.profit_percent.sum() * 100.0, + results.profit_abs.sum(), + results.profit_percent.sum() * 100.0 / max_open_trades, + str(timedelta( + minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00', + len(results[results.profit_abs > 0]), + len(results[results.profit_abs < 0]) + ]) + # Ignore type as floatfmt does allow tuples but mypy does not know that + return tabulate(tabular_data, headers=headers, + floatfmt=floatfmt, tablefmt="pipe") # type: ignore diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 9bd0327e0..e90a4c7ea 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -14,6 +14,7 @@ from tabulate import tabulate from freqtrade.configuration import (TimeRange, remove_credentials, validate_config_consistency) +from freqtrade.optimize.backtest_reports import generate_text_table from freqtrade.data import history from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import OperationalException @@ -129,55 +130,6 @@ class Backtesting: return data, timerange - def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame, - skip_nan: bool = False) -> str: - """ - Generates and returns a text table for the given backtest data and the results dataframe - :return: pretty printed table with tabulate as str - """ - stake_currency = str(self.config.get('stake_currency')) - max_open_trades = self.config.get('max_open_trades') - - floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f') - tabular_data = [] - headers = ['pair', 'buy count', 'avg profit %', 'cum profit %', - 'tot profit ' + stake_currency, 'tot profit %', 'avg duration', - 'profit', 'loss'] - for pair in data: - result = results[results.pair == pair] - if skip_nan and result.profit_abs.isnull().all(): - continue - - tabular_data.append([ - pair, - len(result.index), - result.profit_percent.mean() * 100.0, - result.profit_percent.sum() * 100.0, - result.profit_abs.sum(), - result.profit_percent.sum() * 100.0 / max_open_trades, - str(timedelta( - minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00', - len(result[result.profit_abs > 0]), - len(result[result.profit_abs < 0]) - ]) - - # Append Total - tabular_data.append([ - 'TOTAL', - len(results.index), - results.profit_percent.mean() * 100.0, - results.profit_percent.sum() * 100.0, - results.profit_abs.sum(), - results.profit_percent.sum() * 100.0 / max_open_trades, - str(timedelta( - minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00', - len(results[results.profit_abs > 0]), - len(results[results.profit_abs < 0]) - ]) - # Ignore type as floatfmt does allow tuples but mypy does not know that - return tabulate(tabular_data, headers=headers, - floatfmt=floatfmt, tablefmt="pipe") # type: ignore - def _generate_text_table_sell_reason(self, data: Dict[str, Dict], results: DataFrame) -> str: """ Generate small table outlining Backtest results @@ -509,13 +461,19 @@ class Backtesting: print(f"Result for strategy {strategy}") print(' BACKTESTING REPORT '.center(133, '=')) - print(self._generate_text_table(data, results)) + print(generate_text_table(data, + stake_currency=self.config['stake_currency'], + max_open_trades=self.config['max_open_trades'], + results=results)) print(' SELL REASON STATS '.center(133, '=')) print(self._generate_text_table_sell_reason(data, results)) print(' LEFT OPEN TRADES REPORT '.center(133, '=')) - print(self._generate_text_table(data, results.loc[results.open_at_end], True)) + print(generate_text_table(data, + stake_currency=self.config['stake_currency'], + max_open_trades=self.config['max_open_trades'], + results=results.loc[results.open_at_end], skip_nan=True)) print() if len(all_results) > 1: # Print Strategy summary table diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 4e2fd01cf..67332c10a 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -19,6 +19,7 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.optimize import setup_configuration, start_backtesting +from freqtrade.optimize.backtest_reports import generate_text_table from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy @@ -360,8 +361,8 @@ def test_tickerdata_to_dataframe_bt(default_conf, mocker, testdatadir) -> None: def test_generate_text_table(default_conf, mocker): patch_exchange(mocker) - default_conf['max_open_trades'] = 2 - backtesting = Backtesting(default_conf) + # default_conf['max_open_trades'] = 2 + # backtesting = Backtesting(default_conf) results = pd.DataFrame( { @@ -384,7 +385,9 @@ def test_generate_text_table(default_conf, mocker): '| TOTAL | 2 | 15.00 | 30.00 | ' '0.60000000 | 15.00 | 0:20:00 | 2 | 0 |' ) - assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str + assert generate_text_table(data={'ETH/BTC': {}}, + stake_currency='BTC', max_open_trades=2, + results=results) == result_str def test_generate_text_table_sell_reason(default_conf, mocker): From caec345c0b77364412ae0ec2c524ff2eab9ef316 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 07:28:30 +0100 Subject: [PATCH 177/324] Extract generate_text_table_sell_reason from backtesting class --- freqtrade/optimize/backtest_reports.py | 13 +++++++++++++ freqtrade/optimize/backtesting.py | 17 ++++------------- tests/optimize/test_backtesting.py | 8 +++----- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/freqtrade/optimize/backtest_reports.py b/freqtrade/optimize/backtest_reports.py index 501d22228..8912af22a 100644 --- a/freqtrade/optimize/backtest_reports.py +++ b/freqtrade/optimize/backtest_reports.py @@ -51,3 +51,16 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra # Ignore type as floatfmt does allow tuples but mypy does not know that return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe") # type: ignore + + +def generate_text_table_sell_reason(data: Dict[str, Dict], results: DataFrame) -> str: + """ + Generate small table outlining Backtest results + """ + tabular_data = [] + headers = ['Sell Reason', 'Count', 'Profit', 'Loss'] + for reason, count in results['sell_reason'].value_counts().iteritems(): + profit = len(results[(results['sell_reason'] == reason) & (results['profit_abs'] >= 0)]) + loss = len(results[(results['sell_reason'] == reason) & (results['profit_abs'] < 0)]) + tabular_data.append([reason.value, count, profit, loss]) + return tabulate(tabular_data, headers=headers, tablefmt="pipe") diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index e90a4c7ea..1c92860a8 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -14,12 +14,13 @@ from tabulate import tabulate from freqtrade.configuration import (TimeRange, remove_credentials, validate_config_consistency) -from freqtrade.optimize.backtest_reports import generate_text_table from freqtrade.data import history from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import file_dump_json +from freqtrade.optimize.backtest_reports import ( + generate_text_table, generate_text_table_sell_reason) from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode @@ -130,17 +131,7 @@ class Backtesting: return data, timerange - def _generate_text_table_sell_reason(self, data: Dict[str, Dict], results: DataFrame) -> str: - """ - Generate small table outlining Backtest results - """ - tabular_data = [] - headers = ['Sell Reason', 'Count', 'Profit', 'Loss'] - for reason, count in results['sell_reason'].value_counts().iteritems(): - profit = len(results[(results['sell_reason'] == reason) & (results['profit_abs'] >= 0)]) - loss = len(results[(results['sell_reason'] == reason) & (results['profit_abs'] < 0)]) - tabular_data.append([reason.value, count, profit, loss]) - return tabulate(tabular_data, headers=headers, tablefmt="pipe") + def _generate_text_table_strategy(self, all_results: dict) -> str: """ @@ -467,7 +458,7 @@ class Backtesting: results=results)) print(' SELL REASON STATS '.center(133, '=')) - print(self._generate_text_table_sell_reason(data, results)) + print(generate_text_table_sell_reason(data, results)) print(' LEFT OPEN TRADES REPORT '.center(133, '=')) print(generate_text_table(data, diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 67332c10a..554d8974b 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -19,7 +19,8 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.optimize import setup_configuration, start_backtesting -from freqtrade.optimize.backtest_reports import generate_text_table +from freqtrade.optimize.backtest_reports import ( + generate_text_table, generate_text_table_sell_reason) from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy @@ -361,8 +362,6 @@ def test_tickerdata_to_dataframe_bt(default_conf, mocker, testdatadir) -> None: def test_generate_text_table(default_conf, mocker): patch_exchange(mocker) - # default_conf['max_open_trades'] = 2 - # backtesting = Backtesting(default_conf) results = pd.DataFrame( { @@ -392,7 +391,6 @@ def test_generate_text_table(default_conf, mocker): def test_generate_text_table_sell_reason(default_conf, mocker): patch_exchange(mocker) - backtesting = Backtesting(default_conf) results = pd.DataFrame( { @@ -412,7 +410,7 @@ def test_generate_text_table_sell_reason(default_conf, mocker): '| roi | 2 | 2 | 0 |\n' '| stop_loss | 1 | 0 | 1 |' ) - assert backtesting._generate_text_table_sell_reason( + assert generate_text_table_sell_reason( data={'ETH/BTC': {}}, results=results) == result_str From 904e1647e1579f2bc0ff396937ea6982afd4ccd7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 07:32:12 +0100 Subject: [PATCH 178/324] Extract generate_text_table_strategy to seperate module --- freqtrade/optimize/backtest_reports.py | 29 +++++++++++++++++++ freqtrade/optimize/backtesting.py | 39 ++++---------------------- tests/optimize/test_backtesting.py | 15 +++------- 3 files changed, 38 insertions(+), 45 deletions(-) diff --git a/freqtrade/optimize/backtest_reports.py b/freqtrade/optimize/backtest_reports.py index 8912af22a..8f0436563 100644 --- a/freqtrade/optimize/backtest_reports.py +++ b/freqtrade/optimize/backtest_reports.py @@ -64,3 +64,32 @@ def generate_text_table_sell_reason(data: Dict[str, Dict], results: DataFrame) - loss = len(results[(results['sell_reason'] == reason) & (results['profit_abs'] < 0)]) tabular_data.append([reason.value, count, profit, loss]) return tabulate(tabular_data, headers=headers, tablefmt="pipe") + + +def generate_text_table_strategy(stake_currency: str, max_open_trades: str, + all_results: Dict) -> str: + """ + Generate summary table per strategy + """ + + floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f') + tabular_data = [] + headers = ['Strategy', 'buy count', 'avg profit %', 'cum profit %', + f'tot profit {stake_currency}', 'tot profit %', 'avg duration', + 'profit', 'loss'] + for strategy, results in all_results.items(): + tabular_data.append([ + strategy, + len(results.index), + results.profit_percent.mean() * 100.0, + results.profit_percent.sum() * 100.0, + results.profit_abs.sum(), + results.profit_percent.sum() * 100.0 / max_open_trades, + str(timedelta( + minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00', + len(results[results.profit_abs > 0]), + len(results[results.profit_abs < 0]) + ]) + # Ignore type as floatfmt does allow tuples but mypy does not know that + return tabulate(tabular_data, headers=headers, + floatfmt=floatfmt, tablefmt="pipe") # type: ignore diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 1c92860a8..ae3fbed0a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -10,7 +10,6 @@ from pathlib import Path from typing import Any, Dict, List, NamedTuple, Optional from pandas import DataFrame -from tabulate import tabulate from freqtrade.configuration import (TimeRange, remove_credentials, validate_config_consistency) @@ -20,7 +19,8 @@ from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import file_dump_json from freqtrade.optimize.backtest_reports import ( - generate_text_table, generate_text_table_sell_reason) + generate_text_table, generate_text_table_sell_reason, + generate_text_table_strategy) from freqtrade.persistence import Trade from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode @@ -131,37 +131,6 @@ class Backtesting: return data, timerange - - - def _generate_text_table_strategy(self, all_results: dict) -> str: - """ - Generate summary table per strategy - """ - stake_currency = str(self.config.get('stake_currency')) - max_open_trades = self.config.get('max_open_trades') - - floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f') - tabular_data = [] - headers = ['Strategy', 'buy count', 'avg profit %', 'cum profit %', - 'tot profit ' + stake_currency, 'tot profit %', 'avg duration', - 'profit', 'loss'] - for strategy, results in all_results.items(): - tabular_data.append([ - strategy, - len(results.index), - results.profit_percent.mean() * 100.0, - results.profit_percent.sum() * 100.0, - results.profit_abs.sum(), - results.profit_percent.sum() * 100.0 / max_open_trades, - str(timedelta( - minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00', - len(results[results.profit_abs > 0]), - len(results[results.profit_abs < 0]) - ]) - # Ignore type as floatfmt does allow tuples but mypy does not know that - return tabulate(tabular_data, headers=headers, - floatfmt=floatfmt, tablefmt="pipe") # type: ignore - def _store_backtest_result(self, recordfilename: Path, results: DataFrame, strategyname: Optional[str] = None) -> None: @@ -469,5 +438,7 @@ class Backtesting: if len(all_results) > 1: # Print Strategy summary table print(' Strategy Summary '.center(133, '=')) - print(self._generate_text_table_strategy(all_results)) + print(generate_text_table_strategy(self.config['stake_currency'], + self.config['max_open_trades'], + all_results=all_results)) print('\nFor more details, please look at the detail tables above') diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 554d8974b..57b80f837 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -20,7 +20,8 @@ from freqtrade.data.history import get_timerange from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.optimize import setup_configuration, start_backtesting from freqtrade.optimize.backtest_reports import ( - generate_text_table, generate_text_table_sell_reason) + generate_text_table, generate_text_table_sell_reason, + generate_text_table_strategy) from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy @@ -361,7 +362,6 @@ def test_tickerdata_to_dataframe_bt(default_conf, mocker, testdatadir) -> None: def test_generate_text_table(default_conf, mocker): - patch_exchange(mocker) results = pd.DataFrame( { @@ -390,7 +390,6 @@ def test_generate_text_table(default_conf, mocker): def test_generate_text_table_sell_reason(default_conf, mocker): - patch_exchange(mocker) results = pd.DataFrame( { @@ -414,13 +413,7 @@ def test_generate_text_table_sell_reason(default_conf, mocker): data={'ETH/BTC': {}}, results=results) == result_str -def test_generate_text_table_strategyn(default_conf, mocker): - """ - Test Backtesting.generate_text_table_sell_reason() method - """ - patch_exchange(mocker) - default_conf['max_open_trades'] = 2 - backtesting = Backtesting(default_conf) +def test_generate_text_table_strategy(default_conf, mocker): results = {} results['ETH/BTC'] = pd.DataFrame( { @@ -455,7 +448,7 @@ def test_generate_text_table_strategyn(default_conf, mocker): '| LTC/BTC | 3 | 30.00 | 90.00 ' '| 1.30000000 | 45.00 | 0:20:00 | 3 | 0 |' ) - assert backtesting._generate_text_table_strategy(all_results=results) == result_str + assert generate_text_table_strategy('BTC', 2, all_results=results) == result_str def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: From 10ee23622a34797170e2ad37b3389b7b1ea7ebfd Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 07:33:55 +0100 Subject: [PATCH 179/324] Extract tests for backtest_reports to their own test module --- tests/optimize/test_backtest_reports.py | 96 +++++++++++++++++++++++++ tests/optimize/test_backtesting.py | 93 ------------------------ 2 files changed, 96 insertions(+), 93 deletions(-) create mode 100644 tests/optimize/test_backtest_reports.py diff --git a/tests/optimize/test_backtest_reports.py b/tests/optimize/test_backtest_reports.py new file mode 100644 index 000000000..726202517 --- /dev/null +++ b/tests/optimize/test_backtest_reports.py @@ -0,0 +1,96 @@ +import pandas as pd + +from freqtrade.optimize.backtest_reports import ( + generate_text_table, generate_text_table_sell_reason, + generate_text_table_strategy) +from freqtrade.strategy.interface import SellType + + +def test_generate_text_table(default_conf, mocker): + + results = pd.DataFrame( + { + 'pair': ['ETH/BTC', 'ETH/BTC'], + 'profit_percent': [0.1, 0.2], + 'profit_abs': [0.2, 0.4], + 'trade_duration': [10, 30], + 'profit': [2, 0], + 'loss': [0, 0] + } + ) + + result_str = ( + '| pair | buy count | avg profit % | cum profit % | ' + 'tot profit BTC | tot profit % | avg duration | profit | loss |\n' + '|:--------|------------:|---------------:|---------------:|' + '-----------------:|---------------:|:---------------|---------:|-------:|\n' + '| ETH/BTC | 2 | 15.00 | 30.00 | ' + '0.60000000 | 15.00 | 0:20:00 | 2 | 0 |\n' + '| TOTAL | 2 | 15.00 | 30.00 | ' + '0.60000000 | 15.00 | 0:20:00 | 2 | 0 |' + ) + assert generate_text_table(data={'ETH/BTC': {}}, + stake_currency='BTC', max_open_trades=2, + results=results) == result_str + + +def test_generate_text_table_sell_reason(default_conf, mocker): + + results = pd.DataFrame( + { + 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], + 'profit_percent': [0.1, 0.2, -0.3], + 'profit_abs': [0.2, 0.4, -0.5], + 'trade_duration': [10, 30, 10], + 'profit': [2, 0, 0], + 'loss': [0, 0, 1], + 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + } + ) + + result_str = ( + '| Sell Reason | Count | Profit | Loss |\n' + '|:--------------|--------:|---------:|-------:|\n' + '| roi | 2 | 2 | 0 |\n' + '| stop_loss | 1 | 0 | 1 |' + ) + assert generate_text_table_sell_reason( + data={'ETH/BTC': {}}, results=results) == result_str + + +def test_generate_text_table_strategy(default_conf, mocker): + results = {} + results['ETH/BTC'] = pd.DataFrame( + { + 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], + 'profit_percent': [0.1, 0.2, 0.3], + 'profit_abs': [0.2, 0.4, 0.5], + 'trade_duration': [10, 30, 10], + 'profit': [2, 0, 0], + 'loss': [0, 0, 1], + 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + } + ) + results['LTC/BTC'] = pd.DataFrame( + { + 'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'], + 'profit_percent': [0.4, 0.2, 0.3], + 'profit_abs': [0.4, 0.4, 0.5], + 'trade_duration': [15, 30, 15], + 'profit': [4, 1, 0], + 'loss': [0, 0, 1], + 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + } + ) + + result_str = ( + '| Strategy | buy count | avg profit % | cum profit % ' + '| tot profit BTC | tot profit % | avg duration | profit | loss |\n' + '|:-----------|------------:|---------------:|---------------:' + '|-----------------:|---------------:|:---------------|---------:|-------:|\n' + '| ETH/BTC | 3 | 20.00 | 60.00 ' + '| 1.10000000 | 30.00 | 0:17:00 | 3 | 0 |\n' + '| LTC/BTC | 3 | 30.00 | 90.00 ' + '| 1.30000000 | 45.00 | 0:20:00 | 3 | 0 |' + ) + assert generate_text_table_strategy('BTC', 2, all_results=results) == result_str diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 57b80f837..9f31114b4 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -19,9 +19,6 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.optimize import setup_configuration, start_backtesting -from freqtrade.optimize.backtest_reports import ( - generate_text_table, generate_text_table_sell_reason, - generate_text_table_strategy) from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy @@ -361,96 +358,6 @@ def test_tickerdata_to_dataframe_bt(default_conf, mocker, testdatadir) -> None: assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC']) -def test_generate_text_table(default_conf, mocker): - - results = pd.DataFrame( - { - 'pair': ['ETH/BTC', 'ETH/BTC'], - 'profit_percent': [0.1, 0.2], - 'profit_abs': [0.2, 0.4], - 'trade_duration': [10, 30], - 'profit': [2, 0], - 'loss': [0, 0] - } - ) - - result_str = ( - '| pair | buy count | avg profit % | cum profit % | ' - 'tot profit BTC | tot profit % | avg duration | profit | loss |\n' - '|:--------|------------:|---------------:|---------------:|' - '-----------------:|---------------:|:---------------|---------:|-------:|\n' - '| ETH/BTC | 2 | 15.00 | 30.00 | ' - '0.60000000 | 15.00 | 0:20:00 | 2 | 0 |\n' - '| TOTAL | 2 | 15.00 | 30.00 | ' - '0.60000000 | 15.00 | 0:20:00 | 2 | 0 |' - ) - assert generate_text_table(data={'ETH/BTC': {}}, - stake_currency='BTC', max_open_trades=2, - results=results) == result_str - - -def test_generate_text_table_sell_reason(default_conf, mocker): - - results = pd.DataFrame( - { - 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], - 'profit_percent': [0.1, 0.2, -0.3], - 'profit_abs': [0.2, 0.4, -0.5], - 'trade_duration': [10, 30, 10], - 'profit': [2, 0, 0], - 'loss': [0, 0, 1], - 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] - } - ) - - result_str = ( - '| Sell Reason | Count | Profit | Loss |\n' - '|:--------------|--------:|---------:|-------:|\n' - '| roi | 2 | 2 | 0 |\n' - '| stop_loss | 1 | 0 | 1 |' - ) - assert generate_text_table_sell_reason( - data={'ETH/BTC': {}}, results=results) == result_str - - -def test_generate_text_table_strategy(default_conf, mocker): - results = {} - results['ETH/BTC'] = pd.DataFrame( - { - 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], - 'profit_percent': [0.1, 0.2, 0.3], - 'profit_abs': [0.2, 0.4, 0.5], - 'trade_duration': [10, 30, 10], - 'profit': [2, 0, 0], - 'loss': [0, 0, 1], - 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] - } - ) - results['LTC/BTC'] = pd.DataFrame( - { - 'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'], - 'profit_percent': [0.4, 0.2, 0.3], - 'profit_abs': [0.4, 0.4, 0.5], - 'trade_duration': [15, 30, 15], - 'profit': [4, 1, 0], - 'loss': [0, 0, 1], - 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] - } - ) - - result_str = ( - '| Strategy | buy count | avg profit % | cum profit % ' - '| tot profit BTC | tot profit % | avg duration | profit | loss |\n' - '|:-----------|------------:|---------------:|---------------:' - '|-----------------:|---------------:|:---------------|---------:|-------:|\n' - '| ETH/BTC | 3 | 20.00 | 60.00 ' - '| 1.10000000 | 30.00 | 0:17:00 | 3 | 0 |\n' - '| LTC/BTC | 3 | 30.00 | 90.00 ' - '| 1.30000000 | 45.00 | 0:20:00 | 3 | 0 |' - ) - assert generate_text_table_strategy('BTC', 2, all_results=results) == result_str - - def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: def get_timerange(input1): return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) From 8cc48cf4b042f4b47ad3902addd2fafc5e433762 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 09:31:35 +0100 Subject: [PATCH 180/324] Fix tests where mocks fail now --- tests/optimize/test_backtesting.py | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 9f31114b4..83d212e3d 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -366,11 +366,8 @@ def test_backtesting_start(default_conf, mocker, testdatadir, caplog) -> None: mocker.patch('freqtrade.data.history.get_timerange', get_timerange) mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock()) patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.optimize.backtesting.Backtesting', - backtest=MagicMock(), - _generate_text_table=MagicMock(return_value='1'), - ) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) + mocker.patch('freqtrade.optimize.backtesting.generate_text_table', MagicMock(return_value=1)) default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] default_conf['ticker_interval'] = '1m' @@ -399,11 +396,8 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog, testdatadir) -> mocker.patch('freqtrade.data.history.get_timerange', get_timerange) mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', MagicMock()) patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.optimize.backtesting.Backtesting', - backtest=MagicMock(), - _generate_text_table=MagicMock(return_value='1'), - ) + mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) + mocker.patch('freqtrade.optimize.backtesting.generate_text_table', MagicMock(return_value=1)) default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] default_conf['ticker_interval'] = "1m" @@ -714,7 +708,8 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): patch_exchange(mocker, api_mock) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) - mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock()) + mocker.patch('freqtrade.optimize.backtesting.generate_text_table', MagicMock()) + patched_configuration_load_config_file(mocker, default_conf) args = [ @@ -760,10 +755,9 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): backtestmock = MagicMock() mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) gen_table_mock = MagicMock() - mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', gen_table_mock) + mocker.patch('freqtrade.optimize.backtesting.generate_text_table', gen_table_mock) gen_strattable_mock = MagicMock() - mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table_strategy', - gen_strattable_mock) + mocker.patch('freqtrade.optimize.backtesting.generate_text_table_strategy', gen_strattable_mock) patched_configuration_load_config_file(mocker, default_conf) args = [ From a9fbad0741fe7475a2d0b90c22788a42a24e3a05 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 09:37:54 +0100 Subject: [PATCH 181/324] Improve docstrings --- freqtrade/optimize/backtest_reports.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtest_reports.py b/freqtrade/optimize/backtest_reports.py index 8f0436563..5778747cf 100644 --- a/freqtrade/optimize/backtest_reports.py +++ b/freqtrade/optimize/backtest_reports.py @@ -9,7 +9,12 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra results: DataFrame, skip_nan: bool = False) -> str: """ Generates and returns a text table for the given backtest data and the results dataframe - :return: pretty printed table with tabulate as str + :param data: Dict of containing data that was used during backtesting. + :param stake_currency: stake-currency - used to correctly name headers + :param max_open_trades: Maximum allowed open trades + :param results: Dataframe containing the backtest results + :param skip_nan: Print "left open" open trades + :return: pretty printed table with tabulate as string """ floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f') @@ -56,6 +61,9 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra def generate_text_table_sell_reason(data: Dict[str, Dict], results: DataFrame) -> str: """ Generate small table outlining Backtest results + :param data: Dict of containing data that was used during backtesting. + :param results: Dataframe containing the backtest results + :return: pretty printed table with tabulate as string """ tabular_data = [] headers = ['Sell Reason', 'Count', 'Profit', 'Loss'] @@ -70,6 +78,10 @@ def generate_text_table_strategy(stake_currency: str, max_open_trades: str, all_results: Dict) -> str: """ Generate summary table 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: pretty printed table with tabulate as string """ floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f') From e89fa44680e795f5a52067406526e41c6a1ae756 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 2 Jan 2020 11:50:54 +0300 Subject: [PATCH 182/324] Arrange common section for update trade state methods --- freqtrade/freqtradebot.py | 170 +++++++++++++++++++------------------- 1 file changed, 87 insertions(+), 83 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 774e001c8..d5d918585 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -183,7 +183,7 @@ class FreqtradeBot: return max(0, self.config['max_open_trades'] - open_trades) # -# BUY / enter positions / open trades part +# BUY / enter positions / open trades logic and methods # def enter_positions(self) -> int: @@ -499,7 +499,7 @@ class FreqtradeBot: return True # -# SELL / exit positions / close trades part +# SELL / exit positions / close trades logic and methods # def exit_positions(self, trades: List[Any]) -> int: @@ -528,87 +528,6 @@ class FreqtradeBot: return trades_closed - def get_real_amount(self, trade: Trade, order: Dict, order_amount: float = None) -> float: - """ - Get real amount for the trade - Necessary for exchanges which charge fees in base currency (e.g. binance) - """ - if order_amount is None: - order_amount = order['amount'] - # Only run for closed orders - if trade.fee_open == 0 or order['status'] == 'open': - return order_amount - - # use fee from order-dict if possible - if ('fee' in order and order['fee'] is not None and - (order['fee'].keys() >= {'currency', 'cost'})): - if (order['fee']['currency'] is not None and - order['fee']['cost'] is not None and - trade.pair.startswith(order['fee']['currency'])): - new_amount = order_amount - order['fee']['cost'] - logger.info("Applying fee on amount for %s (from %s to %s) from Order", - trade, order['amount'], new_amount) - return new_amount - - # Fallback to Trades - trades = self.exchange.get_trades_for_order(trade.open_order_id, trade.pair, - trade.open_date) - - if len(trades) == 0: - logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade) - return order_amount - amount = 0 - fee_abs = 0 - for exectrade in trades: - amount += exectrade['amount'] - if ("fee" in exectrade and exectrade['fee'] is not None and - (exectrade['fee'].keys() >= {'currency', 'cost'})): - # only applies if fee is in quote currency! - if (exectrade['fee']['currency'] is not None and - exectrade['fee']['cost'] is not None and - trade.pair.startswith(exectrade['fee']['currency'])): - fee_abs += exectrade['fee']['cost'] - - if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC): - logger.warning(f"Amount {amount} does not match amount {trade.amount}") - raise DependencyException("Half bought? Amounts don't match") - real_amount = amount - fee_abs - if fee_abs != 0: - logger.info(f"Applying fee on amount for {trade} " - f"(from {order_amount} to {real_amount}) from Trades") - return real_amount - - def update_trade_state(self, trade, action_order: dict = None): - """ - Checks trades with open orders and updates the amount if necessary - """ - # Get order details for actual price per unit - if trade.open_order_id: - # Update trade with order values - logger.info('Found open order for %s', trade) - try: - order = action_order or self.exchange.get_order(trade.open_order_id, trade.pair) - except InvalidOrderException as exception: - logger.warning('Unable to fetch order %s: %s', trade.open_order_id, exception) - return - # Try update amount (binance-fix) - try: - new_amount = self.get_real_amount(trade, order) - if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC): - order['amount'] = new_amount - # Fee was applied, so set to 0 - trade.fee_open = 0 - trade.recalc_open_trade_price() - - except DependencyException as exception: - logger.warning("Could not update trade amount: %s", exception) - - trade.update(order) - - # Updating wallets when order is closed - if not trade.is_open: - self.wallets.update() - def get_sell_rate(self, pair: str, refresh: bool) -> float: """ Get sell rate - either using get-ticker bid or first bid based on orderbook @@ -1047,3 +966,88 @@ class FreqtradeBot: # Send the message self.rpc.send_msg(msg) + +# +# Common update trade state methods +# + + def update_trade_state(self, trade, action_order: dict = None): + """ + Checks trades with open orders and updates the amount if necessary + """ + # Get order details for actual price per unit + if trade.open_order_id: + # Update trade with order values + logger.info('Found open order for %s', trade) + try: + order = action_order or self.exchange.get_order(trade.open_order_id, trade.pair) + except InvalidOrderException as exception: + logger.warning('Unable to fetch order %s: %s', trade.open_order_id, exception) + return + # Try update amount (binance-fix) + try: + new_amount = self.get_real_amount(trade, order) + if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC): + order['amount'] = new_amount + # Fee was applied, so set to 0 + trade.fee_open = 0 + trade.recalc_open_trade_price() + + except DependencyException as exception: + logger.warning("Could not update trade amount: %s", exception) + + trade.update(order) + + # Updating wallets when order is closed + if not trade.is_open: + self.wallets.update() + + def get_real_amount(self, trade: Trade, order: Dict, order_amount: float = None) -> float: + """ + Get real amount for the trade + Necessary for exchanges which charge fees in base currency (e.g. binance) + """ + if order_amount is None: + order_amount = order['amount'] + # Only run for closed orders + if trade.fee_open == 0 or order['status'] == 'open': + return order_amount + + # use fee from order-dict if possible + if ('fee' in order and order['fee'] is not None and + (order['fee'].keys() >= {'currency', 'cost'})): + if (order['fee']['currency'] is not None and + order['fee']['cost'] is not None and + trade.pair.startswith(order['fee']['currency'])): + new_amount = order_amount - order['fee']['cost'] + logger.info("Applying fee on amount for %s (from %s to %s) from Order", + trade, order['amount'], new_amount) + return new_amount + + # Fallback to Trades + trades = self.exchange.get_trades_for_order(trade.open_order_id, trade.pair, + trade.open_date) + + if len(trades) == 0: + logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade) + return order_amount + amount = 0 + fee_abs = 0 + for exectrade in trades: + amount += exectrade['amount'] + if ("fee" in exectrade and exectrade['fee'] is not None and + (exectrade['fee'].keys() >= {'currency', 'cost'})): + # only applies if fee is in quote currency! + if (exectrade['fee']['currency'] is not None and + exectrade['fee']['cost'] is not None and + trade.pair.startswith(exectrade['fee']['currency'])): + fee_abs += exectrade['fee']['cost'] + + if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC): + logger.warning(f"Amount {amount} does not match amount {trade.amount}") + raise DependencyException("Half bought? Amounts don't match") + real_amount = amount - fee_abs + if fee_abs != 0: + logger.info(f"Applying fee on amount for {trade} " + f"(from {order_amount} to {real_amount}) from Trades") + return real_amount From 6fbdd6bee9520afc13450cb63c3fba4591537ce3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 09:51:24 +0100 Subject: [PATCH 183/324] Remove unused directory from user_data --- user_data/backtest_data/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 user_data/backtest_data/.gitkeep diff --git a/user_data/backtest_data/.gitkeep b/user_data/backtest_data/.gitkeep deleted file mode 100644 index e69de29bb..000000000 From 2c8e8d8ef65021fbb3ee3446768fe3e3fa34c36f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 09:51:47 +0100 Subject: [PATCH 184/324] Align columns for btanalysis loading --- freqtrade/data/btanalysis.py | 2 +- tests/data/test_btanalysis.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 2fc931a9b..04b2ca980 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -47,7 +47,7 @@ def load_backtest_data(filename) -> pd.DataFrame: utc=True, infer_datetime_format=True ) - df['profitabs'] = df['close_rate'] - df['open_rate'] + df['profit'] = df['close_rate'] - df['open_rate'] df = df.sort_values("open_time").reset_index(drop=True) return df diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 13711c63e..60d9c3ea5 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -20,7 +20,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 + ["profitabs"] + assert list(bt_data.columns) == BT_DATA_COLUMNS + ["profit"] assert len(bt_data) == 179 # Test loading from string (must yield same result) From 9325880fe5c84973a6553f47993639042f574ac9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 10:38:59 +0100 Subject: [PATCH 185/324] Split config-validation requires --- freqtrade/configuration/config_validation.py | 11 +++++-- freqtrade/constants.py | 30 ++++++++++++-------- tests/test_configuration.py | 1 + 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 43eead46a..9afa39ca7 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -1,4 +1,5 @@ import logging +from copy import deepcopy from typing import Any, Dict from jsonschema import Draft4Validator, validators @@ -42,15 +43,21 @@ def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]: :param conf: Config in JSON format :return: Returns the config if valid, otherwise throw an exception """ + conf_schema = deepcopy(constants.CONF_SCHEMA) + if conf.get('runmode', RunMode.OTHER) in (RunMode.DRY_RUN, RunMode.LIVE): + conf_schema['required'] = constants.SCHEMA_TRADE_REQUIRED + else: + conf_schema['required'] = constants.SCHEMA_MINIMAL_REQUIRED + try: - FreqtradeValidator(constants.CONF_SCHEMA).validate(conf) + FreqtradeValidator(conf_schema).validate(conf) return conf except ValidationError as e: logger.critical( f"Invalid configuration. See config.json.example. Reason: {e}" ) raise ValidationError( - best_match(Draft4Validator(constants.CONF_SCHEMA).iter_errors(conf)).message + best_match(Draft4Validator(conf_schema).iter_errors(conf)).message ) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index d7c6249d5..b21279bb4 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -269,16 +269,22 @@ CONF_SCHEMA = { 'required': ['process_throttle_secs', 'allowed_risk', 'capital_available_percentage'] } }, - 'required': [ - 'exchange', - 'max_open_trades', - 'stake_currency', - 'stake_amount', - 'dry_run', - 'dry_run_wallet', - 'bid_strategy', - 'unfilledtimeout', - 'stoploss', - 'minimal_roi', - ] } + +SCHEMA_TRADE_REQUIRED = [ + 'exchange', + 'max_open_trades', + 'stake_currency', + 'stake_amount', + 'dry_run', + 'dry_run_wallet', + 'bid_strategy', + 'unfilledtimeout', + 'stoploss', + 'minimal_roi', +] + +SCHEMA_MINIMAL_REQUIRED = [ + 'exchange', + 'dry_run', +] diff --git a/tests/test_configuration.py b/tests/test_configuration.py index ee3d23131..531a88688 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -49,6 +49,7 @@ def test_load_config_missing_attributes(default_conf) -> None: conf = deepcopy(default_conf) conf.pop('stake_currency') + conf['runmode'] = RunMode.DRY_RUN with pytest.raises(ValidationError, match=r".*'stake_currency' is a required property.*"): validate_config_schema(conf) From 20fc3b7978d036da11a56df2b8c81622854e2a6e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 10:41:10 +0100 Subject: [PATCH 186/324] validate config for utils too --- freqtrade/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 45520ecf7..9fe15aea6 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -12,7 +12,8 @@ from colorama import init as colorama_init from tabulate import tabulate from freqtrade.configuration import (Configuration, TimeRange, - remove_credentials) + remove_credentials, + validate_config_consistency) from freqtrade.configuration.directory_operations import (copy_sample_files, create_userdata_dir) from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGY @@ -40,6 +41,7 @@ def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str # Ensure we do not use Exchange credentials remove_credentials(config) + validate_config_consistency(config) return config From 22fcf7b4dc64c82058e4eb1349422ac83199c043 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 10:47:37 +0100 Subject: [PATCH 187/324] Allow empty stake currency in certain cases --- freqtrade/configuration/config_validation.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 9afa39ca7..f7886eec2 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -48,6 +48,10 @@ def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]: conf_schema['required'] = constants.SCHEMA_TRADE_REQUIRED else: conf_schema['required'] = constants.SCHEMA_MINIMAL_REQUIRED + # Dynamically allow empty stake-currency + # Since the minimal config specifies this too. + # It's not allowed for Dry-run or live modes + conf_schema['properties']['stake_currency']['enum'] += [''] try: FreqtradeValidator(conf_schema).validate(conf) From 9382b38c41509e51495d48248b968d58bdf1e6df Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 10:56:00 +0100 Subject: [PATCH 188/324] Fix mypy error --- freqtrade/configuration/config_validation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index f7886eec2..6f56790f4 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -51,7 +51,7 @@ def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]: # Dynamically allow empty stake-currency # Since the minimal config specifies this too. # It's not allowed for Dry-run or live modes - conf_schema['properties']['stake_currency']['enum'] += [''] + conf_schema['properties']['stake_currency']['enum'] += [''] # type: ignore try: FreqtradeValidator(conf_schema).validate(conf) From 1b8943ac54fabbad046503efeac88f0425a6684a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 10:59:10 +0100 Subject: [PATCH 189/324] Add documentation for tradable_balance_ratio --- config_full.json.example | 1 + docs/configuration.md | 7 +++++++ freqtrade/constants.py | 6 ++++++ 3 files changed, 14 insertions(+) diff --git a/config_full.json.example b/config_full.json.example index b9631f63d..4e692d371 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -2,6 +2,7 @@ "max_open_trades": 3, "stake_currency": "BTC", "stake_amount": 0.05, + "tradable_balance_ratio": 0.99, "fiat_display_currency": "USD", "amount_reserve_percent" : 0.05, "dry_run": false, diff --git a/docs/configuration.md b/docs/configuration.md index 90f0aa791..1acd6162c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -43,6 +43,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `max_open_trades` | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades).
***Datatype:*** *Positive integer or -1.* | `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *String* | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#understand-stake_amount). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Positive float or `"unlimited"`.* +| `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. Only applies when `stake_amount="unlimited"`. [More information below](#understand-stake_amount).
*Defaults to `0.99` 99%).*
***Datatype:*** *Positive float between `0.0` and `1.0`.* | `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 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* @@ -144,6 +145,12 @@ To allow the bot to trade all the available `stake_currency` in your account set "stake_amount" : "unlimited", ``` +When using `stake_amount="unlimited"`, we recommend to also set `tradable_balance_ratio=0.99` (99%) - to keep a minimum balance for eventual fees. +This will keep 1% of your account balance "untradable" in your account. + +!!! Note: + This configuration will allow increasing / decreasing stakes depending on the performance of the bot (lower stake if bot is loosing, higher stakes if the bot has a winning record, since higher balances are available). + In this case a trade amount is calculated as: ```python diff --git a/freqtrade/constants.py b/freqtrade/constants.py index d7c6249d5..d5ba00cd5 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -73,6 +73,12 @@ CONF_SCHEMA = { 'minimum': 0.0001, 'pattern': UNLIMITED_STAKE_AMOUNT }, + 'tradable_balance_ratio': { + 'type': ['number'], + 'minimum': 0.1, + 'maximum': 1, + 'default': 0.99 + }, 'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT}, 'dry_run': {'type': 'boolean'}, 'dry_run_wallet': {'type': 'number', 'default': DRY_RUN_WALLET}, From f15e5e9d5715c233d49adbe692dd38215e71a769 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 2 Jan 2020 13:51:25 +0300 Subject: [PATCH 190/324] Add _notify_buy() --- freqtrade/freqtradebot.py | 42 +++++++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d5d918585..9570a80ef 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -400,8 +400,6 @@ class FreqtradeBot: :param pair: pair for which we want to create a LIMIT_BUY :return: None """ - stake_currency = self.config['stake_currency'] - fiat_currency = self.config.get('fiat_display_currency', None) time_in_force = self.strategy.order_time_in_force['buy'] if price: @@ -458,17 +456,6 @@ class FreqtradeBot: amount = order['amount'] buy_limit_filled_price = order['price'] - self.rpc.send_msg({ - 'type': RPCMessageType.BUY_NOTIFICATION, - 'exchange': self.exchange.name.capitalize(), - 'pair': pair, - 'limit': buy_limit_filled_price, - 'order_type': order_type, - 'stake_amount': stake_amount, - 'stake_currency': stake_currency, - 'fiat_currency': fiat_currency - }) - # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') trade = Trade( @@ -486,6 +473,8 @@ class FreqtradeBot: ticker_interval=timeframe_to_minutes(self.config['ticker_interval']) ) + self._notify_buy(trade, order_type) + # Update fees if order is closed if order_status == 'closed': self.update_trade_state(trade, order) @@ -498,6 +487,30 @@ class FreqtradeBot: return True + def _notify_buy(self, trade: Trade, order_type: str): + """ + Sends rpc notification when a buy occured. + """ + msg = { + 'type': RPCMessageType.BUY_NOTIFICATION, + 'exchange': self.exchange.name.capitalize(), + 'pair': trade.pair, + 'limit': trade.open_rate, + 'order_type': order_type, + 'stake_amount': trade.stake_amount, + } + + if 'stake_currency' in self.config and 'fiat_display_currency' in self.config: + stake_currency = self.config['stake_currency'] + fiat_currency = self.config['fiat_display_currency'] + msg.update({ + 'stake_currency': stake_currency, + 'fiat_currency': fiat_currency, + }) + + # Send the message + self.rpc.send_msg(msg) + # # SELL / exit positions / close trades logic and methods # @@ -952,10 +965,9 @@ class FreqtradeBot: 'profit_percent': profit_percent, 'sell_reason': trade.sell_reason, 'open_date': trade.open_date, - 'close_date': trade.close_date or datetime.utcnow() + 'close_date': trade.close_date or datetime.utcnow(), } - # For regular case, when the configuration exists if 'stake_currency' in self.config and 'fiat_display_currency' in self.config: stake_currency = self.config['stake_currency'] fiat_currency = self.config['fiat_display_currency'] From 88efa4065bdcb222091d9594c2cb86a5a8b03461 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 2 Jan 2020 13:56:16 +0300 Subject: [PATCH 191/324] Align the name of a variable to be same for buy and sell parts --- freqtrade/freqtradebot.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9570a80ef..5e6ffbfde 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -913,16 +913,16 @@ class FreqtradeBot: except InvalidOrderException: logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") - ordertype = self.strategy.order_types[sell_type] + order_type = self.strategy.order_types[sell_type] if sell_reason == SellType.EMERGENCY_SELL: # Emergencysells (default to market!) - ordertype = self.strategy.order_types.get("emergencysell", "market") + order_type = self.strategy.order_types.get("emergencysell", "market") amount = self._safe_sell_amount(trade.pair, trade.amount) # Execute sell and update trade record order = self.exchange.sell(pair=str(trade.pair), - ordertype=ordertype, + ordertype=order_type, amount=amount, rate=limit, time_in_force=self.strategy.order_time_in_force['sell'] ) @@ -938,7 +938,7 @@ class FreqtradeBot: # Lock pair for one candle to prevent immediate rebuys self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['ticker_interval'])) - self._notify_sell(trade, ordertype) + self._notify_sell(trade, order_type) def _notify_sell(self, trade: Trade, order_type: str): """ From a47a25ca8889667bfec6cd948a2773b471cff87a Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Thu, 2 Jan 2020 14:38:25 +0300 Subject: [PATCH 192/324] Refine passing msg params --- freqtrade/freqtradebot.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5e6ffbfde..c18d9131f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -498,16 +498,10 @@ class FreqtradeBot: 'limit': trade.open_rate, 'order_type': order_type, 'stake_amount': trade.stake_amount, + 'stake_currency': self.config['stake_currency'], + 'fiat_currency': self.config.get('fiat_display_currency', None), } - if 'stake_currency' in self.config and 'fiat_display_currency' in self.config: - stake_currency = self.config['stake_currency'] - fiat_currency = self.config['fiat_display_currency'] - msg.update({ - 'stake_currency': stake_currency, - 'fiat_currency': fiat_currency, - }) - # Send the message self.rpc.send_msg(msg) @@ -966,13 +960,12 @@ class FreqtradeBot: 'sell_reason': trade.sell_reason, 'open_date': trade.open_date, 'close_date': trade.close_date or datetime.utcnow(), + 'stake_currency': self.config['stake_currency'], } - if 'stake_currency' in self.config and 'fiat_display_currency' in self.config: - stake_currency = self.config['stake_currency'] + if 'fiat_display_currency' in self.config: fiat_currency = self.config['fiat_display_currency'] msg.update({ - 'stake_currency': stake_currency, 'fiat_currency': fiat_currency, }) From 64db1f67365b128e8a9d9eb04893cbb5c5a8dd9c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 13:16:18 +0100 Subject: [PATCH 193/324] Prepare tests to valiate reduced full amount. --- tests/test_freqtradebot.py | 15 ++++++++++----- tests/test_integration.py | 15 +++++++++++---- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 1c17ce735..dc17ddaaa 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -150,11 +150,14 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: freqtrade.get_trade_stake_amount('ETH/BTC') -def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, - limit_buy_order, fee, mocker) -> None: +@pytest.mark.parametrize("balance_ratio,result1,result2", [ + (1, 0.005, 0.005), + (0.99, 0.00495, 0.00495), + ]) +def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1, + result2, limit_buy_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) - patch_wallet(mocker, free=default_conf['stake_amount']) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, @@ -164,20 +167,22 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, conf = deepcopy(default_conf) conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT + conf['dry_run_wallet'] = 0.01 conf['max_open_trades'] = 2 + conf['tradable_balance_ratio'] = balance_ratio freqtrade = FreqtradeBot(conf) patch_get_signal(freqtrade) # no open trades, order amount should be 'balance / max_open_trades' result = freqtrade.get_trade_stake_amount('ETH/BTC') - assert result == default_conf['stake_amount'] / conf['max_open_trades'] + assert result == result1 # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' freqtrade.execute_buy('ETH/BTC', result) result = freqtrade.get_trade_stake_amount('LTC/BTC') - assert result == default_conf['stake_amount'] / (conf['max_open_trades'] - 1) + assert result == result2 # create 2 trades, order amount should be None freqtrade.execute_buy('LTC/BTC', result) diff --git a/tests/test_integration.py b/tests/test_integration.py index 1108969ad..d1be961cf 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,10 +1,11 @@ - from unittest.mock import MagicMock +import pytest + from freqtrade.persistence import Trade +from freqtrade.rpc.rpc import RPC from freqtrade.strategy.interface import SellCheckTuple, SellType from tests.conftest import get_patched_freqtradebot, patch_get_signal -from freqtrade.rpc.rpc import RPC def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, @@ -112,13 +113,19 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, assert not trade.is_open -def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, mocker) -> None: +@pytest.mark.parametrize("balance_ratio,result1", [ + (1, 200), + (0.99, 198), +]) +def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, mocker, balance_ratio, + result1) -> None: """ Tests workflow """ default_conf['max_open_trades'] = 5 default_conf['forcebuy_enable'] = True default_conf['stake_amount'] = 'unlimited' + default_conf['tradable_balance_ratio'] = balance_ratio default_conf['dry_run_wallet'] = 1000 default_conf['exchange']['name'] = 'binance' default_conf['telegram']['enabled'] = True @@ -165,7 +172,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc assert len(trades) == 5 for trade in trades: - assert trade.stake_amount == 200 + assert trade.stake_amount == result1 # Reset trade open order id's trade.open_order_id = None trades = Trade.get_open_trades() From cba156dfff18d634440f1c4d7e46f95475f97f7f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 13:20:57 +0100 Subject: [PATCH 194/324] Add offset calculation for relative stake maximum limit --- freqtrade/freqtradebot.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d5d918585..6a6a14375 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -278,7 +278,15 @@ class FreqtradeBot: free_open_trades = self.get_free_open_trades() if not free_open_trades: return None - available_amount = self.wallets.get_free(self.config['stake_currency']) + + val_tied_up = Trade.total_open_trades_stakes() + + # Ensure 1% is used from the overall balance + # Otherwise we'd risk lowering stakes with each open trade. + # (tied up + current free) * ratio) - tied up + available_amount = ((val_tied_up + self.wallets.get_free(self.config['stake_currency'])) * + self.config['tradable_balance_ratio']) - val_tied_up + return available_amount / free_open_trades def _check_available_stake_amount(self, stake_amount: Optional[float]) -> Optional[float]: From bfef3cf497425fad61f00382119938ea39357eb4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 13:37:35 +0100 Subject: [PATCH 195/324] Add additional test case for lower balance ratios --- tests/test_freqtradebot.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index dc17ddaaa..9ca54d003 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -150,12 +150,13 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: freqtrade.get_trade_stake_amount('ETH/BTC') -@pytest.mark.parametrize("balance_ratio,result1,result2", [ - (1, 0.005, 0.005), - (0.99, 0.00495, 0.00495), +@pytest.mark.parametrize("balance_ratio,result1", [ + (1, 0.005), + (0.99, 0.00495), + (0.50, 0.0025), ]) def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1, - result2, limit_buy_order, fee, mocker) -> None: + limit_buy_order, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -182,7 +183,7 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r freqtrade.execute_buy('ETH/BTC', result) result = freqtrade.get_trade_stake_amount('LTC/BTC') - assert result == result2 + assert result == result1 # create 2 trades, order amount should be None freqtrade.execute_buy('LTC/BTC', result) From 94afb7cb1dff0c8d0d92d785fd9aed5854b17a66 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 13:45:03 +0100 Subject: [PATCH 196/324] Improve integration test with a few additional tests --- tests/test_integration.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index d1be961cf..98bf1862b 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -120,7 +120,10 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, mocker, balance_ratio, result1) -> None: """ - Tests workflow + Tests workflow unlimited stake-amount + Buy 4 trades, forcebuy a 5th trade + Sell one trade, calculated stake amount should now be lower than before since + one trade was sold at a loss. """ default_conf['max_open_trades'] = 5 default_conf['forcebuy_enable'] = True @@ -166,6 +169,8 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc trades = Trade.query.all() assert len(trades) == 4 + assert freqtrade.get_trade_stake_amount('XRP/BTC') == result1 + rpc._rpc_forcebuy('TKN/BTC', None) trades = Trade.query.all() @@ -184,6 +189,8 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc trades = Trade.get_open_trades() # One trade sold assert len(trades) == 4 + # stake-amount should now be reduced, since one trade was sold at a loss. + assert freqtrade.get_trade_stake_amount('XRP/BTC') < result1 # Validate that balance of sold trade is not in dry-run balances anymore. bals2 = freqtrade.wallets.get_all_balances() assert bals != bals2 From 6e615998c07809a99f1a82598ec07f2d8925c645 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 13:52:35 +0100 Subject: [PATCH 197/324] Fix documentation typo --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 1acd6162c..594b79050 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -148,7 +148,7 @@ To allow the bot to trade all the available `stake_currency` in your account set When using `stake_amount="unlimited"`, we recommend to also set `tradable_balance_ratio=0.99` (99%) - to keep a minimum balance for eventual fees. This will keep 1% of your account balance "untradable" in your account. -!!! Note: +!!! Note This configuration will allow increasing / decreasing stakes depending on the performance of the bot (lower stake if bot is loosing, higher stakes if the bot has a winning record, since higher balances are available). In this case a trade amount is calculated as: From c13c11cfa19050ee430dcd31ba95b951eb32918e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 14:41:28 +0100 Subject: [PATCH 198/324] Type does not need to be a list --- freqtrade/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index d5ba00cd5..dd3c07e69 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -74,7 +74,7 @@ CONF_SCHEMA = { 'pattern': UNLIMITED_STAKE_AMOUNT }, 'tradable_balance_ratio': { - 'type': ['number'], + 'type': 'number', 'minimum': 0.1, 'maximum': 1, 'default': 0.99 From b48bf035f6758bedf5773ec9b1b1ed98ca3477ae Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 14:52:34 +0100 Subject: [PATCH 199/324] Add note about MacOS installation --- docs/installation.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/installation.md b/docs/installation.md index 27b7a94c5..f7dce8787 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -270,3 +270,19 @@ The easiest way is to download install Microsoft Visual Studio Community [here]( Now you have an environment ready, the next step is [Bot Configuration](configuration.md). + + +## Trouble shooting + +### MacOS installation error + +Newer versions of MacOS may have installation fail with errors like `error: command 'g++' failed with exit status 1`. + +This error will require an 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. From 90744ff5abe262ef4d0731d966c5d8ad74ade076 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 19:36:31 +0100 Subject: [PATCH 200/324] show percent instead of ratio (!) --- freqtrade/plot/plotting.py | 4 ++-- tests/test_plotting.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index db4637ee5..e1989b249 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -120,8 +120,8 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: ) ) # Create description for sell summarizing the trade - desc = trades.apply(lambda row: f"{round(row['profitperc'], 3)}%, {row['sell_reason']}, " - f"{row['duration']} min", + desc = trades.apply(lambda row: f"{round(row['profitperc'] * 100, 1)}%, " + f"{row['sell_reason']}, {row['duration']} min", axis=1) trade_sells = go.Scatter( x=trades["close_time"], diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 9934d2493..271246517 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -119,6 +119,7 @@ def test_plot_trades(testdatadir, caplog): assert trade_sell.yaxis == 'y' assert len(trades) == len(trade_sell.x) assert trade_sell.marker.color == 'red' + assert trade_sell.text[0] == "4.0%, roi, 15 min" def test_generate_candlestick_graph_no_signals_no_trades(default_conf, mocker, testdatadir, caplog): From 560aea876efa6aaaec65f7def6c58a9c4613d33a Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 2 Jan 2020 20:20:29 +0100 Subject: [PATCH 201/324] Remove fiat_currency temporary variable --- freqtrade/freqtradebot.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c18d9131f..373055165 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -964,9 +964,8 @@ class FreqtradeBot: } if 'fiat_display_currency' in self.config: - fiat_currency = self.config['fiat_display_currency'] msg.update({ - 'fiat_currency': fiat_currency, + 'fiat_currency': self.config['fiat_display_currency'], }) # Send the message From da1fea6582790e99b3cd69688dd6dfb7c3b57e00 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jan 2020 06:37:36 +0100 Subject: [PATCH 202/324] Minor correction to wording of MacOS Specific install doc --- docs/installation.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/installation.md b/docs/installation.md index f7dce8787..267d91c8d 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -271,14 +271,13 @@ The easiest way is to download install Microsoft Visual Studio Community [here]( Now you have an environment ready, the next step is [Bot Configuration](configuration.md). - -## Trouble shooting +## Troubleshooting ### MacOS installation error -Newer versions of MacOS may have installation fail with errors like `error: command 'g++' failed with exit status 1`. +Newer versions of MacOS may have installation failed with errors like `error: command 'g++' failed with exit status 1`. -This error will require an explicit installation of the SDK Headers, which are not installed by default in this version of MacOS. +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 From 11059e532bf444c42daca77e31405d9e9b44f9a6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jan 2020 06:39:47 +0100 Subject: [PATCH 203/324] Fix missed default minimum in documentation --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 594b79050..2f7ef9f29 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -43,7 +43,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `max_open_trades` | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades).
***Datatype:*** *Positive integer or -1.* | `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *String* | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#understand-stake_amount). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Positive float or `"unlimited"`.* -| `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. Only applies when `stake_amount="unlimited"`. [More information below](#understand-stake_amount).
*Defaults to `0.99` 99%).*
***Datatype:*** *Positive float between `0.0` and `1.0`.* +| `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. Only applies when `stake_amount="unlimited"`. [More information below](#understand-stake_amount).
*Defaults to `0.99` 99%).*
***Datatype:*** *Positive float between `0.1` and `1.0`.* | `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 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* From a8d56b28503f62cae1f729cf27955fc98b7b6f39 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jan 2020 07:07:59 +0100 Subject: [PATCH 204/324] IMplement check for unlimited settings verifying that either max_open_trades or stake_amount is set for operations without edge --- freqtrade/configuration/config_validation.py | 12 ++++++++++++ tests/test_configuration.py | 8 ++++++++ 2 files changed, 20 insertions(+) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 43eead46a..02dee2afe 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -67,12 +67,24 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None: _validate_trailing_stoploss(conf) _validate_edge(conf) _validate_whitelist(conf) + _validate_unlimited_amount(conf) # validate configuration before returning logger.info('Validating configuration ...') validate_config_schema(conf) +def _validate_unlimited_amount(conf: Dict[str, Any]) -> None: + """ + If edge is disabled, either max_open_trades or stake_amount need to be set. + :raise: OperationalException if config validation failed + """ + if (not conf.get('edge', {}).get('enabled') + and conf.get('max_open_trades') == float('inf') + and conf.get('stake_amount') == constants.UNLIMITED_STAKE_AMOUNT): + raise OperationalException("`max_open_trades` and `stake_amount` cannot both be unlimited.") + + def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None: if conf.get('stoploss') == 0.0: diff --git a/tests/test_configuration.py b/tests/test_configuration.py index ee3d23131..6c0035395 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -722,6 +722,14 @@ def test_validate_default_conf(default_conf) -> None: validate_config_schema(default_conf) +def test_validate_max_open_trades(default_conf): + default_conf['max_open_trades'] = float('inf') + default_conf['stake_amount'] = 'unlimited' + with pytest.raises(OperationalException, match='`max_open_trades` and `stake_amount` ' + 'cannot both be unlimited.'): + validate_config_consistency(default_conf) + + def test_validate_tsl(default_conf): default_conf['stoploss'] = 0.0 with pytest.raises(OperationalException, match='The config stoploss needs to be different ' From 4ac1ac7ef5c227eee989219e420fe46163f95622 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jan 2020 09:56:06 +0100 Subject: [PATCH 205/324] Warn about tradable balance being applied to the current amount of the balance --- docs/configuration.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/configuration.md b/docs/configuration.md index 2f7ef9f29..4448f4548 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -157,6 +157,9 @@ In this case a trade amount is calculated as: currency_balance / (max_open_trades - current_open_trades) ``` +!!! Warning + `tradable_balance_ratio` applies to the current balance (free balance + tied up in trades). Therefore, assuming a starting balance of 1000, a configuration of `tradable_balance_ratio=0.99` will not guarantee that 10 units will always remain available on the exchange. The free amount may reduce to 5 units if the total balance is reduce to 500 (either by a losing streak, or by withdrawing balance). + !!! Note "When using Dry-Run Mode" When using `"stake_amount" : "unlimited",` in combination with Dry-Run, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time. It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency. From 3c7981160c3407e8a5c0c5a8d02b5ec2d362d61d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jan 2020 10:14:23 +0100 Subject: [PATCH 206/324] Extract `get_available_stake_amount` --- freqtrade/freqtradebot.py | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6a6a14375..bdba74215 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -270,6 +270,22 @@ class FreqtradeBot: return self._check_available_stake_amount(stake_amount) + def _get_available_stake_amount(self) -> float: + """ + Return the total currently available balance in stake currency, + respecting tradable_balance_ratio. + Calculated as + + free amount ) * tradable_balance_ratio - + """ + val_tied_up = Trade.total_open_trades_stakes() + + # Ensure % is used from the overall balance + # Otherwise we'd risk lowering stakes with each open trade. + # (tied up + current free) * ratio) - tied up + available_amount = ((val_tied_up + self.wallets.get_free(self.config['stake_currency'])) * + self.config['tradable_balance_ratio']) - val_tied_up + return available_amount + def _calculate_unlimited_stake_amount(self) -> Optional[float]: """ Calculate stake amount for "unlimited" stake amount @@ -279,13 +295,7 @@ class FreqtradeBot: if not free_open_trades: return None - val_tied_up = Trade.total_open_trades_stakes() - - # Ensure 1% is used from the overall balance - # Otherwise we'd risk lowering stakes with each open trade. - # (tied up + current free) * ratio) - tied up - available_amount = ((val_tied_up + self.wallets.get_free(self.config['stake_currency'])) * - self.config['tradable_balance_ratio']) - val_tied_up + available_amount = self._get_available_stake_amount() return available_amount / free_open_trades From 455838648da4dab24c1d5193764c880542faf406 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jan 2020 10:41:10 +0100 Subject: [PATCH 207/324] Apply get_available_balance logic to regular trades, too --- 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 bdba74215..aa47adfd4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -305,7 +305,7 @@ class FreqtradeBot: for the stake currency :return: float: Stake amount """ - available_amount = self.wallets.get_free(self.config['stake_currency']) + available_amount = self._get_available_stake_amount() if stake_amount is not None and available_amount < stake_amount: raise DependencyException( diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 9ca54d003..a2e6789ef 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3628,6 +3628,7 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order, # Initialize to 2 times stake amount default_conf['dry_run_wallet'] = 0.002 default_conf['max_open_trades'] = 2 + default_conf['tradable_balance_ratio'] = 1.0 patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -3649,5 +3650,5 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order, n = bot.enter_positions() assert n == 0 assert log_has_re(r"Unable to create trade for XRP/BTC: " - r"Available balance \(0 BTC\) is lower than stake amount \(0.001 BTC\)", + r"Available balance \(0.0 BTC\) is lower than stake amount \(0.001 BTC\)", caplog) From 6d01653bfec14900cd75fb0938a8c196f9ff8e20 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jan 2020 10:41:34 +0100 Subject: [PATCH 208/324] Adapt test to test more cases with reduced tradable_balance --- tests/test_freqtradebot.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index a2e6789ef..c8b627ca4 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -641,11 +641,15 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None: @pytest.mark.parametrize("max_open", range(0, 5)) -def test_create_trades_multiple_trades(default_conf, ticker, - fee, mocker, max_open) -> None: +@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, + max_open, tradable_balance_ratio, modifier) -> None: patch_RPCManager(mocker) patch_exchange(mocker) default_conf['max_open_trades'] = max_open + default_conf['tradable_balance_ratio'] = tradable_balance_ratio + default_conf['dry_run_wallet'] = 0.001 * max_open + mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, @@ -656,10 +660,11 @@ def test_create_trades_multiple_trades(default_conf, ticker, patch_get_signal(freqtrade) n = freqtrade.enter_positions() - assert n == max_open - trades = Trade.get_open_trades() - assert len(trades) == max_open + # Expected trades should be max_open * a modified value + # depending on the configured tradable_balance + assert n == max(int(max_open * modifier), 0) + assert len(trades) == max(int(max_open * modifier), 0) def test_create_trades_preopen(default_conf, ticker, fee, mocker) -> None: From f3beaa3374eae8300b5f845cb68dca4e16b1f9a8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jan 2020 10:58:31 +0100 Subject: [PATCH 209/324] Deprecate capital_available_percentage --- config.json.example | 1 - config_binance.json.example | 1 - config_full.json.example | 1 - config_kraken.json.example | 1 - docs/configuration.md | 2 +- docs/edge.md | 2 +- freqtrade/configuration/deprecated_settings.py | 10 ++++++++++ freqtrade/constants.py | 2 +- freqtrade/edge/__init__.py | 4 +++- tests/conftest.py | 2 +- tests/test_configuration.py | 10 ++++++++++ 11 files changed, 27 insertions(+), 9 deletions(-) diff --git a/config.json.example b/config.json.example index a2add358f..d46582d2b 100644 --- a/config.json.example +++ b/config.json.example @@ -59,7 +59,6 @@ "enabled": false, "process_throttle_secs": 3600, "calculate_since_number_of_days": 7, - "capital_available_percentage": 0.5, "allowed_risk": 0.01, "stoploss_range_min": -0.01, "stoploss_range_max": -0.1, diff --git a/config_binance.json.example b/config_binance.json.example index 8dc6f5c3a..11c309053 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -64,7 +64,6 @@ "enabled": false, "process_throttle_secs": 3600, "calculate_since_number_of_days": 7, - "capital_available_percentage": 0.5, "allowed_risk": 0.01, "stoploss_range_min": -0.01, "stoploss_range_max": -0.1, diff --git a/config_full.json.example b/config_full.json.example index 4e692d371..981fc5209 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -97,7 +97,6 @@ "enabled": false, "process_throttle_secs": 3600, "calculate_since_number_of_days": 7, - "capital_available_percentage": 0.5, "allowed_risk": 0.01, "stoploss_range_min": -0.01, "stoploss_range_max": -0.1, diff --git a/config_kraken.json.example b/config_kraken.json.example index 401ee7c9f..3cac29cef 100644 --- a/config_kraken.json.example +++ b/config_kraken.json.example @@ -70,7 +70,6 @@ "enabled": false, "process_throttle_secs": 3600, "calculate_since_number_of_days": 7, - "capital_available_percentage": 0.5, "allowed_risk": 0.01, "stoploss_range_min": -0.01, "stoploss_range_max": -0.1, diff --git a/docs/configuration.md b/docs/configuration.md index 4448f4548..d0450f5c9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -43,7 +43,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `max_open_trades` | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades).
***Datatype:*** *Positive integer or -1.* | `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *String* | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#understand-stake_amount). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Positive float or `"unlimited"`.* -| `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. Only applies when `stake_amount="unlimited"`. [More information below](#understand-stake_amount).
*Defaults to `0.99` 99%).*
***Datatype:*** *Positive float between `0.1` and `1.0`.* +| `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#understand-stake_amount).
*Defaults to `0.99` 99%).*
***Datatype:*** *Positive float between `0.1` and `1.0`.* | `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 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* diff --git a/docs/edge.md b/docs/edge.md index e7909594e..95f3de42e 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -148,7 +148,7 @@ Edge module has following configuration options: | `enabled` | If true, then Edge will run periodically.
*Defaults to `false`.*
***Datatype:*** *Boolean* | `process_throttle_secs` | How often should Edge run in seconds.
*Defaults to `3600` (once per hour).*
***Datatype:*** *Integer* | `calculate_since_number_of_days` | Number of days of data against which Edge calculates Win Rate, Risk Reward and Expectancy.
**Note** that it downloads historical data so increasing this number would lead to slowing down the bot.
*Defaults to `7`.*
***Datatype:*** *Integer* -| `capital_available_percentage` | This is the percentage of the total capital on exchange in stake currency.
As an example if you have 10 ETH available in your wallet on the exchange and this value is 0.5 (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers it as available capital.
*Defaults to `0.5`.*
***Datatype:*** *Float* +| `capital_available_percentage` | **DEPRECATED - replaced with `tradable_balance_ratio`** This is the percentage of the total capital on exchange in stake currency.
As an example if you have 10 ETH available in your wallet on the exchange and this value is 0.5 (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers it as available capital.
*Defaults to `0.5`.*
***Datatype:*** *Float* | `allowed_risk` | Ratio of allowed risk per trade.
*Defaults to `0.01` (1%)).*
***Datatype:*** *Float* | `stoploss_range_min` | Minimum stoploss.
*Defaults to `-0.01`.*
***Datatype:*** *Float* | `stoploss_range_max` | Maximum stoploss.
*Defaults to `-0.10`.*
***Datatype:*** *Float* diff --git a/freqtrade/configuration/deprecated_settings.py b/freqtrade/configuration/deprecated_settings.py index 260aae419..a104bf6e3 100644 --- a/freqtrade/configuration/deprecated_settings.py +++ b/freqtrade/configuration/deprecated_settings.py @@ -80,3 +80,13 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None: f"Using precision_filter setting is deprecated and has been replaced by" "PrecisionFilter. Please refer to the docs on configuration details") config['pairlists'].append({'method': 'PrecisionFilter'}) + + if (config.get('edge', {}.get('enabled', False)) + and config.get('edge', {}).get('capital_available_percentage')): + logger.warning( + "DEPRECATED: " + "Using 'edge.capital_available_percentage' has been deprecated in favor of " + "'tradable_balance_ratio'. Please migrate your configuration to " + "'tradable_balance_ratio' and remove 'capital_available_percentage' " + "from the edge configuration." + ) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index dd3c07e69..7dd75c438 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -272,7 +272,7 @@ CONF_SCHEMA = { 'max_trade_duration_minute': {'type': 'integer'}, 'remove_pumps': {'type': 'boolean'} }, - 'required': ['process_throttle_secs', 'allowed_risk', 'capital_available_percentage'] + 'required': ['process_throttle_secs', 'allowed_risk'] } }, 'required': [ diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 19d65d9d7..15883357b 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -57,7 +57,9 @@ class Edge: if self.config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT: raise OperationalException('Edge works only with unlimited stake amount') - self._capital_percentage: float = self.edge_config.get('capital_available_percentage') + # Deprecated capital_available_percentage. Will use tradable_balance_ratio in the future. + self._capital_percentage: float = self.edge_config.get( + 'capital_available_percentage', self.config['tradable_balance_ratio']) self._allowed_risk: float = self.edge_config.get('allowed_risk') self._since_number_of_days: int = self.edge_config.get('calculate_since_number_of_days', 14) self._last_updated: int = 0 # Timestamp of pairs last updated time diff --git a/tests/conftest.py b/tests/conftest.py index 501f89fff..7bb4cf4c9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1317,12 +1317,12 @@ def buy_order_fee(): def edge_conf(default_conf): conf = deepcopy(default_conf) conf['max_open_trades'] = -1 + conf['tradable_balance_ratio'] = 0.5 conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT conf['edge'] = { "enabled": True, "process_throttle_secs": 1800, "calculate_since_number_of_days": 14, - "capital_available_percentage": 0.5, "allowed_risk": 0.01, "stoploss_range_min": -0.01, "stoploss_range_max": -0.1, diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 6c0035395..3e7b51c0e 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1036,6 +1036,16 @@ def test_process_deprecated_setting_pairlists(mocker, default_conf, caplog): assert log_has_re(r'DEPRECATED.*in pairlist is deprecated and must be moved*', caplog) +def test_process_deprecated_setting_edge(mocker, edge_conf, caplog): + patched_configuration_load_config_file(mocker, edge_conf) + edge_conf.update({'edge': { + 'capital_available_percentage': 0.5, + }}) + + process_temporary_deprecated_settings(edge_conf) + assert log_has_re(r"DEPRECATED.*Using 'edge.capital_available_percentage'*", caplog) + + def test_check_conflicting_settings(mocker, default_conf, caplog): patched_configuration_load_config_file(mocker, default_conf) From 0dd274917f820e6fc4892503ea7b09a4f5f42b7d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jan 2020 11:16:59 +0100 Subject: [PATCH 210/324] Update documentation regarding configuration of stake_amount --- docs/configuration.md | 56 +++++++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index d0450f5c9..e9756e8ac 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -40,10 +40,10 @@ Mandatory parameters are marked as **Required**, which means that they are requi | Parameter | Description | |------------|-------------| -| `max_open_trades` | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades).
***Datatype:*** *Positive integer or -1.* +| `max_open_trades` | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades). [More information below](#configuring-amount-per-trade).
***Datatype:*** *Positive integer or -1.* | `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *String* -| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#understand-stake_amount). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Positive float or `"unlimited"`.* -| `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#understand-stake_amount).
*Defaults to `0.99` 99%).*
***Datatype:*** *Positive float between `0.1` and `1.0`.* +| `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Positive float or `"unlimited"`.* +| `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade).
*Defaults to `0.99` 99%).*
***Datatype:*** *Positive float between `0.1` and `1.0`.* | `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 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* @@ -130,26 +130,39 @@ Values set in the configuration file always overwrite values set in the strategy * `sell_profit_only` (ask_strategy) * `ignore_roi_if_buy_signal` (ask_strategy) -### Understand stake_amount +### Configuring amount per trade -The `stake_amount` configuration parameter is an amount of crypto-currency your bot will use for each trade. +There are several methods to configure how much of the stake currency the bot will use to enter a trade. All methods respect the [available balance configuration](#available-balance) as explained below. -The minimal configuration value is 0.0001. Please check your exchange's trading minimums to avoid problems. +#### Available balance + +By default, the bot assumes that the `complete amount - 1%` is at it's disposal, and when using [dynamic stake amount](#dynamic-stake-amount), it will split the complete balance into `max_open_trades` buckets per trade. +Freqtrade will reserve 1% for eventual fees when entering a trade and will therefore not touch that by default. + +You can configure the "untouched" amount by using the `tradable_balance_ratio` setting. + +For example, if you have 10 ETH available in your wallet on the exchange and `tradable_balance_ratio=0.5` (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers this as available balance. + +!!! Warning + `tradable_balance_ratio` applies to the current balance (free balance + tied up in trades). Therefore, assuming a starting balance of 1000, a configuration of `tradable_balance_ratio=0.99` will not guarantee that 10 units will always remain available on the exchange. The free amount may reduce to 5 units if the total balance is reduce to 500 (either by a losing streak, or by withdrawing balance). + +#### Static Stake amount + +The `stake_amount` configuration statically configures the amount of stake-currency your bot will use for each trade. + +The minimal configuration value is 0.0001, however, please check your exchange's trading minimums for the stake currency you're using to avoid problems. This setting works in combination with `max_open_trades`. The maximum capital engaged in trades is `stake_amount * max_open_trades`. For example, the bot will at most use (0.05 BTC x 3) = 0.15 BTC, assuming a configuration of `max_open_trades=3` and `stake_amount=0.05`. -To allow the bot to trade all the available `stake_currency` in your account set - -```json -"stake_amount" : "unlimited", -``` - -When using `stake_amount="unlimited"`, we recommend to also set `tradable_balance_ratio=0.99` (99%) - to keep a minimum balance for eventual fees. -This will keep 1% of your account balance "untradable" in your account. - !!! Note - This configuration will allow increasing / decreasing stakes depending on the performance of the bot (lower stake if bot is loosing, higher stakes if the bot has a winning record, since higher balances are available). + This setting respects the [available balance configuration](#available-balance). + +#### Dynamic stake amount + +Alternatively, you can use a dynamic stake amount, which will use the available balance on the exchange, and divide that equally by the amount of allowed trades (`max_open_trades`). + +To configure this, set `stake_amount="unlimited"`. We also recommend to set `tradable_balance_ratio=0.99` (99%) - to keep a minimum balance for eventual fees. In this case a trade amount is calculated as: @@ -157,8 +170,15 @@ In this case a trade amount is calculated as: currency_balance / (max_open_trades - current_open_trades) ``` -!!! Warning - `tradable_balance_ratio` applies to the current balance (free balance + tied up in trades). Therefore, assuming a starting balance of 1000, a configuration of `tradable_balance_ratio=0.99` will not guarantee that 10 units will always remain available on the exchange. The free amount may reduce to 5 units if the total balance is reduce to 500 (either by a losing streak, or by withdrawing balance). +To allow the bot to trade all the available `stake_currency` in your account (minus `tradable_balance_ratio`) set + +```json +"stake_amount" : "unlimited", +"tradable_balance_ratio": 0.99, +``` + +!!! Note + This configuration will allow increasing / decreasing stakes depending on the performance of the bot (lower stake if bot is loosing, higher stakes if the bot has a winning record, since higher balances are available). !!! Note "When using Dry-Run Mode" When using `"stake_amount" : "unlimited",` in combination with Dry-Run, the balance will be simulated starting with a stake of `dry_run_wallet` which will evolve over time. It is therefore important to set `dry_run_wallet` to a sensible value (like 0.05 or 0.01 for BTC and 1000 or 100 for USDT, for example), otherwise it may simulate trades with 100 BTC (or more) or 0.05 USDT (or less) at once - which may not correspond to your real available balance or is less than the exchange minimal limit for the order amount for the stake currency. From 55041878ae7da0758a8e58d2c1add3139e54fcbb Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jan 2020 11:20:08 +0100 Subject: [PATCH 211/324] Update Backtesting fee documentation --- docs/backtesting.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index ac7c8e11a..c2359b370 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -79,12 +79,14 @@ Please also read about the [strategy startup period](strategy-customization.md#s Sometimes your account has certain fee rebates (fee reductions starting with a certain account size or monthly volume), which are not visible to ccxt. To account for this in backtesting, you can use `--fee 0.001` to supply this value to backtesting. -This fee must be a percentage, and will be applied twice (once for trade entry, and once for trade exit). +This fee must be a ratio, and will be applied twice (once for trade entry, and once for trade exit). ```bash freqtrade backtesting --fee 0.001 ``` +!!! Note + Only supply this parameter if you want to experiment with different fee values. By default, Backtesting fetches the exchange's default fee from the exchange. #### Running backtest with smaller testset by using timerange From 71dd0386640e675771881395e19163ce1131df41 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jan 2020 11:23:06 +0100 Subject: [PATCH 212/324] add tradable_balance_ratio to to all config samples --- config.json.example | 1 + config_binance.json.example | 1 + config_kraken.json.example | 1 + 3 files changed, 3 insertions(+) diff --git a/config.json.example b/config.json.example index d46582d2b..8b85e71eb 100644 --- a/config.json.example +++ b/config.json.example @@ -2,6 +2,7 @@ "max_open_trades": 3, "stake_currency": "BTC", "stake_amount": 0.05, + "tradable_balance_ratio": 0.99, "fiat_display_currency": "USD", "ticker_interval" : "5m", "dry_run": false, diff --git a/config_binance.json.example b/config_binance.json.example index 11c309053..0521a3a35 100644 --- a/config_binance.json.example +++ b/config_binance.json.example @@ -2,6 +2,7 @@ "max_open_trades": 3, "stake_currency": "BTC", "stake_amount": 0.05, + "tradable_balance_ratio": 0.99, "fiat_display_currency": "USD", "ticker_interval" : "5m", "dry_run": true, diff --git a/config_kraken.json.example b/config_kraken.json.example index 3cac29cef..a527b569d 100644 --- a/config_kraken.json.example +++ b/config_kraken.json.example @@ -2,6 +2,7 @@ "max_open_trades": 5, "stake_currency": "EUR", "stake_amount": 10, + "tradable_balance_ratio": 0.99, "fiat_display_currency": "EUR", "ticker_interval" : "5m", "dry_run": true, From 7e7c82cf4a80cd7d70a3a0317cf7158cefd9c6f5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jan 2020 11:34:17 +0100 Subject: [PATCH 213/324] Small adjustments to relative_stake PR --- docs/configuration.md | 2 +- docs/edge.md | 2 +- freqtrade/configuration/deprecated_settings.py | 4 ++-- tests/test_configuration.py | 1 + 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index e9756e8ac..3b8760366 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -146,7 +146,7 @@ For example, if you have 10 ETH available in your wallet on the exchange and `tr !!! Warning `tradable_balance_ratio` applies to the current balance (free balance + tied up in trades). Therefore, assuming a starting balance of 1000, a configuration of `tradable_balance_ratio=0.99` will not guarantee that 10 units will always remain available on the exchange. The free amount may reduce to 5 units if the total balance is reduce to 500 (either by a losing streak, or by withdrawing balance). -#### Static Stake amount +#### Static stake amount The `stake_amount` configuration statically configures the amount of stake-currency your bot will use for each trade. diff --git a/docs/edge.md b/docs/edge.md index 95f3de42e..dcefe7451 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -148,7 +148,7 @@ Edge module has following configuration options: | `enabled` | If true, then Edge will run periodically.
*Defaults to `false`.*
***Datatype:*** *Boolean* | `process_throttle_secs` | How often should Edge run in seconds.
*Defaults to `3600` (once per hour).*
***Datatype:*** *Integer* | `calculate_since_number_of_days` | Number of days of data against which Edge calculates Win Rate, Risk Reward and Expectancy.
**Note** that it downloads historical data so increasing this number would lead to slowing down the bot.
*Defaults to `7`.*
***Datatype:*** *Integer* -| `capital_available_percentage` | **DEPRECATED - replaced with `tradable_balance_ratio`** This is the percentage of the total capital on exchange in stake currency.
As an example if you have 10 ETH available in your wallet on the exchange and this value is 0.5 (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers it as available capital.
*Defaults to `0.5`.*
***Datatype:*** *Float* +| `capital_available_percentage` | **DEPRECATED - [replaced with `tradable_balance_ratio`](configuration.md#Available balance)** This is the percentage of the total capital on exchange in stake currency.
As an example if you have 10 ETH available in your wallet on the exchange and this value is 0.5 (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers it as available capital.
*Defaults to `0.5`.*
***Datatype:*** *Float* | `allowed_risk` | Ratio of allowed risk per trade.
*Defaults to `0.01` (1%)).*
***Datatype:*** *Float* | `stoploss_range_min` | Minimum stoploss.
*Defaults to `-0.01`.*
***Datatype:*** *Float* | `stoploss_range_max` | Maximum stoploss.
*Defaults to `-0.10`.*
***Datatype:*** *Float* diff --git a/freqtrade/configuration/deprecated_settings.py b/freqtrade/configuration/deprecated_settings.py index a104bf6e3..78d8218d4 100644 --- a/freqtrade/configuration/deprecated_settings.py +++ b/freqtrade/configuration/deprecated_settings.py @@ -81,8 +81,8 @@ def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None: "PrecisionFilter. Please refer to the docs on configuration details") config['pairlists'].append({'method': 'PrecisionFilter'}) - if (config.get('edge', {}.get('enabled', False)) - and config.get('edge', {}).get('capital_available_percentage')): + if (config.get('edge', {}).get('enabled', False) + and 'capital_available_percentage' in config.get('edge', {})): logger.warning( "DEPRECATED: " "Using 'edge.capital_available_percentage' has been deprecated in favor of " diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 3e7b51c0e..a489875d9 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1039,6 +1039,7 @@ def test_process_deprecated_setting_pairlists(mocker, default_conf, caplog): def test_process_deprecated_setting_edge(mocker, edge_conf, caplog): patched_configuration_load_config_file(mocker, edge_conf) edge_conf.update({'edge': { + 'enabled': True, 'capital_available_percentage': 0.5, }}) From e1f89e3ad34187caf0385a841f13ec1c710fbd17 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jan 2020 20:11:58 +0100 Subject: [PATCH 214/324] Reword Note in backtesting fee docs --- docs/backtesting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index c2359b370..45759e2aa 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -86,7 +86,7 @@ freqtrade backtesting --fee 0.001 ``` !!! Note - Only supply this parameter if you want to experiment with different fee values. By default, Backtesting fetches the exchange's default fee from the exchange. + Only supply this option (or the corresponding configuration parameter) if you want to experiment with different fee values. By default, Backtesting fetches the default fee from the exchange pair/market info. #### Running backtest with smaller testset by using timerange From 24aa596e3c956df3e3b8967e0334246574f8e434 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Sat, 4 Jan 2020 01:08:37 +0300 Subject: [PATCH 215/324] Minor: Refine fee example in the docs Taken from https://github.com/freqtrade/freqtrade/issues/2738#issuecomment-570687230. slightly reworded. --- docs/backtesting.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index 45759e2aa..41428085d 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -78,9 +78,11 @@ Please also read about the [strategy startup period](strategy-customization.md#s #### Supplying custom fee value Sometimes your account has certain fee rebates (fee reductions starting with a certain account size or monthly volume), which are not visible to ccxt. -To account for this in backtesting, you can use `--fee 0.001` to supply this value to backtesting. +To account for this in backtesting, you can use the `--fee` command line option to supply this value to backtesting. This fee must be a ratio, and will be applied twice (once for trade entry, and once for trade exit). +For example, if the buying and selling commission fee is 0.1% (i.e., 0.001 written as ratio), then you would run backtesting as the following: + ```bash freqtrade backtesting --fee 0.001 ``` From 84ef588163e95adaa3359200ddaa3a7ef64d0b8c Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jan 2020 13:27:22 +0100 Subject: [PATCH 216/324] support dicts as indicators --- freqtrade/plot/plotting.py | 66 +++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 8 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index e1989b249..2bf4afcfb 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Any, Dict, List +from typing import Any, Dict, List, Union import pandas as pd from freqtrade.configuration import TimeRange @@ -13,6 +13,8 @@ from freqtrade.resolvers import StrategyResolver logger = logging.getLogger(__name__) +IndicatorType = Union[List[str], Dict[str, Dict]] + try: from plotly.subplots import make_subplots from plotly.offline import plot @@ -54,9 +56,9 @@ def init_plotscript(config): } -def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> make_subplots: +def _add_indicators_list(fig, row, indicators: List[str], data: pd.DataFrame) -> make_subplots: """ - Generator all the indicator selected by the user for a specific row + Generate all the indicator selected by the user for a specific row :param fig: Plot figure to append to :param row: row number for this plot :param indicators: List of indicators present in the dataframe @@ -81,6 +83,51 @@ def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> make_ return fig +def _add_indicators_dict(fig, row, indicators: Dict[str, Dict], + data: pd.DataFrame) -> make_subplots: + """ + Generate all the indicators selected by the user for a specific row, based on the configuration + :param fig: Plot figure to append to + :param row: row number for this plot + :param indicators: Dict of Indicators with configuration options. + Dict key must correspond to dataframe column. + :param data: candlestick DataFrame + """ + for indicator, conf in indicators.items(): + print(conf) + if indicator in data: + scatter = go.Scatter( + x=data['date'], + y=data[indicator].values, + mode='lines', + name=indicator + ) + fig.add_trace(scatter, row, 1) + else: + logger.info( + 'Indicator "%s" ignored. Reason: This indicator is not found ' + 'in your strategy.', + indicator + ) + + return fig + + +def add_indicators(fig, row, indicators: IndicatorType, data: pd.DataFrame) -> make_subplots: + """ + Generate all the indicator selected by the user for a specific row + :param fig: Plot figure to append to + :param row: row number for this plot + :param indicators: List of indicators present in the dataframe, Or Dict of Indicators + with configuration options. Dict key must correspond to dataframe column. + :param data: candlestick DataFrame + """ + if isinstance(indicators, list): + _add_indicators_list(fig, row, indicators, data) + else: + _add_indicators_dict(fig, row, indicators, data) + + def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> make_subplots: """ Add profit-plot @@ -144,8 +191,8 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, - indicators1: List[str] = [], - indicators2: List[str] = [],) -> go.Figure: + indicators1: IndicatorType = [], + indicators2: IndicatorType = [],) -> go.Figure: """ Generate the graph from the data generated by Backtesting or from DB Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators @@ -254,7 +301,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra name='Volume', marker_color='DarkSlateGrey', marker_line_color='DarkSlateGrey' - ) + ) fig.add_trace(volume, 2, 1) # Add indicators to separate row @@ -355,12 +402,15 @@ def load_and_plot_trades(config: Dict[str, Any]): trades_pair = trades.loc[trades['pair'] == pair] trades_pair = extract_trades_of_period(dataframe, trades_pair) + indicators1 = config["indicators1"] + indicators2 = config["indicators2"] + fig = generate_candlestick_graph( pair=pair, data=dataframe, trades=trades_pair, - indicators1=config["indicators1"], - indicators2=config["indicators2"], + indicators1=indicators1, + indicators2=indicators2, ) store_plot_file(fig, filename=generate_plot_filename(pair, config['ticker_interval']), From 5d5074ac9c1af08034f283c39244a4e50b8652da Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 3 Jan 2020 20:10:22 +0100 Subject: [PATCH 217/324] Implement first working version of plot_config --- freqtrade/plot/plotting.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 2bf4afcfb..cb5806f75 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -96,11 +96,15 @@ def _add_indicators_dict(fig, row, indicators: Dict[str, Dict], for indicator, conf in indicators.items(): print(conf) if indicator in data: + kwargs = {'x': data['date'], + 'y': data[indicator].values, + 'mode': 'lines', + 'name': indicator + } + if 'color' in conf: + kwargs.update({'line': {'color': conf['color']}}) scatter = go.Scatter( - x=data['date'], - y=data[indicator].values, - mode='lines', - name=indicator + **kwargs ) fig.add_trace(scatter, row, 1) else: @@ -123,9 +127,9 @@ def add_indicators(fig, row, indicators: IndicatorType, data: pd.DataFrame) -> m :param data: candlestick DataFrame """ if isinstance(indicators, list): - _add_indicators_list(fig, row, indicators, data) + return _add_indicators_list(fig, row, indicators, data) else: - _add_indicators_dict(fig, row, indicators, data) + return _add_indicators_dict(fig, row, indicators, data) def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> make_subplots: @@ -376,6 +380,17 @@ def store_plot_file(fig, filename: str, directory: Path, auto_open: bool = False logger.info(f"Stored plot as {_filename}") +def _get_plot_indicators(config, strategy): + + if hasattr(strategy, 'plot_config'): + indicators1 = strategy.plot_config['main_plot'] + indicators2 = strategy.plot_config['subplots'] + else: + indicators1 = config.get("indicators1") + indicators2 = config.get("indicators2") + return indicators1, indicators2 + + def load_and_plot_trades(config: Dict[str, Any]): """ From configuration provided @@ -402,8 +417,7 @@ def load_and_plot_trades(config: Dict[str, Any]): trades_pair = trades.loc[trades['pair'] == pair] trades_pair = extract_trades_of_period(dataframe, trades_pair) - indicators1 = config["indicators1"] - indicators2 = config["indicators2"] + indicators1, indicators2 = _get_plot_indicators(config, strategy) fig = generate_candlestick_graph( pair=pair, From 5853b9904c2cefa163d6c6d3b8536b5170b74ad3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Jan 2020 11:13:45 +0100 Subject: [PATCH 218/324] make Plot_config the default approach --- freqtrade/plot/plotting.py | 128 ++++++++++++++++--------------------- 1 file changed, 56 insertions(+), 72 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index cb5806f75..c11776fab 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -56,35 +56,7 @@ def init_plotscript(config): } -def _add_indicators_list(fig, row, indicators: List[str], data: pd.DataFrame) -> make_subplots: - """ - Generate all the indicator selected by the user for a specific row - :param fig: Plot figure to append to - :param row: row number for this plot - :param indicators: List of indicators present in the dataframe - :param data: candlestick DataFrame - """ - for indicator in indicators: - if indicator in data: - scatter = go.Scatter( - x=data['date'], - y=data[indicator].values, - mode='lines', - name=indicator - ) - fig.add_trace(scatter, row, 1) - else: - logger.info( - 'Indicator "%s" ignored. Reason: This indicator is not found ' - 'in your strategy.', - indicator - ) - - return fig - - -def _add_indicators_dict(fig, row, indicators: Dict[str, Dict], - data: pd.DataFrame) -> make_subplots: +def add_indicators(fig, row, indicators: Dict[str, Dict], data: pd.DataFrame) -> make_subplots: """ Generate all the indicators selected by the user for a specific row, based on the configuration :param fig: Plot figure to append to @@ -94,7 +66,7 @@ def _add_indicators_dict(fig, row, indicators: Dict[str, Dict], :param data: candlestick DataFrame """ for indicator, conf in indicators.items(): - print(conf) + logger.debug(f"indicator {indicator} with config {conf}") if indicator in data: kwargs = {'x': data['date'], 'y': data[indicator].values, @@ -117,21 +89,6 @@ def _add_indicators_dict(fig, row, indicators: Dict[str, Dict], return fig -def add_indicators(fig, row, indicators: IndicatorType, data: pd.DataFrame) -> make_subplots: - """ - Generate all the indicator selected by the user for a specific row - :param fig: Plot figure to append to - :param row: row number for this plot - :param indicators: List of indicators present in the dataframe, Or Dict of Indicators - with configuration options. Dict key must correspond to dataframe column. - :param data: candlestick DataFrame - """ - if isinstance(indicators, list): - return _add_indicators_list(fig, row, indicators, data) - else: - return _add_indicators_dict(fig, row, indicators, data) - - def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> make_subplots: """ Add profit-plot @@ -194,9 +151,39 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: return fig -def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, - indicators1: IndicatorType = [], - indicators2: IndicatorType = [],) -> go.Figure: +def create_plotconfig(indicators1: List[str], indicators2: List[str], plot_config: Dict[str, Dict]) -> Dict[str, Dict]: + """ + Combines indicators 1 and indicators 2 into plot_config if necessary + :param indicators1: List containing Main plot indicators + :param indicators2: List containing Sub plot indicators + :param plot_config: Dict of Dicts containing advanced plot configuration + :return: plot_config - eventually with indicators 1 and 2 + """ + + if not plot_config: + # If no indicators and no plot-config given, use defaults. + if not indicators1: + indicators1 = ['sma', 'ema3', 'ema5'] + if not indicators2: + indicators1 = ['macd', 'macdsignal'] + + # Create subplot configuration if plot_config is not available. + plot_config = { + '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'] = {} + + +def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFrame = None, *, + indicators1: List[str] = [], + indicators2: List[str] = [], + plot_config: Dict[str, Dict] = {}, + ) -> go.Figure: """ Generate the graph from the data generated by Backtesting or from DB Volume will always be ploted in row2, so Row 1 and 3 are to our disposal for custom indicators @@ -205,21 +192,26 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra :param trades: All trades created :param indicators1: List containing Main plot indicators :param indicators2: List containing Sub plot indicators - :return: None + :param plot_config: Dict of Dicts containing advanced plot configuration + :return: Plotly figure """ + plot_config = create_plotconfig(indicators1, indicators2) + rows = 2 + len(plot_config['subplots']) + row_widths = [1 for _ in plot_config['subplots']] # Define the graph fig = make_subplots( - rows=3, + rows=rows, cols=1, shared_xaxes=True, - row_width=[1, 1, 4], + row_width=row_widths + [1, 4], vertical_spacing=0.0001, ) fig['layout'].update(title=pair) fig['layout']['yaxis1'].update(title='Price') fig['layout']['yaxis2'].update(title='Volume') - fig['layout']['yaxis3'].update(title='Other') + for i, name in enumerate(plot_config['subplots']): + fig['layout'][f'yaxis{3 + i}'].update(title=name) fig['layout']['xaxis']['rangeslider'].update(visible=False) # Common information @@ -289,12 +281,13 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra ) fig.add_trace(bb_lower, 1, 1) fig.add_trace(bb_upper, 1, 1) - if 'bb_upperband' in indicators1 and 'bb_lowerband' in indicators1: - indicators1.remove('bb_upperband') - indicators1.remove('bb_lowerband') + 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'] # Add indicators to main plot - fig = add_indicators(fig=fig, row=1, indicators=indicators1, data=data) + fig = add_indicators(fig=fig, row=1, indicators=plot_config['main_plot'], data=data) fig = plot_trades(fig, trades) @@ -309,7 +302,10 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra fig.add_trace(volume, 2, 1) # Add indicators to separate row - fig = add_indicators(fig=fig, row=3, indicators=indicators2, data=data) + for i, name in enumerate(plot_config['subplots']): + fig = add_indicators(fig=fig, row=3 + i, + indicators=plot_config['subplots'][name], + data=data) return fig @@ -380,17 +376,6 @@ def store_plot_file(fig, filename: str, directory: Path, auto_open: bool = False logger.info(f"Stored plot as {_filename}") -def _get_plot_indicators(config, strategy): - - if hasattr(strategy, 'plot_config'): - indicators1 = strategy.plot_config['main_plot'] - indicators2 = strategy.plot_config['subplots'] - else: - indicators1 = config.get("indicators1") - indicators2 = config.get("indicators2") - return indicators1, indicators2 - - def load_and_plot_trades(config: Dict[str, Any]): """ From configuration provided @@ -417,14 +402,13 @@ def load_and_plot_trades(config: Dict[str, Any]): trades_pair = trades.loc[trades['pair'] == pair] trades_pair = extract_trades_of_period(dataframe, trades_pair) - indicators1, indicators2 = _get_plot_indicators(config, strategy) - fig = generate_candlestick_graph( pair=pair, data=dataframe, trades=trades_pair, - indicators1=indicators1, - indicators2=indicators2, + indicators1=config["indicators1"], + indicators2=config["indicators2"], + plot_config=strategy.plot_config if hasattr(strategy, 'plot_config') else {} ) store_plot_file(fig, filename=generate_plot_filename(pair, config['ticker_interval']), From f04873b0b06df81bed51dfd59fb7db1e29422f55 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Jan 2020 11:14:00 +0100 Subject: [PATCH 219/324] Add plot_config to interface --- freqtrade/configuration/cli_options.py | 6 ++---- freqtrade/strategy/interface.py | 2 ++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/configuration/cli_options.py index 4b6429f20..1807cd591 100644 --- a/freqtrade/configuration/cli_options.py +++ b/freqtrade/configuration/cli_options.py @@ -363,15 +363,13 @@ AVAILABLE_CLI_OPTIONS = { "indicators1": Arg( '--indicators1', help='Set indicators from your strategy you want in the first row of the graph. ' - 'Space-separated list. Example: `ema3 ema5`. Default: `%(default)s`.', - default=['sma', 'ema3', 'ema5'], + "Space-separated list. Example: `ema3 ema5`. Default: `['sma', 'ema3', 'ema5']`.", nargs='+', ), "indicators2": Arg( '--indicators2', help='Set indicators from your strategy you want in the third row of the graph. ' - 'Space-separated list. Example: `fastd fastk`. Default: `%(default)s`.', - default=['macd', 'macdsignal'], + "Space-separated list. Example: `fastd fastk`. Default: `['macd', 'macdsignal']`.", nargs='+', ), "plot_limit": Arg( diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 4f2e990d2..a2dee7837 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -112,6 +112,8 @@ class IStrategy(ABC): dp: Optional[DataProvider] = None wallets: Optional[Wallets] = None + plot_config: Dict + def __init__(self, config: dict) -> None: self.config = config # Dict to determine if analysis is necessary From 4628024de6e9d7c35977b9d2459651560fe75111 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Jan 2020 11:18:51 +0100 Subject: [PATCH 220/324] Adapt tests to new add_indicator methodology --- freqtrade/plot/plotting.py | 3 ++- tests/test_plotting.py | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index c11776fab..a79ec6b87 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -177,6 +177,7 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], plot_confi 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, *, @@ -195,7 +196,7 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra :param plot_config: Dict of Dicts containing advanced plot configuration :return: Plotly figure """ - plot_config = create_plotconfig(indicators1, indicators2) + plot_config = create_plotconfig(indicators1, indicators2, plot_config) rows = 2 + len(plot_config['subplots']) row_widths = [1 for _ in plot_config['subplots']] diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 271246517..048f9d60c 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -66,8 +66,8 @@ def test_add_indicators(default_conf, testdatadir, caplog): data = history.load_pair_history(pair=pair, timeframe='1m', datadir=testdatadir, timerange=timerange) - indicators1 = ["ema10"] - indicators2 = ["macd"] + indicators1 = {"ema10": {}} + indicators2 = {"macd": {"color": "red"}} # Generate buy/sell signals and indicators strat = DefaultStrategy(default_conf) @@ -86,9 +86,10 @@ def test_add_indicators(default_conf, testdatadir, caplog): macd = find_trace_in_fig_data(figure.data, "macd") assert isinstance(macd, go.Scatter) assert macd.yaxis == "y3" + assert macd.line.color == "red" # No indicator found - fig3 = add_indicators(fig=deepcopy(fig), row=3, indicators=['no_indicator'], data=data) + fig3 = add_indicators(fig=deepcopy(fig), row=3, indicators={'no_indicator': {}}, data=data) assert fig == fig3 assert log_has_re(r'Indicator "no_indicator" ignored\..*', caplog) From b5a806dec766301632dd4ccad169cba598f3bbd1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Jan 2020 11:30:21 +0100 Subject: [PATCH 221/324] Fix typo and add tests for create_plotconfig --- freqtrade/plot/plotting.py | 2 +- tests/test_plotting.py | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index a79ec6b87..1c8ad552f 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -165,7 +165,7 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], plot_confi if not indicators1: indicators1 = ['sma', 'ema3', 'ema5'] if not indicators2: - indicators1 = ['macd', 'macdsignal'] + indicators2 = ['macd', 'macdsignal'] # Create subplot configuration if plot_config is not available. plot_config = { diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 048f9d60c..908c90a0a 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -13,6 +13,7 @@ from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data from freqtrade.exceptions import OperationalException from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit from freqtrade.plot.plotting import (add_indicators, add_profit, + create_plotconfig, generate_candlestick_graph, generate_plot_filename, generate_profit_graph, init_plotscript, @@ -372,3 +373,26 @@ def test_plot_profit(default_conf, mocker, testdatadir, caplog): assert profit_mock.call_args_list[0][0][0] == default_conf['pairs'] assert store_mock.call_args_list[0][1]['auto_open'] is True + + +@pytest.mark.parametrize("ind1,ind2,plot_conf,exp", [ + ([], [], {}, + {'main_plot': {'sma': {}, 'ema3': {}, 'ema5': {}}, + 'subplots': {'Other': {'macd': {}, 'macdsignal': {}}}}), + (['sma', 'ema3'], ['macd'], {}, + {'main_plot': {'sma': {}, 'ema3': {}}, 'subplots': {'Other': {'macd': {}}}} + ), + ([], [], {'main_plot': {'sma': {}}}, + {'main_plot': {'sma': {}}, 'subplots': {}}), + ([], [], {'subplots': {'RSI': {'rsi': {'color': 'red'}}}}, + {'main_plot': {}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}}), +]) +def test_create_plotconfig(ind1, ind2, plot_conf, exp): + + res = create_plotconfig(ind1, ind2, plot_conf) + assert 'main_plot' in res + assert 'subplots' in res + assert isinstance(res['main_plot'], dict) + assert isinstance(res['subplots'], dict) + + assert res == exp From bdda62039752c967ae51e3717958bb282f825c18 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Jan 2020 12:54:58 +0100 Subject: [PATCH 222/324] add plot_config to startegy interface properly --- freqtrade/plot/plotting.py | 7 +++---- freqtrade/strategy/interface.py | 3 ++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 1c8ad552f..39629fe21 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -1,6 +1,6 @@ import logging from pathlib import Path -from typing import Any, Dict, List, Union +from typing import Any, Dict, List import pandas as pd from freqtrade.configuration import TimeRange @@ -13,8 +13,6 @@ from freqtrade.resolvers import StrategyResolver logger = logging.getLogger(__name__) -IndicatorType = Union[List[str], Dict[str, Dict]] - try: from plotly.subplots import make_subplots from plotly.offline import plot @@ -151,7 +149,8 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: return fig -def create_plotconfig(indicators1: List[str], indicators2: List[str], plot_config: Dict[str, Dict]) -> Dict[str, Dict]: +def create_plotconfig(indicators1: List[str], indicators2: List[str], + plot_config: Dict[str, Dict]) -> Dict[str, Dict]: """ Combines indicators 1 and indicators 2 into plot_config if necessary :param indicators1: List containing Main plot indicators diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index a2dee7837..7bd6a9ac5 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -112,7 +112,8 @@ class IStrategy(ABC): dp: Optional[DataProvider] = None wallets: Optional[Wallets] = None - plot_config: Dict + # Definition of plot_config. See plotting documentation for more details. + plot_config: Dict = {} def __init__(self, config: dict) -> None: self.config = config From 53499e01dec6099a63bf1543f9b202f75d5d5d20 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 4 Jan 2020 20:27:27 +0100 Subject: [PATCH 223/324] Clearly differentiate trade buys sells (positive and negative) * Swap trade buys to cyan circles * Show sell-reason description on buy too * Green positive sells - red negative / 0 sells --- freqtrade/plot/plotting.py | 37 +++++++++++++++++++++++++++---------- tests/test_plotting.py | 25 ++++++++++++++++++------- 2 files changed, 45 insertions(+), 17 deletions(-) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 39629fe21..1660371a1 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -113,11 +113,31 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: """ # Trades can be empty 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['profitperc'] * 100, 1)}%, " + f"{row['sell_reason']}, {row['duration']} min", + axis=1) trade_buys = go.Scatter( x=trades["open_time"], y=trades["open_rate"], mode='markers', - name='trade_buy', + name='Trade buy', + text=trades["desc"], + marker=dict( + symbol='circle-open', + size=11, + line=dict(width=2), + color='cyan' + + ) + ) + + trade_sells = go.Scatter( + x=trades.loc[trades['profitperc'] > 0, "close_time"], + y=trades.loc[trades['profitperc'] > 0, "close_rate"], + text=trades.loc[trades['profitperc'] > 0, "desc"], + mode='markers', + name='Sell - Profit', marker=dict( symbol='square-open', size=11, @@ -125,16 +145,12 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: color='green' ) ) - # Create description for sell summarizing the trade - desc = trades.apply(lambda row: f"{round(row['profitperc'] * 100, 1)}%, " - f"{row['sell_reason']}, {row['duration']} min", - axis=1) - trade_sells = go.Scatter( - x=trades["close_time"], - y=trades["close_rate"], - text=desc, + trade_sells_loss = go.Scatter( + x=trades.loc[trades['profitperc'] <= 0, "close_time"], + y=trades.loc[trades['profitperc'] <= 0, "close_rate"], + text=trades.loc[trades['profitperc'] <= 0, "desc"], mode='markers', - name='trade_sell', + name='Sell - Loss', marker=dict( symbol='square-open', size=11, @@ -144,6 +160,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: ) fig.add_trace(trade_buys, 1, 1) fig.add_trace(trade_sells, 1, 1) + fig.add_trace(trade_sells_loss, 1, 1) else: logger.warning("No trades found.") return fig diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 908c90a0a..e5162bb17 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -110,18 +110,29 @@ def test_plot_trades(testdatadir, caplog): figure = fig1.layout.figure # Check buys - color, should be in first graph, ... - trade_buy = find_trace_in_fig_data(figure.data, "trade_buy") + trade_buy = find_trace_in_fig_data(figure.data, 'Trade buy') assert isinstance(trade_buy, go.Scatter) assert trade_buy.yaxis == 'y' assert len(trades) == len(trade_buy.x) - assert trade_buy.marker.color == 'green' + assert trade_buy.marker.color == 'cyan' + assert trade_buy.marker.symbol == 'circle-open' + assert trade_buy.text[0] == '4.0%, roi, 15 min' - trade_sell = find_trace_in_fig_data(figure.data, "trade_sell") + trade_sell = find_trace_in_fig_data(figure.data, 'Sell - Profit') assert isinstance(trade_sell, go.Scatter) assert trade_sell.yaxis == 'y' - assert len(trades) == len(trade_sell.x) - assert trade_sell.marker.color == 'red' - assert trade_sell.text[0] == "4.0%, roi, 15 min" + assert len(trades.loc[trades['profitperc'] > 0]) == len(trade_sell.x) + assert trade_sell.marker.color == 'green' + assert trade_sell.marker.symbol == 'square-open' + assert trade_sell.text[0] == '4.0%, roi, 15 min' + + trade_sell_loss = find_trace_in_fig_data(figure.data, 'Sell - Loss') + assert isinstance(trade_sell_loss, go.Scatter) + assert trade_sell_loss.yaxis == 'y' + assert len(trades.loc[trades['profitperc'] <= 0]) == len(trade_sell_loss.x) + assert trade_sell_loss.marker.color == 'red' + assert trade_sell_loss.marker.symbol == 'square-open' + 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): @@ -310,7 +321,7 @@ def test_load_and_plot_trades(default_conf, mocker, caplog, testdatadir): "freqtrade.plot.plotting", generate_candlestick_graph=candle_mock, store_plot_file=store_mock - ) + ) load_and_plot_trades(default_conf) # Both mocks should be called once per pair From 7daa5bc33881c1e9919eae61510284739078ced5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 5 Jan 2020 12:50:44 +0100 Subject: [PATCH 224/324] Don't return None from unlimited_stake - 0 handles this just as well --- freqtrade/freqtradebot.py | 15 ++++++++------- tests/test_freqtradebot.py | 6 +++--- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index aa47adfd4..ed9cb983a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -250,12 +250,13 @@ class FreqtradeBot: return used_rate - def get_trade_stake_amount(self, pair) -> Optional[float]: + def get_trade_stake_amount(self, pair) -> float: """ Calculate stake amount for the trade :return: float: Stake amount + :raise: DependencyException if the available stake amount is too low """ - stake_amount: Optional[float] + stake_amount: float if self.edge: stake_amount = self.edge.stake_amount( pair, @@ -286,20 +287,20 @@ class FreqtradeBot: self.config['tradable_balance_ratio']) - val_tied_up return available_amount - def _calculate_unlimited_stake_amount(self) -> Optional[float]: + def _calculate_unlimited_stake_amount(self) -> float: """ Calculate stake amount for "unlimited" stake amount - :return: None if max number of trades reached + :return: 0 if max number of trades reached, else stake_amount to use. """ free_open_trades = self.get_free_open_trades() if not free_open_trades: - return None + return 0 available_amount = self._get_available_stake_amount() return available_amount / free_open_trades - def _check_available_stake_amount(self, stake_amount: Optional[float]) -> Optional[float]: + def _check_available_stake_amount(self, stake_amount: float) -> float: """ Check if stake amount can be fulfilled with the available balance for the stake currency @@ -307,7 +308,7 @@ class FreqtradeBot: """ available_amount = self._get_available_stake_amount() - if stake_amount is not None and available_amount < stake_amount: + if available_amount < stake_amount: raise DependencyException( f"Available balance ({available_amount} {self.config['stake_currency']}) is " f"lower than stake amount ({stake_amount} {self.config['stake_currency']})" diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c8b627ca4..dc74e507c 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -189,13 +189,13 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r freqtrade.execute_buy('LTC/BTC', result) result = freqtrade.get_trade_stake_amount('XRP/BTC') - assert result is None + assert result == 0 # set max_open_trades = None, so do not trade conf['max_open_trades'] = 0 freqtrade = FreqtradeBot(conf) result = freqtrade.get_trade_stake_amount('NEO/BTC') - assert result is None + assert result == 0 def test_edge_called_in_process(mocker, edge_conf) -> None: @@ -576,7 +576,7 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order, patch_get_signal(freqtrade) assert not freqtrade.create_trade('ETH/BTC') - assert freqtrade.get_trade_stake_amount('ETH/BTC') is None + assert freqtrade.get_trade_stake_amount('ETH/BTC') == 0 def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order, fee, From b37f34ff5bc5d2a3399af6c29814031f3bb5f6cf Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 5 Jan 2020 13:25:11 +0100 Subject: [PATCH 225/324] Implement amend_last_stake_amount --- config_full.json.example | 1 + docs/configuration.md | 17 +++++++++++++++++ freqtrade/constants.py | 2 ++ freqtrade/freqtradebot.py | 3 +++ 4 files changed, 23 insertions(+) diff --git a/config_full.json.example b/config_full.json.example index 981fc5209..f39abb00c 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -5,6 +5,7 @@ "tradable_balance_ratio": 0.99, "fiat_display_currency": "USD", "amount_reserve_percent" : 0.05, + "amend_last_stake_amount": false, "dry_run": false, "ticker_interval": "5m", "trailing_stop": false, diff --git a/docs/configuration.md b/docs/configuration.md index 3b8760366..9a05eea2d 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -44,6 +44,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *String* | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Positive float or `"unlimited"`.* | `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade).
*Defaults to `0.99` 99%).*
***Datatype:*** *Positive float between `0.1` and `1.0`.* +| `amend_last_stake_amount` | **Required.** Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade).
*Defaults to `false`.*
***Datatype:*** *Boolean* | `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 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* @@ -146,6 +147,22 @@ For example, if you have 10 ETH available in your wallet on the exchange and `tr !!! Warning `tradable_balance_ratio` applies to the current balance (free balance + tied up in trades). Therefore, assuming a starting balance of 1000, a configuration of `tradable_balance_ratio=0.99` will not guarantee that 10 units will always remain available on the exchange. The free amount may reduce to 5 units if the total balance is reduce to 500 (either by a losing streak, or by withdrawing balance). +#### Amend last stake amount + +Assuming we have a `balance=1000` (USDT), `stake_amount=400`, and `max_open_trades=3`. +The bot would open 2 trades, and will be unable to fill the last trading slot, since the requested 400 USDT are no longer available, since 800 USDT are already tied in other trades. + +To overcome this, the option `amend_last_stake_amount` can be set to `True`, which will enable the bot to reduce stake_amount to the available balance in order to fill the last trade slot. + +In the example above this would mean: + +- Trade1: 400 USDT +- Trade2: 400 USDT +- Trade3: 200 USDT + +!!! Note + This option only applies with [Static stake amount](#static-stake-amount) - since [Dynamic stake amount](#dynamic-stake-amount) divides the balances evenly. + #### Static stake amount The `stake_amount` configuration statically configures the amount of stake-currency your bot will use for each trade. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index ca4f3cf36..a1bd70183 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -79,6 +79,7 @@ CONF_SCHEMA = { 'maximum': 1, 'default': 0.99 }, + 'amend_last_stake_amount': {'type': 'boolean', 'default': False}, 'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT}, 'dry_run': {'type': 'boolean'}, 'dry_run_wallet': {'type': 'number', 'default': DRY_RUN_WALLET}, @@ -282,6 +283,7 @@ SCHEMA_TRADE_REQUIRED = [ 'max_open_trades', 'stake_currency', 'stake_amount', + 'tradable_balance_ratio', 'dry_run', 'dry_run_wallet', 'bid_strategy', diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6c02844f1..5aacdc587 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -308,6 +308,9 @@ class FreqtradeBot: """ available_amount = self._get_available_stake_amount() + if self.config['amend_last_stake_amount']: + stake_amount = min(stake_amount, available_amount) + if available_amount < stake_amount: raise DependencyException( f"Available balance ({available_amount} {self.config['stake_currency']}) is " From ca054799d065af3bf745867bcb20c06fde06aa5a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 5 Jan 2020 13:25:21 +0100 Subject: [PATCH 226/324] Add tests for amend_last_stake_amount --- tests/test_freqtradebot.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index dc74e507c..6d794fc05 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -140,11 +140,45 @@ def test_get_trade_stake_amount(default_conf, ticker, mocker) -> None: assert result == default_conf['stake_amount'] +@pytest.mark.parametrize("amend_last,wallet,max_open,expected", [ + (False, 0.002, 2, [0.001, None]), + (True, 0.002, 2, [0.001, 0.00098]), + (False, 0.003, 3, [0.001, 0.001, None]), + (True, 0.003, 3, [0.001, 0.001, 0.00097]), + ]) +def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_buy_order, + amend_last, wallet, max_open, expected) -> None: + patch_RPCManager(mocker) + patch_exchange(mocker) + mocker.patch.multiple( + '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']}), + get_fee=fee + ) + default_conf['dry_run_wallet'] = wallet + + default_conf['amend_last_stake_amount'] = amend_last + freqtrade = FreqtradeBot(default_conf) + + for i in range(0, max_open): + + if expected[i] is not None: + result = freqtrade.get_trade_stake_amount('ETH/BTC') + assert pytest.approx(result) == expected[i] + freqtrade.execute_buy('ETH/BTC', result) + else: + with pytest.raises(DependencyException): + freqtrade.get_trade_stake_amount('ETH/BTC') + + def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5) freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) with pytest.raises(DependencyException, match=r'.*stake amount.*'): freqtrade.get_trade_stake_amount('ETH/BTC') From d0ccfa1925006bf8be1365b69cb5a5efb0ff79f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 5 Jan 2020 19:50:21 +0100 Subject: [PATCH 227/324] Explicitly given indicators should override plot_config --- freqtrade/plot/plotting.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/freqtrade/plot/plotting.py b/freqtrade/plot/plotting.py index 1660371a1..dc4766a84 100644 --- a/freqtrade/plot/plotting.py +++ b/freqtrade/plot/plotting.py @@ -176,6 +176,12 @@ def create_plotconfig(indicators1: List[str], indicators2: List[str], :return: plot_config - eventually with indicators 1 and 2 """ + if plot_config: + if indicators1: + plot_config['main_plot'] = {ind: {} for ind in indicators1} + if indicators2: + plot_config['subplots'] = {'Other': {ind: {} for ind in indicators2}} + if not plot_config: # If no indicators and no plot-config given, use defaults. if not indicators1: From 888ea58df246186c68330de93b66d7c442e2bcee Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 5 Jan 2020 19:50:38 +0100 Subject: [PATCH 228/324] Add tests for new behaviour --- tests/test_plotting.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index e5162bb17..78c01eb97 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -387,16 +387,37 @@ def test_plot_profit(default_conf, mocker, testdatadir, caplog): @pytest.mark.parametrize("ind1,ind2,plot_conf,exp", [ + # No indicators, use plot_conf ([], [], {}, {'main_plot': {'sma': {}, 'ema3': {}, 'ema5': {}}, 'subplots': {'Other': {'macd': {}, 'macdsignal': {}}}}), + # use indicators (['sma', 'ema3'], ['macd'], {}, - {'main_plot': {'sma': {}, 'ema3': {}}, 'subplots': {'Other': {'macd': {}}}} - ), + {'main_plot': {'sma': {}, 'ema3': {}}, 'subplots': {'Other': {'macd': {}}}}), + # only main_plot - adds empty subplots ([], [], {'main_plot': {'sma': {}}}, {'main_plot': {'sma': {}}, 'subplots': {}}), + # Main and subplots + ([], [], {'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}}, + {'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}}), + # no main_plot, adds empty main_plot ([], [], {'subplots': {'RSI': {'rsi': {'color': 'red'}}}}, {'main_plot': {}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}}), + # indicator 1 / 2 should have prevelance + (['sma', 'ema3'], ['macd'], + {'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}}, + {'main_plot': {'sma': {}, 'ema3': {}}, 'subplots': {'Other': {'macd': {}}}} + ), + # indicator 1 - overrides plot_config main_plot + (['sma', 'ema3'], [], + {'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}}, + {'main_plot': {'sma': {}, 'ema3': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}} + ), + # indicator 2 - overrides plot_config subplots + ([], ['macd', 'macd_signal'], + {'main_plot': {'sma': {}}, 'subplots': {'RSI': {'rsi': {'color': 'red'}}}}, + {'main_plot': {'sma': {}}, 'subplots': {'Other': {'macd': {}, 'macd_signal': {}}}} + ), ]) def test_create_plotconfig(ind1, ind2, plot_conf, exp): From b614964ba96fdee8b713d4e8ea7c1f6441d23cf8 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2020 07:41:39 +0000 Subject: [PATCH 229/324] Bump pytest-mock from 1.13.0 to 2.0.0 Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 1.13.0 to 2.0.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/v1.13.0...v2.0.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 1357bba00..64402bb60 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,7 +11,7 @@ mypy==0.761 pytest==5.3.2 pytest-asyncio==0.10.0 pytest-cov==2.8.1 -pytest-mock==1.13.0 +pytest-mock==2.0.0 pytest-random-order==1.0.4 # Convert jupyter notebooks to markdown documents From aabeece4c096296bd6cfaf1efefdaced24620743 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2020 07:42:19 +0000 Subject: [PATCH 230/324] Bump flake8-tidy-imports from 3.1.0 to 4.0.0 Bumps [flake8-tidy-imports](https://github.com/adamchainz/flake8-tidy-imports) from 3.1.0 to 4.0.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/3.1.0...4.0.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 1357bba00..13bb62e48 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,7 +6,7 @@ coveralls==1.9.2 flake8==3.7.9 flake8-type-annotations==0.1.0 -flake8-tidy-imports==3.1.0 +flake8-tidy-imports==4.0.0 mypy==0.761 pytest==5.3.2 pytest-asyncio==0.10.0 From d846114d3cdbfa845069aa42f995905a74d86f03 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2020 07:42:44 +0000 Subject: [PATCH 231/324] Bump scikit-learn from 0.22 to 0.22.1 Bumps [scikit-learn](https://github.com/scikit-learn/scikit-learn) from 0.22 to 0.22.1. - [Release notes](https://github.com/scikit-learn/scikit-learn/releases) - [Commits](https://github.com/scikit-learn/scikit-learn/compare/0.22...0.22.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 9b408dbed..43cad1a0e 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -3,7 +3,7 @@ # Required for hyperopt scipy==1.4.1 -scikit-learn==0.22 +scikit-learn==0.22.1 scikit-optimize==0.5.2 filelock==3.0.12 joblib==0.14.1 From 3c0d184097af10bad14b130af968a327daa92666 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2020 07:43:06 +0000 Subject: [PATCH 232/324] Bump ccxt from 1.21.23 to 1.21.32 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.21.23 to 1.21.32. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/1.21.23...1.21.32) 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 add1ea0fd..97161df8c 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.21.23 +ccxt==1.21.32 SQLAlchemy==1.3.12 python-telegram-bot==12.2.0 arrow==0.15.4 From 6da97fafa8db18e8a58c01db894b67cf9a6e94cc Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2020 11:45:12 +0000 Subject: [PATCH 233/324] Bump coveralls from 1.9.2 to 1.10.0 Bumps [coveralls](https://github.com/coveralls-clients/coveralls-python) from 1.9.2 to 1.10.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/1.9.2...1.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 13bb62e48..5cc2e39f2 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -3,7 +3,7 @@ -r requirements-plot.txt -r requirements-hyperopt.txt -coveralls==1.9.2 +coveralls==1.10.0 flake8==3.7.9 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.0.0 From 6ac7dcf5e9f52735e20f9ae2b9c82b23523efbdf Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2020 11:45:22 +0000 Subject: [PATCH 234/324] Bump arrow from 0.15.4 to 0.15.5 Bumps [arrow](https://github.com/crsmithdev/arrow) from 0.15.4 to 0.15.5. - [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.4...0.15.5) 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 97161df8c..c10536603 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -3,7 +3,7 @@ ccxt==1.21.32 SQLAlchemy==1.3.12 python-telegram-bot==12.2.0 -arrow==0.15.4 +arrow==0.15.5 cachetools==4.0.0 requests==2.22.0 urllib3==1.25.7 From 2b3f2e5fa817088c04267c5a488aa371812be1c4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 6 Jan 2020 12:55:12 +0100 Subject: [PATCH 235/324] Add first version of documentation --- docs/plotting.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/docs/plotting.md b/docs/plotting.md index ba737562f..5b5199821 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -120,6 +120,62 @@ To plot trades from a backtesting result, use `--export-filename ` freqtrade plot-dataframe --strategy AwesomeStrategy --export-filename user_data/backtest_results/backtest-result.json -p BTC/ETH ``` +### Plot dataframe basics + +![plot-dataframe](assets/plot-dataframe.png) + +plot-dataframe will require backtesting data, a strategy as well as either a backtesting-results file or a Database file, containing trades corresponding to the strategy. + +The resulting plot will have the following elements: + +* Green triangles: Buy signals from the strategy. (Note: not every Buy-signal also generates a trade) +* Red triangles: Sell signals from the strategy. +* Cyan Circles: Trade entry +* Red Square: Trade exit for loss or 0% profit +* Green Square: Trade exit for profit +* Indicators corresponding to the candle scale (e.g. SMA/EMA), as specified with `--indicators1`. +* Indicators with different scales (e.g. MACD, RSI) below the volume bars, as specified via `--indicators2`. +* Volume (bar chart at the bottom of the main chart) + +#### Advanced Plot configuration + +An advanced plot-configuration can be specified in the strategy. + +This configuration allows to specify fixed colors (otherwise consecutive plots may produce different colorschemes each time, making comparisons diffiult.). +It also allows multiple subplots to display both MACD and RSI at the same time. + +Additional features when using plot_config: + +* Specify colors per indicator +* Specify additional subplots + +Sample configuration with inline comments explaining the process: + +``` python + plot_config = { + 'main_plot': { + # Configuration for main plot indicators. + # Specifies `ema10` to be red, and `ema50` to be a shade of gray + 'ema10': {'color': 'red'}, + 'ema50': {'color': '#CCCCCC'}, + }, + 'subplots': { + # Create subplot MACD + "MACD": { + 'macd': {'color': 'blue'}, + 'macdsignal': {'color': 'orange'}, + }, + # Additional subplot RSI + "RSI": { + 'rsi': {'color': 'red'}, + } + } + } +``` + +!!! Note + The above configuration assumes that `ema10`, `ema50`, `macd`, `macdsignal` and `rsi` are columns in the DataFrame created by the strategy. + ## Plot profit ![plot-profit](assets/plot-profit.png) From 3883d18b8acd80fd541ffb2a69b9a1136d7341ec Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 6 Jan 2020 12:59:17 +0100 Subject: [PATCH 236/324] Add bollinger note --- docs/plotting.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/plotting.md b/docs/plotting.md index 5b5199821..dade97968 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -137,6 +137,9 @@ The resulting plot will have the following elements: * Indicators with different scales (e.g. MACD, RSI) below the volume bars, as specified via `--indicators2`. * Volume (bar chart at the bottom of the main chart) +!!! Note "Bollinger Bands" + Bollinger bands are automatically added to the plot if the columns `bb_lowerband` and `bb_upperband` exist, and are painted as a light blue Area spanning from the lower band to the upper band. + #### Advanced Plot configuration An advanced plot-configuration can be specified in the strategy. From c3fd894a6c25c709f284b8d529408408256fb505 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 7 Jan 2020 07:16:31 +0100 Subject: [PATCH 237/324] Regenerate plots with new settings --- docs/assets/plot-dataframe.png | Bin 177349 -> 216176 bytes docs/assets/plot-dataframe2.png | Bin 0 -> 195009 bytes docs/plotting.md | 2 +- freqtrade/plot/plotting.py | 4 ++-- 4 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 docs/assets/plot-dataframe2.png diff --git a/docs/assets/plot-dataframe.png b/docs/assets/plot-dataframe.png index eb90a173469bafbd11afd28c31c8e3eccd9b9e5b..6310b23b4b8198734173b011bce7c7d079ac3fec 100644 GIT binary patch literal 216176 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 z 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

jj9>k~K~SvRZ|By!WYsQG;i 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

3Mw#Z*c8)g^BQ;Bau8lf+*Q2n115-$Kcd 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 H*Ynn2bl`;q literal 177349 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

y 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

CLz8o1U8Dr=!D`r6ufzSh$S!FWYCieMFEQ`j3 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

7~F&+3=jsBOlDVmviow#^mN$wy&R@DI=8t$%( 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%0DF*ACRH?C4qbs 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

tCyNkFHRuu3cx_|_Dl<&EA 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%0Ahi< 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 Z{Fj}KN`_uxy Date: Wed, 8 Jan 2020 19:35:00 +0100 Subject: [PATCH 238/324] Add plot_config to optional plot --- docs/plotting.md | 2 ++ freqtrade/templates/base_strategy.py.j2 | 2 +- freqtrade/templates/sample_strategy.py | 16 ++++++++++++++++ .../templates/subtemplates/plot_config_full.j2 | 18 ++++++++++++++++++ .../subtemplates/plot_config_minimal.j2 | 0 freqtrade/utils.py | 2 ++ 6 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 freqtrade/templates/subtemplates/plot_config_full.j2 create mode 100644 freqtrade/templates/subtemplates/plot_config_minimal.j2 diff --git a/docs/plotting.md b/docs/plotting.md index a3d886473..2f084b66a 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -161,6 +161,8 @@ Sample configuration with inline comments explaining the process: # Specifies `ema10` to be red, and `ema50` to be a shade of gray 'ema10': {'color': 'red'}, 'ema50': {'color': '#CCCCCC'}, + # By omitting color, a random color is selected. + 'sar': {}, }, 'subplots': { # Create subplot MACD diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2 index 32573ec9e..fbf083387 100644 --- a/freqtrade/templates/base_strategy.py.j2 +++ b/freqtrade/templates/base_strategy.py.j2 @@ -78,7 +78,7 @@ class {{ strategy }}(IStrategy): 'buy': 'gtc', 'sell': 'gtc' } - + {{ plot_config | indent(4) }} def informative_pairs(self): """ Define additional, informative pair/interval combinations to be cached from the exchange. diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 228e56b82..92f6aefba 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -80,6 +80,22 @@ class SampleStrategy(IStrategy): 'sell': 'gtc' } + plot_config = { + 'main_plot': { + 'tema': {}, + 'sar': {'color': 'white'}, + }, + 'subplots': { + "MACD": { + 'macd': {'color': 'blue'}, + 'macdsignal': {'color': 'orange'}, + }, + "RSI": { + 'rsi': {'color': 'red'}, + } + } + } + def informative_pairs(self): """ Define additional, informative pair/interval combinations to be cached from the exchange. diff --git a/freqtrade/templates/subtemplates/plot_config_full.j2 b/freqtrade/templates/subtemplates/plot_config_full.j2 new file mode 100644 index 000000000..ab02c7892 --- /dev/null +++ b/freqtrade/templates/subtemplates/plot_config_full.j2 @@ -0,0 +1,18 @@ + +plot_config = { + # Main plot indicators (Moving averages, ...) + 'main_plot': { + 'tema': {}, + 'sar': {'color': 'white'}, + }, + 'subplots': { + # Subplots - each dict defines one additional plot + "MACD": { + 'macd': {'color': 'blue'}, + 'macdsignal': {'color': 'orange'}, + }, + "RSI": { + 'rsi': {'color': 'red'}, + } + } +} diff --git a/freqtrade/templates/subtemplates/plot_config_minimal.j2 b/freqtrade/templates/subtemplates/plot_config_minimal.j2 new file mode 100644 index 000000000..e69de29bb diff --git a/freqtrade/utils.py b/freqtrade/utils.py index 9fe15aea6..2f7b2d717 100644 --- a/freqtrade/utils.py +++ b/freqtrade/utils.py @@ -102,12 +102,14 @@ def deploy_new_strategy(strategy_name, strategy_path: Path, subtemplate: str): indicators = render_template(templatefile=f"subtemplates/indicators_{subtemplate}.j2",) buy_trend = render_template(templatefile=f"subtemplates/buy_trend_{subtemplate}.j2",) sell_trend = render_template(templatefile=f"subtemplates/sell_trend_{subtemplate}.j2",) + plot_config = render_template(templatefile=f"subtemplates/plot_config_{subtemplate}.j2",) strategy_text = render_template(templatefile='base_strategy.py.j2', arguments={"strategy": strategy_name, "indicators": indicators, "buy_trend": buy_trend, "sell_trend": sell_trend, + "plot_config": plot_config, }) logger.info(f"Writing strategy to `{strategy_path}`.") From db34cb1b75bb64f61bba371e43661dd8f67937b8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 8 Jan 2020 19:41:34 +0100 Subject: [PATCH 239/324] Do some adjustments to the wording of the index.md section --- docs/index.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/index.md b/docs/index.md index 5871479d9..3bde10707 100644 --- a/docs/index.md +++ b/docs/index.md @@ -22,13 +22,13 @@ Freqtrade is a crypto-currency algorithmic trading software develop in python (3 ## Features - 1. Download markets datas : download historical datas of the exchange and the markets your may want to trade with. - 2. Select markets : create your list or use an automatic one based on top traded volume (not available during backtesting). You can blacklist markets you don't want to trade. - 3. Backtest : Test your strategy on past datas (based on [ohcl](https://en.wikipedia.org/wiki/Open-high-low-close_chart) candles). - 4. Optimize : Find the best parameters for your strategy (use machining learning). You can optimize buy, sell, take profit and stop-loss. - 5. Run : Run the bot on exchange without playing money (dry-run) or with money (live). - 6. Run using edge (optionnal module) : the concept is to find the best historical [trade expectancy](https://www.freqtrade.io/en/latest/edge/#expectancy) by markets based on variation of the stop-loss and then allow/reject markets to trade (the sizing of the trade is based on a risk of a percentage of your capital) - 7. Control/Monitor/Analyse : use Telegram or a REST API (start/stop the bot, profit/loss, daily summary, current open trades results...). Futher analysis can be done as trades are saved (SQLite database) + 1. Download market data: Download historical data of the exchange and the markets your may want to trade with. + 2. Select markets: Create your list or use an automatic one based on top traded volume (not available during backtesting). You can blacklist markets you don't want to trade. + 3. Backtest: Test your strategy on past data (based on [ohlcv](https://en.wikipedia.org/wiki/Open-high-low-close_chart) candles). + 4. Optimize: Find the best parameters for your strategy using machining learning. You can optimize buy, sell, take profit (ROI) and stop-loss. + 5. Run: Run the bot on exchange with simulated money (dry-run) or with real money (live). + 6. Run using edge (optional module): The concept is to find the best historical [trade expectancy](https://www.freqtrade.io/en/latest/edge/#expectancy) by markets based on variation of the stop-loss and then allow/reject markets to trade (the sizing of the trade is based on a risk of a percentage of your capital). + 7. Control/Monitor/Analyse: Use Telegram or a REST API (start/stop the bot, profit/loss, daily summary, current open trades results...). Futher analysis can be done as trades are saved (SQLite database). ## Requirements From 9559cb988ef49b68374373f6da8c9d1c88cd7d0e Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Thu, 9 Jan 2020 04:12:43 +0300 Subject: [PATCH 240/324] reworked --- docs/index.md | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/docs/index.md b/docs/index.md index 3bde10707..3293999b7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -11,8 +11,11 @@ Download Follow @freqtrade + ## Introduction -Freqtrade is a crypto-currency algorithmic trading software develop in python (3.6+) supported on windows, macOs and Linux. + +Freqtrade is a crypto-currency algorithmic trading software developed in python (3.6+) 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. @@ -22,13 +25,13 @@ Freqtrade is a crypto-currency algorithmic trading software develop in python (3 ## Features - 1. Download market data: Download historical data of the exchange and the markets your may want to trade with. - 2. Select markets: Create your list or use an automatic one based on top traded volume (not available during backtesting). You can blacklist markets you don't want to trade. - 3. Backtest: Test your strategy on past data (based on [ohlcv](https://en.wikipedia.org/wiki/Open-high-low-close_chart) candles). - 4. Optimize: Find the best parameters for your strategy using machining learning. You can optimize buy, sell, take profit (ROI) and stop-loss. - 5. Run: Run the bot on exchange with simulated money (dry-run) or with real money (live). - 6. Run using edge (optional module): The concept is to find the best historical [trade expectancy](https://www.freqtrade.io/en/latest/edge/#expectancy) by markets based on variation of the stop-loss and then allow/reject markets to trade (the sizing of the trade is based on a risk of a percentage of your capital). - 7. Control/Monitor/Analyse: Use Telegram or a REST API (start/stop the bot, profit/loss, daily summary, current open trades results...). Futher analysis can be done as trades are saved (SQLite database). +- Download market data: Download historical data of the exchange and the markets your may want to trade with. The historical data can be based on [OHLCV](https://en.wikipedia.org/wiki/Open-high-low-close_chart) candles or be trade ticks (for exchanges that support this). +- Select markets: Create your static list or use an automatic one based on top traded volumes and/or prices (not available during backtesting). You can also explicitly blacklist markets you don't want to trade. +- Backtest: Test your strategy on downloaded historical data. +- Optimize: Find the best parameters for your strategy using hyperoptimization which employs machining learning methods. You can optimize buy, sell, take profit (ROI), stop-loss and trailing stop-loss parameters for your strategy. +- Run: Run the bot on exchange with simulated money (Dry-Run mode) or with real money (Live-Trade mode). +- Run using Edge (optional module): The concept is to find the best historical [trade expectancy](https://www.freqtrade.io/en/latest/edge/#expectancy) by markets based on variation of the stop-loss and then allow/reject markets to trade. The sizing of the trade is based on a risk of a percentage of your capital. +- Control/Monitor/Analyse: Use Telegram or a REST API (start/stop the bot, show profit/loss, daily summary, current open trades results, etc.) Further analysis can be done as trades are saved (SQLite persistence database). ## Requirements @@ -55,10 +58,10 @@ To run this bot we recommend you a cloud instance with a minimum of: ## Support -Help / Slack -For any questions not covered by the documentation or for further information about the bot, we encourage you to join our Slack channel. +### Help / 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. -Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) to join Slack channel. +Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) to join the Freqtrade Slack channel. ## Ready to try? From cee8f3349e043b721d2c2b8ae6a6536544609d57 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Thu, 9 Jan 2020 04:16:57 +0300 Subject: [PATCH 241/324] rearrange features -- move Run to the top --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 3293999b7..8c1a8fcc6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -25,11 +25,11 @@ Freqtrade is a crypto-currency algorithmic trading software developed in python ## Features -- Download market data: Download historical data of the exchange and the markets your may want to trade with. The historical data can be based on [OHLCV](https://en.wikipedia.org/wiki/Open-high-low-close_chart) candles or be trade ticks (for exchanges that support this). +- Run: Run the bot on exchange with simulated money (Dry-Run mode) or with real money (Live-Trade mode). - Select markets: Create your static list or use an automatic one based on top traded volumes and/or prices (not available during backtesting). You can also explicitly blacklist markets you don't want to trade. +- Download market data: Download historical data of the exchange and the markets your may want to trade with. The historical data can be based on [OHLCV](https://en.wikipedia.org/wiki/Open-high-low-close_chart) candles or be trade ticks (for exchanges that support this). - Backtest: Test your strategy on downloaded historical data. - Optimize: Find the best parameters for your strategy using hyperoptimization which employs machining learning methods. You can optimize buy, sell, take profit (ROI), stop-loss and trailing stop-loss parameters for your strategy. -- Run: Run the bot on exchange with simulated money (Dry-Run mode) or with real money (Live-Trade mode). - Run using Edge (optional module): The concept is to find the best historical [trade expectancy](https://www.freqtrade.io/en/latest/edge/#expectancy) by markets based on variation of the stop-loss and then allow/reject markets to trade. The sizing of the trade is based on a risk of a percentage of your capital. - Control/Monitor/Analyse: Use Telegram or a REST API (start/stop the bot, show profit/loss, daily summary, current open trades results, etc.) Further analysis can be done as trades are saved (SQLite persistence database). From 135487b2c98b541bb1ba45c96ddabe975fb77a52 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 9 Jan 2020 06:35:05 +0100 Subject: [PATCH 242/324] SPlit control and Analyse feature into 2 seperate points --- docs/index.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 8c1a8fcc6..1c292b21d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,8 +30,9 @@ Freqtrade is a crypto-currency algorithmic trading software developed in python - Download market data: Download historical data of the exchange and the markets your may want to trade with. The historical data can be based on [OHLCV](https://en.wikipedia.org/wiki/Open-high-low-close_chart) candles or be trade ticks (for exchanges that support this). - Backtest: Test your strategy on downloaded historical data. - Optimize: Find the best parameters for your strategy using hyperoptimization which employs machining learning methods. You can optimize buy, sell, take profit (ROI), stop-loss and trailing stop-loss parameters for your strategy. -- Run using Edge (optional module): The concept is to find the best historical [trade expectancy](https://www.freqtrade.io/en/latest/edge/#expectancy) by markets based on variation of the stop-loss and then allow/reject markets to trade. The sizing of the trade is based on a risk of a percentage of your capital. -- Control/Monitor/Analyse: Use Telegram or a REST API (start/stop the bot, show profit/loss, daily summary, current open trades results, etc.) Further analysis can be done as trades are saved (SQLite persistence database). +- Run using Edge (optional module): The concept is to find the best historical [trade expectancy](edge.md#expectancy) by markets based on variation of the stop-loss and then allow/reject markets to trade. The sizing of the trade is based on a risk of a percentage of your capital. +- Control/Monitor: Use Telegram or a REST API (start/stop the bot, show profit/loss, daily summary, current open trades results, etc.). +- Analyse: Further analysis can be possibilities on either Backtesting data or Freqtrade trading history (SQL database), including automated standard plots, and methods to load the data into [interactive environments](data-analysis.md). ## Requirements From 7461b5dc02b14b5ce731188e8d0f3215d3da0fd3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 9 Jan 2020 06:37:18 +0100 Subject: [PATCH 243/324] Mention custom strategy in features --- docs/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/index.md b/docs/index.md index 1c292b21d..b664552bc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -28,6 +28,7 @@ Freqtrade is a crypto-currency algorithmic trading software developed in python - Run: Run the bot on exchange with simulated money (Dry-Run mode) or with real money (Live-Trade mode). - Select markets: Create your static list or use an automatic one based on top traded volumes and/or prices (not available during backtesting). You can also explicitly blacklist markets you don't want to trade. - Download market data: Download historical data of the exchange and the markets your may want to trade with. The historical data can be based on [OHLCV](https://en.wikipedia.org/wiki/Open-high-low-close_chart) candles or be trade ticks (for exchanges that support this). +- Strategy: Write your strategy in python, using [pandas](https://pandas.pydata.org/). Example strategies to inspire you are available in the [strategy repository](https://github.com/freqtrade/freqtrade-strategies). - Backtest: Test your strategy on downloaded historical data. - Optimize: Find the best parameters for your strategy using hyperoptimization which employs machining learning methods. You can optimize buy, sell, take profit (ROI), stop-loss and trailing stop-loss parameters for your strategy. - Run using Edge (optional module): The concept is to find the best historical [trade expectancy](edge.md#expectancy) by markets based on variation of the stop-loss and then allow/reject markets to trade. The sizing of the trade is based on a risk of a percentage of your capital. From 989ab646a9ec1367b3eef17d8740c6bc7c84a4f2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 9 Jan 2020 06:46:39 +0100 Subject: [PATCH 244/324] Add profit % to sell_reason table --- freqtrade/optimize/backtest_reports.py | 10 ++++++---- tests/optimize/test_backtest_reports.py | 12 ++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/freqtrade/optimize/backtest_reports.py b/freqtrade/optimize/backtest_reports.py index 5778747cf..555250760 100644 --- a/freqtrade/optimize/backtest_reports.py +++ b/freqtrade/optimize/backtest_reports.py @@ -66,11 +66,13 @@ def generate_text_table_sell_reason(data: Dict[str, Dict], results: DataFrame) - :return: pretty printed table with tabulate as string """ tabular_data = [] - headers = ['Sell Reason', 'Count', 'Profit', 'Loss'] + headers = ['Sell Reason', 'Count', 'Profit', 'Loss', 'Profit %'] for reason, count in results['sell_reason'].value_counts().iteritems(): - profit = len(results[(results['sell_reason'] == reason) & (results['profit_abs'] >= 0)]) - loss = len(results[(results['sell_reason'] == reason) & (results['profit_abs'] < 0)]) - tabular_data.append([reason.value, count, profit, loss]) + result = results.loc[results['sell_reason'] == reason] + profit = len(result[result['profit_abs'] >= 0]) + loss = len(result[results['profit_abs'] < 0]) + profit_mean = round(result['profit_percent'].mean() * 100.0, 2) + tabular_data.append([reason.value, count, profit, loss, profit_mean]) return tabulate(tabular_data, headers=headers, tablefmt="pipe") diff --git a/tests/optimize/test_backtest_reports.py b/tests/optimize/test_backtest_reports.py index 726202517..107389a42 100644 --- a/tests/optimize/test_backtest_reports.py +++ b/tests/optimize/test_backtest_reports.py @@ -39,8 +39,8 @@ def test_generate_text_table_sell_reason(default_conf, mocker): results = pd.DataFrame( { 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], - 'profit_percent': [0.1, 0.2, -0.3], - 'profit_abs': [0.2, 0.4, -0.5], + 'profit_percent': [0.1, 0.2, -0.1], + 'profit_abs': [0.2, 0.4, -0.2], 'trade_duration': [10, 30, 10], 'profit': [2, 0, 0], 'loss': [0, 0, 1], @@ -49,10 +49,10 @@ def test_generate_text_table_sell_reason(default_conf, mocker): ) result_str = ( - '| Sell Reason | Count | Profit | Loss |\n' - '|:--------------|--------:|---------:|-------:|\n' - '| roi | 2 | 2 | 0 |\n' - '| stop_loss | 1 | 0 | 1 |' + '| Sell Reason | Count | Profit | Loss | Profit % |\n' + '|:--------------|--------:|---------:|-------:|-----------:|\n' + '| roi | 2 | 2 | 0 | 15 |\n' + '| stop_loss | 1 | 0 | 1 | -10 |' ) assert generate_text_table_sell_reason( data={'ETH/BTC': {}}, results=results) == result_str From c475729c13c22b404c469dec6dbdb9256a312fb8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 9 Jan 2020 06:52:34 +0100 Subject: [PATCH 245/324] Extract edge reporting to optimize_reports --- freqtrade/optimize/backtesting.py | 2 +- freqtrade/optimize/edge_cli.py | 32 ++----------------- ...acktest_reports.py => optimize_reports.py} | 26 +++++++++++++++ tests/optimize/test_backtest_reports.py | 16 ++++++++-- tests/optimize/test_edge_cli.py | 14 -------- 5 files changed, 44 insertions(+), 46 deletions(-) rename freqtrade/optimize/{backtest_reports.py => optimize_reports.py} (83%) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ae3fbed0a..18cc27ff4 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -18,7 +18,7 @@ from freqtrade.data.dataprovider import DataProvider from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.misc import file_dump_json -from freqtrade.optimize.backtest_reports import ( +from freqtrade.optimize.optimize_reports import ( generate_text_table, generate_text_table_sell_reason, generate_text_table_strategy) from freqtrade.persistence import Trade diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 4944f1dbb..be19688d8 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -6,13 +6,12 @@ This module contains the edge backtesting interface import logging from typing import Any, Dict -from tabulate import tabulate - from freqtrade import constants from freqtrade.configuration import (TimeRange, remove_credentials, validate_config_consistency) from freqtrade.edge import Edge -from freqtrade.resolvers import StrategyResolver, ExchangeResolver +from freqtrade.optimize.optimize_reports import generate_edge_table +from freqtrade.resolvers import ExchangeResolver, StrategyResolver logger = logging.getLogger(__name__) @@ -44,33 +43,8 @@ class EdgeCli: self.edge._timerange = TimeRange.parse_timerange(None if self.config.get( 'timerange') is None else str(self.config.get('timerange'))) - def _generate_edge_table(self, results: dict) -> str: - - floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', '.d') - tabular_data = [] - headers = ['pair', 'stoploss', 'win rate', 'risk reward ratio', - 'required risk reward', 'expectancy', 'total number of trades', - 'average duration (min)'] - - for result in results.items(): - if result[1].nb_trades > 0: - tabular_data.append([ - result[0], - result[1].stoploss, - result[1].winrate, - result[1].risk_reward_ratio, - result[1].required_risk_reward, - result[1].expectancy, - result[1].nb_trades, - round(result[1].avg_trade_duration) - ]) - - # Ignore type as floatfmt does allow tuples but mypy does not know that - return tabulate(tabular_data, headers=headers, - floatfmt=floatfmt, tablefmt="pipe") # type: ignore - def start(self) -> None: result = self.edge.calculate() if result: print('') # blank line for readability - print(self._generate_edge_table(self.edge._cached_pairs)) + print(generate_edge_table(self.edge._cached_pairs)) diff --git a/freqtrade/optimize/backtest_reports.py b/freqtrade/optimize/optimize_reports.py similarity index 83% rename from freqtrade/optimize/backtest_reports.py rename to freqtrade/optimize/optimize_reports.py index 555250760..ffc8c53d4 100644 --- a/freqtrade/optimize/backtest_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -107,3 +107,29 @@ def generate_text_table_strategy(stake_currency: str, max_open_trades: str, # Ignore type as floatfmt does allow tuples but mypy does not know that return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe") # type: ignore + + +def generate_edge_table(results: dict) -> str: + + floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', '.d') + tabular_data = [] + headers = ['pair', 'stoploss', 'win rate', 'risk reward ratio', + 'required risk reward', 'expectancy', 'total number of trades', + 'average duration (min)'] + + for result in results.items(): + if result[1].nb_trades > 0: + tabular_data.append([ + result[0], + result[1].stoploss, + result[1].winrate, + result[1].risk_reward_ratio, + result[1].required_risk_reward, + result[1].expectancy, + result[1].nb_trades, + round(result[1].avg_trade_duration) + ]) + + # Ignore type as floatfmt does allow tuples but mypy does not know that + return tabulate(tabular_data, headers=headers, + floatfmt=floatfmt, tablefmt="pipe") # type: ignore diff --git a/tests/optimize/test_backtest_reports.py b/tests/optimize/test_backtest_reports.py index 107389a42..518b50d0f 100644 --- a/tests/optimize/test_backtest_reports.py +++ b/tests/optimize/test_backtest_reports.py @@ -1,7 +1,8 @@ import pandas as pd -from freqtrade.optimize.backtest_reports import ( - generate_text_table, generate_text_table_sell_reason, +from freqtrade.edge import PairInfo +from freqtrade.optimize.optimize_reports import ( + generate_edge_table, generate_text_table, generate_text_table_sell_reason, generate_text_table_strategy) from freqtrade.strategy.interface import SellType @@ -94,3 +95,14 @@ def test_generate_text_table_strategy(default_conf, mocker): '| 1.30000000 | 45.00 | 0:20:00 | 3 | 0 |' ) assert generate_text_table_strategy('BTC', 2, all_results=results) == result_str + + +def test_generate_edge_table(edge_conf, mocker): + + results = {} + results['ETH/BTC'] = PairInfo(-0.01, 0.60, 2, 1, 3, 10, 60) + + assert generate_edge_table(results).count(':|') == 7 + assert generate_edge_table(results).count('| ETH/BTC |') == 1 + assert generate_edge_table(results).count( + '| risk reward ratio | required risk reward | expectancy |') == 1 diff --git a/tests/optimize/test_edge_cli.py b/tests/optimize/test_edge_cli.py index ddfa7156e..acc0d2d16 100644 --- a/tests/optimize/test_edge_cli.py +++ b/tests/optimize/test_edge_cli.py @@ -3,7 +3,6 @@ from unittest.mock import MagicMock -from freqtrade.edge import PairInfo from freqtrade.optimize import setup_configuration, start_edge from freqtrade.optimize.edge_cli import EdgeCli from freqtrade.state import RunMode @@ -106,16 +105,3 @@ 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_generate_edge_table(edge_conf, mocker): - patch_exchange(mocker) - edge_cli = EdgeCli(edge_conf) - - results = {} - results['ETH/BTC'] = PairInfo(-0.01, 0.60, 2, 1, 3, 10, 60) - - assert edge_cli._generate_edge_table(results).count(':|') == 7 - assert edge_cli._generate_edge_table(results).count('| ETH/BTC |') == 1 - assert edge_cli._generate_edge_table(results).count( - '| risk reward ratio | required risk reward | expectancy |') == 1 From 785cd2a640bda12ef39a1391e9f07f2b89dc229d Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 9 Jan 2020 06:52:56 +0100 Subject: [PATCH 246/324] Rename test module --- freqtrade/optimize/optimize_reports.py | 4 ++-- .../{test_backtest_reports.py => test_optimize_reports.py} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename tests/optimize/{test_backtest_reports.py => test_optimize_reports.py} (100%) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index ffc8c53d4..3801546b1 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -114,8 +114,8 @@ def generate_edge_table(results: dict) -> str: floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', '.d') tabular_data = [] headers = ['pair', 'stoploss', 'win rate', 'risk reward ratio', - 'required risk reward', 'expectancy', 'total number of trades', - 'average duration (min)'] + 'required risk reward', 'expectancy', 'total number of trades', + 'average duration (min)'] for result in results.items(): if result[1].nb_trades > 0: diff --git a/tests/optimize/test_backtest_reports.py b/tests/optimize/test_optimize_reports.py similarity index 100% rename from tests/optimize/test_backtest_reports.py rename to tests/optimize/test_optimize_reports.py From b748ed34358466e3925f547f23a343a996d2b3c2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 9 Jan 2020 19:59:13 +0100 Subject: [PATCH 247/324] UPdate documentaiton wording --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index 9a05eea2d..f242a4744 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -149,7 +149,7 @@ For example, if you have 10 ETH available in your wallet on the exchange and `tr #### Amend last stake amount -Assuming we have a `balance=1000` (USDT), `stake_amount=400`, and `max_open_trades=3`. +Assuming we have the tradable balance of 1000 USDT (`balance=1000`), `stake_amount=400`, and `max_open_trades=3` (This example assumes `tradable_balance_ratio=1`). The bot would open 2 trades, and will be unable to fill the last trading slot, since the requested 400 USDT are no longer available, since 800 USDT are already tied in other trades. To overcome this, the option `amend_last_stake_amount` can be set to `True`, which will enable the bot to reduce stake_amount to the available balance in order to fill the last trade slot. From 9713dc8d94c181d293b6994b2587280dde60e71b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 9 Jan 2020 20:09:21 +0100 Subject: [PATCH 248/324] Ensure wallets.update is called before buy closes #2756 --- freqtrade/freqtradebot.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5aacdc587..06f435421 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -257,6 +257,9 @@ class FreqtradeBot: :raise: DependencyException if the available stake amount is too low """ stake_amount: float + # Ensure wallets are uptodate. + self.wallets.update() + if self.edge: stake_amount = self.edge.stake_amount( pair, From e94dfdeff24def65c0a52f2fb1690c4a5ca5845e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 9 Jan 2020 20:13:14 +0100 Subject: [PATCH 249/324] UPdate documentation to remove inexisting setting --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index f242a4744..a0e7610f6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -149,7 +149,7 @@ For example, if you have 10 ETH available in your wallet on the exchange and `tr #### Amend last stake amount -Assuming we have the tradable balance of 1000 USDT (`balance=1000`), `stake_amount=400`, and `max_open_trades=3` (This example assumes `tradable_balance_ratio=1`). +Assuming we have the tradable balance of 1000 USDT, `stake_amount=400`, and `max_open_trades=3` (This example assumes `tradable_balance_ratio=1`). The bot would open 2 trades, and will be unable to fill the last trading slot, since the requested 400 USDT are no longer available, since 800 USDT are already tied in other trades. To overcome this, the option `amend_last_stake_amount` can be set to `True`, which will enable the bot to reduce stake_amount to the available balance in order to fill the last trade slot. From fab19ae3a7e0bf554f102ba292fc7199268aa847 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 10 Jan 2020 06:36:28 +0100 Subject: [PATCH 250/324] Implement last_stake_amount_min_ratio --- config_full.json.example | 1 + docs/configuration.md | 6 +++++- freqtrade/constants.py | 4 ++++ freqtrade/freqtradebot.py | 7 ++++++- tests/test_freqtradebot.py | 18 ++++++++++++------ 5 files changed, 28 insertions(+), 8 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index f39abb00c..82d8bd04a 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -6,6 +6,7 @@ "fiat_display_currency": "USD", "amount_reserve_percent" : 0.05, "amend_last_stake_amount": false, + "last_stake_amount_min_ratio": 0.5, "dry_run": false, "ticker_interval": "5m", "trailing_stop": false, diff --git a/docs/configuration.md b/docs/configuration.md index a0e7610f6..e609f89d9 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -45,6 +45,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Positive float or `"unlimited"`.* | `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade).
*Defaults to `0.99` 99%).*
***Datatype:*** *Positive float between `0.1` and `1.0`.* | `amend_last_stake_amount` | **Required.** Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade).
*Defaults to `false`.*
***Datatype:*** *Boolean* +| `last_stake_amount_min_ratio` | **Required.** Minimum amount that has to be left. Applies only to "last_stake_amount" if `amend_last_stake_amount=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 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* @@ -149,7 +150,7 @@ For example, if you have 10 ETH available in your wallet on the exchange and `tr #### Amend last stake amount -Assuming we have the tradable balance of 1000 USDT, `stake_amount=400`, and `max_open_trades=3` (This example assumes `tradable_balance_ratio=1`). +Assuming we have the tradable balance of 1000 USDT, `stake_amount=400`, and `max_open_trades=3`. The bot would open 2 trades, and will be unable to fill the last trading slot, since the requested 400 USDT are no longer available, since 800 USDT are already tied in other trades. To overcome this, the option `amend_last_stake_amount` can be set to `True`, which will enable the bot to reduce stake_amount to the available balance in order to fill the last trade slot. @@ -163,6 +164,9 @@ In the example above this would mean: !!! Note 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, close to the minimum tradable amount. + #### Static stake amount The `stake_amount` configuration statically configures the amount of stake-currency your bot will use for each trade. diff --git a/freqtrade/constants.py b/freqtrade/constants.py index a1bd70183..e9c103db9 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -80,6 +80,9 @@ CONF_SCHEMA = { 'default': 0.99 }, 'amend_last_stake_amount': {'type': 'boolean', 'default': False}, + 'last_stake_amount_min_ratio': { + 'type': 'number', 'minimum': 0.0, 'maximum': 1.0, 'default': 0.5 + }, 'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT}, 'dry_run': {'type': 'boolean'}, 'dry_run_wallet': {'type': 'number', 'default': DRY_RUN_WALLET}, @@ -284,6 +287,7 @@ SCHEMA_TRADE_REQUIRED = [ 'stake_currency', 'stake_amount', 'tradable_balance_ratio', + 'last_stake_amount_min_ratio', 'dry_run', 'dry_run_wallet', 'bid_strategy', diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 06f435421..e3f2616a2 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -312,7 +312,12 @@ class FreqtradeBot: available_amount = self._get_available_stake_amount() if self.config['amend_last_stake_amount']: - stake_amount = min(stake_amount, available_amount) + # Remaining amount needs to be at least stake_amount * last_stake_amount_min_ratio + # Otherwise the remaining amount is too low to trade. + if available_amount > (stake_amount * self.config['last_stake_amount_min_ratio']): + stake_amount = min(stake_amount, available_amount) + else: + stake_amount = 0 if available_amount < stake_amount: raise DependencyException( diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 6d794fc05..58f88198a 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -140,14 +140,18 @@ def test_get_trade_stake_amount(default_conf, ticker, mocker) -> None: assert result == default_conf['stake_amount'] -@pytest.mark.parametrize("amend_last,wallet,max_open,expected", [ - (False, 0.002, 2, [0.001, None]), - (True, 0.002, 2, [0.001, 0.00098]), - (False, 0.003, 3, [0.001, 0.001, None]), - (True, 0.003, 3, [0.001, 0.001, 0.00097]), +@pytest.mark.parametrize("amend_last,wallet,max_open,lsamr,expected", [ + (False, 0.002, 2, 0.5, [0.001, None]), + (True, 0.002, 2, 0.5, [0.001, 0.00098]), + (False, 0.003, 3, 0.5, [0.001, 0.001, None]), + (True, 0.003, 3, 0.5, [0.001, 0.001, 0.00097]), + (False, 0.0022, 3, 0.5, [0.001, 0.001, None]), + (True, 0.0022, 3, 0.5, [0.001, 0.001, 0.0]), + (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, - amend_last, wallet, max_open, expected) -> None: + amend_last, wallet, max_open, lsamr, expected) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -160,6 +164,8 @@ def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_b default_conf['dry_run_wallet'] = wallet default_conf['amend_last_stake_amount'] = amend_last + default_conf['last_stake_amount_min_ratio'] = lsamr + freqtrade = FreqtradeBot(default_conf) for i in range(0, max_open): From 3faa2d0eb9f579658e7d081fdb31720ad1838a97 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Fri, 10 Jan 2020 22:59:02 +0300 Subject: [PATCH 251/324] Refine description for last_stake_amount_min_ratio --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index e609f89d9..9aa3a7585 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -45,7 +45,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Positive float or `"unlimited"`.* | `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade).
*Defaults to `0.99` 99%).*
***Datatype:*** *Positive float between `0.1` and `1.0`.* | `amend_last_stake_amount` | **Required.** Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade).
*Defaults to `false`.*
***Datatype:*** *Boolean* -| `last_stake_amount_min_ratio` | **Required.** Minimum amount that has to be left. Applies only to "last_stake_amount" if `amend_last_stake_amount=True` [More information below](#configuring-amount-per-trade).
*Defaults to `0.5`.*
***Datatype:*** *Float (as ratio)* +| `last_stake_amount_min_ratio` | **Required.** 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 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* From 83b88e791642d49b96edf55323f5b11ba4b097e6 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Fri, 10 Jan 2020 23:14:17 +0300 Subject: [PATCH 252/324] Remove Required marks for new settings --- docs/configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 9aa3a7585..f83072b03 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -44,8 +44,8 @@ Mandatory parameters are marked as **Required**, which means that they are requi | `stake_currency` | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *String* | `stake_amount` | **Required.** Amount of crypto-currency your bot will use for each trade. Set it to `"unlimited"` to allow the bot to use all available balance. [More information below](#configuring-amount-per-trade). [Strategy Override](#parameters-in-the-strategy).
***Datatype:*** *Positive float or `"unlimited"`.* | `tradable_balance_ratio` | Ratio of the total account balance the bot is allowed to trade. [More information below](#configuring-amount-per-trade).
*Defaults to `0.99` 99%).*
***Datatype:*** *Positive float between `0.1` and `1.0`.* -| `amend_last_stake_amount` | **Required.** Use reduced last stake amount if necessary. [More information below](#configuring-amount-per-trade).
*Defaults to `false`.*
***Datatype:*** *Boolean* -| `last_stake_amount_min_ratio` | **Required.** 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)* +| `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 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* From d3de3983950fdc7e8dbc0159dfb5c979c2b8c623 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Fri, 10 Jan 2020 23:43:09 +0300 Subject: [PATCH 253/324] Docs adjusted --- docs/configuration.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index f83072b03..fe692eacb 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -143,10 +143,10 @@ Freqtrade will reserve 1% for eventual fees when entering a trade and will there You can configure the "untouched" amount by using the `tradable_balance_ratio` setting. -For example, if you have 10 ETH available in your wallet on the exchange and `tradable_balance_ratio=0.5` (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers this as available balance. +For example, if you have 10 ETH available in your wallet on the exchange and `tradable_balance_ratio=0.5` (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers this as available balance. The rest of the wallet is untouched by the trades. !!! Warning - `tradable_balance_ratio` applies to the current balance (free balance + tied up in trades). Therefore, assuming a starting balance of 1000, a configuration of `tradable_balance_ratio=0.99` will not guarantee that 10 units will always remain available on the exchange. The free amount may reduce to 5 units if the total balance is reduce to 500 (either by a losing streak, or by withdrawing balance). + The `tradable_balance_ratio` setting applies to the current balance (free balance + tied up in trades). Therefore, assuming the starting balance of 1000, a configuration with `tradable_balance_ratio=0.99` will not guarantee that 10 currency units will always remain available on the exchange. For example, the free amount may reduce to 5 units if the total balance is reduced to 500 (either by a losing streak, or by withdrawing balance). #### Amend last stake amount @@ -162,10 +162,10 @@ In the example above this would mean: - Trade3: 200 USDT !!! Note - This option only applies with [Static stake amount](#static-stake-amount) - since [Dynamic stake amount](#dynamic-stake-amount) divides the balances evenly. + 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, close to the minimum tradable amount. + 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. #### Static stake amount From 5faebad863a59ed6436fc9351efd93df9908e428 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Jan 2020 11:16:05 +0100 Subject: [PATCH 254/324] Don't hardcode TimeFrames - they can differ by exchange. --- freqtrade/constants.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index e9c103db9..598a64337 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -33,12 +33,6 @@ USER_DATA_FILES = { 'strategy_analysis_example.ipynb': 'notebooks', } -TIMEFRAMES = [ - '1m', '3m', '5m', '15m', '30m', - '1h', '2h', '4h', '6h', '8h', '12h', - '1d', '3d', '1w', -] - SUPPORTED_FIAT = [ "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", @@ -66,7 +60,7 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'max_open_trades': {'type': ['integer', 'number'], 'minimum': -1}, - 'ticker_interval': {'type': 'string', 'enum': TIMEFRAMES}, + 'ticker_interval': {'type': 'string'}, 'stake_currency': {'type': 'string', 'enum': ['BTC', 'XBT', 'ETH', 'USDT', 'EUR', 'USD']}, 'stake_amount': { 'type': ['number', 'string'], From 235a10ab860d887342635a45d73d3ce1e9b83871 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Jan 2020 11:36:28 +0100 Subject: [PATCH 255/324] Don't suppport <1m timeframes --- freqtrade/exchange/exchange.py | 4 ++++ tests/exchange/test_exchange.py | 15 ++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3ef32db62..1ee6c2f1c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -319,6 +319,10 @@ class Exchange: raise OperationalException( f"Invalid ticker interval '{timeframe}'. This exchange supports: {self.timeframes}") + if timeframe_to_minutes(timeframe) < 1: + raise OperationalException( + f"Timeframes < 1m are currently not supported by Freqtrade.") + def validate_ordertypes(self, order_types: Dict) -> None: """ Checks if order-types configured in strategy/config are supported diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index cb40bdbd9..288c10187 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -377,8 +377,11 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog): f"on the exchange and eventually remove XRP/BTC from your whitelist.", caplog) -def test_validate_timeframes(default_conf, mocker): - default_conf["ticker_interval"] = "5m" +@pytest.mark.parametrize("timeframe", [ + ('5m'), ("1m"), ("15m"), ("1h") +]) +def test_validate_timeframes(default_conf, mocker, timeframe): + default_conf["ticker_interval"] = timeframe api_mock = MagicMock() id_mock = PropertyMock(return_value='test_exchange') type(api_mock).id = id_mock @@ -399,7 +402,8 @@ def test_validate_timeframes_failed(default_conf, mocker): api_mock = MagicMock() id_mock = PropertyMock(return_value='test_exchange') type(api_mock).id = id_mock - timeframes = PropertyMock(return_value={'1m': '1m', + timeframes = PropertyMock(return_value={'15s': '15s', + '1m': '1m', '5m': '5m', '15m': '15m', '1h': '1h'}) @@ -411,6 +415,11 @@ def test_validate_timeframes_failed(default_conf, mocker): with pytest.raises(OperationalException, match=r"Invalid ticker interval '3m'. This exchange supports.*"): Exchange(default_conf) + default_conf["ticker_interval"] = "15s" + + with pytest.raises(OperationalException, + match=r"Timeframes < 1m are currently not supported by Freqtrade."): + Exchange(default_conf) def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker): From 13274964a94a38975e66b8963c1c5622a7cec022 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Jan 2020 11:53:44 +0100 Subject: [PATCH 256/324] Implement validation for valid stake currency --- freqtrade/constants.py | 2 +- freqtrade/exchange/exchange.py | 25 +++++++++++++++++++++++-- tests/conftest.py | 1 + 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 598a64337..53bc4af53 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -61,7 +61,7 @@ CONF_SCHEMA = { 'properties': { 'max_open_trades': {'type': ['integer', 'number'], 'minimum': -1}, 'ticker_interval': {'type': 'string'}, - 'stake_currency': {'type': 'string', 'enum': ['BTC', 'XBT', 'ETH', 'USDT', 'EUR', 'USD']}, + 'stake_currency': {'type': 'string'}, 'stake_amount': { 'type': ['number', 'string'], 'minimum': 0.0001, diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 1ee6c2f1c..e15f6885c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -116,6 +116,7 @@ class Exchange: self._load_markets() # Check if all pairs are available + self.validate_stakecurrency(config['stake_currency']) 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', {})) @@ -210,6 +211,14 @@ class Exchange: markets = {k: v for k, v in markets.items() if market_is_active(v)} return markets + def get_quote_currencies(self) -> List[str]: + """ + Return a list of supported quote currencies + """ + markets = self.markets + currencies = set([x['quote'] for _, x in markets.items()]) + return list(currencies) + def klines(self, pair_interval: Tuple[str, str], copy=True) -> DataFrame: if pair_interval in self._klines: return self._klines[pair_interval].copy() if copy else self._klines[pair_interval] @@ -259,11 +268,23 @@ class Exchange: except ccxt.BaseError: logger.exception("Could not reload markets.") + def validate_stakecurrency(self, stake_currency) -> None: + """ + Checks stake-currency against available currencies on the exchange. + :param stake_currency: Stake-currency to validate + :raise: OperationalException if stake-currency is not available. + """ + quote_currencies = self.get_quote_currencies() + if stake_currency not in quote_currencies: + raise OperationalException( + f"{stake_currency} is not available as stake on {self.name}." + f"Available currencies are: {','.join(quote_currencies)}") + def validate_pairs(self, pairs: List[str]) -> None: """ Checks if all given pairs are tradable on the current exchange. - Raises OperationalException if one pair is not available. :param pairs: list of pairs + :raise: OperationalException if one pair is not available :return: None """ @@ -319,7 +340,7 @@ class Exchange: raise OperationalException( f"Invalid ticker interval '{timeframe}'. This exchange supports: {self.timeframes}") - if timeframe_to_minutes(timeframe) < 1: + if timeframe and timeframe_to_minutes(timeframe) < 1: raise OperationalException( f"Timeframes < 1m are currently not supported by Freqtrade.") diff --git a/tests/conftest.py b/tests/conftest.py index 7bb4cf4c9..ac1b10284 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -60,6 +60,7 @@ def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> No mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id)) mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title())) if mock_markets: From ca2880537d6a95bdee468aa4bb0f1463e3ab8d1c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Jan 2020 11:54:11 +0100 Subject: [PATCH 257/324] Modify tests to skip stake_currency validations --- tests/exchange/test_exchange.py | 66 ++++++++++++++++++++------------- 1 file changed, 41 insertions(+), 25 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 288c10187..bd73a98de 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -73,6 +73,7 @@ def test_init(default_conf, mocker, caplog): def test_init_ccxt_kwargs(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) + mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') caplog.set_level(logging.INFO) conf = copy.deepcopy(default_conf) conf['exchange']['ccxt_async_config'] = {'aiohttp_trust_env': True} @@ -121,9 +122,10 @@ 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', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', 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') exchange = ExchangeResolver.load_exchange('Bittrex', default_conf) assert isinstance(exchange, Exchange) assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog) @@ -257,9 +259,10 @@ def test__load_markets(default_conf, mocker, caplog): api_mock = MagicMock() api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError("SomeError")) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) + 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) @@ -324,8 +327,9 @@ def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs d type(api_mock).id = id_mock mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) + 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) @@ -335,8 +339,9 @@ def test_validate_pairs_not_available(default_conf, mocker): 'XRP/BTC': {'inactive': True} }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) + 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) @@ -349,8 +354,9 @@ def test_validate_pairs_exception(default_conf, mocker, caplog): type(api_mock).markets = PropertyMock(return_value={}) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) + 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) @@ -368,8 +374,9 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog): 'NEO/BTC': {'info': 'TestString'}, # info can also be a string ... }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) + 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(f"Pair XRP/BTC is restricted for some users on this exchange." @@ -393,7 +400,8 @@ def test_validate_timeframes(default_conf, mocker, timeframe): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs') + mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) @@ -433,7 +441,8 @@ def test_validate_timeframes_emulated_ohlcv_1(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs') + mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') with pytest.raises(OperationalException, match=r'The ccxt library does not provide the list of timeframes ' r'for the exchange ".*" and this exchange ' @@ -454,6 +463,7 @@ def test_validate_timeframes_emulated_ohlcvi_2(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={'timeframes': None})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') with pytest.raises(OperationalException, match=r'The ccxt library does not provide the list of timeframes ' r'for the exchange ".*" and this exchange ' @@ -474,7 +484,8 @@ def test_validate_timeframes_not_in_config(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={})) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs') + mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') Exchange(default_conf) @@ -484,8 +495,9 @@ def test_validate_order_types(default_conf, mocker): type(api_mock).has = PropertyMock(return_value={'createMarketOrder': True}) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) 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()) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs') + 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', @@ -526,8 +538,9 @@ def test_validate_order_types_not_in_config(default_conf, mocker): api_mock = MagicMock() mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) 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()) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs') + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') + mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') conf = copy.deepcopy(default_conf) Exchange(conf) @@ -538,9 +551,10 @@ def test_validate_required_startup_candles(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='Binance')) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) - mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock()) - mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + 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') default_conf['startup_candle_count'] = 20 ex = Exchange(default_conf) @@ -1674,7 +1688,9 @@ def test_merge_ft_has_dict(default_conf, mocker): _init_ccxt=MagicMock(return_value=MagicMock()), _load_async_markets=MagicMock(), validate_pairs=MagicMock(), - validate_timeframes=MagicMock()) + validate_timeframes=MagicMock(), + validate_stakecurrency=MagicMock() + ) ex = Exchange(default_conf) assert ex._ft_has == Exchange._ft_has_default From 60b47b6eeca7ae9267bbdd951bebda9f6235240c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Jan 2020 12:01:34 +0100 Subject: [PATCH 258/324] Add tests for get_quote_currencies --- freqtrade/exchange/exchange.py | 2 +- tests/exchange/test_exchange.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e15f6885c..a5c10776c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -216,7 +216,7 @@ class Exchange: Return a list of supported quote currencies """ markets = self.markets - currencies = set([x['quote'] for _, x in markets.items()]) + currencies = set([x.get('quote') for _, x in markets.items()]) return list(currencies) def klines(self, pair_interval: Tuple[str, str], copy=True) -> DataFrame: diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index bd73a98de..5924d4b40 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -318,6 +318,27 @@ def test__reload_markets_exception(default_conf, mocker, caplog): assert log_has_re(r"Could not reload markets.*", caplog) +@pytest.mark.parametrize("stake_currency", ['ETH', 'BTC', 'USDT']) +def test_validate_stake_currency(default_conf, stake_currency, mocker, caplog): + default_conf['stake_currency'] = stake_currency + api_mock = MagicMock() + type(api_mock).markets = PropertyMock(return_value={ + 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'}, + 'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'}, + }) + 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) + + +def test_get_quote_currencies(default_conf, mocker): + ex = get_patched_exchange(mocker, default_conf) + + assert set(ex.get_quote_currencies()) == set(['USD', 'BTC', 'USDT']) + + def test_validate_pairs(default_conf, mocker): # test exchange.validate_pairs directly api_mock = MagicMock() type(api_mock).markets = PropertyMock(return_value={ From a7246ba1ecfd96f12e1fd2bbe252b569e12ed872 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Jan 2020 12:51:42 +0100 Subject: [PATCH 259/324] No need to "fix" stake_currency enum anymore --- freqtrade/configuration/config_validation.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/freqtrade/configuration/config_validation.py b/freqtrade/configuration/config_validation.py index 54941043d..5183ad0b4 100644 --- a/freqtrade/configuration/config_validation.py +++ b/freqtrade/configuration/config_validation.py @@ -48,11 +48,6 @@ def validate_config_schema(conf: Dict[str, Any]) -> Dict[str, Any]: conf_schema['required'] = constants.SCHEMA_TRADE_REQUIRED else: conf_schema['required'] = constants.SCHEMA_MINIMAL_REQUIRED - # Dynamically allow empty stake-currency - # Since the minimal config specifies this too. - # It's not allowed for Dry-run or live modes - conf_schema['properties']['stake_currency']['enum'] += [''] # type: ignore - try: FreqtradeValidator(conf_schema).validate(conf) return conf From 3519cebf66e31aa799ae9422ab31e17c20153844 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 11 Jan 2020 13:14:19 +0100 Subject: [PATCH 260/324] Add test for failing stake_validation --- freqtrade/exchange/exchange.py | 9 +++++---- tests/exchange/test_exchange.py | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a5c10776c..3735dac8a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -216,8 +216,9 @@ class Exchange: Return a list of supported quote currencies """ markets = self.markets - currencies = set([x.get('quote') for _, x in markets.items()]) - return list(currencies) + currencies = list(set([x.get('quote') for _, x in markets.items()])) + currencies.sort() + return currencies def klines(self, pair_interval: Tuple[str, str], copy=True) -> DataFrame: if pair_interval in self._klines: @@ -277,8 +278,8 @@ class Exchange: quote_currencies = self.get_quote_currencies() if stake_currency not in quote_currencies: raise OperationalException( - f"{stake_currency} is not available as stake on {self.name}." - f"Available currencies are: {','.join(quote_currencies)}") + f"{stake_currency} is not available as stake on {self.name}. " + f"Available currencies are: {', '.join(quote_currencies)}") def validate_pairs(self, pairs: List[str]) -> None: """ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 5924d4b40..150ba3ea7 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -333,6 +333,23 @@ def test_validate_stake_currency(default_conf, stake_currency, mocker, caplog): Exchange(default_conf) +def test_validate_stake_currency_error(default_conf, mocker, caplog): + default_conf['stake_currency'] = 'XRP' + api_mock = MagicMock() + type(api_mock).markets = PropertyMock(return_value={ + 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'}, + 'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'}, + }) + 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'): + Exchange(default_conf) + + def test_get_quote_currencies(default_conf, mocker): ex = get_patched_exchange(mocker, default_conf) From 53abfdbcbf032de93994fb2965b6d0192660df6a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jan 2020 12:48:29 +0100 Subject: [PATCH 261/324] Use sorted on set instead of explicit list conversion --- freqtrade/exchange/exchange.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3735dac8a..714bb40e4 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -216,9 +216,7 @@ class Exchange: Return a list of supported quote currencies """ markets = self.markets - currencies = list(set([x.get('quote') for _, x in markets.items()])) - currencies.sort() - return currencies + return sorted(set([x['quote'] for _, x in markets.items()])) def klines(self, pair_interval: Tuple[str, str], copy=True) -> DataFrame: if pair_interval in self._klines: From fa1f9bcdbd697274c50489a06c7c42e586db4999 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jan 2020 14:37:45 +0100 Subject: [PATCH 262/324] expose precisionMode from exchange class --- freqtrade/exchange/exchange.py | 5 +++++ tests/conftest.py | 1 + 2 files changed, 6 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3ef32db62..f29bc3c6f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -188,6 +188,11 @@ class Exchange: self._load_markets() return self._api.markets + @property + def precisionMode(self) -> str: + """exchange ccxt precisionMode""" + return self._api.precisionMode + def get_markets(self, base_currencies: List[str] = None, quote_currencies: List[str] = None, pairs_only: bool = False, active_only: bool = False) -> Dict: """ diff --git a/tests/conftest.py b/tests/conftest.py index 7bb4cf4c9..ec53141c1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -62,6 +62,7 @@ def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> No mocker.patch('freqtrade.exchange.Exchange.validate_ordertypes', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id)) mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title())) + mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2)) if mock_markets: mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=get_markets())) From b60d7ad42fde62614c934abf14931b1103b04962 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jan 2020 14:40:58 +0100 Subject: [PATCH 263/324] Use ccxt.decimal_to_precision instead of our own calculation --- freqtrade/exchange/exchange.py | 24 ++++++++++++++---------- tests/exchange/test_exchange.py | 19 +++++++++++++++++++ 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index f29bc3c6f..e1b5c411d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -7,14 +7,15 @@ import inspect import logging from copy import deepcopy from datetime import datetime, timezone -from math import ceil, floor +from math import ceil from random import randint 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 +from ccxt.base.decimal_to_precision import (ROUND, ROUND_DOWN, ROUND_UP, + TRUNCATE, decimal_to_precision) from pandas import DataFrame from freqtrade.data.converter import parse_ticker_dataframe @@ -367,26 +368,29 @@ class Exchange: """ return endpoint in self._api.has and self._api.has[endpoint] - def symbol_amount_prec(self, pair, amount: float): + def symbol_amount_prec(self, pair, amount: float) -> float: ''' Returns the amount to buy or sell to a precision the Exchange accepts Rounded down ''' if self.markets[pair]['precision']['amount']: - symbol_prec = self.markets[pair]['precision']['amount'] - big_amount = amount * pow(10, symbol_prec) - amount = floor(big_amount) / pow(10, symbol_prec) + amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE, + precision=self.markets[pair]['precision']['amount'], + counting_mode=self.precisionMode, + )) + return amount - def symbol_price_prec(self, pair, price: float): + def symbol_price_prec(self, pair, price: float) -> float: ''' Returns the price buying or selling with to the precision the Exchange accepts Rounds up ''' if self.markets[pair]['precision']['price']: - symbol_prec = self.markets[pair]['precision']['price'] - big_price = price * pow(10, symbol_prec) - price = ceil(big_price) / pow(10, symbol_prec) + price = float(decimal_to_precision(price, rounding_mode=ROUND, + precision=self.markets[pair]['precision']['price'], + counting_mode=self.precisionMode, + )) return price def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index cb40bdbd9..b90e94547 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -181,6 +181,11 @@ def test_symbol_amount_prec(default_conf, mocker): markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': 4}}}) exchange = get_patched_exchange(mocker, default_conf, id="binance") + # digits counting mode + # DECIMAL_PLACES = 2 + # SIGNIFICANT_DIGITS = 3 + # TICK_SIZE = 4 + mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2)) mocker.patch('freqtrade.exchange.Exchange.markets', markets) amount = 2.34559 @@ -188,6 +193,12 @@ def test_symbol_amount_prec(default_conf, mocker): amount = exchange.symbol_amount_prec(pair, amount) assert amount == 2.3455 + markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': 0.0001}}}) + mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=4)) + mocker.patch('freqtrade.exchange.Exchange.markets', markets) + amount = exchange.symbol_amount_prec(pair, amount) + assert amount == 2.3455 + def test_symbol_price_prec(default_conf, mocker): ''' @@ -197,12 +208,20 @@ def test_symbol_price_prec(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, id="binance") mocker.patch('freqtrade.exchange.Exchange.markets', markets) + mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2)) price = 2.34559 pair = 'ETH/BTC' price = exchange.symbol_price_prec(pair, price) assert price == 2.3456 + markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': 0.0001}}}) + mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=4)) + mocker.patch('freqtrade.exchange.Exchange.markets', markets) + + price = exchange.symbol_price_prec(pair, price) + assert price == 2.3456 + def test_set_sandbox(default_conf, mocker): """ From 5fcab1eee8efce5bfa17d43f70542da65dcc4449 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Jan 2020 14:55:05 +0100 Subject: [PATCH 264/324] Align method names to internal ccxt names These methods are reimplemented from ccxt so we can test their behaviour. --- freqtrade/exchange/binance.py | 6 ++--- freqtrade/exchange/exchange.py | 19 ++++++++-------- freqtrade/pairlist/PrecisionFilter.py | 4 ++-- tests/exchange/test_binance.py | 8 +++---- tests/exchange/test_exchange.py | 32 +++++++++++++-------------- tests/exchange/test_kraken.py | 8 +++---- tests/test_freqtradebot.py | 8 +++---- tests/test_integration.py | 8 +++---- 8 files changed, 47 insertions(+), 46 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 96f72fcf5..12326f083 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -41,7 +41,7 @@ class Binance(Exchange): """ ordertype = "stop_loss_limit" - stop_price = self.symbol_price_prec(pair, stop_price) + stop_price = self.price_to_precision(pair, stop_price) # Ensure rate is less than stop price if stop_price <= rate: @@ -57,9 +57,9 @@ class Binance(Exchange): params = self._params.copy() params.update({'stopPrice': stop_price}) - amount = self.symbol_amount_prec(pair, amount) + amount = self.amount_to_precision(pair, amount) - rate = self.symbol_price_prec(pair, rate) + rate = self.price_to_precision(pair, rate) order = self._api.create_order(pair, ordertype, 'sell', amount, rate, params) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e1b5c411d..dd6f257fe 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -7,7 +7,6 @@ import inspect 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 @@ -368,10 +367,11 @@ class Exchange: """ return endpoint in self._api.has and self._api.has[endpoint] - def symbol_amount_prec(self, pair, amount: float) -> float: + def amount_to_precision(self, pair, amount: float) -> float: ''' Returns the amount to buy or sell to a precision the Exchange accepts - Rounded down + Reimplementation of ccxt internal methods - ensuring we can test the result is correct + based on our definitions. ''' if self.markets[pair]['precision']['amount']: amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE, @@ -381,10 +381,11 @@ class Exchange: return amount - def symbol_price_prec(self, pair, price: float) -> float: + def price_to_precision(self, pair, price: float) -> float: ''' Returns the price buying or selling with to the precision the Exchange accepts - Rounds up + Reimplementation of ccxt internal methods - ensuring we can test the result is correct + based on our definitions. ''' if self.markets[pair]['precision']['price']: price = float(decimal_to_precision(price, rounding_mode=ROUND, @@ -396,7 +397,7 @@ class Exchange: def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, params: Dict = {}) -> Dict[str, Any]: order_id = f'dry_run_{side}_{randint(0, 10**6)}' - _amount = self.symbol_amount_prec(pair, amount) + _amount = self.amount_to_precision(pair, amount) dry_order = { "id": order_id, 'pair': pair, @@ -431,13 +432,13 @@ class Exchange: rate: float, params: Dict = {}) -> Dict: try: # Set the precision for amount and price(rate) as accepted by the exchange - amount = self.symbol_amount_prec(pair, amount) + amount = self.amount_to_precision(pair, amount) needs_price = (ordertype != 'market' or self._api.options.get("createMarketBuyOrderRequiresPrice", False)) - rate = self.symbol_price_prec(pair, rate) if needs_price else None + rate_for_order = self.price_to_precision(pair, rate) if needs_price else None return self._api.create_order(pair, ordertype, side, - amount, rate, params) + amount, rate_for_order, params) except ccxt.InsufficientFunds as e: raise DependencyException( diff --git a/freqtrade/pairlist/PrecisionFilter.py b/freqtrade/pairlist/PrecisionFilter.py index aedcc5a88..5d364795d 100644 --- a/freqtrade/pairlist/PrecisionFilter.py +++ b/freqtrade/pairlist/PrecisionFilter.py @@ -35,8 +35,8 @@ class PrecisionFilter(IPairList): """ stop_price = ticker['ask'] * stoploss # Adjust stop-prices to precision - sp = self._exchange.symbol_price_prec(ticker["symbol"], stop_price) - stop_gap_price = self._exchange.symbol_price_prec(ticker["symbol"], stop_price * 0.99) + sp = self._exchange.price_to_precision(ticker["symbol"], stop_price) + stop_gap_price = self._exchange.price_to_precision(ticker["symbol"], stop_price * 0.99) logger.debug(f"{ticker['symbol']} - {sp} : {stop_gap_price}") if sp <= stop_gap_price: logger.info(f"Removed {ticker['symbol']} from whitelist, " diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 0a12c1cb1..4bc918c3d 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -22,8 +22,8 @@ def test_stoploss_limit_order(default_conf, mocker): }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + 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, 'binance') @@ -71,8 +71,8 @@ def test_stoploss_limit_order_dry_run(default_conf, mocker): api_mock = MagicMock() order_type = 'stop_loss_limit' default_conf['dry_run'] = True - mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + 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, 'binance') diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index b90e94547..d666d29ac 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -173,7 +173,7 @@ def test_validate_order_time_in_force(default_conf, mocker, caplog): ex.validate_order_time_in_force(tif2) -def test_symbol_amount_prec(default_conf, mocker): +def test_amount_to_precision(default_conf, mocker): ''' Test rounds down to 4 Decimal places ''' @@ -190,17 +190,17 @@ def test_symbol_amount_prec(default_conf, mocker): amount = 2.34559 pair = 'ETH/BTC' - amount = exchange.symbol_amount_prec(pair, amount) + amount = exchange.amount_to_precision(pair, amount) assert amount == 2.3455 markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': 0.0001}}}) mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=4)) mocker.patch('freqtrade.exchange.Exchange.markets', markets) - amount = exchange.symbol_amount_prec(pair, amount) + amount = exchange.amount_to_precision(pair, amount) assert amount == 2.3455 -def test_symbol_price_prec(default_conf, mocker): +def test_sprice_to_precision(default_conf, mocker): ''' Test rounds up to 4 decimal places ''' @@ -212,14 +212,14 @@ def test_symbol_price_prec(default_conf, mocker): price = 2.34559 pair = 'ETH/BTC' - price = exchange.symbol_price_prec(pair, price) + price = exchange.price_to_precision(pair, price) assert price == 2.3456 markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': 0.0001}}}) mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=4)) mocker.patch('freqtrade.exchange.Exchange.markets', markets) - price = exchange.symbol_price_prec(pair, price) + price = exchange.price_to_precision(pair, price) assert price == 2.3456 @@ -615,8 +615,8 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, } }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + 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, id=exchange_name) order = exchange.create_order( @@ -656,8 +656,8 @@ def test_buy_prod(default_conf, mocker, exchange_name): } }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + 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, id=exchange_name) order = exchange.buy(pair='ETH/BTC', ordertype=order_type, @@ -730,8 +730,8 @@ def test_buy_considers_time_in_force(default_conf, mocker, exchange_name): } }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + 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, id=exchange_name) order_type = 'limit' @@ -792,8 +792,8 @@ def test_sell_prod(default_conf, mocker, exchange_name): }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + 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, id=exchange_name) order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) @@ -856,8 +856,8 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): }) api_mock.options = {} default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + 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, id=exchange_name) order_type = 'limit' diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 3ad62d85a..8490ee1a2 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -21,8 +21,8 @@ def test_buy_kraken_trading_agreement(default_conf, mocker): }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + 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, id="kraken") order = exchange.buy(pair='ETH/BTC', ordertype=order_type, @@ -53,8 +53,8 @@ def test_sell_kraken_trading_agreement(default_conf, mocker): }) default_conf['dry_run'] = False - mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y) - mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y) + 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, id="kraken") order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 58f88198a..5267a267b 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2435,8 +2435,8 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, - symbol_amount_prec=lambda s, x, y: y, - symbol_price_prec=lambda s, x, y: y, + amount_to_precision=lambda s, x, y: y, + price_to_precision=lambda s, x, y: y, stoploss_limit=stoploss_limit, cancel_order=cancel_order, ) @@ -2478,8 +2478,8 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, - symbol_amount_prec=lambda s, x, y: y, - symbol_price_prec=lambda s, x, y: y, + amount_to_precision=lambda s, x, y: y, + price_to_precision=lambda s, x, y: y, ) stoploss_limit = MagicMock(return_value={ diff --git a/tests/test_integration.py b/tests/test_integration.py index 98bf1862b..2b4a1b946 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -58,8 +58,8 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, - symbol_amount_prec=lambda s, x, y: y, - symbol_price_prec=lambda s, x, y: y, + 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, ) @@ -137,8 +137,8 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, - symbol_amount_prec=lambda s, x, y: y, - symbol_price_prec=lambda s, x, y: y, + amount_to_precision=lambda s, x, y: y, + price_to_precision=lambda s, x, y: y, ) mocker.patch.multiple( From 495728f50215b19bc9ba165b0df10f0a76411898 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Mon, 13 Jan 2020 06:31:15 +0300 Subject: [PATCH 265/324] Refine docs --- docs/plotting.md | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/plotting.md b/docs/plotting.md index 2f084b66a..bc4ca8ae0 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -124,34 +124,34 @@ freqtrade plot-dataframe --strategy AwesomeStrategy --export-filename user_data/ ![plot-dataframe2](assets/plot-dataframe2.png) -plot-dataframe will require backtesting data, a strategy as well as either a backtesting-results file or a Database file, containing trades corresponding to the strategy. +The plot-dataframe subcommand requires backtesting data, a strategy and either a backtesting-results file or a database, containing trades corresponding to the strategy. The resulting plot will have the following elements: -* Green triangles: Buy signals from the strategy. (Note: not every Buy-signal also generates a trade) -* Red triangles: Sell signals from the strategy. -* Cyan Circles: Trade entry -* Red Square: Trade exit for loss or 0% profit -* Green Square: Trade exit for profit -* Indicators corresponding to the candle scale (e.g. SMA/EMA), as specified with `--indicators1`. -* Indicators with different scales (e.g. MACD, RSI) below the volume bars, as specified via `--indicators2`. -* Volume (bar chart at the bottom of the main chart) +* Green triangles: Buy signals from the strategy. (Note: not every buy signal generates a trade, compare to cyan circles.) +* Red triangles: Sell signals from the strategy. (Also, not every sell signal terminates a trade, compare to red and green squares.) +* Cyan circles: Trade entry points. +* Red squares: Trade exit points for trades with loss or 0% profit. +* Green squares: Trade exit points for profitable trades. +* Indicators with values corresponding to the candle scale (e.g. SMA/EMA), as specified with `--indicators1`. +* Volume (bar chart at the bottom of the main chart). +* Indicators with values in different scales (e.g. MACD, RSI) below the volume bars, as specified with `--indicators2`. !!! Note "Bollinger Bands" - Bollinger bands are automatically added to the plot if the columns `bb_lowerband` and `bb_upperband` exist, and are painted as a light blue Area spanning from the lower band to the upper band. + Bollinger bands are automatically added to the plot if the columns `bb_lowerband` and `bb_upperband` exist, and are painted as a light blue area spanning from the lower band to the upper band. -#### Advanced Plot configuration +#### Advanced plot configuration -An advanced plot-configuration can be specified in the strategy. +An advanced plot configuration can be specified in the strategy in the `plot_config` parameter. -This configuration allows to specify fixed colors (otherwise consecutive plots may produce different colorschemes each time, making comparisons diffiult.). -It also allows multiple subplots to display both MACD and RSI at the same time. - -Additional features when using plot_config: +Additional features when using plot_config include: * Specify colors per indicator * Specify additional subplots +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. + Sample configuration with inline comments explaining the process: ``` python From af366357698e24742219d7ff2ed18d65fe382be3 Mon Sep 17 00:00:00 2001 From: hroff-1902 <47309513+hroff-1902@users.noreply.github.com> Date: Mon, 13 Jan 2020 06:36:05 +0300 Subject: [PATCH 266/324] Minor changes in the docs --- docs/plotting.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/plotting.md b/docs/plotting.md index bc4ca8ae0..ecd5e1603 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -124,7 +124,7 @@ freqtrade plot-dataframe --strategy AwesomeStrategy --export-filename user_data/ ![plot-dataframe2](assets/plot-dataframe2.png) -The plot-dataframe subcommand requires backtesting data, a strategy and either a backtesting-results file or a database, containing trades corresponding to the strategy. +The `plot-dataframe` subcommand requires backtesting data, a strategy and either a backtesting-results file or a database, containing trades corresponding to the strategy. The resulting plot will have the following elements: @@ -185,12 +185,12 @@ Sample configuration with inline comments explaining the process: ![plot-profit](assets/plot-profit.png) -The `freqtrade plot-profit` subcommand shows an interactive graph with three plots: +The `plot-profit` subcommand shows an interactive graph with three plots: -1) Average closing price for all pairs -2) The summarized profit made by backtesting. - Note that this is not the real-world profit, but more of an estimate. -3) Profit for each individual pair +* Average closing price for all pairs. +* The summarized profit made by backtesting. +Note that this is not the real-world profit, but more of an estimate. +* Profit for each individual pair. The first graph is good to get a grip of how the overall market progresses. From 2f8ed7ed191b02fca21b138b26d1cda8f02eb44f Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2020 07:55:35 +0000 Subject: [PATCH 267/324] Bump numpy from 1.18.0 to 1.18.1 Bumps [numpy](https://github.com/numpy/numpy) from 1.18.0 to 1.18.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.18.0...v1.18.1) 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 e0e2942b1..c7dd07ee4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Load common requirements -r requirements-common.txt -numpy==1.18.0 +numpy==1.18.1 pandas==0.25.3 From b3938a86c3b5961c66c33eb3e1b45e3c22056f62 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2020 07:56:20 +0000 Subject: [PATCH 268/324] Bump python-telegram-bot from 12.2.0 to 12.3.0 Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 12.2.0 to 12.3.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.2.0...v12.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 c10536603..dc693a25f 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -2,7 +2,7 @@ # mainly used for Raspberry pi installs ccxt==1.21.32 SQLAlchemy==1.3.12 -python-telegram-bot==12.2.0 +python-telegram-bot==12.3.0 arrow==0.15.5 cachetools==4.0.0 requests==2.22.0 From c67b2530999cd95c4d3b8155b6fee4ac4d5c0b5c Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2020 10:38:34 +0000 Subject: [PATCH 269/324] Bump ccxt from 1.21.32 to 1.21.56 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.21.32 to 1.21.56. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/1.21.32...1.21.56) 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 dc693a25f..15a9d687f 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.21.32 +ccxt==1.21.56 SQLAlchemy==1.3.12 python-telegram-bot==12.3.0 arrow==0.15.5 From 797dc8a4dacf526e859af144d501d1732f935ca5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Jan 2020 15:54:53 +0100 Subject: [PATCH 270/324] Add more detailed tests for amount_to_precision --- tests/exchange/test_exchange.py | 45 ++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index d666d29ac..379e7d091 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -173,12 +173,19 @@ def test_validate_order_time_in_force(default_conf, mocker, caplog): ex.validate_order_time_in_force(tif2) -def test_amount_to_precision(default_conf, mocker): +@pytest.mark.parametrize("amount,precision,expected", [ + (2.34559, 4, 2.3455), + (2.34559, 5, 2.34559), + (2.34559, 3, 2.345), + (2.9999, 3, 2.999), + (2.9909, 3, 2.990), +]) +def test_amount_to_precision_decimal_places(default_conf, mocker, amount, precision, expected): ''' - Test rounds down to 4 Decimal places + Test rounds down ''' - markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': 4}}}) + markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': precision}}}) exchange = get_patched_exchange(mocker, default_conf, id="binance") # digits counting mode @@ -188,16 +195,36 @@ def test_amount_to_precision(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2)) mocker.patch('freqtrade.exchange.Exchange.markets', markets) - amount = 2.34559 pair = 'ETH/BTC' - amount = exchange.amount_to_precision(pair, amount) - assert amount == 2.3455 + assert exchange.amount_to_precision(pair, amount) == expected - markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': 0.0001}}}) + +@pytest.mark.parametrize("amount,precision,expected", [ + (2.34559, 0.0001, 2.3455), + (2.34559, 0.00001, 2.34559), + (2.34559, 0.001, 2.345), + (2.9999, 0.001, 2.999), + (2.9909, 0.001, 2.990), + (2.9909, 0.005, 2.990), + (2.9999, 0.005, 2.995), +]) +def test_amount_to_precision_tick_size(default_conf, mocker, amount, precision, expected): + ''' + Test rounds down + ''' + + markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': precision}}}) + + exchange = get_patched_exchange(mocker, default_conf, id="binance") + # digits counting mode + # DECIMAL_PLACES = 2 + # SIGNIFICANT_DIGITS = 3 + # TICK_SIZE = 4 mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=4)) mocker.patch('freqtrade.exchange.Exchange.markets', markets) - amount = exchange.amount_to_precision(pair, amount) - assert amount == 2.3455 + + pair = 'ETH/BTC' + assert exchange.amount_to_precision(pair, amount) == expected def test_sprice_to_precision(default_conf, mocker): From 425ec53b28ec7d4b463f2f36b6b6c97fa10a99e8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Jan 2020 16:01:35 +0100 Subject: [PATCH 271/324] Combine amount_to_precision tests into one --- tests/exchange/test_exchange.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 379e7d091..a131aaab5 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -173,14 +173,22 @@ def test_validate_order_time_in_force(default_conf, mocker, caplog): ex.validate_order_time_in_force(tif2) -@pytest.mark.parametrize("amount,precision,expected", [ - (2.34559, 4, 2.3455), - (2.34559, 5, 2.34559), - (2.34559, 3, 2.345), - (2.9999, 3, 2.999), - (2.9909, 3, 2.990), +@pytest.mark.parametrize("amount,precision_mode,precision,expected", [ + (2.34559, 2, 4, 2.3455), + (2.34559, 2, 5, 2.34559), + (2.34559, 2, 3, 2.345), + (2.9999, 2, 3, 2.999), + (2.9909, 2, 3, 2.990), + # Tests for Tick-size + (2.34559, 4, 0.0001, 2.3455), + (2.34559, 4, 0.00001, 2.34559), + (2.34559, 4, 0.001, 2.345), + (2.9999, 4, 0.001, 2.999), + (2.9909, 4, 0.001, 2.990), + (2.9909, 4, 0.005, 2.990), + (2.9999, 4, 0.005, 2.995), ]) -def test_amount_to_precision_decimal_places(default_conf, mocker, amount, precision, expected): +def test_amount_to_precision(default_conf, mocker, amount, precision_mode, precision, expected): ''' Test rounds down ''' @@ -192,7 +200,8 @@ def test_amount_to_precision_decimal_places(default_conf, mocker, amount, precis # DECIMAL_PLACES = 2 # SIGNIFICANT_DIGITS = 3 # TICK_SIZE = 4 - mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2)) + mocker.patch('freqtrade.exchange.Exchange.precisionMode', + PropertyMock(return_value=precision_mode)) mocker.patch('freqtrade.exchange.Exchange.markets', markets) pair = 'ETH/BTC' From d7957bd7916b4a72e24c94018f0dd0aede45a9f9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Jan 2020 16:04:39 +0100 Subject: [PATCH 272/324] add advanced tests for price_to_precision --- tests/exchange/test_exchange.py | 60 ++++++++++++--------------------- 1 file changed, 22 insertions(+), 38 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index a131aaab5..04cf2ff22 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -208,55 +208,39 @@ def test_amount_to_precision(default_conf, mocker, amount, precision_mode, preci assert exchange.amount_to_precision(pair, amount) == expected -@pytest.mark.parametrize("amount,precision,expected", [ - (2.34559, 0.0001, 2.3455), - (2.34559, 0.00001, 2.34559), - (2.34559, 0.001, 2.345), - (2.9999, 0.001, 2.999), - (2.9909, 0.001, 2.990), - (2.9909, 0.005, 2.990), - (2.9999, 0.005, 2.995), +@pytest.mark.parametrize("price,precision_mode,precision,expected", [ + (2.34559, 2, 4, 2.3456), + (2.34559, 2, 5, 2.34559), + (2.34559, 2, 3, 2.346), + (2.9999, 2, 3, 3.000), + (2.9909, 2, 3, 2.991), + # Tests for Tick_size + (2.34559, 4, 0.0001, 2.3456), + (2.34559, 4, 0.00001, 2.34559), + (2.34559, 4, 0.001, 2.346), + (2.9999, 4, 0.001, 3.000), + (2.9909, 4, 0.001, 2.991), + (2.9909, 4, 0.005, 2.99), + (2.9973, 4, 0.005, 2.995), + (2.9977, 4, 0.005, 3.0), ]) -def test_amount_to_precision_tick_size(default_conf, mocker, amount, precision, expected): +def test_price_to_precision(default_conf, mocker, price, precision_mode, precision, expected): ''' - Test rounds down + Test price to precision ''' - - markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': precision}}}) + markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': precision}}}) exchange = get_patched_exchange(mocker, default_conf, id="binance") + mocker.patch('freqtrade.exchange.Exchange.markets', markets) # digits counting mode # DECIMAL_PLACES = 2 # SIGNIFICANT_DIGITS = 3 # TICK_SIZE = 4 - mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=4)) - mocker.patch('freqtrade.exchange.Exchange.markets', markets) + mocker.patch('freqtrade.exchange.Exchange.precisionMode', + PropertyMock(return_value=precision_mode)) pair = 'ETH/BTC' - assert exchange.amount_to_precision(pair, amount) == expected - - -def test_sprice_to_precision(default_conf, mocker): - ''' - Test rounds up to 4 decimal places - ''' - markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': 4}}}) - - exchange = get_patched_exchange(mocker, default_conf, id="binance") - mocker.patch('freqtrade.exchange.Exchange.markets', markets) - mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2)) - - price = 2.34559 - pair = 'ETH/BTC' - price = exchange.price_to_precision(pair, price) - assert price == 2.3456 - - markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': 0.0001}}}) - mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=4)) - mocker.patch('freqtrade.exchange.Exchange.markets', markets) - - price = exchange.price_to_precision(pair, price) - assert price == 2.3456 + assert exchange.price_to_precision(pair, price) == expected def test_set_sandbox(default_conf, mocker): From bea4ad8effa97626de4ae51d7f0e6c06c963a00f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Jan 2020 20:16:20 +0100 Subject: [PATCH 273/324] Revert price_to_precision to rounding up --- freqtrade/exchange/exchange.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index dd6f257fe..a7243d4d2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -7,6 +7,7 @@ import inspect 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 @@ -14,7 +15,7 @@ import arrow import ccxt import ccxt.async_support as ccxt_async from ccxt.base.decimal_to_precision import (ROUND, ROUND_DOWN, ROUND_UP, - TRUNCATE, decimal_to_precision) + TRUNCATE, TICK_SIZE, decimal_to_precision) from pandas import DataFrame from freqtrade.data.converter import parse_ticker_dataframe @@ -383,15 +384,27 @@ class Exchange: def price_to_precision(self, pair, price: float) -> float: ''' - Returns the price buying or selling with to the precision the Exchange accepts - Reimplementation of ccxt internal methods - ensuring we can test the result is correct - based on our definitions. + Returns the price rounded up to the precision the Exchange accepts. + Partial Reimplementation of ccxt internal method decimal_to_precision(), + which does not support rounding up + TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and + align with amount_to_precision(). + Rounds up ''' if self.markets[pair]['precision']['price']: - price = float(decimal_to_precision(price, rounding_mode=ROUND, - precision=self.markets[pair]['precision']['price'], - counting_mode=self.precisionMode, - )) + # price = float(decimal_to_precision(price, rounding_mode=ROUND, + # precision=self.markets[pair]['precision']['price'], + # counting_mode=self.precisionMode, + # )) + if self.precisionMode == TICK_SIZE: + precision = self.markets[pair]['precision']['price'] + missing = price % precision + if missing != 0: + price = price - missing + precision + else: + symbol_prec = self.markets[pair]['precision']['price'] + big_price = price * pow(10, symbol_prec) + price = ceil(big_price) / pow(10, symbol_prec) return price def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, From 1e58cd70ad1253784a3f6d76997cf090c54362ab Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Jan 2020 20:16:47 +0100 Subject: [PATCH 274/324] Adapt tests to round price up --- tests/exchange/test_exchange.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 04cf2ff22..c3cf7ae58 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -220,9 +220,13 @@ def test_amount_to_precision(default_conf, mocker, amount, precision_mode, preci (2.34559, 4, 0.001, 2.346), (2.9999, 4, 0.001, 3.000), (2.9909, 4, 0.001, 2.991), - (2.9909, 4, 0.005, 2.99), - (2.9973, 4, 0.005, 2.995), + (2.9909, 4, 0.005, 2.995), + (2.9973, 4, 0.005, 3.0), (2.9977, 4, 0.005, 3.0), + (234.43, 4, 0.5, 234.5), + (234.53, 4, 0.5, 235.0), + (0.891534, 4, 0.0001, 0.8916), + ]) def test_price_to_precision(default_conf, mocker, price, precision_mode, precision, expected): ''' @@ -240,7 +244,7 @@ def test_price_to_precision(default_conf, mocker, price, precision_mode, precisi PropertyMock(return_value=precision_mode)) pair = 'ETH/BTC' - assert exchange.price_to_precision(pair, price) == expected + assert pytest.approx(exchange.price_to_precision(pair, price)) == expected def test_set_sandbox(default_conf, mocker): From 4c823f12e3d740e274f80cbff4e6f37ef9e676ac Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Jan 2020 20:25:58 +0100 Subject: [PATCH 275/324] Sort imports --- 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 a7243d4d2..4c5c9194c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -14,8 +14,8 @@ 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, ROUND_DOWN, ROUND_UP, - TRUNCATE, TICK_SIZE, 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.data.converter import parse_ticker_dataframe From 4013701bdb42b4d42dd0ea9db2fed733c187e756 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Jan 2020 06:42:53 +0100 Subject: [PATCH 276/324] allow wallet update to be skipped if the value is fresh enough. Value is NOT configurable, having this wrong can result in bans on the exchange. --- freqtrade/wallets.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py index 54c3f9138..c52767162 100644 --- a/freqtrade/wallets.py +++ b/freqtrade/wallets.py @@ -2,7 +2,10 @@ """ Wallet """ import logging -from typing import Dict, NamedTuple, Any +from typing import Any, Dict, NamedTuple + +import arrow + from freqtrade.exchange import Exchange from freqtrade.persistence import Trade @@ -24,7 +27,7 @@ class Wallets: self._exchange = exchange self._wallets: Dict[str, Wallet] = {} self.start_cap = config['dry_run_wallet'] - + self._last_wallet_refresh = 0 self.update() def get_free(self, currency) -> float: @@ -95,12 +98,21 @@ class Wallets: balances[currency].get('total', None) ) - def update(self) -> None: - if self._config['dry_run']: - self._update_dry() - else: - self._update_live() - logger.info('Wallets synced.') + def update(self, require_update: bool = True) -> None: + """ + Updates wallets from the configured version. + By default, updates from the exchange. + Update-skipping should only be used for user-invoked /balance calls, since + 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 self._config['dry_run']: + self._update_dry() + else: + self._update_live() + logger.info('Wallets synced.') + self._last_wallet_refresh = arrow.utcnow().timestamp def get_all_balances(self) -> Dict[str, Any]: return self._wallets From c8806a16a12fad76485f92e2babbe0a9de25c262 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Jan 2020 06:43:41 +0100 Subject: [PATCH 277/324] Allow wallet update from /balance --- freqtrade/rpc/rpc.py | 2 ++ tests/test_wallets.py | 7 ++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c187dae4f..0469d418d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -306,6 +306,8 @@ class RPC: except (TemporaryError, DependencyException): raise RPCException('Error getting current tickers.') + self._freqtrade.wallets.update(require_update=False) + for coin, balance in self._freqtrade.wallets.get_all_balances().items(): if not balance.total: continue diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 3177edc05..884470014 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -32,7 +32,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 freqtrade.wallets._last_wallet_refresh > 0 mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value={ @@ -61,6 +61,11 @@ def test_sync_wallet_at_boot(mocker, default_conf): assert freqtrade.wallets.get_free('GAS') == 0.270739 assert freqtrade.wallets.get_used('GAS') == 0.1 assert freqtrade.wallets.get_total('GAS') == 0.260439 + update_mock = mocker.patch('freqtrade.wallets.Wallets._update_live') + freqtrade.wallets.update(False) + assert update_mock.call_count == 0 + freqtrade.wallets.update() + assert update_mock.call_count == 1 def test_sync_wallet_missing_data(mocker, default_conf): From f73f0b1653b8914218c326776a4adffa0cf75fa7 Mon Sep 17 00:00:00 2001 From: Tejesh Date: Wed, 15 Jan 2020 19:29:00 +0530 Subject: [PATCH 278/324] Update comments on backtesting --- 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 18cc27ff4..1070f2481 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -296,7 +296,7 @@ class Backtesting: """ # Arguments are long and noisy, so this is commented out. # Uncomment if you need to debug the backtest() method. -# logger.debug(f"Start backtest, args: {args}") + # logger.debug(f"Start backtest, args: {args}") processed = args['processed'] stake_amount = args['stake_amount'] max_open_trades = args.get('max_open_trades', 0) From 6feb68b18da6f1edbdbd9a7cde05e8a262ab5568 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Jan 2020 19:51:33 +0100 Subject: [PATCH 279/324] Change feature sorting to tell more of a story --- docs/index.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/index.md b/docs/index.md index b664552bc..c88c73619 100644 --- a/docs/index.md +++ b/docs/index.md @@ -25,15 +25,15 @@ Freqtrade is a crypto-currency algorithmic trading software developed in python ## Features -- Run: Run the bot on exchange with simulated money (Dry-Run mode) or with real money (Live-Trade mode). -- Select markets: Create your static list or use an automatic one based on top traded volumes and/or prices (not available during backtesting). You can also explicitly blacklist markets you don't want to trade. -- Download market data: Download historical data of the exchange and the markets your may want to trade with. The historical data can be based on [OHLCV](https://en.wikipedia.org/wiki/Open-high-low-close_chart) candles or be trade ticks (for exchanges that support this). -- Strategy: Write your strategy in python, using [pandas](https://pandas.pydata.org/). Example strategies to inspire you are available in the [strategy repository](https://github.com/freqtrade/freqtrade-strategies). +- Develop your Strategy: Write your strategy in python, using [pandas](https://pandas.pydata.org/). Example strategies to inspire you are available in the [strategy repository](https://github.com/freqtrade/freqtrade-strategies). +- Download market data: Download historical data of the exchange and the markets your may want to trade with. - Backtest: Test your strategy on downloaded historical data. - Optimize: Find the best parameters for your strategy using hyperoptimization which employs machining learning methods. You can optimize buy, sell, take profit (ROI), stop-loss and trailing stop-loss parameters for your strategy. +- Select markets: Create your static list or use an automatic one based on top traded volumes and/or prices (not available during backtesting). You can also explicitly blacklist markets you don't want to trade. +- Run: Test your strategy with simulated money (Dry-Run mode) or deploy it with real money (Live-Trade mode). - Run using Edge (optional module): The concept is to find the best historical [trade expectancy](edge.md#expectancy) by markets based on variation of the stop-loss and then allow/reject markets to trade. The sizing of the trade is based on a risk of a percentage of your capital. - Control/Monitor: Use Telegram or a REST API (start/stop the bot, show profit/loss, daily summary, current open trades results, etc.). -- Analyse: Further analysis can be possibilities on either Backtesting data or Freqtrade trading history (SQL database), including automated standard plots, and methods to load the data into [interactive environments](data-analysis.md). +- Analyse: Further analysis can be performed on either Backtesting data or Freqtrade trading history (SQL database), including automated standard plots, and methods to load the data into [interactive environments](data-analysis.md). ## Requirements From d1bf388b0edfdf2cab6d9800cdddf50c9b771654 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Jan 2020 19:56:14 +0100 Subject: [PATCH 280/324] Wallet amount must be compared with >= --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e3f2616a2..a5d980504 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -904,7 +904,7 @@ class FreqtradeBot: """ wallet_amount = self.wallets.get_free(pair.split('/')[0]) logger.debug(f"{pair} - Wallet: {wallet_amount} - Trade-amount: {amount}") - if wallet_amount > amount: + if wallet_amount >= amount: return amount elif wallet_amount > amount * 0.98: logger.info(f"{pair} - Falling back to wallet-amount.") From 90ed4c665bf21f3388f7bd90a16f5f02d5062621 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Jan 2020 19:59:08 +0100 Subject: [PATCH 281/324] Cover equal case via test --- tests/test_freqtradebot.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 58f88198a..eedf79553 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2764,6 +2764,9 @@ def test__safe_sell_amount(default_conf, fee, caplog, mocker): assert freqtrade._safe_sell_amount(trade.pair, trade.amount) == amount_wallet assert log_has_re(r'.*Falling back to wallet-amount.', caplog) + caplog.clear() + assert freqtrade._safe_sell_amount(trade.pair, amount_wallet) == amount_wallet + assert not log_has_re(r'.*Falling back to wallet-amount.', caplog) def test__safe_sell_amount_error(default_conf, fee, caplog, mocker): From 8bcfe4a6aaa370a1dc2f0554c4309d1a713ce152 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Jan 2020 21:01:36 +0100 Subject: [PATCH 282/324] Up log level of safe_sell_amount message --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a5d980504..4db5f08b1 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -903,7 +903,7 @@ class FreqtradeBot: :raise: DependencyException: if available balance is not within 2% of the available amount. """ wallet_amount = self.wallets.get_free(pair.split('/')[0]) - logger.debug(f"{pair} - Wallet: {wallet_amount} - Trade-amount: {amount}") + logger.info(f"Selling {pair} - Wallet: {wallet_amount} - Trade-amount: {amount}") if wallet_amount >= amount: return amount elif wallet_amount > amount * 0.98: From 29a5e4fba15a8f269cacd04ff53937fa78fad01a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Jan 2020 21:52:10 +0100 Subject: [PATCH 283/324] Update wallets before getting amount --- freqtrade/freqtradebot.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4db5f08b1..e712892f1 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -902,15 +902,19 @@ class FreqtradeBot: :return: amount to sell :raise: DependencyException: if available balance is not within 2% of the available amount. """ + # Update wallets to ensure amounts tied up in a stoploss is now free! + self.wallets.update() + wallet_amount = self.wallets.get_free(pair.split('/')[0]) - logger.info(f"Selling {pair} - Wallet: {wallet_amount} - Trade-amount: {amount}") + logger.debug(f"{pair} - Wallet: {wallet_amount} - Trade-amount: {amount}") if wallet_amount >= amount: return amount elif wallet_amount > amount * 0.98: logger.info(f"{pair} - Falling back to wallet-amount.") return wallet_amount else: - raise DependencyException("Not enough amount to sell.") + raise DependencyException( + f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}") def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> None: """ From fa1e9dd70d1e48298ddc0c2603ae4ef351e93e85 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Jan 2020 21:53:04 +0100 Subject: [PATCH 284/324] Adjust tests to allow updating within safe_sell_amount --- tests/rpc/test_rpc.py | 1 + tests/test_freqtradebot.py | 5 +++++ tests/test_integration.py | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index ac959cb0e..36fce1797 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -513,6 +513,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: ), get_fee=fee, ) + mocker.patch('freqtrade.wallets.Wallets.get_free', return_value=1000) freqtradebot = get_patched_freqtradebot(mocker, default_conf) patch_get_signal(freqtradebot, (True, False)) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index eedf79553..1cba39c58 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2750,6 +2750,7 @@ def test__safe_sell_amount(default_conf, fee, caplog, mocker): amount = 95.33 amount_wallet = 95.29 mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet)) + wallet_update = mocker.patch('freqtrade.wallets.Wallets.update') trade = Trade( pair='LTC/ETH', amount=amount, @@ -2762,11 +2763,15 @@ def test__safe_sell_amount(default_conf, fee, caplog, mocker): freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) + wallet_update.reset_mock() assert freqtrade._safe_sell_amount(trade.pair, trade.amount) == amount_wallet assert log_has_re(r'.*Falling back to wallet-amount.', caplog) + assert wallet_update.call_count == 1 caplog.clear() + wallet_update.reset_mock() assert freqtrade._safe_sell_amount(trade.pair, amount_wallet) == amount_wallet assert not log_has_re(r'.*Falling back to wallet-amount.', caplog) + assert wallet_update.call_count == 1 def test__safe_sell_amount_error(default_conf, fee, caplog, mocker): diff --git a/tests/test_integration.py b/tests/test_integration.py index 98bf1862b..ad3e897f8 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -97,8 +97,8 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee, # Only order for 3rd trade needs to be cancelled assert cancel_order_mock.call_count == 1 - # Wallets should only be called once per sell cycle - assert wallets_mock.call_count == 1 + # Wallets must be updated between stoploss cancellation and selling. + assert wallets_mock.call_count == 2 trade = trades[0] assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value From 9474cb17922ac4b6a0969bf1617a42c1caa210d0 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2020 07:48:46 +0000 Subject: [PATCH 285/324] Bump ccxt from 1.21.56 to 1.21.76 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.21.56 to 1.21.76. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/1.21.56...1.21.76) 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 15a9d687f..daf4984c0 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.21.56 +ccxt==1.21.76 SQLAlchemy==1.3.12 python-telegram-bot==12.3.0 arrow==0.15.5 From 8d4515935ae9ac849798a7111b4aa3bff0a39802 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2020 07:49:18 +0000 Subject: [PATCH 286/324] Bump pytest from 5.3.2 to 5.3.3 Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.3.2 to 5.3.3. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/5.3.2...5.3.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 16f9baf95..e602bf184 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.7.9 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.0.0 mypy==0.761 -pytest==5.3.2 +pytest==5.3.3 pytest-asyncio==0.10.0 pytest-cov==2.8.1 pytest-mock==2.0.0 From 6e3336cb30aee9a279d9fda3659695e916d36560 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Jan 2020 20:10:06 +0100 Subject: [PATCH 287/324] Adapt test to verify behaviour of stoploss_on_exchange in dry-run --- tests/test_freqtradebot.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5a4820f2f..6c6bd1753 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1318,6 +1318,14 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog, rate=0.00002344 * 0.95 * 0.99, stop_price=0.00002344 * 0.95) + # price fell below stoploss, so dry-run sells trade. + mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ + 'bid': 0.00002144, + 'ask': 0.00002146, + 'last': 0.00002144 + })) + assert freqtrade.handle_trade(trade) is True + def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog, limit_buy_order, limit_sell_order) -> None: From 099bbc5c7f56df2ce193c42c60c99c7c45ea7b85 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Jan 2020 20:14:40 +0100 Subject: [PATCH 288/324] Fix bug when stoploss_on_exchange in combination with dry-run does not sell orders --- freqtrade/strategy/interface.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 7bd6a9ac5..27bc8280e 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -389,9 +389,11 @@ class IStrategy(ABC): trade.adjust_stop_loss(high or current_rate, stop_loss_value) # 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 - (not self.order_types.get('stoploss_on_exchange'))): + (not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])): sell_type = SellType.STOP_LOSS From 1bf475fa1a1f5281f01b1b6a8ab3104f7553c1e8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 20 Jan 2020 20:24:40 +0100 Subject: [PATCH 289/324] Remove .get calls for dry_run - it's a mandatory property --- freqtrade/freqtradebot.py | 5 ++--- freqtrade/rpc/rpc.py | 6 +++--- freqtrade/rpc/rpc_manager.py | 2 +- tests/test_configuration.py | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e712892f1..acff2b2ad 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -63,8 +63,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.get('dry_run', False)) + persistence.init(self.config.get('db_url', None), clean_open_orders=self.config['dry_run']) self.wallets = Wallets(self.config, self.exchange) @@ -930,7 +929,7 @@ class FreqtradeBot: # if stoploss is on exchange and we are on dry_run mode, # we consider the sell price stop price - if self.config.get('dry_run', False) and sell_type == 'stoploss' \ + if self.config['dry_run'] and sell_type == 'stoploss' \ and self.strategy.order_types['stoploss_on_exchange']: limit = trade.stop_loss diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 0469d418d..d58b99f39 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -88,7 +88,7 @@ class RPC: """ config = self._freqtrade.config val = { - 'dry_run': config.get('dry_run', False), + 'dry_run': config['dry_run'], 'stake_currency': config['stake_currency'], 'stake_amount': config['stake_amount'], 'minimal_roi': config['minimal_roi'].copy(), @@ -337,7 +337,7 @@ class RPC: 'stake': stake_currency, }) if total == 0.0: - if self._freqtrade.config.get('dry_run', False): + if self._freqtrade.config['dry_run']: raise RPCException('Running in Dry Run, balances are not available.') else: raise RPCException('All balances are zero.') @@ -351,7 +351,7 @@ class RPC: 'symbol': symbol, 'value': value, 'stake': stake_currency, - 'note': 'Simulated balances' if self._freqtrade.config.get('dry_run', False) else '' + 'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else '' } def _rpc_start(self) -> Dict[str, str]: diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index cb9e697e9..f687fe4d1 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -62,7 +62,7 @@ class RPCManager: logger.error(f"Message type {msg['type']} not implemented by handler {mod.name}.") def startup_messages(self, config, pairlist) -> None: - if config.get('dry_run', False): + if config['dry_run']: self.send_msg({ 'type': RPCMessageType.WARNING_NOTIFICATION, 'status': 'Dry run is enabled. All trades are simulated.' diff --git a/tests/test_configuration.py b/tests/test_configuration.py index a4d4c4abc..cbcd6416a 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -323,7 +323,7 @@ def test_load_dry_run(default_conf, mocker, config_value, expected, arglist) -> configuration = Configuration(Arguments(arglist).get_parsed_arg()) validated_conf = configuration.load_config() - assert validated_conf.get('dry_run') is expected + assert validated_conf['dry_run'] is expected def test_load_custom_strategy(default_conf, mocker) -> None: From c1c2717bc9a9216c042a7bec2efe94c15bfc4f18 Mon Sep 17 00:00:00 2001 From: Daniel Goller Date: Tue, 21 Jan 2020 15:49:24 +0000 Subject: [PATCH 290/324] added missing word in hyperopt loss example --- freqtrade/templates/sample_hyperopt_loss.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/templates/sample_hyperopt_loss.py b/freqtrade/templates/sample_hyperopt_loss.py index 5a2fb72b6..ee2c28bc9 100644 --- a/freqtrade/templates/sample_hyperopt_loss.py +++ b/freqtrade/templates/sample_hyperopt_loss.py @@ -27,7 +27,7 @@ class SampleHyperOptLoss(IHyperOptLoss): Defines the default loss function for hyperopt This is intended to give you some inspiration for your own loss function. - The Function needs to return a number (float) - which becomes for better backtest results. + The Function needs to return a number (float) - which becomes smaller for better backtest results. """ @staticmethod From bff0a0953794c1de68c582c8ba50a6f25845086f Mon Sep 17 00:00:00 2001 From: Daniel Goller Date: Tue, 21 Jan 2020 16:14:19 +0000 Subject: [PATCH 291/324] line was too long --- freqtrade/templates/sample_hyperopt_loss.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/templates/sample_hyperopt_loss.py b/freqtrade/templates/sample_hyperopt_loss.py index ee2c28bc9..4173d97f5 100644 --- a/freqtrade/templates/sample_hyperopt_loss.py +++ b/freqtrade/templates/sample_hyperopt_loss.py @@ -27,7 +27,8 @@ class SampleHyperOptLoss(IHyperOptLoss): Defines the default loss function for hyperopt This is intended to give you some inspiration for your own loss function. - The Function needs to return a number (float) - which becomes smaller for better backtest results. + The Function needs to return a number (float) - which becomes smaller for better backtest + results. """ @staticmethod From 7d2d0235a0407b564038f02b4834ab365cc72f8a Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Jan 2020 06:08:34 +0100 Subject: [PATCH 292/324] Fix typo in sell-reason table generation --- freqtrade/optimize/optimize_reports.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/optimize_reports.py b/freqtrade/optimize/optimize_reports.py index 3801546b1..67056eaa9 100644 --- a/freqtrade/optimize/optimize_reports.py +++ b/freqtrade/optimize/optimize_reports.py @@ -70,7 +70,7 @@ def generate_text_table_sell_reason(data: Dict[str, Dict], results: DataFrame) - for reason, count in results['sell_reason'].value_counts().iteritems(): result = results.loc[results['sell_reason'] == reason] profit = len(result[result['profit_abs'] >= 0]) - loss = len(result[results['profit_abs'] < 0]) + loss = len(result[result['profit_abs'] < 0]) profit_mean = round(result['profit_percent'].mean() * 100.0, 2) tabular_data.append([reason.value, count, profit, loss, profit_mean]) return tabulate(tabular_data, headers=headers, tablefmt="pipe") From e13045b599f921e749e494dd49b13cc00d1d9e57 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Jan 2020 06:17:13 +0100 Subject: [PATCH 293/324] upgrade pip in windows environment --- build_helpers/install_windows.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/build_helpers/install_windows.ps1 b/build_helpers/install_windows.ps1 index 30427c3cc..138fba208 100644 --- a/build_helpers/install_windows.ps1 +++ b/build_helpers/install_windows.ps1 @@ -2,6 +2,7 @@ # Downloaded from https://www.lfd.uci.edu/~gohlke/pythonlibs/#ta-lib # Invoke-WebRequest -Uri "https://download.lfd.uci.edu/pythonlibs/xxxxxxx/TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl" -OutFile "TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl" +python -m pip install --upgrade pip pip install build_helpers\TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl pip install -r requirements-dev.txt From 8a940eb0c1d7cc15bb7cc559875259f819dc2ad2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Jan 2020 14:46:28 +0100 Subject: [PATCH 294/324] Align price finding function name on buy side with get_sell_rate --- freqtrade/freqtradebot.py | 4 ++-- tests/test_freqtradebot.py | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index acff2b2ad..387ddb063 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -218,7 +218,7 @@ class FreqtradeBot: return trades_created - def get_target_bid(self, pair: str, tick: Dict = None) -> float: + def get_buy_rate(self, pair: str, tick: Dict = None) -> float: """ Calculates bid target between current ask price and last price :return: float: Price @@ -435,7 +435,7 @@ class FreqtradeBot: buy_limit_requested = price else: # Calculate price - buy_limit_requested = self.get_target_bid(pair) + buy_limit_requested = self.get_buy_rate(pair) 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: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 6c6bd1753..d00fab9c7 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -912,7 +912,7 @@ def test_balance_fully_ask_side(mocker, default_conf) -> None: mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={'ask': 20, 'last': 10})) - assert freqtrade.get_target_bid('ETH/BTC') == 20 + assert freqtrade.get_buy_rate('ETH/BTC') == 20 def test_balance_fully_last_side(mocker, default_conf) -> None: @@ -921,7 +921,7 @@ def test_balance_fully_last_side(mocker, default_conf) -> None: mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={'ask': 20, 'last': 10})) - assert freqtrade.get_target_bid('ETH/BTC') == 10 + assert freqtrade.get_buy_rate('ETH/BTC') == 10 def test_balance_bigger_last_ask(mocker, default_conf) -> None: @@ -929,7 +929,7 @@ def test_balance_bigger_last_ask(mocker, default_conf) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={'ask': 5, 'last': 10})) - assert freqtrade.get_target_bid('ETH/BTC') == 5 + assert freqtrade.get_buy_rate('ETH/BTC') == 5 def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: @@ -938,10 +938,10 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: freqtrade = FreqtradeBot(default_conf) stake_amount = 2 bid = 0.11 - get_bid = MagicMock(return_value=bid) + buy_rate_mock = MagicMock(return_value=bid) mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', - get_target_bid=get_bid, + 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']}) @@ -958,7 +958,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: pair = 'ETH/BTC' assert freqtrade.execute_buy(pair, stake_amount) - assert get_bid.call_count == 1 + assert buy_rate_mock.call_count == 1 assert buy_mm.call_count == 1 call_args = buy_mm.call_args_list[0][1] assert call_args['pair'] == pair @@ -975,8 +975,8 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: # Test calling with price fix_price = 0.06 assert freqtrade.execute_buy(pair, stake_amount, fix_price) - # Make sure get_target_bid wasn't called again - assert get_bid.call_count == 1 + # Make sure get_buy_rate wasn't called again + assert buy_rate_mock.call_count == 1 assert buy_mm.call_count == 2 call_args = buy_mm.call_args_list[1][1] @@ -3500,7 +3500,7 @@ def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_o def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2) -> None: """ - test if function get_target_bid will return the order book price + test if function get_buy_rate will return the order book price instead of the ask rate """ patch_exchange(mocker) @@ -3518,13 +3518,13 @@ def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2) -> None: default_conf['telegram']['enabled'] = False freqtrade = FreqtradeBot(default_conf) - assert freqtrade.get_target_bid('ETH/BTC') == 0.043935 + assert freqtrade.get_buy_rate('ETH/BTC') == 0.043935 assert ticker_mock.call_count == 0 def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2) -> None: """ - test if function get_target_bid will return the ask rate (since its value is lower) + test if function get_buy_rate will return the ask rate (since its value is lower) instead of the order book rate (even if enabled) """ patch_exchange(mocker) @@ -3543,7 +3543,7 @@ def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2) -> None: freqtrade = FreqtradeBot(default_conf) # orderbook shall be used even if tickers would be lower. - assert freqtrade.get_target_bid('ETH/BTC') != 0.042 + assert freqtrade.get_buy_rate('ETH/BTC') != 0.042 assert ticker_mock.call_count == 0 From f36bc80ad12a9784e2d20a9b9b9a7d10e2d62208 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Jan 2020 19:43:02 +0100 Subject: [PATCH 295/324] Add parametrized tests for get_buy_rate --- tests/test_freqtradebot.py | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index d00fab9c7..e0f2ecd3a 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -906,30 +906,22 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None: assert ("ETH/BTC", default_conf["ticker_interval"]) in refresh_mock.call_args[0][0] -def test_balance_fully_ask_side(mocker, default_conf) -> None: - default_conf['bid_strategy']['ask_last_balance'] = 0.0 +@pytest.mark.parametrize("ask,last,last_ab,expected", [ + (20, 10, 0.0, 20), # Full ask side + (20, 10, 1.0, 10), # Full last side + (20, 10, 0.5, 15), # Between ask and last + (20, 10, 0.7, 13), # Between ask and last + (20, 10, 0.3, 17), # Between ask and last + (5, 10, 1.0, 5), # last bigger than ask + (5, 10, 0.5, 5), # last bigger than ask +]) +def test_get_buy_rate(mocker, default_conf, ask, last, last_ab, expected) -> None: + default_conf['bid_strategy']['ask_last_balance'] = last_ab freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - MagicMock(return_value={'ask': 20, 'last': 10})) + MagicMock(return_value={'ask': ask, 'last': last})) - assert freqtrade.get_buy_rate('ETH/BTC') == 20 - - -def test_balance_fully_last_side(mocker, default_conf) -> None: - default_conf['bid_strategy']['ask_last_balance'] = 1.0 - freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - MagicMock(return_value={'ask': 20, 'last': 10})) - - assert freqtrade.get_buy_rate('ETH/BTC') == 10 - - -def test_balance_bigger_last_ask(mocker, default_conf) -> None: - default_conf['bid_strategy']['ask_last_balance'] = 1.0 - freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', - MagicMock(return_value={'ask': 5, 'last': 10})) - assert freqtrade.get_buy_rate('ETH/BTC') == 5 + assert freqtrade.get_buy_rate('ETH/BTC') == expected def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None: From 58ceda4b903172d153d34337c9bb4f72a2dda4dc Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Jan 2020 19:54:55 +0100 Subject: [PATCH 296/324] update wallets after forcesell --- freqtrade/rpc/rpc.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index d58b99f39..c38f36c1d 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -425,6 +425,7 @@ class RPC: for trade in Trade.get_open_trades(): _exec_forcesell(trade) Trade.session.flush() + self._freqtrade.wallets.update() return {'result': 'Created sell orders for all open trades.'} # Query for trade @@ -437,6 +438,7 @@ class RPC: _exec_forcesell(trade) Trade.session.flush() + self._freqtrade.wallets.update() return {'result': f'Created sell order for trade {trade_id}.'} def _rpc_forcebuy(self, pair: str, price: Optional[float]) -> Optional[Trade]: From aad10ceee35303370c4a01a1b88e92691b01b615 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Jan 2020 20:50:09 +0100 Subject: [PATCH 297/324] Add threading lock object for /forcesell Protects against stoploss_on_exchange order recreation in case of /forcesell (it's a timing issue, so may or may not happen). --- freqtrade/freqtradebot.py | 16 +++++++++++----- freqtrade/rpc/rpc.py | 37 +++++++++++++++++++------------------ 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 387ddb063..e3856e200 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -7,6 +7,7 @@ import traceback from datetime import datetime from math import isclose from os import getpid +from threading import Lock from typing import Any, Dict, List, Optional, Tuple import arrow @@ -27,7 +28,6 @@ from freqtrade.state import State from freqtrade.strategy.interface import IStrategy, SellType from freqtrade.wallets import Wallets - logger = logging.getLogger(__name__) @@ -92,6 +92,8 @@ class FreqtradeBot: # the initial state of the bot. # Keep this at the end of this initialization method. self.rpc: RPCManager = RPCManager(self) + # Protect sell-logic from forcesell and viceversa + self._sell_lock = Lock() def cleanup(self) -> None: """ @@ -132,8 +134,12 @@ class FreqtradeBot: self.dataprovider.refresh(self._create_pair_whitelist(self.active_pair_whitelist), self.strategy.informative_pairs()) - # First process current opened trades (positions) - self.exit_positions(trades) + # Protect from collisions with forcesell. + # Without this, freqtrade my try to recreate stoploss_on_exchange orders + # while selling is in process, since telegram messages arrive in an different thread. + with self._sell_lock: + # First process current opened trades (positions) + self.exit_positions(trades) # Then looking for buy opportunities if self.get_free_open_trades(): @@ -748,8 +754,8 @@ class FreqtradeBot: Check and execute sell """ should_sell = self.strategy.should_sell( - trade, sell_rate, datetime.utcnow(), buy, sell, - force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 + trade, sell_rate, datetime.utcnow(), buy, sell, + force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 ) if should_sell.sell_flag: diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index c38f36c1d..41097c211 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -420,26 +420,27 @@ class RPC: if self._freqtrade.state != State.RUNNING: raise RPCException('trader is not running') - if trade_id == 'all': - # Execute sell for all open orders - for trade in Trade.get_open_trades(): - _exec_forcesell(trade) + with self._freqtrade._sell_lock: + if trade_id == 'all': + # Execute sell for all open orders + for trade in Trade.get_open_trades(): + _exec_forcesell(trade) + Trade.session.flush() + self._freqtrade.wallets.update() + return {'result': 'Created sell orders for all open trades.'} + + # Query for trade + trade = Trade.get_trades( + trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ] + ).first() + if not trade: + logger.warning('forcesell: Invalid argument received') + raise RPCException('invalid argument') + + _exec_forcesell(trade) Trade.session.flush() self._freqtrade.wallets.update() - return {'result': 'Created sell orders for all open trades.'} - - # Query for trade - trade = Trade.get_trades( - trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ] - ).first() - if not trade: - logger.warning('forcesell: Invalid argument received') - raise RPCException('invalid argument') - - _exec_forcesell(trade) - Trade.session.flush() - self._freqtrade.wallets.update() - return {'result': f'Created sell order for trade {trade_id}.'} + return {'result': f'Created sell order for trade {trade_id}.'} def _rpc_forcebuy(self, pair: str, price: Optional[float]) -> Optional[Trade]: """ From f4c7edf551442f369c5f451d5cd66000f196d728 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 14 Dec 2019 02:12:16 +0300 Subject: [PATCH 298/324] No args for backtest(), use arguments --- freqtrade/optimize/backtesting.py | 53 +++++++++++++++---------------- freqtrade/optimize/hyperopt.py | 14 ++++---- 2 files changed, 32 insertions(+), 35 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 1070f2481..92129b324 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -279,30 +279,28 @@ class Backtesting: return bt_res return None - def backtest(self, args: Dict) -> DataFrame: + def backtest(self, processed: Dict, stake_amount: int, + start_date, end_date, + max_open_trades: int = 0, position_stacking: bool = False) -> DataFrame: """ - Implements backtesting functionality + Implement backtesting functionality NOTE: This method is used by Hyperopt at each iteration. Please keep it optimized. Of course try to not have ugly code. By some accessor are sometime slower than functions. - Avoid, logging on this method + Avoid extensive logging in this method and functions it calls. - :param args: a dict containing: - stake_amount: btc amount to use for each trade - processed: a processed dictionary with format {pair, data} - max_open_trades: maximum number of concurrent trades (default: 0, disabled) - position_stacking: do we allow position stacking? (default: False) - :return: DataFrame + :param processed: a processed dictionary with format {pair, data} + :param stake_amount: amount to use for each trade + :param start_date: backtesting timerange start datetime + :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? + :return: DataFrame with trades (results of backtesting) """ - # Arguments are long and noisy, so this is commented out. - # Uncomment if you need to debug the backtest() method. - # logger.debug(f"Start backtest, args: {args}") - processed = args['processed'] - stake_amount = args['stake_amount'] - max_open_trades = args.get('max_open_trades', 0) - position_stacking = args.get('position_stacking', False) - start_date = args['start_date'] - end_date = args['end_date'] + logger.debug(f"Run backtest, stake_amount: {stake_amount}, " + f"start_date: {start_date}, end_date: {end_date}, " + f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}" + ) trades = [] trade_count_lock: Dict = {} @@ -369,18 +367,21 @@ class Backtesting: def start(self) -> None: """ - Run a backtesting end-to-end + Run backtesting end-to-end :return: None """ data: Dict[str, Any] = {} + 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() @@ -403,14 +404,12 @@ class Backtesting: ) # Execute backtest and print results all_results[self.strategy.get_strategy_name()] = self.backtest( - { - 'stake_amount': self.config.get('stake_amount'), - 'processed': preprocessed, - 'max_open_trades': max_open_trades, - 'position_stacking': self.config.get('position_stacking', False), - 'start_date': min_date, - 'end_date': max_date, - } + processed=preprocessed, + stake_amount=self.config.get('stake_amount'), + start_date=min_date, + end_date=max_date, + max_open_trades=max_open_trades, + position_stacking=position_stacking, ) for strategy, results in all_results.items(): diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index a8f1a71ef..525f491f3 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -372,14 +372,12 @@ class Hyperopt: min_date, max_date = get_timerange(processed) backtesting_results = self.backtesting.backtest( - { - 'stake_amount': self.config['stake_amount'], - 'processed': processed, - 'max_open_trades': self.max_open_trades, - 'position_stacking': self.position_stacking, - 'start_date': min_date, - 'end_date': max_date, - } + processed=processed, + stake_amount=self.config['stake_amount'], + start_date=min_date, + end_date=max_date, + max_open_trades=self.max_open_trades, + position_stacking=self.position_stacking, ) return self._get_results_dict(backtesting_results, min_date, max_date, params_dict, params_details) From 52f0ed53109e1a4aa8429510321b4a6fe1f08660 Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Sat, 14 Dec 2019 02:12:44 +0300 Subject: [PATCH 299/324] Adjust tests --- tests/optimize/test_backtest_detail.py | 12 ++--- tests/optimize/test_backtesting.py | 73 ++++++++++++-------------- 2 files changed, 38 insertions(+), 47 deletions(-) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 47cb9f353..bd2765430 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -382,13 +382,11 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: data_processed = {pair: frame.copy()} min_date, max_date = get_timerange({pair: frame}) results = backtesting.backtest( - { - 'stake_amount': default_conf['stake_amount'], - 'processed': data_processed, - 'max_open_trades': 10, - 'start_date': min_date, - 'end_date': max_date, - } + processed=data_processed, + stake_amount=default_conf['stake_amount'], + start_date=min_date, + end_date=max_date, + max_open_trades=10, ) assert len(results) == len(data.trades) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 83d212e3d..acbc44e21 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -103,14 +103,12 @@ def simple_backtest(config, contour, num_results, mocker, testdatadir) -> None: min_date, max_date = get_timerange(processed) assert isinstance(processed, dict) results = backtesting.backtest( - { - 'stake_amount': config['stake_amount'], - 'processed': processed, - 'max_open_trades': 1, - 'position_stacking': False, - 'start_date': min_date, - 'end_date': max_date, - } + processed=processed, + stake_amount=config['stake_amount'], + start_date=min_date, + end_date=max_date, + max_open_trades=1, + position_stacking=False, ) # results :: assert len(results) == num_results @@ -132,7 +130,7 @@ def _load_pair_as_ticks(pair, tickfreq): # FIX: fixturize this? -def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC', record=None): +def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC'): data = history.load_data(datadir=datadir, timeframe='1m', pairs=[pair]) data = trim_dictlist(data, -201) patch_exchange(mocker) @@ -140,13 +138,12 @@ def _make_backtest_conf(mocker, datadir, conf=None, pair='UNITTEST/BTC', record= processed = backtesting.strategy.tickerdata_to_dataframe(data) min_date, max_date = get_timerange(processed) return { - 'stake_amount': conf['stake_amount'], 'processed': processed, - 'max_open_trades': 10, - 'position_stacking': False, - 'record': record, + 'stake_amount': conf['stake_amount'], 'start_date': min_date, 'end_date': max_date, + 'max_open_trades': 10, + 'position_stacking': False, } @@ -422,14 +419,12 @@ def test_backtest(default_conf, fee, mocker, testdatadir) -> None: data_processed = backtesting.strategy.tickerdata_to_dataframe(data) min_date, max_date = get_timerange(data_processed) results = backtesting.backtest( - { - 'stake_amount': default_conf['stake_amount'], - 'processed': data_processed, - 'max_open_trades': 10, - 'position_stacking': False, - 'start_date': min_date, - 'end_date': max_date, - } + processed=data_processed, + stake_amount=default_conf['stake_amount'], + start_date=min_date, + end_date=max_date, + max_open_trades=10, + position_stacking=False, ) assert not results.empty assert len(results) == 2 @@ -478,14 +473,12 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker, testdatadir) - processed = backtesting.strategy.tickerdata_to_dataframe(data) min_date, max_date = get_timerange(processed) results = backtesting.backtest( - { - 'stake_amount': default_conf['stake_amount'], - 'processed': processed, - 'max_open_trades': 1, - 'position_stacking': False, - 'start_date': min_date, - 'end_date': max_date, - } + processed=processed, + stake_amount=default_conf['stake_amount'], + start_date=min_date, + end_date=max_date, + max_open_trades=1, + position_stacking=False, ) assert not results.empty assert len(results) == 1 @@ -525,7 +518,7 @@ def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): backtesting = Backtesting(default_conf) backtesting.strategy.advise_buy = fun # Override backtesting.strategy.advise_sell = fun # Override - results = backtesting.backtest(backtest_conf) + results = backtesting.backtest(**backtest_conf) assert results.empty @@ -540,7 +533,7 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir): backtesting = Backtesting(default_conf) backtesting.strategy.advise_buy = fun # Override backtesting.strategy.advise_sell = fun # Override - results = backtesting.backtest(backtest_conf) + results = backtesting.backtest(**backtest_conf) assert results.empty @@ -553,7 +546,7 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): backtesting = Backtesting(default_conf) backtesting.strategy.advise_buy = _trend_alternate # Override backtesting.strategy.advise_sell = _trend_alternate # Override - results = backtesting.backtest(backtest_conf) + results = backtesting.backtest(**backtest_conf) backtesting._store_backtest_result("test_.json", results) # 200 candles in backtest data # won't buy on first (shifted by 1) @@ -598,15 +591,15 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) data_processed = backtesting.strategy.tickerdata_to_dataframe(data) min_date, max_date = get_timerange(data_processed) backtest_conf = { - 'stake_amount': default_conf['stake_amount'], 'processed': data_processed, - 'max_open_trades': 3, - 'position_stacking': False, + 'stake_amount': default_conf['stake_amount'], 'start_date': min_date, 'end_date': max_date, + 'max_open_trades': 3, + 'position_stacking': False, } - results = backtesting.backtest(backtest_conf) + results = backtesting.backtest(**backtest_conf) # Make sure we have parallel trades assert len(evaluate_result_multi(results, '5m', 2)) > 0 @@ -614,14 +607,14 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) assert len(evaluate_result_multi(results, '5m', 3)) == 0 backtest_conf = { - 'stake_amount': default_conf['stake_amount'], 'processed': data_processed, - 'max_open_trades': 1, - 'position_stacking': False, + 'stake_amount': default_conf['stake_amount'], 'start_date': min_date, 'end_date': max_date, + 'max_open_trades': 1, + 'position_stacking': False, } - results = backtesting.backtest(backtest_conf) + results = backtesting.backtest(**backtest_conf) assert len(evaluate_result_multi(results, '5m', 1)) == 0 From bd4dd8403b0b6223bff4ef494f28c15c506caf75 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Jan 2020 12:48:13 +0100 Subject: [PATCH 300/324] Fix type-errors with stake_amount --- freqtrade/optimize/backtesting.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 92129b324..cdf74f65f 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -279,7 +279,7 @@ class Backtesting: return bt_res return None - def backtest(self, processed: Dict, stake_amount: int, + def backtest(self, processed: Dict, stake_amount: float, start_date, end_date, max_open_trades: int = 0, position_stacking: bool = False) -> DataFrame: """ @@ -300,7 +300,7 @@ class Backtesting: logger.debug(f"Run backtest, stake_amount: {stake_amount}, " f"start_date: {start_date}, end_date: {end_date}, " f"max_open_trades: {max_open_trades}, position_stacking: {position_stacking}" - ) + ) trades = [] trade_count_lock: Dict = {} @@ -405,7 +405,7 @@ class Backtesting: # Execute backtest and print results all_results[self.strategy.get_strategy_name()] = self.backtest( processed=preprocessed, - stake_amount=self.config.get('stake_amount'), + stake_amount=self.config['stake_amount'], start_date=min_date, end_date=max_date, max_open_trades=max_open_trades, From a3ac05cc16f3b9fd684fd0330f9b5e3191a34d4a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Jan 2020 13:38:13 +0100 Subject: [PATCH 301/324] Fix missed mock --- tests/test_utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index e145a8470..7ca2cac5a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -665,11 +665,11 @@ def test_start_list_strategies(mocker, caplog, capsys): assert "DefaultStrategy" in captured.out -def test_start_test_pairlist(mocker, caplog, markets, tickers, default_conf, capsys): +def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys): + patch_exchange(mocker, mock_markets=True) mocker.patch.multiple('freqtrade.exchange.Exchange', - markets=PropertyMock(return_value=markets), exchange_has=MagicMock(return_value=True), - get_tickers=tickers + get_tickers=tickers, ) default_conf['pairlists'] = [ From 3f2542fcbcf185ac61668f1394c781da5c7ef421 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Jan 2020 10:44:42 +0100 Subject: [PATCH 302/324] Move edge-module out of __init__.py --- freqtrade/edge/__init__.py | 465 +---------------------------- freqtrade/edge/edge_positioning.py | 464 ++++++++++++++++++++++++++++ 2 files changed, 465 insertions(+), 464 deletions(-) create mode 100644 freqtrade/edge/edge_positioning.py diff --git a/freqtrade/edge/__init__.py b/freqtrade/edge/__init__.py index 15883357b..d275a80e3 100644 --- a/freqtrade/edge/__init__.py +++ b/freqtrade/edge/__init__.py @@ -1,464 +1 @@ -# pragma pylint: disable=W0603 -""" Edge positioning package """ -import logging -from typing import Any, Dict, NamedTuple - -import arrow -import numpy as np -import utils_find_1st as utf1st -from pandas import DataFrame - -from freqtrade import constants -from freqtrade.configuration import TimeRange -from freqtrade.data import history -from freqtrade.exceptions import OperationalException -from freqtrade.strategy.interface import SellType - -logger = logging.getLogger(__name__) - - -class PairInfo(NamedTuple): - stoploss: float - winrate: float - risk_reward_ratio: float - required_risk_reward: float - expectancy: float - nb_trades: int - avg_trade_duration: float - - -class Edge: - """ - Calculates Win Rate, Risk Reward Ratio, Expectancy - against historical data for a give set of markets and a strategy - it then adjusts stoploss and position size accordingly - and force it into the strategy - Author: https://github.com/mishaker - """ - - config: Dict = {} - _cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs - - def __init__(self, config: Dict[str, Any], exchange, strategy) -> None: - - self.config = config - self.exchange = exchange - self.strategy = strategy - - self.edge_config = self.config.get('edge', {}) - self._cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs - self._final_pairs: list = [] - - # checking max_open_trades. it should be -1 as with Edge - # the number of trades is determined by position size - if self.config['max_open_trades'] != float('inf'): - logger.critical('max_open_trades should be -1 in config !') - - if self.config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT: - raise OperationalException('Edge works only with unlimited stake amount') - - # Deprecated capital_available_percentage. Will use tradable_balance_ratio in the future. - self._capital_percentage: float = self.edge_config.get( - 'capital_available_percentage', self.config['tradable_balance_ratio']) - self._allowed_risk: float = self.edge_config.get('allowed_risk') - self._since_number_of_days: int = self.edge_config.get('calculate_since_number_of_days', 14) - self._last_updated: int = 0 # Timestamp of pairs last updated time - self._refresh_pairs = True - - self._stoploss_range_min = float(self.edge_config.get('stoploss_range_min', -0.01)) - self._stoploss_range_max = float(self.edge_config.get('stoploss_range_max', -0.05)) - self._stoploss_range_step = float(self.edge_config.get('stoploss_range_step', -0.001)) - - # calculating stoploss range - self._stoploss_range = np.arange( - self._stoploss_range_min, - self._stoploss_range_max, - self._stoploss_range_step - ) - - self._timerange: TimeRange = TimeRange.parse_timerange("%s-" % arrow.now().shift( - days=-1 * self._since_number_of_days).format('YYYYMMDD')) - if config.get('fee'): - self.fee = config['fee'] - else: - self.fee = self.exchange.get_fee(symbol=self.config['exchange']['pair_whitelist'][0]) - - def calculate(self) -> bool: - pairs = self.config['exchange']['pair_whitelist'] - heartbeat = self.edge_config.get('process_throttle_secs') - - if (self._last_updated > 0) and ( - self._last_updated + heartbeat > arrow.utcnow().timestamp): - return False - - data: Dict[str, Any] = {} - logger.info('Using stake_currency: %s ...', self.config['stake_currency']) - logger.info('Using local backtesting data (using whitelist in given config) ...') - - if self._refresh_pairs: - history.refresh_data( - datadir=self.config['datadir'], - pairs=pairs, - exchange=self.exchange, - timeframe=self.strategy.ticker_interval, - timerange=self._timerange, - ) - - data = history.load_data( - datadir=self.config['datadir'], - pairs=pairs, - timeframe=self.strategy.ticker_interval, - timerange=self._timerange, - startup_candles=self.strategy.startup_candle_count, - ) - - if not data: - # Reinitializing cached pairs - self._cached_pairs = {} - logger.critical("No data found. Edge is stopped ...") - return False - - preprocessed = self.strategy.tickerdata_to_dataframe(data) - - # Print timeframe - min_date, max_date = history.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 - ) - headers = ['date', 'buy', 'open', 'close', 'sell', 'high', 'low'] - - trades: list = [] - for pair, pair_data in preprocessed.items(): - # Sorting dataframe by date and reset index - pair_data = pair_data.sort_values(by=['date']) - pair_data = pair_data.reset_index(drop=True) - - ticker_data = self.strategy.advise_sell( - self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() - - trades += self._find_trades_for_stoploss_range(ticker_data, pair, self._stoploss_range) - - # If no trade found then exit - if len(trades) == 0: - logger.info("No trades found.") - return False - - # 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 - - return True - - def stake_amount(self, pair: str, free_capital: float, - total_capital: float, capital_in_trade: float) -> float: - stoploss = self.stoploss(pair) - available_capital = (total_capital + capital_in_trade) * self._capital_percentage - allowed_capital_at_risk = available_capital * self._allowed_risk - max_position_size = abs(allowed_capital_at_risk / stoploss) - position_size = min(max_position_size, free_capital) - if pair in self._cached_pairs: - logger.info( - 'winrate: %s, expectancy: %s, position size: %s, pair: %s,' - ' capital in trade: %s, free capital: %s, total capital: %s,' - ' stoploss: %s, available capital: %s.', - self._cached_pairs[pair].winrate, - self._cached_pairs[pair].expectancy, - position_size, pair, - capital_in_trade, free_capital, total_capital, - stoploss, available_capital - ) - return round(position_size, 15) - - def stoploss(self, pair: str) -> float: - if pair in self._cached_pairs: - return self._cached_pairs[pair].stoploss - else: - logger.warning('tried to access stoploss of a non-existing pair, ' - 'strategy stoploss is returned instead.') - return self.strategy.stoploss - - def adjust(self, pairs) -> list: - """ - Filters out and sorts "pairs" according to Edge calculated pairs - """ - final = [] - for pair, info in self._cached_pairs.items(): - if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \ - info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)) and \ - pair in pairs: - final.append(pair) - - if self._final_pairs != final: - self._final_pairs = final - if self._final_pairs: - logger.info( - 'Minimum expectancy and minimum winrate are met only for %s,' - ' so other pairs are filtered out.', - self._final_pairs - ) - else: - logger.info( - 'Edge removed all pairs as no pair with minimum expectancy ' - 'and minimum winrate was found !' - ) - - return self._final_pairs - - def accepted_pairs(self) -> list: - """ - return a list of accepted pairs along with their winrate, expectancy and stoploss - """ - final = [] - for pair, info in self._cached_pairs.items(): - if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \ - info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)): - final.append({ - 'Pair': pair, - 'Winrate': info.winrate, - 'Expectancy': info.expectancy, - 'Stoploss': info.stoploss, - }) - return final - - def _fill_calculable_fields(self, result: DataFrame) -> DataFrame: - """ - The result frame contains a number of columns that are calculable - from other columns. These are left blank till all rows are added, - to be populated in single vector calls. - - Columns to be populated are: - - Profit - - trade duration - - profit abs - :param result Dataframe - :return: result Dataframe - """ - - # stake and fees - # stake = 0.015 - # 0.05% is 0.0005 - # fee = 0.001 - - # we set stake amount to an arbitrary amount. - # as it doesn't change the calculation. - # all returned values are relative. they are percentages. - stake = 0.015 - fee = self.fee - open_fee = fee / 2 - close_fee = fee / 2 - - result['trade_duration'] = result['close_time'] - result['open_time'] - - result['trade_duration'] = result['trade_duration'].map( - lambda x: int(x.total_seconds() / 60)) - - # Spends, Takes, Profit, Absolute Profit - - # Buy Price - result['buy_vol'] = stake / result['open_rate'] # How many target are we buying - result['buy_fee'] = stake * open_fee - result['buy_spend'] = stake + result['buy_fee'] # How much we're spending - - # Sell price - result['sell_sum'] = result['buy_vol'] * result['close_rate'] - result['sell_fee'] = result['sell_sum'] * close_fee - result['sell_take'] = result['sell_sum'] - result['sell_fee'] - - # profit_percent - result['profit_percent'] = (result['sell_take'] - result['buy_spend']) / result['buy_spend'] - - # Absolute profit - result['profit_abs'] = result['sell_take'] - result['buy_spend'] - - return result - - def _process_expectancy(self, results: DataFrame) -> Dict[str, Any]: - """ - This calculates WinRate, Required Risk Reward, Risk Reward and Expectancy of all pairs - The calulation will be done per pair and per strategy. - """ - # Removing pairs having less than min_trades_number - min_trades_number = self.edge_config.get('min_trade_number', 10) - results = results.groupby(['pair', 'stoploss']).filter(lambda x: len(x) > min_trades_number) - ################################### - - # Removing outliers (Only Pumps) from the dataset - # The method to detect outliers is to calculate standard deviation - # Then every value more than (standard deviation + 2*average) is out (pump) - # - # 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()]) - ########################################################################## - - # Removing trades having a duration more than X minutes (set in config) - max_trade_duration = self.edge_config.get('max_trade_duration_minute', 1440) - results = results[results.trade_duration < max_trade_duration] - ####################################################################### - - if results.empty: - return {} - - groupby_aggregator = { - 'profit_abs': [ - ('nb_trades', 'count'), # number of all trades - ('profit_sum', lambda x: x[x > 0].sum()), # cumulative profit of all winning trades - ('loss_sum', lambda x: abs(x[x < 0].sum())), # cumulative loss of all losing trades - ('nb_win_trades', lambda x: x[x > 0].count()) # number of winning trades - ], - 'trade_duration': [('avg_trade_duration', 'mean')] - } - - # Group by (pair and stoploss) by applying above aggregator - df = results.groupby(['pair', 'stoploss'])['profit_abs', 'trade_duration'].agg( - groupby_aggregator).reset_index(col_level=1) - - # Dropping level 0 as we don't need it - df.columns = df.columns.droplevel(0) - - # 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'] - - # Win rate = number of profitable trades / number of trades - df['winrate'] = df['nb_win_trades'] / df['nb_trades'] - - # risk_reward_ratio = average win / average loss - df['risk_reward_ratio'] = df['average_win'] / df['average_loss'] - - # required_risk_reward = (1 / winrate) - 1 - df['required_risk_reward'] = (1 / df['winrate']) - 1 - - # expectancy = (risk_reward_ratio * winrate) - (lossrate) - df['expectancy'] = (df['risk_reward_ratio'] * df['winrate']) - (1 - df['winrate']) - - # sort by expectancy and stoploss - df = df.sort_values(by=['expectancy', 'stoploss'], ascending=False).groupby( - 'pair').first().sort_values(by=['expectancy'], ascending=False).reset_index() - - final = {} - for x in df.itertuples(): - final[x.pair] = PairInfo( - x.stoploss, - x.winrate, - x.risk_reward_ratio, - x.required_risk_reward, - x.expectancy, - x.nb_trades, - x.avg_trade_duration - ) - - # Returning a list of pairs in order of "expectancy" - return final - - def _find_trades_for_stoploss_range(self, ticker_data, pair, stoploss_range): - buy_column = ticker_data['buy'].values - sell_column = ticker_data['sell'].values - date_column = ticker_data['date'].values - ohlc_columns = ticker_data[['open', 'high', 'low', 'close']].values - - result: list = [] - for stoploss in stoploss_range: - result += self._detect_next_stop_or_sell_point( - buy_column, sell_column, date_column, ohlc_columns, round(stoploss, 6), pair - ) - - return result - - def _detect_next_stop_or_sell_point(self, buy_column, sell_column, date_column, - ohlc_columns, stoploss, pair): - """ - Iterate through ohlc_columns in order to find the next trade - Next trade opens from the first buy signal noticed to - The sell or stoploss signal after it. - It then cuts OHLC, buy_column, sell_column and date_column. - Cut from (the exit trade index) + 1. - - Author: https://github.com/mishaker - """ - - result: list = [] - start_point = 0 - - while True: - open_trade_index = utf1st.find_1st(buy_column, 1, utf1st.cmp_equal) - - # Return empty if we don't find trade entry (i.e. buy==1) or - # we find a buy but at the end of array - if open_trade_index == -1 or open_trade_index == len(buy_column) - 1: - break - else: - # When a buy signal is seen, - # trade opens in reality on the next candle - open_trade_index += 1 - - stop_price_percentage = stoploss + 1 - open_price = ohlc_columns[open_trade_index, 0] - stop_price = (open_price * stop_price_percentage) - - # Searching for the index where stoploss is hit - stop_index = utf1st.find_1st( - ohlc_columns[open_trade_index:, 2], stop_price, utf1st.cmp_smaller) - - # If we don't find it then we assume stop_index will be far in future (infinite number) - if stop_index == -1: - stop_index = float('inf') - - # Searching for the index where sell is hit - sell_index = utf1st.find_1st(sell_column[open_trade_index:], 1, utf1st.cmp_equal) - - # If we don't find it then we assume sell_index will be far in future (infinite number) - if sell_index == -1: - sell_index = float('inf') - - # Check if we don't find any stop or sell point (in that case trade remains open) - # It is not interesting for Edge to consider it so we simply ignore the trade - # And stop iterating there is no more entry - if stop_index == sell_index == float('inf'): - break - - if stop_index <= sell_index: - exit_index = open_trade_index + stop_index - exit_type = SellType.STOP_LOSS - exit_price = stop_price - elif stop_index > sell_index: - # If exit is SELL then we exit at the next candle - exit_index = open_trade_index + sell_index + 1 - - # Check if we have the next candle - if len(ohlc_columns) - 1 < exit_index: - break - - exit_type = SellType.SELL_SIGNAL - exit_price = ohlc_columns[exit_index, 0] - - trade = {'pair': pair, - 'stoploss': stoploss, - 'profit_percent': '', - 'profit_abs': '', - 'open_time': date_column[open_trade_index], - 'close_time': 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), - 'exit_type': exit_type - } - - result.append(trade) - - # Giving a view of exit_index till the end of array - buy_column = buy_column[exit_index:] - sell_column = sell_column[exit_index:] - date_column = date_column[exit_index:] - ohlc_columns = ohlc_columns[exit_index:] - start_point += exit_index - - return result +from .edge_positioning import Edge, PairInfo # noqa: F401 diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py new file mode 100644 index 000000000..15883357b --- /dev/null +++ b/freqtrade/edge/edge_positioning.py @@ -0,0 +1,464 @@ +# pragma pylint: disable=W0603 +""" Edge positioning package """ +import logging +from typing import Any, Dict, NamedTuple + +import arrow +import numpy as np +import utils_find_1st as utf1st +from pandas import DataFrame + +from freqtrade import constants +from freqtrade.configuration import TimeRange +from freqtrade.data import history +from freqtrade.exceptions import OperationalException +from freqtrade.strategy.interface import SellType + +logger = logging.getLogger(__name__) + + +class PairInfo(NamedTuple): + stoploss: float + winrate: float + risk_reward_ratio: float + required_risk_reward: float + expectancy: float + nb_trades: int + avg_trade_duration: float + + +class Edge: + """ + Calculates Win Rate, Risk Reward Ratio, Expectancy + against historical data for a give set of markets and a strategy + it then adjusts stoploss and position size accordingly + and force it into the strategy + Author: https://github.com/mishaker + """ + + config: Dict = {} + _cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs + + def __init__(self, config: Dict[str, Any], exchange, strategy) -> None: + + self.config = config + self.exchange = exchange + self.strategy = strategy + + self.edge_config = self.config.get('edge', {}) + self._cached_pairs: Dict[str, Any] = {} # Keeps a list of pairs + self._final_pairs: list = [] + + # checking max_open_trades. it should be -1 as with Edge + # the number of trades is determined by position size + if self.config['max_open_trades'] != float('inf'): + logger.critical('max_open_trades should be -1 in config !') + + if self.config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT: + raise OperationalException('Edge works only with unlimited stake amount') + + # Deprecated capital_available_percentage. Will use tradable_balance_ratio in the future. + self._capital_percentage: float = self.edge_config.get( + 'capital_available_percentage', self.config['tradable_balance_ratio']) + self._allowed_risk: float = self.edge_config.get('allowed_risk') + self._since_number_of_days: int = self.edge_config.get('calculate_since_number_of_days', 14) + self._last_updated: int = 0 # Timestamp of pairs last updated time + self._refresh_pairs = True + + self._stoploss_range_min = float(self.edge_config.get('stoploss_range_min', -0.01)) + self._stoploss_range_max = float(self.edge_config.get('stoploss_range_max', -0.05)) + self._stoploss_range_step = float(self.edge_config.get('stoploss_range_step', -0.001)) + + # calculating stoploss range + self._stoploss_range = np.arange( + self._stoploss_range_min, + self._stoploss_range_max, + self._stoploss_range_step + ) + + self._timerange: TimeRange = TimeRange.parse_timerange("%s-" % arrow.now().shift( + days=-1 * self._since_number_of_days).format('YYYYMMDD')) + if config.get('fee'): + self.fee = config['fee'] + else: + self.fee = self.exchange.get_fee(symbol=self.config['exchange']['pair_whitelist'][0]) + + def calculate(self) -> bool: + pairs = self.config['exchange']['pair_whitelist'] + heartbeat = self.edge_config.get('process_throttle_secs') + + if (self._last_updated > 0) and ( + self._last_updated + heartbeat > arrow.utcnow().timestamp): + return False + + data: Dict[str, Any] = {} + logger.info('Using stake_currency: %s ...', self.config['stake_currency']) + logger.info('Using local backtesting data (using whitelist in given config) ...') + + if self._refresh_pairs: + history.refresh_data( + datadir=self.config['datadir'], + pairs=pairs, + exchange=self.exchange, + timeframe=self.strategy.ticker_interval, + timerange=self._timerange, + ) + + data = history.load_data( + datadir=self.config['datadir'], + pairs=pairs, + timeframe=self.strategy.ticker_interval, + timerange=self._timerange, + startup_candles=self.strategy.startup_candle_count, + ) + + if not data: + # Reinitializing cached pairs + self._cached_pairs = {} + logger.critical("No data found. Edge is stopped ...") + return False + + preprocessed = self.strategy.tickerdata_to_dataframe(data) + + # Print timeframe + min_date, max_date = history.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 + ) + headers = ['date', 'buy', 'open', 'close', 'sell', 'high', 'low'] + + trades: list = [] + for pair, pair_data in preprocessed.items(): + # Sorting dataframe by date and reset index + pair_data = pair_data.sort_values(by=['date']) + pair_data = pair_data.reset_index(drop=True) + + ticker_data = self.strategy.advise_sell( + self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() + + trades += self._find_trades_for_stoploss_range(ticker_data, pair, self._stoploss_range) + + # If no trade found then exit + if len(trades) == 0: + logger.info("No trades found.") + return False + + # 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 + + return True + + def stake_amount(self, pair: str, free_capital: float, + total_capital: float, capital_in_trade: float) -> float: + stoploss = self.stoploss(pair) + available_capital = (total_capital + capital_in_trade) * self._capital_percentage + allowed_capital_at_risk = available_capital * self._allowed_risk + max_position_size = abs(allowed_capital_at_risk / stoploss) + position_size = min(max_position_size, free_capital) + if pair in self._cached_pairs: + logger.info( + 'winrate: %s, expectancy: %s, position size: %s, pair: %s,' + ' capital in trade: %s, free capital: %s, total capital: %s,' + ' stoploss: %s, available capital: %s.', + self._cached_pairs[pair].winrate, + self._cached_pairs[pair].expectancy, + position_size, pair, + capital_in_trade, free_capital, total_capital, + stoploss, available_capital + ) + return round(position_size, 15) + + def stoploss(self, pair: str) -> float: + if pair in self._cached_pairs: + return self._cached_pairs[pair].stoploss + else: + logger.warning('tried to access stoploss of a non-existing pair, ' + 'strategy stoploss is returned instead.') + return self.strategy.stoploss + + def adjust(self, pairs) -> list: + """ + Filters out and sorts "pairs" according to Edge calculated pairs + """ + final = [] + for pair, info in self._cached_pairs.items(): + if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \ + info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)) and \ + pair in pairs: + final.append(pair) + + if self._final_pairs != final: + self._final_pairs = final + if self._final_pairs: + logger.info( + 'Minimum expectancy and minimum winrate are met only for %s,' + ' so other pairs are filtered out.', + self._final_pairs + ) + else: + logger.info( + 'Edge removed all pairs as no pair with minimum expectancy ' + 'and minimum winrate was found !' + ) + + return self._final_pairs + + def accepted_pairs(self) -> list: + """ + return a list of accepted pairs along with their winrate, expectancy and stoploss + """ + final = [] + for pair, info in self._cached_pairs.items(): + if info.expectancy > float(self.edge_config.get('minimum_expectancy', 0.2)) and \ + info.winrate > float(self.edge_config.get('minimum_winrate', 0.60)): + final.append({ + 'Pair': pair, + 'Winrate': info.winrate, + 'Expectancy': info.expectancy, + 'Stoploss': info.stoploss, + }) + return final + + def _fill_calculable_fields(self, result: DataFrame) -> DataFrame: + """ + The result frame contains a number of columns that are calculable + from other columns. These are left blank till all rows are added, + to be populated in single vector calls. + + Columns to be populated are: + - Profit + - trade duration + - profit abs + :param result Dataframe + :return: result Dataframe + """ + + # stake and fees + # stake = 0.015 + # 0.05% is 0.0005 + # fee = 0.001 + + # we set stake amount to an arbitrary amount. + # as it doesn't change the calculation. + # all returned values are relative. they are percentages. + stake = 0.015 + fee = self.fee + open_fee = fee / 2 + close_fee = fee / 2 + + result['trade_duration'] = result['close_time'] - result['open_time'] + + result['trade_duration'] = result['trade_duration'].map( + lambda x: int(x.total_seconds() / 60)) + + # Spends, Takes, Profit, Absolute Profit + + # Buy Price + result['buy_vol'] = stake / result['open_rate'] # How many target are we buying + result['buy_fee'] = stake * open_fee + result['buy_spend'] = stake + result['buy_fee'] # How much we're spending + + # Sell price + result['sell_sum'] = result['buy_vol'] * result['close_rate'] + result['sell_fee'] = result['sell_sum'] * close_fee + result['sell_take'] = result['sell_sum'] - result['sell_fee'] + + # profit_percent + result['profit_percent'] = (result['sell_take'] - result['buy_spend']) / result['buy_spend'] + + # Absolute profit + result['profit_abs'] = result['sell_take'] - result['buy_spend'] + + return result + + def _process_expectancy(self, results: DataFrame) -> Dict[str, Any]: + """ + This calculates WinRate, Required Risk Reward, Risk Reward and Expectancy of all pairs + The calulation will be done per pair and per strategy. + """ + # Removing pairs having less than min_trades_number + min_trades_number = self.edge_config.get('min_trade_number', 10) + results = results.groupby(['pair', 'stoploss']).filter(lambda x: len(x) > min_trades_number) + ################################### + + # Removing outliers (Only Pumps) from the dataset + # The method to detect outliers is to calculate standard deviation + # Then every value more than (standard deviation + 2*average) is out (pump) + # + # 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()]) + ########################################################################## + + # Removing trades having a duration more than X minutes (set in config) + max_trade_duration = self.edge_config.get('max_trade_duration_minute', 1440) + results = results[results.trade_duration < max_trade_duration] + ####################################################################### + + if results.empty: + return {} + + groupby_aggregator = { + 'profit_abs': [ + ('nb_trades', 'count'), # number of all trades + ('profit_sum', lambda x: x[x > 0].sum()), # cumulative profit of all winning trades + ('loss_sum', lambda x: abs(x[x < 0].sum())), # cumulative loss of all losing trades + ('nb_win_trades', lambda x: x[x > 0].count()) # number of winning trades + ], + 'trade_duration': [('avg_trade_duration', 'mean')] + } + + # Group by (pair and stoploss) by applying above aggregator + df = results.groupby(['pair', 'stoploss'])['profit_abs', 'trade_duration'].agg( + groupby_aggregator).reset_index(col_level=1) + + # Dropping level 0 as we don't need it + df.columns = df.columns.droplevel(0) + + # 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'] + + # Win rate = number of profitable trades / number of trades + df['winrate'] = df['nb_win_trades'] / df['nb_trades'] + + # risk_reward_ratio = average win / average loss + df['risk_reward_ratio'] = df['average_win'] / df['average_loss'] + + # required_risk_reward = (1 / winrate) - 1 + df['required_risk_reward'] = (1 / df['winrate']) - 1 + + # expectancy = (risk_reward_ratio * winrate) - (lossrate) + df['expectancy'] = (df['risk_reward_ratio'] * df['winrate']) - (1 - df['winrate']) + + # sort by expectancy and stoploss + df = df.sort_values(by=['expectancy', 'stoploss'], ascending=False).groupby( + 'pair').first().sort_values(by=['expectancy'], ascending=False).reset_index() + + final = {} + for x in df.itertuples(): + final[x.pair] = PairInfo( + x.stoploss, + x.winrate, + x.risk_reward_ratio, + x.required_risk_reward, + x.expectancy, + x.nb_trades, + x.avg_trade_duration + ) + + # Returning a list of pairs in order of "expectancy" + return final + + def _find_trades_for_stoploss_range(self, ticker_data, pair, stoploss_range): + buy_column = ticker_data['buy'].values + sell_column = ticker_data['sell'].values + date_column = ticker_data['date'].values + ohlc_columns = ticker_data[['open', 'high', 'low', 'close']].values + + result: list = [] + for stoploss in stoploss_range: + result += self._detect_next_stop_or_sell_point( + buy_column, sell_column, date_column, ohlc_columns, round(stoploss, 6), pair + ) + + return result + + def _detect_next_stop_or_sell_point(self, buy_column, sell_column, date_column, + ohlc_columns, stoploss, pair): + """ + Iterate through ohlc_columns in order to find the next trade + Next trade opens from the first buy signal noticed to + The sell or stoploss signal after it. + It then cuts OHLC, buy_column, sell_column and date_column. + Cut from (the exit trade index) + 1. + + Author: https://github.com/mishaker + """ + + result: list = [] + start_point = 0 + + while True: + open_trade_index = utf1st.find_1st(buy_column, 1, utf1st.cmp_equal) + + # Return empty if we don't find trade entry (i.e. buy==1) or + # we find a buy but at the end of array + if open_trade_index == -1 or open_trade_index == len(buy_column) - 1: + break + else: + # When a buy signal is seen, + # trade opens in reality on the next candle + open_trade_index += 1 + + stop_price_percentage = stoploss + 1 + open_price = ohlc_columns[open_trade_index, 0] + stop_price = (open_price * stop_price_percentage) + + # Searching for the index where stoploss is hit + stop_index = utf1st.find_1st( + ohlc_columns[open_trade_index:, 2], stop_price, utf1st.cmp_smaller) + + # If we don't find it then we assume stop_index will be far in future (infinite number) + if stop_index == -1: + stop_index = float('inf') + + # Searching for the index where sell is hit + sell_index = utf1st.find_1st(sell_column[open_trade_index:], 1, utf1st.cmp_equal) + + # If we don't find it then we assume sell_index will be far in future (infinite number) + if sell_index == -1: + sell_index = float('inf') + + # Check if we don't find any stop or sell point (in that case trade remains open) + # It is not interesting for Edge to consider it so we simply ignore the trade + # And stop iterating there is no more entry + if stop_index == sell_index == float('inf'): + break + + if stop_index <= sell_index: + exit_index = open_trade_index + stop_index + exit_type = SellType.STOP_LOSS + exit_price = stop_price + elif stop_index > sell_index: + # If exit is SELL then we exit at the next candle + exit_index = open_trade_index + sell_index + 1 + + # Check if we have the next candle + if len(ohlc_columns) - 1 < exit_index: + break + + exit_type = SellType.SELL_SIGNAL + exit_price = ohlc_columns[exit_index, 0] + + trade = {'pair': pair, + 'stoploss': stoploss, + 'profit_percent': '', + 'profit_abs': '', + 'open_time': date_column[open_trade_index], + 'close_time': 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), + 'exit_type': exit_type + } + + result.append(trade) + + # Giving a view of exit_index till the end of array + buy_column = buy_column[exit_index:] + sell_column = sell_column[exit_index:] + date_column = date_column[exit_index:] + ohlc_columns = ohlc_columns[exit_index:] + start_point += exit_index + + return result From 80ed1c3e146da9c3f36f978ceae0df250acc4131 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Jan 2020 10:51:09 +0100 Subject: [PATCH 303/324] Move utils to commands --- freqtrade/commands/__init__.py | 0 freqtrade/{ => commands}/utils.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 freqtrade/commands/__init__.py rename freqtrade/{ => commands}/utils.py (100%) diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/freqtrade/utils.py b/freqtrade/commands/utils.py similarity index 100% rename from freqtrade/utils.py rename to freqtrade/commands/utils.py From 6e852804671efa0546341b5db5f35b79bc9e9278 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Jan 2020 10:55:39 +0100 Subject: [PATCH 304/324] Adjust imports --- freqtrade/commands/__init__.py | 6 ++++++ freqtrade/configuration/arguments.py | 12 +++++------ freqtrade/optimize/__init__.py | 2 +- freqtrade/plot/plot_utils.py | 2 +- tests/test_utils.py | 30 ++++++++++++++-------------- 5 files changed, 29 insertions(+), 23 deletions(-) diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index e69de29bb..0c31048f2 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -0,0 +1,6 @@ +from .utils import (setup_utils_configuration, start_create_userdir, # noqa: F401 + start_download_data, start_hyperopt_list, + start_hyperopt_show, start_list_exchanges, + start_list_markets, start_list_strategies, + start_list_timeframes, start_new_hyperopt, + start_new_strategy, start_test_pairlist, start_trading) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index b2197619d..3c71fa548 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -131,12 +131,12 @@ class Arguments: self._build_args(optionlist=['version'], parser=self.parser) from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge - from freqtrade.utils import (start_create_userdir, start_download_data, - start_hyperopt_list, start_hyperopt_show, - start_list_exchanges, start_list_markets, - start_list_strategies, start_new_hyperopt, - start_new_strategy, start_list_timeframes, - start_test_pairlist, start_trading) + from freqtrade.commands import (start_create_userdir, start_download_data, + start_hyperopt_list, start_hyperopt_show, + start_list_exchanges, start_list_markets, + start_list_strategies, start_new_hyperopt, + start_new_strategy, start_list_timeframes, + start_test_pairlist, start_trading) from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit subparsers = self.parser.add_subparsers(dest='command', diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 34760372f..98b521d4e 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -4,7 +4,7 @@ from typing import Any, Dict from freqtrade import constants from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.state import RunMode -from freqtrade.utils import setup_utils_configuration +from freqtrade.commands import setup_utils_configuration logger = logging.getLogger(__name__) diff --git a/freqtrade/plot/plot_utils.py b/freqtrade/plot/plot_utils.py index 9eff08396..b2f16dff0 100644 --- a/freqtrade/plot/plot_utils.py +++ b/freqtrade/plot/plot_utils.py @@ -2,7 +2,7 @@ from typing import Any, Dict from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode -from freqtrade.utils import setup_utils_configuration +from freqtrade.commands import setup_utils_configuration def validate_plot_args(args: Dict[str, Any]): diff --git a/tests/test_utils.py b/tests/test_utils.py index 7ca2cac5a..91009ec42 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -4,15 +4,15 @@ from unittest.mock import MagicMock, PropertyMock import pytest +from freqtrade.commands import (setup_utils_configuration, + start_create_userdir, start_download_data, + start_hyperopt_list, start_hyperopt_show, + start_list_exchanges, start_list_markets, + start_list_strategies, start_list_timeframes, + start_new_hyperopt, start_new_strategy, + start_test_pairlist, start_trading) from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode -from freqtrade.utils import (setup_utils_configuration, start_create_userdir, - start_download_data, start_hyperopt_list, - start_hyperopt_show, start_list_exchanges, - start_list_markets, start_list_strategies, - start_list_timeframes, start_new_hyperopt, - start_new_strategy, start_test_pairlist, - start_trading) from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) @@ -451,8 +451,8 @@ def test_create_datadir(caplog, mocker): # Added assert here to analyze random test-failures ... assert len(caplog.record_tuples) == 0 - cud = mocker.patch("freqtrade.utils.create_userdata_dir", MagicMock()) - csf = mocker.patch("freqtrade.utils.copy_sample_files", MagicMock()) + cud = mocker.patch("freqtrade.commands.create_userdata_dir", MagicMock()) + csf = mocker.patch("freqtrade.commands.copy_sample_files", MagicMock()) args = [ "create-userdir", "--userdir", @@ -538,7 +538,7 @@ def test_start_new_hyperopt_no_arg(mocker, caplog): def test_download_data_keyboardInterrupt(mocker, caplog, markets): - dl_mock = mocker.patch('freqtrade.utils.refresh_backtest_ohlcv_data', + dl_mock = mocker.patch('freqtrade.commands.refresh_backtest_ohlcv_data', MagicMock(side_effect=KeyboardInterrupt)) patch_exchange(mocker) mocker.patch( @@ -556,7 +556,7 @@ def test_download_data_keyboardInterrupt(mocker, caplog, markets): def test_download_data_no_markets(mocker, caplog): - dl_mock = mocker.patch('freqtrade.utils.refresh_backtest_ohlcv_data', + dl_mock = mocker.patch('freqtrade.commands.refresh_backtest_ohlcv_data', MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) patch_exchange(mocker, id='binance') mocker.patch( @@ -574,7 +574,7 @@ def test_download_data_no_markets(mocker, caplog): def test_download_data_no_exchange(mocker, caplog): - mocker.patch('freqtrade.utils.refresh_backtest_ohlcv_data', + mocker.patch('freqtrade.commands.refresh_backtest_ohlcv_data', MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) patch_exchange(mocker) mocker.patch( @@ -594,7 +594,7 @@ def test_download_data_no_pairs(mocker, caplog): mocker.patch.object(Path, "exists", MagicMock(return_value=False)) - mocker.patch('freqtrade.utils.refresh_backtest_ohlcv_data', + mocker.patch('freqtrade.commands.refresh_backtest_ohlcv_data', MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) patch_exchange(mocker) mocker.patch( @@ -613,9 +613,9 @@ def test_download_data_no_pairs(mocker, caplog): def test_download_data_trades(mocker, caplog): - dl_mock = mocker.patch('freqtrade.utils.refresh_backtest_trades_data', + dl_mock = mocker.patch('freqtrade.commands.refresh_backtest_trades_data', MagicMock(return_value=[])) - convert_mock = mocker.patch('freqtrade.utils.convert_trades_to_ohlcv', + convert_mock = mocker.patch('freqtrade.commands.convert_trades_to_ohlcv', MagicMock(return_value=[])) patch_exchange(mocker) mocker.patch( From 926bf07df1420c2e08dadbb53e8359a1ffa20900 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Jan 2020 13:08:58 +0100 Subject: [PATCH 305/324] Seperate a few commands into specific files --- freqtrade/commands/__init__.py | 16 +- freqtrade/commands/deploy_commands.py | 113 ++++++++ freqtrade/commands/hyperopt_commands.py | 114 ++++++++ freqtrade/commands/list_commands.py | 157 +++++++++++ freqtrade/commands/trade_commands.py | 25 ++ freqtrade/commands/utils.py | 356 ------------------------ 6 files changed, 419 insertions(+), 362 deletions(-) create mode 100644 freqtrade/commands/deploy_commands.py create mode 100644 freqtrade/commands/hyperopt_commands.py create mode 100644 freqtrade/commands/list_commands.py create mode 100644 freqtrade/commands/trade_commands.py diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index 0c31048f2..4c57efeae 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -1,6 +1,10 @@ -from .utils import (setup_utils_configuration, start_create_userdir, # noqa: F401 - start_download_data, start_hyperopt_list, - start_hyperopt_show, start_list_exchanges, - start_list_markets, start_list_strategies, - start_list_timeframes, start_new_hyperopt, - start_new_strategy, start_test_pairlist, start_trading) +from .hyperopt_commands import (start_hyperopt_list, start_hyperopt_show) # noqa: 401 + +from .list_commands import (start_list_exchanges, # noqa: F401 + start_list_markets, start_list_strategies, + start_list_timeframes) +from .utils import setup_utils_configuration # noqa: F401 +from .utils import (start_download_data, # noqa: F401 + start_test_pairlist) +from .deploy_commands import (start_new_hyperopt, start_new_strategy, start_create_userdir) # noqa: F401 +from .trade_commands import start_trading # noqa: F401 diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py new file mode 100644 index 000000000..892e15db1 --- /dev/null +++ b/freqtrade/commands/deploy_commands.py @@ -0,0 +1,113 @@ +import logging +import sys +from pathlib import Path +from typing import Any, Dict + +from freqtrade.configuration.directory_operations import (copy_sample_files, + create_userdata_dir) +from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGY +from freqtrade.exceptions import OperationalException +from freqtrade.misc import render_template +from freqtrade.state import RunMode + +from .utils import setup_utils_configuration + +logger = logging.getLogger(__name__) + + +def start_create_userdir(args: Dict[str, Any]) -> None: + """ + Create "user_data" directory to contain user data strategies, hyperopt, ...) + :param args: Cli args from Arguments() + :return: None + """ + if "user_data_dir" in args and args["user_data_dir"]: + userdir = create_userdata_dir(args["user_data_dir"], create_dir=True) + copy_sample_files(userdir, overwrite=args["reset"]) + else: + logger.warning("`create-userdir` requires --userdir to be set.") + sys.exit(1) + + +def deploy_new_strategy(strategy_name, strategy_path: Path, subtemplate: str): + """ + Deploy new strategy from template to strategy_path + """ + indicators = render_template(templatefile=f"subtemplates/indicators_{subtemplate}.j2",) + buy_trend = render_template(templatefile=f"subtemplates/buy_trend_{subtemplate}.j2",) + sell_trend = render_template(templatefile=f"subtemplates/sell_trend_{subtemplate}.j2",) + plot_config = render_template(templatefile=f"subtemplates/plot_config_{subtemplate}.j2",) + + strategy_text = render_template(templatefile='base_strategy.py.j2', + arguments={"strategy": strategy_name, + "indicators": indicators, + "buy_trend": buy_trend, + "sell_trend": sell_trend, + "plot_config": plot_config, + }) + + logger.info(f"Writing strategy to `{strategy_path}`.") + strategy_path.write_text(strategy_text) + + +def start_new_strategy(args: Dict[str, Any]) -> None: + + config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) + + if "strategy" in args and args["strategy"]: + if args["strategy"] == "DefaultStrategy": + raise OperationalException("DefaultStrategy is not allowed as name.") + + new_path = config['user_data_dir'] / USERPATH_STRATEGY / (args["strategy"] + ".py") + + if new_path.exists(): + raise OperationalException(f"`{new_path}` already exists. " + "Please choose another Strategy Name.") + + deploy_new_strategy(args['strategy'], new_path, args['template']) + + else: + raise OperationalException("`new-strategy` requires --strategy to be set.") + + +def deploy_new_hyperopt(hyperopt_name, hyperopt_path: Path, subtemplate: str): + """ + Deploys a new hyperopt template to hyperopt_path + """ + buy_guards = render_template( + templatefile=f"subtemplates/hyperopt_buy_guards_{subtemplate}.j2",) + sell_guards = render_template( + templatefile=f"subtemplates/hyperopt_sell_guards_{subtemplate}.j2",) + buy_space = render_template( + templatefile=f"subtemplates/hyperopt_buy_space_{subtemplate}.j2",) + sell_space = render_template( + templatefile=f"subtemplates/hyperopt_sell_space_{subtemplate}.j2",) + + strategy_text = render_template(templatefile='base_hyperopt.py.j2', + arguments={"hyperopt": hyperopt_name, + "buy_guards": buy_guards, + "sell_guards": sell_guards, + "buy_space": buy_space, + "sell_space": sell_space, + }) + + logger.info(f"Writing hyperopt to `{hyperopt_path}`.") + hyperopt_path.write_text(strategy_text) + + +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": + raise OperationalException("DefaultHyperopt is not allowed as name.") + + new_path = config['user_data_dir'] / USERPATH_HYPEROPTS / (args["hyperopt"] + ".py") + + if new_path.exists(): + raise OperationalException(f"`{new_path}` already exists. " + "Please choose another Strategy Name.") + deploy_new_hyperopt(args['hyperopt'], new_path, args['template']) + else: + raise OperationalException("`new-hyperopt` requires --hyperopt to be set.") diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py new file mode 100644 index 000000000..db3f69050 --- /dev/null +++ b/freqtrade/commands/hyperopt_commands.py @@ -0,0 +1,114 @@ +import logging +from operator import itemgetter +from typing import Any, Dict, List + +from colorama import init as colorama_init + +from .utils import setup_utils_configuration +from freqtrade.exceptions import OperationalException +from freqtrade.state import RunMode + +logger = logging.getLogger(__name__) + + +def start_hyperopt_list(args: Dict[str, Any]) -> None: + """ + List hyperopt epochs previously evaluated + """ + from freqtrade.optimize.hyperopt import Hyperopt + + config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) + + only_best = config.get('hyperopt_list_best', False) + only_profitable = config.get('hyperopt_list_profitable', False) + print_colorized = config.get('print_colorized', False) + print_json = config.get('print_json', False) + no_details = config.get('hyperopt_list_no_details', False) + no_header = False + + trials_file = (config['user_data_dir'] / + 'hyperopt_results' / 'hyperopt_results.pickle') + + # Previous evaluations + trials = Hyperopt.load_previous_results(trials_file) + total_epochs = len(trials) + + trials = _hyperopt_filter_trials(trials, only_best, only_profitable) + + # TODO: fetch the interval for epochs to print from the cli option + epoch_start, epoch_stop = 0, None + + if print_colorized: + colorama_init(autoreset=True) + + try: + # Human-friendly indexes used here (starting from 1) + for val in trials[epoch_start:epoch_stop]: + Hyperopt.print_results_explanation(val, total_epochs, not only_best, print_colorized) + + except KeyboardInterrupt: + print('User interrupted..') + + if trials and not no_details: + sorted_trials = sorted(trials, key=itemgetter('loss')) + results = sorted_trials[0] + Hyperopt.print_epoch_details(results, total_epochs, print_json, no_header) + + +def start_hyperopt_show(args: Dict[str, Any]) -> None: + """ + Show details of a hyperopt epoch previously evaluated + """ + from freqtrade.optimize.hyperopt import Hyperopt + + config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) + + only_best = config.get('hyperopt_list_best', False) + only_profitable = config.get('hyperopt_list_profitable', False) + no_header = config.get('hyperopt_show_no_header', False) + + trials_file = (config['user_data_dir'] / + 'hyperopt_results' / 'hyperopt_results.pickle') + + # Previous evaluations + trials = Hyperopt.load_previous_results(trials_file) + total_epochs = len(trials) + + trials = _hyperopt_filter_trials(trials, only_best, only_profitable) + trials_epochs = len(trials) + + n = config.get('hyperopt_show_index', -1) + if n > trials_epochs: + raise OperationalException( + f"The index of the epoch to show should be less than {trials_epochs + 1}.") + if n < -trials_epochs: + raise OperationalException( + f"The index of the epoch to show should be greater than {-trials_epochs - 1}.") + + # Translate epoch index from human-readable format to pythonic + if n > 0: + n -= 1 + + print_json = config.get('print_json', False) + + if trials: + val = trials[n] + Hyperopt.print_epoch_details(val, total_epochs, print_json, no_header, + header_str="Epoch details") + + +def _hyperopt_filter_trials(trials: List, only_best: bool, only_profitable: bool) -> List: + """ + Filter our items from the list of hyperopt results + """ + if only_best: + trials = [x for x in trials if x['is_best']] + if only_profitable: + trials = [x for x in trials if x['results_metrics']['profit'] > 0] + + logger.info(f"{len(trials)} " + + ("best " if only_best else "") + + ("profitable " if only_profitable else "") + + "epochs found.") + + return trials diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py new file mode 100644 index 000000000..90efaf66f --- /dev/null +++ b/freqtrade/commands/list_commands.py @@ -0,0 +1,157 @@ +import csv +import logging +import sys +from collections import OrderedDict +from pathlib import Path +from typing import Any, Dict + +import rapidjson +from tabulate import tabulate + +from freqtrade.constants import USERPATH_STRATEGY +from freqtrade.exceptions import OperationalException +from freqtrade.exchange import (available_exchanges, ccxt_exchanges, + market_is_active, symbol_is_pair) +from freqtrade.misc import plural +from freqtrade.resolvers import ExchangeResolver, StrategyResolver +from freqtrade.state import RunMode + +from .utils import setup_utils_configuration + +logger = logging.getLogger(__name__) + + +def start_list_exchanges(args: Dict[str, Any]) -> None: + """ + Print available exchanges + :param args: Cli args from Arguments() + :return: None + """ + exchanges = ccxt_exchanges() if args['list_exchanges_all'] else available_exchanges() + if args['print_one_column']: + print('\n'.join(exchanges)) + else: + if args['list_exchanges_all']: + print(f"All exchanges supported by the ccxt library: {', '.join(exchanges)}") + else: + print(f"Exchanges available for Freqtrade: {', '.join(exchanges)}") + + +def start_list_strategies(args: Dict[str, Any]) -> None: + """ + Print Strategies available in a directory + """ + config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) + + directory = Path(config.get('strategy_path', config['user_data_dir'] / USERPATH_STRATEGY)) + strategies = StrategyResolver.search_all_objects(directory) + # Sort alphabetically + strategies = sorted(strategies, key=lambda x: x['name']) + strats_to_print = [{'name': s['name'], 'location': s['location'].name} for s in strategies] + + if args['print_one_column']: + print('\n'.join([s['name'] for s in strategies])) + else: + print(tabulate(strats_to_print, headers='keys', tablefmt='pipe')) + + +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 + + # Init exchange + exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) + + if args['print_one_column']: + print('\n'.join(exchange.timeframes)) + else: + print(f"Timeframes available for the exchange `{exchange.name}`: " + f"{', '.join(exchange.timeframes)}") + + +def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: + """ + Print pairs/markets on the exchange + :param args: Cli args from Arguments() + :param pairs_only: if True print only pairs, otherwise print all instruments (markets) + :return: None + """ + config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) + + # Init exchange + exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) + + # By default only active pairs/markets are to be shown + active_only = not args.get('list_pairs_all', False) + + base_currencies = args.get('base_currencies', []) + quote_currencies = args.get('quote_currencies', []) + + try: + pairs = exchange.get_markets(base_currencies=base_currencies, + quote_currencies=quote_currencies, + pairs_only=pairs_only, + active_only=active_only) + # Sort the pairs/markets by symbol + pairs = OrderedDict(sorted(pairs.items())) + except Exception as e: + raise OperationalException(f"Cannot get markets. Reason: {e}") from e + + else: + summary_str = ((f"Exchange {exchange.name} has {len(pairs)} ") + + ("active " if active_only else "") + + (plural(len(pairs), "pair" if pairs_only else "market")) + + (f" with {', '.join(base_currencies)} as base " + f"{plural(len(base_currencies), 'currency', 'currencies')}" + if base_currencies else "") + + (" and" if base_currencies and quote_currencies else "") + + (f" with {', '.join(quote_currencies)} as quote " + f"{plural(len(quote_currencies), 'currency', 'currencies')}" + if quote_currencies else "")) + + headers = ["Id", "Symbol", "Base", "Quote", "Active", + *(['Is pair'] if not pairs_only else [])] + + tabular_data = [] + for _, v in pairs.items(): + 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'])} + if not pairs_only else {})}) + + if (args.get('print_one_column', False) or + args.get('list_pairs_print_json', False) or + args.get('print_csv', False)): + # Print summary string in the log in case of machine-readable + # regular formats. + logger.info(f"{summary_str}.") + else: + # Print empty string separating leading logs and output in case of + # human-readable formats. + print() + + if len(pairs): + if args.get('print_list', False): + # print data as a list, with human-readable summary + print(f"{summary_str}: {', '.join(pairs.keys())}.") + elif args.get('print_one_column', False): + print('\n'.join(pairs.keys())) + elif args.get('list_pairs_print_json', False): + print(rapidjson.dumps(list(pairs.keys()), default=str)) + elif args.get('print_csv', False): + writer = csv.DictWriter(sys.stdout, fieldnames=headers) + writer.writeheader() + writer.writerows(tabular_data) + else: + # print data as a table, with the human-readable summary + print(f"{summary_str}:") + print(tabulate(tabular_data, headers='keys', tablefmt='pipe')) + elif not (args.get('print_one_column', False) or + args.get('list_pairs_print_json', False) or + args.get('print_csv', False)): + print(f"{summary_str}.") diff --git a/freqtrade/commands/trade_commands.py b/freqtrade/commands/trade_commands.py new file mode 100644 index 000000000..2c0c4c9c1 --- /dev/null +++ b/freqtrade/commands/trade_commands.py @@ -0,0 +1,25 @@ +import logging + +from typing import Any, Dict + + +logger = logging.getLogger(__name__) + + +def start_trading(args: Dict[str, Any]) -> int: + """ + Main entry point for trading mode + """ + from freqtrade.worker import Worker + # Load and run worker + worker = None + try: + worker = Worker(args) + worker.run() + except KeyboardInterrupt: + logger.info('SIGINT received, aborting ...') + finally: + if worker: + logger.info("worker found ... calling exit") + worker.exit() + return 0 diff --git a/freqtrade/commands/utils.py b/freqtrade/commands/utils.py index 2f7b2d717..ba9173cf2 100644 --- a/freqtrade/commands/utils.py +++ b/freqtrade/commands/utils.py @@ -46,139 +46,6 @@ def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str return config -def start_trading(args: Dict[str, Any]) -> int: - """ - Main entry point for trading mode - """ - from freqtrade.worker import Worker - # Load and run worker - worker = None - try: - worker = Worker(args) - worker.run() - except KeyboardInterrupt: - logger.info('SIGINT received, aborting ...') - finally: - if worker: - logger.info("worker found ... calling exit") - worker.exit() - return 0 - - -def start_list_exchanges(args: Dict[str, Any]) -> None: - """ - Print available exchanges - :param args: Cli args from Arguments() - :return: None - """ - exchanges = ccxt_exchanges() if args['list_exchanges_all'] else available_exchanges() - if args['print_one_column']: - print('\n'.join(exchanges)) - else: - if args['list_exchanges_all']: - print(f"All exchanges supported by the ccxt library: {', '.join(exchanges)}") - else: - print(f"Exchanges available for Freqtrade: {', '.join(exchanges)}") - - -def start_create_userdir(args: Dict[str, Any]) -> None: - """ - Create "user_data" directory to contain user data strategies, hyperopt, ...) - :param args: Cli args from Arguments() - :return: None - """ - if "user_data_dir" in args and args["user_data_dir"]: - userdir = create_userdata_dir(args["user_data_dir"], create_dir=True) - copy_sample_files(userdir, overwrite=args["reset"]) - else: - logger.warning("`create-userdir` requires --userdir to be set.") - sys.exit(1) - - -def deploy_new_strategy(strategy_name, strategy_path: Path, subtemplate: str): - """ - Deploy new strategy from template to strategy_path - """ - indicators = render_template(templatefile=f"subtemplates/indicators_{subtemplate}.j2",) - buy_trend = render_template(templatefile=f"subtemplates/buy_trend_{subtemplate}.j2",) - sell_trend = render_template(templatefile=f"subtemplates/sell_trend_{subtemplate}.j2",) - plot_config = render_template(templatefile=f"subtemplates/plot_config_{subtemplate}.j2",) - - strategy_text = render_template(templatefile='base_strategy.py.j2', - arguments={"strategy": strategy_name, - "indicators": indicators, - "buy_trend": buy_trend, - "sell_trend": sell_trend, - "plot_config": plot_config, - }) - - logger.info(f"Writing strategy to `{strategy_path}`.") - strategy_path.write_text(strategy_text) - - -def start_new_strategy(args: Dict[str, Any]) -> None: - - config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) - - if "strategy" in args and args["strategy"]: - if args["strategy"] == "DefaultStrategy": - raise OperationalException("DefaultStrategy is not allowed as name.") - - new_path = config['user_data_dir'] / USERPATH_STRATEGY / (args["strategy"] + ".py") - - if new_path.exists(): - raise OperationalException(f"`{new_path}` already exists. " - "Please choose another Strategy Name.") - - deploy_new_strategy(args['strategy'], new_path, args['template']) - - else: - raise OperationalException("`new-strategy` requires --strategy to be set.") - - -def deploy_new_hyperopt(hyperopt_name, hyperopt_path: Path, subtemplate: str): - """ - Deploys a new hyperopt template to hyperopt_path - """ - buy_guards = render_template( - templatefile=f"subtemplates/hyperopt_buy_guards_{subtemplate}.j2",) - sell_guards = render_template( - templatefile=f"subtemplates/hyperopt_sell_guards_{subtemplate}.j2",) - buy_space = render_template( - templatefile=f"subtemplates/hyperopt_buy_space_{subtemplate}.j2",) - sell_space = render_template( - templatefile=f"subtemplates/hyperopt_sell_space_{subtemplate}.j2",) - - strategy_text = render_template(templatefile='base_hyperopt.py.j2', - arguments={"hyperopt": hyperopt_name, - "buy_guards": buy_guards, - "sell_guards": sell_guards, - "buy_space": buy_space, - "sell_space": sell_space, - }) - - logger.info(f"Writing hyperopt to `{hyperopt_path}`.") - hyperopt_path.write_text(strategy_text) - - -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": - raise OperationalException("DefaultHyperopt is not allowed as name.") - - new_path = config['user_data_dir'] / USERPATH_HYPEROPTS / (args["hyperopt"] + ".py") - - if new_path.exists(): - raise OperationalException(f"`{new_path}` already exists. " - "Please choose another Strategy Name.") - deploy_new_hyperopt(args['hyperopt'], new_path, args['template']) - else: - raise OperationalException("`new-hyperopt` requires --hyperopt to be set.") - - def start_download_data(args: Dict[str, Any]) -> None: """ Download data (former download_backtest_data.py script) @@ -227,126 +94,6 @@ def start_download_data(args: Dict[str, Any]) -> None: f"on exchange {exchange.name}.") -def start_list_strategies(args: Dict[str, Any]) -> None: - """ - Print Strategies available in a directory - """ - config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) - - directory = Path(config.get('strategy_path', config['user_data_dir'] / USERPATH_STRATEGY)) - strategies = StrategyResolver.search_all_objects(directory) - # Sort alphabetically - strategies = sorted(strategies, key=lambda x: x['name']) - strats_to_print = [{'name': s['name'], 'location': s['location'].name} for s in strategies] - - if args['print_one_column']: - print('\n'.join([s['name'] for s in strategies])) - else: - print(tabulate(strats_to_print, headers='keys', tablefmt='pipe')) - - -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 - - # Init exchange - exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) - - if args['print_one_column']: - print('\n'.join(exchange.timeframes)) - else: - print(f"Timeframes available for the exchange `{exchange.name}`: " - f"{', '.join(exchange.timeframes)}") - - -def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None: - """ - Print pairs/markets on the exchange - :param args: Cli args from Arguments() - :param pairs_only: if True print only pairs, otherwise print all instruments (markets) - :return: None - """ - config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) - - # Init exchange - exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) - - # By default only active pairs/markets are to be shown - active_only = not args.get('list_pairs_all', False) - - base_currencies = args.get('base_currencies', []) - quote_currencies = args.get('quote_currencies', []) - - try: - pairs = exchange.get_markets(base_currencies=base_currencies, - quote_currencies=quote_currencies, - pairs_only=pairs_only, - active_only=active_only) - # Sort the pairs/markets by symbol - pairs = OrderedDict(sorted(pairs.items())) - except Exception as e: - raise OperationalException(f"Cannot get markets. Reason: {e}") from e - - else: - summary_str = ((f"Exchange {exchange.name} has {len(pairs)} ") + - ("active " if active_only else "") + - (plural(len(pairs), "pair" if pairs_only else "market")) + - (f" with {', '.join(base_currencies)} as base " - f"{plural(len(base_currencies), 'currency', 'currencies')}" - if base_currencies else "") + - (" and" if base_currencies and quote_currencies else "") + - (f" with {', '.join(quote_currencies)} as quote " - f"{plural(len(quote_currencies), 'currency', 'currencies')}" - if quote_currencies else "")) - - headers = ["Id", "Symbol", "Base", "Quote", "Active", - *(['Is pair'] if not pairs_only else [])] - - tabular_data = [] - for _, v in pairs.items(): - 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'])} - if not pairs_only else {})}) - - if (args.get('print_one_column', False) or - args.get('list_pairs_print_json', False) or - args.get('print_csv', False)): - # Print summary string in the log in case of machine-readable - # regular formats. - logger.info(f"{summary_str}.") - else: - # Print empty string separating leading logs and output in case of - # human-readable formats. - print() - - if len(pairs): - if args.get('print_list', False): - # print data as a list, with human-readable summary - print(f"{summary_str}: {', '.join(pairs.keys())}.") - elif args.get('print_one_column', False): - print('\n'.join(pairs.keys())) - elif args.get('list_pairs_print_json', False): - print(rapidjson.dumps(list(pairs.keys()), default=str)) - elif args.get('print_csv', False): - writer = csv.DictWriter(sys.stdout, fieldnames=headers) - writer.writeheader() - writer.writerows(tabular_data) - else: - # print data as a table, with the human-readable summary - print(f"{summary_str}:") - print(tabulate(tabular_data, headers='keys', tablefmt='pipe')) - elif not (args.get('print_one_column', False) or - args.get('list_pairs_print_json', False) or - args.get('print_csv', False)): - print(f"{summary_str}.") - - def start_test_pairlist(args: Dict[str, Any]) -> None: """ Test Pairlist configuration @@ -377,106 +124,3 @@ def start_test_pairlist(args: Dict[str, Any]) -> None: print(rapidjson.dumps(list(pairlist), default=str)) else: print(pairlist) - - -def start_hyperopt_list(args: Dict[str, Any]) -> None: - """ - List hyperopt epochs previously evaluated - """ - from freqtrade.optimize.hyperopt import Hyperopt - - config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) - - only_best = config.get('hyperopt_list_best', False) - only_profitable = config.get('hyperopt_list_profitable', False) - print_colorized = config.get('print_colorized', False) - print_json = config.get('print_json', False) - no_details = config.get('hyperopt_list_no_details', False) - no_header = False - - trials_file = (config['user_data_dir'] / - 'hyperopt_results' / 'hyperopt_results.pickle') - - # Previous evaluations - trials = Hyperopt.load_previous_results(trials_file) - total_epochs = len(trials) - - trials = _hyperopt_filter_trials(trials, only_best, only_profitable) - - # TODO: fetch the interval for epochs to print from the cli option - epoch_start, epoch_stop = 0, None - - if print_colorized: - colorama_init(autoreset=True) - - try: - # Human-friendly indexes used here (starting from 1) - for val in trials[epoch_start:epoch_stop]: - Hyperopt.print_results_explanation(val, total_epochs, not only_best, print_colorized) - - except KeyboardInterrupt: - print('User interrupted..') - - if trials and not no_details: - sorted_trials = sorted(trials, key=itemgetter('loss')) - results = sorted_trials[0] - Hyperopt.print_epoch_details(results, total_epochs, print_json, no_header) - - -def start_hyperopt_show(args: Dict[str, Any]) -> None: - """ - Show details of a hyperopt epoch previously evaluated - """ - from freqtrade.optimize.hyperopt import Hyperopt - - config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE) - - only_best = config.get('hyperopt_list_best', False) - only_profitable = config.get('hyperopt_list_profitable', False) - no_header = config.get('hyperopt_show_no_header', False) - - trials_file = (config['user_data_dir'] / - 'hyperopt_results' / 'hyperopt_results.pickle') - - # Previous evaluations - trials = Hyperopt.load_previous_results(trials_file) - total_epochs = len(trials) - - trials = _hyperopt_filter_trials(trials, only_best, only_profitable) - trials_epochs = len(trials) - - n = config.get('hyperopt_show_index', -1) - if n > trials_epochs: - raise OperationalException( - f"The index of the epoch to show should be less than {trials_epochs + 1}.") - if n < -trials_epochs: - raise OperationalException( - f"The index of the epoch to show should be greater than {-trials_epochs - 1}.") - - # Translate epoch index from human-readable format to pythonic - if n > 0: - n -= 1 - - print_json = config.get('print_json', False) - - if trials: - val = trials[n] - Hyperopt.print_epoch_details(val, total_epochs, print_json, no_header, - header_str="Epoch details") - - -def _hyperopt_filter_trials(trials: List, only_best: bool, only_profitable: bool) -> List: - """ - Filter our items from the list of hyperopt results - """ - if only_best: - trials = [x for x in trials if x['is_best']] - if only_profitable: - trials = [x for x in trials if x['results_metrics']['profit'] > 0] - - logger.info(f"{len(trials)} " + - ("best " if only_best else "") + - ("profitable " if only_profitable else "") + - "epochs found.") - - return trials From 7e233041877d7c4976735e10269a2f3421c530f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Jan 2020 13:09:13 +0100 Subject: [PATCH 306/324] Adjust tests to new paths --- tests/test_utils.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index 91009ec42..c686d2117 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -451,8 +451,8 @@ def test_create_datadir(caplog, mocker): # Added assert here to analyze random test-failures ... assert len(caplog.record_tuples) == 0 - cud = mocker.patch("freqtrade.commands.create_userdata_dir", MagicMock()) - csf = mocker.patch("freqtrade.commands.copy_sample_files", MagicMock()) + cud = mocker.patch("freqtrade.commands.deploy_commands.create_userdata_dir", MagicMock()) + csf = mocker.patch("freqtrade.commands.deploy_commands.copy_sample_files", MagicMock()) args = [ "create-userdir", "--userdir", @@ -538,7 +538,7 @@ def test_start_new_hyperopt_no_arg(mocker, caplog): def test_download_data_keyboardInterrupt(mocker, caplog, markets): - dl_mock = mocker.patch('freqtrade.commands.refresh_backtest_ohlcv_data', + dl_mock = mocker.patch('freqtrade.commands.utils.refresh_backtest_ohlcv_data', MagicMock(side_effect=KeyboardInterrupt)) patch_exchange(mocker) mocker.patch( @@ -556,7 +556,7 @@ def test_download_data_keyboardInterrupt(mocker, caplog, markets): def test_download_data_no_markets(mocker, caplog): - dl_mock = mocker.patch('freqtrade.commands.refresh_backtest_ohlcv_data', + dl_mock = mocker.patch('freqtrade.commands.utils.refresh_backtest_ohlcv_data', MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) patch_exchange(mocker, id='binance') mocker.patch( @@ -574,7 +574,7 @@ def test_download_data_no_markets(mocker, caplog): def test_download_data_no_exchange(mocker, caplog): - mocker.patch('freqtrade.commands.refresh_backtest_ohlcv_data', + mocker.patch('freqtrade.commands.utils.refresh_backtest_ohlcv_data', MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) patch_exchange(mocker) mocker.patch( @@ -594,7 +594,7 @@ def test_download_data_no_pairs(mocker, caplog): mocker.patch.object(Path, "exists", MagicMock(return_value=False)) - mocker.patch('freqtrade.commands.refresh_backtest_ohlcv_data', + mocker.patch('freqtrade.commands.utils.refresh_backtest_ohlcv_data', MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) patch_exchange(mocker) mocker.patch( @@ -613,9 +613,9 @@ def test_download_data_no_pairs(mocker, caplog): def test_download_data_trades(mocker, caplog): - dl_mock = mocker.patch('freqtrade.commands.refresh_backtest_trades_data', + dl_mock = mocker.patch('freqtrade.commands.utils.refresh_backtest_trades_data', MagicMock(return_value=[])) - convert_mock = mocker.patch('freqtrade.commands.convert_trades_to_ohlcv', + convert_mock = mocker.patch('freqtrade.commands.utils.convert_trades_to_ohlcv', MagicMock(return_value=[])) patch_exchange(mocker) mocker.patch( From 70a0346b0a2b781dda480dc1a00da9215ac80eea Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Jan 2020 13:17:26 +0100 Subject: [PATCH 307/324] Move data-stuff to data-commands --- freqtrade/commands/__init__.py | 16 +++---- freqtrade/commands/data_commands.py | 64 +++++++++++++++++++++++++ freqtrade/commands/utils.py | 73 ++--------------------------- tests/test_utils.py | 12 ++--- 4 files changed, 81 insertions(+), 84 deletions(-) create mode 100644 freqtrade/commands/data_commands.py diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index 4c57efeae..5fa667ccc 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -1,10 +1,10 @@ -from .hyperopt_commands import (start_hyperopt_list, start_hyperopt_show) # noqa: 401 - -from .list_commands import (start_list_exchanges, # noqa: F401 +from freqtrade.commands.data_commands import start_download_data # noqa: F401 +from freqtrade.commands.deploy_commands import (start_create_userdir, # noqa: F401 + start_new_hyperopt, start_new_strategy) +from freqtrade.commands.hyperopt_commands import (start_hyperopt_list, # noqa: F401 + start_hyperopt_show) +from freqtrade.commands.list_commands import (start_list_exchanges, # noqa: F401 start_list_markets, start_list_strategies, start_list_timeframes) -from .utils import setup_utils_configuration # noqa: F401 -from .utils import (start_download_data, # noqa: F401 - start_test_pairlist) -from .deploy_commands import (start_new_hyperopt, start_new_strategy, start_create_userdir) # noqa: F401 -from .trade_commands import start_trading # noqa: F401 +from freqtrade.commands.trade_commands import start_trading # noqa: F401 +from freqtrade.commands.utils import setup_utils_configuration, start_test_pairlist # noqa: F401 diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py new file mode 100644 index 000000000..b1c847d1d --- /dev/null +++ b/freqtrade/commands/data_commands.py @@ -0,0 +1,64 @@ +import logging +import sys +from typing import Any, Dict, List + +import arrow + +from freqtrade.configuration import TimeRange +from freqtrade.data.history import (convert_trades_to_ohlcv, + refresh_backtest_ohlcv_data, + refresh_backtest_trades_data) +from freqtrade.exceptions import OperationalException +from freqtrade.resolvers import ExchangeResolver +from freqtrade.state import RunMode +from .utils import setup_utils_configuration + +logger = logging.getLogger(__name__) + + +def start_download_data(args: Dict[str, Any]) -> None: + """ + Download data (former download_backtest_data.py script) + """ + config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) + + 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 'pairs' not in config: + raise OperationalException( + "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"]}') + + pairs_not_available: List[str] = [] + + # Init exchange + exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config) + try: + + if config.get('download_trades'): + pairs_not_available = refresh_backtest_trades_data( + exchange, pairs=config["pairs"], datadir=config['datadir'], + timerange=timerange, erase=config.get("erase")) + + # Convert downloaded trade data to different timeframes + convert_trades_to_ohlcv( + pairs=config["pairs"], timeframes=config["timeframes"], + datadir=config['datadir'], timerange=timerange, erase=config.get("erase")) + else: + pairs_not_available = refresh_backtest_ohlcv_data( + exchange, pairs=config["pairs"], timeframes=config["timeframes"], + datadir=config['datadir'], timerange=timerange, erase=config.get("erase")) + + except KeyboardInterrupt: + sys.exit("SIGINT received, aborting ...") + + finally: + if pairs_not_available: + logger.info(f"Pairs [{','.join(pairs_not_available)}] not available " + f"on exchange {exchange.name}.") diff --git a/freqtrade/commands/utils.py b/freqtrade/commands/utils.py index ba9173cf2..a597d5335 100644 --- a/freqtrade/commands/utils.py +++ b/freqtrade/commands/utils.py @@ -1,30 +1,11 @@ -import csv import logging -import sys -from collections import OrderedDict -from operator import itemgetter -from pathlib import Path -from typing import Any, Dict, List +from typing import Any, Dict -import arrow import rapidjson -from colorama import init as colorama_init -from tabulate import tabulate -from freqtrade.configuration import (Configuration, TimeRange, - remove_credentials, +from freqtrade.configuration import (Configuration, remove_credentials, validate_config_consistency) -from freqtrade.configuration.directory_operations import (copy_sample_files, - create_userdata_dir) -from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGY -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 (available_exchanges, ccxt_exchanges, - market_is_active, symbol_is_pair) -from freqtrade.misc import plural, render_template -from freqtrade.resolvers import ExchangeResolver, StrategyResolver +from freqtrade.resolvers import ExchangeResolver from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -46,54 +27,6 @@ def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str return config -def start_download_data(args: Dict[str, Any]) -> None: - """ - Download data (former download_backtest_data.py script) - """ - config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) - - 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 'pairs' not in config: - raise OperationalException( - "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"]}') - - pairs_not_available: List[str] = [] - - # Init exchange - exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config) - try: - - if config.get('download_trades'): - pairs_not_available = refresh_backtest_trades_data( - exchange, pairs=config["pairs"], datadir=config['datadir'], - timerange=timerange, erase=config.get("erase")) - - # Convert downloaded trade data to different timeframes - convert_trades_to_ohlcv( - pairs=config["pairs"], timeframes=config["timeframes"], - datadir=config['datadir'], timerange=timerange, erase=config.get("erase")) - else: - pairs_not_available = refresh_backtest_ohlcv_data( - exchange, pairs=config["pairs"], timeframes=config["timeframes"], - datadir=config['datadir'], timerange=timerange, erase=config.get("erase")) - - except KeyboardInterrupt: - sys.exit("SIGINT received, aborting ...") - - finally: - if pairs_not_available: - logger.info(f"Pairs [{','.join(pairs_not_available)}] not available " - f"on exchange {exchange.name}.") - - def start_test_pairlist(args: Dict[str, Any]) -> None: """ Test Pairlist configuration diff --git a/tests/test_utils.py b/tests/test_utils.py index c686d2117..1328e3981 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -538,7 +538,7 @@ def test_start_new_hyperopt_no_arg(mocker, caplog): def test_download_data_keyboardInterrupt(mocker, caplog, markets): - dl_mock = mocker.patch('freqtrade.commands.utils.refresh_backtest_ohlcv_data', + dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(side_effect=KeyboardInterrupt)) patch_exchange(mocker) mocker.patch( @@ -556,7 +556,7 @@ def test_download_data_keyboardInterrupt(mocker, caplog, markets): def test_download_data_no_markets(mocker, caplog): - dl_mock = mocker.patch('freqtrade.commands.utils.refresh_backtest_ohlcv_data', + dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) patch_exchange(mocker, id='binance') mocker.patch( @@ -574,7 +574,7 @@ def test_download_data_no_markets(mocker, caplog): def test_download_data_no_exchange(mocker, caplog): - mocker.patch('freqtrade.commands.utils.refresh_backtest_ohlcv_data', + mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) patch_exchange(mocker) mocker.patch( @@ -594,7 +594,7 @@ def test_download_data_no_pairs(mocker, caplog): mocker.patch.object(Path, "exists", MagicMock(return_value=False)) - mocker.patch('freqtrade.commands.utils.refresh_backtest_ohlcv_data', + mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data', MagicMock(return_value=["ETH/BTC", "XRP/BTC"])) patch_exchange(mocker) mocker.patch( @@ -613,9 +613,9 @@ def test_download_data_no_pairs(mocker, caplog): def test_download_data_trades(mocker, caplog): - dl_mock = mocker.patch('freqtrade.commands.utils.refresh_backtest_trades_data', + dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_trades_data', MagicMock(return_value=[])) - convert_mock = mocker.patch('freqtrade.commands.utils.convert_trades_to_ohlcv', + convert_mock = mocker.patch('freqtrade.commands.data_commands.convert_trades_to_ohlcv', MagicMock(return_value=[])) patch_exchange(mocker) mocker.patch( From b254bdfea31c95b237b2642c6b55050d1e727b45 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Jan 2020 13:20:34 +0100 Subject: [PATCH 308/324] Move plot_utils to plot_commands --- freqtrade/commands/__init__.py | 1 + freqtrade/{plot/plot_utils.py => commands/plot_commands.py} | 2 +- freqtrade/configuration/arguments.py | 2 +- tests/test_plotting.py | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) rename freqtrade/{plot/plot_utils.py => commands/plot_commands.py} (94%) diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index 5fa667ccc..45088e8a3 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -6,5 +6,6 @@ from freqtrade.commands.hyperopt_commands import (start_hyperopt_list, # noqa: from freqtrade.commands.list_commands import (start_list_exchanges, # noqa: F401 start_list_markets, start_list_strategies, start_list_timeframes) +from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit # noqa: F401 from freqtrade.commands.trade_commands import start_trading # noqa: F401 from freqtrade.commands.utils import setup_utils_configuration, start_test_pairlist # noqa: F401 diff --git a/freqtrade/plot/plot_utils.py b/freqtrade/commands/plot_commands.py similarity index 94% rename from freqtrade/plot/plot_utils.py rename to freqtrade/commands/plot_commands.py index b2f16dff0..4c68cc3c5 100644 --- a/freqtrade/plot/plot_utils.py +++ b/freqtrade/commands/plot_commands.py @@ -2,7 +2,7 @@ from typing import Any, Dict from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode -from freqtrade.commands import setup_utils_configuration +from freqtrade.commands.utils import setup_utils_configuration def validate_plot_args(args: Dict[str, Any]): diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index 3c71fa548..c61e4ec13 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -136,8 +136,8 @@ class Arguments: start_list_exchanges, start_list_markets, start_list_strategies, start_new_hyperopt, start_new_strategy, start_list_timeframes, + start_plot_dataframe, start_plot_profit, start_test_pairlist, start_trading) - from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit subparsers = self.parser.add_subparsers(dest='command', # Use custom message when no subhandler is added diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 78c01eb97..e7ec4ce46 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -11,7 +11,7 @@ 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.plot_utils import start_plot_dataframe, start_plot_profit +from freqtrade.commands import start_plot_dataframe, start_plot_profit from freqtrade.plot.plotting import (add_indicators, add_profit, create_plotconfig, generate_candlestick_graph, From e033df6a2fce1d50333e70e599cd7dfde187b162 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Jan 2020 13:32:52 +0100 Subject: [PATCH 309/324] Move optimize_commands to commands module --- .../__init__.py => commands/optimize_commands.py} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename freqtrade/{optimize/__init__.py => commands/optimize_commands.py} (89%) diff --git a/freqtrade/optimize/__init__.py b/freqtrade/commands/optimize_commands.py similarity index 89% rename from freqtrade/optimize/__init__.py rename to freqtrade/commands/optimize_commands.py index 98b521d4e..e635e61fd 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/commands/optimize_commands.py @@ -2,14 +2,14 @@ import logging from typing import Any, Dict from freqtrade import constants +from freqtrade.commands.utils import setup_utils_configuration from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.state import RunMode -from freqtrade.commands import setup_utils_configuration logger = logging.getLogger(__name__) -def setup_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str, Any]: +def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str, Any]: """ Prepare the configuration for the Hyperopt module :param args: Cli args from Arguments() @@ -35,7 +35,7 @@ def start_backtesting(args: Dict[str, Any]) -> None: from freqtrade.optimize.backtesting import Backtesting # Initialize configuration - config = setup_configuration(args, RunMode.BACKTEST) + config = setup_optimize_configuration(args, RunMode.BACKTEST) logger.info('Starting freqtrade in Backtesting mode') @@ -58,7 +58,7 @@ def start_hyperopt(args: Dict[str, Any]) -> None: raise OperationalException( f"{e}. Please ensure that the hyperopt dependencies are installed.") from e # Initialize configuration - config = setup_configuration(args, RunMode.HYPEROPT) + config = setup_optimize_configuration(args, RunMode.HYPEROPT) logger.info('Starting freqtrade in Hyperopt mode') @@ -94,7 +94,7 @@ def start_edge(args: Dict[str, Any]) -> None: """ from freqtrade.optimize.edge_cli import EdgeCli # Initialize configuration - config = setup_configuration(args, RunMode.EDGE) + config = setup_optimize_configuration(args, RunMode.EDGE) logger.info('Starting freqtrade in Edge mode') # Initialize Edge object From f347e5934a6be5d515c983daac4755ee3a8ec9ca Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Jan 2020 13:33:13 +0100 Subject: [PATCH 310/324] Small adjustments for moved commands --- freqtrade/commands/__init__.py | 30 ++++++++++++++++++---------- freqtrade/configuration/arguments.py | 2 +- freqtrade/optimize/__init__.py | 0 tests/optimize/test_backtesting.py | 12 +++++------ tests/optimize/test_edge_cli.py | 8 ++++---- tests/optimize/test_hyperopt.py | 8 ++++---- tests/test_main.py | 4 ++-- 7 files changed, 36 insertions(+), 28 deletions(-) create mode 100644 freqtrade/optimize/__init__.py diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index 45088e8a3..8339486fb 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -1,11 +1,19 @@ -from freqtrade.commands.data_commands import start_download_data # noqa: F401 -from freqtrade.commands.deploy_commands import (start_create_userdir, # noqa: F401 - start_new_hyperopt, start_new_strategy) -from freqtrade.commands.hyperopt_commands import (start_hyperopt_list, # noqa: F401 - start_hyperopt_show) -from freqtrade.commands.list_commands import (start_list_exchanges, # noqa: F401 - start_list_markets, start_list_strategies, - start_list_timeframes) -from freqtrade.commands.plot_commands import start_plot_dataframe, start_plot_profit # noqa: F401 -from freqtrade.commands.trade_commands import start_trading # noqa: F401 -from freqtrade.commands.utils import setup_utils_configuration, start_test_pairlist # noqa: F401 +# flake8: noqa: F401 + +from freqtrade.commands.data_commands import start_download_data +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_markets, + start_list_strategies, + start_list_timeframes) +from freqtrade.commands.optimize_commands import (start_backtesting, + start_edge, start_hyperopt) +from freqtrade.commands.plot_commands import (start_plot_dataframe, + start_plot_profit) +from freqtrade.commands.trade_commands import start_trading +from freqtrade.commands.utils import (setup_utils_configuration, + start_test_pairlist) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/configuration/arguments.py index c61e4ec13..21800e821 100644 --- a/freqtrade/configuration/arguments.py +++ b/freqtrade/configuration/arguments.py @@ -130,13 +130,13 @@ class Arguments: self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot') self._build_args(optionlist=['version'], parser=self.parser) - from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge from freqtrade.commands import (start_create_userdir, start_download_data, start_hyperopt_list, start_hyperopt_show, start_list_exchanges, start_list_markets, start_list_strategies, start_new_hyperopt, start_new_strategy, start_list_timeframes, start_plot_dataframe, start_plot_profit, + start_backtesting, start_hyperopt, start_edge, start_test_pairlist, start_trading) subparsers = self.parser.add_subparsers(dest='command', diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index acbc44e21..07872da57 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -12,13 +12,13 @@ from arrow import Arrow from freqtrade import constants from freqtrade.configuration import TimeRange +from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_backtesting from freqtrade.data import history from freqtrade.data.btanalysis import evaluate_result_multi from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import get_timerange from freqtrade.exceptions import DependencyException, OperationalException -from freqtrade.optimize import setup_configuration, start_backtesting from freqtrade.optimize.backtesting import Backtesting from freqtrade.state import RunMode from freqtrade.strategy.default_strategy import DefaultStrategy @@ -177,7 +177,7 @@ def _trend_alternate(dataframe=None, metadata=None): # Unit tests -def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: +def test_setup_optimize_configuration_without_arguments(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) args = [ @@ -186,7 +186,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> '--strategy', 'DefaultStrategy', ] - config = setup_configuration(get_args(args), RunMode.BACKTEST) + config = setup_optimize_configuration(get_args(args), RunMode.BACKTEST) assert 'max_open_trades' in config assert 'stake_currency' in config assert 'stake_amount' in config @@ -227,7 +227,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> '--fee', '0', ] - config = setup_configuration(get_args(args), RunMode.BACKTEST) + config = setup_optimize_configuration(get_args(args), RunMode.BACKTEST) assert 'max_open_trades' in config assert 'stake_currency' in config assert 'stake_amount' in config @@ -260,7 +260,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> assert log_has('Parameter --fee detected, setting fee to: {} ...'.format(config['fee']), caplog) -def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None: +def test_setup_optimize_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None: default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT patched_configuration_load_config_file(mocker, default_conf) @@ -272,7 +272,7 @@ def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog ] with pytest.raises(DependencyException, match=r'.*stake amount.*'): - setup_configuration(get_args(args), RunMode.BACKTEST) + setup_optimize_configuration(get_args(args), RunMode.BACKTEST) def test_start(mocker, fee, default_conf, caplog) -> None: diff --git a/tests/optimize/test_edge_cli.py b/tests/optimize/test_edge_cli.py index acc0d2d16..96dd0899d 100644 --- a/tests/optimize/test_edge_cli.py +++ b/tests/optimize/test_edge_cli.py @@ -3,14 +3,14 @@ from unittest.mock import MagicMock -from freqtrade.optimize import setup_configuration, start_edge +from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_edge from freqtrade.optimize.edge_cli import EdgeCli from freqtrade.state import RunMode from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) -def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: +def test_setup_optimize_configuration_without_arguments(mocker, default_conf, caplog) -> None: patched_configuration_load_config_file(mocker, default_conf) args = [ @@ -19,7 +19,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> '--strategy', 'DefaultStrategy', ] - config = setup_configuration(get_args(args), RunMode.EDGE) + config = setup_optimize_configuration(get_args(args), RunMode.EDGE) assert config['runmode'] == RunMode.EDGE assert 'max_open_trades' in config @@ -53,7 +53,7 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N '--stoplosses=-0.01,-0.10,-0.001' ] - config = setup_configuration(get_args(args), RunMode.EDGE) + config = setup_optimize_configuration(get_args(args), RunMode.EDGE) assert 'max_open_trades' in config assert 'stake_currency' in config assert 'stake_amount' in config diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 473d760ac..69d110649 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -9,10 +9,10 @@ import pytest from arrow import Arrow from filelock import Timeout -from freqtrade.exceptions import OperationalException +from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.history import load_tickerdata_file -from freqtrade.optimize import setup_configuration, start_hyperopt +from freqtrade.exceptions import OperationalException from freqtrade.optimize.default_hyperopt import DefaultHyperOpt from freqtrade.optimize.default_hyperopt_loss import DefaultHyperOptLoss from freqtrade.optimize.hyperopt import Hyperopt @@ -77,7 +77,7 @@ def test_setup_hyperopt_configuration_without_arguments(mocker, default_conf, ca '--hyperopt', 'DefaultHyperOpt', ] - config = setup_configuration(get_args(args), RunMode.HYPEROPT) + config = setup_optimize_configuration(get_args(args), RunMode.HYPEROPT) assert 'max_open_trades' in config assert 'stake_currency' in config assert 'stake_amount' in config @@ -117,7 +117,7 @@ def test_setup_hyperopt_configuration_with_arguments(mocker, default_conf, caplo '--print-all' ] - config = setup_configuration(get_args(args), RunMode.HYPEROPT) + config = setup_optimize_configuration(get_args(args), RunMode.HYPEROPT) assert 'max_open_trades' in config assert 'stake_currency' in config assert 'stake_amount' in config diff --git a/tests/test_main.py b/tests/test_main.py index 76b1bf658..462ac6427 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -26,7 +26,7 @@ def test_parse_args_backtesting(mocker) -> None: Test that main() can start backtesting and also ensure we can pass some specific arguments further argument parsing is done in test_arguments.py """ - backtesting_mock = mocker.patch('freqtrade.optimize.start_backtesting', MagicMock()) + backtesting_mock = mocker.patch('freqtrade.commands.start_backtesting') backtesting_mock.__name__ = PropertyMock("start_backtesting") # it's sys.exit(0) at the end of backtesting with pytest.raises(SystemExit): @@ -42,7 +42,7 @@ def test_parse_args_backtesting(mocker) -> None: def test_main_start_hyperopt(mocker) -> None: - hyperopt_mock = mocker.patch('freqtrade.optimize.start_hyperopt', MagicMock()) + hyperopt_mock = mocker.patch('freqtrade.commands.start_hyperopt', MagicMock()) hyperopt_mock.__name__ = PropertyMock("start_hyperopt") # it's sys.exit(0) at the end of hyperopt with pytest.raises(SystemExit): From a1c684f67ca3dcf0c6293d71511c72a0dcbd2cb3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Jan 2020 13:36:33 +0100 Subject: [PATCH 311/324] Simplify noqa setup for module imports --- freqtrade/configuration/__init__.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py index 63c38d8c5..a631609d7 100644 --- a/freqtrade/configuration/__init__.py +++ b/freqtrade/configuration/__init__.py @@ -1,5 +1,7 @@ -from freqtrade.configuration.arguments import Arguments # noqa: F401 -from freqtrade.configuration.check_exchange import check_exchange, remove_credentials # noqa: F401 -from freqtrade.configuration.timerange import TimeRange # noqa: F401 -from freqtrade.configuration.configuration import Configuration # noqa: F401 -from freqtrade.configuration.config_validation import validate_config_consistency # noqa: F401 +# flake8: noqa: F401 + +from freqtrade.configuration.arguments import Arguments +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_validation import validate_config_consistency From 7f851ad8d950d5c26da08f76ad239610136af51c Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Jan 2020 13:38:56 +0100 Subject: [PATCH 312/324] Move arguments and cli_options to commands module --- freqtrade/{configuration => commands}/arguments.py | 0 freqtrade/{configuration => commands}/cli_options.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename freqtrade/{configuration => commands}/arguments.py (100%) rename freqtrade/{configuration => commands}/cli_options.py (100%) diff --git a/freqtrade/configuration/arguments.py b/freqtrade/commands/arguments.py similarity index 100% rename from freqtrade/configuration/arguments.py rename to freqtrade/commands/arguments.py diff --git a/freqtrade/configuration/cli_options.py b/freqtrade/commands/cli_options.py similarity index 100% rename from freqtrade/configuration/cli_options.py rename to freqtrade/commands/cli_options.py From a3e9d04383bdc37aea465561306334042adc801a Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Jan 2020 13:41:04 +0100 Subject: [PATCH 313/324] Adjust imports to new place for arguments --- freqtrade/commands/__init__.py | 1 + freqtrade/commands/arguments.py | 2 +- freqtrade/commands/cli_options.py | 6 +++--- freqtrade/configuration/__init__.py | 1 - freqtrade/main.py | 2 +- tests/conftest.py | 2 +- tests/test_arguments.py | 4 ++-- tests/test_configuration.py | 3 ++- tests/test_main.py | 2 +- 9 files changed, 12 insertions(+), 11 deletions(-) diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index 8339486fb..dcb814c50 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -1,5 +1,6 @@ # flake8: noqa: F401 +from freqtrade.commands.arguments import Arguments from freqtrade.commands.data_commands import start_download_data from freqtrade.commands.deploy_commands import (start_create_userdir, start_new_hyperopt, diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 21800e821..724814554 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -7,7 +7,7 @@ from pathlib import Path from typing import Any, Dict, List, Optional from freqtrade import constants -from freqtrade.configuration.cli_options import AVAILABLE_CLI_OPTIONS +from freqtrade.commands.cli_options import AVAILABLE_CLI_OPTIONS ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"] diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 1807cd591..490f26cfa 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -1,7 +1,7 @@ """ Definition of cli arguments used in arguments.py """ -import argparse +from argparse import ArgumentTypeError from freqtrade import __version__, constants @@ -12,7 +12,7 @@ def check_int_positive(value: str) -> int: if uint <= 0: raise ValueError except ValueError: - raise argparse.ArgumentTypeError( + raise ArgumentTypeError( f"{value} is invalid for this parameter, should be a positive integer value" ) return uint @@ -24,7 +24,7 @@ def check_int_nonzero(value: str) -> int: if uint == 0: raise ValueError except ValueError: - raise argparse.ArgumentTypeError( + raise ArgumentTypeError( f"{value} is invalid for this parameter, should be a non-zero integer value" ) return uint diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py index a631609d7..54fc4e427 100644 --- a/freqtrade/configuration/__init__.py +++ b/freqtrade/configuration/__init__.py @@ -1,6 +1,5 @@ # flake8: noqa: F401 -from freqtrade.configuration.arguments import Arguments from freqtrade.configuration.check_exchange import check_exchange, remove_credentials from freqtrade.configuration.timerange import TimeRange from freqtrade.configuration.configuration import Configuration diff --git a/freqtrade/main.py b/freqtrade/main.py index 811e29864..a75eeebed 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -14,7 +14,7 @@ if sys.version_info < (3, 6): import logging from typing import Any, List -from freqtrade.configuration import Arguments +from freqtrade.commands import Arguments logger = logging.getLogger('freqtrade') diff --git a/tests/conftest.py b/tests/conftest.py index 295c91f56..395388f73 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,7 @@ import pytest from telegram import Chat, Message, Update from freqtrade import constants, persistence -from freqtrade.configuration import Arguments +from freqtrade.commands import Arguments from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.edge import Edge, PairInfo from freqtrade.exchange import Exchange diff --git a/tests/test_arguments.py b/tests/test_arguments.py index d8fbace0f..60da0082a 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -5,8 +5,8 @@ from unittest.mock import MagicMock import pytest -from freqtrade.configuration import Arguments -from freqtrade.configuration.cli_options import check_int_positive +from freqtrade.commands import Arguments +from freqtrade.commands.cli_options import check_int_positive # Parse common command-line-arguments. Used for all tools diff --git a/tests/test_configuration.py b/tests/test_configuration.py index cbcd6416a..74de166c1 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -10,7 +10,8 @@ from unittest.mock import MagicMock import pytest from jsonschema import ValidationError -from freqtrade.configuration import (Arguments, Configuration, check_exchange, +from freqtrade.commands import Arguments +from freqtrade.configuration import (Configuration, check_exchange, remove_credentials, validate_config_consistency) from freqtrade.configuration.config_validation import validate_config_schema diff --git a/tests/test_main.py b/tests/test_main.py index 462ac6427..1229f748a 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -5,7 +5,7 @@ from unittest.mock import MagicMock, PropertyMock import pytest -from freqtrade.configuration import Arguments +from freqtrade.commands import Arguments from freqtrade.exceptions import OperationalException, FreqtradeException from freqtrade.freqtradebot import FreqtradeBot from freqtrade.main import main From 2d02c3f2a4097aa56182ff9d56bf4f631ac06696 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Jan 2020 13:43:30 +0100 Subject: [PATCH 314/324] Split out pairlist_commands --- freqtrade/commands/__init__.py | 4 +-- freqtrade/commands/pairlist_commands.py | 43 +++++++++++++++++++++++++ freqtrade/commands/utils.py | 35 -------------------- 3 files changed, 45 insertions(+), 37 deletions(-) create mode 100644 freqtrade/commands/pairlist_commands.py diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index dcb814c50..e16a6f203 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -13,8 +13,8 @@ from freqtrade.commands.list_commands import (start_list_exchanges, start_list_timeframes) 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.trade_commands import start_trading -from freqtrade.commands.utils import (setup_utils_configuration, - start_test_pairlist) +from freqtrade.commands.utils import setup_utils_configuration diff --git a/freqtrade/commands/pairlist_commands.py b/freqtrade/commands/pairlist_commands.py new file mode 100644 index 000000000..06ba2e680 --- /dev/null +++ b/freqtrade/commands/pairlist_commands.py @@ -0,0 +1,43 @@ +import logging +from typing import Any, Dict + +import rapidjson + +from freqtrade.resolvers import ExchangeResolver +from freqtrade.state import RunMode + +from .utils import setup_utils_configuration + +logger = logging.getLogger(__name__) + + +def start_test_pairlist(args: Dict[str, Any]) -> None: + """ + Test Pairlist configuration + """ + from freqtrade.pairlist.pairlistmanager import PairListManager + config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) + + exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) + + quote_currencies = args.get('quote_currencies') + if not quote_currencies: + quote_currencies = [config.get('stake_currency')] + results = {} + for curr in quote_currencies: + config['stake_currency'] = curr + # Do not use ticker_interval set in the config + pairlists = PairListManager(exchange, config) + pairlists.refresh_pairlist() + results[curr] = pairlists.whitelist + + for curr, pairlist in results.items(): + if not args.get('print_one_column', False): + print(f"Pairs for {curr}: ") + + if args.get('print_one_column', False): + print('\n'.join(pairlist)) + elif args.get('list_pairs_print_json', False): + print(rapidjson.dumps(list(pairlist), default=str)) + else: + print(pairlist) diff --git a/freqtrade/commands/utils.py b/freqtrade/commands/utils.py index a597d5335..f77a35383 100644 --- a/freqtrade/commands/utils.py +++ b/freqtrade/commands/utils.py @@ -1,11 +1,8 @@ import logging from typing import Any, Dict -import rapidjson - from freqtrade.configuration import (Configuration, remove_credentials, validate_config_consistency) -from freqtrade.resolvers import ExchangeResolver from freqtrade.state import RunMode logger = logging.getLogger(__name__) @@ -25,35 +22,3 @@ def setup_utils_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str validate_config_consistency(config) return config - - -def start_test_pairlist(args: Dict[str, Any]) -> None: - """ - Test Pairlist configuration - """ - from freqtrade.pairlist.pairlistmanager import PairListManager - config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE) - - exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False) - - quote_currencies = args.get('quote_currencies') - if not quote_currencies: - quote_currencies = [config.get('stake_currency')] - results = {} - for curr in quote_currencies: - config['stake_currency'] = curr - # Do not use ticker_interval set in the config - pairlists = PairListManager(exchange, config) - pairlists.refresh_pairlist() - results[curr] = pairlists.whitelist - - for curr, pairlist in results.items(): - if not args.get('print_one_column', False): - print(f"Pairs for {curr}: ") - - if args.get('print_one_column', False): - print('\n'.join(pairlist)) - elif args.get('list_pairs_print_json', False): - print(rapidjson.dumps(list(pairlist), default=str)) - else: - print(pairlist) From 8c9119b4710e467d4833638d582aba5914d61d2b Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Jan 2020 13:45:37 +0100 Subject: [PATCH 315/324] Add docustring to commands module --- freqtrade/commands/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index e16a6f203..1dadbec1e 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -1,5 +1,11 @@ # flake8: noqa: F401 +""" +Commands module. +Contains all start-commands, subcommands and CLI Interface creation. +Note: Be careful with file-scoped imports in these subfiles. + as they are parsed on startup, nothing containing optional modules should be loaded. +""" from freqtrade.commands.arguments import Arguments from freqtrade.commands.data_commands import start_download_data from freqtrade.commands.deploy_commands import (start_create_userdir, From 02563019fc84d559584ae007c6c9d5f4055da911 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Jan 2020 13:55:48 +0100 Subject: [PATCH 316/324] move setup_utils_config to configuration module --- freqtrade/commands/__init__.py | 1 - freqtrade/commands/data_commands.py | 3 +-- freqtrade/commands/deploy_commands.py | 3 +-- freqtrade/commands/hyperopt_commands.py | 2 +- freqtrade/commands/list_commands.py | 3 +-- freqtrade/commands/optimize_commands.py | 2 +- freqtrade/commands/pairlist_commands.py | 3 +-- freqtrade/commands/plot_commands.py | 2 +- freqtrade/configuration/__init__.py | 1 + .../{commands/utils.py => configuration/config_setup.py} | 5 +++-- tests/commands/__init__.py | 0 tests/{test_utils.py => commands/test_commands.py} | 8 ++++---- 12 files changed, 15 insertions(+), 18 deletions(-) rename freqtrade/{commands/utils.py => configuration/config_setup.py} (79%) create mode 100644 tests/commands/__init__.py rename tests/{test_utils.py => commands/test_commands.py} (99%) diff --git a/freqtrade/commands/__init__.py b/freqtrade/commands/__init__.py index 1dadbec1e..990c1107a 100644 --- a/freqtrade/commands/__init__.py +++ b/freqtrade/commands/__init__.py @@ -23,4 +23,3 @@ from freqtrade.commands.pairlist_commands import start_test_pairlist from freqtrade.commands.plot_commands import (start_plot_dataframe, start_plot_profit) from freqtrade.commands.trade_commands import start_trading -from freqtrade.commands.utils import setup_utils_configuration diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index b1c847d1d..c01772023 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -4,14 +4,13 @@ from typing import Any, Dict, List import arrow -from freqtrade.configuration import TimeRange +from freqtrade.configuration import TimeRange, setup_utils_configuration from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data, refresh_backtest_trades_data) from freqtrade.exceptions import OperationalException from freqtrade.resolvers import ExchangeResolver from freqtrade.state import RunMode -from .utils import setup_utils_configuration logger = logging.getLogger(__name__) diff --git a/freqtrade/commands/deploy_commands.py b/freqtrade/commands/deploy_commands.py index 892e15db1..99ae63244 100644 --- a/freqtrade/commands/deploy_commands.py +++ b/freqtrade/commands/deploy_commands.py @@ -3,6 +3,7 @@ import sys 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.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGY @@ -10,8 +11,6 @@ from freqtrade.exceptions import OperationalException from freqtrade.misc import render_template from freqtrade.state import RunMode -from .utils import setup_utils_configuration - logger = logging.getLogger(__name__) diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index db3f69050..5c6f25848 100644 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -4,7 +4,7 @@ from typing import Any, Dict, List from colorama import init as colorama_init -from .utils import setup_utils_configuration +from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode diff --git a/freqtrade/commands/list_commands.py b/freqtrade/commands/list_commands.py index 90efaf66f..022822782 100644 --- a/freqtrade/commands/list_commands.py +++ b/freqtrade/commands/list_commands.py @@ -8,6 +8,7 @@ from typing import Any, Dict import rapidjson from tabulate import tabulate +from freqtrade.configuration import setup_utils_configuration from freqtrade.constants import USERPATH_STRATEGY from freqtrade.exceptions import OperationalException from freqtrade.exchange import (available_exchanges, ccxt_exchanges, @@ -16,8 +17,6 @@ from freqtrade.misc import plural from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.state import RunMode -from .utils import setup_utils_configuration - logger = logging.getLogger(__name__) diff --git a/freqtrade/commands/optimize_commands.py b/freqtrade/commands/optimize_commands.py index e635e61fd..a2d1b4601 100644 --- a/freqtrade/commands/optimize_commands.py +++ b/freqtrade/commands/optimize_commands.py @@ -2,7 +2,7 @@ import logging from typing import Any, Dict from freqtrade import constants -from freqtrade.commands.utils import setup_utils_configuration +from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.state import RunMode diff --git a/freqtrade/commands/pairlist_commands.py b/freqtrade/commands/pairlist_commands.py index 06ba2e680..bf0b217a5 100644 --- a/freqtrade/commands/pairlist_commands.py +++ b/freqtrade/commands/pairlist_commands.py @@ -3,11 +3,10 @@ from typing import Any, Dict import rapidjson +from freqtrade.configuration import setup_utils_configuration from freqtrade.resolvers import ExchangeResolver from freqtrade.state import RunMode -from .utils import setup_utils_configuration - logger = logging.getLogger(__name__) diff --git a/freqtrade/commands/plot_commands.py b/freqtrade/commands/plot_commands.py index 4c68cc3c5..028933ba7 100644 --- a/freqtrade/commands/plot_commands.py +++ b/freqtrade/commands/plot_commands.py @@ -1,8 +1,8 @@ from typing import Any, Dict +from freqtrade.configuration import setup_utils_configuration from freqtrade.exceptions import OperationalException from freqtrade.state import RunMode -from freqtrade.commands.utils import setup_utils_configuration def validate_plot_args(args: Dict[str, Any]): diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py index 54fc4e427..d41ac97ec 100644 --- a/freqtrade/configuration/__init__.py +++ b/freqtrade/configuration/__init__.py @@ -1,5 +1,6 @@ # 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 diff --git a/freqtrade/commands/utils.py b/freqtrade/configuration/config_setup.py similarity index 79% rename from freqtrade/commands/utils.py rename to freqtrade/configuration/config_setup.py index f77a35383..64f283e42 100644 --- a/freqtrade/commands/utils.py +++ b/freqtrade/configuration/config_setup.py @@ -1,8 +1,9 @@ import logging from typing import Any, Dict -from freqtrade.configuration import (Configuration, remove_credentials, - validate_config_consistency) +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/tests/commands/__init__.py b/tests/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_utils.py b/tests/commands/test_commands.py similarity index 99% rename from tests/test_utils.py rename to tests/commands/test_commands.py index 1328e3981..65d7f6eaf 100644 --- a/tests/test_utils.py +++ b/tests/commands/test_commands.py @@ -4,13 +4,13 @@ from unittest.mock import MagicMock, PropertyMock import pytest -from freqtrade.commands import (setup_utils_configuration, - start_create_userdir, start_download_data, +from freqtrade.commands import (start_create_userdir, start_download_data, start_hyperopt_list, start_hyperopt_show, start_list_exchanges, start_list_markets, start_list_strategies, start_list_timeframes, start_new_hyperopt, start_new_strategy, 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 (get_args, log_has, log_has_re, patch_exchange, @@ -639,7 +639,7 @@ def test_start_list_strategies(mocker, caplog, capsys): args = [ "list-strategies", "--strategy-path", - str(Path(__file__).parent / "strategy"), + str(Path(__file__).parent.parent / "strategy"), "-1" ] pargs = get_args(args) @@ -654,7 +654,7 @@ def test_start_list_strategies(mocker, caplog, capsys): args = [ "list-strategies", "--strategy-path", - str(Path(__file__).parent / "strategy"), + str(Path(__file__).parent.parent / "strategy"), ] pargs = get_args(args) # pargs['config'] = None From 33645e45fd3902a673ead1fbaf4f1a521134eaae Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 27 Jan 2020 02:49:25 +0300 Subject: [PATCH 317/324] Minor cosmetics in start_trading --- freqtrade/commands/trade_commands.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/commands/trade_commands.py b/freqtrade/commands/trade_commands.py index 2c0c4c9c1..352fac26d 100644 --- a/freqtrade/commands/trade_commands.py +++ b/freqtrade/commands/trade_commands.py @@ -10,8 +10,10 @@ def start_trading(args: Dict[str, Any]) -> int: """ Main entry point for trading mode """ + # Import here to avoid loading worker module when it's not used from freqtrade.worker import Worker - # Load and run worker + + # Create and run worker worker = None try: worker = Worker(args) From 30e3e434abc76e6c0af40b256540cc32479c410c Mon Sep 17 00:00:00 2001 From: hroff-1902 Date: Mon, 27 Jan 2020 03:34:53 +0300 Subject: [PATCH 318/324] Add notify_status() to FreqtradeBot --- freqtrade/freqtradebot.py | 10 ++++++++++ freqtrade/worker.py | 24 +++++++----------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e3856e200..6c519280d 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -95,6 +95,16 @@ class FreqtradeBot: # Protect sell-logic from forcesell and viceversa self._sell_lock = Lock() + def notify_status(self, msg: str) -> None: + """ + Public method for users of this class (worker, etc.) to send notifications + via RPC about changes in the bot status. + """ + self.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': msg + }) + def cleanup(self) -> None: """ Cleanup pending resources on an already stopped bot diff --git a/freqtrade/worker.py b/freqtrade/worker.py index 22651d269..972ff0d61 100755 --- a/freqtrade/worker.py +++ b/freqtrade/worker.py @@ -12,7 +12,6 @@ from freqtrade import __version__, constants from freqtrade.configuration import Configuration from freqtrade.exceptions import OperationalException, TemporaryError from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.rpc import RPCMessageType from freqtrade.state import State logger = logging.getLogger(__name__) @@ -84,10 +83,8 @@ class Worker: # Log state transition if state != old_state: - self.freqtrade.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': f'{state.name.lower()}' - }) + self.freqtrade.notify_status(f'{state.name.lower()}') + logger.info('Changing state to: %s', state.name) if state == State.RUNNING: self.freqtrade.startup() @@ -136,10 +133,9 @@ class Worker: except OperationalException: tb = traceback.format_exc() hint = 'Issue `/start` if you think it is safe to restart.' - self.freqtrade.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': f'OperationalException:\n```\n{tb}```{hint}' - }) + + self.freqtrade.notify_status(f'OperationalException:\n```\n{tb}```{hint}') + logger.exception('OperationalException. Stopping trader ...') self.freqtrade.state = State.STOPPED @@ -159,10 +155,7 @@ class Worker: # Load and validate config and create new instance of the bot self._init(True) - self.freqtrade.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': 'config reloaded' - }) + self.freqtrade.notify_status('config reloaded') # Tell systemd that we completed reconfiguration if self._sd_notify: @@ -176,8 +169,5 @@ class Worker: self._sd_notify.notify("STOPPING=1") if self.freqtrade: - self.freqtrade.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, - 'status': 'process died' - }) + self.freqtrade.notify_status('process died') self.freqtrade.cleanup() From 161c06fd4ef5afdd3deec036f1d1be94bc4621ab Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2020 07:42:52 +0000 Subject: [PATCH 319/324] Bump plotly from 4.4.1 to 4.5.0 Bumps [plotly](https://github.com/plotly/plotly.py) from 4.4.1 to 4.5.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.4.1...v4.5.0) 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 415d4b888..26467d90b 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.4.1 +plotly==4.5.0 From 184a6005a6db67918a62b84896813302451880ed Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2020 07:43:11 +0000 Subject: [PATCH 320/324] Bump urllib3 from 1.25.7 to 1.25.8 Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.25.7 to 1.25.8. - [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.7...1.25.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 daf4984c0..e358c8445 100644 --- a/requirements-common.txt +++ b/requirements-common.txt @@ -6,7 +6,7 @@ python-telegram-bot==12.3.0 arrow==0.15.5 cachetools==4.0.0 requests==2.22.0 -urllib3==1.25.7 +urllib3==1.25.8 wrapt==1.11.2 jsonschema==3.2.0 TA-Lib==0.4.17 From 66939bdcf636204c963aae75df9ccb5cc919fdb5 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2020 07:43:41 +0000 Subject: [PATCH 321/324] Bump ccxt from 1.21.76 to 1.21.91 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.21.76 to 1.21.91. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md) - [Commits](https://github.com/ccxt/ccxt/compare/1.21.76...1.21.91) 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 daf4984c0..c826541a6 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.21.76 +ccxt==1.21.91 SQLAlchemy==1.3.12 python-telegram-bot==12.3.0 arrow==0.15.5 From a3b0f752896f6f88efdf33099787f56cde32a26a Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2020 07:44:27 +0000 Subject: [PATCH 322/324] Bump pytest from 5.3.3 to 5.3.4 Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.3.3 to 5.3.4. - [Release notes](https://github.com/pytest-dev/pytest/releases) - [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst) - [Commits](https://github.com/pytest-dev/pytest/compare/5.3.3...5.3.4) 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 e602bf184..6330d93e5 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -8,7 +8,7 @@ flake8==3.7.9 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.0.0 mypy==0.761 -pytest==5.3.3 +pytest==5.3.4 pytest-asyncio==0.10.0 pytest-cov==2.8.1 pytest-mock==2.0.0 From c9ee678a52fcdc0cf2a08ea88baa4263d8b40576 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 27 Jan 2020 08:36:41 +0000 Subject: [PATCH 323/324] Bump sqlalchemy from 1.3.12 to 1.3.13 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.12 to 1.3.13. - [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 c826541a6..14fee6e80 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.21.91 -SQLAlchemy==1.3.12 +SQLAlchemy==1.3.13 python-telegram-bot==12.3.0 arrow==0.15.5 cachetools==4.0.0 From cff8498b4227f9c0f4688eb158a348b2067de677 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 31 Jan 2020 20:17:53 +0100 Subject: [PATCH 324/324] Version bump 2020.01 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index bab359368..15b7d66d7 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ FreqTrade bot """ -__version__ = '2019.11' +__version__ = '2020.01' if __version__ == 'develop':