From 5d3261e92fad6e4b7d5a843877ea3db2d646866a Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Tue, 7 Sep 2021 12:24:39 +0530 Subject: [PATCH 01/64] Added Ftx interest rate calculation --- freqtrade/leverage/interest.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/leverage/interest.py b/freqtrade/leverage/interest.py index aacbb3532..c687c8b5b 100644 --- a/freqtrade/leverage/interest.py +++ b/freqtrade/leverage/interest.py @@ -20,7 +20,7 @@ def interest( :param exchange_name: The exchanged being trading on :param borrowed: The amount of currency being borrowed - :param rate: The rate of interest + :param rate: The rate of interest (i.e daily interest rate) :param hours: The time in hours that the currency has been borrowed for Raises: @@ -36,7 +36,8 @@ def interest( # Rounded based on https://kraken-fees-calculator.github.io/ return borrowed * rate * (one+ceil(hours/four)) elif exchange_name == "ftx": - # TODO-lev: Add FTX interest formula - raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") + # As Explained under #Interest rates section in + # https://help.ftx.com/hc/en-us/articles/360053007671-Spot-Margin-Trading-Explainer + return borrowed * rate * ceil(hours)/twenty_four else: - raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") + raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") \ No newline at end of file From d07c7f7f275af5169b4ba3ac6ca25974c687998f Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Tue, 7 Sep 2021 12:28:23 +0530 Subject: [PATCH 02/64] Added Ftx interest rate calculation --- freqtrade/leverage/interest.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/leverage/interest.py b/freqtrade/leverage/interest.py index aacbb3532..c687c8b5b 100644 --- a/freqtrade/leverage/interest.py +++ b/freqtrade/leverage/interest.py @@ -20,7 +20,7 @@ def interest( :param exchange_name: The exchanged being trading on :param borrowed: The amount of currency being borrowed - :param rate: The rate of interest + :param rate: The rate of interest (i.e daily interest rate) :param hours: The time in hours that the currency has been borrowed for Raises: @@ -36,7 +36,8 @@ def interest( # Rounded based on https://kraken-fees-calculator.github.io/ return borrowed * rate * (one+ceil(hours/four)) elif exchange_name == "ftx": - # TODO-lev: Add FTX interest formula - raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") + # As Explained under #Interest rates section in + # https://help.ftx.com/hc/en-us/articles/360053007671-Spot-Margin-Trading-Explainer + return borrowed * rate * ceil(hours)/twenty_four else: - raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") + raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") \ No newline at end of file From 17a5cc96feb7058f7831cd0cbc5663654adfca13 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 13 Sep 2021 00:09:55 -0600 Subject: [PATCH 03/64] Added set_margin_mode to create_order --- freqtrade/exchange/binance.py | 4 ++++ freqtrade/exchange/exchange.py | 10 +++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index fcd027d52..d079d4ad6 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -152,6 +152,10 @@ class Binance(Exchange): max_lev = 1/margin_req return max_lev + def lev_prep(self, pair: str, leverage: float): + self.set_margin_mode(pair, self.collateral) + self._set_leverage(leverage, pair, self.trading_mode) + @retrier def _set_leverage( self, diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2fb63d201..07a817006 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -150,7 +150,7 @@ class Exchange: if config.get('trading_mode') else TradingMode.SPOT ) - collateral: Optional[Collateral] = ( + self.collateral: Optional[Collateral] = ( Collateral(config.get('collateral')) if config.get('collateral') else None @@ -176,7 +176,7 @@ class Exchange: self.validate_order_time_in_force(config.get('order_time_in_force', {})) self.validate_required_startup_candles(config.get('startup_candle_count', 0), config.get('timeframe', '')) - self.validate_trading_mode_and_collateral(self.trading_mode, collateral) + self.validate_trading_mode_and_collateral(self.trading_mode, self.collateral) # Converts the interval provided in minutes in config to seconds self.markets_refresh_interval: int = exchange_config.get( "markets_refresh_interval", 60) * 60 @@ -770,6 +770,10 @@ class Exchange: # Order handling + def lev_prep(self, pair: str, leverage: float): + self.set_margin_mode(pair, self.collateral) + self._set_leverage(leverage, pair) + def create_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, time_in_force: str = 'gtc', leverage=1.0) -> Dict: @@ -778,7 +782,7 @@ class Exchange: return dry_order if self.trading_mode != TradingMode.SPOT: - self._set_leverage(leverage, pair) + self.lev_prep(pair, leverage) params = self._params.copy() if time_in_force != 'gtc' and ordertype != 'market': param = self._ft_has.get('time_in_force_parameter', '') From cbaf477bec00071877eb3946b8b2c89ec15c7ac4 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 15 Sep 2021 21:55:19 -0600 Subject: [PATCH 04/64] changed kraken set lev implementation --- freqtrade/exchange/exchange.py | 13 +++++++++---- freqtrade/exchange/kraken.py | 10 ++++++---- tests/exchange/test_exchange.py | 8 +++++++- tests/exchange/test_kraken.py | 12 ------------ 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 0c3b29e1a..554873100 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -775,6 +775,13 @@ class Exchange: self.set_margin_mode(pair, self.collateral) self._set_leverage(leverage, pair) + def _get_params(self, time_in_force: str, ordertype: str, leverage: float) -> Dict: + params = self._params.copy() + if time_in_force != 'gtc' and ordertype != 'market': + param = self._ft_has.get('time_in_force_parameter', '') + params.update({param: time_in_force}) + return params + def create_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, time_in_force: str = 'gtc', leverage=1.0) -> Dict: @@ -784,10 +791,8 @@ class Exchange: if self.trading_mode != TradingMode.SPOT: self.lev_prep(pair, leverage) - params = self._params.copy() - if time_in_force != 'gtc' and ordertype != 'market': - param = self._ft_has.get('time_in_force_parameter', '') - params.update({param: time_in_force}) + + params = self._get_params(time_in_force, ordertype, leverage) try: # Set the precision for amount and price(rate) as accepted by the exchange diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 661000d4d..60af42c69 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -182,8 +182,10 @@ class Kraken(Exchange): Kraken set's the leverage as an option in the order object, so we need to add it to params """ + return + + def _get_params(self, time_in_force: str, ordertype: str, leverage: float) -> Dict: + params = super()._get_params(time_in_force, ordertype, leverage) if leverage > 1.0: - self._params['leverage'] = leverage - else: - if 'leverage' in self._params: - del self._params['leverage'] + params['leverage'] = leverage + return params diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 535726b4b..8c7f908b2 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1110,7 +1110,13 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) order = exchange.create_order( - pair='ETH/BTC', ordertype=ordertype, side=side, amount=1, rate=200) + pair='ETH/BTC', + ordertype=ordertype, + side=side, + amount=1, + rate=200, + leverage=3.0 + ) assert 'id' in order assert 'info' in order diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 374b054a6..74a06c96c 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -305,15 +305,3 @@ def test_fill_leverage_brackets_kraken(default_conf, mocker): 'XLTCUSDT': [1], 'LTC/ETH': [1] } - - -def test__set_leverage_kraken(default_conf, mocker): - exchange = get_patched_exchange(mocker, default_conf, id="kraken") - exchange._set_leverage(1) - assert 'leverage' not in exchange._params - exchange._set_leverage(3) - assert exchange._params['leverage'] == 3 - exchange._set_leverage(1.0) - assert 'leverage' not in exchange._params - exchange._set_leverage(3.0) - assert exchange._params['leverage'] == 3 From 457e738b4a049cbc522987612dc2e4bd91b272e9 Mon Sep 17 00:00:00 2001 From: Sergey Khliustin Date: Thu, 16 Sep 2021 14:48:02 +0300 Subject: [PATCH 05/64] Added days parameter to PerformanceFilter --- freqtrade/persistence/models.py | 28 ++++++++++++++++++- .../plugins/pairlist/PerformanceFilter.py | 15 ++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 8c8c1e0a9..91a26eba7 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -2,7 +2,7 @@ This module contains the class to persist trades into SQLite """ import logging -from datetime import datetime, timezone +from datetime import datetime, timezone, timedelta from decimal import Decimal from typing import Any, Dict, List, Optional @@ -856,6 +856,32 @@ class Trade(_DECL_BASE, LocalTrade): for pair, profit, profit_abs, count in pair_rates ] + @staticmethod + def get_performance(days: int) -> List[Dict[str, Any]]: + """ + Returns List of dicts containing all Trades, including profit and trade count + NOTE: Not supported in Backtesting. + """ + start_date = datetime.today() - timedelta(days) + pair_rates = Trade.query.with_entities( + Trade.pair, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False) & (Trade.close_date >= start_date))\ + .group_by(Trade.pair) \ + .order_by(desc('profit_sum_abs')) \ + .all() + return [ + { + 'pair': pair, + 'profit': profit, + 'profit_abs': profit_abs, + 'count': count + } + for pair, profit, profit_abs, count in pair_rates + ] + @staticmethod def get_best_pair(start_date: datetime = datetime.fromtimestamp(0)): """ diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index 46a289ae6..4d530fe88 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -2,11 +2,12 @@ Performance pair list filter """ import logging -from typing import Dict, List +from typing import Dict, List, Any import pandas as pd from freqtrade.persistence import Trade +from freqtrade.exceptions import OperationalException from freqtrade.plugins.pairlist.IPairList import IPairList @@ -15,6 +16,13 @@ logger = logging.getLogger(__name__) class PerformanceFilter(IPairList): + def __init__(self, exchange, pairlistmanager, + config: Dict[str, Any], pairlistconfig: Dict[str, Any], + pairlist_pos: int) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + + self._days = pairlistconfig.get('days', 0) + @property def needstickers(self) -> bool: """ @@ -40,7 +48,10 @@ class PerformanceFilter(IPairList): """ # Get the trading performance for pairs from database try: - performance = pd.DataFrame(Trade.get_overall_performance()) + if self._days > 0: + performance = pd.DataFrame(Trade.get_performance(self._days)) + else: + performance = pd.DataFrame(Trade.get_overall_performance()) except AttributeError: # Performancefilter does not work in backtesting. self.log_once("PerformanceFilter is not available in this mode.", logger.warning) From 57c7926515b9973a3a6f767963f5e0c52e2b44c2 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 16 Sep 2021 23:05:13 -0600 Subject: [PATCH 06/64] leverage updates on exchange classes --- freqtrade/data/leverage_brackets.json | 1214 +++++++++++++++++++++++++ freqtrade/exchange/binance.py | 74 +- freqtrade/exchange/exchange.py | 44 +- freqtrade/exchange/ftx.py | 16 +- freqtrade/exchange/kraken.py | 17 +- freqtrade/freqtradebot.py | 3 +- tests/exchange/test_binance.py | 52 +- tests/exchange/test_exchange.py | 44 +- tests/exchange/test_ftx.py | 86 +- tests/exchange/test_kraken.py | 34 +- tests/test_freqtradebot.py | 9 +- 11 files changed, 1467 insertions(+), 126 deletions(-) create mode 100644 freqtrade/data/leverage_brackets.json diff --git a/freqtrade/data/leverage_brackets.json b/freqtrade/data/leverage_brackets.json new file mode 100644 index 000000000..4450b015e --- /dev/null +++ b/freqtrade/data/leverage_brackets.json @@ -0,0 +1,1214 @@ +{ + "1000SHIB/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "1INCH/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "AAVE/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"] + ], + "ADA/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "ADA/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "AKRO/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ALGO/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [1000000.0, "0.25"], + [2000000.0, "0.5"] + ], + "ALICE/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ALPHA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ANKR/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ATA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ATOM/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [1000000.0, "0.25"], + [2000000.0, "0.5"] + ], + "AUDIO/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "AVAX/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "AXS/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"], + [15000000.0, "0.5"] + ], + "BAKE/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BAL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BAND/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BAT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BCH/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "BEL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BLZ/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BNB/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "BNB/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "BTC/BUSD": [ + [0.0, "0.004"], + [25000.0, "0.005"], + [100000.0, "0.01"], + [500000.0, "0.025"], + [1000000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"], + [30000000.0, "0.5"] + ], + "BTC/USDT": [ + [0.0, "0.004"], + [50000.0, "0.005"], + [250000.0, "0.01"], + [1000000.0, "0.025"], + [5000000.0, "0.05"], + [20000000.0, "0.1"], + [50000000.0, "0.125"], + [100000000.0, "0.15"], + [200000000.0, "0.25"], + [300000000.0, "0.5"] + ], + "BTCBUSD_210129": [ + [0.0, "0.004"], + [5000.0, "0.005"], + [25000.0, "0.01"], + [100000.0, "0.025"], + [500000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"] + ], + "BTCBUSD_210226": [ + [0.0, "0.004"], + [5000.0, "0.005"], + [25000.0, "0.01"], + [100000.0, "0.025"], + [500000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"] + ], + "BTCDOM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BTCSTUSDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BTCUSDT_210326": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "BTCUSDT_210625": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "BTCUSDT_210924": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"], + [20000000.0, "0.5"] + ], + "BTS/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BTT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BZRX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "C98/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CELR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CHR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CHZ/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "COMP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "COTI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CRV/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CTK/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CVC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DASH/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DEFI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DENT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DGB/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DODO/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DOGE/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "DOGE/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "DOT/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "DOTECOUSDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DYDX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "EGLD/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ENJ/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "EOS/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "ETC/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "ETH/BUSD": [ + [0.0, "0.004"], + [25000.0, "0.005"], + [100000.0, "0.01"], + [500000.0, "0.025"], + [1000000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"], + [30000000.0, "0.5"] + ], + "ETH/USDT": [ + [0.0, "0.005"], + [10000.0, "0.0065"], + [100000.0, "0.01"], + [500000.0, "0.02"], + [1000000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"] + ], + "ETHUSDT_210326": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "ETHUSDT_210625": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "ETHUSDT_210924": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"], + [20000000.0, "0.5"] + ], + "FIL/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"] + ], + "FLM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "FTM/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "FTT/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "GRT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "GTC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "HBAR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "HNT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "HOT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ICP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ICX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "IOST/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "IOTA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "IOTX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "KAVA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "KEEP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "KNC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "KSM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LENDUSDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LINA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LINK/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "LIT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LRC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LTC/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "LUNA/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"], + [15000000.0, "0.5"] + ], + "MANA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "MASK/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "MATIC/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "MKR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "MTL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "NEAR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "NEO/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "NKN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "OCEAN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "OGN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "OMG/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ONE/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ONT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "QTUM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RAY/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "REEF/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "REN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RLC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RSR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RUNE/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RVN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SAND/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SFP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SKL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SNX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SOL/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "SOL/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.25"], + [10000000.0, "0.5"] + ], + "SRM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "STMX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "STORJ/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SUSHI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SXP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "THETA/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"] + ], + "TLM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "TOMO/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "TRB/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "TRX/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "UNFI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "UNI/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"] + ], + "VET/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "WAVES/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "XEM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "XLM/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "XMR/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "XRP/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "XRP/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "XTZ/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "YFI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "YFII/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ZEC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ZEN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ZIL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ZRX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ] +} diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index f4998d9a7..17e865d64 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -1,5 +1,7 @@ """ Binance exchange subclass """ +import json import logging +from pathlib import Path from typing import Dict, List, Optional, Tuple import arrow @@ -47,8 +49,8 @@ class Binance(Exchange): ) @retrier(retries=0) - def stoploss(self, pair: str, amount: float, - stop_price: float, order_types: Dict, side: str) -> Dict: + def stoploss(self, pair: str, amount: float, stop_price: float, + order_types: Dict, side: str, leverage: float) -> Dict: """ creates a stoploss limit order. this stoploss-limit is binance-specific. @@ -76,7 +78,7 @@ class Binance(Exchange): if self._config['dry_run']: dry_order = self.create_dry_run_order( - pair, ordertype, side, amount, stop_price) + pair, ordertype, side, amount, stop_price, leverage) return dry_order try: @@ -87,8 +89,15 @@ class Binance(Exchange): rate = self.price_to_precision(pair, rate) - order = self._api.create_order(symbol=pair, type=ordertype, side=side, - amount=amount, price=rate, params=params) + order = self._api.create_order( + symbol=pair, + type=ordertype, + side=side, + amount=amount, + price=rate, + params=params, + leverage=leverage + ) logger.info('stoploss limit order added for %s. ' 'stop price: %s. limit: %s', pair, stop_price, rate) self._log_exchange_response('create_stoploss_order', order) @@ -119,26 +128,33 @@ class Binance(Exchange): Assigns property _leverage_brackets to a dictionary of information about the leverage allowed on each pair """ - try: - leverage_brackets = self._api.load_leverage_brackets() - for pair, brackets in leverage_brackets.items(): - self._leverage_brackets[pair] = [ - [ - min_amount, - float(margin_req) - ] for [ - min_amount, - margin_req - ] in brackets - ] + if self.trading_mode == TradingMode.FUTURES: + try: + if self._config['dry_run']: + leverage_brackets_path = Path('data') / 'leverage_brackets.json' + with open(leverage_brackets_path) as json_file: + leverage_brackets = json.load(json_file) + else: + leverage_brackets = self._api.load_leverage_brackets() - except ccxt.DDoSProtection as e: - raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError(f'Could not fetch leverage amounts due to' - f'{e.__class__.__name__}. Message: {e}') from e - except ccxt.BaseError as e: - raise OperationalException(e) from e + for pair, brackets in leverage_brackets.items(): + self._leverage_brackets[pair] = [ + [ + min_amount, + float(margin_req) + ] for [ + min_amount, + margin_req + ] in brackets + ] + + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError(f'Could not fetch leverage amounts due to' + f'{e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: """ @@ -153,10 +169,6 @@ class Binance(Exchange): max_lev = 1/margin_req return max_lev - def lev_prep(self, pair: str, leverage: float): - self.set_margin_mode(pair, self.collateral) - self._set_leverage(leverage, pair, self.trading_mode) - @retrier def _set_leverage( self, @@ -170,9 +182,11 @@ class Binance(Exchange): """ trading_mode = trading_mode or self.trading_mode + if self._config['dry_run'] or trading_mode != TradingMode.FUTURES: + return + try: - if trading_mode == TradingMode.FUTURES: - self._api.set_leverage(symbol=pair, leverage=leverage) + self._api.set_leverage(symbol=pair, leverage=leverage) except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 554873100..8bbc88235 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -258,6 +258,13 @@ class Exchange: """exchange ccxt precisionMode""" return self._api.precisionMode + @property + def running_live_mode(self) -> bool: + return ( + self._config['runmode'].value not in ('backtest', 'hyperopt') and + not self._config['dry_run'] + ) + def _log_exchange_response(self, endpoint, response) -> None: """ Log exchange responses """ if self.log_responses: @@ -617,15 +624,13 @@ class Exchange: # The value returned should satisfy both limits: for amount (base currency) and # for cost (quote, stake currency), so max() is used here. # See also #2575 at github. - return self._apply_leverage_to_stake_amount( + return self._divide_stake_amount_by_leverage( max(min_stake_amounts) * amount_reserve_percent, leverage or 1.0 ) - def _apply_leverage_to_stake_amount(self, stake_amount: float, leverage: float): + def _divide_stake_amount_by_leverage(self, stake_amount: float, leverage: float): """ - #TODO-lev: Find out how this works on Kraken and FTX - # * Should be implemented by child classes if leverage affects the stake_amount Takes the minimum stake amount for a pair with no leverage and returns the minimum stake amount when leverage is considered :param stake_amount: The stake amount for a pair before leverage is considered @@ -636,7 +641,7 @@ class Exchange: # Dry-run methods def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, - rate: float, params: Dict = {}) -> Dict[str, Any]: + rate: float, leverage: float, params: Dict = {}) -> Dict[str, Any]: order_id = f'dry_run_{side}_{datetime.now().timestamp()}' _amount = self.amount_to_precision(pair, amount) dry_order: Dict[str, Any] = { @@ -653,7 +658,8 @@ class Exchange: 'timestamp': arrow.utcnow().int_timestamp * 1000, 'status': "closed" if ordertype == "market" else "open", 'fee': None, - 'info': {} + 'info': {}, + 'leverage': leverage } if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]: dry_order["info"] = {"stopPrice": dry_order["price"]} @@ -663,7 +669,7 @@ class Exchange: average = self.get_dry_market_fill_price(pair, side, amount, rate) dry_order.update({ 'average': average, - 'cost': dry_order['amount'] * average, + 'cost': (dry_order['amount'] * average) / leverage }) dry_order = self.add_dry_order_fee(pair, dry_order) @@ -771,7 +777,7 @@ class Exchange: # Order handling - def lev_prep(self, pair: str, leverage: float): + def _lev_prep(self, pair: str, leverage: float): self.set_margin_mode(pair, self.collateral) self._set_leverage(leverage, pair) @@ -783,14 +789,14 @@ class Exchange: return params def create_order(self, pair: str, ordertype: str, side: str, amount: float, - rate: float, time_in_force: str = 'gtc', leverage=1.0) -> Dict: - + rate: float, leverage: float = 1.0, time_in_force: str = 'gtc') -> Dict: + # TODO-lev: remove default for leverage if self._config['dry_run']: - dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate) + dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate, leverage) return dry_order if self.trading_mode != TradingMode.SPOT: - self.lev_prep(pair, leverage) + self._lev_prep(pair, leverage) params = self._get_params(time_in_force, ordertype, leverage) @@ -831,8 +837,8 @@ class Exchange: """ raise OperationalException(f"stoploss is not implemented for {self.name}.") - def stoploss(self, pair: str, amount: float, - stop_price: float, order_types: Dict, side: str) -> Dict: + def stoploss(self, pair: str, amount: float, stop_price: float, + order_types: Dict, side: str, leverage: float) -> Dict: """ creates a stoploss order. The precise ordertype is determined by the order_types dict or exchange default. @@ -1595,15 +1601,13 @@ class Exchange: self._async_get_trade_history(pair=pair, since=since, until=until, from_id=from_id)) - @retrier def fill_leverage_brackets(self): """ #TODO-lev: Should maybe be renamed, leverage_brackets might not be accurate for kraken Assigns property _leverage_brackets to a dictionary of information about the leverage allowed on each pair """ - raise OperationalException( - f"{self.name.capitalize()}.fill_leverage_brackets has not been implemented.") + return def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: """ @@ -1624,7 +1628,9 @@ class Exchange: Set's the leverage before making a trade, in order to not have the same leverage on every trade """ - if not self.exchange_has("setLeverage"): + # TODO-lev: Make a documentation page that says you can't run 2 bots + # TODO-lev: on the same account with leverage + if self._config['dry_run'] or not self.exchange_has("setLeverage"): # Some exchanges only support one collateral type return @@ -1644,7 +1650,7 @@ class Exchange: Set's the margin mode on the exchange to cross or isolated for a specific pair :param symbol: base/quote currency pair (e.g. "ADA/USDT") ''' - if not self.exchange_has("setMarginMode"): + if self._config['dry_run'] or not self.exchange_has("setMarginMode"): # Some exchanges only support one collateral type return diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 095d8eaa1..eaf9a0477 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -49,8 +49,8 @@ class Ftx(Exchange): ) @retrier(retries=0) - def stoploss(self, pair: str, amount: float, - stop_price: float, order_types: Dict, side: str) -> Dict: + def stoploss(self, pair: str, amount: float, stop_price: float, + order_types: Dict, side: str, leverage: float) -> Dict: """ Creates a stoploss order. depending on order_types.stoploss configuration, uses 'market' or limit order. @@ -69,7 +69,7 @@ class Ftx(Exchange): if self._config['dry_run']: dry_order = self.create_dry_run_order( - pair, ordertype, side, amount, stop_price) + pair, ordertype, side, amount, stop_price, leverage) return dry_order try: @@ -81,8 +81,14 @@ class Ftx(Exchange): params['stopPrice'] = stop_price amount = self.amount_to_precision(pair, amount) - order = self._api.create_order(symbol=pair, type=ordertype, side=side, - amount=amount, params=params) + order = self._api.create_order( + symbol=pair, + type=ordertype, + side=side, + amount=amount, + leverage=leverage, + params=params + ) self._log_exchange_response('create_stoploss_order', order) logger.info('stoploss order added for %s. ' 'stop price: %s.', pair, stop_price) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 60af42c69..d6a816c9e 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -85,8 +85,8 @@ class Kraken(Exchange): )) @retrier(retries=0) - def stoploss(self, pair: str, amount: float, - stop_price: float, order_types: Dict, side: str) -> Dict: + def stoploss(self, pair: str, amount: float, stop_price: float, + order_types: Dict, side: str, leverage: float) -> Dict: """ Creates a stoploss market order. Stoploss market orders is the only stoploss type supported by kraken. @@ -108,14 +108,21 @@ class Kraken(Exchange): if self._config['dry_run']: dry_order = self.create_dry_run_order( - pair, ordertype, side, amount, stop_price) + pair, ordertype, side, amount, stop_price, leverage) return dry_order try: amount = self.amount_to_precision(pair, amount) - order = self._api.create_order(symbol=pair, type=ordertype, side=side, - amount=amount, price=stop_price, params=params) + order = self._api.create_order( + symbol=pair, + type=ordertype, + side=side, + amount=amount, + price=stop_price, + leverage=leverage, + params=params + ) self._log_exchange_response('create_stoploss_order', order) logger.info('stoploss order added for %s. ' 'stop price: %s.', pair, stop_price) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ca1e9f9b0..2738ec634 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -737,7 +737,8 @@ class FreqtradeBot(LoggingMixin): amount=trade.amount, stop_price=stop_price, order_types=self.strategy.order_types, - side=trade.exit_side + side=trade.exit_side, + leverage=trade.leverage ) order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss') diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 5a1087534..f0642fda9 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -48,13 +48,20 @@ def test_stoploss_order_binance( amount=1, stop_price=190, side=side, - order_types={'stoploss_on_exchange_limit_ratio': 1.05} + order_types={'stoploss_on_exchange_limit_ratio': 1.05}, + leverage=1.0 ) api_mock.create_order.reset_mock() order_types = {} if limitratio is None else {'stoploss_on_exchange_limit_ratio': limitratio} - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, - order_types=order_types, side=side) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types=order_types, + side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -71,17 +78,31 @@ def test_stoploss_order_binance( with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0) with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("binance Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance", "stoploss", "create_order", retries=1, - pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + pair='ETH/BTC', amount=1, stop_price=220, order_types={}, + side=side, leverage=1.0) def test_stoploss_order_dry_run_binance(default_conf, mocker): @@ -94,12 +115,25 @@ def test_stoploss_order_dry_run_binance(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') with pytest.raises(OperationalException): - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, side="sell", - order_types={'stoploss_on_exchange_limit_ratio': 1.05}) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=190, + side="sell", + order_types={'stoploss_on_exchange_limit_ratio': 1.05}, + leverage=1.0 + ) api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side="sell", + leverage=1.0 + ) assert 'id' in order assert 'info' in order diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 8c7f908b2..d641b0a63 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -403,7 +403,6 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: # With Leverage result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss, 3.0) assert isclose(result, expected_result/3) - # TODO-lev: Min stake for base, kraken and ftx # min amount is set markets["ETH/BTC"]["limits"] = { @@ -420,7 +419,6 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: # With Leverage result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 5.0) assert isclose(result, expected_result/5) - # TODO-lev: Min stake for base, kraken and ftx # min amount and cost are set (cost is minimal) markets["ETH/BTC"]["limits"] = { @@ -437,7 +435,6 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: # With Leverage result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10) assert isclose(result, expected_result/10) - # TODO-lev: Min stake for base, kraken and ftx # min amount and cost are set (amount is minial) markets["ETH/BTC"]["limits"] = { @@ -454,7 +451,6 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: # With Leverage result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 7.0) assert isclose(result, expected_result/7.0) - # TODO-lev: Min stake for base, kraken and ftx result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4) expected_result = max(8, 2 * 2) * 1.5 @@ -462,7 +458,6 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: # With Leverage result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4, 8.0) assert isclose(result, expected_result/8.0) - # TODO-lev: Min stake for base, kraken and ftx # Really big stoploss result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) @@ -471,7 +466,6 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: # With Leverage result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0) assert isclose(result, expected_result/12) - # TODO-lev: Min stake for base, kraken and ftx def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: @@ -493,7 +487,6 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: assert round(result, 8) == round(expected_result, 8) result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss, 3.0) assert round(result, 8) == round(expected_result/3, 8) - # TODO-lev: Min stake for base, kraken and ftx def test_set_sandbox(default_conf, mocker): @@ -1004,7 +997,13 @@ def test_create_dry_run_order(default_conf, mocker, side, exchange_name): exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) order = exchange.create_dry_run_order( - pair='ETH/BTC', ordertype='limit', side=side, amount=1, rate=200) + pair='ETH/BTC', + ordertype='limit', + side=side, + amount=1, + rate=200, + leverage=1.0 + ) assert 'id' in order assert f'dry_run_{side}_' in order["id"] assert order["side"] == side @@ -1027,7 +1026,13 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice, ) order = exchange.create_dry_run_order( - pair='LTC/USDT', ordertype='limit', side=side, amount=1, rate=startprice) + pair='LTC/USDT', + ordertype='limit', + side=side, + amount=1, + rate=startprice, + leverage=1.0 + ) assert order_book_l2_usd.call_count == 1 assert 'id' in order assert f'dry_run_{side}_' in order["id"] @@ -1073,7 +1078,13 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou ) order = exchange.create_dry_run_order( - pair='LTC/USDT', ordertype='market', side=side, amount=amount, rate=rate) + pair='LTC/USDT', + ordertype='market', + side=side, + amount=amount, + rate=rate, + leverage=1.0 + ) assert 'id' in order assert f'dry_run_{side}_' in order["id"] assert order["side"] == side @@ -2664,7 +2675,14 @@ def test_get_fee(default_conf, mocker, exchange_name): def test_stoploss_order_unsupported_exchange(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, id='bittrex') with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side="sell", + leverage=1.0 + ) with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): exchange.stoploss_adjust(1, {}, side="sell") @@ -3024,7 +3042,7 @@ def test_calculate_backoff(retrycount, max_retries, expected): (20.0, 5.0, 4.0), (100.0, 100.0, 1.0) ]) -def test_apply_leverage_to_stake_amount( +def test_divide_stake_amount_by_leverage( exchange, stake_amount, leverage, @@ -3033,7 +3051,7 @@ def test_apply_leverage_to_stake_amount( default_conf ): exchange = get_patched_exchange(mocker, default_conf, id=exchange) - assert exchange._apply_leverage_to_stake_amount(stake_amount, leverage) == min_stake_with_lev + assert exchange._divide_stake_amount_by_leverage(stake_amount, leverage) == min_stake_with_lev @pytest.mark.parametrize("exchange_name,trading_mode", [ diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 88c4c069b..ca6b24d64 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -1,10 +1,9 @@ from random import randint -from unittest.mock import MagicMock, PropertyMock +from unittest.mock import MagicMock import ccxt import pytest -from freqtrade.enums import TradingMode from freqtrade.exceptions import DependencyException, InvalidOrderException from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT from tests.conftest import get_patched_exchange @@ -14,8 +13,6 @@ from .test_exchange import ccxt_exceptionhandlers STOPLOSS_ORDERTYPE = 'stop' -# TODO-lev: All these stoploss tests with shorts - @pytest.mark.parametrize('order_price,exchangelimitratio,side', [ (217.8, 1.05, "sell"), @@ -39,8 +36,14 @@ def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitrati exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') # stoploss_on_exchange_limit_ratio is irrelevant for ftx market orders - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, side=side, - order_types={'stoploss_on_exchange_limit_ratio': exchangelimitratio}) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=190, + side=side, + order_types={'stoploss_on_exchange_limit_ratio': exchangelimitratio}, + leverage=1.0 + ) assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE @@ -54,7 +57,14 @@ def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitrati api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -67,8 +77,13 @@ def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitrati assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220 api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, - order_types={'stoploss': 'limit'}, side=side) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={'stoploss': 'limit'}, side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -85,17 +100,32 @@ def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitrati with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("ftx Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) ccxt_exceptionhandlers(mocker, default_conf, api_mock, "ftx", "stoploss", "create_order", retries=1, - pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + pair='ETH/BTC', amount=1, stop_price=220, order_types={}, + side=side, leverage=1.0) @pytest.mark.parametrize('side', [("sell"), ("buy")]) @@ -109,7 +139,14 @@ def test_stoploss_order_dry_run_ftx(default_conf, mocker, side): api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -230,26 +267,3 @@ def test_fill_leverage_brackets_ftx(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, id="ftx") exchange.fill_leverage_brackets() assert exchange._leverage_brackets == {} - - -@pytest.mark.parametrize("trading_mode", [ - (TradingMode.MARGIN), - (TradingMode.FUTURES) -]) -def test__set_leverage(mocker, default_conf, trading_mode): - - api_mock = MagicMock() - api_mock.set_leverage = MagicMock() - type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) - - ccxt_exceptionhandlers( - mocker, - default_conf, - api_mock, - "ftx", - "_set_leverage", - "set_leverage", - pair="XRP/USDT", - leverage=5.0, - trading_mode=trading_mode - ) diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 74a06c96c..a8cd8d8ef 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -195,7 +195,9 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedpr order_types={ 'stoploss': ordertype, 'stoploss_on_exchange_limit_ratio': 0.99 - }) + }, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -219,17 +221,32 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedpr with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("kraken Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken", "stoploss", "create_order", retries=1, - pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + pair='ETH/BTC', amount=1, stop_price=220, order_types={}, + side=side, leverage=1.0) @pytest.mark.parametrize('side', ['buy', 'sell']) @@ -243,7 +260,14 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker, side): api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index f87841fe8..28ca0ee49 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1349,7 +1349,8 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, pair='ETH/BTC', order_types=freqtrade.strategy.order_types, stop_price=0.00002346 * 0.95, - side="sell" + side="sell", + leverage=1.0 ) # price fell below stoploss, so dry-run sells trade. @@ -1537,7 +1538,8 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, pair='ETH/BTC', order_types=freqtrade.strategy.order_types, stop_price=0.00002346 * 0.96, - side="sell" + side="sell", + leverage=1.0 ) # price fell below stoploss, so dry-run sells trade. @@ -1661,7 +1663,8 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, pair='NEO/BTC', order_types=freqtrade.strategy.order_types, stop_price=0.00002346 * 0.99, - side="sell" + side="sell", + leverage=1.0 ) From dced167ea2d93a7c0d46960b0ace6f4625809500 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 16 Sep 2021 23:23:36 -0600 Subject: [PATCH 07/64] fixed some stuff in the leverage brackets binance test --- freqtrade/exchange/binance.py | 2 +- tests/exchange/test_binance.py | 117 +++++++++++++++++++-------------- 2 files changed, 70 insertions(+), 49 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 17e865d64..769073052 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -131,7 +131,7 @@ class Binance(Exchange): if self.trading_mode == TradingMode.FUTURES: try: if self._config['dry_run']: - leverage_brackets_path = Path('data') / 'leverage_brackets.json' + leverage_brackets_path = Path('freqtrade/data') / 'leverage_brackets.json' with open(leverage_brackets_path) as json_file: leverage_brackets = json.load(json_file) else: diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index f0642fda9..03b1d5044 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -2,6 +2,9 @@ from datetime import datetime, timezone from random import randint from unittest.mock import MagicMock, PropertyMock +import json +from pathlib import Path + import ccxt import pytest @@ -203,58 +206,76 @@ def test_get_max_leverage_binance(default_conf, mocker, pair, nominal_value, max def test_fill_leverage_brackets_binance(default_conf, mocker): api_mock = MagicMock() - api_mock.load_leverage_brackets = MagicMock(return_value={ - 'ADA/BUSD': [[0.0, 0.025], - [100000.0, 0.05], - [500000.0, 0.1], - [1000000.0, 0.15], - [2000000.0, 0.25], - [5000000.0, 0.5]], - 'BTC/USDT': [[0.0, 0.004], - [50000.0, 0.005], - [250000.0, 0.01], - [1000000.0, 0.025], - [5000000.0, 0.05], - [20000000.0, 0.1], - [50000000.0, 0.125], - [100000000.0, 0.15], - [200000000.0, 0.25], - [300000000.0, 0.5]], - "ZEC/USDT": [[0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5]], + # api_mock.load_leverage_brackets = MagicMock(return_value={ + # 'ADA/BUSD': [[0.0, 0.025], + # [100000.0, 0.05], + # [500000.0, 0.1], + # [1000000.0, 0.15], + # [2000000.0, 0.25], + # [5000000.0, 0.5]], + # 'BTC/USDT': [[0.0, 0.004], + # [50000.0, 0.005], + # [250000.0, 0.01], + # [1000000.0, 0.025], + # [5000000.0, 0.05], + # [20000000.0, 0.1], + # [50000000.0, 0.125], + # [100000000.0, 0.15], + # [200000000.0, 0.25], + # [300000000.0, 0.5]], + # "ZEC/USDT": [[0.0, 0.01], + # [5000.0, 0.025], + # [25000.0, 0.05], + # [100000.0, 0.1], + # [250000.0, 0.125], + # [1000000.0, 0.5]], - }) + # }) exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + exchange.trading_mode = TradingMode.FUTURES exchange.fill_leverage_brackets() - assert exchange._leverage_brackets == { - 'ADA/BUSD': [[0.0, 0.025], - [100000.0, 0.05], - [500000.0, 0.1], - [1000000.0, 0.15], - [2000000.0, 0.25], - [5000000.0, 0.5]], - 'BTC/USDT': [[0.0, 0.004], - [50000.0, 0.005], - [250000.0, 0.01], - [1000000.0, 0.025], - [5000000.0, 0.05], - [20000000.0, 0.1], - [50000000.0, 0.125], - [100000000.0, 0.15], - [200000000.0, 0.25], - [300000000.0, 0.5]], - "ZEC/USDT": [[0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5]], - } + leverage_brackets_path = Path('freqtrade/data') / 'leverage_brackets.json' + with open(leverage_brackets_path) as json_file: + leverage_brackets = json.load(json_file) + + for pair, brackets in leverage_brackets.items(): + leverage_brackets[pair] = [ + [ + min_amount, + float(margin_req) + ] for [ + min_amount, + margin_req + ] in brackets + ] + + assert exchange._leverage_brackets == leverage_brackets + + # assert exchange._leverage_brackets == { + # 'ADA/BUSD': [[0.0, 0.025], + # [100000.0, 0.05], + # [500000.0, 0.1], + # [1000000.0, 0.15], + # [2000000.0, 0.25], + # [5000000.0, 0.5]], + # 'BTC/USDT': [[0.0, 0.004], + # [50000.0, 0.005], + # [250000.0, 0.01], + # [1000000.0, 0.025], + # [5000000.0, 0.05], + # [20000000.0, 0.1], + # [50000000.0, 0.125], + # [100000000.0, 0.15], + # [200000000.0, 0.25], + # [300000000.0, 0.5]], + # "ZEC/USDT": [[0.0, 0.01], + # [5000.0, 0.025], + # [25000.0, 0.05], + # [100000.0, 0.1], + # [250000.0, 0.125], + # [1000000.0, 0.5]], + # } api_mock = MagicMock() api_mock.load_leverage_brackets = MagicMock() From 798a0c9827a72e7b8abd6ecef6fe0a6531c78a60 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 17 Sep 2021 00:10:53 -0600 Subject: [PATCH 08/64] Tried to add call count to test_create_order --- tests/exchange/test_exchange.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index d641b0a63..8448819aa 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1094,10 +1094,7 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou assert round(order["average"], 4) == round(endprice, 4) -@pytest.mark.parametrize("side", [ - ("buy"), - ("sell") -]) +@pytest.mark.parametrize("side", ["buy", "sell"]) @pytest.mark.parametrize("ordertype,rate,marketprice", [ ("market", None, None), ("market", 200, True), @@ -1126,7 +1123,7 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, side=side, amount=1, rate=200, - leverage=3.0 + leverage=1.0 ) assert 'id' in order @@ -1138,6 +1135,21 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, assert api_mock.create_order.call_args[0][3] == 1 assert api_mock.create_order.call_args[0][4] is rate + assert api_mock._set_leverage.call_count == 0 if side == "buy" else 1 + assert api_mock.set_margin_mode.call_count == 0 if side == "buy" else 1 + + order = exchange.create_order( + pair='ETH/BTC', + ordertype=ordertype, + side=side, + amount=1, + rate=200, + leverage=3.0 + ) + + assert api_mock._set_leverage.call_count == 1 + assert api_mock.set_margin_mode.call_count == 1 + def test_buy_dry_run(default_conf, mocker): default_conf['dry_run'] = True From 32e52cd4606ce3a687a5e3640ddd4fcc64c46aaf Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 17 Sep 2021 00:41:00 -0600 Subject: [PATCH 09/64] Added leverage brackets dry run test --- freqtrade/exchange/binance.py | 2 +- tests/exchange/test_binance.py | 135 ++-- tests/exchange/test_exchange.py | 5 +- tests/leverage_brackets.py | 1215 +++++++++++++++++++++++++++++++ 4 files changed, 1283 insertions(+), 74 deletions(-) create mode 100644 tests/leverage_brackets.py diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 769073052..572fa2141 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -33,7 +33,7 @@ class Binance(Exchange): # TradingMode.SPOT always supported and not required in this list # (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported # (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported - # (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported + (TradingMode.FUTURES, Collateral.ISOLATED) ] def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 03b1d5044..4999e94af 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -2,16 +2,14 @@ from datetime import datetime, timezone from random import randint from unittest.mock import MagicMock, PropertyMock -import json -from pathlib import Path - import ccxt import pytest -from freqtrade.enums import TradingMode +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException from tests.conftest import get_mock_coro, get_patched_exchange, log_has_re from tests.exchange.test_exchange import ccxt_exceptionhandlers +from tests.leverage_brackets import leverage_brackets @pytest.mark.parametrize('limitratio,expected,side', [ @@ -206,76 +204,61 @@ def test_get_max_leverage_binance(default_conf, mocker, pair, nominal_value, max def test_fill_leverage_brackets_binance(default_conf, mocker): api_mock = MagicMock() - # api_mock.load_leverage_brackets = MagicMock(return_value={ - # 'ADA/BUSD': [[0.0, 0.025], - # [100000.0, 0.05], - # [500000.0, 0.1], - # [1000000.0, 0.15], - # [2000000.0, 0.25], - # [5000000.0, 0.5]], - # 'BTC/USDT': [[0.0, 0.004], - # [50000.0, 0.005], - # [250000.0, 0.01], - # [1000000.0, 0.025], - # [5000000.0, 0.05], - # [20000000.0, 0.1], - # [50000000.0, 0.125], - # [100000000.0, 0.15], - # [200000000.0, 0.25], - # [300000000.0, 0.5]], - # "ZEC/USDT": [[0.0, 0.01], - # [5000.0, 0.025], - # [25000.0, 0.05], - # [100000.0, 0.1], - # [250000.0, 0.125], - # [1000000.0, 0.5]], + api_mock.load_leverage_brackets = MagicMock(return_value={ + 'ADA/BUSD': [[0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5]], + 'BTC/USDT': [[0.0, 0.004], + [50000.0, 0.005], + [250000.0, 0.01], + [1000000.0, 0.025], + [5000000.0, 0.05], + [20000000.0, 0.1], + [50000000.0, 0.125], + [100000000.0, 0.15], + [200000000.0, 0.25], + [300000000.0, 0.5]], + "ZEC/USDT": [[0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5]], - # }) + }) + default_conf['dry_run'] = False + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['collateral'] = Collateral.ISOLATED exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") - exchange.trading_mode = TradingMode.FUTURES exchange.fill_leverage_brackets() - leverage_brackets_path = Path('freqtrade/data') / 'leverage_brackets.json' - with open(leverage_brackets_path) as json_file: - leverage_brackets = json.load(json_file) - - for pair, brackets in leverage_brackets.items(): - leverage_brackets[pair] = [ - [ - min_amount, - float(margin_req) - ] for [ - min_amount, - margin_req - ] in brackets - ] - - assert exchange._leverage_brackets == leverage_brackets - - # assert exchange._leverage_brackets == { - # 'ADA/BUSD': [[0.0, 0.025], - # [100000.0, 0.05], - # [500000.0, 0.1], - # [1000000.0, 0.15], - # [2000000.0, 0.25], - # [5000000.0, 0.5]], - # 'BTC/USDT': [[0.0, 0.004], - # [50000.0, 0.005], - # [250000.0, 0.01], - # [1000000.0, 0.025], - # [5000000.0, 0.05], - # [20000000.0, 0.1], - # [50000000.0, 0.125], - # [100000000.0, 0.15], - # [200000000.0, 0.25], - # [300000000.0, 0.5]], - # "ZEC/USDT": [[0.0, 0.01], - # [5000.0, 0.025], - # [25000.0, 0.05], - # [100000.0, 0.1], - # [250000.0, 0.125], - # [1000000.0, 0.5]], - # } + assert exchange._leverage_brackets == { + 'ADA/BUSD': [[0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5]], + 'BTC/USDT': [[0.0, 0.004], + [50000.0, 0.005], + [250000.0, 0.01], + [1000000.0, 0.025], + [5000000.0, 0.05], + [20000000.0, 0.1], + [50000000.0, 0.125], + [100000000.0, 0.15], + [200000000.0, 0.25], + [300000000.0, 0.5]], + "ZEC/USDT": [[0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5]], + } api_mock = MagicMock() api_mock.load_leverage_brackets = MagicMock() @@ -291,12 +274,22 @@ def test_fill_leverage_brackets_binance(default_conf, mocker): ) +def test_fill_leverage_brackets_binance_dryrun(default_conf, mocker): + api_mock = MagicMock() + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['collateral'] = Collateral.ISOLATED + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + exchange.fill_leverage_brackets() + + assert exchange._leverage_brackets == leverage_brackets() + + def test__set_leverage_binance(mocker, default_conf): api_mock = MagicMock() api_mock.set_leverage = MagicMock() type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) - + default_conf['dry_run'] = False exchange = get_patched_exchange(mocker, default_conf, id="binance") exchange._set_leverage(3.0, trading_mode=TradingMode.MARGIN) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 8448819aa..ce09e31e7 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3076,6 +3076,7 @@ def test__set_leverage(mocker, default_conf, exchange_name, trading_mode): api_mock = MagicMock() api_mock.set_leverage = MagicMock() type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) + default_conf['dry_run'] = False ccxt_exceptionhandlers( mocker, @@ -3099,6 +3100,7 @@ def test_set_margin_mode(mocker, default_conf, collateral): api_mock = MagicMock() api_mock.set_margin_mode = MagicMock() type(api_mock).has = PropertyMock(return_value={'setMarginMode': True}) + default_conf['dry_run'] = False ccxt_exceptionhandlers( mocker, @@ -3130,7 +3132,6 @@ def test_set_margin_mode(mocker, default_conf, collateral): # TODO-lev: Remove once implemented ("binance", TradingMode.MARGIN, Collateral.CROSS, True), ("binance", TradingMode.FUTURES, Collateral.CROSS, True), - ("binance", TradingMode.FUTURES, Collateral.ISOLATED, True), ("kraken", TradingMode.MARGIN, Collateral.CROSS, True), ("kraken", TradingMode.FUTURES, Collateral.CROSS, True), ("ftx", TradingMode.MARGIN, Collateral.CROSS, True), @@ -3139,7 +3140,7 @@ def test_set_margin_mode(mocker, default_conf, collateral): # TODO-lev: Uncomment once implemented # ("binance", TradingMode.MARGIN, Collateral.CROSS, False), # ("binance", TradingMode.FUTURES, Collateral.CROSS, False), - # ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False), + ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False), # ("kraken", TradingMode.MARGIN, Collateral.CROSS, False), # ("kraken", TradingMode.FUTURES, Collateral.CROSS, False), # ("ftx", TradingMode.MARGIN, Collateral.CROSS, False), diff --git a/tests/leverage_brackets.py b/tests/leverage_brackets.py new file mode 100644 index 000000000..aa60a7af2 --- /dev/null +++ b/tests/leverage_brackets.py @@ -0,0 +1,1215 @@ +def leverage_brackets(): + return { + "1000SHIB/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "1INCH/USDT": [ + [0.0, 0.012], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "AAVE/USDT": [ + [0.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.1665], + [10000000.0, 0.25] + ], + "ADA/BUSD": [ + [0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5] + ], + "ADA/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "AKRO/USDT": [ + [0.0, 0.012], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ALGO/USDT": [ + [0.0, 0.01], + [50000.0, 0.025], + [150000.0, 0.05], + [250000.0, 0.1], + [500000.0, 0.125], + [1000000.0, 0.25], + [2000000.0, 0.5] + ], + "ALICE/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ALPHA/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ANKR/USDT": [ + [0.0, 0.012], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ATA/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ATOM/USDT": [ + [0.0, 0.01], + [50000.0, 0.025], + [150000.0, 0.05], + [250000.0, 0.1], + [500000.0, 0.125], + [1000000.0, 0.25], + [2000000.0, 0.5] + ], + "AUDIO/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "AVAX/USDT": [ + [0.0, 0.01], + [50000.0, 0.025], + [150000.0, 0.05], + [250000.0, 0.1], + [500000.0, 0.125], + [750000.0, 0.25], + [1000000.0, 0.5] + ], + "AXS/USDT": [ + [0.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.1665], + [10000000.0, 0.25], + [15000000.0, 0.5] + ], + "BAKE/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "BAL/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "BAND/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "BAT/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "BCH/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "BEL/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "BLZ/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "BNB/BUSD": [ + [0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5] + ], + "BNB/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "BTC/BUSD": [ + [0.0, 0.004], + [25000.0, 0.005], + [100000.0, 0.01], + [500000.0, 0.025], + [1000000.0, 0.05], + [2000000.0, 0.1], + [5000000.0, 0.125], + [10000000.0, 0.15], + [20000000.0, 0.25], + [30000000.0, 0.5] + ], + "BTC/USDT": [ + [0.0, 0.004], + [50000.0, 0.005], + [250000.0, 0.01], + [1000000.0, 0.025], + [5000000.0, 0.05], + [20000000.0, 0.1], + [50000000.0, 0.125], + [100000000.0, 0.15], + [200000000.0, 0.25], + [300000000.0, 0.5] + ], + "BTCBUSD_210129": [ + [0.0, 0.004], + [5000.0, 0.005], + [25000.0, 0.01], + [100000.0, 0.025], + [500000.0, 0.05], + [2000000.0, 0.1], + [5000000.0, 0.125], + [10000000.0, 0.15], + [20000000.0, 0.25] + ], + "BTCBUSD_210226": [ + [0.0, 0.004], + [5000.0, 0.005], + [25000.0, 0.01], + [100000.0, 0.025], + [500000.0, 0.05], + [2000000.0, 0.1], + [5000000.0, 0.125], + [10000000.0, 0.15], + [20000000.0, 0.25] + ], + "BTCDOM/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "BTCSTUSDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "BTCUSDT_210326": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "BTCUSDT_210625": [ + [0.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "BTCUSDT_210924": [ + [0.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25], + [20000000.0, 0.5] + ], + "BTS/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "BTT/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "BZRX/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "C98/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "CELR/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "CHR/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "CHZ/USDT": [ + [0.0, 0.012], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "COMP/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "COTI/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "CRV/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "CTK/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "CVC/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "DASH/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "DEFI/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "DENT/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "DGB/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "DODO/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "DOGE/BUSD": [ + [0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5] + ], + "DOGE/USDT": [ + [0.0, 0.01], + [50000.0, 0.025], + [150000.0, 0.05], + [250000.0, 0.1], + [500000.0, 0.125], + [750000.0, 0.25], + [1000000.0, 0.5] + ], + "DOT/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "DOTECOUSDT": [ + [0.0, 0.012], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "DYDX/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "EGLD/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ENJ/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "EOS/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "ETC/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "ETH/BUSD": [ + [0.0, 0.004], + [25000.0, 0.005], + [100000.0, 0.01], + [500000.0, 0.025], + [1000000.0, 0.05], + [2000000.0, 0.1], + [5000000.0, 0.125], + [10000000.0, 0.15], + [20000000.0, 0.25], + [30000000.0, 0.5] + ], + "ETH/USDT": [ + [0.0, 0.005], + [10000.0, 0.0065], + [100000.0, 0.01], + [500000.0, 0.02], + [1000000.0, 0.05], + [2000000.0, 0.1], + [5000000.0, 0.125], + [10000000.0, 0.15], + [20000000.0, 0.25] + ], + "ETHUSDT_210326": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "ETHUSDT_210625": [ + [0.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "ETHUSDT_210924": [ + [0.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25], + [20000000.0, 0.5] + ], + "FIL/USDT": [ + [0.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.1665], + [10000000.0, 0.25] + ], + "FLM/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "FTM/USDT": [ + [0.0, 0.01], + [50000.0, 0.025], + [150000.0, 0.05], + [250000.0, 0.1], + [500000.0, 0.125], + [750000.0, 0.25], + [1000000.0, 0.5] + ], + "FTT/BUSD": [ + [0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5] + ], + "GRT/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "GTC/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "HBAR/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "HNT/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "HOT/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ICP/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ICX/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "IOST/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "IOTA/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "IOTX/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "KAVA/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "KEEP/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "KNC/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "KSM/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "LENDUSDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "LINA/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "LINK/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "LIT/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "LRC/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "LTC/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "LUNA/USDT": [ + [0.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.1665], + [10000000.0, 0.25], + [15000000.0, 0.5] + ], + "MANA/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "MASK/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "MATIC/USDT": [ + [0.0, 0.01], + [50000.0, 0.025], + [150000.0, 0.05], + [250000.0, 0.1], + [500000.0, 0.125], + [750000.0, 0.25], + [1000000.0, 0.5] + ], + "MKR/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "MTL/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "NEAR/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "NEO/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "NKN/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "OCEAN/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "OGN/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "OMG/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ONE/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ONT/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "QTUM/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "RAY/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "REEF/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "REN/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "RLC/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "RSR/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "RUNE/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "RVN/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "SAND/USDT": [ + [0.0, 0.012], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "SC/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "SFP/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "SKL/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "SNX/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "SOL/BUSD": [ + [0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5] + ], + "SOL/USDT": [ + [0.0, 0.01], + [50000.0, 0.025], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.25], + [10000000.0, 0.5] + ], + "SRM/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "STMX/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "STORJ/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "SUSHI/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "SXP/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "THETA/USDT": [ + [0.0, 0.01], + [50000.0, 0.025], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.1665], + [10000000.0, 0.25] + ], + "TLM/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "TOMO/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "TRB/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "TRX/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "UNFI/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "UNI/USDT": [ + [0.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.1665], + [10000000.0, 0.25] + ], + "VET/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "WAVES/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "XEM/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "XLM/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "XMR/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "XRP/BUSD": [ + [0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5] + ], + "XRP/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "XTZ/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "YFI/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "YFII/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ZEC/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ZEN/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ZIL/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ZRX/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ] + } From 54ef36a4971d77467a10fa343857aec27a312e5a Mon Sep 17 00:00:00 2001 From: Sergey Khliustin Date: Fri, 17 Sep 2021 13:45:44 +0300 Subject: [PATCH 10/64] Updates after review to PerformanceFilter days param --- freqtrade/persistence/models.py | 34 ++++--------------- .../plugins/pairlist/PerformanceFilter.py | 5 +-- 2 files changed, 7 insertions(+), 32 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 91a26eba7..222be7169 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -832,43 +832,21 @@ class Trade(_DECL_BASE, LocalTrade): return total_open_stake_amount or 0 @staticmethod - def get_overall_performance() -> List[Dict[str, Any]]: + def get_overall_performance(days=None) -> List[Dict[str, Any]]: """ Returns List of dicts containing all Trades, including profit and trade count NOTE: Not supported in Backtesting. """ + filters = [Trade.is_open.is_(False)] + if days: + start_date = datetime.today() - timedelta(days) + filters.append((Trade.close_date >= start_date)) pair_rates = Trade.query.with_entities( Trade.pair, func.sum(Trade.close_profit).label('profit_sum'), func.sum(Trade.close_profit_abs).label('profit_sum_abs'), func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ - .group_by(Trade.pair) \ - .order_by(desc('profit_sum_abs')) \ - .all() - return [ - { - 'pair': pair, - 'profit': profit, - 'profit_abs': profit_abs, - 'count': count - } - for pair, profit, profit_abs, count in pair_rates - ] - - @staticmethod - def get_performance(days: int) -> List[Dict[str, Any]]: - """ - Returns List of dicts containing all Trades, including profit and trade count - NOTE: Not supported in Backtesting. - """ - start_date = datetime.today() - timedelta(days) - pair_rates = Trade.query.with_entities( - Trade.pair, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False) & (Trade.close_date >= start_date))\ + ).filter(filters)\ .group_by(Trade.pair) \ .order_by(desc('profit_sum_abs')) \ .all() diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index 4d530fe88..920b884a0 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -48,10 +48,7 @@ class PerformanceFilter(IPairList): """ # Get the trading performance for pairs from database try: - if self._days > 0: - performance = pd.DataFrame(Trade.get_performance(self._days)) - else: - performance = pd.DataFrame(Trade.get_overall_performance()) + performance = pd.DataFrame(Trade.get_overall_performance(self._days)) except AttributeError: # Performancefilter does not work in backtesting. self.log_once("PerformanceFilter is not available in this mode.", logger.warning) From 982deeedf04cb1be9c5ae7b1ec3598a9d09a4b9a Mon Sep 17 00:00:00 2001 From: sergeykhliustin <51409210+sergeykhliustin@users.noreply.github.com> Date: Fri, 17 Sep 2021 18:23:13 +0300 Subject: [PATCH 11/64] Update freqtrade/persistence/models.py Co-authored-by: Matthias --- freqtrade/persistence/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 222be7169..a2e30d58c 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -846,7 +846,7 @@ class Trade(_DECL_BASE, LocalTrade): func.sum(Trade.close_profit).label('profit_sum'), func.sum(Trade.close_profit_abs).label('profit_sum_abs'), func.count(Trade.pair).label('count') - ).filter(filters)\ + ).filter(*filters)\ .group_by(Trade.pair) \ .order_by(desc('profit_sum_abs')) \ .all() From 3a98fb72a491536cef7b601573c3ca45f64e8fc5 Mon Sep 17 00:00:00 2001 From: raphael Date: Fri, 17 Sep 2021 11:42:33 -0400 Subject: [PATCH 12/64] Update prepare_trials_columns() return type Was returning str, updated to pd.DataFrame --- freqtrade/optimize/hyperopt_tools.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index b2e024f65..2fcab0b0f 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -7,6 +7,7 @@ from pathlib import Path from typing import Any, Dict, Iterator, List, Optional, Tuple import numpy as np +import pandas as pd import rapidjson import tabulate from colorama import Fore, Style @@ -298,7 +299,7 @@ class HyperoptTools(): f"Objective: {results['loss']:.5f}") @staticmethod - def prepare_trials_columns(trials, legacy_mode: bool, has_drawdown: bool) -> str: + def prepare_trials_columns(trials, legacy_mode: bool, has_drawdown: bool) -> pd.DataFrame: trials['Best'] = '' From 5fc993231a2d138e9ff994f1d4548888551dd73c Mon Sep 17 00:00:00 2001 From: raphael Date: Fri, 17 Sep 2021 11:51:55 -0400 Subject: [PATCH 13/64] Update HyperoptTools.export_csv_file usage --- freqtrade/commands/hyperopt_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index 089529d15..614c4b3f5 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -53,7 +53,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: if epochs and export_csv: HyperoptTools.export_csv_file( - config, epochs, total_epochs, not config.get('hyperopt_list_best', False), export_csv + config, epochs, export_csv ) From 124e97f3b9a7006cfcf2337255f10c8bcc3e0038 Mon Sep 17 00:00:00 2001 From: raphael Date: Fri, 17 Sep 2021 11:57:36 -0400 Subject: [PATCH 14/64] Remove ununsed variables from export_csv_file --- freqtrade/optimize/hyperopt_tools.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index b2e024f65..6a5c31d56 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -435,8 +435,7 @@ class HyperoptTools(): return table @staticmethod - def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool, - csv_file: str) -> None: + def export_csv_file(config: dict, results: list, csv_file: str) -> None: """ Log result to csv-file """ From fb6beb90e87e75d2997651324daaf0c5954e661a Mon Sep 17 00:00:00 2001 From: Ottavio Miele Date: Fri, 17 Sep 2021 18:03:54 +0200 Subject: [PATCH 15/64] Include Raspberry Pi armv6 (0, 0W,...) --- setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index 217500569..aee7c80b5 100755 --- a/setup.sh +++ b/setup.sh @@ -62,7 +62,7 @@ function updateenv() { then REQUIREMENTS_PLOT="-r requirements-plot.txt" fi - if [ "${SYS_ARCH}" == "armv7l" ]; then + if [ "${SYS_ARCH}" == "armv7l" ] || [ "${SYS_ARCH}" == "armv6l" ]; then echo "Detected Raspberry, installing cython, skipping hyperopt installation." ${PYTHON} -m pip install --upgrade cython else From 9525a5b96cf779dfa80abdae355e2d5c9382f505 Mon Sep 17 00:00:00 2001 From: raphael Date: Fri, 17 Sep 2021 14:10:37 -0400 Subject: [PATCH 16/64] Add type to "trials" parameter --- freqtrade/optimize/hyperopt_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 2fcab0b0f..1874b2949 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -299,7 +299,7 @@ class HyperoptTools(): f"Objective: {results['loss']:.5f}") @staticmethod - def prepare_trials_columns(trials, legacy_mode: bool, has_drawdown: bool) -> pd.DataFrame: + def prepare_trials_columns(trials: pd.DataFrame, legacy_mode: bool, has_drawdown: bool) -> pd.DataFrame: trials['Best'] = '' From e715f2a253afe2ef4332166c0357595976dd7dfc Mon Sep 17 00:00:00 2001 From: raphael Date: Fri, 17 Sep 2021 14:23:26 -0400 Subject: [PATCH 17/64] Update formatting Line 302 was too long --- freqtrade/optimize/hyperopt_tools.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 1874b2949..ac92ad682 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -299,7 +299,8 @@ class HyperoptTools(): f"Objective: {results['loss']:.5f}") @staticmethod - def prepare_trials_columns(trials: pd.DataFrame, legacy_mode: bool, has_drawdown: bool) -> pd.DataFrame: + def prepare_trials_columns(trials: pd.DataFrame, legacy_mode: bool, + has_drawdown: bool) -> pd.DataFrame: trials['Best'] = '' From 4b2c1a9b8e0e487a60454125e8916026b8443c5c Mon Sep 17 00:00:00 2001 From: raphael Date: Fri, 17 Sep 2021 14:39:15 -0400 Subject: [PATCH 18/64] Remove trailing whitespace --- freqtrade/optimize/hyperopt_tools.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index ac92ad682..7a4ce5f23 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -299,9 +299,8 @@ class HyperoptTools(): f"Objective: {results['loss']:.5f}") @staticmethod - def prepare_trials_columns(trials: pd.DataFrame, legacy_mode: bool, + def prepare_trials_columns(trials: pd.DataFrame, legacy_mode: bool, has_drawdown: bool) -> pd.DataFrame: - trials['Best'] = '' if 'results_metrics.winsdrawslosses' not in trials.columns: From d7395e873be5560932752e00200c70fc2cd169c3 Mon Sep 17 00:00:00 2001 From: Sergey Khliustin Date: Fri, 17 Sep 2021 22:05:57 +0300 Subject: [PATCH 19/64] Removed unused OperationalException --- freqtrade/plugins/pairlist/PerformanceFilter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index 920b884a0..5750b721c 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -7,7 +7,6 @@ from typing import Dict, List, Any import pandas as pd from freqtrade.persistence import Trade -from freqtrade.exceptions import OperationalException from freqtrade.plugins.pairlist.IPairList import IPairList From 2c21bbfa0c6c914ec77610be1b118eb9defbdc45 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 17 Sep 2021 14:16:52 -0600 Subject: [PATCH 20/64] Fixed create order margin call count tests and made _ccxt_config a computed property --- freqtrade/exchange/bibox.py | 5 +++- freqtrade/exchange/binance.py | 20 ++++++++++++- freqtrade/exchange/exchange.py | 53 +++++++++++++++------------------ tests/conftest.py | 26 +++++++++++++--- tests/exchange/test_binance.py | 12 ++++++++ tests/exchange/test_exchange.py | 27 +++++++++-------- 6 files changed, 96 insertions(+), 47 deletions(-) diff --git a/freqtrade/exchange/bibox.py b/freqtrade/exchange/bibox.py index f0c2dd00b..074dd2b10 100644 --- a/freqtrade/exchange/bibox.py +++ b/freqtrade/exchange/bibox.py @@ -20,4 +20,7 @@ class Bibox(Exchange): # fetchCurrencies API point requires authentication for Bibox, # so switch it off for Freqtrade load_markets() - _ccxt_config: Dict = {"has": {"fetchCurrencies": False}} + @property + def _ccxt_config(self) -> Dict: + # Parameters to add directly to ccxt sync/async initialization. + return {"has": {"fetchCurrencies": False}} diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 572fa2141..60a1b8019 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -33,9 +33,27 @@ class Binance(Exchange): # TradingMode.SPOT always supported and not required in this list # (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported # (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported - (TradingMode.FUTURES, Collateral.ISOLATED) + # (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported ] + @property + def _ccxt_config(self) -> Dict: + # Parameters to add directly to ccxt sync/async initialization. + if self.trading_mode == TradingMode.MARGIN: + return { + "options": { + "defaultType": "margin" + } + } + elif self.trading_mode == TradingMode.FUTURES: + return { + "options": { + "defaultType": "future" + } + } + else: + return {} + def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: """ Verify stop_loss against stoploss-order value (limit or price) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 8bbc88235..4021e7d02 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -49,9 +49,6 @@ class Exchange: _config: Dict = {} - # Parameters to add directly to ccxt sync/async initialization. - _ccxt_config: Dict = {} - # Parameters to add directly to buy/sell calls (like agreeing to trading agreement) _params: Dict = {} @@ -131,21 +128,6 @@ class Exchange: self._trades_pagination = self._ft_has['trades_pagination'] self._trades_pagination_arg = self._ft_has['trades_pagination_arg'] - # Initialize ccxt objects - ccxt_config = self._ccxt_config.copy() - ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_config) - ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_sync_config', {}), ccxt_config) - - self._api = self._init_ccxt(exchange_config, ccxt_kwargs=ccxt_config) - - ccxt_async_config = self._ccxt_config.copy() - ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), - ccxt_async_config) - ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_async_config', {}), - ccxt_async_config) - self._api_async = self._init_ccxt( - exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config) - self.trading_mode: TradingMode = ( TradingMode(config.get('trading_mode')) if config.get('trading_mode') @@ -157,6 +139,21 @@ class Exchange: else None ) + # Initialize ccxt objects + ccxt_config = self._ccxt_config + ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_config) + ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_sync_config', {}), ccxt_config) + + self._api = self._init_ccxt(exchange_config, ccxt_kwargs=ccxt_config) + + ccxt_async_config = self._ccxt_config + ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), + ccxt_async_config) + ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_async_config', {}), + ccxt_async_config) + self._api_async = self._init_ccxt( + exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config) + if self.trading_mode != TradingMode.SPOT: self.fill_leverage_brackets() @@ -210,7 +207,7 @@ class Exchange: 'secret': exchange_config.get('secret'), 'password': exchange_config.get('password'), 'uid': exchange_config.get('uid', ''), - 'options': exchange_config.get('options', {}) + # 'options': exchange_config.get('options', {}) } if ccxt_kwargs: logger.info('Applying additional ccxt config: %s', ccxt_kwargs) @@ -231,6 +228,11 @@ class Exchange: return api + @property + def _ccxt_config(self) -> Dict: + # Parameters to add directly to ccxt sync/async initialization. + return {} + @property def name(self) -> str: """exchange Name (from ccxt)""" @@ -258,13 +260,6 @@ class Exchange: """exchange ccxt precisionMode""" return self._api.precisionMode - @property - def running_live_mode(self) -> bool: - return ( - self._config['runmode'].value not in ('backtest', 'hyperopt') and - not self._config['dry_run'] - ) - def _log_exchange_response(self, endpoint, response) -> None: """ Log exchange responses """ if self.log_responses: @@ -624,12 +619,12 @@ class Exchange: # The value returned should satisfy both limits: for amount (base currency) and # for cost (quote, stake currency), so max() is used here. # See also #2575 at github. - return self._divide_stake_amount_by_leverage( + return self._get_stake_amount_considering_leverage( max(min_stake_amounts) * amount_reserve_percent, leverage or 1.0 ) - def _divide_stake_amount_by_leverage(self, stake_amount: float, leverage: float): + def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float): """ Takes the minimum stake amount for a pair with no leverage and returns the minimum stake amount when leverage is considered @@ -1603,7 +1598,7 @@ class Exchange: def fill_leverage_brackets(self): """ - #TODO-lev: Should maybe be renamed, leverage_brackets might not be accurate for kraken + # TODO-lev: Should maybe be renamed, leverage_brackets might not be accurate for kraken Assigns property _leverage_brackets to a dictionary of information about the leverage allowed on each pair """ diff --git a/tests/conftest.py b/tests/conftest.py index 3de299752..d2f24fa69 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,7 +18,7 @@ from freqtrade import constants from freqtrade.commands import Arguments from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo -from freqtrade.enums import RunMode +from freqtrade.enums import Collateral, RunMode, TradingMode from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import LocalTrade, Trade, init_db @@ -81,7 +81,13 @@ def patched_configuration_load_config_file(mocker, config) -> None: ) -def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> None: +def patch_exchange( + mocker, + api_mock=None, + id='binance', + mock_markets=True, + mock_supported_modes=True +) -> None: mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) @@ -90,10 +96,22 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No 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())) + if mock_supported_modes: + mocker.patch( + f'freqtrade.exchange.{id.capitalize()}._supported_trading_mode_collateral_pairs', + PropertyMock(return_value=[ + (TradingMode.MARGIN, Collateral.CROSS), + (TradingMode.MARGIN, Collateral.ISOLATED), + (TradingMode.FUTURES, Collateral.CROSS), + (TradingMode.FUTURES, Collateral.ISOLATED) + ]) + ) + if api_mock: mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) else: @@ -101,8 +119,8 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No def get_patched_exchange(mocker, config, api_mock=None, id='binance', - mock_markets=True) -> Exchange: - patch_exchange(mocker, api_mock, id, mock_markets) + mock_markets=True, mock_supported_modes=True) -> Exchange: + patch_exchange(mocker, api_mock, id, mock_markets, mock_supported_modes) config['exchange']['name'] = id try: exchange = ExchangeResolver.load_exchange(id, config) diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 4999e94af..cbbace1db 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -336,3 +336,15 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog): assert exchange._api_async.fetch_ohlcv.call_count == 2 assert res == ohlcv assert log_has_re(r"Candle-data for ETH/BTC available starting with .*", caplog) + + +@pytest.mark.parametrize("trading_mode,collateral,config", [ + ("", "", {}), + ("margin", "cross", {"options": {"defaultType": "margin"}}), + ("futures", "isolated", {"options": {"defaultType": "future"}}), +]) +def test__ccxt_config(default_conf, mocker, trading_mode, collateral, config): + default_conf['trading_mode'] = trading_mode + default_conf['collateral'] = collateral + exchange = get_patched_exchange(mocker, default_conf, id="binance") + assert exchange._ccxt_config == config diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index ce09e31e7..8b16a9f12 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -132,10 +132,9 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog): assert log_has("Applying additional ccxt config: {'TestKWARG': 11, 'TestKWARG44': 11}", caplog) assert ex._api.headers == {'hello': 'world'} + assert ex._ccxt_config == {} Exchange._headers = {} - # TODO-lev: Test with options - def test_destroy(default_conf, mocker, caplog): caplog.set_level(logging.DEBUG) @@ -1116,6 +1115,8 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, 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) + exchange._set_leverage = MagicMock() + exchange.set_margin_mode = MagicMock() order = exchange.create_order( pair='ETH/BTC', @@ -1134,10 +1135,10 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, assert api_mock.create_order.call_args[0][2] == side assert api_mock.create_order.call_args[0][3] == 1 assert api_mock.create_order.call_args[0][4] is rate + assert exchange._set_leverage.call_count == 0 + assert exchange.set_margin_mode.call_count == 0 - assert api_mock._set_leverage.call_count == 0 if side == "buy" else 1 - assert api_mock.set_margin_mode.call_count == 0 if side == "buy" else 1 - + exchange.trading_mode = TradingMode.FUTURES order = exchange.create_order( pair='ETH/BTC', ordertype=ordertype, @@ -1147,8 +1148,8 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, leverage=3.0 ) - assert api_mock._set_leverage.call_count == 1 - assert api_mock.set_margin_mode.call_count == 1 + assert exchange._set_leverage.call_count == 1 + assert exchange.set_margin_mode.call_count == 1 def test_buy_dry_run(default_conf, mocker): @@ -3042,7 +3043,6 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None: (3, 5, 5), (4, 5, 2), (5, 5, 1), - ]) def test_calculate_backoff(retrycount, max_retries, expected): assert calculate_backoff(retrycount, max_retries) == expected @@ -3054,7 +3054,7 @@ def test_calculate_backoff(retrycount, max_retries, expected): (20.0, 5.0, 4.0), (100.0, 100.0, 1.0) ]) -def test_divide_stake_amount_by_leverage( +def test_get_stake_amount_considering_leverage( exchange, stake_amount, leverage, @@ -3063,7 +3063,8 @@ def test_divide_stake_amount_by_leverage( default_conf ): exchange = get_patched_exchange(mocker, default_conf, id=exchange) - assert exchange._divide_stake_amount_by_leverage(stake_amount, leverage) == min_stake_with_lev + assert exchange._get_stake_amount_considering_leverage( + stake_amount, leverage) == min_stake_with_lev @pytest.mark.parametrize("exchange_name,trading_mode", [ @@ -3132,6 +3133,7 @@ def test_set_margin_mode(mocker, default_conf, collateral): # TODO-lev: Remove once implemented ("binance", TradingMode.MARGIN, Collateral.CROSS, True), ("binance", TradingMode.FUTURES, Collateral.CROSS, True), + ("binance", TradingMode.FUTURES, Collateral.ISOLATED, True), ("kraken", TradingMode.MARGIN, Collateral.CROSS, True), ("kraken", TradingMode.FUTURES, Collateral.CROSS, True), ("ftx", TradingMode.MARGIN, Collateral.CROSS, True), @@ -3140,7 +3142,7 @@ def test_set_margin_mode(mocker, default_conf, collateral): # TODO-lev: Uncomment once implemented # ("binance", TradingMode.MARGIN, Collateral.CROSS, False), # ("binance", TradingMode.FUTURES, Collateral.CROSS, False), - ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False), + # ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False), # ("kraken", TradingMode.MARGIN, Collateral.CROSS, False), # ("kraken", TradingMode.FUTURES, Collateral.CROSS, False), # ("ftx", TradingMode.MARGIN, Collateral.CROSS, False), @@ -3154,7 +3156,8 @@ def test_validate_trading_mode_and_collateral( collateral, exception_thrown ): - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange( + mocker, default_conf, id=exchange_name, mock_supported_modes=False) if (exception_thrown): with pytest.raises(OperationalException): exchange.validate_trading_mode_and_collateral(trading_mode, collateral) From 12c12d42df5e0f838aad1ef971ca8530345d2d00 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 08:26:48 +0200 Subject: [PATCH 21/64] Add documentation for days parameter in PerformanceFilter --- docs/includes/pairlists.md | 14 ++++++++++++++ freqtrade/persistence/models.py | 4 ++-- freqtrade/plugins/pairlist/PerformanceFilter.py | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 69e12d5dc..71165f93b 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -165,6 +165,7 @@ Example to remove the first 10 pairs from the pairlist: ```json "pairlists": [ + // ... { "method": "OffsetFilter", "offset": 10 @@ -190,6 +191,19 @@ Sorts pairs by past trade performance, as follows: Trade count is used as a tie breaker. +You can use the `days` parameter to only consider performance of the past X days. +Not defining this parameter (or setting it to 0) will use all-time performance. + +```json +"pairlists": [ + // ... + { + "method": "PerformanceFilter", + "days": 10 + } +], +``` + !!! Note `PerformanceFilter` does not support backtesting mode. diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a2e30d58c..f5f4e3a26 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -2,7 +2,7 @@ This module contains the class to persist trades into SQLite """ import logging -from datetime import datetime, timezone, timedelta +from datetime import datetime, timedelta, timezone from decimal import Decimal from typing import Any, Dict, List, Optional @@ -840,7 +840,7 @@ class Trade(_DECL_BASE, LocalTrade): filters = [Trade.is_open.is_(False)] if days: start_date = datetime.today() - timedelta(days) - filters.append((Trade.close_date >= start_date)) + filters.append(Trade.close_date >= start_date) pair_rates = Trade.query.with_entities( Trade.pair, func.sum(Trade.close_profit).label('profit_sum'), diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index 5750b721c..ee443b0fe 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -2,7 +2,7 @@ Performance pair list filter """ import logging -from typing import Dict, List, Any +from typing import Any, Dict, List import pandas as pd From 564e0b9a1ac89c19f2a9c1e94c04cafd4fd96afe Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 08:36:06 +0200 Subject: [PATCH 22/64] Switch performanceFilter to use Minutes lookback resolution closes #5060 --- docs/includes/pairlists.md | 4 ++-- freqtrade/persistence/models.py | 6 +++--- freqtrade/plugins/pairlist/PerformanceFilter.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 71165f93b..b612a4ddf 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -191,7 +191,7 @@ Sorts pairs by past trade performance, as follows: Trade count is used as a tie breaker. -You can use the `days` parameter to only consider performance of the past X days. +You can use the `minutes` parameter to only consider performance of the past X minutes (rolling window). Not defining this parameter (or setting it to 0) will use all-time performance. ```json @@ -199,7 +199,7 @@ Not defining this parameter (or setting it to 0) will use all-time performance. // ... { "method": "PerformanceFilter", - "days": 10 + "minutes": 1440 // rolling 24h } ], ``` diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index f5f4e3a26..bc5ef961a 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -832,14 +832,14 @@ class Trade(_DECL_BASE, LocalTrade): return total_open_stake_amount or 0 @staticmethod - def get_overall_performance(days=None) -> List[Dict[str, Any]]: + def get_overall_performance(minutes=None) -> List[Dict[str, Any]]: """ Returns List of dicts containing all Trades, including profit and trade count NOTE: Not supported in Backtesting. """ filters = [Trade.is_open.is_(False)] - if days: - start_date = datetime.today() - timedelta(days) + if minutes: + start_date = datetime.now(timezone.utc) - timedelta(minutes=minutes) filters.append(Trade.close_date >= start_date) pair_rates = Trade.query.with_entities( Trade.pair, diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index ee443b0fe..301ee57ab 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -20,7 +20,7 @@ class PerformanceFilter(IPairList): pairlist_pos: int) -> None: super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) - self._days = pairlistconfig.get('days', 0) + self._minutes = pairlistconfig.get('minutes', 0) @property def needstickers(self) -> bool: @@ -47,7 +47,7 @@ class PerformanceFilter(IPairList): """ # Get the trading performance for pairs from database try: - performance = pd.DataFrame(Trade.get_overall_performance(self._days)) + performance = pd.DataFrame(Trade.get_overall_performance(self._minutes)) except AttributeError: # Performancefilter does not work in backtesting. self.log_once("PerformanceFilter is not available in this mode.", logger.warning) From 56fb25c5e521415c9880e47c4938bfe357550bec Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 09:10:37 +0200 Subject: [PATCH 23/64] Add test for PerformanceFilter lookback --- tests/plugins/test_pairlist.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 34770c03d..1ce8d172c 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -12,7 +12,8 @@ from freqtrade.persistence import Trade from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.resolvers import PairListResolver -from tests.conftest import get_patched_exchange, get_patched_freqtradebot, log_has, log_has_re +from tests.conftest import (create_mock_trades, get_patched_exchange, get_patched_freqtradebot, + log_has, log_has_re) @pytest.fixture(scope="function") @@ -663,6 +664,31 @@ def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None: assert log_has("PerformanceFilter is not available in this mode.", caplog) +@pytest.mark.usefixtures("init_persistence") +def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee) -> None: + whitelist_conf['exchange']['pair_whitelist'].append('XRP/BTC') + whitelist_conf['pairlists'] = [ + {"method": "StaticPairList"}, + {"method": "PerformanceFilter", "minutes": 60} + ] + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + exchange = get_patched_exchange(mocker, whitelist_conf) + pm = PairListManager(exchange, whitelist_conf) + pm.refresh_pairlist() + + assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC'] + + with time_machine.travel("2021-09-01 05:00:00 +00:00") as t: + create_mock_trades(fee) + pm.refresh_pairlist() + assert pm.whitelist == ['XRP/BTC', 'ETH/BTC', 'TKN/BTC'] + + # Move to "outside" of lookback window, so original sorting is restored. + t.move_to("2021-09-01 07:00:00 +00:00") + pm.refresh_pairlist() + assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC'] + + def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}] From d84ef34740a65df4922a13b8e8e523167637ae2e Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 17 Jul 2021 18:09:08 +0300 Subject: [PATCH 24/64] A helper to calculate stoploss value from absolute price. --- docs/strategy-advanced.md | 6 ++++ docs/strategy-customization.md | 41 +++++++++++++++++++++++++++ freqtrade/strategy/__init__.py | 3 +- freqtrade/strategy/strategy_helper.py | 11 +++++++ 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 4409af6ea..2b9517f3b 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -288,6 +288,12 @@ Stoploss values returned from `custom_stoploss()` always specify a percentage re The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`. +### Calculating stoploss percentage from absolute price + +Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss at specified absolute price level, we need to use `stop_rate` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. + +The helper function [`stoploss_from_absolute()`](strategy-customization.md#stoploss_from_absolute) can be used to convert from an absolute price, to a current price relative stop which can be returned from `custom_stoploss()`. + #### Stepped stoploss Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index cfea60d22..1f8116deb 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -639,6 +639,47 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation. +!!! Note + Providing invalid input to `stoploss_from_open()` may produce "CustomStoploss function did not return valid stoploss" warnings. + This may happen if `current_profit` parameter is below specified `open_relative_stop`. Such situations may arise when closing trade + is blocked by `confirm_trade_exit()` method. Warnings can be solved by never blocking stop loss sells by checking `sell_reason` in + `confirm_trade_exit()`, or by using `return stoploss_from_open(...) or 1` idiom, which will request to not change stop loss when + `current_profit < open_relative_stop`. + +### *stoploss_from_absolute()* + +In some situations it may be confusing to deal with stops relative to current rate. Instead, you may define a stoploss level using an absolute price. + +??? Example "Returning a stoploss using absolute price from the custom stoploss function" + + Say the open price was $100, and `current_price` is $121 (`current_profit` will be `0.21`). + + If we want a stop price at $107 price we can call `stoploss_from_absolute(107, current_rate)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100. + + ``` python + + from datetime import datetime + from freqtrade.persistence import Trade + from freqtrade.strategy import IStrategy, stoploss_from_open + + class AwesomeStrategy(IStrategy): + + # ... populate_* methods + + use_custom_stoploss = True + + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: + + # once the profit has risen above 10%, keep the stoploss at 7% above the open price + if current_profit > 0.10: + return stoploss_from_absolute(trade.open_rate * 1.07, current_rate) + + return 1 + + ``` + + Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation. ## Additional data (Wallets) diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index be655fc33..703cdabc1 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -4,4 +4,5 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timefr from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter, IntParameter, RealParameter) from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open +from freqtrade.strategy.strategy_helper import (merge_informative_pair, + stoploss_from_absolute, stoploss_from_open) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index e089ebf31..32f7a9886 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -83,3 +83,14 @@ def stoploss_from_open(open_relative_stop: float, current_profit: float) -> floa # negative stoploss values indicate the requested stop price is higher than the current price return max(stoploss, 0.0) + + +def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float: + """ + Given current price and desired stop price, return a stop loss value that is relative to current + price. + :param stop_rate: Stop loss price. + :param current_rate: Current asset price. + :return: Positive stop loss value relative to current price + """ + return 1 - (stop_rate / current_rate) From 1fdb656334208f57916fe4539ab83e9363d7b984 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 17 Jul 2021 19:19:49 +0300 Subject: [PATCH 25/64] Add a decorator which can be used to declare populate_indicators() functions for informative pairs. --- docs/strategy-customization.md | 84 +++++++++- freqtrade/edge/edge_positioning.py | 2 +- freqtrade/freqtradebot.py | 2 +- freqtrade/strategy/__init__.py | 2 +- freqtrade/strategy/interface.py | 53 ++++++- freqtrade/strategy/strategy_helper.py | 148 +++++++++++++++++- tests/rpc/test_rpc_apiserver.py | 1 + .../strats/informative_decorator_strategy.py | 75 +++++++++ tests/strategy/test_interface.py | 2 +- tests/strategy/test_strategy_helpers.py | 55 +++++++ tests/strategy/test_strategy_loading.py | 6 +- 11 files changed, 414 insertions(+), 16 deletions(-) create mode 100644 tests/strategy/strats/informative_decorator_strategy.py diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 1f8116deb..526c111c5 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -679,7 +679,89 @@ In some situations it may be confusing to deal with stops relative to current ra ``` - Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation. +### *@informative()* + +In most common case it is possible to easily define informative pairs by using a decorator. All decorated `populate_indicators_*` methods run in isolation, +not having access to data from other informative pairs, in the end all informative dataframes are merged and passed to main `populate_indicators()` method. +When hyperopting, please follow instructions of [optimizing an indicator parameter](hyperopt.md#optimizing-an-indicator-parameter). + +??? Example "Fast and easy way to define informative pairs" + + Most of the time we do not need power and flexibility offered by `merge_informative_pair()`, therefore we can use a decorator to quickly define informative pairs. + + ``` python + + from datetime import datetime + from freqtrade.persistence import Trade + from freqtrade.strategy import IStrategy, informative + + class AwesomeStrategy(IStrategy): + + # This method is not required. + # def informative_pairs(self): ... + + # Define informative upper timeframe for each pair. Decorators can be stacked on same + # method. Available in populate_indicators as 'rsi_30m' and 'rsi_1h'. + @informative('30m') + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + # Define BTC/STAKE informative pair. Available in populate_indicators and other methods as + # 'btc_rsi_1h'. Current stake currency should be specified as {stake} format variable + # instead of hardcoding actual stake currency. Available in populate_indicators and other + # methods as 'btc_rsi_1h'. + @informative('1h', 'BTC/{stake}') + def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + # Define BTC/ETH informative pair. You must specify quote currency if it is different from + # stake currency. Available in populate_indicators and other methods as 'eth_btc_rsi_1h'. + @informative('1h', 'ETH/BTC') + def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + # Define BTC/STAKE informative pair. A custom formatter may be specified for formatting + # column names. Format string supports these format variables: + # * {asset} - full name of the asset, for example 'BTC/USDT'. + # * {base} - base currency in lower case, for example 'eth'. + # * {BASE} - same as {base}, except in upper case. + # * {quote} - quote currency in lower case, for example 'usdt'. + # * {QUOTE} - same as {quote}, except in upper case. + # * {column} - name of dataframe column. + # * {timeframe} - timeframe of informative dataframe. + # A callable `fmt(**kwargs) -> str` may be specified, to implement custom formatting. + # Available in populate_indicators and other methods as 'rsi_upper'. + @informative('1h', 'BTC/{stake}', '{name}') + def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # Strategy timeframe indicators for current pair. + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + # Informative pairs are available in this method. + dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h'] + return dataframe + + ``` + + See docstring of `@informative()` decorator for more information. + +!!! Note + Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs + manually as described [in the DataProvider section](#complete-data-provider-sample). + +!!! Warning + Methods tagged with `@informative()` decorator must always have unique names! Re-using same name (for example when copy-pasting already defined informative method) + will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators + created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique! + +!!! Warning + When using a legacy hyperopt implementation informative pairs defined with a decorator will not be executed. Please update your strategy if necessary. ## Additional data (Wallets) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index f12b1b37d..1950f0d08 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -119,7 +119,7 @@ class Edge: ) # Download informative pairs too res = defaultdict(list) - for p, t in self.strategy.informative_pairs(): + for p, t in self.strategy.gather_informative_pairs(): res[t].append(p) for timeframe, inf_pairs in res.items(): timerange_startup = deepcopy(self._timerange) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7f668273c..bdc438c9a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -160,7 +160,7 @@ class FreqtradeBot(LoggingMixin): # Refreshing candles self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist), - self.strategy.informative_pairs()) + self.strategy.gather_informative_pairs()) strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)() diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 703cdabc1..a7de34916 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -4,5 +4,5 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timefr from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter, IntParameter, RealParameter) from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.strategy_helper import (merge_informative_pair, +from freqtrade.strategy.strategy_helper import (informative, merge_informative_pair, stoploss_from_absolute, stoploss_from_open) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 00ad3faf0..8e8b8b404 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -6,7 +6,7 @@ import logging import warnings from abc import ABC, abstractmethod from datetime import datetime, timedelta, timezone -from typing import Dict, List, Optional, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Tuple, Union import arrow from pandas import DataFrame @@ -19,6 +19,8 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.persistence import PairLocks, Trade from freqtrade.strategy.hyper import HyperStrategyMixin +from freqtrade.strategy.strategy_helper import (InformativeData, _create_and_merge_informative_pair, + _format_pair_name) from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -134,6 +136,39 @@ class IStrategy(ABC, HyperStrategyMixin): self._last_candle_seen_per_pair: Dict[str, datetime] = {} super().__init__(config) + # Gather informative pairs from @informative-decorated methods. + self._ft_informative: Dict[ + Tuple[str, str], Tuple[InformativeData, + Callable[[Any, DataFrame, dict], DataFrame]]] = {} + for attr_name in dir(self.__class__): + cls_method = getattr(self.__class__, attr_name) + if not callable(cls_method): + continue + ft_informative = getattr(cls_method, '_ft_informative', []) + if not isinstance(ft_informative, list): + # Type check is required because mocker would return a mock object that evaluates to + # True, confusing this code. + continue + for informative_data in ft_informative: + asset = informative_data.asset + timeframe = informative_data.timeframe + if asset: + pair = _format_pair_name(self.config, asset) + if (pair, timeframe) in self._ft_informative: + raise OperationalException(f'Informative pair {pair} {timeframe} can not ' + f'be defined more than once!') + self._ft_informative[(pair, timeframe)] = (informative_data, cls_method) + elif self.dp is not None: + for pair in self.dp.current_whitelist(): + if (pair, timeframe) in self._ft_informative: + raise OperationalException(f'Informative pair {pair} {timeframe} can ' + f'not be defined more than once!') + self._ft_informative[(pair, timeframe)] = (informative_data, cls_method) + + def _format_pair(self, pair: str) -> str: + return pair.format(stake_currency=self.config['stake_currency'], + stake=self.config['stake_currency']).upper() + @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ @@ -377,6 +412,14 @@ class IStrategy(ABC, HyperStrategyMixin): # END - Intended to be overridden by strategy ### + def gather_informative_pairs(self) -> ListPairsWithTimeframes: + """ + Internal method which gathers all informative pairs (user or automatically defined). + """ + informative_pairs = self.informative_pairs() + informative_pairs += list(self._ft_informative.keys()) + return list(set(informative_pairs)) + def get_strategy_name(self) -> str: """ Returns strategy class name @@ -793,6 +836,14 @@ class IStrategy(ABC, HyperStrategyMixin): :return: a Dataframe with all mandatory indicators for the strategies """ logger.debug(f"Populating indicators for pair {metadata.get('pair')}.") + + # call populate_indicators_Nm() which were tagged with @informative decorator. + for (pair, timeframe), (informative_data, populate_fn) in self._ft_informative.items(): + if not informative_data.asset and pair != metadata['pair']: + continue + dataframe = _create_and_merge_informative_pair( + self, dataframe, metadata, informative_data, populate_fn) + if self._populate_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " "the current function headers!", DeprecationWarning) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 32f7a9886..aa828d330 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -1,10 +1,24 @@ -import pandas as pd +from typing import Any, Callable, NamedTuple, Optional, Union +import pandas as pd +from mypy_extensions import KwArg +from pandas import DataFrame + +from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes +class InformativeData(NamedTuple): + asset: Optional[str] + timeframe: str + fmt: Union[str, Callable[[KwArg(str)], str], None] + ffill: bool + + def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, - timeframe: str, timeframe_inf: str, ffill: bool = True) -> pd.DataFrame: + timeframe: str, timeframe_inf: str, ffill: bool = True, + append_timeframe: bool = True, + date_column: str = 'date') -> pd.DataFrame: """ Correctly merge informative samples to the original dataframe, avoiding lookahead bias. @@ -24,6 +38,8 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, :param timeframe: Timeframe of the original pair sample. :param timeframe_inf: Timeframe of the informative pair sample. :param ffill: Forwardfill missing values - optional but usually required + :param append_timeframe: Rename columns by appending timeframe. + :param date_column: A custom date column name. :return: Merged dataframe :raise: ValueError if the secondary timeframe is shorter than the dataframe timeframe """ @@ -32,25 +48,29 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, minutes = timeframe_to_minutes(timeframe) if minutes == minutes_inf: # No need to forwardshift if the timeframes are identical - informative['date_merge'] = informative["date"] + informative['date_merge'] = informative[date_column] elif minutes < minutes_inf: # Subtract "small" timeframe so merging is not delayed by 1 small candle # Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073 informative['date_merge'] = ( - informative["date"] + pd.to_timedelta(minutes_inf, 'm') - pd.to_timedelta(minutes, 'm') + informative[date_column] + pd.to_timedelta(minutes_inf, 'm') - + pd.to_timedelta(minutes, 'm') ) else: raise ValueError("Tried to merge a faster timeframe to a slower timeframe." "This would create new rows, and can throw off your regular indicators.") # Rename columns to be unique - informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns] + date_merge = 'date_merge' + if append_timeframe: + date_merge = f'date_merge_{timeframe_inf}' + informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns] # Combine the 2 dataframes # all indicators on the informative sample MUST be calculated before this point dataframe = pd.merge(dataframe, informative, left_on='date', - right_on=f'date_merge_{timeframe_inf}', how='left') - dataframe = dataframe.drop(f'date_merge_{timeframe_inf}', axis=1) + right_on=date_merge, how='left') + dataframe = dataframe.drop(date_merge, axis=1) if ffill: dataframe = dataframe.ffill() @@ -94,3 +114,117 @@ def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float: :return: Positive stop loss value relative to current price """ return 1 - (stop_rate / current_rate) + + +def informative(timeframe: str, asset: str = '', + fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None, + ffill: bool = True) -> Callable[[Callable[[Any, DataFrame, dict], DataFrame]], + Callable[[Any, DataFrame, dict], DataFrame]]: + """ + A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to + define informative indicators. + + Example usage: + + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + :param timeframe: Informative timeframe. Must always be higher than strategy timeframe. + :param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use + current pair. + :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not + specified, defaults to {asset}_{name}_{timeframe} if asset is specified, or {name}_{timeframe} + otherwise. + * {asset}: name of informative asset, provided in lower-case, with / replaced with _. Stake + currency is not included in this string. + * {name}: user-specified dataframe column name. + * {timeframe}: informative timeframe. + :param ffill: ffill dataframe after mering informative pair. + """ + _asset = asset + _timeframe = timeframe + _fmt = fmt + _ffill = ffill + + def decorator(fn: Callable[[Any, DataFrame, dict], DataFrame]): + informative_pairs = getattr(fn, '_ft_informative', []) + informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill)) + setattr(fn, '_ft_informative', informative_pairs) + return fn + return decorator + + +def _format_pair_name(config, pair: str) -> str: + return pair.format(stake_currency=config['stake_currency'], + stake=config['stake_currency']).upper() + + +def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, + metadata: dict, informative_data: InformativeData, + populate_indicators: Callable[[Any, DataFrame, dict], + DataFrame]): + asset = informative_data.asset or '' + timeframe = informative_data.timeframe + fmt = informative_data.fmt + ffill = informative_data.ffill + config = strategy.config + dp = strategy.dp + + if asset: + # Insert stake currency if needed. + asset = _format_pair_name(config, asset) + else: + # Not specifying an asset will define informative dataframe for current pair. + asset = metadata['pair'] + + if '/' in asset: + base, quote = asset.split('/') + else: + # When futures are supported this may need reevaluation. + # base, quote = asset, None + raise OperationalException('Not implemented.') + + # Default format. This optimizes for the common case: informative pairs using same stake + # currency. When quote currency matches stake currency, column name will omit base currency. + # This allows easily reconfiguring strategy to use different base currency. In a rare case + # where it is desired to keep quote currency in column name at all times user should specify + # fmt='{base}_{quote}_{column}_{timeframe}' format or similar. + if not fmt: + fmt = '{column}_{timeframe}' # Informatives of current pair + if asset != metadata['pair']: + if quote == config['stake_currency']: + fmt = '{base}_' + fmt # Informatives of other pair + else: + fmt = '{base}_{quote}_' + fmt # Informatives of different quote currency + + inf_metadata = {'pair': asset, 'timeframe': timeframe} + inf_dataframe = dp.get_pair_dataframe(asset, timeframe) + inf_dataframe = populate_indicators(strategy, inf_dataframe, inf_metadata) + + formatter: Any = None + if callable(fmt): + formatter = fmt # A custom user-specified formatter function. + else: + formatter = fmt.format # A default string formatter. + + fmt_args = { + 'BASE': base.upper(), + 'QUOTE': quote.upper(), + 'base': base.lower(), + 'quote': quote.lower(), + 'asset': asset, + 'timeframe': timeframe, + } + inf_dataframe.rename(columns=lambda column: formatter(column=column, **fmt_args), + inplace=True) + + date_column = formatter(column='date', **fmt_args) + if date_column in dataframe.columns: + raise OperationalException(f'Duplicate column name {date_column} exists in ' + f'dataframe! Ensure column names are unique!') + dataframe = merge_informative_pair(dataframe, inf_dataframe, strategy.timeframe, timeframe, + ffill=ffill, append_timeframe=False, + date_column=date_column) + return dataframe diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 2852486ed..43eb70938 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1218,6 +1218,7 @@ def test_api_strategies(botclient): assert_response(rc) assert rc.json() == {'strategies': [ 'HyperoptableStrategy', + 'InformativeDecoratorTest', 'StrategyTestV2', 'TestStrategyLegacyV1' ]} diff --git a/tests/strategy/strats/informative_decorator_strategy.py b/tests/strategy/strats/informative_decorator_strategy.py new file mode 100644 index 000000000..a32ad79e8 --- /dev/null +++ b/tests/strategy/strats/informative_decorator_strategy.py @@ -0,0 +1,75 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement + +from pandas import DataFrame + +from freqtrade.strategy import informative, merge_informative_pair +from freqtrade.strategy.interface import IStrategy + + +class InformativeDecoratorTest(IStrategy): + """ + Strategy used by tests freqtrade bot. + Please do not modify this strategy, it's intended for internal use only. + Please look at the SampleStrategy in the user_data/strategy directory + or strategy repository https://github.com/freqtrade/freqtrade-strategies + for samples and inspiration. + """ + INTERFACE_VERSION = 2 + stoploss = -0.10 + timeframe = '5m' + startup_candle_count: int = 20 + + def informative_pairs(self): + return [('BTC/USDT', '5m')] + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['buy'] = 0 + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['sell'] = 0 + return dataframe + + # Decorator stacking test. + @informative('30m') + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + # Simple informative test. + @informative('1h', 'BTC/{stake}') + def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + # Quote currency different from stake currency test. + @informative('1h', 'ETH/BTC') + def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + # Formatting test. + @informative('30m', 'BTC/{stake}', '{column}_{BASE}_{QUOTE}_{base}_{quote}_{asset}_{timeframe}') + def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + # Custom formatter test + @informative('30m', 'ETH/{stake}', fmt=lambda column, **kwargs: column + '_from_callable') + def populate_indicators_eth_30m(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # Strategy timeframe indicators for current pair. + dataframe['rsi'] = 14 + # Informative pairs are available in this method. + dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h'] + + # Mixing manual informative pairs with decorators. + informative = self.dp.get_pair_dataframe('BTC/USDT', '5m') + informative['rsi'] = 14 + dataframe = merge_informative_pair(dataframe, informative, self.timeframe, '5m', ffill=True) + + return dataframe diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 250dcf63d..dcb9e3e64 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -607,7 +607,7 @@ def test_is_informative_pairs_callback(default_conf): strategy = StrategyResolver.load_strategy(default_conf) # Should return empty # Uses fallback to base implementation - assert [] == strategy.informative_pairs() + assert [] == strategy.gather_informative_pairs() @pytest.mark.parametrize('error', [ diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 3b84fc254..7784f3f77 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -4,6 +4,7 @@ import numpy as np import pandas as pd import pytest +from freqtrade.data.dataprovider import DataProvider from freqtrade.strategy import merge_informative_pair, stoploss_from_open, timeframe_to_minutes @@ -132,3 +133,57 @@ def test_stoploss_from_open(): assert stoploss == 0 else: assert isclose(stop_price, expected_stop_price, rel_tol=0.00001) + + +def test_informative_decorator(mocker, default_conf): + test_data_5m = generate_test_data('5m', 40) + test_data_30m = generate_test_data('30m', 40) + test_data_1h = generate_test_data('1h', 40) + data = { + ('XRP/USDT', '5m'): test_data_5m, + ('XRP/USDT', '30m'): test_data_30m, + ('XRP/USDT', '1h'): test_data_1h, + ('LTC/USDT', '5m'): test_data_5m, + ('LTC/USDT', '30m'): test_data_30m, + ('LTC/USDT', '1h'): test_data_1h, + ('BTC/USDT', '30m'): test_data_30m, + ('BTC/USDT', '5m'): test_data_5m, + ('BTC/USDT', '1h'): test_data_1h, + ('ETH/USDT', '1h'): test_data_1h, + ('ETH/USDT', '30m'): test_data_30m, + ('ETH/BTC', '1h'): test_data_1h, + } + from .strats.informative_decorator_strategy import InformativeDecoratorTest + default_conf['stake_currency'] = 'USDT' + InformativeDecoratorTest.dp = DataProvider({}, None, None) + mocker.patch.object(InformativeDecoratorTest.dp, 'current_whitelist', return_value=[ + 'XRP/USDT', 'LTC/USDT' + ]) + strategy = InformativeDecoratorTest(config=default_conf) + + assert len(strategy._ft_informative) == 8 + informative_pairs = [('XRP/USDT', '1h'), ('LTC/USDT', '1h'), ('XRP/USDT', '30m'), + ('LTC/USDT', '30m'), ('BTC/USDT', '1h'), ('BTC/USDT', '30m'), + ('BTC/USDT', '5m'), ('ETH/BTC', '1h'), ('ETH/USDT', '30m')] + for inf_pair in informative_pairs: + assert inf_pair in strategy.gather_informative_pairs() + + def test_historic_ohlcv(pair, timeframe): + return data[(pair, timeframe or strategy.timeframe)].copy() + mocker.patch('freqtrade.data.dataprovider.DataProvider.historic_ohlcv', + side_effect=test_historic_ohlcv) + + analyzed = strategy.advise_all_indicators( + {p: data[(p, strategy.timeframe)] for p in ('XRP/USDT', 'LTC/USDT')}) + expected_columns = [ + 'rsi_1h', 'rsi_30m', # Stacked informative decorators + 'btc_rsi_1h', # BTC 1h informative + 'rsi_BTC_USDT_btc_usdt_BTC/USDT_30m', # Column formatting + 'rsi_from_callable', # Custom column formatter + 'eth_btc_rsi_1h', # Quote currency not matching stake currency + 'rsi', 'rsi_less', # Non-informative columns + 'rsi_5m', # Manual informative dataframe + ] + for _, dataframe in analyzed.items(): + for col in expected_columns: + assert col in dataframe.columns diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 2cbc9d0c6..3a30a824a 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -35,7 +35,7 @@ def test_search_all_strategies_no_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=False) assert isinstance(strategies, list) - assert len(strategies) == 3 + assert len(strategies) == 4 assert isinstance(strategies[0], dict) @@ -43,10 +43,10 @@ def test_search_all_strategies_with_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=True) assert isinstance(strategies, list) - assert len(strategies) == 4 + assert len(strategies) == 5 # with enum_failed=True search_all_objects() shall find 2 good strategies # and 1 which fails to load - assert len([x for x in strategies if x['class'] is not None]) == 3 + assert len([x for x in strategies if x['class'] is not None]) == 4 assert len([x for x in strategies if x['class'] is None]) == 1 From f2a1d9d2fc8efe238e47f5404dd6c924511e683b Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 5 Sep 2021 09:54:05 +0300 Subject: [PATCH 26/64] [SQUASH] Address PR comments. --- docs/strategy-customization.md | 56 ++++++++++++++++++++------- freqtrade/strategy/interface.py | 18 ++++----- freqtrade/strategy/strategy_helper.py | 32 +++++++++------ 3 files changed, 72 insertions(+), 34 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 526c111c5..f2bf6cf7c 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -681,9 +681,47 @@ In some situations it may be confusing to deal with stops relative to current ra ### *@informative()* +``` python +def informative(timeframe: str, asset: str = '', + fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None, + ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]: + """ + A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to + define informative indicators. + + Example usage: + + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + :param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe. + :param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use + current pair. + :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not + specified, defaults to: + * {base}_{column}_{timeframe} if asset is specified and quote currency does match stake + curerncy. + * {base}_{quote}_{column}_{timeframe} if asset is specified and quote currency does not match + stake curerncy. + * {column}_{timeframe} if asset is not specified. + Format string supports these format variables: + * {asset} - full name of the asset, for example 'BTC/USDT'. + * {base} - base currency in lower case, for example 'eth'. + * {BASE} - same as {base}, except in upper case. + * {quote} - quote currency in lower case, for example 'usdt'. + * {QUOTE} - same as {quote}, except in upper case. + * {column} - name of dataframe column. + * {timeframe} - timeframe of informative dataframe. + :param ffill: ffill dataframe after merging informative pair. + """ +``` + In most common case it is possible to easily define informative pairs by using a decorator. All decorated `populate_indicators_*` methods run in isolation, not having access to data from other informative pairs, in the end all informative dataframes are merged and passed to main `populate_indicators()` method. -When hyperopting, please follow instructions of [optimizing an indicator parameter](hyperopt.md#optimizing-an-indicator-parameter). +When hyperopting, use of hyperoptable parameter `.value` attribute is not supported. Please use `.range` attribute. See [optimizing an indicator parameter](hyperopt.md#optimizing-an-indicator-parameter) +for more information. ??? Example "Fast and easy way to define informative pairs" @@ -725,17 +763,9 @@ When hyperopting, please follow instructions of [optimizing an indicator paramet return dataframe # Define BTC/STAKE informative pair. A custom formatter may be specified for formatting - # column names. Format string supports these format variables: - # * {asset} - full name of the asset, for example 'BTC/USDT'. - # * {base} - base currency in lower case, for example 'eth'. - # * {BASE} - same as {base}, except in upper case. - # * {quote} - quote currency in lower case, for example 'usdt'. - # * {QUOTE} - same as {quote}, except in upper case. - # * {column} - name of dataframe column. - # * {timeframe} - timeframe of informative dataframe. - # A callable `fmt(**kwargs) -> str` may be specified, to implement custom formatting. - # Available in populate_indicators and other methods as 'rsi_upper'. - @informative('1h', 'BTC/{stake}', '{name}') + # column names. A callable `fmt(**kwargs) -> str` may be specified, to implement custom + # formatting. Available in populate_indicators and other methods as 'rsi_upper'. + @informative('1h', 'BTC/{stake}', '{column}') def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14) return dataframe @@ -749,8 +779,6 @@ When hyperopting, please follow instructions of [optimizing an indicator paramet ``` - See docstring of `@informative()` decorator for more information. - !!! Note Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs manually as described [in the DataProvider section](#complete-data-provider-sample). diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 8e8b8b404..0546deb01 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -6,7 +6,7 @@ import logging import warnings from abc import ABC, abstractmethod from datetime import datetime, timedelta, timezone -from typing import Any, Callable, Dict, List, Optional, Tuple, Union +from typing import Dict, List, Optional, Tuple, Union import arrow from pandas import DataFrame @@ -19,7 +19,8 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.persistence import PairLocks, Trade from freqtrade.strategy.hyper import HyperStrategyMixin -from freqtrade.strategy.strategy_helper import (InformativeData, _create_and_merge_informative_pair, +from freqtrade.strategy.strategy_helper import (InformativeData, PopulateIndicators, + _create_and_merge_informative_pair, _format_pair_name) from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -138,20 +139,23 @@ class IStrategy(ABC, HyperStrategyMixin): # Gather informative pairs from @informative-decorated methods. self._ft_informative: Dict[ - Tuple[str, str], Tuple[InformativeData, - Callable[[Any, DataFrame, dict], DataFrame]]] = {} + Tuple[str, str], Tuple[InformativeData, PopulateIndicators]] = {} for attr_name in dir(self.__class__): cls_method = getattr(self.__class__, attr_name) if not callable(cls_method): continue - ft_informative = getattr(cls_method, '_ft_informative', []) + ft_informative = getattr(cls_method, '_ft_informative', None) if not isinstance(ft_informative, list): # Type check is required because mocker would return a mock object that evaluates to # True, confusing this code. continue + strategy_timeframe_minutes = timeframe_to_minutes(self.timeframe) for informative_data in ft_informative: asset = informative_data.asset timeframe = informative_data.timeframe + if timeframe_to_minutes(timeframe) < strategy_timeframe_minutes: + raise OperationalException('Informative timeframe must be equal or higher than ' + 'strategy timeframe!') if asset: pair = _format_pair_name(self.config, asset) if (pair, timeframe) in self._ft_informative: @@ -165,10 +169,6 @@ class IStrategy(ABC, HyperStrategyMixin): f'not be defined more than once!') self._ft_informative[(pair, timeframe)] = (informative_data, cls_method) - def _format_pair(self, pair: str) -> str: - return pair.format(stake_currency=self.config['stake_currency'], - stake=self.config['stake_currency']).upper() - @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index aa828d330..64d9bdea8 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -8,6 +8,9 @@ from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes +PopulateIndicators = Callable[[Any, DataFrame, dict], DataFrame] + + class InformativeData(NamedTuple): asset: Optional[str] timeframe: str @@ -118,8 +121,7 @@ def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float: def informative(timeframe: str, asset: str = '', fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None, - ffill: bool = True) -> Callable[[Callable[[Any, DataFrame, dict], DataFrame]], - Callable[[Any, DataFrame, dict], DataFrame]]: + ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]: """ A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to define informative indicators. @@ -131,24 +133,32 @@ def informative(timeframe: str, asset: str = '', dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) return dataframe - :param timeframe: Informative timeframe. Must always be higher than strategy timeframe. + :param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe. :param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use current pair. :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not - specified, defaults to {asset}_{name}_{timeframe} if asset is specified, or {name}_{timeframe} - otherwise. - * {asset}: name of informative asset, provided in lower-case, with / replaced with _. Stake - currency is not included in this string. - * {name}: user-specified dataframe column name. - * {timeframe}: informative timeframe. - :param ffill: ffill dataframe after mering informative pair. + specified, defaults to: + * {base}_{column}_{timeframe} if asset is specified and quote currency does match stake + curerncy. + * {base}_{quote}_{column}_{timeframe} if asset is specified and quote currency does not match + stake curerncy. + * {column}_{timeframe} if asset is not specified. + Format string supports these format variables: + * {asset} - full name of the asset, for example 'BTC/USDT'. + * {base} - base currency in lower case, for example 'eth'. + * {BASE} - same as {base}, except in upper case. + * {quote} - quote currency in lower case, for example 'usdt'. + * {QUOTE} - same as {quote}, except in upper case. + * {column} - name of dataframe column. + * {timeframe} - timeframe of informative dataframe. + :param ffill: ffill dataframe after merging informative pair. """ _asset = asset _timeframe = timeframe _fmt = fmt _ffill = ffill - def decorator(fn: Callable[[Any, DataFrame, dict], DataFrame]): + def decorator(fn: PopulateIndicators): informative_pairs = getattr(fn, '_ft_informative', []) informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill)) setattr(fn, '_ft_informative', informative_pairs) From dfa61b7ad2297a94b0b26309ca74c64a7c1d08a7 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Tue, 7 Sep 2021 15:40:53 +0300 Subject: [PATCH 27/64] [SQUASH] Fix informatives for each pair not being created because dataprovider was not available. Fix not being able to have informative dataframe of a pair in whitelist. --- docs/strategy-customization.md | 4 +-- freqtrade/freqtradebot.py | 10 ++++--- freqtrade/optimize/backtesting.py | 3 +- freqtrade/optimize/edge_cli.py | 3 ++ freqtrade/strategy/interface.py | 38 +++++++++++++++---------- freqtrade/strategy/strategy_helper.py | 13 ++++----- tests/strategy/test_strategy_helpers.py | 9 +++--- 7 files changed, 47 insertions(+), 33 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index f2bf6cf7c..a994d9acd 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -702,9 +702,9 @@ def informative(timeframe: str, asset: str = '', :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not specified, defaults to: * {base}_{column}_{timeframe} if asset is specified and quote currency does match stake - curerncy. + currency. * {base}_{quote}_{column}_{timeframe} if asset is specified and quote currency does not match - stake curerncy. + stake currency. * {column}_{timeframe} if asset is not specified. Format string supports these format variables: * {asset} - full name of the asset, for example 'BTC/USDT'. diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index bdc438c9a..b79916639 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -83,10 +83,12 @@ class FreqtradeBot(LoggingMixin): self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists) - # Attach Dataprovider to Strategy baseclass - IStrategy.dp = self.dataprovider - # Attach Wallets to Strategy baseclass - IStrategy.wallets = self.wallets + # Attach Dataprovider to strategy instance + self.strategy.dp = self.dataprovider + # Attach Wallets to strategy instance + self.strategy.wallets = self.wallets + # Late initialization (may depend on dp/wallets) + self.strategy._initialize() # Initializing Edge only if enabled self.edge = Edge(self.config, self.exchange, self.strategy) if \ diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3e06bfa1b..ef491ae5e 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -154,11 +154,12 @@ class Backtesting: self.strategy: IStrategy = strategy strategy.dp = self.dataprovider # Attach Wallets to Strategy baseclass - IStrategy.wallets = self.wallets + strategy.wallets = self.wallets # Set stoploss_on_exchange to false for backtesting, # since a "perfect" stoploss-sell is assumed anyway # And the regular "stoploss" function would not apply to that case self.strategy.order_types['stoploss_on_exchange'] = False + strategy._initialize() def _load_protections(self, strategy: IStrategy): if self.config.get('enable_protections', False): diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 417faa685..abb5ca635 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -8,6 +8,7 @@ from typing import Any, Dict from freqtrade import constants from freqtrade.configuration import TimeRange, validate_config_consistency +from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge from freqtrade.optimize.optimize_reports import generate_edge_table from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -33,6 +34,8 @@ class EdgeCli: self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) self.strategy = StrategyResolver.load_strategy(self.config) + self.strategy.dp = DataProvider(config, None) + self.strategy._initialize() validate_config_consistency(self.config) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 0546deb01..951979212 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -137,9 +137,13 @@ class IStrategy(ABC, HyperStrategyMixin): self._last_candle_seen_per_pair: Dict[str, datetime] = {} super().__init__(config) + def _initialize(self): + """ + Late initialization tasks, which may depend on availability of dataprovider/wallets/etc. + """ # Gather informative pairs from @informative-decorated methods. self._ft_informative: Dict[ - Tuple[str, str], Tuple[InformativeData, PopulateIndicators]] = {} + Tuple[str, str], List[Tuple[InformativeData, PopulateIndicators]]] = {} for attr_name in dir(self.__class__): cls_method = getattr(self.__class__, attr_name) if not callable(cls_method): @@ -158,16 +162,19 @@ class IStrategy(ABC, HyperStrategyMixin): 'strategy timeframe!') if asset: pair = _format_pair_name(self.config, asset) - if (pair, timeframe) in self._ft_informative: - raise OperationalException(f'Informative pair {pair} {timeframe} can not ' - f'be defined more than once!') - self._ft_informative[(pair, timeframe)] = (informative_data, cls_method) - elif self.dp is not None: + try: + self._ft_informative[(pair, timeframe)].append( + (informative_data, cls_method)) + except KeyError: + self._ft_informative[(pair, timeframe)] = [(informative_data, cls_method)] + else: for pair in self.dp.current_whitelist(): - if (pair, timeframe) in self._ft_informative: - raise OperationalException(f'Informative pair {pair} {timeframe} can ' - f'not be defined more than once!') - self._ft_informative[(pair, timeframe)] = (informative_data, cls_method) + try: + self._ft_informative[(pair, timeframe)].append( + (informative_data, cls_method)) + except KeyError: + self._ft_informative[(pair, timeframe)] = \ + [(informative_data, cls_method)] @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -838,11 +845,12 @@ class IStrategy(ABC, HyperStrategyMixin): logger.debug(f"Populating indicators for pair {metadata.get('pair')}.") # call populate_indicators_Nm() which were tagged with @informative decorator. - for (pair, timeframe), (informative_data, populate_fn) in self._ft_informative.items(): - if not informative_data.asset and pair != metadata['pair']: - continue - dataframe = _create_and_merge_informative_pair( - self, dataframe, metadata, informative_data, populate_fn) + for (pair, timeframe), informatives in self._ft_informative.items(): + for (informative_data, populate_fn) in informatives: + if not informative_data.asset and pair != metadata['pair']: + continue + dataframe = _create_and_merge_informative_pair( + self, dataframe, metadata, informative_data, populate_fn) if self._populate_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 64d9bdea8..a4023f953 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -139,9 +139,9 @@ def informative(timeframe: str, asset: str = '', :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not specified, defaults to: * {base}_{column}_{timeframe} if asset is specified and quote currency does match stake - curerncy. + currency. * {base}_{quote}_{column}_{timeframe} if asset is specified and quote currency does not match - stake curerncy. + stake currency. * {column}_{timeframe} if asset is not specified. Format string supports these format variables: * {asset} - full name of the asset, for example 'BTC/USDT'. @@ -203,11 +203,10 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, # fmt='{base}_{quote}_{column}_{timeframe}' format or similar. if not fmt: fmt = '{column}_{timeframe}' # Informatives of current pair - if asset != metadata['pair']: - if quote == config['stake_currency']: - fmt = '{base}_' + fmt # Informatives of other pair - else: - fmt = '{base}_{quote}_' + fmt # Informatives of different quote currency + if quote != config['stake_currency']: + fmt = '{quote}_' + fmt # Informatives of different quote currency + if informative_data.asset: + fmt = '{base}_' + fmt # Informatives of other pair inf_metadata = {'pair': asset, 'timeframe': timeframe} inf_dataframe = dp.get_pair_dataframe(asset, timeframe) diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 7784f3f77..0ee554ede 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -155,11 +155,12 @@ def test_informative_decorator(mocker, default_conf): } from .strats.informative_decorator_strategy import InformativeDecoratorTest default_conf['stake_currency'] = 'USDT' - InformativeDecoratorTest.dp = DataProvider({}, None, None) - mocker.patch.object(InformativeDecoratorTest.dp, 'current_whitelist', return_value=[ - 'XRP/USDT', 'LTC/USDT' - ]) strategy = InformativeDecoratorTest(config=default_conf) + strategy.dp = DataProvider({}, None, None) + mocker.patch.object(strategy.dp, 'current_whitelist', return_value=[ + 'XRP/USDT', 'LTC/USDT', 'BTC/USDT' + ]) + strategy._initialize() assert len(strategy._ft_informative) == 8 informative_pairs = [('XRP/USDT', '1h'), ('LTC/USDT', '1h'), ('XRP/USDT', '30m'), From f81df19b934b738bf148f51fdb5a9c7ab1dfb34f Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Tue, 7 Sep 2021 15:53:12 +0300 Subject: [PATCH 28/64] [TMP] Make tests not fail for now. --- freqtrade/strategy/interface.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 951979212..6e312e15d 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -424,7 +424,8 @@ class IStrategy(ABC, HyperStrategyMixin): Internal method which gathers all informative pairs (user or automatically defined). """ informative_pairs = self.informative_pairs() - informative_pairs += list(self._ft_informative.keys()) + if hasattr(self, '_ft_informative'): + informative_pairs += list(self._ft_informative.keys()) return list(set(informative_pairs)) def get_strategy_name(self) -> str: @@ -844,13 +845,14 @@ class IStrategy(ABC, HyperStrategyMixin): """ logger.debug(f"Populating indicators for pair {metadata.get('pair')}.") - # call populate_indicators_Nm() which were tagged with @informative decorator. - for (pair, timeframe), informatives in self._ft_informative.items(): - for (informative_data, populate_fn) in informatives: - if not informative_data.asset and pair != metadata['pair']: - continue - dataframe = _create_and_merge_informative_pair( - self, dataframe, metadata, informative_data, populate_fn) + if hasattr(self, '_ft_informative'): + # call populate_indicators_Nm() which were tagged with @informative decorator. + for (pair, timeframe), informatives in self._ft_informative.items(): + for (informative_data, populate_fn) in informatives: + if not informative_data.asset and pair != metadata['pair']: + continue + dataframe = _create_and_merge_informative_pair( + self, dataframe, metadata, informative_data, populate_fn) if self._populate_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " From 5dc78a0c66f385edd14db16a806e1f75bd453e83 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Fri, 10 Sep 2021 09:36:52 +0300 Subject: [PATCH 29/64] [SQUASH] Get rid of _initialize() and fix informatives for dynamic pairlists. --- freqtrade/freqtradebot.py | 2 - freqtrade/optimize/backtesting.py | 1 - freqtrade/optimize/edge_cli.py | 1 - freqtrade/strategy/interface.py | 56 ++++++++----------------- freqtrade/strategy/strategy_helper.py | 17 ++++---- tests/strategy/test_strategy_helpers.py | 3 +- 6 files changed, 27 insertions(+), 53 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b79916639..1cb8988ff 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -87,8 +87,6 @@ class FreqtradeBot(LoggingMixin): self.strategy.dp = self.dataprovider # Attach Wallets to strategy instance self.strategy.wallets = self.wallets - # Late initialization (may depend on dp/wallets) - self.strategy._initialize() # Initializing Edge only if enabled self.edge = Edge(self.config, self.exchange, self.strategy) if \ diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ef491ae5e..79c861ee8 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -159,7 +159,6 @@ class Backtesting: # since a "perfect" stoploss-sell is assumed anyway # And the regular "stoploss" function would not apply to that case self.strategy.order_types['stoploss_on_exchange'] = False - strategy._initialize() def _load_protections(self, strategy: IStrategy): if self.config.get('enable_protections', False): diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index abb5ca635..f211da750 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -35,7 +35,6 @@ class EdgeCli: self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) self.strategy = StrategyResolver.load_strategy(self.config) self.strategy.dp = DataProvider(config, None) - self.strategy._initialize() validate_config_consistency(self.config) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 6e312e15d..00c56f5df 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -121,7 +121,7 @@ class IStrategy(ABC, HyperStrategyMixin): # Class level variables (intentional) containing # the dataprovider (dp) (access to other candles, historic data, ...) # and wallets - access to the current balance. - dp: Optional[DataProvider] = None + dp: DataProvider wallets: Optional[Wallets] = None # Filled from configuration stake_currency: str @@ -137,44 +137,23 @@ class IStrategy(ABC, HyperStrategyMixin): self._last_candle_seen_per_pair: Dict[str, datetime] = {} super().__init__(config) - def _initialize(self): - """ - Late initialization tasks, which may depend on availability of dataprovider/wallets/etc. - """ # Gather informative pairs from @informative-decorated methods. - self._ft_informative: Dict[ - Tuple[str, str], List[Tuple[InformativeData, PopulateIndicators]]] = {} + self._ft_informative: List[Tuple[InformativeData, PopulateIndicators]] = [] for attr_name in dir(self.__class__): cls_method = getattr(self.__class__, attr_name) if not callable(cls_method): continue - ft_informative = getattr(cls_method, '_ft_informative', None) - if not isinstance(ft_informative, list): + informative_data_list = getattr(cls_method, '_ft_informative', None) + if not isinstance(informative_data_list, list): # Type check is required because mocker would return a mock object that evaluates to # True, confusing this code. continue strategy_timeframe_minutes = timeframe_to_minutes(self.timeframe) - for informative_data in ft_informative: - asset = informative_data.asset - timeframe = informative_data.timeframe - if timeframe_to_minutes(timeframe) < strategy_timeframe_minutes: + for informative_data in informative_data_list: + if timeframe_to_minutes(informative_data.timeframe) < strategy_timeframe_minutes: raise OperationalException('Informative timeframe must be equal or higher than ' 'strategy timeframe!') - if asset: - pair = _format_pair_name(self.config, asset) - try: - self._ft_informative[(pair, timeframe)].append( - (informative_data, cls_method)) - except KeyError: - self._ft_informative[(pair, timeframe)] = [(informative_data, cls_method)] - else: - for pair in self.dp.current_whitelist(): - try: - self._ft_informative[(pair, timeframe)].append( - (informative_data, cls_method)) - except KeyError: - self._ft_informative[(pair, timeframe)] = \ - [(informative_data, cls_method)] + self._ft_informative.append((informative_data, cls_method)) @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -424,8 +403,13 @@ class IStrategy(ABC, HyperStrategyMixin): Internal method which gathers all informative pairs (user or automatically defined). """ informative_pairs = self.informative_pairs() - if hasattr(self, '_ft_informative'): - informative_pairs += list(self._ft_informative.keys()) + for inf_data, _ in self._ft_informative: + if inf_data.asset: + pair_tf = (_format_pair_name(self.config, inf_data.asset), inf_data.timeframe) + informative_pairs.append(pair_tf) + else: + for pair in self.dp.current_whitelist(): + informative_pairs.append((pair, inf_data.timeframe)) return list(set(informative_pairs)) def get_strategy_name(self) -> str: @@ -845,14 +829,10 @@ class IStrategy(ABC, HyperStrategyMixin): """ logger.debug(f"Populating indicators for pair {metadata.get('pair')}.") - if hasattr(self, '_ft_informative'): - # call populate_indicators_Nm() which were tagged with @informative decorator. - for (pair, timeframe), informatives in self._ft_informative.items(): - for (informative_data, populate_fn) in informatives: - if not informative_data.asset and pair != metadata['pair']: - continue - dataframe = _create_and_merge_informative_pair( - self, dataframe, metadata, informative_data, populate_fn) + # call populate_indicators_Nm() which were tagged with @informative decorator. + for inf_data, populate_fn in self._ft_informative: + dataframe = _create_and_merge_informative_pair( + self, dataframe, metadata, inf_data, populate_fn) if self._populate_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index a4023f953..15c6d8a69 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -171,14 +171,13 @@ def _format_pair_name(config, pair: str) -> str: stake=config['stake_currency']).upper() -def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, - metadata: dict, informative_data: InformativeData, - populate_indicators: Callable[[Any, DataFrame, dict], - DataFrame]): - asset = informative_data.asset or '' - timeframe = informative_data.timeframe - fmt = informative_data.fmt - ffill = informative_data.ffill +def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: dict, + inf_data: InformativeData, + populate_indicators: PopulateIndicators): + asset = inf_data.asset or '' + timeframe = inf_data.timeframe + fmt = inf_data.fmt + ffill = inf_data.ffill config = strategy.config dp = strategy.dp @@ -205,7 +204,7 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, fmt = '{column}_{timeframe}' # Informatives of current pair if quote != config['stake_currency']: fmt = '{quote}_' + fmt # Informatives of different quote currency - if informative_data.asset: + if inf_data.asset: fmt = '{base}_' + fmt # Informatives of other pair inf_metadata = {'pair': asset, 'timeframe': timeframe} diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 0ee554ede..95ca0416f 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -160,9 +160,8 @@ def test_informative_decorator(mocker, default_conf): mocker.patch.object(strategy.dp, 'current_whitelist', return_value=[ 'XRP/USDT', 'LTC/USDT', 'BTC/USDT' ]) - strategy._initialize() - assert len(strategy._ft_informative) == 8 + assert len(strategy._ft_informative) == 6 # Equal to number of decorators used informative_pairs = [('XRP/USDT', '1h'), ('LTC/USDT', '1h'), ('XRP/USDT', '30m'), ('LTC/USDT', '30m'), ('BTC/USDT', '1h'), ('BTC/USDT', '30m'), ('BTC/USDT', '5m'), ('ETH/BTC', '1h'), ('ETH/USDT', '30m')] From bb6ae682fc7d82175196741e31723c5f797ffd2f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Sep 2021 09:59:10 +0200 Subject: [PATCH 30/64] Small simplifications --- docs/strategy-customization.md | 2 +- freqtrade/strategy/strategy_helper.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index a994d9acd..800dd9326 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -783,7 +783,7 @@ for more information. Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs manually as described [in the DataProvider section](#complete-data-provider-sample). -!!! Warning +!!! Warning "Duplicate method names" Methods tagged with `@informative()` decorator must always have unique names! Re-using same name (for example when copy-pasting already defined informative method) will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique! diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 15c6d8a69..746d656df 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -177,9 +177,7 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: asset = inf_data.asset or '' timeframe = inf_data.timeframe fmt = inf_data.fmt - ffill = inf_data.ffill config = strategy.config - dp = strategy.dp if asset: # Insert stake currency if needed. @@ -208,7 +206,7 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: fmt = '{base}_' + fmt # Informatives of other pair inf_metadata = {'pair': asset, 'timeframe': timeframe} - inf_dataframe = dp.get_pair_dataframe(asset, timeframe) + inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe) inf_dataframe = populate_indicators(strategy, inf_dataframe, inf_metadata) formatter: Any = None @@ -233,6 +231,6 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: raise OperationalException(f'Duplicate column name {date_column} exists in ' f'dataframe! Ensure column names are unique!') dataframe = merge_informative_pair(dataframe, inf_dataframe, strategy.timeframe, timeframe, - ffill=ffill, append_timeframe=False, + ffill=inf_data.ffill, append_timeframe=False, date_column=date_column) return dataframe From e88c4701bb2c321ed7cd5d6fed7fa6db6f4f41f6 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 12 Sep 2021 18:26:41 +0300 Subject: [PATCH 31/64] [SQUASH] Address PR comments. --- docs/strategy-customization.md | 19 ++- freqtrade/strategy/__init__.py | 5 +- freqtrade/strategy/informative_decorator.py | 134 ++++++++++++++++++++ freqtrade/strategy/interface.py | 11 +- freqtrade/strategy/strategy_helper.py | 132 ------------------- 5 files changed, 152 insertions(+), 149 deletions(-) create mode 100644 freqtrade/strategy/informative_decorator.py diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 800dd9326..671768bfa 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -652,9 +652,7 @@ In some situations it may be confusing to deal with stops relative to current ra ??? Example "Returning a stoploss using absolute price from the custom stoploss function" - Say the open price was $100, and `current_price` is $121 (`current_profit` will be `0.21`). - - If we want a stop price at $107 price we can call `stoploss_from_absolute(107, current_rate)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100. + If we want to trail a stop price at 2xATR below current proce we can call `stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate)`. ``` python @@ -664,18 +662,17 @@ In some situations it may be confusing to deal with stops relative to current ra class AwesomeStrategy(IStrategy): - # ... populate_* methods - use_custom_stoploss = True + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['atr'] = ta.ATR(dataframe, timeperiod=14) + return dataframe + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: - - # once the profit has risen above 10%, keep the stoploss at 7% above the open price - if current_profit > 0.10: - return stoploss_from_absolute(trade.open_rate * 1.07, current_rate) - - return 1 + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + candle = dataframe.iloc[-1].squeeze() + return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate) ``` diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index a7de34916..2ea0ad2b4 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -3,6 +3,7 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timefr timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter, IntParameter, RealParameter) +from freqtrade.strategy.informative_decorator import informative from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.strategy_helper import (informative, merge_informative_pair, - stoploss_from_absolute, stoploss_from_open) +from freqtrade.strategy.strategy_helper import (merge_informative_pair, stoploss_from_absolute, + stoploss_from_open) diff --git a/freqtrade/strategy/informative_decorator.py b/freqtrade/strategy/informative_decorator.py new file mode 100644 index 000000000..f09e634b0 --- /dev/null +++ b/freqtrade/strategy/informative_decorator.py @@ -0,0 +1,134 @@ +from typing import Any, Callable, NamedTuple, Optional, Union + +from mypy_extensions import KwArg +from pandas import DataFrame + +from freqtrade.exceptions import OperationalException +from freqtrade.strategy.strategy_helper import merge_informative_pair + + +PopulateIndicators = Callable[[Any, DataFrame, dict], DataFrame] + + +class InformativeData(NamedTuple): + asset: Optional[str] + timeframe: str + fmt: Union[str, Callable[[KwArg(str)], str], None] + ffill: bool + + +def informative(timeframe: str, asset: str = '', + fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None, + ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]: + """ + A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to + define informative indicators. + + Example usage: + + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + :param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe. + :param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use + current pair. + :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not + specified, defaults to: + * {base}_{column}_{timeframe} if asset is specified and quote currency does match stake + currency. + * {base}_{quote}_{column}_{timeframe} if asset is specified and quote currency does not match + stake currency. + * {column}_{timeframe} if asset is not specified. + Format string supports these format variables: + * {asset} - full name of the asset, for example 'BTC/USDT'. + * {base} - base currency in lower case, for example 'eth'. + * {BASE} - same as {base}, except in upper case. + * {quote} - quote currency in lower case, for example 'usdt'. + * {QUOTE} - same as {quote}, except in upper case. + * {column} - name of dataframe column. + * {timeframe} - timeframe of informative dataframe. + :param ffill: ffill dataframe after merging informative pair. + """ + _asset = asset + _timeframe = timeframe + _fmt = fmt + _ffill = ffill + + def decorator(fn: PopulateIndicators): + informative_pairs = getattr(fn, '_ft_informative', []) + informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill)) + setattr(fn, '_ft_informative', informative_pairs) + return fn + return decorator + + +def _format_pair_name(config, pair: str) -> str: + return pair.format(stake_currency=config['stake_currency'], + stake=config['stake_currency']).upper() + + +def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: dict, + inf_data: InformativeData, + populate_indicators: PopulateIndicators): + asset = inf_data.asset or '' + timeframe = inf_data.timeframe + fmt = inf_data.fmt + config = strategy.config + + if asset: + # Insert stake currency if needed. + asset = _format_pair_name(config, asset) + else: + # Not specifying an asset will define informative dataframe for current pair. + asset = metadata['pair'] + + if '/' in asset: + base, quote = asset.split('/') + else: + # When futures are supported this may need reevaluation. + # base, quote = asset, None + raise OperationalException('Not implemented.') + + # Default format. This optimizes for the common case: informative pairs using same stake + # currency. When quote currency matches stake currency, column name will omit base currency. + # This allows easily reconfiguring strategy to use different base currency. In a rare case + # where it is desired to keep quote currency in column name at all times user should specify + # fmt='{base}_{quote}_{column}_{timeframe}' format or similar. + if not fmt: + fmt = '{column}_{timeframe}' # Informatives of current pair + if quote != config['stake_currency']: + fmt = '{quote}_' + fmt # Informatives of different quote currency + if inf_data.asset: + fmt = '{base}_' + fmt # Informatives of other pair + + inf_metadata = {'pair': asset, 'timeframe': timeframe} + inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe) + inf_dataframe = populate_indicators(strategy, inf_dataframe, inf_metadata) + + formatter: Any = None + if callable(fmt): + formatter = fmt # A custom user-specified formatter function. + else: + formatter = fmt.format # A default string formatter. + + fmt_args = { + 'BASE': base.upper(), + 'QUOTE': quote.upper(), + 'base': base.lower(), + 'quote': quote.lower(), + 'asset': asset, + 'timeframe': timeframe, + } + inf_dataframe.rename(columns=lambda column: formatter(column=column, **fmt_args), + inplace=True) + + date_column = formatter(column='date', **fmt_args) + if date_column in dataframe.columns: + raise OperationalException(f'Duplicate column name {date_column} exists in ' + f'dataframe! Ensure column names are unique!') + dataframe = merge_informative_pair(dataframe, inf_dataframe, strategy.timeframe, timeframe, + ffill=inf_data.ffill, append_timeframe=False, + date_column=date_column) + return dataframe diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 00c56f5df..7420bd9fd 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -19,9 +19,9 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.persistence import PairLocks, Trade from freqtrade.strategy.hyper import HyperStrategyMixin -from freqtrade.strategy.strategy_helper import (InformativeData, PopulateIndicators, - _create_and_merge_informative_pair, - _format_pair_name) +from freqtrade.strategy.informative_decorator import (InformativeData, PopulateIndicators, + _create_and_merge_informative_pair, + _format_pair_name) from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -121,7 +121,7 @@ class IStrategy(ABC, HyperStrategyMixin): # Class level variables (intentional) containing # the dataprovider (dp) (access to other candles, historic data, ...) # and wallets - access to the current balance. - dp: DataProvider + dp: Optional[DataProvider] wallets: Optional[Wallets] = None # Filled from configuration stake_currency: str @@ -408,6 +408,9 @@ class IStrategy(ABC, HyperStrategyMixin): pair_tf = (_format_pair_name(self.config, inf_data.asset), inf_data.timeframe) informative_pairs.append(pair_tf) else: + if not self.dp: + raise OperationalException('@informative decorator with unspecified asset ' + 'requires DataProvider instance.') for pair in self.dp.current_whitelist(): informative_pairs.append((pair, inf_data.timeframe)) return list(set(informative_pairs)) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 746d656df..f813b39c5 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -1,23 +1,8 @@ -from typing import Any, Callable, NamedTuple, Optional, Union - import pandas as pd -from mypy_extensions import KwArg -from pandas import DataFrame -from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes -PopulateIndicators = Callable[[Any, DataFrame, dict], DataFrame] - - -class InformativeData(NamedTuple): - asset: Optional[str] - timeframe: str - fmt: Union[str, Callable[[KwArg(str)], str], None] - ffill: bool - - def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, timeframe: str, timeframe_inf: str, ffill: bool = True, append_timeframe: bool = True, @@ -117,120 +102,3 @@ def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float: :return: Positive stop loss value relative to current price """ return 1 - (stop_rate / current_rate) - - -def informative(timeframe: str, asset: str = '', - fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None, - ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]: - """ - A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to - define informative indicators. - - Example usage: - - @informative('1h') - def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) - return dataframe - - :param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe. - :param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use - current pair. - :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not - specified, defaults to: - * {base}_{column}_{timeframe} if asset is specified and quote currency does match stake - currency. - * {base}_{quote}_{column}_{timeframe} if asset is specified and quote currency does not match - stake currency. - * {column}_{timeframe} if asset is not specified. - Format string supports these format variables: - * {asset} - full name of the asset, for example 'BTC/USDT'. - * {base} - base currency in lower case, for example 'eth'. - * {BASE} - same as {base}, except in upper case. - * {quote} - quote currency in lower case, for example 'usdt'. - * {QUOTE} - same as {quote}, except in upper case. - * {column} - name of dataframe column. - * {timeframe} - timeframe of informative dataframe. - :param ffill: ffill dataframe after merging informative pair. - """ - _asset = asset - _timeframe = timeframe - _fmt = fmt - _ffill = ffill - - def decorator(fn: PopulateIndicators): - informative_pairs = getattr(fn, '_ft_informative', []) - informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill)) - setattr(fn, '_ft_informative', informative_pairs) - return fn - return decorator - - -def _format_pair_name(config, pair: str) -> str: - return pair.format(stake_currency=config['stake_currency'], - stake=config['stake_currency']).upper() - - -def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: dict, - inf_data: InformativeData, - populate_indicators: PopulateIndicators): - asset = inf_data.asset or '' - timeframe = inf_data.timeframe - fmt = inf_data.fmt - config = strategy.config - - if asset: - # Insert stake currency if needed. - asset = _format_pair_name(config, asset) - else: - # Not specifying an asset will define informative dataframe for current pair. - asset = metadata['pair'] - - if '/' in asset: - base, quote = asset.split('/') - else: - # When futures are supported this may need reevaluation. - # base, quote = asset, None - raise OperationalException('Not implemented.') - - # Default format. This optimizes for the common case: informative pairs using same stake - # currency. When quote currency matches stake currency, column name will omit base currency. - # This allows easily reconfiguring strategy to use different base currency. In a rare case - # where it is desired to keep quote currency in column name at all times user should specify - # fmt='{base}_{quote}_{column}_{timeframe}' format or similar. - if not fmt: - fmt = '{column}_{timeframe}' # Informatives of current pair - if quote != config['stake_currency']: - fmt = '{quote}_' + fmt # Informatives of different quote currency - if inf_data.asset: - fmt = '{base}_' + fmt # Informatives of other pair - - inf_metadata = {'pair': asset, 'timeframe': timeframe} - inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe) - inf_dataframe = populate_indicators(strategy, inf_dataframe, inf_metadata) - - formatter: Any = None - if callable(fmt): - formatter = fmt # A custom user-specified formatter function. - else: - formatter = fmt.format # A default string formatter. - - fmt_args = { - 'BASE': base.upper(), - 'QUOTE': quote.upper(), - 'base': base.lower(), - 'quote': quote.lower(), - 'asset': asset, - 'timeframe': timeframe, - } - inf_dataframe.rename(columns=lambda column: formatter(column=column, **fmt_args), - inplace=True) - - date_column = formatter(column='date', **fmt_args) - if date_column in dataframe.columns: - raise OperationalException(f'Duplicate column name {date_column} exists in ' - f'dataframe! Ensure column names are unique!') - dataframe = merge_informative_pair(dataframe, inf_dataframe, strategy.timeframe, timeframe, - ffill=inf_data.ffill, append_timeframe=False, - date_column=date_column) - return dataframe From 7e6aa9390ae9c970f8b22afb591161492f833807 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 18 Sep 2021 10:17:50 +0300 Subject: [PATCH 32/64] [SQUASH] Unconditionally include quote currency when asset is explicitly specified. Added docs suggesting to use string formatting to make strategy independent of configured stake currency. --- docs/strategy-customization.md | 26 +++++++++++++++++---- freqtrade/strategy/informative_decorator.py | 11 +++------ tests/strategy/test_strategy_helpers.py | 2 +- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 671768bfa..2b22dd274 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -698,10 +698,7 @@ def informative(timeframe: str, asset: str = '', current pair. :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not specified, defaults to: - * {base}_{column}_{timeframe} if asset is specified and quote currency does match stake - currency. - * {base}_{quote}_{column}_{timeframe} if asset is specified and quote currency does not match - stake currency. + * {base}_{quote}_{column}_{timeframe} if asset is specified. * {column}_{timeframe} if asset is not specified. Format string supports these format variables: * {asset} - full name of the asset, for example 'BTC/USDT'. @@ -746,7 +743,7 @@ for more information. # Define BTC/STAKE informative pair. Available in populate_indicators and other methods as # 'btc_rsi_1h'. Current stake currency should be specified as {stake} format variable # instead of hardcoding actual stake currency. Available in populate_indicators and other - # methods as 'btc_rsi_1h'. + # methods as 'btc_usdt_rsi_1h' (when stake currency is USDT). @informative('1h', 'BTC/{stake}') def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) @@ -780,6 +777,25 @@ for more information. Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs manually as described [in the DataProvider section](#complete-data-provider-sample). +!!! Note + Use string formatting when accessing informative dataframes of other pairs. This will allow easily changing stake currency in config without having to adjust strategy code. + + ``` python + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + stake = self.config['stake_currency'] + dataframe.loc[ + ( + (dataframe[f'btc_{stake}_rsi_1h'] < 35) + & + (dataframe['volume'] > 0) + ), + ['buy', 'buy_tag']] = (1, 'buy_signal_rsi') + + return dataframe + ``` + + Alternatively column renaming may be used to remove stake currency from column names: `@informative('1h', 'BTC/{stake}', fmt='{base}_{column}_{timeframe}')`. + !!! Warning "Duplicate method names" Methods tagged with `@informative()` decorator must always have unique names! Re-using same name (for example when copy-pasting already defined informative method) will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators diff --git a/freqtrade/strategy/informative_decorator.py b/freqtrade/strategy/informative_decorator.py index f09e634b0..c8ebf5989 100644 --- a/freqtrade/strategy/informative_decorator.py +++ b/freqtrade/strategy/informative_decorator.py @@ -36,10 +36,7 @@ def informative(timeframe: str, asset: str = '', current pair. :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not specified, defaults to: - * {base}_{column}_{timeframe} if asset is specified and quote currency does match stake - currency. - * {base}_{quote}_{column}_{timeframe} if asset is specified and quote currency does not match - stake currency. + * {base}_{quote}_{column}_{timeframe} if asset is specified. * {column}_{timeframe} if asset is not specified. Format string supports these format variables: * {asset} - full name of the asset, for example 'BTC/USDT'. @@ -88,7 +85,7 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: base, quote = asset.split('/') else: # When futures are supported this may need reevaluation. - # base, quote = asset, None + # base, quote = asset, '' raise OperationalException('Not implemented.') # Default format. This optimizes for the common case: informative pairs using same stake @@ -98,10 +95,8 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: # fmt='{base}_{quote}_{column}_{timeframe}' format or similar. if not fmt: fmt = '{column}_{timeframe}' # Informatives of current pair - if quote != config['stake_currency']: - fmt = '{quote}_' + fmt # Informatives of different quote currency if inf_data.asset: - fmt = '{base}_' + fmt # Informatives of other pair + fmt = '{base}_{quote}_' + fmt # Informatives of other pairs inf_metadata = {'pair': asset, 'timeframe': timeframe} inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe) diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 95ca0416f..d4206ba8c 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -177,7 +177,7 @@ def test_informative_decorator(mocker, default_conf): {p: data[(p, strategy.timeframe)] for p in ('XRP/USDT', 'LTC/USDT')}) expected_columns = [ 'rsi_1h', 'rsi_30m', # Stacked informative decorators - 'btc_rsi_1h', # BTC 1h informative + 'btc_usdt_rsi_1h', # BTC 1h informative 'rsi_BTC_USDT_btc_usdt_BTC/USDT_30m', # Column formatting 'rsi_from_callable', # Custom column formatter 'eth_btc_rsi_1h', # Quote currency not matching stake currency From e4ca42faec580a3c336298d42402fec0d8e56f93 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 18 Sep 2021 10:18:33 +0300 Subject: [PATCH 33/64] [SQUASH] Update stoploss_from_absolute to behave more like stoploss_from_open and add a test for it. --- freqtrade/strategy/strategy_helper.py | 16 +++++++++++++++- tests/strategy/test_strategy_helpers.py | 11 ++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index f813b39c5..175bcaccb 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -97,8 +97,22 @@ def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float: """ Given current price and desired stop price, return a stop loss value that is relative to current price. + + The requested stop can be positive for a stop above the open price, or negative for + a stop below the open price. The return value is always >= 0. + + Returns 0 if the resulting stop price would be above the current price. + :param stop_rate: Stop loss price. :param current_rate: Current asset price. :return: Positive stop loss value relative to current price """ - return 1 - (stop_rate / current_rate) + + # formula is undefined for current_rate 0, return maximum value + if current_rate == 0: + return 1 + + stoploss = 1 - (stop_rate / current_rate) + + # negative stoploss values indicate the requested stop price is higher than the current price + return max(stoploss, 0.0) diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index d4206ba8c..9132382fa 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -5,7 +5,8 @@ import pandas as pd import pytest from freqtrade.data.dataprovider import DataProvider -from freqtrade.strategy import merge_informative_pair, stoploss_from_open, timeframe_to_minutes +from freqtrade.strategy import (merge_informative_pair, stoploss_from_open, timeframe_to_minutes, + stoploss_from_absolute) def generate_test_data(timeframe: str, size: int): @@ -135,6 +136,14 @@ def test_stoploss_from_open(): assert isclose(stop_price, expected_stop_price, rel_tol=0.00001) +def test_stoploss_from_absolute(): + assert stoploss_from_absolute(90, 100) == 1 - (90 / 100) + assert stoploss_from_absolute(100, 100) == 0 + assert stoploss_from_absolute(110, 100) == 0 + assert stoploss_from_absolute(100, 0) == 1 + assert stoploss_from_absolute(0, 100) == 1 + + def test_informative_decorator(mocker, default_conf): test_data_5m = generate_test_data('5m', 40) test_data_30m = generate_test_data('30m', 40) From 216f75bbb91b8d72790f386dfc8b05340ade3c3d Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 18 Sep 2021 02:53:34 -0600 Subject: [PATCH 34/64] parametrized test_sell_profit_only , test__safe_exit_amount, test_order_book_bid_strategy1 --- tests/test_freqtradebot.py | 244 ++++++++++--------------------------- 1 file changed, 65 insertions(+), 179 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index eb3c77cc7..5268d40ec 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3128,16 +3128,28 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf, ticker, fee, assert mock_insuf.call_count == 1 -def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy_order_open, - fee, mocker) -> None: +@pytest.mark.parametrize('profit_only,bid,ask,handle_first,handle_second,sell_type', [ + # Enable profit + (True, 0.00001172, 0.00001173, False, True, SellType.SELL_SIGNAL.value), + # Disable profit + (False, 0.00002172, 0.00002173, True, False, SellType.SELL_SIGNAL.value), + # Enable loss + # * Shouldn't this be SellType.STOP_LOSS.value + (True, 0.00000172, 0.00000173, False, False, None), + # Disable loss + (False, 0.00000172, 0.00000173, True, False, SellType.SELL_SIGNAL.value), +]) +def test_sell_profit_only_enable_profit( + default_conf, limit_buy_order, limit_buy_order_open, + fee, mocker, profit_only, bid, ask, handle_first, handle_second, sell_type) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=MagicMock(return_value={ - 'bid': 0.00001172, - 'ask': 0.00001173, - 'last': 0.00001172 + 'bid': bid, + 'ask': ask, + 'last': bid }), create_order=MagicMock(side_effect=[ limit_buy_order_open, @@ -3147,128 +3159,29 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy ) default_conf.update({ 'use_sell_signal': True, - 'sell_profit_only': True, + 'sell_profit_only': profit_only, 'sell_profit_offset': 0.1, }) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - + if sell_type == SellType.SELL_SIGNAL.value: + freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) + else: + freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple( + sell_type=SellType.NONE)) freqtrade.enter_positions() trade = Trade.query.first() trade.update(limit_buy_order) freqtrade.wallets.update() patch_get_signal(freqtrade, value=(False, True, None)) - assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_trade(trade) is handle_first - freqtrade.strategy.sell_profit_offset = 0.0 - assert freqtrade.handle_trade(trade) is True + if handle_second: + freqtrade.strategy.sell_profit_offset = 0.0 + assert freqtrade.handle_trade(trade) is True - assert trade.sell_reason == SellType.SELL_SIGNAL.value - - -def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, limit_buy_order_open, - fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=MagicMock(return_value={ - 'bid': 0.00002172, - 'ask': 0.00002173, - 'last': 0.00002172 - }), - create_order=MagicMock(side_effect=[ - limit_buy_order_open, - {'id': 1234553382}, - ]), - get_fee=fee, - ) - default_conf.update({ - 'use_sell_signal': True, - 'sell_profit_only': False, - }) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.enter_positions() - - trade = Trade.query.first() - trade.update(limit_buy_order) - freqtrade.wallets.update() - patch_get_signal(freqtrade, value=(False, True, None)) - assert freqtrade.handle_trade(trade) is True - assert trade.sell_reason == SellType.SELL_SIGNAL.value - - -def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_order_open, - fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=MagicMock(return_value={ - 'bid': 0.00000172, - 'ask': 0.00000173, - 'last': 0.00000172 - }), - create_order=MagicMock(side_effect=[ - limit_buy_order_open, - {'id': 1234553382}, - ]), - get_fee=fee, - ) - default_conf.update({ - 'use_sell_signal': True, - 'sell_profit_only': True, - }) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple( - sell_type=SellType.NONE)) - freqtrade.enter_positions() - - trade = Trade.query.first() - trade.update(limit_buy_order) - patch_get_signal(freqtrade, value=(False, True, None)) - assert freqtrade.handle_trade(trade) is False - - -def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, limit_buy_order_open, - fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=MagicMock(return_value={ - 'bid': 0.0000172, - 'ask': 0.0000173, - 'last': 0.0000172 - }), - create_order=MagicMock(side_effect=[ - limit_buy_order_open, - {'id': 1234553382}, - ]), - get_fee=fee, - ) - default_conf.update({ - 'use_sell_signal': True, - 'sell_profit_only': False, - }) - - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - - freqtrade.enter_positions() - - trade = Trade.query.first() - trade.update(limit_buy_order) - freqtrade.wallets.update() - patch_get_signal(freqtrade, value=(False, True, None)) - assert freqtrade.handle_trade(trade) is True - assert trade.sell_reason == SellType.SELL_SIGNAL.value + assert trade.sell_reason == sell_type def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_open, @@ -3306,11 +3219,15 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_ assert trade.amount != amnt -def test__safe_exit_amount(default_conf, fee, caplog, mocker): +@pytest.mark.parametrize('amount_wallet,has_err', [ + (95.29, False), + (91.29, True) +]) +def test__safe_exit_amount(default_conf, fee, caplog, mocker, amount_wallet, has_err): patch_RPCManager(mocker) patch_exchange(mocker) amount = 95.33 - amount_wallet = 95.29 + amount_wallet = amount_wallet mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet)) wallet_update = mocker.patch('freqtrade.wallets.Wallets.update') trade = Trade( @@ -3324,37 +3241,19 @@ def test__safe_exit_amount(default_conf, fee, caplog, mocker): ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - - wallet_update.reset_mock() - assert freqtrade._safe_exit_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_exit_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_exit_amount_error(default_conf, fee, 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", - fee_open=fee.return_value, - fee_close=fee.return_value, - ) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - with pytest.raises(DependencyException, match=r"Not enough amount to sell."): - assert freqtrade._safe_exit_amount(trade.pair, trade.amount) + if has_err: + with pytest.raises(DependencyException, match=r"Not enough amount to sell."): + assert freqtrade._safe_exit_amount(trade.pair, trade.amount) + else: + wallet_update.reset_mock() + assert freqtrade._safe_exit_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_exit_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_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplog) -> None: @@ -4131,50 +4030,37 @@ def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_o assert trade is None -def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2) -> None: +@pytest.mark.parametrize('exception_thrown,ask,last,order_book_top,order_book', [ + (False, 0.045, 0.046, 2, None), + (True, 0.042, 0.046, 1, {'bids': [[]], 'asks': [[]]}) +]) +def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2, exception_thrown, + ask, last, order_book_top, order_book, caplog) -> None: """ - test if function get_rate will return the order book price - instead of the ask rate + test if function get_rate will return the order book price instead of the ask rate """ patch_exchange(mocker) - ticker_mock = MagicMock(return_value={'ask': 0.045, 'last': 0.046}) + ticker_mock = MagicMock(return_value={'ask': ask, 'last': last}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - fetch_l2_order_book=order_book_l2, + fetch_l2_order_book=MagicMock(return_value=order_book) if order_book else order_book_l2, fetch_ticker=ticker_mock, - ) default_conf['exchange']['name'] = 'binance' default_conf['bid_strategy']['use_order_book'] = True - default_conf['bid_strategy']['order_book_top'] = 2 + default_conf['bid_strategy']['order_book_top'] = order_book_top default_conf['bid_strategy']['ask_last_balance'] = 0 default_conf['telegram']['enabled'] = False freqtrade = FreqtradeBot(default_conf) - assert freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") == 0.043935 - assert ticker_mock.call_count == 0 - - -def test_order_book_bid_strategy_exception(mocker, default_conf, caplog) -> None: - patch_exchange(mocker) - ticker_mock = MagicMock(return_value={'ask': 0.042, 'last': 0.046}) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_l2_order_book=MagicMock(return_value={'bids': [[]], 'asks': [[]]}), - fetch_ticker=ticker_mock, - - ) - default_conf['exchange']['name'] = 'binance' - default_conf['bid_strategy']['use_order_book'] = True - default_conf['bid_strategy']['order_book_top'] = 1 - default_conf['bid_strategy']['ask_last_balance'] = 0 - default_conf['telegram']['enabled'] = False - - freqtrade = FreqtradeBot(default_conf) - # orderbook shall be used even if tickers would be lower. - with pytest.raises(PricingError): - freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") - assert log_has_re(r'Buy Price at location 1 from orderbook could not be determined.', caplog) + if exception_thrown: + with pytest.raises(PricingError): + freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") + assert log_has_re( + r'Buy Price at location 1 from orderbook could not be determined.', caplog) + else: + assert freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") == 0.043935 + assert ticker_mock.call_count == 0 def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2) -> None: From 42a2fdc1c520c97b27cf937e25aa7bd85a581a21 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 18 Sep 2021 03:01:08 -0600 Subject: [PATCH 35/64] parametrized test_order_dict --- tests/test_freqtradebot.py | 47 ++++++++------------------------------ 1 file changed, 10 insertions(+), 37 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5268d40ec..8e8ae4da2 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -78,11 +78,15 @@ def test_bot_cleanup(mocker, default_conf, caplog) -> None: assert coo_mock.call_count == 1 -def test_order_dict_dry_run(default_conf, mocker, caplog) -> None: +@pytest.mark.parametrize('runmode', [ + RunMode.DRY_RUN, + RunMode.LIVE +]) +def test_order_dict(default_conf, mocker, runmode, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) conf = default_conf.copy() - conf['runmode'] = RunMode.DRY_RUN + conf['runmode'] = runmode conf['order_types'] = { 'buy': 'market', 'sell': 'limit', @@ -92,45 +96,14 @@ def test_order_dict_dry_run(default_conf, mocker, caplog) -> None: conf['bid_strategy']['price_side'] = 'ask' freqtrade = FreqtradeBot(conf) + if runmode == RunMode.LIVE: + assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog) assert freqtrade.strategy.order_types['stoploss_on_exchange'] caplog.clear() # is left untouched conf = default_conf.copy() - conf['runmode'] = RunMode.DRY_RUN - conf['order_types'] = { - 'buy': 'market', - 'sell': 'limit', - 'stoploss': 'limit', - 'stoploss_on_exchange': False, - } - freqtrade = FreqtradeBot(conf) - assert not freqtrade.strategy.order_types['stoploss_on_exchange'] - assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog) - - -def test_order_dict_live(default_conf, mocker, caplog) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - - conf = default_conf.copy() - conf['runmode'] = RunMode.LIVE - conf['order_types'] = { - 'buy': 'market', - 'sell': 'limit', - 'stoploss': 'limit', - 'stoploss_on_exchange': True, - } - conf['bid_strategy']['price_side'] = 'ask' - - freqtrade = FreqtradeBot(conf) - assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog) - assert freqtrade.strategy.order_types['stoploss_on_exchange'] - - caplog.clear() - # is left untouched - conf = default_conf.copy() - conf['runmode'] = RunMode.LIVE + conf['runmode'] = runmode conf['order_types'] = { 'buy': 'market', 'sell': 'limit', @@ -3139,7 +3112,7 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf, ticker, fee, # Disable loss (False, 0.00000172, 0.00000173, True, False, SellType.SELL_SIGNAL.value), ]) -def test_sell_profit_only_enable_profit( +def test_sell_profit_only( default_conf, limit_buy_order, limit_buy_order_open, fee, mocker, profit_only, bid, ask, handle_first, handle_second, sell_type) -> None: patch_RPCManager(mocker) From 518a59ad41daa50c51deb8d7e70e75b3b9a8bad0 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 18 Sep 2021 03:20:00 -0600 Subject: [PATCH 36/64] parametrized test_edge_overrides_stoploss --- tests/test_freqtradebot.py | 60 ++++++++++---------------------------- 1 file changed, 15 insertions(+), 45 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 8e8ae4da2..debe2bee0 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -192,8 +192,14 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None: 'LTC/BTC', freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21 -def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf) -> None: - +@pytest.mark.parametrize('buy_price_mult,ignore_strat_sl', [ + # Override stoploss + (0.79, False), + # Override strategy stoploss + (0.85, True) +]) +def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, + buy_price_mult, ignore_strat_sl, edge_conf) -> None: patch_RPCManager(mocker) patch_exchange(mocker) patch_edge(mocker) @@ -207,9 +213,9 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=MagicMock(return_value={ - 'bid': buy_price * 0.79, - 'ask': buy_price * 0.79, - 'last': buy_price * 0.79 + 'bid': buy_price * buy_price_mult, + 'ask': buy_price * buy_price_mult, + 'last': buy_price * buy_price_mult, }), get_fee=fee, ) @@ -226,46 +232,10 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf ############################################# # stoploss shoud be hit - assert freqtrade.handle_trade(trade) is True - assert log_has('Executing Sell for NEO/BTC. Reason: stop_loss', caplog) - assert trade.sell_reason == SellType.STOP_LOSS.value - - -def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, - mocker, edge_conf) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - patch_edge(mocker) - edge_conf['max_open_trades'] = float('inf') - - # Strategy stoploss is -0.1 but Edge imposes a stoploss at -0.2 - # Thus, if price falls 15%, stoploss should not be triggered - # - # mocking the ticker: price is falling ... - buy_price = limit_buy_order['price'] - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=MagicMock(return_value={ - 'bid': buy_price * 0.85, - 'ask': buy_price * 0.85, - 'last': buy_price * 0.85 - }), - get_fee=fee, - ) - ############################################# - - # Create a trade with "limit_buy_order" price - freqtrade = FreqtradeBot(edge_conf) - freqtrade.active_pair_whitelist = ['NEO/BTC'] - patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.enter_positions() - trade = Trade.query.first() - trade.update(limit_buy_order) - ############################################# - - # stoploss shoud not be hit - assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_trade(trade) is not ignore_strat_sl + if not ignore_strat_sl: + assert log_has('Executing Sell for NEO/BTC. Reason: stop_loss', caplog) + assert trade.sell_reason == SellType.STOP_LOSS.value def test_total_open_trades_stakes(mocker, default_conf, ticker, fee) -> None: From 979c6f2f263404ae78f4002262e7a2b3cc8497b3 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 18 Sep 2021 03:49:15 -0600 Subject: [PATCH 37/64] moved leverage_brackets.json to exchange/binance_leverage_brackets.json --- freqtrade/exchange/binance.py | 6 ++++-- .../binance_leverage_brackets.json} | 0 2 files changed, 4 insertions(+), 2 deletions(-) rename freqtrade/{data/leverage_brackets.json => exchange/binance_leverage_brackets.json} (100%) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 60a1b8019..69d781395 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -149,7 +149,9 @@ class Binance(Exchange): if self.trading_mode == TradingMode.FUTURES: try: if self._config['dry_run']: - leverage_brackets_path = Path('freqtrade/data') / 'leverage_brackets.json' + leverage_brackets_path = ( + Path(__file__).parent / 'binance_leverage_brackets.json' + ) with open(leverage_brackets_path) as json_file: leverage_brackets = json.load(json_file) else: @@ -187,7 +189,7 @@ class Binance(Exchange): max_lev = 1/margin_req return max_lev - @retrier + @ retrier def _set_leverage( self, leverage: float, diff --git a/freqtrade/data/leverage_brackets.json b/freqtrade/exchange/binance_leverage_brackets.json similarity index 100% rename from freqtrade/data/leverage_brackets.json rename to freqtrade/exchange/binance_leverage_brackets.json From 713e7819f7e534a36984f9f4c76a11bbfd948896 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 18 Sep 2021 15:27:58 +0300 Subject: [PATCH 38/64] [SQUASH] Remove mypy import. --- freqtrade/strategy/informative_decorator.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/strategy/informative_decorator.py b/freqtrade/strategy/informative_decorator.py index c8ebf5989..4c5f21108 100644 --- a/freqtrade/strategy/informative_decorator.py +++ b/freqtrade/strategy/informative_decorator.py @@ -1,6 +1,5 @@ from typing import Any, Callable, NamedTuple, Optional, Union -from mypy_extensions import KwArg from pandas import DataFrame from freqtrade.exceptions import OperationalException @@ -13,12 +12,12 @@ PopulateIndicators = Callable[[Any, DataFrame, dict], DataFrame] class InformativeData(NamedTuple): asset: Optional[str] timeframe: str - fmt: Union[str, Callable[[KwArg(str)], str], None] + fmt: Union[str, Callable[[Any], str], None] ffill: bool def informative(timeframe: str, asset: str = '', - fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None, + fmt: Optional[Union[str, Callable[[Any], str]]] = None, ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]: """ A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to From eab7f8f6944e77fa7ae69bf99fe885aa76aea5a5 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 18 Sep 2021 15:44:21 +0300 Subject: [PATCH 39/64] [SQUASH] Doh. --- tests/strategy/test_strategy_helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 9132382fa..a01b55050 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -5,8 +5,8 @@ import pandas as pd import pytest from freqtrade.data.dataprovider import DataProvider -from freqtrade.strategy import (merge_informative_pair, stoploss_from_open, timeframe_to_minutes, - stoploss_from_absolute) +from freqtrade.strategy import (merge_informative_pair, stoploss_from_absolute, stoploss_from_open, + timeframe_to_minutes) def generate_test_data(timeframe: str, size: int): From c1895a0fc230f14e9de79a3042d77d3379a61c1f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 15:30:33 +0200 Subject: [PATCH 40/64] Remove warning related to legacy hyperopt --- docs/strategy-customization.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 2b22dd274..725252b30 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -801,9 +801,6 @@ for more information. will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique! -!!! Warning - When using a legacy hyperopt implementation informative pairs defined with a decorator will not be executed. Please update your strategy if necessary. - ## Additional data (Wallets) The strategy provides access to the `Wallets` object. This contains the current balances on the exchange. From ec9dbc550e66c2694bfe32a43c3442952d8b685c Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 18 Sep 2021 19:19:53 -0600 Subject: [PATCH 41/64] parametrized test_create_trade_minimal_amount --- tests/test_freqtradebot.py | 93 +++++++++++--------------------------- 1 file changed, 26 insertions(+), 67 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index debe2bee0..209c8a2a2 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -319,8 +319,16 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, freqtrade.create_trade('ETH/BTC') -def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order_open, - fee, mocker) -> None: +@pytest.mark.parametrize('stake_amount,create,amount_enough,max_open_trades', [ + (0.0005, True, True, 99), + (0.000000005, True, False, 99), + (0, False, True, 99), + (UNLIMITED_STAKE_AMOUNT, False, True, 0), +]) +def test_create_trade_minimal_amount( + default_conf, ticker, limit_buy_order_open, fee, mocker, + stake_amount, create, amount_enough, max_open_trades, caplog +) -> None: patch_RPCManager(mocker) patch_exchange(mocker) buy_mock = MagicMock(return_value=limit_buy_order_open) @@ -330,74 +338,25 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order_open, create_order=buy_mock, get_fee=fee, ) - default_conf['stake_amount'] = 0.0005 + default_conf['max_open_trades'] = max_open_trades freqtrade = FreqtradeBot(default_conf) + freqtrade.config['stake_amount'] = stake_amount patch_get_signal(freqtrade) - 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_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order_open, - fee, mocker, caplog) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - buy_mock = MagicMock(return_value=limit_buy_order_open) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - create_order=buy_mock, - get_fee=fee, - ) - - freqtrade = FreqtradeBot(default_conf) - freqtrade.config['stake_amount'] = 0.000000005 - - patch_get_signal(freqtrade) - - assert freqtrade.create_trade('ETH/BTC') - assert log_has_re(r"Stake amount for pair .* is too small.*", caplog) - - -def test_create_trade_zero_stake_amount(default_conf, ticker, limit_buy_order_open, - fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - buy_mock = MagicMock(return_value=limit_buy_order_open) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - create_order=buy_mock, - get_fee=fee, - ) - - freqtrade = FreqtradeBot(default_conf) - freqtrade.config['stake_amount'] = 0 - - patch_get_signal(freqtrade) - - assert not freqtrade.create_trade('ETH/BTC') - - -def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order_open, - fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - create_order=MagicMock(return_value=limit_buy_order_open), - get_fee=fee, - ) - default_conf['max_open_trades'] = 0 - default_conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT - - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - assert not freqtrade.create_trade('ETH/BTC') - assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0 + if create: + assert freqtrade.create_trade('ETH/BTC') + if amount_enough: + rate, amount = buy_mock.call_args[1]['rate'], buy_mock.call_args[1]['amount'] + assert rate * amount <= default_conf['stake_amount'] + else: + assert log_has_re( + r"Stake amount for pair .* is too small.*", + caplog + ) + else: + assert not freqtrade.create_trade('ETH/BTC') + if not max_open_trades: + assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0 def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee, From cee4ed541b8e4bec16ce8a23598531fb4d64ec57 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 18 Sep 2021 19:46:46 -0600 Subject: [PATCH 42/64] parametrized test_update_trade_state_withorderdict --- tests/test_freqtradebot.py | 104 +++++++++++++------------------------ 1 file changed, 35 insertions(+), 69 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 209c8a2a2..72d1f6150 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -359,8 +359,12 @@ def test_create_trade_minimal_amount( assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0 +@pytest.mark.parametrize('whitelist,positions', [ + (["ETH/BTC"], 1), # No pairs left + ([], 0), # No pairs in whitelist +]) def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee, - mocker, caplog) -> None: + whitelist, positions, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -369,36 +373,20 @@ def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_ope create_order=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) - - default_conf['exchange']['pair_whitelist'] = ["ETH/BTC"] + default_conf['exchange']['pair_whitelist'] = whitelist freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) n = freqtrade.enter_positions() - assert n == 1 - assert not log_has_re(r"No currency pair in active pair whitelist.*", caplog) - n = freqtrade.enter_positions() - assert n == 0 - assert log_has_re(r"No currency pair in active pair whitelist.*", caplog) - - -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( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - create_order=MagicMock(return_value={'id': limit_buy_order['id']}), - get_fee=fee, - ) - default_conf['exchange']['pair_whitelist'] = [] - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - n = freqtrade.enter_positions() - assert n == 0 - assert log_has("Active pair whitelist is empty.", caplog) + assert n == positions + if positions: + assert not log_has_re(r"No currency pair in active pair whitelist.*", caplog) + n = freqtrade.enter_positions() + assert n == 0 + assert log_has_re(r"No currency pair in active pair whitelist.*", caplog) + else: + assert n == 0 + assert log_has("Active pair whitelist is empty.", caplog) @pytest.mark.usefixtures("init_persistence") @@ -1555,30 +1543,27 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, stop_price=0.00002346 * 0.99) -def test_enter_positions(mocker, default_conf, caplog) -> None: +@pytest.mark.parametrize('return_value,side_effect,log_message', [ + (False, None, 'Found no buy signals for whitelisted currencies. Trying again...'), + (None, DependencyException, 'Unable to create trade for ETH/BTC: ') +]) +def test_enter_positions(mocker, default_conf, return_value, side_effect, + log_message, caplog) -> None: caplog.set_level(logging.DEBUG) freqtrade = get_patched_freqtradebot(mocker, default_conf) - 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) - # 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: - freqtrade = get_patched_freqtradebot(mocker, default_conf) - mock_ct = mocker.patch( 'freqtrade.freqtradebot.FreqtradeBot.create_trade', - MagicMock(side_effect=DependencyException) + MagicMock( + return_value=return_value, + side_effect=side_effect + ) ) n = freqtrade.enter_positions() assert n == 0 + assert log_has(log_message, caplog) + # create_trade should be called once for every pair in the whitelist. assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist']) - assert log_has('Unable to create trade for ETH/BTC: ', caplog) def test_exit_positions(mocker, default_conf, limit_buy_order, caplog) -> None: @@ -1672,8 +1657,13 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No assert log_has_re('Found open order for.*', caplog) +@pytest.mark.parametrize('initial_amount,has_rounding_fee', [ + (90.99181073 + 1e-14, True), + (8.0, False) +]) def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, fee, - mocker): + mocker, initial_amount, has_rounding_fee, caplog): + trades_for_order[0]['amount'] = initial_amount mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) # fetch_order should not be called!! mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) @@ -1694,32 +1684,8 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_ freqtrade.update_trade_state(trade, '123456', limit_buy_order) assert trade.amount != amount assert trade.amount == limit_buy_order['amount'] - - -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) - # fetch_order should not be called!! - mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) - patch_exchange(mocker) - amount = sum(x['amount'] for x in trades_for_order) - freqtrade = get_patched_freqtradebot(mocker, default_conf) - trade = Trade( - pair='LTC/ETH', - 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, - ) - freqtrade.update_trade_state(trade, '123456', limit_buy_order) - assert trade.amount != amount - assert trade.amount == limit_buy_order['amount'] - assert log_has_re(r'Applying fee on amount for .*', caplog) + if has_rounding_fee: + assert log_has_re(r'Applying fee on amount for .*', caplog) def test_update_trade_state_exception(mocker, default_conf, From c54259b4c55598bbc8579bc4f65d322f7180faac Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Sun, 19 Sep 2021 11:35:29 +0530 Subject: [PATCH 43/64] Added ftx interest formula tests --- tests/leverage/{test_leverage.py => test_interest.py} | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) rename tests/leverage/{test_leverage.py => test_interest.py} (83%) diff --git a/tests/leverage/test_leverage.py b/tests/leverage/test_interest.py similarity index 83% rename from tests/leverage/test_leverage.py rename to tests/leverage/test_interest.py index 7b7ca0f9b..c7e787bdb 100644 --- a/tests/leverage/test_leverage.py +++ b/tests/leverage/test_interest.py @@ -22,9 +22,10 @@ twentyfive_hours = Decimal(25.0) ('kraken', 0.00025, five_hours, 0.045), ('kraken', 0.00025, twentyfive_hours, 0.12), # FTX - # TODO-lev: - implement FTX tests - # ('ftx', Decimal(0.0005), ten_mins, 0.06), - # ('ftx', Decimal(0.0005), five_hours, 0.045), + ('ftx', 0.0005, ten_mins, 0.00125), + ('ftx', 0.00025, ten_mins, 0.000625), + ('ftx', 0.00025, five_hours, 0.003125), + ('ftx', 0.00025, twentyfive_hours, 0.015625), ]) def test_interest(exchange, interest_rate, hours, expected): borrowed = Decimal(60.0) From 27bd30d266b121c0ead4c08c302dfa776e5596bd Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Sun, 19 Sep 2021 11:42:29 +0530 Subject: [PATCH 44/64] fixed formatting issues --- freqtrade/leverage/interest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/leverage/interest.py b/freqtrade/leverage/interest.py index c687c8b5b..2878ad784 100644 --- a/freqtrade/leverage/interest.py +++ b/freqtrade/leverage/interest.py @@ -40,4 +40,4 @@ def interest( # https://help.ftx.com/hc/en-us/articles/360053007671-Spot-Margin-Trading-Explainer return borrowed * rate * ceil(hours)/twenty_four else: - raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") \ No newline at end of file + raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") From 835e0e69fcabadc658327950baac6fcb3c4dcfc5 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 19 Sep 2021 02:23:05 -0600 Subject: [PATCH 45/64] removed leverage from create order api call --- docs/leverage.md | 4 ++++ freqtrade/exchange/binance.py | 11 ++--------- freqtrade/exchange/ftx.py | 10 ++-------- freqtrade/exchange/kraken.py | 11 ++--------- 4 files changed, 10 insertions(+), 26 deletions(-) diff --git a/docs/leverage.md b/docs/leverage.md index c4b975a0b..9448c64c3 100644 --- a/docs/leverage.md +++ b/docs/leverage.md @@ -15,3 +15,7 @@ For longs, the currency which pays the interest fee for the `borrowed` will alre Rollover fee = P (borrowed money) * R (quat_hourly_interest) * ceiling(T/4) (in hours) I (interest) = Opening fee + Rollover fee [source](https://support.kraken.com/hc/en-us/articles/206161568-What-are-the-fees-for-margin-trading-) + +# TODO-lev: Mention that says you can't run 2 bots on the same account with leverage, + +#TODO-lev: Create a huge risk disclaimer \ No newline at end of file diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 69d781395..7d83e971b 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -107,15 +107,8 @@ class Binance(Exchange): rate = self.price_to_precision(pair, rate) - order = self._api.create_order( - symbol=pair, - type=ordertype, - side=side, - amount=amount, - price=rate, - params=params, - leverage=leverage - ) + order = self._api.create_order(symbol=pair, type=ordertype, side=side, + amount=amount, price=rate, params=params) logger.info('stoploss limit order added for %s. ' 'stop price: %s. limit: %s', pair, stop_price, rate) self._log_exchange_response('create_stoploss_order', order) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index eaf9a0477..0f572dee9 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -81,14 +81,8 @@ class Ftx(Exchange): params['stopPrice'] = stop_price amount = self.amount_to_precision(pair, amount) - order = self._api.create_order( - symbol=pair, - type=ordertype, - side=side, - amount=amount, - leverage=leverage, - params=params - ) + order = self._api.create_order(symbol=pair, type=ordertype, side=side, + amount=amount, params=params) self._log_exchange_response('create_stoploss_order', order) logger.info('stoploss order added for %s. ' 'stop price: %s.', pair, stop_price) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index d6a816c9e..ec49c963f 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -114,15 +114,8 @@ class Kraken(Exchange): try: amount = self.amount_to_precision(pair, amount) - order = self._api.create_order( - symbol=pair, - type=ordertype, - side=side, - amount=amount, - price=stop_price, - leverage=leverage, - params=params - ) + order = self._api.create_order(symbol=pair, type=ordertype, side=side, + amount=amount, price=stop_price, params=params) self._log_exchange_response('create_stoploss_order', order) logger.info('stoploss order added for %s. ' 'stop price: %s.', pair, stop_price) From fa74b95a013e44ce1289b2c3cc231a75c54601d4 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 19 Sep 2021 02:33:28 -0600 Subject: [PATCH 46/64] reduced amount of code for leverage_brackets test --- tests/exchange/test_binance.py | 40 +- tests/leverage_brackets.py | 1215 -------------------------------- 2 files changed, 38 insertions(+), 1217 deletions(-) delete mode 100644 tests/leverage_brackets.py diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index cbbace1db..0c3e86fdd 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -9,7 +9,6 @@ from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException from tests.conftest import get_mock_coro, get_patched_exchange, log_has_re from tests.exchange.test_exchange import ccxt_exceptionhandlers -from tests.leverage_brackets import leverage_brackets @pytest.mark.parametrize('limitratio,expected,side', [ @@ -281,7 +280,44 @@ def test_fill_leverage_brackets_binance_dryrun(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") exchange.fill_leverage_brackets() - assert exchange._leverage_brackets == leverage_brackets() + leverage_brackets = { + "1000SHIB/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "1INCH/USDT": [ + [0.0, 0.012], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "AAVE/USDT": [ + [0.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.1665], + [10000000.0, 0.25] + ], + "ADA/BUSD": [ + [0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5] + ] + } + + for key, value in leverage_brackets.items(): + assert exchange._leverage_brackets[key] == value def test__set_leverage_binance(mocker, default_conf): diff --git a/tests/leverage_brackets.py b/tests/leverage_brackets.py deleted file mode 100644 index aa60a7af2..000000000 --- a/tests/leverage_brackets.py +++ /dev/null @@ -1,1215 +0,0 @@ -def leverage_brackets(): - return { - "1000SHIB/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "1INCH/USDT": [ - [0.0, 0.012], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "AAVE/USDT": [ - [0.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.1665], - [10000000.0, 0.25] - ], - "ADA/BUSD": [ - [0.0, 0.025], - [100000.0, 0.05], - [500000.0, 0.1], - [1000000.0, 0.15], - [2000000.0, 0.25], - [5000000.0, 0.5] - ], - "ADA/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "AKRO/USDT": [ - [0.0, 0.012], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ALGO/USDT": [ - [0.0, 0.01], - [50000.0, 0.025], - [150000.0, 0.05], - [250000.0, 0.1], - [500000.0, 0.125], - [1000000.0, 0.25], - [2000000.0, 0.5] - ], - "ALICE/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ALPHA/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ANKR/USDT": [ - [0.0, 0.012], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ATA/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ATOM/USDT": [ - [0.0, 0.01], - [50000.0, 0.025], - [150000.0, 0.05], - [250000.0, 0.1], - [500000.0, 0.125], - [1000000.0, 0.25], - [2000000.0, 0.5] - ], - "AUDIO/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "AVAX/USDT": [ - [0.0, 0.01], - [50000.0, 0.025], - [150000.0, 0.05], - [250000.0, 0.1], - [500000.0, 0.125], - [750000.0, 0.25], - [1000000.0, 0.5] - ], - "AXS/USDT": [ - [0.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.1665], - [10000000.0, 0.25], - [15000000.0, 0.5] - ], - "BAKE/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "BAL/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "BAND/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "BAT/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "BCH/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "BEL/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "BLZ/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "BNB/BUSD": [ - [0.0, 0.025], - [100000.0, 0.05], - [500000.0, 0.1], - [1000000.0, 0.15], - [2000000.0, 0.25], - [5000000.0, 0.5] - ], - "BNB/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "BTC/BUSD": [ - [0.0, 0.004], - [25000.0, 0.005], - [100000.0, 0.01], - [500000.0, 0.025], - [1000000.0, 0.05], - [2000000.0, 0.1], - [5000000.0, 0.125], - [10000000.0, 0.15], - [20000000.0, 0.25], - [30000000.0, 0.5] - ], - "BTC/USDT": [ - [0.0, 0.004], - [50000.0, 0.005], - [250000.0, 0.01], - [1000000.0, 0.025], - [5000000.0, 0.05], - [20000000.0, 0.1], - [50000000.0, 0.125], - [100000000.0, 0.15], - [200000000.0, 0.25], - [300000000.0, 0.5] - ], - "BTCBUSD_210129": [ - [0.0, 0.004], - [5000.0, 0.005], - [25000.0, 0.01], - [100000.0, 0.025], - [500000.0, 0.05], - [2000000.0, 0.1], - [5000000.0, 0.125], - [10000000.0, 0.15], - [20000000.0, 0.25] - ], - "BTCBUSD_210226": [ - [0.0, 0.004], - [5000.0, 0.005], - [25000.0, 0.01], - [100000.0, 0.025], - [500000.0, 0.05], - [2000000.0, 0.1], - [5000000.0, 0.125], - [10000000.0, 0.15], - [20000000.0, 0.25] - ], - "BTCDOM/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "BTCSTUSDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "BTCUSDT_210326": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "BTCUSDT_210625": [ - [0.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "BTCUSDT_210924": [ - [0.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25], - [20000000.0, 0.5] - ], - "BTS/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "BTT/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "BZRX/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "C98/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "CELR/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "CHR/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "CHZ/USDT": [ - [0.0, 0.012], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "COMP/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "COTI/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "CRV/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "CTK/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "CVC/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "DASH/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "DEFI/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "DENT/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "DGB/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "DODO/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "DOGE/BUSD": [ - [0.0, 0.025], - [100000.0, 0.05], - [500000.0, 0.1], - [1000000.0, 0.15], - [2000000.0, 0.25], - [5000000.0, 0.5] - ], - "DOGE/USDT": [ - [0.0, 0.01], - [50000.0, 0.025], - [150000.0, 0.05], - [250000.0, 0.1], - [500000.0, 0.125], - [750000.0, 0.25], - [1000000.0, 0.5] - ], - "DOT/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "DOTECOUSDT": [ - [0.0, 0.012], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "DYDX/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "EGLD/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ENJ/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "EOS/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "ETC/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "ETH/BUSD": [ - [0.0, 0.004], - [25000.0, 0.005], - [100000.0, 0.01], - [500000.0, 0.025], - [1000000.0, 0.05], - [2000000.0, 0.1], - [5000000.0, 0.125], - [10000000.0, 0.15], - [20000000.0, 0.25], - [30000000.0, 0.5] - ], - "ETH/USDT": [ - [0.0, 0.005], - [10000.0, 0.0065], - [100000.0, 0.01], - [500000.0, 0.02], - [1000000.0, 0.05], - [2000000.0, 0.1], - [5000000.0, 0.125], - [10000000.0, 0.15], - [20000000.0, 0.25] - ], - "ETHUSDT_210326": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "ETHUSDT_210625": [ - [0.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "ETHUSDT_210924": [ - [0.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25], - [20000000.0, 0.5] - ], - "FIL/USDT": [ - [0.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.1665], - [10000000.0, 0.25] - ], - "FLM/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "FTM/USDT": [ - [0.0, 0.01], - [50000.0, 0.025], - [150000.0, 0.05], - [250000.0, 0.1], - [500000.0, 0.125], - [750000.0, 0.25], - [1000000.0, 0.5] - ], - "FTT/BUSD": [ - [0.0, 0.025], - [100000.0, 0.05], - [500000.0, 0.1], - [1000000.0, 0.15], - [2000000.0, 0.25], - [5000000.0, 0.5] - ], - "GRT/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "GTC/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "HBAR/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "HNT/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "HOT/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ICP/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ICX/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "IOST/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "IOTA/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "IOTX/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "KAVA/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "KEEP/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "KNC/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "KSM/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "LENDUSDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "LINA/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "LINK/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "LIT/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "LRC/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "LTC/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "LUNA/USDT": [ - [0.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.1665], - [10000000.0, 0.25], - [15000000.0, 0.5] - ], - "MANA/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "MASK/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "MATIC/USDT": [ - [0.0, 0.01], - [50000.0, 0.025], - [150000.0, 0.05], - [250000.0, 0.1], - [500000.0, 0.125], - [750000.0, 0.25], - [1000000.0, 0.5] - ], - "MKR/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "MTL/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "NEAR/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "NEO/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "NKN/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "OCEAN/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "OGN/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "OMG/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ONE/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ONT/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "QTUM/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "RAY/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "REEF/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "REN/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "RLC/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "RSR/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "RUNE/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "RVN/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "SAND/USDT": [ - [0.0, 0.012], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "SC/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "SFP/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "SKL/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "SNX/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "SOL/BUSD": [ - [0.0, 0.025], - [100000.0, 0.05], - [500000.0, 0.1], - [1000000.0, 0.15], - [2000000.0, 0.25], - [5000000.0, 0.5] - ], - "SOL/USDT": [ - [0.0, 0.01], - [50000.0, 0.025], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.25], - [10000000.0, 0.5] - ], - "SRM/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "STMX/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "STORJ/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "SUSHI/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "SXP/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "THETA/USDT": [ - [0.0, 0.01], - [50000.0, 0.025], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.1665], - [10000000.0, 0.25] - ], - "TLM/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "TOMO/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "TRB/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "TRX/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "UNFI/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "UNI/USDT": [ - [0.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.1665], - [10000000.0, 0.25] - ], - "VET/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "WAVES/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "XEM/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "XLM/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "XMR/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "XRP/BUSD": [ - [0.0, 0.025], - [100000.0, 0.05], - [500000.0, 0.1], - [1000000.0, 0.15], - [2000000.0, 0.25], - [5000000.0, 0.5] - ], - "XRP/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "XTZ/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "YFI/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "YFII/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ZEC/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ZEN/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ZIL/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ZRX/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ] - } From 2d679177e506067753c3a5475c80a8f69ce70f2f Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 19 Sep 2021 03:05:58 -0600 Subject: [PATCH 47/64] Added in lev prep before creating api order --- freqtrade/exchange/binance.py | 1 + freqtrade/exchange/exchange.py | 13 ++++++------- freqtrade/exchange/ftx.py | 1 + freqtrade/exchange/kraken.py | 4 ++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 7d83e971b..35f427c34 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -107,6 +107,7 @@ class Binance(Exchange): rate = self.price_to_precision(pair, rate) + self._lev_prep(pair, leverage) order = self._api.create_order(symbol=pair, type=ordertype, side=side, amount=amount, price=rate, params=params) logger.info('stoploss limit order added for %s. ' diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 4021e7d02..4617fd4c2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -773,10 +773,11 @@ class Exchange: # Order handling def _lev_prep(self, pair: str, leverage: float): - self.set_margin_mode(pair, self.collateral) - self._set_leverage(leverage, pair) + if self.trading_mode != TradingMode.SPOT: + self.set_margin_mode(pair, self.collateral) + self._set_leverage(leverage, pair) - def _get_params(self, time_in_force: str, ordertype: str, leverage: float) -> Dict: + def _get_params(self, ordertype: str, leverage: float, time_in_force: str = 'gtc') -> Dict: params = self._params.copy() if time_in_force != 'gtc' and ordertype != 'market': param = self._ft_has.get('time_in_force_parameter', '') @@ -790,10 +791,7 @@ class Exchange: dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate, leverage) return dry_order - if self.trading_mode != TradingMode.SPOT: - self._lev_prep(pair, leverage) - - params = self._get_params(time_in_force, ordertype, leverage) + params = self._get_params(ordertype, leverage, time_in_force) try: # Set the precision for amount and price(rate) as accepted by the exchange @@ -802,6 +800,7 @@ class Exchange: or self._api.options.get("createMarketBuyOrderRequiresPrice", False)) rate_for_order = self.price_to_precision(pair, rate) if needs_price else None + self._lev_prep(pair, leverage) order = self._api.create_order(pair, ordertype, side, amount, rate_for_order, params) self._log_exchange_response('create_order', order) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 0f572dee9..62adea04c 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -81,6 +81,7 @@ class Ftx(Exchange): params['stopPrice'] = stop_price amount = self.amount_to_precision(pair, amount) + self._lev_prep(pair, leverage) order = self._api.create_order(symbol=pair, type=ordertype, side=side, amount=amount, params=params) self._log_exchange_response('create_stoploss_order', order) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index ec49c963f..19d0a4967 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -184,8 +184,8 @@ class Kraken(Exchange): """ return - def _get_params(self, time_in_force: str, ordertype: str, leverage: float) -> Dict: - params = super()._get_params(time_in_force, ordertype, leverage) + def _get_params(self, ordertype: str, leverage: float, time_in_force: str = 'gtc') -> Dict: + params = super()._get_params(ordertype, leverage, time_in_force) if leverage > 1.0: params['leverage'] = leverage return params From ab88217186afa23718421ad67867a9d624024878 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Sep 2021 13:16:30 +0200 Subject: [PATCH 48/64] Improve /balance output to include starting balance and percentual change closes #5503 --- freqtrade/rpc/api_server/api_schemas.py | 7 +++++++ freqtrade/rpc/rpc.py | 24 ++++++++++++++++++++---- freqtrade/rpc/telegram.py | 23 +++++++++++++++-------- tests/rpc/test_rpc_apiserver.py | 14 ++++++++++---- tests/rpc/test_rpc_telegram.py | 2 ++ 5 files changed, 54 insertions(+), 16 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 3adbebc16..eb6082087 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -46,6 +46,13 @@ class Balances(BaseModel): value: float stake: str note: str + starting_capital: float + starting_capital_ratio: float + starting_capital_pct: float + starting_capital_fiat: float + starting_capital_fiat_ratio: float + starting_capital_fiat_pct: float + class Count(BaseModel): diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b7b1fe603..12a444b80 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -458,6 +458,9 @@ class RPC: raise RPCException('Error getting current tickers.') self._freqtrade.wallets.update(require_update=False) + starting_capital = self._freqtrade.wallets.get_starting_balance() + starting_capital_fiat = self._fiat_converter.convert_amount( + starting_capital, stake_currency, fiat_display_currency) if self._fiat_converter else 0 for coin, balance in self._freqtrade.wallets.get_all_balances().items(): if not balance.total: @@ -493,15 +496,28 @@ class RPC: else: raise RPCException('All balances are zero.') - symbol = fiat_display_currency - value = self._fiat_converter.convert_amount(total, stake_currency, - symbol) if self._fiat_converter else 0 + value = self._fiat_converter.convert_amount( + total, stake_currency, fiat_display_currency) if self._fiat_converter else 0 + + starting_capital_ratio = 0.0 + starting_capital_fiat_ratio = 0.0 + if starting_capital: + starting_capital_ratio = (total / starting_capital) - 1 + if starting_capital_fiat: + starting_capital_fiat_ratio = (value / starting_capital_fiat) - 1 + return { 'currencies': output, 'total': total, - 'symbol': symbol, + 'symbol': fiat_display_currency, 'value': value, 'stake': stake_currency, + 'starting_capital': starting_capital, + 'starting_capital_ratio': starting_capital_ratio, + 'starting_capital_pct': round(starting_capital_ratio * 100, 2), + 'starting_capital_fiat': starting_capital_fiat, + 'starting_capital_fiat_ratio': starting_capital_fiat_ratio, + 'starting_capital_fiat_pct': round(starting_capital_fiat_ratio * 100, 2), 'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else '' } diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a988d2b60..ffb7385da 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -603,12 +603,15 @@ class Telegram(RPCHandler): output = '' if self._config['dry_run']: - output += ( - 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" - ) + output += "*Warning:* Simulated balances in Dry Mode.\n" + + output += ("Starting capital: " + f"`{result['starting_capital']}` {self._config['stake_currency']}" + ) + output += (f" `{result['starting_capital_fiat']}` " + f"{self._config['fiat_display_currency']}.\n" + ) if result['starting_capital_fiat'] > 0 else '.\n' + total_dust_balance = 0 total_dust_currencies = 0 for curr in result['currencies']: @@ -641,9 +644,13 @@ class Telegram(RPCHandler): f"{round_coin_value(total_dust_balance, result['stake'], False)}`\n") output += ("\n*Estimated Value*:\n" - f"\t`{result['stake']}: {result['total']: .8f}`\n" + f"\t`{result['stake']}: " + f"{round_coin_value(result['total'], result['stake'], False)}`" + f" `({result['starting_capital_pct']}%)`\n" f"\t`{result['symbol']}: " - f"{round_coin_value(result['value'], result['symbol'], False)}`\n") + f"{round_coin_value(result['value'], result['symbol'], False)}`" + f" `({result['starting_capital_fiat_pct']}%)`\n" + ) self._send_msg(output, reload_able=True, callback_path="update_balance", query=update.callback_query) except RPCException as e: diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 43eb70938..7c98b2df7 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -422,20 +422,22 @@ def test_api_stopbuy(botclient): assert ftbot.config['max_open_trades'] == 0 -def test_api_balance(botclient, mocker, rpc_balance): +def test_api_balance(botclient, mocker, rpc_balance, tickers): 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_tickers', tickers) mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', side_effect=lambda a, b: f"{a}/{b}") ftbot.wallets.update() rc = client_get(client, f"{BASE_URI}/balance") assert_response(rc) - assert "currencies" in rc.json() - assert len(rc.json()["currencies"]) == 5 - assert rc.json()['currencies'][0] == { + response = rc.json() + assert "currencies" in response + assert len(response["currencies"]) == 5 + assert response['currencies'][0] == { 'currency': 'BTC', 'free': 12.0, 'balance': 12.0, @@ -443,6 +445,10 @@ def test_api_balance(botclient, mocker, rpc_balance): 'est_stake': 12.0, 'stake': 'BTC', } + assert 'starting_capital' in response + assert 'starting_capital_fiat' in response + assert 'starting_capital_pct' in response + assert 'starting_capital_ratio' in response def test_api_count(botclient, mocker, ticker, fee, markets): diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 2013dad7d..21f1cd000 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -576,6 +576,8 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None 'total': 100.0, 'symbol': 100.0, 'value': 1000.0, + 'starting_capital': 1000, + 'starting_capital_fiat': 1000, }) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) From ec03531771904a3a20dc31e4c64d0f13156fdcce Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Sep 2021 13:29:09 +0200 Subject: [PATCH 49/64] Improve naming of variables --- freqtrade/rpc/api_server/api_schemas.py | 1 - freqtrade/rpc/rpc.py | 15 ++++++--------- freqtrade/rpc/telegram.py | 3 +-- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index eb6082087..46187f571 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -54,7 +54,6 @@ class Balances(BaseModel): starting_capital_fiat_pct: float - class Count(BaseModel): current: int max: int diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 12a444b80..f6599b429 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -459,7 +459,7 @@ class RPC: self._freqtrade.wallets.update(require_update=False) starting_capital = self._freqtrade.wallets.get_starting_balance() - starting_capital_fiat = self._fiat_converter.convert_amount( + starting_cap_fiat = self._fiat_converter.convert_amount( starting_capital, stake_currency, fiat_display_currency) if self._fiat_converter else 0 for coin, balance in self._freqtrade.wallets.get_all_balances().items(): @@ -500,11 +500,8 @@ class RPC: total, stake_currency, fiat_display_currency) if self._fiat_converter else 0 starting_capital_ratio = 0.0 - starting_capital_fiat_ratio = 0.0 - if starting_capital: - starting_capital_ratio = (total / starting_capital) - 1 - if starting_capital_fiat: - starting_capital_fiat_ratio = (value / starting_capital_fiat) - 1 + starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0 + starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0 return { 'currencies': output, @@ -515,9 +512,9 @@ class RPC: 'starting_capital': starting_capital, 'starting_capital_ratio': starting_capital_ratio, 'starting_capital_pct': round(starting_capital_ratio * 100, 2), - 'starting_capital_fiat': starting_capital_fiat, - 'starting_capital_fiat_ratio': starting_capital_fiat_ratio, - 'starting_capital_fiat_pct': round(starting_capital_fiat_ratio * 100, 2), + 'starting_capital_fiat': starting_cap_fiat, + 'starting_capital_fiat_ratio': starting_cap_fiat_ratio, + 'starting_capital_fiat_pct': round(starting_cap_fiat_ratio * 100, 2), 'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else '' } diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ffb7385da..19c58b63d 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -649,8 +649,7 @@ class Telegram(RPCHandler): f" `({result['starting_capital_pct']}%)`\n" f"\t`{result['symbol']}: " f"{round_coin_value(result['value'], result['symbol'], False)}`" - f" `({result['starting_capital_fiat_pct']}%)`\n" - ) + f" `({result['starting_capital_fiat_pct']}%)`\n") self._send_msg(output, reload_able=True, callback_path="update_balance", query=update.callback_query) except RPCException as e: From d8d6f245a71c45ded9bc9926d2ad04995ec1d8a2 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 19 Sep 2021 16:44:02 -0600 Subject: [PATCH 50/64] Fixed breaking tests in test_freqtradebot.py --- tests/test_freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index cd9dd6103..bb9527011 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1559,7 +1559,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, @pytest.mark.parametrize('return_value,side_effect,log_message', [ - (False, None, 'Found no buy signals for whitelisted currencies. Trying again...'), + (False, None, 'Found no enter signals for whitelisted currencies. Trying again...'), (None, DependencyException, 'Unable to create trade for ETH/BTC: ') ]) def test_enter_positions(mocker, default_conf, return_value, side_effect, @@ -3126,7 +3126,7 @@ def test__safe_exit_amount(default_conf, fee, caplog, mocker, amount_wallet, has freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) if has_err: - with pytest.raises(DependencyException, match=r"Not enough amount to sell."): + with pytest.raises(DependencyException, match=r"Not enough amount to exit trade."): assert freqtrade._safe_exit_amount(trade.pair, trade.amount) else: wallet_update.reset_mock() From 4b5cd891cdc68c6132da40ce7aeb426e3f5d641c Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 21 Sep 2021 07:11:53 +0200 Subject: [PATCH 51/64] Add V3 test strategy --- .../strats/informative_decorator_strategy.py | 2 +- tests/strategy/strats/legacy_strategy_v1.py | 2 +- tests/strategy/strats/strategy_test_v2.py | 2 +- tests/strategy/strats/strategy_test_v3.py | 159 ++++++++++++++++++ tests/strategy/test_interface.py | 8 +- tests/strategy/test_strategy_loading.py | 6 +- 6 files changed, 169 insertions(+), 10 deletions(-) create mode 100644 tests/strategy/strats/strategy_test_v3.py diff --git a/tests/strategy/strats/informative_decorator_strategy.py b/tests/strategy/strats/informative_decorator_strategy.py index a32ad79e8..4dd2d84eb 100644 --- a/tests/strategy/strats/informative_decorator_strategy.py +++ b/tests/strategy/strats/informative_decorator_strategy.py @@ -3,7 +3,7 @@ from pandas import DataFrame from freqtrade.strategy import informative, merge_informative_pair -from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy import IStrategy class InformativeDecoratorTest(IStrategy): diff --git a/tests/strategy/strats/legacy_strategy_v1.py b/tests/strategy/strats/legacy_strategy_v1.py index ebfce632b..adb75c33e 100644 --- a/tests/strategy/strats/legacy_strategy_v1.py +++ b/tests/strategy/strats/legacy_strategy_v1.py @@ -4,7 +4,7 @@ import talib.abstract as ta from pandas import DataFrame -from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy import IStrategy # -------------------------------- diff --git a/tests/strategy/strats/strategy_test_v2.py b/tests/strategy/strats/strategy_test_v2.py index 53e39526f..428ecc8c0 100644 --- a/tests/strategy/strats/strategy_test_v2.py +++ b/tests/strategy/strats/strategy_test_v2.py @@ -4,7 +4,7 @@ import talib.abstract as ta from pandas import DataFrame import freqtrade.vendor.qtpylib.indicators as qtpylib -from freqtrade.strategy.interface import IStrategy +from freqtrade.strategy import IStrategy class StrategyTestV2(IStrategy): diff --git a/tests/strategy/strats/strategy_test_v3.py b/tests/strategy/strats/strategy_test_v3.py new file mode 100644 index 000000000..347fa43bb --- /dev/null +++ b/tests/strategy/strats/strategy_test_v3.py @@ -0,0 +1,159 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement + +import talib.abstract as ta +from pandas import DataFrame + +import freqtrade.vendor.qtpylib.indicators as qtpylib +from freqtrade.strategy import (BooleanParameter, DecimalParameter, IntParameter, IStrategy, + RealParameter) + + +class StrategyTestV3(IStrategy): + """ + Strategy used by tests freqtrade bot. + Please do not modify this strategy, it's intended for internal use only. + Please look at the SampleStrategy in the user_data/strategy directory + or strategy repository https://github.com/freqtrade/freqtrade-strategies + for samples and inspiration. + """ + INTERFACE_VERSION = 3 + + # Minimal ROI designed for the strategy + minimal_roi = { + "40": 0.0, + "30": 0.01, + "20": 0.02, + "0": 0.04 + } + + # Optimal stoploss designed for the strategy + stoploss = -0.10 + + # Optimal timeframe for the strategy + timeframe = '5m' + + # Optional order type mapping + order_types = { + 'buy': 'limit', + 'sell': 'limit', + 'stoploss': 'limit', + 'stoploss_on_exchange': False + } + + # Number of candles the strategy requires before producing valid signals + startup_candle_count: int = 20 + + # Optional time in force for orders + order_time_in_force = { + 'buy': 'gtc', + 'sell': 'gtc', + } + + buy_params = { + 'buy_rsi': 35, + # Intentionally not specified, so "default" is tested + # 'buy_plusdi': 0.4 + } + + sell_params = { + 'sell_rsi': 74, + 'sell_minusdi': 0.4 + } + + buy_rsi = IntParameter([0, 50], default=30, space='buy') + buy_plusdi = RealParameter(low=0, high=1, default=0.5, space='buy') + sell_rsi = IntParameter(low=50, high=100, default=70, space='sell') + sell_minusdi = DecimalParameter(low=0, high=1, default=0.5001, decimals=3, space='sell', + load=False) + protection_enabled = BooleanParameter(default=True) + protection_cooldown_lookback = IntParameter([0, 50], default=30) + + @property + def protections(self): + prot = [] + if self.protection_enabled.value: + prot.append({ + "method": "CooldownPeriod", + "stop_duration_candles": self.protection_cooldown_lookback.value + }) + return prot + + def informative_pairs(self): + + return [] + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + + # Momentum Indicator + # ------------------------------------ + + # ADX + dataframe['adx'] = ta.ADX(dataframe) + + # MACD + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + # Minus Directional Indicator / Movement + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # Plus Directional Indicator / Movement + dataframe['plus_di'] = ta.PLUS_DI(dataframe) + + # RSI + dataframe['rsi'] = ta.RSI(dataframe) + + # Stoch fast + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + dataframe['fastk'] = stoch_fast['fastk'] + + # Bollinger bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + + # EMA - Exponential Moving Average + dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + + dataframe.loc[ + ( + (dataframe['rsi'] < self.buy_rsi.value) & + (dataframe['fastd'] < 35) & + (dataframe['adx'] > 30) & + (dataframe['plus_di'] > self.buy_plusdi.value) + ) | + ( + (dataframe['adx'] > 65) & + (dataframe['plus_di'] > self.buy_plusdi.value) + ), + 'enter_trade'] = 1 + # TODO-lev: Add short logic + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + ( + (qtpylib.crossed_above(dataframe['rsi'], self.sell_rsi.value)) | + (qtpylib.crossed_above(dataframe['fastd'], 70)) + ) & + (dataframe['adx'] > 10) & + (dataframe['minus_di'] > 0) + ) | + ( + (dataframe['adx'] > 70) & + (dataframe['minus_di'] > self.sell_minusdi.value) + ), + 'exit_trade'] = 1 + + # TODO-lev: Add short logic + return dataframe diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 61ad5b734..b5e5a9eaa 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -581,10 +581,10 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> assert buy_mock.call_count == 1 assert buy_mock.call_count == 1 # only skipped analyze adds buy and sell columns, otherwise it's all mocked - assert 'buy' in ret.columns - assert 'sell' in ret.columns - assert ret['buy'].sum() == 0 - assert ret['sell'].sum() == 0 + assert 'enter_long' in ret.columns + assert 'exit_long' in ret.columns + assert ret['enter_long'].sum() == 0 + assert ret['exit_long'].sum() == 0 assert not log_has('TA Analysis Launched', caplog) assert log_has('Skipping TA Analysis for already analyzed candle', caplog) diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index e7571b798..3e8392596 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -35,7 +35,7 @@ def test_search_all_strategies_no_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=False) assert isinstance(strategies, list) - assert len(strategies) == 4 + assert len(strategies) == 5 assert isinstance(strategies[0], dict) @@ -43,10 +43,10 @@ def test_search_all_strategies_with_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=True) assert isinstance(strategies, list) - assert len(strategies) == 5 + assert len(strategies) == 6 # with enum_failed=True search_all_objects() shall find 2 good strategies # and 1 which fails to load - assert len([x for x in strategies if x['class'] is not None]) == 4 + assert len([x for x in strategies if x['class'] is not None]) == 5 assert len([x for x in strategies if x['class'] is None]) == 1 From 7a5c7e70208659e69de75b10caff828f5a17eb6f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 21 Sep 2021 19:14:14 +0200 Subject: [PATCH 52/64] Update some tests to use StrategyV3 --- freqtrade/strategy/interface.py | 5 ++-- tests/rpc/test_rpc_apiserver.py | 3 ++- tests/strategy/test_interface.py | 16 +++++------ tests/strategy/test_strategy_loading.py | 36 ++++++++++++------------- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 34cf9f749..139729910 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -563,9 +563,8 @@ class IStrategy(ABC, HyperStrategyMixin): message = "" if dataframe is None: message = "No dataframe returned (return statement missing?)." - elif 'buy' not in dataframe: - # TODO-lev: Something? - message = "Buy column not set." + elif 'enter_long' not in dataframe: + message = "enter_long/buy column not set." elif df_len != len(dataframe): message = message_template.format("length") elif df_close != dataframe["close"].iloc[-1]: diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 7c98b2df7..dc29c3027 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1226,7 +1226,8 @@ def test_api_strategies(botclient): 'HyperoptableStrategy', 'InformativeDecoratorTest', 'StrategyTestV2', - 'TestStrategyLegacyV1' + 'StrategyTestV3', + 'TestStrategyLegacyV1', ]} diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index b5e5a9eaa..c09d5209c 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -23,11 +23,11 @@ from freqtrade.strategy.interface import SellCheckTuple from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from tests.conftest import log_has, log_has_re -from .strats.strategy_test_v2 import StrategyTestV2 +from .strats.strategy_test_v3 import StrategyTestV3 # Avoid to reinit the same object again and again -_STRATEGY = StrategyTestV2(config={}) +_STRATEGY = StrategyTestV3(config={}) _STRATEGY.dp = DataProvider({}, None, None) @@ -224,8 +224,8 @@ def test_assert_df_raise(mocker, caplog, ohlcv_history): def test_assert_df(ohlcv_history, caplog): df_len = len(ohlcv_history) - 1 - ohlcv_history.loc[:, 'buy'] = 0 - ohlcv_history.loc[:, 'sell'] = 0 + ohlcv_history.loc[:, 'enter_long'] = 0 + ohlcv_history.loc[:, 'exit_long'] = 0 # Ensure it's running when passed correctly _STRATEGY.assert_df(ohlcv_history, len(ohlcv_history), ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[df_len, 'date']) @@ -248,8 +248,8 @@ def test_assert_df(ohlcv_history, caplog): _STRATEGY.assert_df(None, len(ohlcv_history), ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date']) with pytest.raises(StrategyError, - match="Buy column not set"): - _STRATEGY.assert_df(ohlcv_history.drop('buy', axis=1), len(ohlcv_history), + match="enter_long/buy column not set."): + _STRATEGY.assert_df(ohlcv_history.drop('enter_long', axis=1), len(ohlcv_history), ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date']) _STRATEGY.disable_dataframe_checks = True @@ -528,7 +528,7 @@ def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: advise_sell=sell_mock, ) - strategy = StrategyTestV2({}) + strategy = StrategyTestV3({}) strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'}) assert ind_mock.call_count == 1 assert buy_mock.call_count == 1 @@ -559,7 +559,7 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> advise_sell=sell_mock, ) - strategy = StrategyTestV2({}) + strategy = StrategyTestV3({}) strategy.dp = DataProvider({}, None, None) strategy.process_only_new_candles = True diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 3e8392596..2d4cf7c35 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -74,7 +74,7 @@ def test_load_strategy_base64(result, caplog, default_conf): def test_load_strategy_invalid_directory(result, caplog, default_conf): - default_conf['strategy'] = 'StrategyTestV2' + default_conf['strategy'] = 'StrategyTestV3' extra_dir = Path.cwd() / 'some/path' with pytest.raises(OperationalException): StrategyResolver._load_strategy('StrategyTestV2', config=default_conf, @@ -99,7 +99,7 @@ def test_load_strategy_noname(default_conf): StrategyResolver.load_strategy(default_conf) -def test_strategy(result, default_conf): +def test_strategy_v2(result, default_conf): default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) @@ -129,7 +129,7 @@ def test_strategy(result, default_conf): def test_strategy_override_minimal_roi(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'minimal_roi': { "20": 0.1, "0": 0.5 @@ -146,7 +146,7 @@ def test_strategy_override_minimal_roi(caplog, default_conf): def test_strategy_override_stoploss(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'stoploss': -0.5 }) strategy = StrategyResolver.load_strategy(default_conf) @@ -158,7 +158,7 @@ def test_strategy_override_stoploss(caplog, default_conf): def test_strategy_override_trailing_stop(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'trailing_stop': True }) strategy = StrategyResolver.load_strategy(default_conf) @@ -171,7 +171,7 @@ def test_strategy_override_trailing_stop(caplog, default_conf): def test_strategy_override_trailing_stop_positive(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'trailing_stop_positive': -0.1, 'trailing_stop_positive_offset': -0.2 @@ -191,7 +191,7 @@ def test_strategy_override_timeframe(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'timeframe': 60, 'stake_currency': 'ETH' }) @@ -207,7 +207,7 @@ def test_strategy_override_process_only_new_candles(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'process_only_new_candles': True }) strategy = StrategyResolver.load_strategy(default_conf) @@ -227,7 +227,7 @@ def test_strategy_override_order_types(caplog, default_conf): 'stoploss_on_exchange': True, } default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'order_types': order_types }) strategy = StrategyResolver.load_strategy(default_conf) @@ -241,12 +241,12 @@ def test_strategy_override_order_types(caplog, default_conf): " 'stoploss_on_exchange': True}.", caplog) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'order_types': {'buy': 'market'} }) # Raise error for invalid configuration with pytest.raises(ImportError, - match=r"Impossible to load Strategy 'StrategyTestV2'. " + match=r"Impossible to load Strategy 'StrategyTestV3'. " r"Order-types mapping is incomplete."): StrategyResolver.load_strategy(default_conf) @@ -260,7 +260,7 @@ def test_strategy_override_order_tif(caplog, default_conf): } default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'order_time_in_force': order_time_in_force }) strategy = StrategyResolver.load_strategy(default_conf) @@ -273,12 +273,12 @@ def test_strategy_override_order_tif(caplog, default_conf): " {'buy': 'fok', 'sell': 'gtc'}.", caplog) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'order_time_in_force': {'buy': 'fok'} }) # Raise error for invalid configuration with pytest.raises(ImportError, - match=r"Impossible to load Strategy 'StrategyTestV2'. " + match=r"Impossible to load Strategy 'StrategyTestV3'. " r"Order-time-in-force mapping is incomplete."): StrategyResolver.load_strategy(default_conf) @@ -286,7 +286,7 @@ def test_strategy_override_order_tif(caplog, default_conf): def test_strategy_override_use_sell_signal(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', }) strategy = StrategyResolver.load_strategy(default_conf) assert strategy.use_sell_signal @@ -296,7 +296,7 @@ def test_strategy_override_use_sell_signal(caplog, default_conf): assert default_conf['use_sell_signal'] default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'use_sell_signal': False, }) strategy = StrategyResolver.load_strategy(default_conf) @@ -309,7 +309,7 @@ def test_strategy_override_use_sell_signal(caplog, default_conf): def test_strategy_override_use_sell_profit_only(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', }) strategy = StrategyResolver.load_strategy(default_conf) assert not strategy.sell_profit_only @@ -319,7 +319,7 @@ def test_strategy_override_use_sell_profit_only(caplog, default_conf): assert not default_conf['sell_profit_only'] default_conf.update({ - 'strategy': 'StrategyTestV2', + 'strategy': 'StrategyTestV3', 'sell_profit_only': True, }) strategy = StrategyResolver.load_strategy(default_conf) From c791b95405118429972cf62652f3bbf13eb770c6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 21 Sep 2021 20:18:14 +0200 Subject: [PATCH 53/64] Use new TestStrategy (V3) by default in tests --- build_helpers/publish_docker_arm64.sh | 2 +- build_helpers/publish_docker_multi.sh | 2 +- tests/commands/test_commands.py | 10 ++--- tests/conftest.py | 4 +- tests/conftest_trades.py | 8 ++-- tests/data/test_btanalysis.py | 6 +-- tests/data/test_history.py | 9 +++-- tests/optimize/test_backtesting.py | 35 ++++++++-------- tests/optimize/test_edge_cli.py | 8 ++-- tests/optimize/test_hyperopt.py | 4 +- tests/optimize/test_hyperopt_tools.py | 22 +++++----- tests/optimize/test_optimize_reports.py | 3 +- tests/rpc/test_rpc_apiserver.py | 32 +++++++-------- tests/rpc/test_rpc_telegram.py | 8 ++-- .../strats/informative_decorator_strategy.py | 3 +- tests/strategy/strats/strategy_test_v3.py | 24 ++++++----- tests/strategy/test_default_strategy.py | 16 ++++---- tests/strategy/test_interface.py | 11 ----- tests/strategy/test_strategy_loading.py | 40 +++++++++---------- tests/test_arguments.py | 3 +- tests/test_configuration.py | 9 +++-- 21 files changed, 127 insertions(+), 132 deletions(-) diff --git a/build_helpers/publish_docker_arm64.sh b/build_helpers/publish_docker_arm64.sh index 1ad8074d4..70f99e54b 100755 --- a/build_helpers/publish_docker_arm64.sh +++ b/build_helpers/publish_docker_arm64.sh @@ -42,7 +42,7 @@ docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_I docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM # Run backtest -docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV2 +docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3 if [ $? -ne 0 ]; then echo "failed running backtest" diff --git a/build_helpers/publish_docker_multi.sh b/build_helpers/publish_docker_multi.sh index dd6ac841e..fd5f0ef93 100755 --- a/build_helpers/publish_docker_multi.sh +++ b/build_helpers/publish_docker_multi.sh @@ -53,7 +53,7 @@ docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT # Run backtest -docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV2 +docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3 if [ $? -ne 0 ]; then echo "failed running backtest" diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 135510b38..a1d89d7d3 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -19,8 +19,8 @@ from freqtrade.commands.deploy_commands import (clean_ui_subdir, download_and_in from freqtrade.configuration import setup_utils_configuration from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException -from tests.conftest import (create_mock_trades, get_args, log_has, log_has_re, patch_exchange, - patched_configuration_load_config_file) +from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_args, log_has, + log_has_re, patch_exchange, patched_configuration_load_config_file) from tests.conftest_trades import MOCK_TRADE_COUNT @@ -774,7 +774,7 @@ def test_start_list_strategies(mocker, caplog, capsys): captured = capsys.readouterr() assert "TestStrategyLegacyV1" in captured.out assert "legacy_strategy_v1.py" not in captured.out - assert "StrategyTestV2" in captured.out + assert CURRENT_TEST_STRATEGY in captured.out # Test regular output args = [ @@ -789,7 +789,7 @@ def test_start_list_strategies(mocker, caplog, capsys): captured = capsys.readouterr() assert "TestStrategyLegacyV1" in captured.out assert "legacy_strategy_v1.py" in captured.out - assert "StrategyTestV2" in captured.out + assert CURRENT_TEST_STRATEGY in captured.out # Test color output args = [ @@ -803,7 +803,7 @@ def test_start_list_strategies(mocker, caplog, capsys): captured = capsys.readouterr() assert "TestStrategyLegacyV1" in captured.out assert "legacy_strategy_v1.py" in captured.out - assert "StrategyTestV2" in captured.out + assert CURRENT_TEST_STRATEGY in captured.out assert "LOAD FAILED" in captured.out diff --git a/tests/conftest.py b/tests/conftest.py index d54e3a9a1..a9fd42a05 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -35,6 +35,8 @@ logging.getLogger('').setLevel(logging.INFO) # Do not mask numpy errors as warnings that no one read, raise the exсeption np.seterr(all='raise') +CURRENT_TEST_STRATEGY = 'StrategyTestV3' + def pytest_addoption(parser): parser.addoption('--longrun', action='store_true', dest="longrun", @@ -406,7 +408,7 @@ def get_default_conf(testdatadir): "user_data_dir": Path("user_data"), "verbosity": 3, "strategy_path": str(Path(__file__).parent / "strategy" / "strats"), - "strategy": "StrategyTestV2", + "strategy": CURRENT_TEST_STRATEGY, "disableparamexport": True, "internals": {}, "export": "none", diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 700cd3fa7..cf3c970f6 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -33,7 +33,7 @@ def mock_trade_1(fee): open_rate=0.123, exchange='binance', open_order_id='dry_run_buy_12345', - strategy='StrategyTestV2', + strategy='StrategyTestV3', timeframe=5, ) o = Order.parse_from_ccxt_object(mock_order_1(), 'ETH/BTC', 'buy') @@ -87,7 +87,7 @@ def mock_trade_2(fee): exchange='binance', is_open=False, open_order_id='dry_run_sell_12345', - strategy='StrategyTestV2', + strategy='StrategyTestV3', timeframe=5, sell_reason='sell_signal', open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), @@ -146,7 +146,7 @@ def mock_trade_3(fee): close_profit_abs=0.000155, exchange='binance', is_open=False, - strategy='StrategyTestV2', + strategy='StrategyTestV3', timeframe=5, sell_reason='roi', open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), @@ -189,7 +189,7 @@ def mock_trade_4(fee): open_rate=0.123, exchange='binance', open_order_id='prod_buy_12345', - strategy='StrategyTestV2', + strategy='StrategyTestV3', timeframe=5, ) o = Order.parse_from_ccxt_object(mock_order_4(), 'ETC/BTC', 'buy') diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 1dcd04a80..e7b8c5b2f 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -16,7 +16,7 @@ from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, BT_DATA_COLUMNS_MID, BT_ get_latest_hyperopt_file, load_backtest_data, load_trades, load_trades_from_db) from freqtrade.data.history import load_data, load_pair_history -from tests.conftest import create_mock_trades +from tests.conftest import CURRENT_TEST_STRATEGY, create_mock_trades from tests.conftest_trades import MOCK_TRADE_COUNT @@ -128,7 +128,7 @@ def test_load_trades_from_db(default_conf, fee, mocker): for col in BT_DATA_COLUMNS: if col not in ['index', 'open_at_end']: assert col in trades.columns - trades = load_trades_from_db(db_url=default_conf['db_url'], strategy='StrategyTestV2') + trades = load_trades_from_db(db_url=default_conf['db_url'], strategy=CURRENT_TEST_STRATEGY) assert len(trades) == 4 trades = load_trades_from_db(db_url=default_conf['db_url'], strategy='NoneStrategy') assert len(trades) == 0 @@ -186,7 +186,7 @@ def test_load_trades(default_conf, mocker): db_url=default_conf.get('db_url'), exportfilename=default_conf.get('exportfilename'), no_trades=False, - strategy="StrategyTestV2", + strategy=CURRENT_TEST_STRATEGY, ) assert db_mock.call_count == 1 diff --git a/tests/data/test_history.py b/tests/data/test_history.py index 575a590e7..73ceabbbf 100644 --- a/tests/data/test_history.py +++ b/tests/data/test_history.py @@ -26,7 +26,8 @@ from freqtrade.data.history.jsondatahandler import JsonDataHandler, JsonGzDataHa from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import file_dump_json from freqtrade.resolvers import StrategyResolver -from tests.conftest import get_patched_exchange, log_has, log_has_re, patch_exchange +from tests.conftest import (CURRENT_TEST_STRATEGY, get_patched_exchange, log_has, log_has_re, + patch_exchange) # Change this if modifying UNITTEST/BTC testdatafile @@ -380,7 +381,7 @@ def test_file_dump_json_tofile(testdatadir) -> None: def test_get_timerange(default_conf, mocker, testdatadir) -> None: patch_exchange(mocker) - default_conf.update({'strategy': 'StrategyTestV2'}) + default_conf.update({'strategy': CURRENT_TEST_STRATEGY}) strategy = StrategyResolver.load_strategy(default_conf) data = strategy.advise_all_indicators( @@ -398,7 +399,7 @@ def test_get_timerange(default_conf, mocker, testdatadir) -> None: def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) -> None: patch_exchange(mocker) - default_conf.update({'strategy': 'StrategyTestV2'}) + default_conf.update({'strategy': CURRENT_TEST_STRATEGY}) strategy = StrategyResolver.load_strategy(default_conf) data = strategy.advise_all_indicators( @@ -422,7 +423,7 @@ def test_validate_backtest_data_warn(default_conf, mocker, caplog, testdatadir) def test_validate_backtest_data(default_conf, mocker, caplog, testdatadir) -> None: patch_exchange(mocker) - default_conf.update({'strategy': 'StrategyTestV2'}) + default_conf.update({'strategy': CURRENT_TEST_STRATEGY}) strategy = StrategyResolver.load_strategy(default_conf) timerange = TimeRange('index', 'index', 200, 250) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index d2ccef9db..0d31846d5 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -22,7 +22,7 @@ from freqtrade.exceptions import DependencyException, OperationalException from freqtrade.optimize.backtesting import Backtesting from freqtrade.persistence import LocalTrade from freqtrade.resolvers import StrategyResolver -from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, +from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) @@ -159,7 +159,7 @@ def test_setup_optimize_configuration_without_arguments(mocker, default_conf, ca args = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--export', 'none' ] @@ -194,7 +194,7 @@ def test_setup_bt_configuration_with_arguments(mocker, default_conf, caplog) -> args = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--datadir', '/foo/bar', '--timeframe', '1m', '--enable-position-stacking', @@ -244,7 +244,7 @@ def test_setup_optimize_configuration_stake_amount(mocker, default_conf, caplog) args = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--stake-amount', '1', '--starting-balance', '2' ] @@ -255,7 +255,7 @@ def test_setup_optimize_configuration_stake_amount(mocker, default_conf, caplog) args = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--stake-amount', '1', '--starting-balance', '0.5' ] @@ -273,7 +273,7 @@ def test_start(mocker, fee, default_conf, caplog) -> None: args = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, ] pargs = get_args(args) start_backtesting(pargs) @@ -306,7 +306,7 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None: def test_backtesting_init_no_timeframe(mocker, default_conf, caplog) -> None: patch_exchange(mocker) del default_conf['timeframe'] - default_conf['strategy_list'] = ['StrategyTestV2', + default_conf['strategy_list'] = [CURRENT_TEST_STRATEGY, 'SampleStrategy'] mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) @@ -344,7 +344,6 @@ def test_data_to_dataframe_bt(default_conf, mocker, testdatadir) -> None: assert len(processed['UNITTEST/BTC']) == 102 # Load strategy to compare the result between Backtesting function and strategy are the same - default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) processed2 = strategy.advise_all_indicators(data) @@ -486,7 +485,7 @@ def test_backtesting_pairlist_list(default_conf, mocker, caplog, testdatadir, ti Backtesting(default_conf) # Multiple strategies - default_conf['strategy_list'] = ['StrategyTestV2', 'TestStrategyLegacyV1'] + default_conf['strategy_list'] = [CURRENT_TEST_STRATEGY, 'TestStrategyLegacyV1'] with pytest.raises(OperationalException, match='PrecisionFilter not allowed for backtesting multiple strategies.'): Backtesting(default_conf) @@ -803,7 +802,7 @@ def test_backtest_pricecontours(default_conf, fee, mocker, testdatadir, def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): - # Override the default buy trend function in our StrategyTestV2 + # Override the default buy trend function in our StrategyTest def fun(dataframe=None, pair=None): buy_value = 1 sell_value = 1 @@ -819,7 +818,7 @@ def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): def test_backtest_only_sell(mocker, default_conf, testdatadir): - # Override the default buy trend function in our StrategyTestV2 + # Override the default buy trend function in our StrategyTest def fun(dataframe=None, pair=None): buy_value = 0 sell_value = 1 @@ -948,7 +947,7 @@ def test_backtest_start_timerange(default_conf, mocker, caplog, testdatadir): args = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--datadir', str(testdatadir), '--timeframe', '1m', '--timerange', '1510694220-1510700340', @@ -1019,7 +1018,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): '--enable-position-stacking', '--disable-max-market-positions', '--strategy-list', - 'StrategyTestV2', + CURRENT_TEST_STRATEGY, 'TestStrategyLegacyV1', ] args = get_args(args) @@ -1042,7 +1041,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): 'Backtesting with data from 2017-11-14 21:17:00 ' 'up to 2017-11-14 22:58:00 (0 days).', 'Parameter --enable-position-stacking detected ...', - 'Running backtesting for Strategy StrategyTestV2', + f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}', 'Running backtesting for Strategy TestStrategyLegacyV1', ] @@ -1123,7 +1122,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat '--enable-position-stacking', '--disable-max-market-positions', '--strategy-list', - 'StrategyTestV2', + CURRENT_TEST_STRATEGY, 'TestStrategyLegacyV1', ] args = get_args(args) @@ -1140,7 +1139,7 @@ def test_backtest_start_multi_strat_nomock(default_conf, mocker, caplog, testdat 'Backtesting with data from 2017-11-14 21:17:00 ' 'up to 2017-11-14 22:58:00 (0 days).', 'Parameter --enable-position-stacking detected ...', - 'Running backtesting for Strategy StrategyTestV2', + f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}', 'Running backtesting for Strategy TestStrategyLegacyV1', ] @@ -1228,7 +1227,7 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker, '--timeframe', '5m', '--timeframe-detail', '1m', '--strategy-list', - 'StrategyTestV2' + CURRENT_TEST_STRATEGY ] args = get_args(args) start_backtesting(args) @@ -1242,7 +1241,7 @@ def test_backtest_start_multi_strat_nomock_detail(default_conf, mocker, 'up to 2019-10-13 11:10:00 (2 days).', 'Backtesting with data from 2019-10-11 01:40:00 ' 'up to 2019-10-13 11:10:00 (2 days).', - 'Running backtesting for Strategy StrategyTestV2', + f'Running backtesting for Strategy {CURRENT_TEST_STRATEGY}', ] for line in exists: diff --git a/tests/optimize/test_edge_cli.py b/tests/optimize/test_edge_cli.py index 18d5f1c76..e091c9c53 100644 --- a/tests/optimize/test_edge_cli.py +++ b/tests/optimize/test_edge_cli.py @@ -6,7 +6,7 @@ from unittest.mock import MagicMock from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_edge from freqtrade.enums import RunMode from freqtrade.optimize.edge_cli import EdgeCli -from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, +from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) @@ -16,7 +16,7 @@ def test_setup_optimize_configuration_without_arguments(mocker, default_conf, ca args = [ 'edge', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, ] config = setup_optimize_configuration(get_args(args), RunMode.EDGE) @@ -46,7 +46,7 @@ def test_setup_edge_configuration_with_arguments(mocker, edge_conf, caplog) -> N args = [ 'edge', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--datadir', '/foo/bar', '--timeframe', '1m', '--timerange', ':100', @@ -80,7 +80,7 @@ def test_start(mocker, fee, edge_conf, caplog) -> None: args = [ 'edge', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, ] pargs = get_args(args) start_edge(pargs) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 27496a1fc..a83277dc6 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -18,7 +18,7 @@ from freqtrade.optimize.hyperopt_tools import HyperoptTools from freqtrade.optimize.optimize_reports import generate_strategy_stats from freqtrade.optimize.space import SKDecimal from freqtrade.strategy.hyper import IntParameter -from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, +from tests.conftest import (CURRENT_TEST_STRATEGY, get_args, log_has, log_has_re, patch_exchange, patched_configuration_load_config_file) @@ -125,7 +125,7 @@ def test_setup_hyperopt_configuration_stake_amount(mocker, default_conf) -> None args = [ 'hyperopt', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--stake-amount', '1', '--starting-balance', '0.5' ] diff --git a/tests/optimize/test_hyperopt_tools.py b/tests/optimize/test_hyperopt_tools.py index 9c2b2e8fc..5a46f238b 100644 --- a/tests/optimize/test_hyperopt_tools.py +++ b/tests/optimize/test_hyperopt_tools.py @@ -10,7 +10,7 @@ import rapidjson from freqtrade.constants import FTHYPT_FILEVERSION from freqtrade.exceptions import OperationalException from freqtrade.optimize.hyperopt_tools import HyperoptTools, hyperopt_serializer -from tests.conftest import log_has +from tests.conftest import CURRENT_TEST_STRATEGY, log_has # Functions for recurrent object patching @@ -167,9 +167,9 @@ def test__pprint_dict(): def test_get_strategy_filename(default_conf): - x = HyperoptTools.get_strategy_filename(default_conf, 'StrategyTestV2') + x = HyperoptTools.get_strategy_filename(default_conf, CURRENT_TEST_STRATEGY) assert isinstance(x, Path) - assert x == Path(__file__).parents[1] / 'strategy/strats/strategy_test_v2.py' + assert x == Path(__file__).parents[1] / 'strategy/strats/strategy_test_v3.py' x = HyperoptTools.get_strategy_filename(default_conf, 'NonExistingStrategy') assert x is None @@ -177,7 +177,7 @@ def test_get_strategy_filename(default_conf): def test_export_params(tmpdir): - filename = Path(tmpdir) / "StrategyTestV2.json" + filename = Path(tmpdir) / f"{CURRENT_TEST_STRATEGY}.json" assert not filename.is_file() params = { "params_details": { @@ -205,12 +205,12 @@ def test_export_params(tmpdir): } } - HyperoptTools.export_params(params, "StrategyTestV2", filename) + HyperoptTools.export_params(params, CURRENT_TEST_STRATEGY, filename) assert filename.is_file() content = rapidjson.load(filename.open('r')) - assert content['strategy_name'] == 'StrategyTestV2' + assert content['strategy_name'] == CURRENT_TEST_STRATEGY assert 'params' in content assert "buy" in content["params"] assert "sell" in content["params"] @@ -223,7 +223,7 @@ def test_try_export_params(default_conf, tmpdir, caplog, mocker): default_conf['disableparamexport'] = False export_mock = mocker.patch("freqtrade.optimize.hyperopt_tools.HyperoptTools.export_params") - filename = Path(tmpdir) / "StrategyTestV2.json" + filename = Path(tmpdir) / f"{CURRENT_TEST_STRATEGY}.json" assert not filename.is_file() params = { "params_details": { @@ -252,17 +252,17 @@ def test_try_export_params(default_conf, tmpdir, caplog, mocker): FTHYPT_FILEVERSION: 2, } - HyperoptTools.try_export_params(default_conf, "StrategyTestV222", params) + HyperoptTools.try_export_params(default_conf, "StrategyTestVXXX", params) assert log_has("Strategy not found, not exporting parameter file.", caplog) assert export_mock.call_count == 0 caplog.clear() - HyperoptTools.try_export_params(default_conf, "StrategyTestV2", params) + HyperoptTools.try_export_params(default_conf, CURRENT_TEST_STRATEGY, params) assert export_mock.call_count == 1 - assert export_mock.call_args_list[0][0][1] == 'StrategyTestV2' - assert export_mock.call_args_list[0][0][2].name == 'strategy_test_v2.json' + assert export_mock.call_args_list[0][0][1] == CURRENT_TEST_STRATEGY + assert export_mock.call_args_list[0][0][2].name == 'strategy_test_v3.json' def test_params_print(capsys): diff --git a/tests/optimize/test_optimize_reports.py b/tests/optimize/test_optimize_reports.py index 83caefd2d..b8cf0c682 100644 --- a/tests/optimize/test_optimize_reports.py +++ b/tests/optimize/test_optimize_reports.py @@ -21,6 +21,7 @@ from freqtrade.optimize.optimize_reports import (generate_backtest_stats, genera text_table_bt_results, text_table_sell_reason, text_table_strategy) from freqtrade.resolvers.strategy_resolver import StrategyResolver +from tests.conftest import CURRENT_TEST_STRATEGY from tests.data.test_history import _backup_file, _clean_test_file @@ -52,7 +53,7 @@ def test_text_table_bt_results(): def test_generate_backtest_stats(default_conf, testdatadir, tmpdir): - default_conf.update({'strategy': 'StrategyTestV2'}) + default_conf.update({'strategy': CURRENT_TEST_STRATEGY}) StrategyResolver.load_strategy(default_conf) results = {'DefStrat': { diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index dc29c3027..afce87b88 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -24,8 +24,8 @@ from freqtrade.rpc import RPC from freqtrade.rpc.api_server import ApiServer from freqtrade.rpc.api_server.api_auth import create_token, get_user_from_token from freqtrade.rpc.api_server.uvicorn_threaded import UvicornServer -from tests.conftest import (create_mock_trades, get_mock_coro, get_patched_freqtradebot, log_has, - log_has_re, patch_get_signal) +from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_mock_coro, + get_patched_freqtradebot, log_has, log_has_re, patch_get_signal) BASE_URI = "/api/v1" @@ -885,7 +885,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'open_trade_value': 15.1668225, 'sell_reason': None, 'sell_order_status': None, - 'strategy': 'StrategyTestV2', + 'strategy': CURRENT_TEST_STRATEGY, 'buy_tag': None, 'timeframe': 5, 'exchange': 'binance', @@ -990,7 +990,7 @@ def test_api_forcebuy(botclient, mocker, fee): close_rate=0.265441, id=22, timeframe=5, - strategy="StrategyTestV2" + strategy=CURRENT_TEST_STRATEGY )) mocker.patch("freqtrade.rpc.RPC._rpc_forcebuy", fbuy_mock) @@ -1040,7 +1040,7 @@ def test_api_forcebuy(botclient, mocker, fee): 'open_trade_value': 0.24605460, 'sell_reason': None, 'sell_order_status': None, - 'strategy': 'StrategyTestV2', + 'strategy': CURRENT_TEST_STRATEGY, 'buy_tag': None, 'timeframe': 5, 'exchange': 'binance', @@ -1107,7 +1107,7 @@ def test_api_pair_candles(botclient, ohlcv_history): f"{BASE_URI}/pair_candles?limit={amount}&pair=XRP%2FBTC&timeframe={timeframe}") assert_response(rc) assert 'strategy' in rc.json() - assert rc.json()['strategy'] == 'StrategyTestV2' + assert rc.json()['strategy'] == CURRENT_TEST_STRATEGY assert 'columns' in rc.json() assert 'data_start_ts' in rc.json() assert 'data_start' in rc.json() @@ -1145,19 +1145,19 @@ def test_api_pair_history(botclient, ohlcv_history): # No pair rc = client_get(client, f"{BASE_URI}/pair_history?timeframe={timeframe}" - "&timerange=20180111-20180112&strategy=StrategyTestV2") + f"&timerange=20180111-20180112&strategy={CURRENT_TEST_STRATEGY}") assert_response(rc, 422) # No Timeframe rc = client_get(client, f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC" - "&timerange=20180111-20180112&strategy=StrategyTestV2") + f"&timerange=20180111-20180112&strategy={CURRENT_TEST_STRATEGY}") assert_response(rc, 422) # No timerange rc = client_get(client, f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" - "&strategy=StrategyTestV2") + f"&strategy={CURRENT_TEST_STRATEGY}") assert_response(rc, 422) # No strategy @@ -1169,14 +1169,14 @@ def test_api_pair_history(botclient, ohlcv_history): # Working rc = client_get(client, f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" - "&timerange=20180111-20180112&strategy=StrategyTestV2") + f"&timerange=20180111-20180112&strategy={CURRENT_TEST_STRATEGY}") assert_response(rc, 200) assert rc.json()['length'] == 289 assert len(rc.json()['data']) == rc.json()['length'] assert 'columns' in rc.json() assert 'data' in rc.json() assert rc.json()['pair'] == 'UNITTEST/BTC' - assert rc.json()['strategy'] == 'StrategyTestV2' + assert rc.json()['strategy'] == CURRENT_TEST_STRATEGY assert rc.json()['data_start'] == '2018-01-11 00:00:00+00:00' assert rc.json()['data_start_ts'] == 1515628800000 assert rc.json()['data_stop'] == '2018-01-12 00:00:00+00:00' @@ -1185,7 +1185,7 @@ def test_api_pair_history(botclient, ohlcv_history): # No data found rc = client_get(client, f"{BASE_URI}/pair_history?pair=UNITTEST%2FBTC&timeframe={timeframe}" - "&timerange=20200111-20200112&strategy=StrategyTestV2") + f"&timerange=20200111-20200112&strategy={CURRENT_TEST_STRATEGY}") assert_response(rc, 502) assert rc.json()['error'] == ("Error querying /api/v1/pair_history: " "No data for UNITTEST/BTC, 5m in 20200111-20200112 found.") @@ -1234,12 +1234,12 @@ def test_api_strategies(botclient): def test_api_strategy(botclient): ftbot, client = botclient - rc = client_get(client, f"{BASE_URI}/strategy/StrategyTestV2") + rc = client_get(client, f"{BASE_URI}/strategy/{CURRENT_TEST_STRATEGY}") assert_response(rc) - assert rc.json()['strategy'] == 'StrategyTestV2' + assert rc.json()['strategy'] == CURRENT_TEST_STRATEGY - data = (Path(__file__).parents[1] / "strategy/strats/strategy_test_v2.py").read_text() + data = (Path(__file__).parents[1] / "strategy/strats/strategy_test_v3.py").read_text() assert rc.json()['code'] == data rc = client_get(client, f"{BASE_URI}/strategy/NoStrat") @@ -1296,7 +1296,7 @@ def test_api_backtesting(botclient, mocker, fee, caplog): # start backtesting data = { - "strategy": "StrategyTestV2", + "strategy": CURRENT_TEST_STRATEGY, "timeframe": "5m", "timerange": "20180110-20180111", "max_open_trades": 3, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 21f1cd000..23ccadca0 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -25,8 +25,8 @@ from freqtrade.loggers import setup_logging from freqtrade.persistence import PairLocks, Trade from freqtrade.rpc import RPC from freqtrade.rpc.telegram import Telegram, authorized_only -from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, log_has_re, - patch_exchange, patch_get_signal, patch_whitelist) +from tests.conftest import (CURRENT_TEST_STRATEGY, create_mock_trades, get_patched_freqtradebot, + log_has, log_has_re, patch_exchange, patch_get_signal, patch_whitelist) class DummyCls(Telegram): @@ -1238,7 +1238,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None: assert msg_mock.call_count == 1 assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0] - assert '*Strategy:* `StrategyTestV2`' in msg_mock.call_args_list[0][0][0] + assert f'*Strategy:* `{CURRENT_TEST_STRATEGY}`' 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() @@ -1247,7 +1247,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None: assert msg_mock.call_count == 1 assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0] - assert '*Strategy:* `StrategyTestV2`' in msg_mock.call_args_list[0][0][0] + assert f'*Strategy:* `{CURRENT_TEST_STRATEGY}`' in msg_mock.call_args_list[0][0][0] assert '*Initial Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] diff --git a/tests/strategy/strats/informative_decorator_strategy.py b/tests/strategy/strats/informative_decorator_strategy.py index 4dd2d84eb..68f8651c2 100644 --- a/tests/strategy/strats/informative_decorator_strategy.py +++ b/tests/strategy/strats/informative_decorator_strategy.py @@ -2,8 +2,7 @@ from pandas import DataFrame -from freqtrade.strategy import informative, merge_informative_pair -from freqtrade.strategy import IStrategy +from freqtrade.strategy import IStrategy, informative, merge_informative_pair class InformativeDecoratorTest(IStrategy): diff --git a/tests/strategy/strats/strategy_test_v3.py b/tests/strategy/strats/strategy_test_v3.py index 347fa43bb..db294d4e9 100644 --- a/tests/strategy/strats/strategy_test_v3.py +++ b/tests/strategy/strats/strategy_test_v3.py @@ -68,15 +68,17 @@ class StrategyTestV3(IStrategy): protection_enabled = BooleanParameter(default=True) protection_cooldown_lookback = IntParameter([0, 50], default=30) - @property - def protections(self): - prot = [] - if self.protection_enabled.value: - prot.append({ - "method": "CooldownPeriod", - "stop_duration_candles": self.protection_cooldown_lookback.value - }) - return prot + # TODO-lev: Can we make this work with protection tests? + # TODO-lev: (Would replace HyperoptableStrategy implicitly ... ) + # @property + # def protections(self): + # prot = [] + # if self.protection_enabled.value: + # prot.append({ + # "method": "CooldownPeriod", + # "stop_duration_candles": self.protection_cooldown_lookback.value + # }) + # return prot def informative_pairs(self): @@ -134,7 +136,7 @@ class StrategyTestV3(IStrategy): (dataframe['adx'] > 65) & (dataframe['plus_di'] > self.buy_plusdi.value) ), - 'enter_trade'] = 1 + 'enter_long'] = 1 # TODO-lev: Add short logic return dataframe @@ -153,7 +155,7 @@ class StrategyTestV3(IStrategy): (dataframe['adx'] > 70) & (dataframe['minus_di'] > self.sell_minusdi.value) ), - 'exit_trade'] = 1 + 'exit_long'] = 1 # TODO-lev: Add short logic return dataframe diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index 06688619b..02597b672 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -4,20 +4,20 @@ from pandas import DataFrame from freqtrade.persistence.models import Trade -from .strats.strategy_test_v2 import StrategyTestV2 +from .strats.strategy_test_v3 import StrategyTestV3 def test_strategy_test_v2_structure(): - assert hasattr(StrategyTestV2, 'minimal_roi') - assert hasattr(StrategyTestV2, 'stoploss') - assert hasattr(StrategyTestV2, 'timeframe') - assert hasattr(StrategyTestV2, 'populate_indicators') - assert hasattr(StrategyTestV2, 'populate_buy_trend') - assert hasattr(StrategyTestV2, 'populate_sell_trend') + assert hasattr(StrategyTestV3, 'minimal_roi') + assert hasattr(StrategyTestV3, 'stoploss') + assert hasattr(StrategyTestV3, 'timeframe') + assert hasattr(StrategyTestV3, 'populate_indicators') + assert hasattr(StrategyTestV3, 'populate_buy_trend') + assert hasattr(StrategyTestV3, 'populate_sell_trend') def test_strategy_test_v2(result, fee): - strategy = StrategyTestV2({}) + strategy = StrategyTestV3({}) metadata = {'pair': 'ETH/BTC'} assert type(strategy.minimal_roi) is dict diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index c09d5209c..65e7da9db 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -177,7 +177,6 @@ def test_get_signal_no_sell_column(default_conf, mocker, caplog, ohlcv_history): def test_ignore_expired_candle(default_conf): - default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) strategy.ignore_buying_expired_candle_after = 60 @@ -262,7 +261,6 @@ def test_assert_df(ohlcv_history, caplog): def test_advise_all_indicators(default_conf, testdatadir) -> None: - default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) timerange = TimeRange.parse_timerange('1510694220-1510700340') @@ -273,7 +271,6 @@ def test_advise_all_indicators(default_conf, testdatadir) -> None: def test_advise_all_indicators_copy(mocker, default_conf, testdatadir) -> None: - default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) aimock = mocker.patch('freqtrade.strategy.interface.IStrategy.advise_indicators') timerange = TimeRange.parse_timerange('1510694220-1510700340') @@ -291,7 +288,6 @@ def test_min_roi_reached(default_conf, fee) -> None: min_roi_list = [{20: 0.05, 55: 0.01, 0: 0.1}, {0: 0.1, 20: 0.05, 55: 0.01}] for roi in min_roi_list: - default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) strategy.minimal_roi = roi trade = Trade( @@ -330,7 +326,6 @@ def test_min_roi_reached2(default_conf, fee) -> None: }, ] for roi in min_roi_list: - default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) strategy.minimal_roi = roi trade = Trade( @@ -365,7 +360,6 @@ def test_min_roi_reached3(default_conf, fee) -> None: 30: 0.05, 55: 0.30, } - default_conf.update({'strategy': 'StrategyTestV2'}) strategy = StrategyResolver.load_strategy(default_conf) strategy.minimal_roi = min_roi trade = Trade( @@ -418,8 +412,6 @@ def test_min_roi_reached3(default_conf, fee) -> None: def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, trailing, custom, profit2, adjusted2, expected2, custom_stop) -> None: - default_conf.update({'strategy': 'StrategyTestV2'}) - strategy = StrategyResolver.load_strategy(default_conf) trade = Trade( pair='ETH/BTC', @@ -466,8 +458,6 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili def test_custom_sell(default_conf, fee, caplog) -> None: - default_conf.update({'strategy': 'StrategyTestV2'}) - strategy = StrategyResolver.load_strategy(default_conf) trade = Trade( pair='ETH/BTC', @@ -591,7 +581,6 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> @pytest.mark.usefixtures("init_persistence") def test_is_pair_locked(default_conf): - default_conf.update({'strategy': 'StrategyTestV2'}) PairLocks.timeframe = default_conf['timeframe'] PairLocks.use_db = True strategy = StrategyResolver.load_strategy(default_conf) diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 2d4cf7c35..b3e79cd27 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -10,7 +10,7 @@ from pandas import DataFrame 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 +from tests.conftest import CURRENT_TEST_STRATEGY, log_has, log_has_re def test_search_strategy(): @@ -18,7 +18,7 @@ def test_search_strategy(): s, _ = StrategyResolver._search_object( directory=default_location, - object_name='StrategyTestV2', + object_name=CURRENT_TEST_STRATEGY, add_source=True, ) assert issubclass(s, IStrategy) @@ -77,7 +77,7 @@ def test_load_strategy_invalid_directory(result, caplog, default_conf): default_conf['strategy'] = 'StrategyTestV3' extra_dir = Path.cwd() / 'some/path' with pytest.raises(OperationalException): - StrategyResolver._load_strategy('StrategyTestV2', config=default_conf, + StrategyResolver._load_strategy(CURRENT_TEST_STRATEGY, config=default_conf, extra_dir=extra_dir) assert log_has_re(r'Path .*' + r'some.*path.*' + r'.* does not exist', caplog) @@ -129,7 +129,7 @@ def test_strategy_v2(result, default_conf): def test_strategy_override_minimal_roi(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'minimal_roi': { "20": 0.1, "0": 0.5 @@ -146,7 +146,7 @@ def test_strategy_override_minimal_roi(caplog, default_conf): def test_strategy_override_stoploss(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'stoploss': -0.5 }) strategy = StrategyResolver.load_strategy(default_conf) @@ -158,7 +158,7 @@ def test_strategy_override_stoploss(caplog, default_conf): def test_strategy_override_trailing_stop(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'trailing_stop': True }) strategy = StrategyResolver.load_strategy(default_conf) @@ -171,7 +171,7 @@ def test_strategy_override_trailing_stop(caplog, default_conf): def test_strategy_override_trailing_stop_positive(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'trailing_stop_positive': -0.1, 'trailing_stop_positive_offset': -0.2 @@ -191,7 +191,7 @@ def test_strategy_override_timeframe(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'timeframe': 60, 'stake_currency': 'ETH' }) @@ -207,7 +207,7 @@ def test_strategy_override_process_only_new_candles(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'process_only_new_candles': True }) strategy = StrategyResolver.load_strategy(default_conf) @@ -227,7 +227,7 @@ def test_strategy_override_order_types(caplog, default_conf): 'stoploss_on_exchange': True, } default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'order_types': order_types }) strategy = StrategyResolver.load_strategy(default_conf) @@ -241,12 +241,12 @@ def test_strategy_override_order_types(caplog, default_conf): " 'stoploss_on_exchange': True}.", caplog) default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'order_types': {'buy': 'market'} }) # Raise error for invalid configuration with pytest.raises(ImportError, - match=r"Impossible to load Strategy 'StrategyTestV3'. " + match=r"Impossible to load Strategy '" + CURRENT_TEST_STRATEGY + "'. " r"Order-types mapping is incomplete."): StrategyResolver.load_strategy(default_conf) @@ -260,7 +260,7 @@ def test_strategy_override_order_tif(caplog, default_conf): } default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'order_time_in_force': order_time_in_force }) strategy = StrategyResolver.load_strategy(default_conf) @@ -273,20 +273,20 @@ def test_strategy_override_order_tif(caplog, default_conf): " {'buy': 'fok', 'sell': 'gtc'}.", caplog) default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'order_time_in_force': {'buy': 'fok'} }) # Raise error for invalid configuration with pytest.raises(ImportError, - match=r"Impossible to load Strategy 'StrategyTestV3'. " - r"Order-time-in-force mapping is incomplete."): + match=f"Impossible to load Strategy '{CURRENT_TEST_STRATEGY}'. " + "Order-time-in-force mapping is incomplete."): StrategyResolver.load_strategy(default_conf) def test_strategy_override_use_sell_signal(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, }) strategy = StrategyResolver.load_strategy(default_conf) assert strategy.use_sell_signal @@ -296,7 +296,7 @@ def test_strategy_override_use_sell_signal(caplog, default_conf): assert default_conf['use_sell_signal'] default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'use_sell_signal': False, }) strategy = StrategyResolver.load_strategy(default_conf) @@ -309,7 +309,7 @@ def test_strategy_override_use_sell_signal(caplog, default_conf): def test_strategy_override_use_sell_profit_only(caplog, default_conf): caplog.set_level(logging.INFO) default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, }) strategy = StrategyResolver.load_strategy(default_conf) assert not strategy.sell_profit_only @@ -319,7 +319,7 @@ def test_strategy_override_use_sell_profit_only(caplog, default_conf): assert not default_conf['sell_profit_only'] default_conf.update({ - 'strategy': 'StrategyTestV3', + 'strategy': CURRENT_TEST_STRATEGY, 'sell_profit_only': True, }) strategy = StrategyResolver.load_strategy(default_conf) diff --git a/tests/test_arguments.py b/tests/test_arguments.py index fca5c6ab9..c2ddaf0ff 100644 --- a/tests/test_arguments.py +++ b/tests/test_arguments.py @@ -7,6 +7,7 @@ import pytest from freqtrade.commands import Arguments from freqtrade.commands.cli_options import check_int_nonzero, check_int_positive +from tests.conftest import CURRENT_TEST_STRATEGY # Parse common command-line-arguments. Used for all tools @@ -123,7 +124,7 @@ def test_parse_args_backtesting_custom() -> None: '-c', 'test_conf.json', '--ticker-interval', '1m', '--strategy-list', - 'StrategyTestV2', + CURRENT_TEST_STRATEGY, 'SampleStrategy' ] call_args = Arguments(args).get_parsed_arg() diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 1ce45e4d5..e25cd800d 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -23,7 +23,8 @@ from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL, ENV_ from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException from freqtrade.loggers import _set_loggers, setup_logging, setup_logging_pre -from tests.conftest import log_has, log_has_re, patched_configuration_load_config_file +from tests.conftest import (CURRENT_TEST_STRATEGY, log_has, log_has_re, + patched_configuration_load_config_file) @pytest.fixture(scope="function") @@ -403,7 +404,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> arglist = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, ] args = Arguments(arglist).get_parsed_arg() @@ -440,7 +441,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non arglist = [ 'backtesting', '--config', 'config.json', - '--strategy', 'StrategyTestV2', + '--strategy', CURRENT_TEST_STRATEGY, '--datadir', '/foo/bar', '--userdir', "/tmp/freqtrade", '--ticker-interval', '1m', @@ -497,7 +498,7 @@ def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> Non '--ticker-interval', '1m', '--export', 'trades', '--strategy-list', - 'StrategyTestV2', + CURRENT_TEST_STRATEGY, 'TestStrategy' ] From 5928ba9c883a6b47bfd425eb497c2ba70f1cfa9c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Sep 2021 20:14:52 +0200 Subject: [PATCH 54/64] Test and document leverage strategy callback --- docs/strategy-advanced.md | 28 +++++++++++++++++ freqtrade/strategy/interface.py | 37 +++++++++++------------ tests/conftest.py | 1 + tests/strategy/strats/strategy_test_v3.py | 9 ++++++ tests/strategy/test_interface.py | 28 ++++++++++++++++- 5 files changed, 83 insertions(+), 20 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 2b9517f3b..13dec60ca 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -642,6 +642,34 @@ Freqtrade will fall back to the `proposed_stake` value should your code raise an !!! Tip Returning `0` or `None` will prevent trades from being placed. +## Leverage Callback + +When trading in markets that allow leverage, this method must return the desired Leverage (Defaults to 1 -> No leverage). + +Assuming a capital of 500USDT, a trade with leverage=3 would result in a position with 500 x 3 = 1500 USDT. + +Values that are above `max_leverage` will be adjusted to `max_leverage`. +For markets / exchanges that don't support leverage, this method is ignored. + +``` python +class AwesomeStrategy(IStrategy): + def leverage(self, pair: str, current_time: 'datetime', current_rate: float, + proposed_leverage: float, max_leverage: float, side: str, + **kwargs) -> float: + """ + Customize leverage for each new trade. + + :param pair: Pair that's currently analyzed + :param current_time: datetime object, containing the current datetime + :param current_rate: Rate, calculated based on pricing settings in ask_strategy. + :param proposed_leverage: A leverage proposed by the bot. + :param max_leverage: Max leverage allowed on this pair + :param side: 'long' or 'short' - indicating the direction of the proposed trade + :return: A leverage amount, which is between 1.0 and max_leverage. + """ + return 1.0 +``` + --- ## Derived strategies diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 139729910..d852c7269 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -370,8 +370,7 @@ class IStrategy(ABC, HyperStrategyMixin): proposed_stake: float, min_stake: float, max_stake: float, **kwargs) -> float: """ - Customize stake size for each new trade. This method is not called when edge module is - enabled. + Customize stake size for each new trade. :param pair: Pair that's currently analyzed :param current_time: datetime object, containing the current datetime @@ -383,6 +382,23 @@ class IStrategy(ABC, HyperStrategyMixin): """ return proposed_stake + def leverage(self, pair: str, current_time: datetime, current_rate: float, + proposed_leverage: float, max_leverage: float, side: str, + **kwargs) -> float: + """ + Customize leverage for each new trade. This method is not called when edge module is + enabled. + + :param pair: Pair that's currently analyzed + :param current_time: datetime object, containing the current datetime + :param current_rate: Rate, calculated based on pricing settings in ask_strategy. + :param proposed_leverage: A leverage proposed by the bot. + :param max_leverage: Max leverage allowed on this pair + :param side: 'long' or 'short' - indicating the direction of the proposed trade + :return: A leverage amount, which is between 1.0 and max_leverage. + """ + return 1.0 + def informative_pairs(self) -> ListPairsWithTimeframes: """ Define additional, informative pair/interval combinations to be cached from the exchange. @@ -971,20 +987,3 @@ class IStrategy(ABC, HyperStrategyMixin): if 'exit_long' not in df.columns: df = df.rename({'sell': 'exit_long'}, axis='columns') return df - - def leverage(self, pair: str, current_time: datetime, current_rate: float, - proposed_leverage: float, max_leverage: float, side: str, - **kwargs) -> float: - """ - Customize leverage for each new trade. This method is not called when edge module is - enabled. - - :param pair: Pair that's currently analyzed - :param current_time: datetime object, containing the current datetime - :param current_rate: Rate, calculated based on pricing settings in ask_strategy. - :param proposed_leverage: A leverage proposed by the bot. - :param max_leverage: Max leverage allowed on this pair - :param side: 'long' or 'short' - indicating the direction of the proposed trade - :return: A leverage amount, which is between 1.0 and max_leverage. - """ - return 1.0 diff --git a/tests/conftest.py b/tests/conftest.py index a9fd42a05..b35ff17d6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -36,6 +36,7 @@ logging.getLogger('').setLevel(logging.INFO) np.seterr(all='raise') CURRENT_TEST_STRATEGY = 'StrategyTestV3' +TRADE_SIDES = ('long', 'short') def pytest_addoption(parser): diff --git a/tests/strategy/strats/strategy_test_v3.py b/tests/strategy/strats/strategy_test_v3.py index db294d4e9..18c4ec93f 100644 --- a/tests/strategy/strats/strategy_test_v3.py +++ b/tests/strategy/strats/strategy_test_v3.py @@ -1,5 +1,6 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement +from datetime import datetime import talib.abstract as ta from pandas import DataFrame @@ -159,3 +160,11 @@ class StrategyTestV3(IStrategy): # TODO-lev: Add short logic return dataframe + + def leverage(self, pair: str, current_time: datetime, current_rate: float, + proposed_leverage: float, max_leverage: float, side: str, + **kwargs) -> float: + # Return 3.0 in all cases. + # Bot-logic must make sure it's an allowed leverage and eventually adjust accordingly. + + return 3.0 diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 65e7da9db..ad393d6a4 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -21,7 +21,7 @@ from freqtrade.strategy.hyper import (BaseParameter, BooleanParameter, Categoric DecimalParameter, IntParameter, RealParameter) from freqtrade.strategy.interface import SellCheckTuple from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper -from tests.conftest import log_has, log_has_re +from tests.conftest import CURRENT_TEST_STRATEGY, TRADE_SIDES, log_has, log_has_re from .strats.strategy_test_v3 import StrategyTestV3 @@ -506,6 +506,32 @@ def test_custom_sell(default_conf, fee, caplog) -> None: assert log_has_re('Custom sell reason returned from custom_sell is too long.*', caplog) +@pytest.mark.parametrize('side', TRADE_SIDES) +def test_leverage_callback(default_conf, side) -> None: + default_conf['strategy'] = 'StrategyTestV2' + strategy = StrategyResolver.load_strategy(default_conf) + + assert strategy.leverage( + pair='XRP/USDT', + current_time=datetime.now(timezone.utc), + current_rate=2.2, + proposed_leverage=1.0, + max_leverage=5.0, + side=side, + ) == 1 + + default_conf['strategy'] = CURRENT_TEST_STRATEGY + strategy = StrategyResolver.load_strategy(default_conf) + assert strategy.leverage( + pair='XRP/USDT', + current_time=datetime.now(timezone.utc), + current_rate=2.2, + proposed_leverage=1.0, + max_leverage=5.0, + side=side, + ) == 3 + + def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) ind_mock = MagicMock(side_effect=lambda x, meta: x) From 4c6b1cd55bb4a4a73f97d053f638712c2bddf78b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Sep 2021 20:36:03 +0200 Subject: [PATCH 55/64] Add very simple short logic to test-strategy --- freqtrade/optimize/backtesting.py | 8 ++++++-- tests/strategy/strats/strategy_test_v3.py | 13 ++++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index b43222fb3..429ba7251 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -139,6 +139,10 @@ class Backtesting: self.config['startup_candle_count'] = self.required_startup self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe) + # TODO-lev: This should come from the configuration setting or better a + # TODO-lev: combination of config/strategy "use_shorts"(?) and "can_short" from the exchange + self._can_short = False + self.progress = BTProgress() self.abort = False @@ -499,8 +503,8 @@ class Backtesting: def check_for_trade_entry(self, row) -> Optional[str]: enter_long = row[LONG_IDX] == 1 exit_long = row[ELONG_IDX] == 1 - enter_short = row[SHORT_IDX] == 1 - exit_short = row[ESHORT_IDX] == 1 + enter_short = self._can_short and row[SHORT_IDX] == 1 + exit_short = self._can_short and row[ESHORT_IDX] == 1 if enter_long == 1 and not any([exit_long, enter_short]): # Long diff --git a/tests/strategy/strats/strategy_test_v3.py b/tests/strategy/strats/strategy_test_v3.py index 18c4ec93f..115211a7c 100644 --- a/tests/strategy/strats/strategy_test_v3.py +++ b/tests/strategy/strats/strategy_test_v3.py @@ -1,6 +1,7 @@ # pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement from datetime import datetime + import talib.abstract as ta from pandas import DataFrame @@ -138,7 +139,11 @@ class StrategyTestV3(IStrategy): (dataframe['plus_di'] > self.buy_plusdi.value) ), 'enter_long'] = 1 - # TODO-lev: Add short logic + dataframe.loc[ + ( + qtpylib.crossed_below(dataframe['rsi'], self.sell_rsi.value) + ), + 'enter_short'] = 1 return dataframe @@ -158,6 +163,12 @@ class StrategyTestV3(IStrategy): ), 'exit_long'] = 1 + dataframe.loc[ + ( + qtpylib.crossed_above(dataframe['rsi'], self.buy_rsi.value) + ), + 'exit_short'] = 1 + # TODO-lev: Add short logic return dataframe From 0e13d57e5792d10dea887d64b9552de5094c7e6c Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Sep 2021 20:42:31 +0200 Subject: [PATCH 56/64] Update advise_* methods to entry/exit --- freqtrade/edge/edge_positioning.py | 4 ++-- freqtrade/optimize/backtesting.py | 4 ++-- freqtrade/strategy/interface.py | 10 ++++---- tests/optimize/test_backtest_detail.py | 4 ++-- tests/optimize/test_backtesting.py | 20 ++++++++-------- tests/optimize/test_hyperopt.py | 16 ++++++------- tests/strategy/test_interface.py | 32 ++++++++++++------------- tests/strategy/test_strategy_loading.py | 16 ++++++------- 8 files changed, 53 insertions(+), 53 deletions(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index bee96c746..e08b3df2f 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -168,8 +168,8 @@ class Edge: pair_data = pair_data.sort_values(by=['date']) pair_data = pair_data.reset_index(drop=True) - df_analyzed = self.strategy.advise_sell( - dataframe=self.strategy.advise_buy( + df_analyzed = self.strategy.advise_exit( + dataframe=self.strategy.advise_entry( dataframe=pair_data, metadata={'pair': pair} ), diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 429ba7251..4094cf0aa 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -274,8 +274,8 @@ class Backtesting: pair_data.loc[:, 'long_tag'] = None pair_data.loc[:, 'short_tag'] = None - df_analyzed = self.strategy.advise_sell( - self.strategy.advise_buy(pair_data, {'pair': pair}), + df_analyzed = self.strategy.advise_exit( + self.strategy.advise_entry(pair_data, {'pair': pair}), {'pair': pair} ).copy() # Trim startup period from analyzed dataframe diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index d852c7269..0d651ccbb 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -489,8 +489,8 @@ class IStrategy(ABC, HyperStrategyMixin): """ logger.debug("TA Analysis Launched") dataframe = self.advise_indicators(dataframe, metadata) - dataframe = self.advise_buy(dataframe, metadata) - dataframe = self.advise_sell(dataframe, metadata) + dataframe = self.advise_entry(dataframe, metadata) + dataframe = self.advise_exit(dataframe, metadata) return dataframe def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -912,7 +912,7 @@ class IStrategy(ABC, HyperStrategyMixin): def advise_all_indicators(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]: """ Populates indicators for given candle (OHLCV) data (for multiple pairs) - Does not run advise_buy or advise_sell! + Does not run advise_entry or advise_exit! Used by optimize operations only, not during dry / live runs. Using .copy() to get a fresh copy of the dataframe for every strategy run. Also copy on output to avoid PerformanceWarnings pandas 1.3.0 started to show. @@ -944,7 +944,7 @@ class IStrategy(ABC, HyperStrategyMixin): else: return self.populate_indicators(dataframe, metadata) - def advise_buy(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + def advise_entry(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the entry order signal for the given dataframe This method should not be overridden. @@ -967,7 +967,7 @@ class IStrategy(ABC, HyperStrategyMixin): return df - def advise_sell(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + def advise_exit(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the exit order signal for the given dataframe This method should not be overridden. diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 9b99648b1..554122bd5 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -598,8 +598,8 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) backtesting.required_startup = 0 - backtesting.strategy.advise_buy = lambda a, m: frame - backtesting.strategy.advise_sell = lambda a, m: frame + backtesting.strategy.advise_entry = lambda a, m: frame + backtesting.strategy.advise_exit = lambda a, m: frame backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss caplog.set_level(logging.DEBUG) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 0d31846d5..662ca0193 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -295,8 +295,8 @@ def test_backtesting_init(mocker, default_conf, order_types) -> None: assert backtesting.config == default_conf assert backtesting.timeframe == '5m' assert callable(backtesting.strategy.advise_all_indicators) - assert callable(backtesting.strategy.advise_buy) - assert callable(backtesting.strategy.advise_sell) + assert callable(backtesting.strategy.advise_entry) + assert callable(backtesting.strategy.advise_exit) assert isinstance(backtesting.strategy.dp, DataProvider) get_fee.assert_called() assert backtesting.fee == 0.5 @@ -811,8 +811,8 @@ def test_backtest_clash_buy_sell(mocker, default_conf, testdatadir): backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) - backtesting.strategy.advise_buy = fun # Override - backtesting.strategy.advise_sell = fun # Override + backtesting.strategy.advise_entry = fun # Override + backtesting.strategy.advise_exit = fun # Override result = backtesting.backtest(**backtest_conf) assert result['results'].empty @@ -827,8 +827,8 @@ def test_backtest_only_sell(mocker, default_conf, testdatadir): backtest_conf = _make_backtest_conf(mocker, conf=default_conf, datadir=testdatadir) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) - backtesting.strategy.advise_buy = fun # Override - backtesting.strategy.advise_sell = fun # Override + backtesting.strategy.advise_entry = fun # Override + backtesting.strategy.advise_exit = fun # Override result = backtesting.backtest(**backtest_conf) assert result['results'].empty @@ -842,8 +842,8 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): backtesting = Backtesting(default_conf) backtesting.required_startup = 0 backtesting._set_strategy(backtesting.strategylist[0]) - backtesting.strategy.advise_buy = _trend_alternate # Override - backtesting.strategy.advise_sell = _trend_alternate # Override + backtesting.strategy.advise_entry = _trend_alternate # Override + backtesting.strategy.advise_exit = _trend_alternate # Override result = backtesting.backtest(**backtest_conf) # 200 candles in backtest data # won't buy on first (shifted by 1) @@ -896,8 +896,8 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) - backtesting.strategy.advise_buy = _trend_alternate_hold # Override - backtesting.strategy.advise_sell = _trend_alternate_hold # Override + backtesting.strategy.advise_entry = _trend_alternate_hold # Override + backtesting.strategy.advise_exit = _trend_alternate_hold # Override processed = backtesting.strategy.advise_all_indicators(data) min_date, max_date = get_timerange(processed) diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index a83277dc6..57d10d048 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -318,8 +318,8 @@ def test_start_calls_optimizer(mocker, hyperopt_conf, capsys) -> None: # Should be called for historical candle data assert dumper.call_count == 1 assert dumper2.call_count == 1 - assert hasattr(hyperopt.backtesting.strategy, "advise_sell") - assert hasattr(hyperopt.backtesting.strategy, "advise_buy") + assert hasattr(hyperopt.backtesting.strategy, "advise_exit") + assert hasattr(hyperopt.backtesting.strategy, "advise_entry") assert hasattr(hyperopt, "max_open_trades") assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hasattr(hyperopt, "position_stacking") @@ -698,8 +698,8 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non assert dumper.call_count == 1 assert dumper2.call_count == 1 - assert hasattr(hyperopt.backtesting.strategy, "advise_sell") - assert hasattr(hyperopt.backtesting.strategy, "advise_buy") + assert hasattr(hyperopt.backtesting.strategy, "advise_exit") + assert hasattr(hyperopt.backtesting.strategy, "advise_entry") assert hasattr(hyperopt, "max_open_trades") assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hasattr(hyperopt, "position_stacking") @@ -772,8 +772,8 @@ def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None: assert dumper.called assert dumper.call_count == 1 assert dumper2.call_count == 1 - assert hasattr(hyperopt.backtesting.strategy, "advise_sell") - assert hasattr(hyperopt.backtesting.strategy, "advise_buy") + assert hasattr(hyperopt.backtesting.strategy, "advise_exit") + assert hasattr(hyperopt.backtesting.strategy, "advise_entry") assert hasattr(hyperopt, "max_open_trades") assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hasattr(hyperopt, "position_stacking") @@ -821,8 +821,8 @@ def test_simplified_interface_sell(mocker, hyperopt_conf, capsys) -> None: assert dumper.called assert dumper.call_count == 1 assert dumper2.call_count == 1 - assert hasattr(hyperopt.backtesting.strategy, "advise_sell") - assert hasattr(hyperopt.backtesting.strategy, "advise_buy") + assert hasattr(hyperopt.backtesting.strategy, "advise_exit") + assert hasattr(hyperopt.backtesting.strategy, "advise_entry") assert hasattr(hyperopt, "max_open_trades") assert hyperopt.max_open_trades == hyperopt_conf['max_open_trades'] assert hasattr(hyperopt, "position_stacking") diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index ad393d6a4..4b39adaf7 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -535,20 +535,20 @@ def test_leverage_callback(default_conf, side) -> None: def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) ind_mock = MagicMock(side_effect=lambda x, meta: x) - buy_mock = MagicMock(side_effect=lambda x, meta: x) - sell_mock = MagicMock(side_effect=lambda x, meta: x) + entry_mock = MagicMock(side_effect=lambda x, meta: x) + exit_mock = MagicMock(side_effect=lambda x, meta: x) mocker.patch.multiple( 'freqtrade.strategy.interface.IStrategy', advise_indicators=ind_mock, - advise_buy=buy_mock, - advise_sell=sell_mock, + advise_entry=entry_mock, + advise_exit=exit_mock, ) strategy = StrategyTestV3({}) strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'}) assert ind_mock.call_count == 1 - assert buy_mock.call_count == 1 - assert buy_mock.call_count == 1 + assert entry_mock.call_count == 1 + assert entry_mock.call_count == 1 assert log_has('TA Analysis Launched', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) @@ -557,8 +557,8 @@ def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: strategy.analyze_ticker(ohlcv_history, {'pair': 'ETH/BTC'}) # No analysis happens as process_only_new_candles is true assert ind_mock.call_count == 2 - assert buy_mock.call_count == 2 - assert buy_mock.call_count == 2 + assert entry_mock.call_count == 2 + assert entry_mock.call_count == 2 assert log_has('TA Analysis Launched', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) @@ -566,13 +566,13 @@ def test_analyze_ticker_default(ohlcv_history, mocker, caplog) -> None: def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> None: caplog.set_level(logging.DEBUG) ind_mock = MagicMock(side_effect=lambda x, meta: x) - buy_mock = MagicMock(side_effect=lambda x, meta: x) - sell_mock = MagicMock(side_effect=lambda x, meta: x) + entry_mock = MagicMock(side_effect=lambda x, meta: x) + exit_mock = MagicMock(side_effect=lambda x, meta: x) mocker.patch.multiple( 'freqtrade.strategy.interface.IStrategy', advise_indicators=ind_mock, - advise_buy=buy_mock, - advise_sell=sell_mock, + advise_entry=entry_mock, + advise_exit=exit_mock, ) strategy = StrategyTestV3({}) @@ -585,8 +585,8 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> assert 'close' in ret.columns assert isinstance(ret, DataFrame) assert ind_mock.call_count == 1 - assert buy_mock.call_count == 1 - assert buy_mock.call_count == 1 + assert entry_mock.call_count == 1 + assert entry_mock.call_count == 1 assert log_has('TA Analysis Launched', caplog) assert not log_has('Skipping TA Analysis for already analyzed candle', caplog) caplog.clear() @@ -594,8 +594,8 @@ def test__analyze_ticker_internal_skip_analyze(ohlcv_history, mocker, caplog) -> ret = strategy._analyze_ticker_internal(ohlcv_history, {'pair': 'ETH/BTC'}) # No analysis happens as process_only_new_candles is true assert ind_mock.call_count == 1 - assert buy_mock.call_count == 1 - assert buy_mock.call_count == 1 + assert entry_mock.call_count == 1 + assert entry_mock.call_count == 1 # only skipped analyze adds buy and sell columns, otherwise it's all mocked assert 'enter_long' in ret.columns assert 'exit_long' in ret.columns diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index b3e79cd27..4e29e1ebc 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -117,11 +117,11 @@ def test_strategy_v2(result, default_conf): df_indicators = strategy.advise_indicators(result, metadata=metadata) assert 'adx' in df_indicators - dataframe = strategy.advise_buy(df_indicators, metadata=metadata) + dataframe = strategy.advise_entry(df_indicators, metadata=metadata) assert 'buy' not in dataframe.columns assert 'enter_long' in dataframe.columns - dataframe = strategy.advise_sell(df_indicators, metadata=metadata) + dataframe = strategy.advise_exit(df_indicators, metadata=metadata) assert 'sell' not in dataframe.columns assert 'exit_long' in dataframe.columns @@ -347,7 +347,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") - strategy.advise_buy(indicators, {'pair': 'ETH/BTC'}) + strategy.advise_entry(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!" \ @@ -356,7 +356,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") - strategy.advise_sell(indicators, {'pair': 'ETH_BTC'}) + strategy.advise_exit(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!" \ @@ -384,11 +384,11 @@ def test_call_deprecated_function(result, monkeypatch, default_conf, caplog): assert isinstance(indicator_df, DataFrame) assert 'adx' in indicator_df.columns - enterdf = strategy.advise_buy(result, metadata=metadata) + enterdf = strategy.advise_entry(result, metadata=metadata) assert isinstance(enterdf, DataFrame) assert 'buy' in enterdf.columns - exitdf = strategy.advise_sell(result, metadata=metadata) + exitdf = strategy.advise_exit(result, metadata=metadata) assert isinstance(exitdf, DataFrame) assert 'sell' in exitdf @@ -411,13 +411,13 @@ def test_strategy_interface_versioning(result, default_conf): assert isinstance(indicator_df, DataFrame) assert 'adx' in indicator_df.columns - enterdf = strategy.advise_buy(result, metadata=metadata) + enterdf = strategy.advise_entry(result, metadata=metadata) assert isinstance(enterdf, DataFrame) assert 'buy' not in enterdf.columns assert 'enter_long' in enterdf.columns - exitdf = strategy.advise_sell(result, metadata=metadata) + exitdf = strategy.advise_exit(result, metadata=metadata) assert isinstance(exitdf, DataFrame) assert 'sell' not in exitdf assert 'exit_long' in exitdf From a0ef89d9101093a090e603d854fe1f53ea69d081 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 22 Sep 2021 20:48:05 +0200 Subject: [PATCH 57/64] Also support column-transition for V1 strategies --- freqtrade/strategy/interface.py | 16 ++++++++-------- tests/strategy/test_strategy_loading.py | 12 +++++++----- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 0d651ccbb..abaf7d224 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -959,13 +959,13 @@ class IStrategy(ABC, HyperStrategyMixin): if self._buy_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " "the current function headers!", DeprecationWarning) - return self.populate_buy_trend(dataframe) # type: ignore + df = self.populate_buy_trend(dataframe) # type: ignore else: df = self.populate_buy_trend(dataframe, metadata) - if 'enter_long' not in df.columns: - df = df.rename({'buy': 'enter_long', 'buy_tag': 'long_tag'}, axis='columns') + if 'enter_long' not in df.columns: + df = df.rename({'buy': 'enter_long', 'buy_tag': 'long_tag'}, axis='columns') - return df + return df def advise_exit(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ @@ -981,9 +981,9 @@ class IStrategy(ABC, HyperStrategyMixin): if self._sell_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " "the current function headers!", DeprecationWarning) - return self.populate_sell_trend(dataframe) # type: ignore + df = self.populate_sell_trend(dataframe) # type: ignore else: df = self.populate_sell_trend(dataframe, metadata) - if 'exit_long' not in df.columns: - df = df.rename({'sell': 'exit_long'}, axis='columns') - return df + if 'exit_long' not in df.columns: + df = df.rename({'sell': 'exit_long'}, axis='columns') + return df diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 4e29e1ebc..e18a3710b 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -99,8 +99,10 @@ def test_load_strategy_noname(default_conf): StrategyResolver.load_strategy(default_conf) -def test_strategy_v2(result, default_conf): - default_conf.update({'strategy': 'StrategyTestV2'}) +@pytest.mark.filterwarnings("ignore:deprecated") +@pytest.mark.parametrize('strategy_name', ['StrategyTestV2', 'TestStrategyLegacyV1']) +def test_strategy_pre_v3(result, default_conf, strategy_name): + default_conf.update({'strategy': strategy_name}) strategy = StrategyResolver.load_strategy(default_conf) metadata = {'pair': 'ETH/BTC'} @@ -364,7 +366,7 @@ def test_deprecate_populate_indicators(result, default_conf): @pytest.mark.filterwarnings("ignore:deprecated") -def test_call_deprecated_function(result, monkeypatch, default_conf, caplog): +def test_call_deprecated_function(result, default_conf, caplog): default_location = Path(__file__).parent / "strats" del default_conf['timeframe'] default_conf.update({'strategy': 'TestStrategyLegacyV1', @@ -386,11 +388,11 @@ def test_call_deprecated_function(result, monkeypatch, default_conf, caplog): enterdf = strategy.advise_entry(result, metadata=metadata) assert isinstance(enterdf, DataFrame) - assert 'buy' in enterdf.columns + assert 'enter_long' in enterdf.columns exitdf = strategy.advise_exit(result, metadata=metadata) assert isinstance(exitdf, DataFrame) - assert 'sell' in exitdf + assert 'exit_long' in exitdf assert log_has("DEPRECATED: Please migrate to using 'timeframe' instead of 'ticker_interval'.", caplog) From 2a678bdbb4494cb143b8a2b0dee4e7aebcaa06f1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 25 Sep 2021 19:31:06 +0200 Subject: [PATCH 58/64] Update buy_tag column to long_tag --- freqtrade/data/btanalysis.py | 1 + freqtrade/enums/signaltype.py | 2 +- freqtrade/optimize/backtesting.py | 7 ++++--- freqtrade/strategy/interface.py | 4 ++-- tests/optimize/test_hyperopt_tools.py | 2 +- tests/strategy/test_interface.py | 2 +- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index 7d97661c4..e8d878838 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -31,6 +31,7 @@ BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date', 'profit_ratio', 'profit_abs', 'sell_reason', 'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs', 'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'buy_tag'] +# TODO-lev: usage of the above might need compatibility code (buy_tag, is_short?, ...?) def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str: diff --git a/freqtrade/enums/signaltype.py b/freqtrade/enums/signaltype.py index b1b86fc47..1f2b6d331 100644 --- a/freqtrade/enums/signaltype.py +++ b/freqtrade/enums/signaltype.py @@ -15,7 +15,7 @@ class SignalTagType(Enum): """ Enum for signal columns """ - BUY_TAG = "buy_tag" + LONG_TAG = "long_tag" SHORT_TAG = "short_tag" diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 4094cf0aa..63d307908 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -45,7 +45,7 @@ LONG_IDX = 5 ELONG_IDX = 6 # Exit long SHORT_IDX = 7 ESHORT_IDX = 8 # Exit short -BUY_TAG_IDX = 9 +ENTER_TAG_IDX = 9 SHORT_TAG_IDX = 10 @@ -454,7 +454,8 @@ class Backtesting: if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): # Enter trade - has_buy_tag = len(row) >= BUY_TAG_IDX + 1 + # TODO-lev: SHORT_TAG ... + has_buy_tag = len(row) >= ENTER_TAG_IDX + 1 trade = LocalTrade( pair=pair, open_rate=row[OPEN_IDX], @@ -464,7 +465,7 @@ class Backtesting: fee_open=self.fee, fee_close=self.fee, is_open=True, - buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None, + buy_tag=row[ENTER_TAG_IDX] if has_buy_tag else None, exchange=self._exchange_name, is_short=(direction == 'short'), ) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index abaf7d224..4e8881295 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -519,7 +519,7 @@ class IStrategy(ABC, HyperStrategyMixin): dataframe[SignalType.EXIT_LONG.value] = 0 dataframe[SignalType.ENTER_SHORT.value] = 0 dataframe[SignalType.EXIT_SHORT.value] = 0 - dataframe[SignalTagType.BUY_TAG.value] = None + dataframe[SignalTagType.LONG_TAG.value] = None dataframe[SignalTagType.SHORT_TAG.value] = None # Other Defs in strategy that want to be called every loop here @@ -690,7 +690,7 @@ class IStrategy(ABC, HyperStrategyMixin): enter_tag_value: Optional[str] = None if enter_long == 1 and not any([exit_long, enter_short]): enter_signal = SignalDirection.LONG - enter_tag_value = latest.get(SignalTagType.BUY_TAG.value, None) + enter_tag_value = latest.get(SignalTagType.LONG_TAG.value, None) if enter_short == 1 and not any([exit_short, enter_long]): enter_signal = SignalDirection.SHORT enter_tag_value = latest.get(SignalTagType.SHORT_TAG.value, None) diff --git a/tests/optimize/test_hyperopt_tools.py b/tests/optimize/test_hyperopt_tools.py index 5a46f238b..17e8248c3 100644 --- a/tests/optimize/test_hyperopt_tools.py +++ b/tests/optimize/test_hyperopt_tools.py @@ -167,7 +167,7 @@ def test__pprint_dict(): def test_get_strategy_filename(default_conf): - x = HyperoptTools.get_strategy_filename(default_conf, CURRENT_TEST_STRATEGY) + x = HyperoptTools.get_strategy_filename(default_conf, 'StrategyTestV3') assert isinstance(x, Path) assert x == Path(__file__).parents[1] / 'strategy/strats/strategy_test_v3.py' diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 4b39adaf7..1ec5eef5a 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -59,7 +59,7 @@ def test_returns_latest_signal(ohlcv_history): assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, False) mocked_history.loc[1, 'exit_long'] = 0 mocked_history.loc[1, 'enter_long'] = 1 - mocked_history.loc[1, 'buy_tag'] = 'buy_signal_01' + mocked_history.loc[1, 'long_tag'] = 'buy_signal_01' assert _STRATEGY.get_entry_signal( 'ETH/BTC', '5m', mocked_history) == (SignalDirection.LONG, 'buy_signal_01') From 4fd00db630e27de686ad79f71096929385e5edfd Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Sep 2021 15:20:59 +0200 Subject: [PATCH 59/64] Use "combined" enter_tag column --- freqtrade/enums/signaltype.py | 3 +-- freqtrade/optimize/backtesting.py | 13 +++++-------- freqtrade/strategy/interface.py | 9 ++++----- tests/optimize/__init__.py | 10 ++++------ tests/optimize/test_backtest_detail.py | 4 ++-- tests/strategy/test_interface.py | 6 ++++-- 6 files changed, 20 insertions(+), 25 deletions(-) diff --git a/freqtrade/enums/signaltype.py b/freqtrade/enums/signaltype.py index 1f2b6d331..fc585318c 100644 --- a/freqtrade/enums/signaltype.py +++ b/freqtrade/enums/signaltype.py @@ -15,8 +15,7 @@ class SignalTagType(Enum): """ Enum for signal columns """ - LONG_TAG = "long_tag" - SHORT_TAG = "short_tag" + ENTER_TAG = "enter_tag" class SignalDirection(Enum): diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 63d307908..4a20d9738 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -46,7 +46,6 @@ ELONG_IDX = 6 # Exit long SHORT_IDX = 7 ESHORT_IDX = 8 # Exit short ENTER_TAG_IDX = 9 -SHORT_TAG_IDX = 10 class Backtesting: @@ -253,7 +252,7 @@ class Backtesting: # Every change to this headers list must evaluate further usages of the resulting tuple # and eventually change the constants for indexes at the top headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long', - 'enter_short', 'exit_short', 'long_tag', 'short_tag'] + 'enter_short', 'exit_short', 'enter_tag'] data: Dict = {} self.progress.init_step(BacktestState.CONVERT, len(processed)) @@ -271,8 +270,7 @@ class Backtesting: if 'exit_long' in pair_data.columns: pair_data.loc[:, 'exit_long'] = 0 pair_data.loc[:, 'exit_short'] = 0 - pair_data.loc[:, 'long_tag'] = None - pair_data.loc[:, 'short_tag'] = None + pair_data.loc[:, 'enter_tag'] = None df_analyzed = self.strategy.advise_exit( self.strategy.advise_entry(pair_data, {'pair': pair}), @@ -287,7 +285,7 @@ class Backtesting: df_analyzed.loc[:, 'enter_short'] = df_analyzed.loc[:, 'enter_short'].shift(1) df_analyzed.loc[:, 'exit_long'] = df_analyzed.loc[:, 'exit_long'].shift(1) df_analyzed.loc[:, 'exit_short'] = df_analyzed.loc[:, 'exit_short'].shift(1) - df_analyzed.loc[:, 'long_tag'] = df_analyzed.loc[:, 'long_tag'].shift(1) + df_analyzed.loc[:, 'enter_tag'] = df_analyzed.loc[:, 'enter_tag'].shift(1) # Update dataprovider cache self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed) @@ -454,8 +452,7 @@ class Backtesting: if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): # Enter trade - # TODO-lev: SHORT_TAG ... - has_buy_tag = len(row) >= ENTER_TAG_IDX + 1 + has_enter_tag = len(row) >= ENTER_TAG_IDX + 1 trade = LocalTrade( pair=pair, open_rate=row[OPEN_IDX], @@ -465,7 +462,7 @@ class Backtesting: fee_open=self.fee, fee_close=self.fee, is_open=True, - buy_tag=row[ENTER_TAG_IDX] if has_buy_tag else None, + buy_tag=row[ENTER_TAG_IDX] if has_enter_tag else None, exchange=self._exchange_name, is_short=(direction == 'short'), ) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 4e8881295..e50795078 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -519,8 +519,7 @@ class IStrategy(ABC, HyperStrategyMixin): dataframe[SignalType.EXIT_LONG.value] = 0 dataframe[SignalType.ENTER_SHORT.value] = 0 dataframe[SignalType.EXIT_SHORT.value] = 0 - dataframe[SignalTagType.LONG_TAG.value] = None - dataframe[SignalTagType.SHORT_TAG.value] = None + dataframe[SignalTagType.ENTER_TAG.value] = None # Other Defs in strategy that want to be called every loop here # twitter_sell = self.watch_twitter_feed(dataframe, metadata) @@ -690,10 +689,10 @@ class IStrategy(ABC, HyperStrategyMixin): enter_tag_value: Optional[str] = None if enter_long == 1 and not any([exit_long, enter_short]): enter_signal = SignalDirection.LONG - enter_tag_value = latest.get(SignalTagType.LONG_TAG.value, None) + enter_tag_value = latest.get(SignalTagType.ENTER_TAG.value, None) if enter_short == 1 and not any([exit_short, enter_long]): enter_signal = SignalDirection.SHORT - enter_tag_value = latest.get(SignalTagType.SHORT_TAG.value, None) + enter_tag_value = latest.get(SignalTagType.ENTER_TAG.value, None) timeframe_seconds = timeframe_to_seconds(timeframe) @@ -963,7 +962,7 @@ class IStrategy(ABC, HyperStrategyMixin): else: df = self.populate_buy_trend(dataframe, metadata) if 'enter_long' not in df.columns: - df = df.rename({'buy': 'enter_long', 'buy_tag': 'long_tag'}, axis='columns') + df = df.rename({'buy': 'enter_long', 'buy_tag': 'enter_tag'}, axis='columns') return df diff --git a/tests/optimize/__init__.py b/tests/optimize/__init__.py index 2ba9485fd..10518758c 100644 --- a/tests/optimize/__init__.py +++ b/tests/optimize/__init__.py @@ -18,7 +18,7 @@ class BTrade(NamedTuple): sell_reason: SellType open_tick: int close_tick: int - buy_tag: Optional[str] = None + enter_tag: Optional[str] = None class BTContainer(NamedTuple): @@ -49,15 +49,13 @@ def _build_backtest_dataframe(data): if len(data[0]) == 8: # No short columns data = [d + [0, 0] for d in data] - columns = columns + ['long_tag'] if len(data[0]) == 11 else columns + columns = columns + ['enter_tag'] if len(data[0]) == 11 else columns frame = DataFrame.from_records(data, columns=columns) frame['date'] = frame['date'].apply(_get_frame_time_from_offset) # Ensure floats are in place for column in ['open', 'high', 'low', 'close', 'volume']: frame[column] = frame[column].astype('float64') - if 'long_tag' not in columns: - frame['long_tag'] = None - if 'short_tag' not in columns: - frame['short_tag'] = None + if 'enter_tag' not in columns: + frame['enter_tag'] = None return frame diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 554122bd5..227d778af 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -532,7 +532,7 @@ tc33 = BTContainer(data=[ sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1, - buy_tag='buy_signal_01' + enter_tag='buy_signal_01' )] ) @@ -621,6 +621,6 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: for c, trade in enumerate(data.trades): res = results.iloc[c] assert res.sell_reason == trade.sell_reason.value - assert res.buy_tag == trade.buy_tag + assert res.buy_tag == trade.enter_tag assert res.open_date == _get_frame_time_from_offset(trade.open_tick) assert res.close_date == _get_frame_time_from_offset(trade.close_tick) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 1ec5eef5a..a9334c616 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -59,7 +59,7 @@ def test_returns_latest_signal(ohlcv_history): assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, False) mocked_history.loc[1, 'exit_long'] = 0 mocked_history.loc[1, 'enter_long'] = 1 - mocked_history.loc[1, 'long_tag'] = 'buy_signal_01' + mocked_history.loc[1, 'enter_tag'] = 'buy_signal_01' assert _STRATEGY.get_entry_signal( 'ETH/BTC', '5m', mocked_history) == (SignalDirection.LONG, 'buy_signal_01') @@ -70,8 +70,10 @@ def test_returns_latest_signal(ohlcv_history): mocked_history.loc[1, 'enter_long'] = 0 mocked_history.loc[1, 'enter_short'] = 1 mocked_history.loc[1, 'exit_short'] = 0 + mocked_history.loc[1, 'enter_tag'] = 'sell_signal_01' + assert _STRATEGY.get_entry_signal( - 'ETH/BTC', '5m', mocked_history) == (SignalDirection.SHORT, None) + 'ETH/BTC', '5m', mocked_history) == (SignalDirection.SHORT, 'sell_signal_01') assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (False, False) assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (True, False) From 4d49f1a0c7627f8e8a96adaf4d90c9dac3fc0eb8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Sep 2021 15:39:34 +0200 Subject: [PATCH 60/64] Reset columns by dropping instead of resetting --- freqtrade/optimize/backtesting.py | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 4a20d9738..c82ee4afc 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -263,14 +263,7 @@ class Backtesting: if not pair_data.empty: # Cleanup from prior runs - # TODO-lev: The below is not 100% compatible with the interface compatibility layer - if 'enter_long' in pair_data.columns: - pair_data.loc[:, 'enter_long'] = 0 - pair_data.loc[:, 'enter_short'] = 0 - if 'exit_long' in pair_data.columns: - pair_data.loc[:, 'exit_long'] = 0 - pair_data.loc[:, 'exit_short'] = 0 - pair_data.loc[:, 'enter_tag'] = None + pair_data.drop(headers[5:] + ['buy', 'sell'], axis=1, errors='ignore') df_analyzed = self.strategy.advise_exit( self.strategy.advise_entry(pair_data, {'pair': pair}), @@ -281,11 +274,11 @@ class Backtesting: startup_candles=self.required_startup) # To avoid using data from future, we use buy/sell signals shifted # from the previous candle - df_analyzed.loc[:, 'enter_long'] = df_analyzed.loc[:, 'enter_long'].shift(1) - df_analyzed.loc[:, 'enter_short'] = df_analyzed.loc[:, 'enter_short'].shift(1) - df_analyzed.loc[:, 'exit_long'] = df_analyzed.loc[:, 'exit_long'].shift(1) - df_analyzed.loc[:, 'exit_short'] = df_analyzed.loc[:, 'exit_short'].shift(1) - df_analyzed.loc[:, 'enter_tag'] = df_analyzed.loc[:, 'enter_tag'].shift(1) + for col in headers[5:]: + if col in df_analyzed.columns: + df_analyzed.loc[:, col] = df_analyzed.loc[:, col].shift(1) + else: + df_analyzed.loc[:, col] = 0 if col != 'enter_tag' else None # Update dataprovider cache self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed) From 84e013de2d5484c943e49b8e9e73d2272736a038 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Sep 2021 19:32:24 +0200 Subject: [PATCH 61/64] Update confirm_trade_entry to support "side" parameter --- docs/strategy-advanced.md | 8 +++++--- freqtrade/freqtradebot.py | 5 ++++- freqtrade/optimize/backtesting.py | 3 ++- freqtrade/strategy/interface.py | 5 +++-- .../templates/subtemplates/strategy_methods_advanced.j2 | 8 +++++--- tests/strategy/test_default_strategy.py | 2 +- 6 files changed, 20 insertions(+), 11 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 13dec60ca..731930020 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -539,9 +539,10 @@ class AwesomeStrategy(IStrategy): # ... populate_* methods def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, - time_in_force: str, current_time: datetime, **kwargs) -> bool: + time_in_force: str, current_time: datetime, + side: str, **kwargs) -> bool: """ - Called right before placing a buy order. + Called right before placing a entry order. Timing for this function is critical, so avoid doing heavy computations or network requests in this method. @@ -549,12 +550,13 @@ class AwesomeStrategy(IStrategy): When not implemented by a strategy, returns True (always confirming). - :param pair: Pair that's about to be bought. + :param pair: Pair that's about to be bought/shorted. :param order_type: Order type (as configured in order_types). usually limit or market. :param amount: Amount in target (quote) currency that's going to be traded. :param rate: Rate that's going to be used when using limit orders :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param current_time: datetime object, containing the current datetime + :param side: 'long' or 'short' - indicating the direction of the proposed trade :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return bool: When True is returned, then the buy-order is placed on the exchange. False aborts the process diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 43a7571f7..51c8b3ad9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -519,9 +519,12 @@ class FreqtradeBot(LoggingMixin): order_type = self.strategy.order_types.get('forcebuy', order_type) # TODO-lev: Will this work for shorting? + # TODO-lev: Add non-hardcoded "side" parameter if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested, - time_in_force=time_in_force, current_time=datetime.now(timezone.utc)): + time_in_force=time_in_force, current_time=datetime.now(timezone.utc), + side='long' + ): logger.info(f"User requested abortion of buying {pair}") return False amount = self.exchange.amount_to_precision(pair, amount) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index c82ee4afc..09248ae09 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -440,7 +440,8 @@ class Backtesting: # Confirm trade entry: if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=stake_amount, rate=row[OPEN_IDX], - time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime()): + time_in_force=time_in_force, current_time=row[DATE_IDX].to_pydatetime(), + side=direction): return None if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index e50795078..2dfd62185 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -230,9 +230,9 @@ class IStrategy(ABC, HyperStrategyMixin): """ pass - # TODO-lev: add side def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, - time_in_force: str, current_time: datetime, **kwargs) -> bool: + time_in_force: str, current_time: datetime, + side: str, **kwargs) -> bool: """ Called right before placing a entry order. Timing for this function is critical, so avoid doing heavy computations or @@ -248,6 +248,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param rate: Rate that's going to be used when using limit orders :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param current_time: datetime object, containing the current datetime + :param side: 'long' or 'short' - indicating the direction of the proposed trade :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return bool: When True is returned, then the buy-order is placed on the exchange. False aborts the process diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 2df23f365..1edf77f10 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -80,9 +80,10 @@ def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', curre return None def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, - time_in_force: str, current_time: 'datetime', **kwargs) -> bool: + time_in_force: str, current_time: datetime, + side: str, **kwargs) -> bool: """ - Called right before placing a buy order. + Called right before placing a entry order. Timing for this function is critical, so avoid doing heavy computations or network requests in this method. @@ -90,12 +91,13 @@ def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: f When not implemented by a strategy, returns True (always confirming). - :param pair: Pair that's about to be bought. + :param pair: Pair that's about to be bought/shorted. :param order_type: Order type (as configured in order_types). usually limit or market. :param amount: Amount in target (quote) currency that's going to be traded. :param rate: Rate that's going to be used when using limit orders :param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled). :param current_time: datetime object, containing the current datetime + :param side: 'long' or 'short' - indicating the direction of the proposed trade :param **kwargs: Ensure to keep this here so updates to this won't break your strategy. :return bool: When True is returned, then the buy-order is placed on the exchange. False aborts the process diff --git a/tests/strategy/test_default_strategy.py b/tests/strategy/test_default_strategy.py index 02597b672..a995491f2 100644 --- a/tests/strategy/test_default_strategy.py +++ b/tests/strategy/test_default_strategy.py @@ -37,7 +37,7 @@ def test_strategy_test_v2(result, fee): assert strategy.confirm_trade_entry(pair='ETH/BTC', order_type='limit', amount=0.1, rate=20000, time_in_force='gtc', - current_time=datetime.utcnow()) is True + current_time=datetime.utcnow(), side='long') is True assert strategy.confirm_trade_exit(pair='ETH/BTC', trade=trade, order_type='limit', amount=0.1, rate=20000, time_in_force='gtc', sell_reason='roi', current_time=datetime.utcnow()) is True From a926f54a25cb91fdb5ab566178dec90a34d40d57 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 26 Sep 2021 19:35:54 +0200 Subject: [PATCH 62/64] Add "side" parameter to custom_stake_amount --- docs/strategy-advanced.md | 2 +- freqtrade/freqtradebot.py | 4 +++- freqtrade/optimize/backtesting.py | 3 ++- freqtrade/strategy/interface.py | 4 ++-- .../templates/subtemplates/strategy_methods_advanced.j2 | 8 ++++---- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 731930020..dc1e2831a 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -619,7 +619,7 @@ It is possible to manage your risk by reducing or increasing stake amount when p class AwesomeStrategy(IStrategy): def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: float, max_stake: float, - **kwargs) -> float: + side: str, **kwargs) -> float: dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe) current_candle = dataframe.iloc[-1].squeeze() diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 51c8b3ad9..5e0508287 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -502,7 +502,9 @@ class FreqtradeBot(LoggingMixin): default_retval=stake_amount)( pair=pair, current_time=datetime.now(timezone.utc), current_rate=enter_limit_requested, proposed_stake=stake_amount, - min_stake=min_stake_amount, max_stake=max_stake_amount) + min_stake=min_stake_amount, max_stake=max_stake_amount, side='long') + # TODO-lev: Add non-hardcoded "side" parameter + stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount) if not stake_amount: diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 09248ae09..4890c20aa 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -429,7 +429,8 @@ class Backtesting: stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, default_retval=stake_amount)( pair=pair, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX], - proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount) + proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount, + side=direction) stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount) if not stake_amount: diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 2dfd62185..a22a0b6b8 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -366,10 +366,9 @@ class IStrategy(ABC, HyperStrategyMixin): """ return None - # TODO-lev: add side def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: float, max_stake: float, - **kwargs) -> float: + side: str, **kwargs) -> float: """ Customize stake size for each new trade. @@ -379,6 +378,7 @@ class IStrategy(ABC, HyperStrategyMixin): :param proposed_stake: A stake amount proposed by the bot. :param min_stake: Minimal stake size allowed by exchange. :param max_stake: Balance available for trading. + :param side: 'long' or 'short' - indicating the direction of the proposed trade :return: A stake size, which is between min_stake and max_stake. """ return proposed_stake diff --git a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 index 1edf77f10..1f064f88e 100644 --- a/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 +++ b/freqtrade/templates/subtemplates/strategy_methods_advanced.j2 @@ -12,12 +12,11 @@ def bot_loop_start(self, **kwargs) -> None: """ pass -def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: float, +def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float, proposed_stake: float, min_stake: float, max_stake: float, - **kwargs) -> float: + side: str, **kwargs) -> float: """ - Customize stake size for each new trade. This method is not called when edge module is - enabled. + Customize stake size for each new trade. :param pair: Pair that's currently analyzed :param current_time: datetime object, containing the current datetime @@ -25,6 +24,7 @@ def custom_stake_amount(self, pair: str, current_time: 'datetime', current_rate: :param proposed_stake: A stake amount proposed by the bot. :param min_stake: Minimal stake size allowed by exchange. :param max_stake: Balance available for trading. + :param side: 'long' or 'short' - indicating the direction of the proposed trade :return: A stake size, which is between min_stake and max_stake. """ return proposed_stake From 6fb0d14f80e3308d61bf1b2be878381637931122 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 Sep 2021 07:07:49 +0200 Subject: [PATCH 63/64] changed naming for signal variable --- freqtrade/freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5e0508287..32edd8588 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -422,11 +422,11 @@ class FreqtradeBot(LoggingMixin): return False # running get_signal on historical data fetched - (side, enter_tag) = self.strategy.get_entry_signal( + (signal, enter_tag) = self.strategy.get_entry_signal( pair, self.strategy.timeframe, analyzed_df ) - if side: + if signal: stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge) bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {}) From d7ce9b9f6d2a53d99eea24738c115bbd56a8d5e3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 27 Sep 2021 19:17:19 +0200 Subject: [PATCH 64/64] Rename sample short strategy --- freqtrade/templates/sample_short_strategy.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/templates/sample_short_strategy.py b/freqtrade/templates/sample_short_strategy.py index bdd0054e8..e9deba6af 100644 --- a/freqtrade/templates/sample_short_strategy.py +++ b/freqtrade/templates/sample_short_strategy.py @@ -15,8 +15,9 @@ import talib.abstract as ta import freqtrade.vendor.qtpylib.indicators as qtpylib +# TODO-lev: Create a meaningfull short strategy (not just revresed signs). # This class is a sample. Feel free to customize it. -class SampleStrategy(IStrategy): +class SampleShortStrategy(IStrategy): """ This is a sample strategy to inspire you. More information in https://www.freqtrade.io/en/latest/strategy-customization/