From 120cad88af542aa761b75e1f9e4e9203bcfe4dfe Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 24 Jul 2021 01:32:42 -0600 Subject: [PATCH 001/134] Add prep functions to exchange --- freqtrade/exchange/binance.py | 103 ++++++++++++++++++++++++++++++++- freqtrade/exchange/bittrex.py | 23 +++++++- freqtrade/exchange/exchange.py | 54 ++++++++++++++++- freqtrade/exchange/kraken.py | 22 ++++++- 4 files changed, 196 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 0c470cb24..63785d184 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -1,6 +1,6 @@ """ Binance exchange subclass """ import logging -from typing import Dict +from typing import Dict, Optional import ccxt @@ -89,3 +89,104 @@ class Binance(Exchange): f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e + + def transfer(self, asset: str, amount: float, frm: str, to: str, pair: Optional[str]): + res = self._api.sapi_post_margin_isolated_transfer({ + "asset": asset, + "amount": amount, + "transFrom": frm, + "transTo": to, + "symbol": pair + }) + logger.info(f"Transfer response: {res}") + + def borrow(self, asset: str, amount: float, pair: str): + res = self._api.sapi_post_margin_loan({ + "asset": asset, + "isIsolated": True, + "symbol": pair, + "amount": amount + }) # borrow from binance + logger.info(f"Borrow response: {res}") + + def repay(self, asset: str, amount: float, pair: str): + res = self._api.sapi_post_margin_repay({ + "asset": asset, + "isIsolated": True, + "symbol": pair, + "amount": amount + }) # borrow from binance + logger.info(f"Borrow response: {res}") + + def setup_leveraged_enter( + self, + pair: str, + leverage: float, + amount: float, + quote_currency: Optional[str], + is_short: Optional[bool] + ): + if not quote_currency or not is_short: + raise OperationalException( + "quote_currency and is_short are required arguments to setup_leveraged_enter" + " when trading with leverage on binance" + ) + open_rate = 2 # TODO-mg: get the real open_rate, or real stake_amount + stake_amount = amount * open_rate + if is_short: + borrowed = stake_amount * ((leverage-1)/leverage) + else: + borrowed = amount + + self.transfer( # Transfer to isolated margin + asset=quote_currency, + amount=stake_amount, + frm='SPOT', + to='ISOLATED_MARGIN', + pair=pair + ) + + self.borrow( + asset=quote_currency, + amount=borrowed, + pair=pair + ) # borrow from binance + + def complete_leveraged_exit( + self, + pair: str, + leverage: float, + amount: float, + quote_currency: Optional[str], + is_short: Optional[bool] + ): + + if not quote_currency or not is_short: + raise OperationalException( + "quote_currency and is_short are required arguments to setup_leveraged_enter" + " when trading with leverage on binance" + ) + + open_rate = 2 # TODO-mg: get the real open_rate, or real stake_amount + stake_amount = amount * open_rate + if is_short: + borrowed = stake_amount * ((leverage-1)/leverage) + else: + borrowed = amount + + self.repay( + asset=quote_currency, + amount=borrowed, + pair=pair + ) # repay binance + + self.transfer( # Transfer to isolated margin + asset=quote_currency, + amount=stake_amount, + frm='ISOLATED_MARGIN', + to='SPOT', + pair=pair + ) + + def apply_leverage_to_stake_amount(self, stake_amount: float, leverage: float): + return stake_amount / leverage diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index 69e2f2b8d..e4d344d27 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -1,8 +1,9 @@ """ Bittrex exchange subclass """ import logging -from typing import Dict +from typing import Dict, Optional from freqtrade.exchange import Exchange +from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) @@ -23,3 +24,23 @@ class Bittrex(Exchange): }, "l2_limit_range": [1, 25, 500], } + + def setup_leveraged_enter( + self, + pair: str, + leverage: float, + amount: float, + quote_currency: Optional[str], + is_short: Optional[bool] + ): + raise OperationalException("Bittrex does not support leveraged trading") + + def complete_leveraged_exit( + self, + pair: str, + leverage: float, + amount: float, + quote_currency: Optional[str], + is_short: Optional[bool] + ): + raise OperationalException("Bittrex does not support leveraged trading") diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c6f60e08a..08cd1256e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -184,6 +184,7 @@ class Exchange: 'secret': exchange_config.get('secret'), 'password': exchange_config.get('password'), 'uid': exchange_config.get('uid', ''), + 'options': exchange_config.get('options', {}) } if ccxt_kwargs: logger.info('Applying additional ccxt config: %s', ccxt_kwargs) @@ -524,8 +525,9 @@ class Exchange: else: return 1 / pow(10, precision) - def get_min_pair_stake_amount(self, pair: str, price: float, - stoploss: float) -> Optional[float]: + def get_min_pair_stake_amount(self, pair: str, price: float, stoploss: float, + leverage: Optional[float] = 1.0) -> Optional[float]: + # TODO-mg: Using leverage makes the min stake amount lower (on binance at least) try: market = self.markets[pair] except KeyError: @@ -559,7 +561,20 @@ 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 max(min_stake_amounts) * amount_reserve_percent + return self.apply_leverage_to_stake_amount( + max(min_stake_amounts) * amount_reserve_percent, + leverage or 1.0 + ) + + def apply_leverage_to_stake_amount(self, stake_amount: float, leverage: float): + """ + #* 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 + :param leverage: The amount of leverage being used on the current trade + """ + return stake_amount # Dry-run methods @@ -686,6 +701,15 @@ class Exchange: raise InvalidOrderException( f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e + def get_max_leverage(self, pair: str, stake_amount: float, price: float) -> float: + """ + Gets the maximum leverage available on this pair that is below the config leverage + but higher than the config min_leverage + """ + + raise OperationalException(f"Leverage is not available on {self.name} using freqtrade") + return 1.0 + # Order handling def create_order(self, pair: str, ordertype: str, side: str, amount: float, @@ -709,6 +733,7 @@ class Exchange: order = self._api.create_order(pair, ordertype, side, amount, rate_for_order, params) self._log_exchange_response('create_order', order) + return order except ccxt.InsufficientFunds as e: @@ -729,6 +754,26 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + def setup_leveraged_enter( + self, + pair: str, + leverage: float, + amount: float, + quote_currency: Optional[str], + is_short: Optional[bool] + ): + raise OperationalException(f"Leverage is not available on {self.name} using freqtrade") + + def complete_leveraged_exit( + self, + pair: str, + leverage: float, + amount: float, + quote_currency: Optional[str], + is_short: Optional[bool] + ): + raise OperationalException(f"Leverage is not available on {self.name} using freqtrade") + def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: """ Verify stop_loss against stoploss-order value (limit or price) @@ -1492,6 +1537,9 @@ class Exchange: self._async_get_trade_history(pair=pair, since=since, until=until, from_id=from_id)) + def transfer(self, asset: str, amount: float, frm: str, to: str, pair: Optional[str]): + self._api.transfer(asset, amount, frm, to) + def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 1b069aa6c..d7dfd3f3b 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -1,6 +1,6 @@ """ Kraken exchange subclass """ import logging -from typing import Any, Dict +from typing import Any, Dict, Optional import ccxt @@ -124,3 +124,23 @@ class Kraken(Exchange): f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e + + def setup_leveraged_enter( + self, + pair: str, + leverage: float, + amount: float, + quote_currency: Optional[str], + is_short: Optional[bool] + ): + return + + def complete_leveraged_exit( + self, + pair: str, + leverage: float, + amount: float, + quote_currency: Optional[str], + is_short: Optional[bool] + ): + return From b48b768757c936a3cf50b6e8650b6bd993dfe3da Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 25 Jul 2021 23:40:38 -0600 Subject: [PATCH 002/134] Added get_interest template method in exchange --- freqtrade/exchange/exchange.py | 14 ++++++++++++-- tests/exchange/test_exchange.py | 2 +- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 08cd1256e..aa3ba4829 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -568,7 +568,7 @@ class Exchange: def apply_leverage_to_stake_amount(self, stake_amount: float, leverage: float): """ - #* Should be implemented by child classes if leverage affects the stake_amount + # * 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 @@ -1531,7 +1531,7 @@ class Exchange: :returns List of trade data """ if not self.exchange_has("fetchTrades"): - raise OperationalException("This exchange does not suport downloading Trades.") + raise OperationalException("This exchange does not support downloading Trades.") return asyncio.get_event_loop().run_until_complete( self._async_get_trade_history(pair=pair, since=since, @@ -1540,6 +1540,16 @@ class Exchange: def transfer(self, asset: str, amount: float, frm: str, to: str, pair: Optional[str]): self._api.transfer(asset, amount, frm, to) + def get_isolated_liq(self, pair: str, open_rate: float, + amount: float, leverage: float, is_short: bool) -> float: + raise OperationalException( + f"Isolated margin is not available on {self.name} using freqtrade" + ) + + def get_interest_rate(self, pair: str, open_rate: float, is_short: bool) -> float: + # TODO-mg: implement + return 0.0005 + def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index a3ebbe8bd..935775477 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2177,7 +2177,7 @@ def test_get_historic_trades_notsupported(default_conf, mocker, caplog, exchange pair = 'ETH/BTC' with pytest.raises(OperationalException, - match="This exchange does not suport downloading Trades."): + match="This exchange does not support downloading Trades."): exchange.get_historic_trades(pair, since=trades_history[0][0], until=trades_history[-1][0]) From 2c0077abc7ec3eb379d77c3658725f44efe19991 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 26 Jul 2021 00:01:57 -0600 Subject: [PATCH 003/134] Exchange stoploss function takes side --- freqtrade/exchange/binance.py | 9 ++++++-- freqtrade/exchange/exchange.py | 5 +++-- freqtrade/exchange/ftx.py | 8 +++++-- freqtrade/exchange/kraken.py | 7 +++++-- freqtrade/freqtradebot.py | 16 ++++++++------ tests/exchange/test_binance.py | 21 ++++++++++--------- tests/exchange/test_exchange.py | 4 ++-- tests/exchange/test_ftx.py | 20 +++++++++--------- tests/exchange/test_kraken.py | 16 +++++++------- tests/test_freqtradebot.py | 37 ++++++++++++++++++++------------- 10 files changed, 85 insertions(+), 58 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 63785d184..0992e2d40 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -24,20 +24,25 @@ class Binance(Exchange): "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], } - def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: """ Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. + :param side: "buy" or "sell" """ + # TODO-mg: Short support return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice']) @retrier(retries=0) - def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: + def stoploss(self, pair: str, amount: float, + stop_price: float, order_types: Dict, side: str) -> Dict: """ creates a stoploss limit order. this stoploss-limit is binance-specific. It may work with a limited number of other exchanges, but this has not been tested yet. + :param side: "buy" or "sell" """ + # TODO-mg: Short support # Limit price threshold: As limit price should always be below stop-price limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) rate = stop_price * limit_price_pct diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index aa3ba4829..24566becf 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -774,14 +774,15 @@ class Exchange: ): raise OperationalException(f"Leverage is not available on {self.name} using freqtrade") - def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: """ Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. """ raise OperationalException(f"stoploss is not implemented for {self.name}.") - def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: + def stoploss(self, pair: str, amount: float, + stop_price: float, order_types: Dict, side: str) -> Dict: """ creates a stoploss order. The precise ordertype is determined by the order_types dict or exchange default. diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 6cd549d60..4a078bbb7 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -31,21 +31,25 @@ class Ftx(Exchange): return (parent_check and market.get('spot', False) is True) - def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: """ Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. """ + # TODO-mg: Short support return order['type'] == 'stop' and stop_loss > float(order['price']) @retrier(retries=0) - def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: + def stoploss(self, pair: str, amount: float, + stop_price: float, order_types: Dict, side: str) -> Dict: """ Creates a stoploss order. depending on order_types.stoploss configuration, uses 'market' or limit order. Limit orders are defined by having orderPrice set, otherwise a market order is used. """ + # TODO-mg: Short support + limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) limit_rate = stop_price * limit_price_pct diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index d7dfd3f3b..36c1608bd 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -67,20 +67,23 @@ class Kraken(Exchange): except ccxt.BaseError as e: raise OperationalException(e) from e - def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: """ Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. """ + # TODO-mg: Short support return (order['type'] in ('stop-loss', 'stop-loss-limit') and stop_loss > float(order['price'])) @retrier(retries=0) - def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: + def stoploss(self, pair: str, amount: float, + stop_price: float, order_types: Dict, side: str) -> Dict: """ Creates a stoploss market order. Stoploss market orders is the only stoploss type supported by kraken. """ + # TODO-mg: Short support params = self._params.copy() if order_types.get('stoploss', 'market') == 'limit': diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 179c99d2c..3f7252659 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -722,9 +722,13 @@ class FreqtradeBot(LoggingMixin): :return: True if the order succeeded, and False in case of problems. """ try: - stoploss_order = self.exchange.stoploss(pair=trade.pair, amount=trade.amount, - stop_price=stop_price, - order_types=self.strategy.order_types) + stoploss_order = self.exchange.stoploss( + pair=trade.pair, + amount=trade.amount, + stop_price=stop_price, + order_types=self.strategy.order_types, + side=trade.exit_side + ) order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss') trade.orders.append(order_obj) @@ -813,11 +817,11 @@ class FreqtradeBot(LoggingMixin): # if trailing stoploss is enabled we check if stoploss value has changed # in which case we cancel stoploss order and put another one with new # value immediately - self.handle_trailing_stoploss_on_exchange(trade, stoploss_order) + self.handle_trailing_stoploss_on_exchange(trade, stoploss_order, side=trade.exit_side) return False - def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> None: + def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict, side: str) -> None: """ Check to see if stoploss on exchange should be updated in case of trailing stoploss on exchange @@ -825,7 +829,7 @@ class FreqtradeBot(LoggingMixin): :param order: Current on exchange stoploss order :return: None """ - if self.exchange.stoploss_adjust(trade.stop_loss, order): + if self.exchange.stoploss_adjust(trade.stop_loss, order, side): # we check if the update is necessary update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat: diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index f2b508761..7b324efa2 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -32,12 +32,13 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected): 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, + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, side="sell", order_types={'stoploss_on_exchange_limit_ratio': 1.05}) 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) + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, + order_types=order_types, side="sell") assert 'id' in order assert 'info' in order @@ -54,17 +55,17 @@ def test_stoploss_order_binance(default_conf, mocker, limitratio, expected): 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={}) + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") 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={}) + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance", "stoploss", "create_order", retries=1, - pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") def test_stoploss_order_dry_run_binance(default_conf, mocker): @@ -77,12 +78,12 @@ 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, + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, side="sell", order_types={'stoploss_on_exchange_limit_ratio': 1.05}) api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") assert 'id' in order assert 'info' in order @@ -100,8 +101,8 @@ def test_stoploss_adjust_binance(mocker, default_conf): 'price': 1500, 'info': {'stopPrice': 1500}, } - assert exchange.stoploss_adjust(1501, order) - assert not exchange.stoploss_adjust(1499, order) + assert exchange.stoploss_adjust(1501, order, side="sell") + assert not exchange.stoploss_adjust(1499, order, side="sell") # Test with invalid order case order['type'] = 'stop_loss' - assert not exchange.stoploss_adjust(1501, order) + assert not exchange.stoploss_adjust(1501, order, side="sell") diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 935775477..e7921747b 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2530,10 +2530,10 @@ 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={}) + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): - exchange.stoploss_adjust(1, {}) + exchange.stoploss_adjust(1, {}, side="sell") def test_merge_ft_has_dict(default_conf, mocker): diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 3794bb79c..3887e2b08 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -32,7 +32,7 @@ def test_stoploss_order_ftx(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') # stoploss_on_exchange_limit_ratio is irrelevant for ftx market orders - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, side="sell", order_types={'stoploss_on_exchange_limit_ratio': 1.05}) assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' @@ -47,7 +47,7 @@ def test_stoploss_order_ftx(default_conf, mocker): api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") assert 'id' in order assert 'info' in order @@ -61,7 +61,7 @@ def test_stoploss_order_ftx(default_conf, mocker): api_mock.create_order.reset_mock() order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, - order_types={'stoploss': 'limit'}) + order_types={'stoploss': 'limit'}, side="sell") assert 'id' in order assert 'info' in order @@ -78,17 +78,17 @@ def test_stoploss_order_ftx(default_conf, mocker): 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={}) + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") 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={}) + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") ccxt_exceptionhandlers(mocker, default_conf, api_mock, "ftx", "stoploss", "create_order", retries=1, - pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") def test_stoploss_order_dry_run_ftx(default_conf, mocker): @@ -101,7 +101,7 @@ def test_stoploss_order_dry_run_ftx(default_conf, mocker): api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") assert 'id' in order assert 'info' in order @@ -118,11 +118,11 @@ def test_stoploss_adjust_ftx(mocker, default_conf): 'type': STOPLOSS_ORDERTYPE, 'price': 1500, } - assert exchange.stoploss_adjust(1501, order) - assert not exchange.stoploss_adjust(1499, order) + assert exchange.stoploss_adjust(1501, order, side="sell") + assert not exchange.stoploss_adjust(1499, order, side="sell") # Test with invalid order case ... order['type'] = 'stop_loss_limit' - assert not exchange.stoploss_adjust(1501, order) + assert not exchange.stoploss_adjust(1501, order, side="sell") def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order): diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index eb79dfc10..c2b96cf17 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -183,7 +183,7 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, side="sell", order_types={'stoploss': ordertype, 'stoploss_on_exchange_limit_ratio': 0.99 }) @@ -208,17 +208,17 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype): 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={}) + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") 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={}) + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken", "stoploss", "create_order", retries=1, - pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") def test_stoploss_order_dry_run_kraken(default_conf, mocker): @@ -231,7 +231,7 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker): api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") assert 'id' in order assert 'info' in order @@ -248,8 +248,8 @@ def test_stoploss_adjust_kraken(mocker, default_conf): 'type': STOPLOSS_ORDERTYPE, 'price': 1500, } - assert exchange.stoploss_adjust(1501, order) - assert not exchange.stoploss_adjust(1499, order) + assert exchange.stoploss_adjust(1501, order, side="sell") + assert not exchange.stoploss_adjust(1499, order, side="sell") # Test with invalid order case ... order['type'] = 'stop_loss_limit' - assert not exchange.stoploss_adjust(1501, order) + assert not exchange.stoploss_adjust(1501, order, side="sell") diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 7c37bb269..61a90dc3f 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1307,10 +1307,13 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, assert freqtrade.handle_stoploss_on_exchange(trade) is False cancel_order_mock.assert_called_once_with(100, 'ETH/BTC') - stoploss_order_mock.assert_called_once_with(amount=85.32423208, - pair='ETH/BTC', - order_types=freqtrade.strategy.order_types, - stop_price=0.00002346 * 0.95) + stoploss_order_mock.assert_called_once_with( + amount=85.32423208, + pair='ETH/BTC', + order_types=freqtrade.strategy.order_types, + stop_price=0.00002346 * 0.95, + side="sell" + ) # price fell below stoploss, so dry-run sells trade. mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ @@ -1381,7 +1384,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c side_effect=InvalidOrderException()) mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', return_value=stoploss_order_hanging) - freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) + freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging, side="buy") assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog) # Still try to create order @@ -1391,7 +1394,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c caplog.clear() cancel_mock = mocker.patch("freqtrade.exchange.Binance.cancel_stoploss_order", MagicMock()) mocker.patch("freqtrade.exchange.Binance.stoploss", side_effect=ExchangeError()) - freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) + freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging, side="buy") assert cancel_mock.call_count == 1 assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog) @@ -1490,10 +1493,13 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, assert freqtrade.handle_stoploss_on_exchange(trade) is False cancel_order_mock.assert_called_once_with(100, 'ETH/BTC') - stoploss_order_mock.assert_called_once_with(amount=85.32423208, - pair='ETH/BTC', - order_types=freqtrade.strategy.order_types, - stop_price=0.00002346 * 0.96) + stoploss_order_mock.assert_called_once_with( + amount=85.32423208, + pair='ETH/BTC', + order_types=freqtrade.strategy.order_types, + stop_price=0.00002346 * 0.96, + side="sell" + ) # price fell below stoploss, so dry-run sells trade. mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ @@ -1611,10 +1617,13 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, # stoploss should be set to 1% as trailing is on assert trade.stop_loss == 0.00002346 * 0.99 cancel_order_mock.assert_called_once_with(100, 'NEO/BTC') - stoploss_order_mock.assert_called_once_with(amount=2132892.49146757, - pair='NEO/BTC', - order_types=freqtrade.strategy.order_types, - stop_price=0.00002346 * 0.99) + stoploss_order_mock.assert_called_once_with( + amount=2132892.49146757, + pair='NEO/BTC', + order_types=freqtrade.strategy.order_types, + stop_price=0.00002346 * 0.99, + side="sell" + ) def test_enter_positions(mocker, default_conf, caplog) -> None: From 4ca1d25db1794a753e1a500cc37fb22868d9327e Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 2 Aug 2021 06:14:30 -0600 Subject: [PATCH 004/134] Removed setup leverage and transfer functions from exchange --- freqtrade/exchange/binance.py | 100 +-------------------------------- freqtrade/exchange/bittrex.py | 23 +------- freqtrade/exchange/exchange.py | 32 +---------- freqtrade/exchange/kraken.py | 22 +------- 4 files changed, 4 insertions(+), 173 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 0992e2d40..dc54de277 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -1,6 +1,6 @@ """ Binance exchange subclass """ import logging -from typing import Dict, Optional +from typing import Dict import ccxt @@ -95,103 +95,5 @@ class Binance(Exchange): except ccxt.BaseError as e: raise OperationalException(e) from e - def transfer(self, asset: str, amount: float, frm: str, to: str, pair: Optional[str]): - res = self._api.sapi_post_margin_isolated_transfer({ - "asset": asset, - "amount": amount, - "transFrom": frm, - "transTo": to, - "symbol": pair - }) - logger.info(f"Transfer response: {res}") - - def borrow(self, asset: str, amount: float, pair: str): - res = self._api.sapi_post_margin_loan({ - "asset": asset, - "isIsolated": True, - "symbol": pair, - "amount": amount - }) # borrow from binance - logger.info(f"Borrow response: {res}") - - def repay(self, asset: str, amount: float, pair: str): - res = self._api.sapi_post_margin_repay({ - "asset": asset, - "isIsolated": True, - "symbol": pair, - "amount": amount - }) # borrow from binance - logger.info(f"Borrow response: {res}") - - def setup_leveraged_enter( - self, - pair: str, - leverage: float, - amount: float, - quote_currency: Optional[str], - is_short: Optional[bool] - ): - if not quote_currency or not is_short: - raise OperationalException( - "quote_currency and is_short are required arguments to setup_leveraged_enter" - " when trading with leverage on binance" - ) - open_rate = 2 # TODO-mg: get the real open_rate, or real stake_amount - stake_amount = amount * open_rate - if is_short: - borrowed = stake_amount * ((leverage-1)/leverage) - else: - borrowed = amount - - self.transfer( # Transfer to isolated margin - asset=quote_currency, - amount=stake_amount, - frm='SPOT', - to='ISOLATED_MARGIN', - pair=pair - ) - - self.borrow( - asset=quote_currency, - amount=borrowed, - pair=pair - ) # borrow from binance - - def complete_leveraged_exit( - self, - pair: str, - leverage: float, - amount: float, - quote_currency: Optional[str], - is_short: Optional[bool] - ): - - if not quote_currency or not is_short: - raise OperationalException( - "quote_currency and is_short are required arguments to setup_leveraged_enter" - " when trading with leverage on binance" - ) - - open_rate = 2 # TODO-mg: get the real open_rate, or real stake_amount - stake_amount = amount * open_rate - if is_short: - borrowed = stake_amount * ((leverage-1)/leverage) - else: - borrowed = amount - - self.repay( - asset=quote_currency, - amount=borrowed, - pair=pair - ) # repay binance - - self.transfer( # Transfer to isolated margin - asset=quote_currency, - amount=stake_amount, - frm='ISOLATED_MARGIN', - to='SPOT', - pair=pair - ) - def apply_leverage_to_stake_amount(self, stake_amount: float, leverage: float): return stake_amount / leverage diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index e4d344d27..69e2f2b8d 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -1,9 +1,8 @@ """ Bittrex exchange subclass """ import logging -from typing import Dict, Optional +from typing import Dict from freqtrade.exchange import Exchange -from freqtrade.exceptions import OperationalException logger = logging.getLogger(__name__) @@ -24,23 +23,3 @@ class Bittrex(Exchange): }, "l2_limit_range": [1, 25, 500], } - - def setup_leveraged_enter( - self, - pair: str, - leverage: float, - amount: float, - quote_currency: Optional[str], - is_short: Optional[bool] - ): - raise OperationalException("Bittrex does not support leveraged trading") - - def complete_leveraged_exit( - self, - pair: str, - leverage: float, - amount: float, - quote_currency: Optional[str], - is_short: Optional[bool] - ): - raise OperationalException("Bittrex does not support leveraged trading") diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 24566becf..4032dc030 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -703,8 +703,7 @@ class Exchange: def get_max_leverage(self, pair: str, stake_amount: float, price: float) -> float: """ - Gets the maximum leverage available on this pair that is below the config leverage - but higher than the config min_leverage + Gets the maximum leverage available on this pair """ raise OperationalException(f"Leverage is not available on {self.name} using freqtrade") @@ -754,26 +753,6 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - def setup_leveraged_enter( - self, - pair: str, - leverage: float, - amount: float, - quote_currency: Optional[str], - is_short: Optional[bool] - ): - raise OperationalException(f"Leverage is not available on {self.name} using freqtrade") - - def complete_leveraged_exit( - self, - pair: str, - leverage: float, - amount: float, - quote_currency: Optional[str], - is_short: Optional[bool] - ): - raise OperationalException(f"Leverage is not available on {self.name} using freqtrade") - def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: """ Verify stop_loss against stoploss-order value (limit or price) @@ -1538,15 +1517,6 @@ class Exchange: self._async_get_trade_history(pair=pair, since=since, until=until, from_id=from_id)) - def transfer(self, asset: str, amount: float, frm: str, to: str, pair: Optional[str]): - self._api.transfer(asset, amount, frm, to) - - def get_isolated_liq(self, pair: str, open_rate: float, - amount: float, leverage: float, is_short: bool) -> float: - raise OperationalException( - f"Isolated margin is not available on {self.name} using freqtrade" - ) - def get_interest_rate(self, pair: str, open_rate: float, is_short: bool) -> float: # TODO-mg: implement return 0.0005 diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 36c1608bd..010b574d6 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -1,6 +1,6 @@ """ Kraken exchange subclass """ import logging -from typing import Any, Dict, Optional +from typing import Any, Dict import ccxt @@ -127,23 +127,3 @@ class Kraken(Exchange): f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e - - def setup_leveraged_enter( - self, - pair: str, - leverage: float, - amount: float, - quote_currency: Optional[str], - is_short: Optional[bool] - ): - return - - def complete_leveraged_exit( - self, - pair: str, - leverage: float, - amount: float, - quote_currency: Optional[str], - is_short: Optional[bool] - ): - return From 53a6ce881cabd02c6b825d758e82b50fab10eb30 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 8 Aug 2021 19:34:33 -0600 Subject: [PATCH 005/134] Added set_leverage function to exchange --- freqtrade/exchange/exchange.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 4032dc030..ad8398e26 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -732,7 +732,6 @@ class Exchange: order = self._api.create_order(pair, ordertype, side, amount, rate_for_order, params) self._log_exchange_response('create_order', order) - return order except ccxt.InsufficientFunds as e: @@ -1521,6 +1520,15 @@ class Exchange: # TODO-mg: implement return 0.0005 + def set_leverage(self, pair, leverage): + """ + Binance Futures must set the leverage before making a futures trade, in order to not + have the same leverage on every trade + # TODO-lev: This may be the case for any futures exchange, or even margin trading on + # TODO-lev: some exchanges, so check this + """ + self._api.set_leverage(symbol=pair, leverage=leverage) + def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) From 0733d69cda37f97b7b6860fd7912a00e46ed2ed9 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 8 Aug 2021 23:13:35 -0600 Subject: [PATCH 006/134] Added TODOs to test files --- freqtrade/exchange/binance.py | 4 ++-- freqtrade/exchange/exchange.py | 4 ++-- freqtrade/exchange/ftx.py | 4 ++-- freqtrade/exchange/kraken.py | 4 ++-- tests/exchange/test_exchange.py | 24 ++++++++++++++++++++++++ tests/exchange/test_ftx.py | 2 ++ tests/exchange/test_kraken.py | 2 ++ 7 files changed, 36 insertions(+), 8 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index dc54de277..a9d3db129 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -30,7 +30,7 @@ class Binance(Exchange): Returns True if adjustment is necessary. :param side: "buy" or "sell" """ - # TODO-mg: Short support + # TODO-lev: Short support return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice']) @retrier(retries=0) @@ -42,7 +42,7 @@ class Binance(Exchange): It may work with a limited number of other exchanges, but this has not been tested yet. :param side: "buy" or "sell" """ - # TODO-mg: Short support + # TODO-lev: Short support # Limit price threshold: As limit price should always be below stop-price limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) rate = stop_price * limit_price_pct diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ad8398e26..ed9521639 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -527,7 +527,7 @@ class Exchange: def get_min_pair_stake_amount(self, pair: str, price: float, stoploss: float, leverage: Optional[float] = 1.0) -> Optional[float]: - # TODO-mg: Using leverage makes the min stake amount lower (on binance at least) + # TODO-lev: Using leverage makes the min stake amount lower (on binance at least) try: market = self.markets[pair] except KeyError: @@ -1517,7 +1517,7 @@ class Exchange: until=until, from_id=from_id)) def get_interest_rate(self, pair: str, open_rate: float, is_short: bool) -> float: - # TODO-mg: implement + # TODO-lev: implement return 0.0005 def set_leverage(self, pair, leverage): diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 4a078bbb7..aca060d2b 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -36,7 +36,7 @@ class Ftx(Exchange): Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. """ - # TODO-mg: Short support + # TODO-lev: Short support return order['type'] == 'stop' and stop_loss > float(order['price']) @retrier(retries=0) @@ -48,7 +48,7 @@ class Ftx(Exchange): Limit orders are defined by having orderPrice set, otherwise a market order is used. """ - # TODO-mg: Short support + # TODO-lev: Short support limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) limit_rate = stop_price * limit_price_pct diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 010b574d6..303c4d885 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -72,7 +72,7 @@ class Kraken(Exchange): Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. """ - # TODO-mg: Short support + # TODO-lev: Short support return (order['type'] in ('stop-loss', 'stop-loss-limit') and stop_loss > float(order['price'])) @@ -83,7 +83,7 @@ class Kraken(Exchange): Creates a stoploss market order. Stoploss market orders is the only stoploss type supported by kraken. """ - # TODO-mg: Short support + # TODO-lev: Short support params = self._params.copy() if order_types.get('stoploss', 'market') == 'limit': diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index e7921747b..3a0dbb258 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -109,6 +109,8 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog): assert log_has("Applying additional ccxt config: {'TestKWARG': 11, 'TestKWARG44': 11}", caplog) assert log_has(asynclogmsg, caplog) + # TODO-lev: Test with options + def test_destroy(default_conf, mocker, caplog): caplog.set_level(logging.DEBUG) @@ -300,6 +302,7 @@ def test_price_get_one_pip(default_conf, mocker, price, precision_mode, precisio def test_get_min_pair_stake_amount(mocker, default_conf) -> None: + # TODO-lev: Test with leverage exchange = get_patched_exchange(mocker, default_conf, id="binance") stoploss = -0.05 markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}} @@ -418,6 +421,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: + # TODO-lev: Test with leverage exchange = get_patched_exchange(mocker, default_conf, id="binance") stoploss = -0.05 markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}} @@ -438,6 +442,11 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: ) +def apply_leverage_to_stake_amount(): + # TODO-lev + return + + def test_set_sandbox(default_conf, mocker): """ Test working scenario @@ -2882,3 +2891,18 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None: ]) def test_calculate_backoff(retrycount, max_retries, expected): assert calculate_backoff(retrycount, max_retries) == expected + + +def test_get_max_leverage(): + # TODO-lev + return + + +def test_get_interest_rate(): + # TODO-lev + return + + +def test_set_leverage(): + # TODO-lev + return diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 3887e2b08..76b01dd35 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -13,6 +13,8 @@ from .test_exchange import ccxt_exceptionhandlers STOPLOSS_ORDERTYPE = 'stop' +# TODO-lev: All these stoploss tests with shorts + def test_stoploss_order_ftx(default_conf, mocker): api_mock = MagicMock() diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index c2b96cf17..60250fc71 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -164,6 +164,8 @@ def test_get_balances_prod(default_conf, mocker): ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken", "get_balances", "fetch_balance") +# TODO-lev: All these stoploss tests with shorts + @pytest.mark.parametrize('ordertype', ['market', 'limit']) def test_stoploss_order_kraken(default_conf, mocker, ordertype): From 97bb555d412370909386841cd698ea1b4fa52437 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 20 Aug 2021 02:40:22 -0600 Subject: [PATCH 007/134] Implemented fill_leverage_brackets get_max_leverage and set_leverage for binance, kraken and ftx. Wrote tests test_apply_leverage_to_stake_amount and test_get_max_leverage --- freqtrade/exchange/binance.py | 42 +++++++++++++++++++++-- freqtrade/exchange/exchange.py | 50 ++++++++++++++++++++------- freqtrade/exchange/ftx.py | 29 +++++++++++++++- freqtrade/exchange/kraken.py | 41 +++++++++++++++++++++- tests/exchange/test_binance.py | 45 ++++++++++++++++++++++++ tests/exchange/test_exchange.py | 61 +++++++++++++++++++++++++++++---- 6 files changed, 245 insertions(+), 23 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index a9d3db129..7de179c0c 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -1,6 +1,6 @@ """ Binance exchange subclass """ import logging -from typing import Dict +from typing import Dict, Optional import ccxt @@ -95,5 +95,43 @@ class Binance(Exchange): except ccxt.BaseError as e: raise OperationalException(e) from e - def apply_leverage_to_stake_amount(self, stake_amount: float, leverage: float): + def _apply_leverage_to_stake_amount(self, stake_amount: float, leverage: float): return stake_amount / leverage + + def fill_leverage_brackets(self): + """ + Assigns property _leverage_brackets to a dictionary of information about the leverage + allowed on each pair + """ + 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 + ] + + def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: + """ + Returns the maximum leverage that a pair can be traded at + :param pair: The base/quote currency pair being traded + :nominal_value: The total value of the trade in quote currency (collateral + debt) + """ + pair_brackets = self._leverage_brackets[pair] + max_lev = 1.0 + for [min_amount, margin_req] in pair_brackets: + print(nominal_value, min_amount) + if nominal_value >= min_amount: + max_lev = 1/margin_req + return max_lev + + def set_leverage(self, pair, leverage): + """ + Binance Futures must set the leverage before making a futures trade, in order to not + have the same leverage on every trade + """ + self._api.set_leverage(symbol=pair, leverage=leverage) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ed9521639..fa3ec3c9b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -69,6 +69,8 @@ class Exchange: } _ft_has: Dict = {} + _leverage_brackets: Dict + def __init__(self, config: Dict[str, Any], validate: bool = True) -> None: """ Initializes this module with the given config, @@ -156,6 +158,16 @@ class Exchange: self.markets_refresh_interval: int = exchange_config.get( "markets_refresh_interval", 60) * 60 + leverage = config.get('leverage_mode') + if leverage is not False: + try: + # TODO-lev: This shouldn't need to happen, but for some reason I get that the + # TODO-lev: method isn't implemented + self.fill_leverage_brackets() + except Exception as error: + logger.debug(error) + logger.debug("Could not load leverage_brackets") + def __del__(self): """ Destructor - clean up async stuff @@ -346,6 +358,7 @@ class Exchange: # Also reload async markets to avoid issues with newly listed pairs self._load_async_markets(reload=True) self._last_markets_refresh = arrow.utcnow().int_timestamp + self.fill_leverage_brackets() except ccxt.BaseError: logger.exception("Could not reload markets.") @@ -561,12 +574,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.apply_leverage_to_stake_amount( + return self._apply_leverage_to_stake_amount( max(min_stake_amounts) * amount_reserve_percent, leverage or 1.0 ) - def apply_leverage_to_stake_amount(self, stake_amount: float, leverage: float): + def _apply_leverage_to_stake_amount(self, stake_amount: float, leverage: float): """ # * 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 @@ -701,14 +714,6 @@ class Exchange: raise InvalidOrderException( f'Tried to get an invalid dry-run-order (id: {order_id}). Message: {e}') from e - def get_max_leverage(self, pair: str, stake_amount: float, price: float) -> float: - """ - Gets the maximum leverage available on this pair - """ - - raise OperationalException(f"Leverage is not available on {self.name} using freqtrade") - return 1.0 - # Order handling def create_order(self, pair: str, ordertype: str, side: str, amount: float, @@ -1520,13 +1525,32 @@ class Exchange: # TODO-lev: implement return 0.0005 + 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.") + + def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: + """ + Returns the maximum leverage that a pair can be traded at + :param pair: The base/quote currency pair being traded + :nominal_value: The total value of the trade in quote currency (collateral + debt) + """ + raise OperationalException( + f"{self.name.capitalize()}.get_max_leverage has not been implemented.") + def set_leverage(self, pair, leverage): """ - Binance Futures must set the leverage before making a futures trade, in order to not + Set's the leverage before making a trade, in order to not have the same leverage on every trade - # TODO-lev: This may be the case for any futures exchange, or even margin trading on - # TODO-lev: some exchanges, so check this """ + raise OperationalException( + f"{self.name.capitalize()}.set_leverage has not been implemented.") + self._api.set_leverage(symbol=pair, leverage=leverage) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index aca060d2b..64e728761 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -1,6 +1,6 @@ """ FTX exchange subclass """ import logging -from typing import Any, Dict +from typing import Any, Dict, Optional import ccxt @@ -156,3 +156,30 @@ class Ftx(Exchange): if order['type'] == 'stop': return safe_value_fallback2(order, order, 'id_stop', 'id') return order['id'] + + def _apply_leverage_to_stake_amount(self, stake_amount: float, leverage: float): + # TODO-lev: implement + return stake_amount + + def fill_leverage_brackets(self): + """ + FTX leverage is static across the account, and doesn't change from pair to pair, + so _leverage_brackets doesn't need to be set + """ + return + + def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: + """ + Returns the maximum leverage that a pair can be traded at, which is always 20 on ftx + :param pair: Here for super method, not used on FTX + :nominal_value: Here for super method, not used on FTX + """ + return 20.0 + + def set_leverage(self, pair, leverage): + """ + Sets the leverage used for the user's account + :param pair: Here for super method, not used on FTX + :param leverage: + """ + self._api.private_post_account_leverage({'leverage': leverage}) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 303c4d885..358a1991c 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -1,6 +1,6 @@ """ Kraken exchange subclass """ import logging -from typing import Any, Dict +from typing import Any, Dict, Optional import ccxt @@ -127,3 +127,42 @@ class Kraken(Exchange): f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e + + def fill_leverage_brackets(self): + """ + Assigns property _leverage_brackets to a dictionary of information about the leverage + allowed on each pair + """ + # TODO-lev: Not sure if this works correctly for futures + leverages = {} + for pair, market in self._api.load_markets().items(): + info = market['info'] + leverage_buy = info['leverage_buy'] + leverage_sell = info['leverage_sell'] + if len(info['leverage_buy']) > 0 or len(info['leverage_sell']) > 0: + if leverage_buy != leverage_sell: + print(f"\033[91m The buy leverage != the sell leverage for {pair}." + "please let freqtrade know because this has never happened before" + ) + if max(leverage_buy) < max(leverage_sell): + leverages[pair] = leverage_buy + else: + leverages[pair] = leverage_sell + else: + leverages[pair] = leverage_buy + self._leverage_brackets = leverages + + def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: + """ + Returns the maximum leverage that a pair can be traded at + :param pair: The base/quote currency pair being traded + :nominal_value: Here for super class, not needed on Kraken + """ + return float(max(self._leverage_brackets[pair])) + + def set_leverage(self, pair, leverage): + """ + Kraken set's the leverage as an option it the order object, so it doesn't do + anything in this function + """ + return diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 7b324efa2..aba185134 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -106,3 +106,48 @@ def test_stoploss_adjust_binance(mocker, default_conf): # Test with invalid order case order['type'] = 'stop_loss' assert not exchange.stoploss_adjust(1501, order, side="sell") + + +@pytest.mark.parametrize('pair,nominal_value,max_lev', [ + ("BNB/BUSD", 0.0, 40.0), + ("BNB/USDT", 100.0, 153.84615384615384), + ("BTC/USDT", 170.30, 250.0), + ("BNB/BUSD", 999999.9, 10.0), + ("BNB/USDT", 5000000.0, 6.666666666666667), + ("BTC/USDT", 300000000.1, 2.0), +]) +def test_get_max_leverage_binance( + default_conf, + mocker, + pair, + nominal_value, + max_lev +): + exchange = get_patched_exchange(mocker, default_conf, id="binance") + exchange._leverage_brackets = { + '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/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]], + } + assert exchange.get_max_leverage(pair, nominal_value) == max_lev diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 3a0dbb258..2a6de95d2 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -442,11 +442,6 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: ) -def apply_leverage_to_stake_amount(): - # TODO-lev - return - - def test_set_sandbox(default_conf, mocker): """ Test working scenario @@ -2893,7 +2888,61 @@ def test_calculate_backoff(retrycount, max_retries, expected): assert calculate_backoff(retrycount, max_retries) == expected -def test_get_max_leverage(): +@pytest.mark.parametrize('exchange,stake_amount,leverage,min_stake_with_lev', [ + ('binance', 9.0, 3.0, 3.0), + ('binance', 20.0, 5.0, 4.0), + ('binance', 100.0, 100.0, 1.0), + # Kraken + ('kraken', 9.0, 3.0, 9.0), + ('kraken', 20.0, 5.0, 20.0), + ('kraken', 100.0, 100.0, 100.0), + # FTX + # TODO-lev: - implement FTX tests + # ('ftx', 9.0, 3.0, 10.0), + # ('ftx', 20.0, 5.0, 20.0), + # ('ftx', 100.0, 100.0, 100.0), +]) +def test_apply_leverage_to_stake_amount( + exchange, + stake_amount, + leverage, + min_stake_with_lev, + mocker, + 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 + + +@pytest.mark.parametrize('exchange_name,pair,nominal_value,max_lev', [ + # Kraken + ("kraken", "ADA/BTC", 0.0, 3.0), + ("kraken", "BTC/EUR", 100.0, 5.0), + ("kraken", "ZEC/USD", 173.31, 2.0), + # FTX + ("ftx", "ADA/BTC", 0.0, 20.0), + ("ftx", "BTC/EUR", 100.0, 20.0), + ("ftx", "ZEC/USD", 173.31, 20.0), + # Binance tests this method inside it's own test file +]) +def test_get_max_leverage( + default_conf, + mocker, + exchange_name, + pair, + nominal_value, + max_lev +): + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange._leverage_brackets = { + 'ADA/BTC': ['2', '3'], + 'BTC/EUR': ['2', '3', '4', '5'], + 'ZEC/USD': ['2'] + } + assert exchange.get_max_leverage(pair, nominal_value) == max_lev + + +def test_fill_leverage_brackets(): # TODO-lev return From 84bc4dd740317610d4e6c3d1345035345cafc94a Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 20 Aug 2021 18:50:02 -0600 Subject: [PATCH 008/134] Removed some outdated TODOs and whitespace --- freqtrade/exchange/exchange.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index fa3ec3c9b..54db415c3 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -540,7 +540,6 @@ class Exchange: def get_min_pair_stake_amount(self, pair: str, price: float, stoploss: float, leverage: Optional[float] = 1.0) -> Optional[float]: - # TODO-lev: Using leverage makes the min stake amount lower (on binance at least) try: market = self.markets[pair] except KeyError: @@ -1551,8 +1550,6 @@ class Exchange: raise OperationalException( f"{self.name.capitalize()}.set_leverage has not been implemented.") - self._api.set_leverage(symbol=pair, leverage=leverage) - def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) From f5fd8dcc05e3cd264ea8720a886f804f0160dcb4 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 21 Aug 2021 01:13:51 -0600 Subject: [PATCH 009/134] Added error handlers to api functions and made a logger warning in fill_leverage_brackets --- freqtrade/exchange/binance.py | 41 +++++++++++++++++++++++++---------- freqtrade/exchange/ftx.py | 10 ++++++++- freqtrade/exchange/kraken.py | 38 +++++++++++++++++++------------- 3 files changed, 61 insertions(+), 28 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 7de179c0c..4199f41ab 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -103,17 +103,26 @@ class Binance(Exchange): Assigns property _leverage_brackets to a dictionary of information about the leverage allowed on each pair """ - 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 - ] + 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 + ] + + 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: """ @@ -134,4 +143,12 @@ class Binance(Exchange): Binance Futures must set the leverage before making a futures trade, in order to not have the same leverage on every trade """ - self._api.set_leverage(symbol=pair, leverage=leverage) + try: + 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: + raise TemporaryError( + f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 64e728761..8ffba92c7 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -182,4 +182,12 @@ class Ftx(Exchange): :param pair: Here for super method, not used on FTX :param leverage: """ - self._api.private_post_account_leverage({'leverage': leverage}) + try: + self._api.private_post_account_leverage({'leverage': leverage}) + 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 diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 358a1991c..e020f7fd8 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -135,22 +135,30 @@ class Kraken(Exchange): """ # TODO-lev: Not sure if this works correctly for futures leverages = {} - for pair, market in self._api.load_markets().items(): - info = market['info'] - leverage_buy = info['leverage_buy'] - leverage_sell = info['leverage_sell'] - if len(info['leverage_buy']) > 0 or len(info['leverage_sell']) > 0: - if leverage_buy != leverage_sell: - print(f"\033[91m The buy leverage != the sell leverage for {pair}." - "please let freqtrade know because this has never happened before" - ) - if max(leverage_buy) < max(leverage_sell): - leverages[pair] = leverage_buy + try: + for pair, market in self._api.load_markets().items(): + info = market['info'] + leverage_buy = info['leverage_buy'] + leverage_sell = info['leverage_sell'] + if len(info['leverage_buy']) > 0 or len(info['leverage_sell']) > 0: + if leverage_buy != leverage_sell: + logger.warning(f"The buy leverage != the sell leverage for {pair}. Please" + "let freqtrade know because this has never happened before" + ) + if max(leverage_buy) < max(leverage_sell): + leverages[pair] = leverage_buy + else: + leverages[pair] = leverage_sell else: - leverages[pair] = leverage_sell - else: - leverages[pair] = leverage_buy - self._leverage_brackets = leverages + leverages[pair] = leverage_buy + self._leverage_brackets = leverages + 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: """ From 4ac223793748e6e92992df39e718a6b4b5363976 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 21 Aug 2021 16:26:04 -0600 Subject: [PATCH 010/134] Changed ftx set_leverage implementation --- freqtrade/exchange/binance.py | 16 ---------------- freqtrade/exchange/exchange.py | 13 ++++++++++--- freqtrade/exchange/ftx.py | 16 ---------------- 3 files changed, 10 insertions(+), 35 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 4199f41ab..b243e9779 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -133,22 +133,6 @@ class Binance(Exchange): pair_brackets = self._leverage_brackets[pair] max_lev = 1.0 for [min_amount, margin_req] in pair_brackets: - print(nominal_value, min_amount) if nominal_value >= min_amount: max_lev = 1/margin_req return max_lev - - def set_leverage(self, pair, leverage): - """ - Binance Futures must set the leverage before making a futures trade, in order to not - have the same leverage on every trade - """ - try: - 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: - raise TemporaryError( - f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e - except ccxt.BaseError as e: - raise OperationalException(e) from e diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 54db415c3..aae8eb08e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1542,13 +1542,20 @@ class Exchange: raise OperationalException( f"{self.name.capitalize()}.get_max_leverage has not been implemented.") - def set_leverage(self, pair, leverage): + def set_leverage(self, leverage: float, pair: Optional[str]): """ Set's the leverage before making a trade, in order to not have the same leverage on every trade """ - raise OperationalException( - f"{self.name.capitalize()}.set_leverage has not been implemented.") + try: + 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: + raise TemporaryError( + f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 8ffba92c7..9ed220806 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -175,19 +175,3 @@ class Ftx(Exchange): :nominal_value: Here for super method, not used on FTX """ return 20.0 - - def set_leverage(self, pair, leverage): - """ - Sets the leverage used for the user's account - :param pair: Here for super method, not used on FTX - :param leverage: - """ - try: - self._api.private_post_account_leverage({'leverage': leverage}) - 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 From 6ac0ab02336767cf738d331d9c5fa14601e6cd38 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 21 Aug 2021 21:10:03 -0600 Subject: [PATCH 011/134] Added short functionality to exchange stoplss methods --- freqtrade/exchange/binance.py | 28 ++++++++++++++++------------ freqtrade/exchange/ftx.py | 17 +++++++++-------- freqtrade/exchange/kraken.py | 21 ++++++++++----------- freqtrade/persistence/models.py | 1 - 4 files changed, 35 insertions(+), 32 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index b243e9779..3721136ea 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -30,8 +30,11 @@ class Binance(Exchange): Returns True if adjustment is necessary. :param side: "buy" or "sell" """ - # TODO-lev: Short support - return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice']) + + return order['type'] == 'stop_loss_limit' and ( + side == "sell" and stop_loss > float(order['info']['stopPrice']) or + side == "buy" and stop_loss < float(order['info']['stopPrice']) + ) @retrier(retries=0) def stoploss(self, pair: str, amount: float, @@ -42,7 +45,6 @@ class Binance(Exchange): It may work with a limited number of other exchanges, but this has not been tested yet. :param side: "buy" or "sell" """ - # TODO-lev: Short support # Limit price threshold: As limit price should always be below stop-price limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) rate = stop_price * limit_price_pct @@ -51,14 +53,16 @@ class Binance(Exchange): stop_price = self.price_to_precision(pair, stop_price) + bad_stop_price = (stop_price <= rate) if side == "sell" else (stop_price >= rate) + # Ensure rate is less than stop price - if stop_price <= rate: + if bad_stop_price: raise OperationalException( - 'In stoploss limit order, stop price should be more than limit price') + 'In stoploss limit order, stop price should be better than limit price') if self._config['dry_run']: dry_order = self.create_dry_run_order( - pair, ordertype, "sell", amount, stop_price) + pair, ordertype, side, amount, stop_price) return dry_order try: @@ -69,7 +73,7 @@ class Binance(Exchange): rate = self.price_to_precision(pair, rate) - order = self._api.create_order(symbol=pair, type=ordertype, side='sell', + 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) @@ -77,21 +81,21 @@ class Binance(Exchange): return order except ccxt.InsufficientFunds as e: raise InsufficientFundsError( - f'Insufficient funds to create {ordertype} sell order on market {pair}. ' - f'Tried to sell amount {amount} at rate {rate}. ' + f'Insufficient funds to create {ordertype} {side} order on market {pair}. ' + f'Tried to {side} amount {amount} at rate {rate}. ' f'Message: {e}') from e except ccxt.InvalidOrder as e: # Errors: # `binance Order would trigger immediately.` raise InvalidOrderException( - f'Could not create {ordertype} sell order on market {pair}. ' - f'Tried to sell amount {amount} at rate {rate}. ' + f'Could not create {ordertype} {side} order on market {pair}. ' + f'Tried to {side} amount {amount} at rate {rate}. ' f'Message: {e}') from e except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e + f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 9ed220806..bd8350853 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -36,8 +36,10 @@ class Ftx(Exchange): Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. """ - # TODO-lev: Short support - return order['type'] == 'stop' and stop_loss > float(order['price']) + return order['type'] == 'stop' and ( + side == "sell" and stop_loss > float(order['price']) or + side == "buy" and stop_loss < float(order['price']) + ) @retrier(retries=0) def stoploss(self, pair: str, amount: float, @@ -48,7 +50,6 @@ class Ftx(Exchange): Limit orders are defined by having orderPrice set, otherwise a market order is used. """ - # TODO-lev: Short support limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) limit_rate = stop_price * limit_price_pct @@ -59,7 +60,7 @@ class Ftx(Exchange): if self._config['dry_run']: dry_order = self.create_dry_run_order( - pair, ordertype, "sell", amount, stop_price) + pair, ordertype, side, amount, stop_price) return dry_order try: @@ -71,7 +72,7 @@ class Ftx(Exchange): params['stopPrice'] = stop_price amount = self.amount_to_precision(pair, amount) - order = self._api.create_order(symbol=pair, type=ordertype, side='sell', + 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. ' @@ -79,19 +80,19 @@ class Ftx(Exchange): return order except ccxt.InsufficientFunds as e: raise InsufficientFundsError( - f'Insufficient funds to create {ordertype} sell order on market {pair}. ' + f'Insufficient funds to create {ordertype} {side} order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e except ccxt.InvalidOrder as e: raise InvalidOrderException( - f'Could not create {ordertype} sell order on market {pair}. ' + f'Could not create {ordertype} {side} order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e + f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index e020f7fd8..f12ac0c20 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -72,18 +72,18 @@ class Kraken(Exchange): Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. """ - # TODO-lev: Short support - return (order['type'] in ('stop-loss', 'stop-loss-limit') - and stop_loss > float(order['price'])) + return (order['type'] in ('stop-loss', 'stop-loss-limit') and ( + (side == "sell" and stop_loss > float(order['price'])) or + (side == "buy" and stop_loss < float(order['price'])) + )) - @retrier(retries=0) + @ retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict, side: str) -> Dict: """ Creates a stoploss market order. Stoploss market orders is the only stoploss type supported by kraken. """ - # TODO-lev: Short support params = self._params.copy() if order_types.get('stoploss', 'market') == 'limit': @@ -98,13 +98,13 @@ class Kraken(Exchange): if self._config['dry_run']: dry_order = self.create_dry_run_order( - pair, ordertype, "sell", amount, stop_price) + pair, ordertype, side, amount, stop_price) return dry_order try: amount = self.amount_to_precision(pair, amount) - order = self._api.create_order(symbol=pair, type=ordertype, side='sell', + 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. ' @@ -112,19 +112,19 @@ class Kraken(Exchange): return order except ccxt.InsufficientFunds as e: raise InsufficientFundsError( - f'Insufficient funds to create {ordertype} sell order on market {pair}. ' + f'Insufficient funds to create {ordertype} {side} order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e except ccxt.InvalidOrder as e: raise InvalidOrderException( - f'Could not create {ordertype} sell order on market {pair}. ' + f'Could not create {ordertype} {side} order on market {pair}. ' f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. ' f'Message: {e}') from e except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e + f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e @@ -133,7 +133,6 @@ class Kraken(Exchange): Assigns property _leverage_brackets to a dictionary of information about the leverage allowed on each pair """ - # TODO-lev: Not sure if this works correctly for futures leverages = {} try: for pair, market in self._api.load_markets().items(): diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 2d8aa0738..70a038c31 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -499,7 +499,6 @@ class LocalTrade(): lower_stop = new_loss < self.stop_loss # stop losses only walk up, never down!, - # TODO-lev # ? But adding more to a leveraged trade would create a lower liquidation price, # ? decreasing the minimum stoploss if (higher_stop and not self.is_short) or (lower_stop and self.is_short): From 70ebf0987161f43aa1aa13eefdcf373e1e72fa82 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 22 Aug 2021 20:58:22 -0600 Subject: [PATCH 012/134] exchange - kraken - minor changes --- freqtrade/exchange/kraken.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index f12ac0c20..567bd6735 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -77,7 +77,7 @@ class Kraken(Exchange): (side == "buy" and stop_loss < float(order['price'])) )) - @ retrier(retries=0) + @retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict, side: str) -> Dict: """ @@ -135,7 +135,7 @@ class Kraken(Exchange): """ leverages = {} try: - for pair, market in self._api.load_markets().items(): + for pair, market in self.markets.items(): info = market['info'] leverage_buy = info['leverage_buy'] leverage_sell = info['leverage_sell'] From 0a624e70eee585b9e9b2fb82d235eab1f32ae0e6 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 22 Aug 2021 23:28:03 -0600 Subject: [PATCH 013/134] added tests for min stake amount with leverage --- tests/exchange/test_exchange.py | 53 +++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 2a6de95d2..cf976c68c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -302,7 +302,6 @@ def test_price_get_one_pip(default_conf, mocker, price, precision_mode, precisio def test_get_min_pair_stake_amount(mocker, default_conf) -> None: - # TODO-lev: Test with leverage exchange = get_patched_exchange(mocker, default_conf, id="binance") stoploss = -0.05 markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}} @@ -374,7 +373,12 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) - assert isclose(result, 2 * (1+0.05) / (1-abs(stoploss))) + expected_result = 2 * (1+0.05) / (1-abs(stoploss)) + assert isclose(result, expected_result) + # 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"] = { @@ -386,7 +390,12 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert isclose(result, 2 * 2 * (1+0.05) / (1-abs(stoploss))) + expected_result = 2 * 2 * (1+0.05) / (1-abs(stoploss)) + assert isclose(result, expected_result) + # 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"] = { @@ -398,7 +407,12 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert isclose(result, max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss))) + expected_result = max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss)) + assert isclose(result, expected_result) + # 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"] = { @@ -410,18 +424,32 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert isclose(result, max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss))) + expected_result = max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss)) + assert isclose(result, expected_result) + # 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) - assert isclose(result, max(8, 2 * 2) * 1.5) + expected_result = max(8, 2 * 2) * 1.5 + assert isclose(result, expected_result) + # 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) - assert isclose(result, max(8, 2 * 2) * 1.5) + expected_result = max(8, 2 * 2) * 1.5 + assert isclose(result, expected_result) + # 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: - # TODO-lev: Test with leverage exchange = get_patched_exchange(mocker, default_conf, id="binance") stoploss = -0.05 markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}} @@ -436,10 +464,11 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) - assert round(result, 8) == round( - max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)), - 8 - ) + expected_result = max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)) + 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): From e5b2b64a3f3d26333e87e82182b970b9a58d76fb Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 22 Aug 2021 23:36:36 -0600 Subject: [PATCH 014/134] Changed stoploss side on some tests --- freqtrade/exchange/ftx.py | 1 - tests/test_freqtradebot.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index bd8350853..1dc30002e 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -50,7 +50,6 @@ class Ftx(Exchange): Limit orders are defined by having orderPrice set, otherwise a market order is used. """ - limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) limit_rate = stop_price * limit_price_pct diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 61a90dc3f..9c420aa65 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1384,7 +1384,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c side_effect=InvalidOrderException()) mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', return_value=stoploss_order_hanging) - freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging, side="buy") + freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging, side="sell") assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog) # Still try to create order @@ -1394,7 +1394,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c caplog.clear() cancel_mock = mocker.patch("freqtrade.exchange.Binance.cancel_stoploss_order", MagicMock()) mocker.patch("freqtrade.exchange.Binance.stoploss", side_effect=ExchangeError()) - freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging, side="buy") + freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging, side="sell") assert cancel_mock.call_count == 1 assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog) From 073426f25c8ea297534eae467c2e148e788e49cf Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 1 Sep 2021 23:40:32 -0600 Subject: [PATCH 015/134] set margin mode exchange function --- freqtrade/exchange/exchange.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index aae8eb08e..8ab568145 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1557,6 +1557,9 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e + def set_margin_mode(self, symbol, marginType, params={}): + self._api.set_margin_mode(symbol, marginType, params) + def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: return exchange_name in ccxt_exchanges(ccxt_module) From c7a2e6c2c65ab1598496b3ae97757ba40c92ece6 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 3 Sep 2021 18:11:39 -0600 Subject: [PATCH 016/134] completed set_margin_mode --- freqtrade/exchange/exchange.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 8ab568145..c53c00e0d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -21,6 +21,7 @@ from pandas import DataFrame from freqtrade.constants import DEFAULT_AMOUNT_RESERVE_PERCENT, ListPairsWithTimeframes from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list +from freqtrade.enums import Collateral from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, RetryableOrderError, TemporaryError) @@ -1557,8 +1558,24 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - def set_margin_mode(self, symbol, marginType, params={}): - self._api.set_margin_mode(symbol, marginType, params) + def set_margin_mode(self, symbol: str, collateral: Collateral, params: dict = {}): + ''' + 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"): + # Some exchanges only support one collateral type + return + + try: + self._api.set_margin_mode(symbol, collateral.value, params) + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool: From 1b20b4f3c7dfa758cde6afb3d887b3f9b7c567aa Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 3 Sep 2021 19:00:04 -0600 Subject: [PATCH 017/134] Wrote failing tests for exchange.set_leverage and exchange.set_margin_mode --- freqtrade/exchange/exchange.py | 2 +- tests/exchange/test_exchange.py | 113 +++++++++++++++++++++++++++++++- 2 files changed, 111 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index c53c00e0d..264d0400d 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1573,7 +1573,7 @@ class Exchange: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e + f'Could not set margin mode due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index cf976c68c..004b8c019 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -10,6 +10,7 @@ import ccxt import pytest from pandas import DataFrame +from freqtrade.enums import Collateral from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException, OperationalException, PricingError, TemporaryError) from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken @@ -2972,6 +2973,71 @@ def test_get_max_leverage( def test_fill_leverage_brackets(): + api_mock = MagicMock() + api_mock.set_leverage = MagicMock(return_value=[ + { + 'amount': 0.14542341, + 'code': 'USDT', + 'datetime': '2021-09-01T08:00:01.000Z', + 'id': '485478', + 'info': {'asset': 'USDT', + 'income': '0.14542341', + 'incomeType': 'FUNDING_FEE', + 'info': 'FUNDING_FEE', + 'symbol': 'XRPUSDT', + 'time': '1630512001000', + 'tradeId': '', + 'tranId': '4854789484855218760'}, + 'symbol': 'XRP/USDT', + 'timestamp': 1630512001000 + }, + { + 'amount': -0.14642341, + 'code': 'USDT', + 'datetime': '2021-09-01T16:00:01.000Z', + 'id': '485479', + 'info': {'asset': 'USDT', + 'income': '-0.14642341', + 'incomeType': 'FUNDING_FEE', + 'info': 'FUNDING_FEE', + 'symbol': 'XRPUSDT', + 'time': '1630512001000', + 'tradeId': '', + 'tranId': '4854789484855218760'}, + 'symbol': 'XRP/USDT', + 'timestamp': 1630512001000 + } + ]) + type(api_mock).has = PropertyMock(return_value={'fetchFundingHistory': True}) + + # mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', lambda pair, since: y) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + date_time = datetime.strptime("2021-09-01T00:00:01.000Z", '%Y-%m-%dT%H:%M:%S.%fZ') + unix_time = int(date_time.strftime('%s')) + expected_fees = -0.001 # 0.14542341 + -0.14642341 + fees_from_datetime = exchange.get_funding_fees( + pair='XRP/USDT', + since=date_time + ) + fees_from_unix_time = exchange.get_funding_fees( + pair='XRP/USDT', + since=unix_time + ) + + assert(isclose(expected_fees, fees_from_datetime)) + assert(isclose(expected_fees, fees_from_unix_time)) + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + exchange_name, + "get_funding_fees", + "fetch_funding_history", + pair="XRP/USDT", + since=unix_time + ) + # TODO-lev return @@ -2981,6 +3047,47 @@ def test_get_interest_rate(): return -def test_set_leverage(): - # TODO-lev - return +@pytest.mark.parametrize("collateral", [ + (Collateral.CROSS), + (Collateral.ISOLATED) +]) +@pytest.mark.parametrize("exchange_name", [("ftx"), ("binance")]) +def test_set_leverage(mocker, default_conf, exchange_name, collateral): + + api_mock = MagicMock() + api_mock.set_leverage = MagicMock() + type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + exchange_name, + "set_leverage", + "set_leverage", + symbol="XRP/USDT", + collateral=collateral + ) + + +@pytest.mark.parametrize("collateral", [ + (Collateral.CROSS), + (Collateral.ISOLATED) +]) +@pytest.mark.parametrize("exchange_name", [("ftx"), ("binance")]) +def test_set_margin_mode(mocker, default_conf, exchange_name, collateral): + + api_mock = MagicMock() + api_mock.set_leverage = MagicMock() + type(api_mock).has = PropertyMock(return_value={'setMarginMode': True}) + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + exchange_name, + "set_margin_mode", + "set_margin_mode", + symbol="XRP/USDT", + collateral=collateral + ) From 9b953f6e60ef8d2c0bfced9df78e34af81559e1e Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 3 Sep 2021 19:25:16 -0600 Subject: [PATCH 018/134] split test_get_max_leverage into separate exchange files --- tests/exchange/test_binance.py | 8 +-- tests/exchange/test_exchange.py | 101 +------------------------------- tests/exchange/test_ftx.py | 10 ++++ tests/exchange/test_kraken.py | 15 +++++ 4 files changed, 29 insertions(+), 105 deletions(-) diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index aba185134..4cf8485a7 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -116,13 +116,7 @@ def test_stoploss_adjust_binance(mocker, default_conf): ("BNB/USDT", 5000000.0, 6.666666666666667), ("BTC/USDT", 300000000.1, 2.0), ]) -def test_get_max_leverage_binance( - default_conf, - mocker, - pair, - nominal_value, - max_lev -): +def test_get_max_leverage_binance(default_conf, mocker, pair, nominal_value, max_lev): exchange = get_patched_exchange(mocker, default_conf, id="binance") exchange._leverage_brackets = { 'BNB/BUSD': [[0.0, 0.025], diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 004b8c019..0eb7ceb25 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2927,10 +2927,9 @@ def test_calculate_backoff(retrycount, max_retries, expected): ('kraken', 20.0, 5.0, 20.0), ('kraken', 100.0, 100.0, 100.0), # FTX - # TODO-lev: - implement FTX tests - # ('ftx', 9.0, 3.0, 10.0), - # ('ftx', 20.0, 5.0, 20.0), - # ('ftx', 100.0, 100.0, 100.0), + ('ftx', 9.0, 3.0, 9.0), + ('ftx', 20.0, 5.0, 20.0), + ('ftx', 100.0, 100.0, 100.0) ]) def test_apply_leverage_to_stake_amount( exchange, @@ -2944,101 +2943,7 @@ def test_apply_leverage_to_stake_amount( assert exchange._apply_leverage_to_stake_amount(stake_amount, leverage) == min_stake_with_lev -@pytest.mark.parametrize('exchange_name,pair,nominal_value,max_lev', [ - # Kraken - ("kraken", "ADA/BTC", 0.0, 3.0), - ("kraken", "BTC/EUR", 100.0, 5.0), - ("kraken", "ZEC/USD", 173.31, 2.0), - # FTX - ("ftx", "ADA/BTC", 0.0, 20.0), - ("ftx", "BTC/EUR", 100.0, 20.0), - ("ftx", "ZEC/USD", 173.31, 20.0), - # Binance tests this method inside it's own test file -]) -def test_get_max_leverage( - default_conf, - mocker, - exchange_name, - pair, - nominal_value, - max_lev -): - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) - exchange._leverage_brackets = { - 'ADA/BTC': ['2', '3'], - 'BTC/EUR': ['2', '3', '4', '5'], - 'ZEC/USD': ['2'] - } - assert exchange.get_max_leverage(pair, nominal_value) == max_lev - - def test_fill_leverage_brackets(): - api_mock = MagicMock() - api_mock.set_leverage = MagicMock(return_value=[ - { - 'amount': 0.14542341, - 'code': 'USDT', - 'datetime': '2021-09-01T08:00:01.000Z', - 'id': '485478', - 'info': {'asset': 'USDT', - 'income': '0.14542341', - 'incomeType': 'FUNDING_FEE', - 'info': 'FUNDING_FEE', - 'symbol': 'XRPUSDT', - 'time': '1630512001000', - 'tradeId': '', - 'tranId': '4854789484855218760'}, - 'symbol': 'XRP/USDT', - 'timestamp': 1630512001000 - }, - { - 'amount': -0.14642341, - 'code': 'USDT', - 'datetime': '2021-09-01T16:00:01.000Z', - 'id': '485479', - 'info': {'asset': 'USDT', - 'income': '-0.14642341', - 'incomeType': 'FUNDING_FEE', - 'info': 'FUNDING_FEE', - 'symbol': 'XRPUSDT', - 'time': '1630512001000', - 'tradeId': '', - 'tranId': '4854789484855218760'}, - 'symbol': 'XRP/USDT', - 'timestamp': 1630512001000 - } - ]) - type(api_mock).has = PropertyMock(return_value={'fetchFundingHistory': True}) - - # mocker.patch('freqtrade.exchange.Exchange.get_funding_fees', lambda pair, since: y) - exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - date_time = datetime.strptime("2021-09-01T00:00:01.000Z", '%Y-%m-%dT%H:%M:%S.%fZ') - unix_time = int(date_time.strftime('%s')) - expected_fees = -0.001 # 0.14542341 + -0.14642341 - fees_from_datetime = exchange.get_funding_fees( - pair='XRP/USDT', - since=date_time - ) - fees_from_unix_time = exchange.get_funding_fees( - pair='XRP/USDT', - since=unix_time - ) - - assert(isclose(expected_fees, fees_from_datetime)) - assert(isclose(expected_fees, fees_from_unix_time)) - - ccxt_exceptionhandlers( - mocker, - default_conf, - api_mock, - exchange_name, - "get_funding_fees", - "fetch_funding_history", - pair="XRP/USDT", - since=unix_time - ) - - # TODO-lev return diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 76b01dd35..8b44b6069 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -193,3 +193,13 @@ def test_get_order_id(mocker, default_conf): } } assert exchange.get_order_id_conditional(order) == '1111' + + +@pytest.mark.parametrize('pair,nominal_value,max_lev', [ + ("ADA/BTC", 0.0, 20.0), + ("BTC/EUR", 100.0, 20.0), + ("ZEC/USD", 173.31, 20.0), +]) +def test_get_max_leverage_ftx(default_conf, mocker, pair, nominal_value, max_lev): + exchange = get_patched_exchange(mocker, default_conf, id="ftx") + assert exchange.get_max_leverage(pair, nominal_value) == max_lev diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 60250fc71..db53ffc48 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -255,3 +255,18 @@ def test_stoploss_adjust_kraken(mocker, default_conf): # Test with invalid order case ... order['type'] = 'stop_loss_limit' assert not exchange.stoploss_adjust(1501, order, side="sell") + + +@pytest.mark.parametrize('pair,nominal_value,max_lev', [ + ("ADA/BTC", 0.0, 3.0), + ("BTC/EUR", 100.0, 5.0), + ("ZEC/USD", 173.31, 2.0), +]) +def test_get_max_leverage_kraken(default_conf, mocker, pair, nominal_value, max_lev): + exchange = get_patched_exchange(mocker, default_conf, id="kraken") + exchange._leverage_brackets = { + 'ADA/BTC': ['2', '3'], + 'BTC/EUR': ['2', '3', '4', '5'], + 'ZEC/USD': ['2'] + } + assert exchange.get_max_leverage(pair, nominal_value) == max_lev From 9d398924c649f80bdeba6c83d5e0c16c2de721c8 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 3 Sep 2021 19:56:13 -0600 Subject: [PATCH 019/134] Wrote dummy tests for exchange.get_interest_rate --- freqtrade/exchange/exchange.py | 4 ++-- tests/exchange/test_exchange.py | 34 ++++++++++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 264d0400d..8bf6d14d1 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1521,9 +1521,9 @@ class Exchange: self._async_get_trade_history(pair=pair, since=since, until=until, from_id=from_id)) - def get_interest_rate(self, pair: str, open_rate: float, is_short: bool) -> float: + def get_interest_rate(self, pair: str, maker_or_taker: str, is_short: bool) -> float: # TODO-lev: implement - return 0.0005 + return (0.0005, 0.0005) def fill_leverage_brackets(self): """ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 0eb7ceb25..710b70afe 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2947,9 +2947,37 @@ def test_fill_leverage_brackets(): return -def test_get_interest_rate(): - # TODO-lev - return +# TODO-lev: These tests don't test anything real, they need to be replaced with real values once +# get_interest_rates is written +@pytest.mark.parametrize('exchange_name,pair,maker_or_taker,is_short,borrow_rate,interest_rate', [ + ('binance', "ADA/USDT", "maker", True, 0.0005, 0.0005), + ('binance', "ADA/USDT", "maker", False, 0.0005, 0.0005), + ('binance', "ADA/USDT", "taker", True, 0.0005, 0.0005), + ('binance', "ADA/USDT", "taker", False, 0.0005, 0.0005), + # Kraken + ('kraken', "ADA/USDT", "maker", True, 0.0005, 0.0005), + ('kraken', "ADA/USDT", "maker", False, 0.0005, 0.0005), + ('kraken', "ADA/USDT", "taker", True, 0.0005, 0.0005), + ('kraken', "ADA/USDT", "taker", False, 0.0005, 0.0005), + # FTX + ('ftx', "ADA/USDT", "maker", True, 0.0005, 0.0005), + ('ftx', "ADA/USDT", "maker", False, 0.0005, 0.0005), + ('ftx', "ADA/USDT", "taker", True, 0.0005, 0.0005), + ('ftx', "ADA/USDT", "taker", False, 0.0005, 0.0005), +]) +def test_get_interest_rate( + default_conf, + mocker, + exchange_name, + pair, + maker_or_taker, + is_short, + borrow_rate, + interest_rate +): + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + assert exchange.get_interest_rate( + pair, maker_or_taker, is_short) == (borrow_rate, interest_rate) @pytest.mark.parametrize("collateral", [ From 01263663be1938d2fdd42a06bfc369956b165224 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 3 Sep 2021 19:56:53 -0600 Subject: [PATCH 020/134] ftx.fill_leverage_brackets test --- freqtrade/exchange/exchange.py | 2 +- tests/exchange/test_ftx.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 8bf6d14d1..b2869df11 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -70,7 +70,7 @@ class Exchange: } _ft_has: Dict = {} - _leverage_brackets: Dict + _leverage_brackets: Dict = {} def __init__(self, config: Dict[str, Any], validate: bool = True) -> None: """ diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 8b44b6069..0f3870a7f 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -195,6 +195,12 @@ def test_get_order_id(mocker, default_conf): assert exchange.get_order_id_conditional(order) == '1111' +def test_fill_leverage_brackets(default_conf, mocker): + exchange = get_patched_exchange(mocker, default_conf, id="ftx") + exchange.fill_leverage_brackets() + assert bool(exchange._leverage_brackets) is False + + @pytest.mark.parametrize('pair,nominal_value,max_lev', [ ("ADA/BTC", 0.0, 20.0), ("BTC/EUR", 100.0, 20.0), From c5d97d07a8e931e0ec89bd66e9e89293fe3ec2da Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 3 Sep 2021 20:20:42 -0600 Subject: [PATCH 021/134] Added failing fill_leverage_brackets test to test_kraken --- freqtrade/exchange/kraken.py | 2 +- tests/exchange/test_ftx.py | 1 + tests/exchange/test_kraken.py | 230 ++++++++++++++++++++++++++++++++++ 3 files changed, 232 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 567bd6735..052e7cac5 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -135,7 +135,7 @@ class Kraken(Exchange): """ leverages = {} try: - for pair, market in self.markets.items(): + for pair, market in self._api.markets.items(): info = market['info'] leverage_buy = info['leverage_buy'] leverage_sell = info['leverage_sell'] diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 0f3870a7f..b3deae3de 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -196,6 +196,7 @@ def test_get_order_id(mocker, default_conf): def test_fill_leverage_brackets(default_conf, mocker): + # FTX only has one account wide leverage, so there's no leverage brackets exchange = get_patched_exchange(mocker, default_conf, id="ftx") exchange.fill_leverage_brackets() assert bool(exchange._leverage_brackets) is False diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index db53ffc48..eddef08b8 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -270,3 +270,233 @@ def test_get_max_leverage_kraken(default_conf, mocker, pair, nominal_value, max_ 'ZEC/USD': ['2'] } assert exchange.get_max_leverage(pair, nominal_value) == max_lev + + +def test_fill_leverage_brackets_kraken(default_conf, mocker): + api_mock = MagicMock() + api_mock.load_markets = MagicMock(return_value={{ + "ADA/BTC": {'active': True, + 'altname': 'ADAXBT', + 'base': 'ADA', + 'baseId': 'ADA', + 'darkpool': False, + 'id': 'ADAXBT', + 'info': {'aclass_base': 'currency', + 'aclass_quote': 'currency', + 'altname': 'ADAXBT', + 'base': 'ADA', + 'fee_volume_currency': 'ZUSD', + 'fees': [['0', '0.26'], + ['50000', '0.24'], + ['100000', '0.22'], + ['250000', '0.2'], + ['500000', '0.18'], + ['1000000', '0.16'], + ['2500000', '0.14'], + ['5000000', '0.12'], + ['10000000', '0.1']], + 'fees_maker': [['0', '0.16'], + ['50000', '0.14'], + ['100000', '0.12'], + ['250000', '0.1'], + ['500000', '0.08'], + ['1000000', '0.06'], + ['2500000', '0.04'], + ['5000000', '0.02'], + ['10000000', '0']], + 'leverage_buy': ['2', '3'], + 'leverage_sell': ['2', '3'], + 'lot': 'unit', + 'lot_decimals': '8', + 'lot_multiplier': '1', + 'margin_call': '80', + 'margin_stop': '40', + 'ordermin': '5', + 'pair_decimals': '8', + 'quote': 'XXBT', + 'wsname': 'ADA/XBT'}, + 'limits': {'amount': {'max': 100000000.0, 'min': 5.0}, + 'cost': {'max': None, 'min': 0}, + 'price': {'max': None, 'min': 1e-08}}, + 'maker': 0.0016, + 'percentage': True, + 'precision': {'amount': 8, 'price': 8}, + 'quote': 'BTC', + 'quoteId': 'XXBT', + 'symbol': 'ADA/BTC', + 'taker': 0.0026, + 'tierBased': True, + 'tiers': {'maker': [[0, 0.0016], + [50000, 0.0014], + [100000, 0.0012], + [250000, 0.001], + [500000, 0.0008], + [1000000, 0.0006], + [2500000, 0.0004], + [5000000, 0.0002], + [10000000, 0.0]], + 'taker': [[0, 0.0026], + [50000, 0.0024], + [100000, 0.0022], + [250000, 0.002], + [500000, 0.0018], + [1000000, 0.0016], + [2500000, 0.0014], + [5000000, 0.0012], + [10000000, 0.0001]]}}, + "BTC/EUR": {'active': True, + 'altname': 'XBTEUR', + 'base': 'BTC', + 'baseId': 'XXBT', + 'darkpool': False, + 'id': 'XXBTZEUR', + 'info': {'aclass_base': 'currency', + 'aclass_quote': 'currency', + 'altname': 'XBTEUR', + 'base': 'XXBT', + 'fee_volume_currency': 'ZUSD', + 'fees': [['0', '0.26'], + ['50000', '0.24'], + ['100000', '0.22'], + ['250000', '0.2'], + ['500000', '0.18'], + ['1000000', '0.16'], + ['2500000', '0.14'], + ['5000000', '0.12'], + ['10000000', '0.1']], + 'fees_maker': [['0', '0.16'], + ['50000', '0.14'], + ['100000', '0.12'], + ['250000', '0.1'], + ['500000', '0.08'], + ['1000000', '0.06'], + ['2500000', '0.04'], + ['5000000', '0.02'], + ['10000000', '0']], + 'leverage_buy': ['2', '3', '4', '5'], + 'leverage_sell': ['2', '3', '4', '5'], + 'lot': 'unit', + 'lot_decimals': '8', + 'lot_multiplier': '1', + 'margin_call': '80', + 'margin_stop': '40', + 'ordermin': '0.0001', + 'pair_decimals': '1', + 'quote': 'ZEUR', + 'wsname': 'XBT/EUR'}, + 'limits': {'amount': {'max': 100000000.0, 'min': 0.0001}, + 'cost': {'max': None, 'min': 0}, + 'price': {'max': None, 'min': 0.1}}, + 'maker': 0.0016, + 'percentage': True, + 'precision': {'amount': 8, 'price': 1}, + 'quote': 'EUR', + 'quoteId': 'ZEUR', + 'symbol': 'BTC/EUR', + 'taker': 0.0026, + 'tierBased': True, + 'tiers': {'maker': [[0, 0.0016], + [50000, 0.0014], + [100000, 0.0012], + [250000, 0.001], + [500000, 0.0008], + [1000000, 0.0006], + [2500000, 0.0004], + [5000000, 0.0002], + [10000000, 0.0]], + 'taker': [[0, 0.0026], + [50000, 0.0024], + [100000, 0.0022], + [250000, 0.002], + [500000, 0.0018], + [1000000, 0.0016], + [2500000, 0.0014], + [5000000, 0.0012], + [10000000, 0.0001]]}}, + "ZEC/USD": {'active': True, + 'altname': 'ZECUSD', + 'base': 'ZEC', + 'baseId': 'XZEC', + 'darkpool': False, + 'id': 'XZECZUSD', + 'info': {'aclass_base': 'currency', + 'aclass_quote': 'currency', + 'altname': 'ZECUSD', + 'base': 'XZEC', + 'fee_volume_currency': 'ZUSD', + 'fees': [['0', '0.26'], + ['50000', '0.24'], + ['100000', '0.22'], + ['250000', '0.2'], + ['500000', '0.18'], + ['1000000', '0.16'], + ['2500000', '0.14'], + ['5000000', '0.12'], + ['10000000', '0.1']], + 'fees_maker': [['0', '0.16'], + ['50000', '0.14'], + ['100000', '0.12'], + ['250000', '0.1'], + ['500000', '0.08'], + ['1000000', '0.06'], + ['2500000', '0.04'], + ['5000000', '0.02'], + ['10000000', '0']], + 'leverage_buy': ['2'], + 'leverage_sell': ['2'], + 'lot': 'unit', + 'lot_decimals': '8', + 'lot_multiplier': '1', + 'margin_call': '80', + 'margin_stop': '40', + 'ordermin': '0.035', + 'pair_decimals': '2', + 'quote': 'ZUSD', + 'wsname': 'ZEC/USD'}, + 'limits': {'amount': {'max': 100000000.0, 'min': 0.035}, + 'cost': {'max': None, 'min': 0}, + 'price': {'max': None, 'min': 0.01}}, + 'maker': 0.0016, + 'percentage': True, + 'precision': {'amount': 8, 'price': 2}, + 'quote': 'USD', + 'quoteId': 'ZUSD', + 'symbol': 'ZEC/USD', + 'taker': 0.0026, + 'tierBased': True, + 'tiers': {'maker': [[0, 0.0016], + [50000, 0.0014], + [100000, 0.0012], + [250000, 0.001], + [500000, 0.0008], + [1000000, 0.0006], + [2500000, 0.0004], + [5000000, 0.0002], + [10000000, 0.0]], + 'taker': [[0, 0.0026], + [50000, 0.0024], + [100000, 0.0022], + [250000, 0.002], + [500000, 0.0018], + [1000000, 0.0016], + [2500000, 0.0014], + [5000000, 0.0012], + [10000000, 0.0001]]}} + + }}) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") + + assert exchange._leverage_brackets == { + 'ADA/BTC': ['2', '3'], + 'BTC/EUR': ['2', '3', '4', '5'], + 'ZEC/USD': ['2'] + } + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + "kraken", + "fill_leverage_brackets", + "fill_leverage_brackets" + ) From 95bd0721ae45eb4463f5d77a0885d5be7f658613 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 3 Sep 2021 20:30:19 -0600 Subject: [PATCH 022/134] Rearranged tests at end of ftx to match other exchanges --- tests/exchange/test_ftx.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index b3deae3de..771065cdd 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -195,13 +195,6 @@ def test_get_order_id(mocker, default_conf): assert exchange.get_order_id_conditional(order) == '1111' -def test_fill_leverage_brackets(default_conf, mocker): - # FTX only has one account wide leverage, so there's no leverage brackets - exchange = get_patched_exchange(mocker, default_conf, id="ftx") - exchange.fill_leverage_brackets() - assert bool(exchange._leverage_brackets) is False - - @pytest.mark.parametrize('pair,nominal_value,max_lev', [ ("ADA/BTC", 0.0, 20.0), ("BTC/EUR", 100.0, 20.0), @@ -210,3 +203,10 @@ def test_fill_leverage_brackets(default_conf, mocker): def test_get_max_leverage_ftx(default_conf, mocker, pair, nominal_value, max_lev): exchange = get_patched_exchange(mocker, default_conf, id="ftx") assert exchange.get_max_leverage(pair, nominal_value) == max_lev + + +def test_fill_leverage_brackets_ftx(default_conf, mocker): + # FTX only has one account wide leverage, so there's no leverage brackets + exchange = get_patched_exchange(mocker, default_conf, id="ftx") + exchange.fill_leverage_brackets() + assert bool(exchange._leverage_brackets) is False From aac1094078829026d64d2fc57bdd176200824135 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 3 Sep 2021 20:30:52 -0600 Subject: [PATCH 023/134] Wrote failing test_fill_leverage_brackets_binance --- tests/exchange/test_binance.py | 64 ++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 4cf8485a7..bc4cfaa36 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -145,3 +145,67 @@ def test_get_max_leverage_binance(default_conf, mocker, pair, nominal_value, max [300000000.0, 0.5]], } assert exchange.get_max_leverage(pair, nominal_value) == max_lev + + +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']], + + }}) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + + 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']], + } + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + "binance", + "fill_leverage_brackets", + "fill_leverage_brackets" + ) From 61fdf74ad984d2020d2c87cae9ed5d7c9df5e1c4 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 4 Sep 2021 19:16:17 -0600 Subject: [PATCH 024/134] Added retrier to exchange functions and reduced failing tests down to 2 --- freqtrade/exchange/exchange.py | 21 ++++++++++++++++++--- tests/exchange/test_binance.py | 4 ++-- tests/exchange/test_exchange.py | 6 +++--- tests/exchange/test_kraken.py | 4 ++-- 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b2869df11..a3102856a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1521,10 +1521,23 @@ class Exchange: self._async_get_trade_history(pair=pair, since=since, until=until, from_id=from_id)) - def get_interest_rate(self, pair: str, maker_or_taker: str, is_short: bool) -> float: + @retrier + def get_interest_rate( + self, + pair: str, + maker_or_taker: str, + is_short: bool + ) -> Tuple[float, float]: + """ + :param pair: base/quote currency pair + :param maker_or_taker: "maker" if limit order, "taker" if market order + :param is_short: True if requesting base interest, False if requesting quote interest + :return: (open_interest, rollover_interest) + """ # TODO-lev: implement return (0.0005, 0.0005) + @retrier def fill_leverage_brackets(self): """ #TODO-lev: Should maybe be renamed, leverage_brackets might not be accurate for kraken @@ -1543,6 +1556,7 @@ class Exchange: raise OperationalException( f"{self.name.capitalize()}.get_max_leverage has not been implemented.") + @retrier def set_leverage(self, leverage: float, pair: Optional[str]): """ Set's the leverage before making a trade, in order to not @@ -1558,7 +1572,8 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - def set_margin_mode(self, symbol: str, collateral: Collateral, params: dict = {}): + @retrier + def set_margin_mode(self, pair: str, collateral: Collateral, params: dict = {}): ''' 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") @@ -1568,7 +1583,7 @@ class Exchange: return try: - self._api.set_margin_mode(symbol, collateral.value, params) + self._api.set_margin_mode(pair, collateral.value, params) except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index bc4cfaa36..aa4c4c62e 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -149,7 +149,7 @@ 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={{ + api_mock.load_leverage_brackets = MagicMock(return_value={ 'ADA/BUSD': [[0.0, '0.025'], [100000.0, '0.05'], [500000.0, '0.1'], @@ -173,7 +173,7 @@ def test_fill_leverage_brackets_binance(default_conf, mocker): [250000.0, '0.125'], [1000000.0, '0.5']], - }}) + }) exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") assert exchange._leverage_brackets == { diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 710b70afe..a5e9f6d4c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2998,8 +2998,8 @@ def test_set_leverage(mocker, default_conf, exchange_name, collateral): exchange_name, "set_leverage", "set_leverage", - symbol="XRP/USDT", - collateral=collateral + pair="XRP/USDT", + leverage=5.0 ) @@ -3021,6 +3021,6 @@ def test_set_margin_mode(mocker, default_conf, exchange_name, collateral): exchange_name, "set_margin_mode", "set_margin_mode", - symbol="XRP/USDT", + pair="XRP/USDT", collateral=collateral ) diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index eddef08b8..90c032679 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -274,7 +274,7 @@ def test_get_max_leverage_kraken(default_conf, mocker, pair, nominal_value, max_ def test_fill_leverage_brackets_kraken(default_conf, mocker): api_mock = MagicMock() - api_mock.load_markets = MagicMock(return_value={{ + api_mock.load_markets = MagicMock(return_value={ "ADA/BTC": {'active': True, 'altname': 'ADAXBT', 'base': 'ADA', @@ -483,7 +483,7 @@ def test_fill_leverage_brackets_kraken(default_conf, mocker): [5000000, 0.0012], [10000000, 0.0001]]}} - }}) + }) exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") assert exchange._leverage_brackets == { From 6ec2e40736e31a8e747cd607936e30888fa2a516 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 4 Sep 2021 19:47:04 -0600 Subject: [PATCH 025/134] Added exceptions to exchange.interest_rate --- freqtrade/exchange/exchange.py | 13 +++++++++++-- tests/exchange/test_exchange.py | 24 ++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a3102856a..4aaa273a9 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1529,13 +1529,22 @@ class Exchange: is_short: bool ) -> Tuple[float, float]: """ + Gets the rate of interest for borrowed currency when margin trading :param pair: base/quote currency pair :param maker_or_taker: "maker" if limit order, "taker" if market order :param is_short: True if requesting base interest, False if requesting quote interest :return: (open_interest, rollover_interest) """ - # TODO-lev: implement - return (0.0005, 0.0005) + try: + # TODO-lev: implement, currently there is no ccxt method for this + return (0.0005, 0.0005) + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e @retrier def fill_leverage_brackets(self): diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index a5e9f6d4c..148545392 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2980,6 +2980,30 @@ def test_get_interest_rate( pair, maker_or_taker, is_short) == (borrow_rate, interest_rate) +@pytest.mark.parametrize("exchange_name", [("binance"), ("ftx"), ("kraken")]) +@pytest.mark.parametrize("maker_or_taker", [("maker"), ("taker")]) +@pytest.mark.parametrize("is_short", [(True), (False)]) +def test_get_interest_rate_exceptions(mocker, default_conf, exchange_name, maker_or_taker, is_short): + + # api_mock = MagicMock() + # # TODO-lev: get_interest_rate currently not implemented on CCXT, so this may need to be renamed + # api_mock.get_interest_rate = MagicMock() + # type(api_mock).has = PropertyMock(return_value={'getInterestRate': True}) + + # ccxt_exceptionhandlers( + # mocker, + # default_conf, + # api_mock, + # exchange_name, + # "get_interest_rate", + # "get_interest_rate", + # pair="XRP/USDT", + # is_short=is_short, + # maker_or_taker="maker_or_taker" + # ) + return + + @pytest.mark.parametrize("collateral", [ (Collateral.CROSS), (Collateral.ISOLATED) From d4389eb07dabbeb2a36c598f0f9da6ff7c64963a Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 4 Sep 2021 19:58:42 -0600 Subject: [PATCH 026/134] fill_leverage_brackets usinge self.markets.items instead of self._api.markets.items --- freqtrade/exchange/kraken.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 052e7cac5..567bd6735 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -135,7 +135,7 @@ class Kraken(Exchange): """ leverages = {} try: - for pair, market in self._api.markets.items(): + for pair, market in self.markets.items(): info = market['info'] leverage_buy = info['leverage_buy'] leverage_sell = info['leverage_sell'] From 23ba49fec2de9fa00728bc70bb8245fc98a542a4 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 4 Sep 2021 21:55:55 -0600 Subject: [PATCH 027/134] Added validating checks for trading_mode and collateral on each exchange --- freqtrade/exchange/binance.py | 10 +++++- freqtrade/exchange/exchange.py | 59 ++++++++++++++++++++++++------- freqtrade/exchange/ftx.py | 9 ++++- freqtrade/exchange/kraken.py | 17 ++++++--- tests/exchange/test_exchange.py | 61 +++++++++++++++++++++++++++++++-- 5 files changed, 133 insertions(+), 23 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 3721136ea..d1506cb6f 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -1,9 +1,10 @@ """ Binance exchange subclass """ import logging -from typing import Dict, Optional +from typing import Dict, List, Optional, Tuple import ccxt +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -24,6 +25,13 @@ class Binance(Exchange): "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], } + _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ + # 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 + ] + 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 06538314e..9abdc9b0b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -22,7 +22,7 @@ from pandas import DataFrame from freqtrade.constants import (DEFAULT_AMOUNT_RESERVE_PERCENT, NON_OPEN_EXCHANGE_STATES, ListPairsWithTimeframes) from freqtrade.data.converter import ohlcv_to_dataframe, trades_dict_to_list -from freqtrade.enums import Collateral +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, RetryableOrderError, TemporaryError) @@ -73,6 +73,10 @@ class Exchange: _leverage_brackets: Dict = {} + _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ + # TradingMode.SPOT always supported and not required in this list + ] + def __init__(self, config: Dict[str, Any], validate: bool = True) -> None: """ Initializes this module with the given config, @@ -138,6 +142,26 @@ class Exchange: self._api_async = self._init_ccxt( exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config) + trading_mode: TradingMode = ( + TradingMode(config.get('trading_mode')) + if config.get('trading_mode') + else TradingMode.SPOT + ) + collateral: Optional[Collateral] = ( + Collateral(config.get('collateral')) + if config.get('collateral') + else None + ) + + if trading_mode != TradingMode.SPOT: + try: + # TODO-lev: This shouldn't need to happen, but for some reason I get that the + # TODO-lev: method isn't implemented + self.fill_leverage_brackets() + except Exception as error: + logger.debug(error) + logger.debug("Could not load leverage_brackets") + logger.info('Using Exchange "%s"', self.name) if validate: @@ -155,21 +179,11 @@ 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(trading_mode, collateral) # Converts the interval provided in minutes in config to seconds self.markets_refresh_interval: int = exchange_config.get( "markets_refresh_interval", 60) * 60 - leverage = config.get('leverage_mode') - if leverage is not False: - try: - # TODO-lev: This shouldn't need to happen, but for some reason I get that the - # TODO-lev: method isn't implemented - self.fill_leverage_brackets() - except Exception as error: - logger.debug(error) - logger.debug("Could not load leverage_brackets") - def __del__(self): """ Destructor - clean up async stuff @@ -376,7 +390,7 @@ class Exchange: raise OperationalException( 'Could not load markets, therefore cannot start. ' 'Please investigate the above error for more details.' - ) + ) quote_currencies = self.get_quote_currencies() if stake_currency not in quote_currencies: raise OperationalException( @@ -488,6 +502,25 @@ class Exchange: f"This strategy requires {startup_candles} candles to start. " f"{self.name} only provides {candle_limit} for {timeframe}.") + def validate_trading_mode_and_collateral( + self, + trading_mode: TradingMode, + collateral: Optional[Collateral] # Only None when trading_mode = TradingMode.SPOT + ): + """ + Checks if freqtrade can perform trades using the configured + trading mode(Margin, Futures) and Collateral(Cross, Isolated) + Throws OperationalException: + If the trading_mode/collateral type are not supported by freqtrade on this exchange + """ + if trading_mode != TradingMode.SPOT and ( + (trading_mode, collateral) not in self._supported_trading_mode_collateral_pairs + ): + collateral_value = collateral and collateral.value + raise OperationalException( + f"Freqtrade does not support {collateral_value} {trading_mode.value} on {self.name}" + ) + def exchange_has(self, endpoint: str) -> bool: """ Checks if exchange implements a specific API endpoint. diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 1dc30002e..3e6ff01a3 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -1,9 +1,10 @@ """ FTX exchange subclass """ import logging -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional, Tuple import ccxt +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -21,6 +22,12 @@ class Ftx(Exchange): "ohlcv_candle_limit": 1500, } + _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ + # 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 + ] + def market_is_tradable(self, market: Dict[str, Any]) -> bool: """ Check if the market symbol is tradable by Freqtrade. diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 567bd6735..7c36c421b 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -1,9 +1,10 @@ """ Kraken exchange subclass """ import logging -from typing import Any, Dict, Optional +from typing import Any, Dict, List, Optional, Tuple import ccxt +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, OperationalException, TemporaryError) from freqtrade.exchange import Exchange @@ -23,6 +24,12 @@ class Kraken(Exchange): "trades_pagination_arg": "since", } + _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ + # 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: No CCXT support + ] + def market_is_tradable(self, market: Dict[str, Any]) -> bool: """ Check if the market symbol is tradable by Freqtrade. @@ -33,7 +40,7 @@ class Kraken(Exchange): return (parent_check and market.get('darkpool', False) is False) - @retrier + @ retrier def get_balances(self) -> dict: if self._config['dry_run']: return {} @@ -48,8 +55,8 @@ class Kraken(Exchange): orders = self._api.fetch_open_orders() order_list = [(x["symbol"].split("/")[0 if x["side"] == "sell" else 1], - x["remaining"] if x["side"] == "sell" else x["remaining"] * x["price"], - # Don't remove the below comment, this can be important for debugging + x["remaining"] if x["side"] == "sell" else x["remaining"] * x["price"], + # Don't remove the below comment, this can be important for debugging # x["side"], x["amount"], ) for x in orders] for bal in balances: @@ -77,7 +84,7 @@ class Kraken(Exchange): (side == "buy" and stop_loss < float(order['price'])) )) - @retrier(retries=0) + @ retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict, side: str) -> Dict: """ diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 729cdf373..a1d417b0a 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -10,7 +10,7 @@ import ccxt import pytest from pandas import DataFrame -from freqtrade.enums import Collateral +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException, OperationalException, PricingError, TemporaryError) from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken @@ -3027,10 +3027,16 @@ def test_get_interest_rate( @pytest.mark.parametrize("exchange_name", [("binance"), ("ftx"), ("kraken")]) @pytest.mark.parametrize("maker_or_taker", [("maker"), ("taker")]) @pytest.mark.parametrize("is_short", [(True), (False)]) -def test_get_interest_rate_exceptions(mocker, default_conf, exchange_name, maker_or_taker, is_short): +def test_get_interest_rate_exceptions( + mocker, + default_conf, + exchange_name, + maker_or_taker, + is_short +): # api_mock = MagicMock() - # # TODO-lev: get_interest_rate currently not implemented on CCXT, so this may need to be renamed + # # TODO-lev: get_interest_rate currently not implemented on CCXT, so this may be renamed # api_mock.get_interest_rate = MagicMock() # type(api_mock).has = PropertyMock(return_value={'getInterestRate': True}) @@ -3092,3 +3098,52 @@ def test_set_margin_mode(mocker, default_conf, exchange_name, collateral): pair="XRP/USDT", collateral=collateral ) + + +@pytest.mark.parametrize("exchange_name, trading_mode, collateral, exception_thrown", [ + ("binance", TradingMode.SPOT, None, False), + ("binance", TradingMode.MARGIN, Collateral.ISOLATED, True), + ("kraken", TradingMode.SPOT, None, False), + ("kraken", TradingMode.MARGIN, Collateral.ISOLATED, True), + ("kraken", TradingMode.FUTURES, Collateral.ISOLATED, True), + ("ftx", TradingMode.SPOT, None, False), + ("ftx", TradingMode.MARGIN, Collateral.ISOLATED, True), + ("ftx", TradingMode.FUTURES, Collateral.ISOLATED, True), + ("bittrex", TradingMode.SPOT, None, False), + ("bittrex", TradingMode.MARGIN, Collateral.CROSS, True), + ("bittrex", TradingMode.MARGIN, Collateral.ISOLATED, True), + ("bittrex", TradingMode.FUTURES, Collateral.CROSS, True), + ("bittrex", TradingMode.FUTURES, Collateral.ISOLATED, True), + + # 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), + ("ftx", TradingMode.FUTURES, Collateral.CROSS, True), + + # TODO-lev: Uncomment once implemented + # ("binance", TradingMode.MARGIN, Collateral.CROSS, False), + # ("binance", TradingMode.FUTURES, Collateral.CROSS, False), + # ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False), + # ("kraken", TradingMode.MARGIN, Collateral.CROSS, False), + # ("kraken", TradingMode.FUTURES, Collateral.CROSS, False), + # ("ftx", TradingMode.MARGIN, Collateral.ISOLATED, False), + # ("ftx", TradingMode.FUTURES, Collateral.ISOLATED, False) +]) +def test_validate_trading_mode_and_collateral( + default_conf, + mocker, + exchange_name, + trading_mode, + collateral, + exception_thrown +): + exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + if (exception_thrown): + with pytest.raises(OperationalException): + exchange.validate_trading_mode_and_collateral(trading_mode, collateral) + else: + exchange.validate_trading_mode_and_collateral(trading_mode, collateral) From 8822b73f9c1b67c0b03fa9f55c223995d396b379 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 5 Sep 2021 22:27:14 -0600 Subject: [PATCH 028/134] test_fill_leverage_brackets_kraken and test_get_max_leverage_binance now pass but test_fill_leverage_brackets_ftx does not if called after test_get_max_leverage_binance --- freqtrade/exchange/binance.py | 5 +- freqtrade/exchange/exchange.py | 8 +- freqtrade/exchange/kraken.py | 42 +++--- tests/conftest.py | 27 +++- tests/exchange/test_binance.py | 97 +++++++------- tests/exchange/test_exchange.py | 6 +- tests/exchange/test_ftx.py | 2 +- tests/exchange/test_kraken.py | 226 +------------------------------- 8 files changed, 103 insertions(+), 310 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index d1506cb6f..e399e96e7 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -110,6 +110,7 @@ class Binance(Exchange): def _apply_leverage_to_stake_amount(self, stake_amount: float, leverage: float): return stake_amount / leverage + @retrier def fill_leverage_brackets(self): """ Assigns property _leverage_brackets to a dictionary of information about the leverage @@ -117,8 +118,8 @@ class Binance(Exchange): """ try: leverage_brackets = self._api.load_leverage_brackets() - for pair, brackets in leverage_brackets.items: - self.leverage_brackets[pair] = [ + for pair, brackets in leverage_brackets.items(): + self._leverage_brackets[pair] = [ [ min_amount, float(margin_req) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 9abdc9b0b..b3d5e0e0f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -154,13 +154,7 @@ class Exchange: ) if trading_mode != TradingMode.SPOT: - try: - # TODO-lev: This shouldn't need to happen, but for some reason I get that the - # TODO-lev: method isn't implemented - self.fill_leverage_brackets() - except Exception as error: - logger.debug(error) - logger.debug("Could not load leverage_brackets") + self.fill_leverage_brackets() logger.info('Using Exchange "%s"', self.name) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 7c36c421b..5207018ad 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -141,30 +141,24 @@ class Kraken(Exchange): allowed on each pair """ leverages = {} - try: - for pair, market in self.markets.items(): - info = market['info'] - leverage_buy = info['leverage_buy'] - leverage_sell = info['leverage_sell'] - if len(info['leverage_buy']) > 0 or len(info['leverage_sell']) > 0: - if leverage_buy != leverage_sell: - logger.warning(f"The buy leverage != the sell leverage for {pair}. Please" - "let freqtrade know because this has never happened before" - ) - if max(leverage_buy) < max(leverage_sell): - leverages[pair] = leverage_buy - else: - leverages[pair] = leverage_sell - else: + + for pair, market in self.markets.items(): + info = market['info'] + leverage_buy = info['leverage_buy'] if 'leverage_buy' in info else [] + leverage_sell = info['leverage_sell'] if 'leverage_sell' in info else [] + if len(leverage_buy) > 0 or len(leverage_sell) > 0: + if leverage_buy != leverage_sell: + logger.warning( + f"The buy({leverage_buy}) and sell({leverage_sell}) leverage are not equal" + "{pair}. Please let freqtrade know because this has never happened before" + ) + if max(leverage_buy) < max(leverage_sell): leverages[pair] = leverage_buy - self._leverage_brackets = leverages - 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 + else: + leverages[pair] = leverage_sell + else: + leverages[pair] = leverage_buy + self._leverage_brackets = leverages def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: """ @@ -176,7 +170,7 @@ class Kraken(Exchange): def set_leverage(self, pair, leverage): """ - Kraken set's the leverage as an option it the order object, so it doesn't do + Kraken set's the leverage as an option in the order object, so it doesn't do anything in this function """ return diff --git a/tests/conftest.py b/tests/conftest.py index 188236f40..f4cbef686 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -437,7 +437,10 @@ def get_markets(): 'max': 500000, }, }, - 'info': {}, + 'info': { + 'leverage_buy': ['2'], + 'leverage_sell': ['2'], + }, }, 'TKN/BTC': { 'id': 'tknbtc', @@ -463,7 +466,10 @@ def get_markets(): 'max': 500000, }, }, - 'info': {}, + 'info': { + 'leverage_buy': ['2', '3', '4', '5'], + 'leverage_sell': ['2', '3', '4', '5'], + }, }, 'BLK/BTC': { 'id': 'blkbtc', @@ -488,7 +494,10 @@ def get_markets(): 'max': 500000, }, }, - 'info': {}, + 'info': { + 'leverage_buy': ['2', '3'], + 'leverage_sell': ['2', '3'], + }, }, 'LTC/BTC': { 'id': 'ltcbtc', @@ -513,7 +522,10 @@ def get_markets(): 'max': 500000, }, }, - 'info': {}, + 'info': { + 'leverage_buy': [], + 'leverage_sell': [], + }, }, 'XRP/BTC': { 'id': 'xrpbtc', @@ -591,7 +603,10 @@ def get_markets(): 'max': None } }, - 'info': {}, + 'info': { + 'leverage_buy': [], + 'leverage_sell': [], + }, }, 'ETH/USDT': { 'id': 'USDT-ETH', @@ -707,6 +722,8 @@ def get_markets(): 'max': None } }, + 'info': { + } }, } diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index aa4c4c62e..f2bd68154 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -1,5 +1,5 @@ from random import randint -from unittest.mock import MagicMock +from unittest.mock import MagicMock, PropertyMock import ccxt import pytest @@ -150,62 +150,67 @@ 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']], + '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.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']], + '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() + type(api_mock).has = PropertyMock(return_value={'loadLeverageBrackets': True}) + ccxt_exceptionhandlers( mocker, default_conf, api_mock, "binance", "fill_leverage_brackets", - "fill_leverage_brackets" + "load_leverage_brackets" ) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index a1d417b0a..509f5404e 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3085,7 +3085,7 @@ def test_set_leverage(mocker, default_conf, exchange_name, collateral): def test_set_margin_mode(mocker, default_conf, exchange_name, collateral): api_mock = MagicMock() - api_mock.set_leverage = MagicMock() + api_mock.set_margin_mode = MagicMock() type(api_mock).has = PropertyMock(return_value={'setMarginMode': True}) ccxt_exceptionhandlers( @@ -3130,8 +3130,8 @@ def test_set_margin_mode(mocker, default_conf, exchange_name, collateral): # ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False), # ("kraken", TradingMode.MARGIN, Collateral.CROSS, False), # ("kraken", TradingMode.FUTURES, Collateral.CROSS, False), - # ("ftx", TradingMode.MARGIN, Collateral.ISOLATED, False), - # ("ftx", TradingMode.FUTURES, Collateral.ISOLATED, False) + # ("ftx", TradingMode.MARGIN, Collateral.CROSS, False), + # ("ftx", TradingMode.FUTURES, Collateral.CROSS, False) ]) def test_validate_trading_mode_and_collateral( default_conf, diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 771065cdd..1ed528dd9 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -209,4 +209,4 @@ def test_fill_leverage_brackets_ftx(default_conf, mocker): # FTX only has one account wide leverage, so there's no leverage brackets exchange = get_patched_exchange(mocker, default_conf, id="ftx") exchange.fill_leverage_brackets() - assert bool(exchange._leverage_brackets) is False + assert exchange._leverage_brackets == {} diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 90c032679..8222f5ce8 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -274,229 +274,11 @@ def test_get_max_leverage_kraken(default_conf, mocker, pair, nominal_value, max_ def test_fill_leverage_brackets_kraken(default_conf, mocker): api_mock = MagicMock() - api_mock.load_markets = MagicMock(return_value={ - "ADA/BTC": {'active': True, - 'altname': 'ADAXBT', - 'base': 'ADA', - 'baseId': 'ADA', - 'darkpool': False, - 'id': 'ADAXBT', - 'info': {'aclass_base': 'currency', - 'aclass_quote': 'currency', - 'altname': 'ADAXBT', - 'base': 'ADA', - 'fee_volume_currency': 'ZUSD', - 'fees': [['0', '0.26'], - ['50000', '0.24'], - ['100000', '0.22'], - ['250000', '0.2'], - ['500000', '0.18'], - ['1000000', '0.16'], - ['2500000', '0.14'], - ['5000000', '0.12'], - ['10000000', '0.1']], - 'fees_maker': [['0', '0.16'], - ['50000', '0.14'], - ['100000', '0.12'], - ['250000', '0.1'], - ['500000', '0.08'], - ['1000000', '0.06'], - ['2500000', '0.04'], - ['5000000', '0.02'], - ['10000000', '0']], - 'leverage_buy': ['2', '3'], - 'leverage_sell': ['2', '3'], - 'lot': 'unit', - 'lot_decimals': '8', - 'lot_multiplier': '1', - 'margin_call': '80', - 'margin_stop': '40', - 'ordermin': '5', - 'pair_decimals': '8', - 'quote': 'XXBT', - 'wsname': 'ADA/XBT'}, - 'limits': {'amount': {'max': 100000000.0, 'min': 5.0}, - 'cost': {'max': None, 'min': 0}, - 'price': {'max': None, 'min': 1e-08}}, - 'maker': 0.0016, - 'percentage': True, - 'precision': {'amount': 8, 'price': 8}, - 'quote': 'BTC', - 'quoteId': 'XXBT', - 'symbol': 'ADA/BTC', - 'taker': 0.0026, - 'tierBased': True, - 'tiers': {'maker': [[0, 0.0016], - [50000, 0.0014], - [100000, 0.0012], - [250000, 0.001], - [500000, 0.0008], - [1000000, 0.0006], - [2500000, 0.0004], - [5000000, 0.0002], - [10000000, 0.0]], - 'taker': [[0, 0.0026], - [50000, 0.0024], - [100000, 0.0022], - [250000, 0.002], - [500000, 0.0018], - [1000000, 0.0016], - [2500000, 0.0014], - [5000000, 0.0012], - [10000000, 0.0001]]}}, - "BTC/EUR": {'active': True, - 'altname': 'XBTEUR', - 'base': 'BTC', - 'baseId': 'XXBT', - 'darkpool': False, - 'id': 'XXBTZEUR', - 'info': {'aclass_base': 'currency', - 'aclass_quote': 'currency', - 'altname': 'XBTEUR', - 'base': 'XXBT', - 'fee_volume_currency': 'ZUSD', - 'fees': [['0', '0.26'], - ['50000', '0.24'], - ['100000', '0.22'], - ['250000', '0.2'], - ['500000', '0.18'], - ['1000000', '0.16'], - ['2500000', '0.14'], - ['5000000', '0.12'], - ['10000000', '0.1']], - 'fees_maker': [['0', '0.16'], - ['50000', '0.14'], - ['100000', '0.12'], - ['250000', '0.1'], - ['500000', '0.08'], - ['1000000', '0.06'], - ['2500000', '0.04'], - ['5000000', '0.02'], - ['10000000', '0']], - 'leverage_buy': ['2', '3', '4', '5'], - 'leverage_sell': ['2', '3', '4', '5'], - 'lot': 'unit', - 'lot_decimals': '8', - 'lot_multiplier': '1', - 'margin_call': '80', - 'margin_stop': '40', - 'ordermin': '0.0001', - 'pair_decimals': '1', - 'quote': 'ZEUR', - 'wsname': 'XBT/EUR'}, - 'limits': {'amount': {'max': 100000000.0, 'min': 0.0001}, - 'cost': {'max': None, 'min': 0}, - 'price': {'max': None, 'min': 0.1}}, - 'maker': 0.0016, - 'percentage': True, - 'precision': {'amount': 8, 'price': 1}, - 'quote': 'EUR', - 'quoteId': 'ZEUR', - 'symbol': 'BTC/EUR', - 'taker': 0.0026, - 'tierBased': True, - 'tiers': {'maker': [[0, 0.0016], - [50000, 0.0014], - [100000, 0.0012], - [250000, 0.001], - [500000, 0.0008], - [1000000, 0.0006], - [2500000, 0.0004], - [5000000, 0.0002], - [10000000, 0.0]], - 'taker': [[0, 0.0026], - [50000, 0.0024], - [100000, 0.0022], - [250000, 0.002], - [500000, 0.0018], - [1000000, 0.0016], - [2500000, 0.0014], - [5000000, 0.0012], - [10000000, 0.0001]]}}, - "ZEC/USD": {'active': True, - 'altname': 'ZECUSD', - 'base': 'ZEC', - 'baseId': 'XZEC', - 'darkpool': False, - 'id': 'XZECZUSD', - 'info': {'aclass_base': 'currency', - 'aclass_quote': 'currency', - 'altname': 'ZECUSD', - 'base': 'XZEC', - 'fee_volume_currency': 'ZUSD', - 'fees': [['0', '0.26'], - ['50000', '0.24'], - ['100000', '0.22'], - ['250000', '0.2'], - ['500000', '0.18'], - ['1000000', '0.16'], - ['2500000', '0.14'], - ['5000000', '0.12'], - ['10000000', '0.1']], - 'fees_maker': [['0', '0.16'], - ['50000', '0.14'], - ['100000', '0.12'], - ['250000', '0.1'], - ['500000', '0.08'], - ['1000000', '0.06'], - ['2500000', '0.04'], - ['5000000', '0.02'], - ['10000000', '0']], - 'leverage_buy': ['2'], - 'leverage_sell': ['2'], - 'lot': 'unit', - 'lot_decimals': '8', - 'lot_multiplier': '1', - 'margin_call': '80', - 'margin_stop': '40', - 'ordermin': '0.035', - 'pair_decimals': '2', - 'quote': 'ZUSD', - 'wsname': 'ZEC/USD'}, - 'limits': {'amount': {'max': 100000000.0, 'min': 0.035}, - 'cost': {'max': None, 'min': 0}, - 'price': {'max': None, 'min': 0.01}}, - 'maker': 0.0016, - 'percentage': True, - 'precision': {'amount': 8, 'price': 2}, - 'quote': 'USD', - 'quoteId': 'ZUSD', - 'symbol': 'ZEC/USD', - 'taker': 0.0026, - 'tierBased': True, - 'tiers': {'maker': [[0, 0.0016], - [50000, 0.0014], - [100000, 0.0012], - [250000, 0.001], - [500000, 0.0008], - [1000000, 0.0006], - [2500000, 0.0004], - [5000000, 0.0002], - [10000000, 0.0]], - 'taker': [[0, 0.0026], - [50000, 0.0024], - [100000, 0.0022], - [250000, 0.002], - [500000, 0.0018], - [1000000, 0.0016], - [2500000, 0.0014], - [5000000, 0.0012], - [10000000, 0.0001]]}} - - }) exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") + exchange.fill_leverage_brackets() assert exchange._leverage_brackets == { - 'ADA/BTC': ['2', '3'], - 'BTC/EUR': ['2', '3', '4', '5'], - 'ZEC/USD': ['2'] + 'BLK/BTC': ['2', '3'], + 'TKN/BTC': ['2', '3', '4', '5'], + 'ETH/BTC': ['2'] } - - ccxt_exceptionhandlers( - mocker, - default_conf, - api_mock, - "kraken", - "fill_leverage_brackets", - "fill_leverage_brackets" - ) From f8248f3771150ec35c699cb465e96048cbb3e591 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 8 Sep 2021 00:19:21 -0600 Subject: [PATCH 029/134] comments, formatting --- freqtrade/enums/signaltype.py | 2 +- freqtrade/optimize/backtesting.py | 4 ++-- freqtrade/persistence/models.py | 2 +- freqtrade/plugins/pairlistmanager.py | 2 +- freqtrade/rpc/rpc.py | 1 + tests/test_persistence.py | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/enums/signaltype.py b/freqtrade/enums/signaltype.py index 23316c15a..b1b86fc47 100644 --- a/freqtrade/enums/signaltype.py +++ b/freqtrade/enums/signaltype.py @@ -3,7 +3,7 @@ from enum import Enum class SignalType(Enum): """ - Enum to distinguish between buy and sell signals + Enum to distinguish between enter and exit signals """ ENTER_LONG = "enter_long" EXIT_LONG = "exit_long" diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index cf670f87d..eadfd467a 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -371,7 +371,7 @@ class Backtesting: trade, sell_row[OPEN_IDX], sell_candle_time, # type: ignore enter=enter, exit_=exit_, low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX] - ) + ) if sell.sell_flag: trade.close_date = sell_candle_time @@ -403,7 +403,7 @@ class Backtesting: detail_data = detail_data.loc[ (detail_data['date'] >= sell_candle_time) & (detail_data['date'] < sell_candle_end) - ] + ] if len(detail_data) == 0: # Fall back to "regular" data if no detail data was found for this candle return self._get_sell_trade_entry_for_candle(trade, sell_row) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 630078ab3..0759b40d3 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -548,7 +548,7 @@ class LocalTrade(): if self.is_open: payment = "BUY" if self.is_short else "SELL" # TODO-lev: On shorts, you buy a little bit more than the amount (amount + interest) - # This wll only print the original amount + # TODO-lev: This wll only print the original amount logger.info(f'{order_type.upper()}_{payment} has been fulfilled for {self}.') # TODO-lev: Double check this self.close(safe_value_fallback(order, 'average', 'price')) diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index face79729..93b5e90e2 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -127,7 +127,7 @@ class PairListManager(): :return: pairlist - whitelisted pairs """ try: - + # TODO-lev: filter for pairlists that are able to trade at the desired leverage whitelist = expand_pairlist(pairlist, self._exchange.get_markets().keys(), keep_invalid) except ValueError as err: logger.error(f"Pair whitelist contains an invalid Wildcard: {err}") diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index ca2e84e48..16f16ed67 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -36,6 +36,7 @@ class RPCException(Exception): raise RPCException('*Status:* `no active trade`') """ + # TODO-lev: Add new configuration options introduced with leveraged/short trading def __init__(self, message: str) -> None: super().__init__(self) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 911d7d6c2..1250e7b92 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -90,7 +90,7 @@ def test_enter_exit_side(fee): @pytest.mark.usefixtures("init_persistence") -def test__set_stop_loss_isolated_liq(fee): +def test_set_stop_loss_isolated_liq(fee): trade = Trade( id=2, pair='ADA/USDT', From d811a73ec08a95350dd9d3dde3aa86623707478f Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 8 Sep 2021 00:20:40 -0600 Subject: [PATCH 030/134] new rpc message types --- freqtrade/enums/rpcmessagetype.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/freqtrade/enums/rpcmessagetype.py b/freqtrade/enums/rpcmessagetype.py index 9c59f6108..34b826ec9 100644 --- a/freqtrade/enums/rpcmessagetype.py +++ b/freqtrade/enums/rpcmessagetype.py @@ -5,13 +5,23 @@ class RPCMessageType(Enum): STATUS = 'status' WARNING = 'warning' STARTUP = 'startup' + BUY = 'buy' BUY_FILL = 'buy_fill' BUY_CANCEL = 'buy_cancel' + SELL = 'sell' SELL_FILL = 'sell_fill' SELL_CANCEL = 'sell_cancel' + SHORT = 'short' + SHORT_FILL = 'short_fill' + SHORT_CANCEL = 'short_cancel' + + EXIT_SHORT = 'exit_short' + EXIT_SHORT_FILL = 'exit_short_fill' + EXIT_SHORT_CANCEL = 'exit_short_cancel' + def __repr__(self): return self.value From 763a6af224cb8d7f326e3d43d031115683dccbd1 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 8 Sep 2021 00:24:32 -0600 Subject: [PATCH 031/134] sample strategy has short --- freqtrade/templates/sample_strategy.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py index 574819949..b2d130059 100644 --- a/freqtrade/templates/sample_strategy.py +++ b/freqtrade/templates/sample_strategy.py @@ -58,6 +58,8 @@ class SampleStrategy(IStrategy): # Hyperoptable parameters buy_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) sell_rsi = IntParameter(low=50, high=100, default=70, space='sell', optimize=True, load=True) + short_rsi = IntParameter(low=51, high=100, default=70, space='sell', optimize=True, load=True) + exit_short_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True) # Optimal timeframe for the strategy. timeframe = '5m' @@ -354,6 +356,16 @@ class SampleStrategy(IStrategy): ), 'buy'] = 1 + dataframe.loc[ + ( + # Signal: RSI crosses above 70 + (qtpylib.crossed_above(dataframe['rsi'], self.short_rsi.value)) & + (dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle + (dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling + (dataframe['volume'] > 0) # Make sure Volume is not 0 + ), + 'enter_short'] = 1 + return dataframe def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -372,4 +384,16 @@ class SampleStrategy(IStrategy): (dataframe['volume'] > 0) # Make sure Volume is not 0 ), 'sell'] = 1 + + dataframe.loc[ + ( + # Signal: RSI crosses above 30 + (qtpylib.crossed_above(dataframe['rsi'], self.exit_short_rsi.value)) & + # Guard: tema below BB middle + (dataframe['tema'] <= dataframe['bb_middleband']) & + (dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising + (dataframe['volume'] > 0) # Make sure Volume is not 0 + ), + 'exit_short'] = 1 + return dataframe From 8f38d6276f3e2aebdf9747462efa9b18d25b6b27 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 8 Sep 2021 00:45:55 -0600 Subject: [PATCH 032/134] notify_buy -> notify_enter, notify_sell -> notify_exit --- freqtrade/freqtradebot.py | 28 ++++++++++++++-------------- tests/test_freqtradebot.py | 6 +++--- tests/test_integration.py | 4 ++-- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 85072efcc..8e3166d79 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -422,7 +422,7 @@ class FreqtradeBot(LoggingMixin): # running get_signal on historical data fetched (side, enter_tag) = self.strategy.get_entry_signal( pair, self.strategy.timeframe, analyzed_df - ) + ) if side: stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge) @@ -592,11 +592,11 @@ class FreqtradeBot(LoggingMixin): # Updating wallets self.wallets.update() - self._notify_buy(trade, order_type) + self._notify_enter(trade, order_type) return True - def _notify_buy(self, trade: Trade, order_type: str) -> None: + def _notify_enter(self, trade: Trade, order_type: str) -> None: """ Sends rpc notification when a buy occurred. """ @@ -619,7 +619,7 @@ class FreqtradeBot(LoggingMixin): # Send the message self.rpc.send_msg(msg) - def _notify_buy_cancel(self, trade: Trade, order_type: str, reason: str) -> None: + def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str) -> None: """ Sends rpc notification when a buy cancel occurred. """ @@ -645,7 +645,7 @@ class FreqtradeBot(LoggingMixin): # Send the message self.rpc.send_msg(msg) - def _notify_buy_fill(self, trade: Trade) -> None: + def _notify_enter_fill(self, trade: Trade) -> None: msg = { 'trade_id': trade.id, 'type': RPCMessageType.BUY_FILL, @@ -788,7 +788,7 @@ class FreqtradeBot(LoggingMixin): # Lock pair for one candle to prevent immediate rebuys self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), reason='Auto lock') - self._notify_sell(trade, "stoploss") + self._notify_exit(trade, "stoploss") return True if trade.open_order_id or not trade.is_open: @@ -1000,8 +1000,8 @@ class FreqtradeBot(LoggingMixin): reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}" self.wallets.update() - self._notify_buy_cancel(trade, order_type=self.strategy.order_types['buy'], - reason=reason) + self._notify_enter_cancel(trade, order_type=self.strategy.order_types['buy'], + reason=reason) return was_trade_fully_canceled def handle_cancel_sell(self, trade: Trade, order: Dict, reason: str) -> str: @@ -1038,7 +1038,7 @@ class FreqtradeBot(LoggingMixin): reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] self.wallets.update() - self._notify_sell_cancel( + self._notify_exit_cancel( trade, order_type=self.strategy.order_types['sell'], reason=reason @@ -1156,11 +1156,11 @@ class FreqtradeBot(LoggingMixin): self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), reason='Auto lock') - self._notify_sell(trade, order_type) + self._notify_exit(trade, order_type) return True - def _notify_sell(self, trade: Trade, order_type: str, fill: bool = False) -> None: + def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False) -> None: """ Sends rpc notification when a sell occurred. """ @@ -1202,7 +1202,7 @@ class FreqtradeBot(LoggingMixin): # Send the message self.rpc.send_msg(msg) - def _notify_sell_cancel(self, trade: Trade, order_type: str, reason: str) -> None: + def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str) -> None: """ Sends rpc notification when a sell cancel occurred. """ @@ -1297,13 +1297,13 @@ class FreqtradeBot(LoggingMixin): # Updating wallets when order is closed if not trade.is_open: if not stoploss_order and not trade.open_order_id: - self._notify_sell(trade, '', True) + self._notify_exit(trade, '', True) self.protections.stop_per_pair(trade.pair) self.protections.global_stop() self.wallets.update() elif not trade.open_order_id: # Buy fill - self._notify_buy_fill(trade) + self._notify_enter_fill(trade) return False diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c72681f02..6e11fb745 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2524,7 +2524,7 @@ def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> Non mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock) freqtrade = FreqtradeBot(default_conf) - freqtrade._notify_buy_cancel = MagicMock() + freqtrade._notify_enter_cancel = MagicMock() trade = MagicMock() trade.pair = 'LTC/USDT' @@ -2566,7 +2566,7 @@ def test_handle_cancel_buy_exchanges(mocker, caplog, default_conf, cancel_order_mock = mocker.patch( 'freqtrade.exchange.Exchange.cancel_order_with_result', return_value=limit_buy_order_canceled_empty) - nofiy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot._notify_buy_cancel') + nofiy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot._notify_enter_cancel') freqtrade = FreqtradeBot(default_conf) reason = CANCEL_REASON['TIMEOUT'] @@ -2596,7 +2596,7 @@ def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order, ) freqtrade = FreqtradeBot(default_conf) - freqtrade._notify_buy_cancel = MagicMock() + freqtrade._notify_enter_cancel = MagicMock() trade = MagicMock() trade.pair = 'LTC/USDT' diff --git a/tests/test_integration.py b/tests/test_integration.py index bd9822c9e..1395012d3 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -70,7 +70,7 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee, mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', create_stoploss_order=MagicMock(return_value=True), - _notify_sell=MagicMock(), + _notify_exit=MagicMock(), ) mocker.patch("freqtrade.strategy.interface.IStrategy.should_exit", should_sell_mock) wallets_mock = mocker.patch("freqtrade.wallets.Wallets.update", MagicMock()) @@ -154,7 +154,7 @@ def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, moc mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', create_stoploss_order=MagicMock(return_value=True), - _notify_sell=MagicMock(), + _notify_exit=MagicMock(), ) should_sell_mock = MagicMock(side_effect=[ SellCheckTuple(sell_type=SellType.NONE), From 528d1438c9558d430efa64e77ba17736bc529721 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 8 Sep 2021 00:49:04 -0600 Subject: [PATCH 033/134] sell_lock -> exit_lock --- freqtrade/freqtradebot.py | 6 +++--- freqtrade/rpc/rpc.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 8e3166d79..5f3cfc185 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -99,7 +99,7 @@ class FreqtradeBot(LoggingMixin): self.state = State[initial_state.upper()] if initial_state else State.STOPPED # Protect sell-logic from forcesell and vice versa - self._sell_lock = Lock() + self._exit_lock = Lock() LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) def notify_status(self, msg: str) -> None: @@ -166,14 +166,14 @@ class FreqtradeBot(LoggingMixin): self.strategy.analyze(self.active_pair_whitelist) - with self._sell_lock: + with self._exit_lock: # Check and handle any timed out open orders self.check_handle_timedout() # Protect from collisions with forcesell. # Without this, freqtrade my try to recreate stoploss_on_exchange orders # while selling is in process, since telegram messages arrive in an different thread. - with self._sell_lock: + with self._exit_lock: trades = Trade.get_open_trades() # First process current opened trades (positions) self.exit_positions(trades) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 16f16ed67..5ab41a61f 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -567,7 +567,7 @@ class RPC: if self._freqtrade.state != State.RUNNING: raise RPCException('trader is not running') - with self._freqtrade._sell_lock: + with self._freqtrade._exit_lock: if trade_id == 'all': # Execute sell for all open orders for trade in Trade.get_open_trades(): @@ -629,7 +629,7 @@ class RPC: Handler for delete . Delete the given trade and close eventually existing open orders. """ - with self._freqtrade._sell_lock: + with self._freqtrade._exit_lock: c_count = 0 trade = Trade.get_trades(trade_filter=[Trade.id == trade_id]).first() if not trade: From 88a5a30a500096cf69477668f549d0c2e836e2c3 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 8 Sep 2021 00:53:09 -0600 Subject: [PATCH 034/134] handle_cancel_buy/sell -> handle_cancel_enter/exit --- freqtrade/freqtradebot.py | 12 +++++------ freqtrade/rpc/rpc.py | 4 ++-- tests/test_freqtradebot.py | 44 +++++++++++++++++++------------------- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5f3cfc185..e20d8fac9 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -912,7 +912,7 @@ class FreqtradeBot(LoggingMixin): default_retval=False)(pair=trade.pair, trade=trade, order=order))): - self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['TIMEOUT']) + self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and ( fully_cancelled @@ -921,7 +921,7 @@ class FreqtradeBot(LoggingMixin): default_retval=False)(pair=trade.pair, trade=trade, order=order))): - self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['TIMEOUT']) + self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) def cancel_all_open_orders(self) -> None: """ @@ -937,13 +937,13 @@ class FreqtradeBot(LoggingMixin): continue if order['side'] == 'buy': - self.handle_cancel_buy(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) + self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) elif order['side'] == 'sell': - self.handle_cancel_sell(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) + self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) Trade.commit() - def handle_cancel_buy(self, trade: Trade, order: Dict, reason: str) -> bool: + def handle_cancel_enter(self, trade: Trade, order: Dict, reason: str) -> bool: """ Buy cancel - cancel order :return: True if order was fully cancelled @@ -1004,7 +1004,7 @@ class FreqtradeBot(LoggingMixin): reason=reason) return was_trade_fully_canceled - def handle_cancel_sell(self, trade: Trade, order: Dict, reason: str) -> str: + def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> str: """ Sell cancel - cancel order and update trade :return: Reason for cancel diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 5ab41a61f..7facacf97 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -549,12 +549,12 @@ class RPC: order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) if order['side'] == 'buy': - fully_canceled = self._freqtrade.handle_cancel_buy( + fully_canceled = self._freqtrade.handle_cancel_enter( trade, order, CANCEL_REASON['FORCE_SELL']) if order['side'] == 'sell': # Cancel order - so it is placed anew with a fresh price. - self._freqtrade.handle_cancel_sell(trade, order, CANCEL_REASON['FORCE_SELL']) + self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_SELL']) if not fully_canceled: # Get current rate and execute sell diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 6e11fb745..7555de6f1 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2490,8 +2490,8 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', - handle_cancel_buy=MagicMock(), - handle_cancel_sell=MagicMock(), + handle_cancel_enter=MagicMock(), + handle_cancel_exit=MagicMock(), ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -2513,7 +2513,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke caplog.clear() -def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> None: +def test_handle_cancel_enter(mocker, caplog, default_conf, limit_buy_order) -> None: patch_RPCManager(mocker) patch_exchange(mocker) cancel_buy_order = deepcopy(limit_buy_order) @@ -2532,35 +2532,35 @@ def test_handle_cancel_buy(mocker, caplog, default_conf, limit_buy_order) -> Non limit_buy_order['filled'] = 0.0 limit_buy_order['status'] = 'open' reason = CANCEL_REASON['TIMEOUT'] - assert freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) + assert freqtrade.handle_cancel_enter(trade, limit_buy_order, reason) assert cancel_order_mock.call_count == 1 cancel_order_mock.reset_mock() caplog.clear() limit_buy_order['filled'] = 0.01 - assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) + assert not freqtrade.handle_cancel_enter(trade, limit_buy_order, reason) assert cancel_order_mock.call_count == 0 assert log_has_re("Order .* for .* not cancelled, as the filled amount.* unsellable.*", caplog) caplog.clear() cancel_order_mock.reset_mock() limit_buy_order['filled'] = 2 - assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) + assert not freqtrade.handle_cancel_enter(trade, limit_buy_order, reason) assert cancel_order_mock.call_count == 1 # Order remained open for some reason (cancel failed) cancel_buy_order['status'] = 'open' cancel_order_mock = MagicMock(return_value=cancel_buy_order) mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock) - assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) + assert not freqtrade.handle_cancel_enter(trade, limit_buy_order, reason) assert log_has_re(r"Order .* for .* not cancelled.", caplog) caplog.clear() @pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'], indirect=['limit_buy_order_canceled_empty']) -def test_handle_cancel_buy_exchanges(mocker, caplog, default_conf, - limit_buy_order_canceled_empty) -> None: +def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf, + limit_buy_order_canceled_empty) -> None: patch_RPCManager(mocker) patch_exchange(mocker) cancel_order_mock = mocker.patch( @@ -2572,7 +2572,7 @@ def test_handle_cancel_buy_exchanges(mocker, caplog, default_conf, reason = CANCEL_REASON['TIMEOUT'] trade = MagicMock() trade.pair = 'LTC/ETH' - assert freqtrade.handle_cancel_buy(trade, limit_buy_order_canceled_empty, reason) + assert freqtrade.handle_cancel_enter(trade, limit_buy_order_canceled_empty, reason) assert cancel_order_mock.call_count == 0 assert log_has_re(r'Buy order fully cancelled. Removing .* from database\.', caplog) assert nofiy_mock.call_count == 1 @@ -2585,8 +2585,8 @@ def test_handle_cancel_buy_exchanges(mocker, caplog, default_conf, 'String Return value', 123 ]) -def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order, - cancelorder) -> None: +def test_handle_cancel_enter_corder_empty(mocker, default_conf, limit_buy_order, + cancelorder) -> None: patch_RPCManager(mocker) patch_exchange(mocker) cancel_order_mock = MagicMock(return_value=cancelorder) @@ -2604,16 +2604,16 @@ def test_handle_cancel_buy_corder_empty(mocker, default_conf, limit_buy_order, limit_buy_order['filled'] = 0.0 limit_buy_order['status'] = 'open' reason = CANCEL_REASON['TIMEOUT'] - assert freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) + assert freqtrade.handle_cancel_enter(trade, limit_buy_order, reason) assert cancel_order_mock.call_count == 1 cancel_order_mock.reset_mock() limit_buy_order['filled'] = 1.0 - assert not freqtrade.handle_cancel_buy(trade, limit_buy_order, reason) + assert not freqtrade.handle_cancel_enter(trade, limit_buy_order, reason) assert cancel_order_mock.call_count == 1 -def test_handle_cancel_sell_limit(mocker, default_conf, fee) -> None: +def test_handle_cancel_exit_limit(mocker, default_conf, fee) -> None: send_msg_mock = patch_RPCManager(mocker) patch_exchange(mocker) cancel_order_mock = MagicMock() @@ -2639,26 +2639,26 @@ def test_handle_cancel_sell_limit(mocker, default_conf, fee) -> None: 'amount': 1, 'status': "open"} reason = CANCEL_REASON['TIMEOUT'] - assert freqtrade.handle_cancel_sell(trade, order, reason) + assert freqtrade.handle_cancel_exit(trade, order, reason) assert cancel_order_mock.call_count == 1 assert send_msg_mock.call_count == 1 send_msg_mock.reset_mock() order['amount'] = 2 - assert freqtrade.handle_cancel_sell(trade, order, reason + assert freqtrade.handle_cancel_exit(trade, order, reason ) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] # Assert cancel_order was not called (callcount remains unchanged) assert cancel_order_mock.call_count == 1 assert send_msg_mock.call_count == 1 - assert freqtrade.handle_cancel_sell(trade, order, reason + assert freqtrade.handle_cancel_exit(trade, order, reason ) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] # Message should not be iterated again assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] assert send_msg_mock.call_count == 1 -def test_handle_cancel_sell_cancel_exception(mocker, default_conf) -> None: +def test_handle_cancel_exit_cancel_exception(mocker, default_conf) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch( @@ -2671,7 +2671,7 @@ def test_handle_cancel_sell_cancel_exception(mocker, default_conf) -> None: order = {'remaining': 1, 'amount': 1, 'status': "open"} - assert freqtrade.handle_cancel_sell(trade, order, reason) == 'error cancelling order' + assert freqtrade.handle_cancel_exit(trade, order, reason) == 'error cancelling order' def test_execute_trade_exit_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> None: @@ -4376,8 +4376,8 @@ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limi mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=[ ExchangeError(), limit_sell_order, limit_buy_order, limit_sell_order]) - buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_buy') - sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_sell') + buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_enter') + sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit') freqtrade = get_patched_freqtradebot(mocker, default_conf) create_mock_trades(fee) From 365662574702dd1291a9400ed510e96e7604cfd6 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 8 Sep 2021 01:12:08 -0600 Subject: [PATCH 035/134] comment updates, formatting, TODOs --- freqtrade/freqtradebot.py | 80 +++++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e20d8fac9..4454455c1 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -66,6 +66,7 @@ class FreqtradeBot(LoggingMixin): init_db(self.config.get('db_url', None), clean_open_orders=self.config['dry_run']) + # TODO-lev: Do anything with this? self.wallets = Wallets(self.config, self.exchange) PairLocks.timeframe = self.config['timeframe'] @@ -77,6 +78,7 @@ class FreqtradeBot(LoggingMixin): # so anything in the Freqtradebot instance should be ready (initialized), including # the initial state of the bot. # Keep this at the end of this initialization method. + # TODO-lev: Do I need to consider the rpc, pairlists or dataprovider? self.rpc: RPCManager = RPCManager(self) self.pairlists = PairListManager(self.exchange, self.config) @@ -98,7 +100,7 @@ class FreqtradeBot(LoggingMixin): initial_state = self.config.get('initial_state') self.state = State[initial_state.upper()] if initial_state else State.STOPPED - # Protect sell-logic from forcesell and vice versa + # Protect exit-logic from forcesell and vice versa self._exit_lock = Lock() LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) @@ -170,9 +172,9 @@ class FreqtradeBot(LoggingMixin): # Check and handle any timed out open orders self.check_handle_timedout() - # Protect from collisions with forcesell. + # Protect from collisions with forceexit. # Without this, freqtrade my try to recreate stoploss_on_exchange orders - # while selling is in process, since telegram messages arrive in an different thread. + # while exiting is in process, since telegram messages arrive in an different thread. with self._exit_lock: trades = Trade.get_open_trades() # First process current opened trades (positions) @@ -289,8 +291,8 @@ class FreqtradeBot(LoggingMixin): def handle_insufficient_funds(self, trade: Trade): """ - Determine if we ever opened a sell order for this trade. - If not, try update buy fees - otherwise "refind" the open order we obviously lost. + Determine if we ever opened a exiting order for this trade. + If not, try update entering fees - otherwise "refind" the open order we obviously lost. """ sell_order = trade.select_order('sell', None) if sell_order: @@ -312,7 +314,7 @@ class FreqtradeBot(LoggingMixin): def refind_lost_order(self, trade): """ Try refinding a lost trade. - Only used when InsufficientFunds appears on sell orders (stoploss or sell). + Only used when InsufficientFunds appears on exit orders (stoploss or long sell/short buy). Tries to walk the stored orders and sell them off eventually. """ logger.info(f"Trying to refind lost order for {trade}") @@ -323,7 +325,7 @@ class FreqtradeBot(LoggingMixin): logger.debug(f"Order {order} is no longer open.") continue if order.ft_order_side == 'buy': - # Skip buy side - this is handled by reupdate_buy_order_fees + # Skip buy side - this is handled by reupdate_enter_order_fees continue try: fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, @@ -350,7 +352,7 @@ class FreqtradeBot(LoggingMixin): def enter_positions(self) -> int: """ - Tries to execute buy orders for new trades (positions) + Tries to execute long buy/short sell orders for new trades (positions) """ trades_created = 0 @@ -366,7 +368,7 @@ class FreqtradeBot(LoggingMixin): if not whitelist: logger.info("No currency pair in active pair whitelist, " - "but checking to sell open trades.") + "but checking to exit open trades.") return trades_created if PairLocks.is_global_lock(): lock = PairLocks.get_pair_longest_lock('*') @@ -621,7 +623,7 @@ class FreqtradeBot(LoggingMixin): def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str) -> None: """ - Sends rpc notification when a buy cancel occurred. + Sends rpc notification when a buy/short cancel occurred. """ current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="buy") @@ -667,7 +669,7 @@ class FreqtradeBot(LoggingMixin): def exit_positions(self, trades: List[Any]) -> int: """ - Tries to execute sell orders for open trades (positions) + Tries to execute sell/exit_short orders for open trades (positions) """ trades_closed = 0 for trade in trades: @@ -693,8 +695,8 @@ class FreqtradeBot(LoggingMixin): def handle_trade(self, trade: Trade) -> bool: """ - Sells the current pair if the threshold is reached and updates the trade record. - :return: True if trade has been sold, False otherwise + Sells/exits_short the current pair if the threshold is reached and updates the trade record. + :return: True if trade has been sold/exited_short, False otherwise """ if not trade.is_open: raise DependencyException(f'Attempt to handle closed trade: {trade}') @@ -702,7 +704,7 @@ class FreqtradeBot(LoggingMixin): logger.debug('Handling %s ...', trade) (enter, exit_) = (False, False) - + # TODO-lev: change to use_exit_signal, ignore_roi_if_enter_signal if (self.config.get('use_sell_signal', True) or self.config.get('ignore_roi_if_buy_signal', False)): analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, @@ -764,6 +766,8 @@ class FreqtradeBot(LoggingMixin): Check if trade is fulfilled in which case the stoploss on exchange should be added immediately if stoploss on exchange is enabled. + # TODO-mg: liquidation price will always be on exchange, even though + # TODO-mg: stoploss_on_exchange might not be enabled """ logger.debug('Handling stoploss on exchange %s ...', trade) @@ -782,6 +786,7 @@ class FreqtradeBot(LoggingMixin): # We check if stoploss order is fulfilled if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): + # TODO-lev: Update to exit reason trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, stoploss_order=True) @@ -797,7 +802,7 @@ class FreqtradeBot(LoggingMixin): # The trade can be closed already (sell-order fill confirmation came in this iteration) return False - # If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange + # If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange if not stoploss_order: stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss stop_price = trade.open_rate * (1 + stoploss) @@ -948,6 +953,7 @@ class FreqtradeBot(LoggingMixin): Buy cancel - cancel order :return: True if order was fully cancelled """ + # TODO-lev: Pay back borrowed/interest and transfer back on margin trades was_trade_fully_canceled = False # Cancelled orders may have the status of 'canceled' or 'closed' @@ -992,6 +998,8 @@ class FreqtradeBot(LoggingMixin): # to the order dict acquired before cancelling. # we need to fall back to the values from order if corder does not contain these keys. trade.amount = filled_amount + # TODO-lev: Check edge cases, we don't want to make leverage > 1.0 if we don't have to + trade.stake_amount = trade.amount * trade.open_rate self.update_trade_state(trade, trade.open_order_id, corder) @@ -1000,13 +1008,15 @@ class FreqtradeBot(LoggingMixin): reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}" self.wallets.update() + # TODO-lev: Should short and exit_short be an order type? + self._notify_enter_cancel(trade, order_type=self.strategy.order_types['buy'], reason=reason) return was_trade_fully_canceled def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> str: """ - Sell cancel - cancel order and update trade + Sell/exit_short cancel - cancel order and update trade :return: Reason for cancel """ # if trade is not partially completed, just cancel the order @@ -1056,6 +1066,7 @@ class FreqtradeBot(LoggingMixin): :return: amount to sell :raise: DependencyException: if available balance is not within 2% of the available amount. """ + # TODO-lev Maybe update? # Update wallets to ensure amounts tied up in a stoploss is now free! self.wallets.update() trade_base_currency = self.exchange.get_pair_base_currency(pair) @@ -1078,7 +1089,7 @@ class FreqtradeBot(LoggingMixin): :param sell_reason: Reason the sell was triggered :return: True if it succeeds (supported) False (not supported) """ - sell_type = 'sell' + sell_type = 'sell' # TODO-lev: Update to exit if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): sell_type = 'stoploss' @@ -1118,22 +1129,25 @@ class FreqtradeBot(LoggingMixin): order_type = self.strategy.order_types.get("forcesell", order_type) amount = self._safe_sell_amount(trade.pair, trade.amount) - time_in_force = self.strategy.order_time_in_force['sell'] + time_in_force = self.strategy.order_time_in_force['sell'] # TODO-lev update to exit if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, time_in_force=time_in_force, sell_reason=sell_reason.sell_reason, - current_time=datetime.now(timezone.utc)): - logger.info(f"User requested abortion of selling {trade.pair}") + current_time=datetime.now(timezone.utc)): # TODO-lev: Update to exit + logger.info(f"User requested abortion of exiting {trade.pair}") return False try: # Execute sell and update trade record - order = self.exchange.create_order(pair=trade.pair, - ordertype=order_type, side="sell", - amount=amount, rate=limit, - time_in_force=time_in_force - ) + order = self.exchange.create_order( + pair=trade.pair, + ordertype=order_type, + side="sell", + amount=amount, + rate=limit, + time_in_force=time_in_force + ) except InsufficientFundsError as e: logger.warning(f"Unable to place order {e}.") # Try to figure out what went wrong @@ -1144,15 +1158,15 @@ class FreqtradeBot(LoggingMixin): trade.orders.append(order_obj) trade.open_order_id = order['id'] - trade.sell_order_status = '' + trade.sell_order_status = '' # TODO-lev: Update to exit_order_status trade.close_rate_requested = limit - trade.sell_reason = sell_reason.sell_reason + trade.sell_reason = sell_reason.sell_reason # TODO-lev: Update to exit_reason # In case of market sell orders the order can be closed immediately if order.get('status', 'unknown') in ('closed', 'expired'): self.update_trade_state(trade, trade.open_order_id, order) Trade.commit() - # Lock pair for one candle to prevent immediate re-buys + # Lock pair for one candle to prevent immediate re-trading self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), reason='Auto lock') @@ -1187,7 +1201,7 @@ class FreqtradeBot(LoggingMixin): 'current_rate': current_rate, 'profit_amount': profit_trade, 'profit_ratio': profit_ratio, - 'sell_reason': trade.sell_reason, + 'sell_reason': trade.sell_reason, # TODO-lev: change to exit_reason 'open_date': trade.open_date, 'close_date': trade.close_date or datetime.utcnow(), 'stake_currency': self.config['stake_currency'], @@ -1206,10 +1220,10 @@ class FreqtradeBot(LoggingMixin): """ Sends rpc notification when a sell cancel occurred. """ - if trade.sell_order_status == reason: + if trade.sell_order_status == reason: # TODO-lev: Update to exit_order_status return else: - trade.sell_order_status = reason + trade.sell_order_status = reason # TODO-lev: Update to exit_order_status profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_trade = trade.calc_profit(rate=profit_rate) @@ -1230,7 +1244,7 @@ class FreqtradeBot(LoggingMixin): 'current_rate': current_rate, 'profit_amount': profit_trade, 'profit_ratio': profit_ratio, - 'sell_reason': trade.sell_reason, + 'sell_reason': trade.sell_reason, # TODO-lev: trade to exit_reason 'open_date': trade.open_date, 'close_date': trade.close_date, 'stake_currency': self.config['stake_currency'], @@ -1316,6 +1330,7 @@ class FreqtradeBot(LoggingMixin): self.wallets.update() if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount: # Eat into dust if we own more than base currency + # TODO-lev: won't be in "base"(quote) currency for shorts logger.info(f"Fee amount for {trade} was in base currency - " f"Eating Fee {fee_abs} into dust.") elif fee_abs != 0: @@ -1392,6 +1407,7 @@ class FreqtradeBot(LoggingMixin): trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', '')) if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC): + # TODO-lev: leverage? logger.warning(f"Amount {amount} does not match amount {trade.amount}") raise DependencyException("Half bought? Amounts don't match") From 8ad53e99ce65e8eb75ca4185b764e122d3af195f Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 8 Sep 2021 01:14:16 -0600 Subject: [PATCH 036/134] reupdate_buy_order_fees -> reupdate_enter_order_fees --- freqtrade/freqtradebot.py | 4 ++-- tests/test_freqtradebot.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4454455c1..2a9b537e4 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -298,9 +298,9 @@ class FreqtradeBot(LoggingMixin): if sell_order: self.refind_lost_order(trade) else: - self.reupdate_buy_order_fees(trade) + self.reupdate_enter_order_fees(trade) - def reupdate_buy_order_fees(self, trade: Trade): + def reupdate_enter_order_fees(self, trade: Trade): """ Get buy order from database, and try to reupdate. Handles trades where the initial fee-update did not work. diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 7555de6f1..fd3fde39f 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4493,14 +4493,14 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee): @pytest.mark.usefixtures("init_persistence") -def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog): +def test_reupdate_enter_order_fees(mocker, default_conf, fee, caplog): freqtrade = get_patched_freqtradebot(mocker, default_conf) mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') create_mock_trades(fee) trades = Trade.get_trades().all() - freqtrade.reupdate_buy_order_fees(trades[0]) + freqtrade.reupdate_enter_order_fees(trades[0]) assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) assert mock_uts.call_count == 1 assert mock_uts.call_args_list[0][0][0] == trades[0] @@ -4523,7 +4523,7 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog): ) Trade.query.session.add(trade) - freqtrade.reupdate_buy_order_fees(trade) + freqtrade.reupdate_enter_order_fees(trade) assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) assert mock_uts.call_count == 0 assert not log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog) @@ -4534,7 +4534,7 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog): def test_handle_insufficient_funds(mocker, default_conf, fee): freqtrade = get_patched_freqtradebot(mocker, default_conf) mock_rlo = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.refind_lost_order') - mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_buy_order_fees') + mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_enter_order_fees') create_mock_trades(fee) trades = Trade.get_trades().all() From 323683d44f47ebf9c553b851f3567dde5baaa2a0 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 8 Sep 2021 01:16:20 -0600 Subject: [PATCH 037/134] some more TODOs --- freqtrade/freqtradebot.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 2a9b537e4..c1d24d141 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -515,7 +515,9 @@ class FreqtradeBot(LoggingMixin): order_type = self.strategy.order_types['buy'] if forcebuy: # Forcebuy can define a different ordertype + # TODO-lev: get a forceshort? What is this order_type = self.strategy.order_types.get('forcebuy', order_type) + # TODO-lev: Will this work for shorting? if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested, @@ -600,7 +602,7 @@ class FreqtradeBot(LoggingMixin): def _notify_enter(self, trade: Trade, order_type: str) -> None: """ - Sends rpc notification when a buy occurred. + Sends rpc notification when a buy/short occurred. """ msg = { 'trade_id': trade.id, @@ -766,8 +768,8 @@ class FreqtradeBot(LoggingMixin): Check if trade is fulfilled in which case the stoploss on exchange should be added immediately if stoploss on exchange is enabled. - # TODO-mg: liquidation price will always be on exchange, even though - # TODO-mg: stoploss_on_exchange might not be enabled + # TODO-lev: liquidation price will always be on exchange, even though + # TODO-lev: stoploss_on_exchange might not be enabled """ logger.debug('Handling stoploss on exchange %s ...', trade) From 786dcb50ebb4390730ec65b3f84ddab61f5e75c6 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 8 Sep 2021 01:20:52 -0600 Subject: [PATCH 038/134] safe_sell_amount -> safe_exit_amount --- freqtrade/freqtradebot.py | 4 ++-- tests/test_freqtradebot.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c1d24d141..22da608c3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1057,7 +1057,7 @@ class FreqtradeBot(LoggingMixin): ) return reason - def _safe_sell_amount(self, pair: str, amount: float) -> float: + def _safe_exit_amount(self, pair: str, amount: float) -> float: """ Get sellable amount. Should be trade.amount - but will fall back to the available amount if necessary. @@ -1130,7 +1130,7 @@ class FreqtradeBot(LoggingMixin): # but we allow this value to be changed) order_type = self.strategy.order_types.get("forcesell", order_type) - amount = self._safe_sell_amount(trade.pair, trade.amount) + amount = self._safe_exit_amount(trade.pair, trade.amount) time_in_force = self.strategy.order_time_in_force['sell'] # TODO-lev update to exit if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index fd3fde39f..81d3311f9 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3345,7 +3345,7 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_ caplog.clear() -def test__safe_sell_amount(default_conf, fee, caplog, mocker): +def test__safe_exit_amount(default_conf, fee, caplog, mocker): patch_RPCManager(mocker) patch_exchange(mocker) amount = 95.33 @@ -3365,18 +3365,18 @@ def test__safe_sell_amount(default_conf, fee, caplog, mocker): patch_get_signal(freqtrade) wallet_update.reset_mock() - assert freqtrade._safe_sell_amount(trade.pair, trade.amount) == amount_wallet + 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_sell_amount(trade.pair, amount_wallet) == amount_wallet + 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 caplog.clear() -def test__safe_sell_amount_error(default_conf, fee, caplog, mocker): +def test__safe_exit_amount_error(default_conf, fee, caplog, mocker): patch_RPCManager(mocker) patch_exchange(mocker) amount = 95.33 @@ -3394,7 +3394,7 @@ def test__safe_sell_amount_error(default_conf, fee, caplog, mocker): freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) with pytest.raises(DependencyException, match=r"Not enough amount to sell."): - assert freqtrade._safe_sell_amount(trade.pair, trade.amount) + assert freqtrade._safe_exit_amount(trade.pair, trade.amount) def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplog) -> None: From f1a8b818967c878efd0cc6faeac11f1a49902f3d Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 8 Sep 2021 01:27:08 -0600 Subject: [PATCH 039/134] sorted test interfac --- tests/strategy/test_interface.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index f0ea36119..1b24c3297 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -1,5 +1,4 @@ # pragma pylint: disable=missing-docstring, C0103 -from freqtrade.enums.signaltype import SignalDirection import logging from datetime import datetime, timedelta, timezone from pathlib import Path @@ -13,6 +12,7 @@ from freqtrade.configuration import TimeRange from freqtrade.data.dataprovider import DataProvider from freqtrade.data.history import load_data from freqtrade.enums import SellType +from freqtrade.enums.signaltype import SignalDirection from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.optimize.space import SKDecimal from freqtrade.persistence import PairLocks, Trade @@ -47,8 +47,8 @@ def test_returns_latest_signal(ohlcv_history): mocked_history.loc[1, 'exit_long'] = 0 mocked_history.loc[1, 'enter_long'] = 1 - assert _STRATEGY.get_entry_signal('ETH/BTC', '5m', mocked_history - ) == (SignalDirection.LONG, None) + assert _STRATEGY.get_entry_signal( + 'ETH/BTC', '5m', mocked_history) == (SignalDirection.LONG, None) assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (True, False) assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, False) mocked_history.loc[1, 'exit_long'] = 0 From 3057a5b9b85ecdece64c55b14eac7db0d9fe2d18 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 8 Sep 2021 01:40:22 -0600 Subject: [PATCH 040/134] freqtradebot local name changes --- freqtrade/freqtradebot.py | 44 +++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 22da608c3..5c1117ea3 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -479,21 +479,21 @@ class FreqtradeBot(LoggingMixin): time_in_force = self.strategy.order_time_in_force['buy'] if price: - buy_limit_requested = price + enter_limit_requested = price else: # Calculate price - proposed_buy_rate = self.exchange.get_rate(pair, refresh=True, side="buy") + proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side="buy") custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, - default_retval=proposed_buy_rate)( + default_retval=proposed_enter_rate)( pair=pair, current_time=datetime.now(timezone.utc), - proposed_rate=proposed_buy_rate) + proposed_rate=proposed_enter_rate) - buy_limit_requested = self.get_valid_price(custom_entry_price, proposed_buy_rate) + enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate) - if not buy_limit_requested: + if not enter_limit_requested: raise PricingError('Could not determine buy price.') - min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, buy_limit_requested, + min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, enter_limit_requested, self.strategy.stoploss) if not self.edge: @@ -501,7 +501,7 @@ class FreqtradeBot(LoggingMixin): stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, default_retval=stake_amount)( pair=pair, current_time=datetime.now(timezone.utc), - current_rate=buy_limit_requested, proposed_stake=stake_amount, + current_rate=enter_limit_requested, proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount) stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount) @@ -511,7 +511,7 @@ class FreqtradeBot(LoggingMixin): logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: " f"{stake_amount} ...") - amount = stake_amount / buy_limit_requested + amount = stake_amount / enter_limit_requested order_type = self.strategy.order_types['buy'] if forcebuy: # Forcebuy can define a different ordertype @@ -520,20 +520,20 @@ class FreqtradeBot(LoggingMixin): # TODO-lev: Will this work for shorting? if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( - pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested, + pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested, time_in_force=time_in_force, current_time=datetime.now(timezone.utc)): logger.info(f"User requested abortion of buying {pair}") return False amount = self.exchange.amount_to_precision(pair, amount) order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy", - amount=amount, rate=buy_limit_requested, + amount=amount, rate=enter_limit_requested, time_in_force=time_in_force) order_obj = Order.parse_from_ccxt_object(order, pair, 'buy') order_id = order['id'] order_status = order.get('status', None) # we assume the order is executed at the price requested - buy_limit_filled_price = buy_limit_requested + enter_limit_filled_price = enter_limit_requested amount_requested = amount if order_status == 'expired' or order_status == 'rejected': @@ -556,13 +556,13 @@ class FreqtradeBot(LoggingMixin): ) stake_amount = order['cost'] amount = safe_value_fallback(order, 'filled', 'amount') - buy_limit_filled_price = safe_value_fallback(order, 'average', 'price') + enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') # in case of FOK the order may be filled immediately and fully elif order_status == 'closed': stake_amount = order['cost'] amount = safe_value_fallback(order, 'filled', 'amount') - buy_limit_filled_price = safe_value_fallback(order, 'average', 'price') + enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') @@ -574,8 +574,8 @@ class FreqtradeBot(LoggingMixin): amount_requested=amount_requested, fee_open=fee, fee_close=fee, - open_rate=buy_limit_filled_price, - open_rate_requested=buy_limit_requested, + open_rate=enter_limit_filled_price, + open_rate_requested=enter_limit_requested, open_date=datetime.utcnow(), exchange=self.exchange.id, open_order_id=order_id, @@ -719,8 +719,8 @@ class FreqtradeBot(LoggingMixin): ) # TODO-lev: side should depend on trade side. - sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") - if self._check_and_execute_exit(trade, sell_rate, enter, exit_): + exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") + if self._check_and_execute_exit(trade, exit_rate, enter, exit_): return True logger.debug('Found no sell signal for %s.', trade) @@ -754,7 +754,7 @@ class FreqtradeBot(LoggingMixin): except InvalidOrderException as e: trade.stoploss_order_id = None logger.error(f'Unable to place a stoploss order on exchange. {e}') - logger.warning('Selling the trade forcefully') + logger.warning('Exiting the trade forcefully') self.execute_trade_exit(trade, trade.stop_loss, sell_reason=SellCheckTuple( sell_type=SellType.EMERGENCY_SELL)) @@ -864,19 +864,19 @@ class FreqtradeBot(LoggingMixin): logger.warning(f"Could not create trailing stoploss order " f"for pair {trade.pair}.") - def _check_and_execute_exit(self, trade: Trade, sell_rate: float, + def _check_and_execute_exit(self, trade: Trade, exit_rate: float, enter: bool, exit_: bool) -> bool: """ Check and execute trade exit """ should_exit: SellCheckTuple = self.strategy.should_exit( - trade, sell_rate, datetime.now(timezone.utc), enter=enter, exit_=exit_, + trade, exit_rate, datetime.now(timezone.utc), enter=enter, exit_=exit_, force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 ) if should_exit.sell_flag: logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.sell_type}') - self.execute_trade_exit(trade, sell_rate, should_exit) + self.execute_trade_exit(trade, exit_rate, should_exit) return True return False From 53006db2b7668408bf4a4cd9dc81877a58a97a63 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 8 Sep 2021 01:48:22 -0600 Subject: [PATCH 041/134] Updated log messages for freqtradebot --- freqtrade/freqtradebot.py | 6 +++--- tests/test_freqtradebot.py | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5c1117ea3..63f7463d1 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -387,7 +387,7 @@ class FreqtradeBot(LoggingMixin): logger.warning('Unable to create trade for %s: %s', pair, exception) if not trades_created: - logger.debug("Found no buy signals for whitelisted currencies. Trying again...") + logger.debug("Found no enter signals for whitelisted currencies. Trying again...") return trades_created @@ -687,7 +687,7 @@ class FreqtradeBot(LoggingMixin): trades_closed += 1 except DependencyException as exception: - logger.warning('Unable to sell trade %s: %s', trade.pair, exception) + logger.warning('Unable to exit trade %s: %s', trade.pair, exception) # Updating wallets if any trade occurred if trades_closed: @@ -1081,7 +1081,7 @@ class FreqtradeBot(LoggingMixin): return wallet_amount else: raise DependencyException( - f"Not enough amount to sell. Trade-amount: {amount}, Wallet: {wallet_amount}") + f"Not enough amount to exit trade. Trade-amount: {amount}, Wallet: {wallet_amount}") def execute_trade_exit(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool: """ diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 81d3311f9..989405e7c 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1200,7 +1200,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, assert trade.stoploss_order_id is None assert trade.sell_reason == SellType.EMERGENCY_SELL.value assert log_has("Unable to place a stoploss order on exchange. ", caplog) - assert log_has("Selling the trade forcefully", caplog) + assert log_has("Exiting the trade forcefully", caplog) # Should call a market sell assert create_order_mock.call_count == 2 @@ -1680,7 +1680,7 @@ def test_enter_positions(mocker, default_conf, caplog) -> None: MagicMock(return_value=False)) n = freqtrade.enter_positions() assert n == 0 - assert log_has('Found no buy signals for whitelisted currencies. Trying again...', caplog) + assert log_has('Found no enter 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']) caplog.clear() @@ -1743,7 +1743,7 @@ def test_exit_positions_exception(mocker, default_conf, limit_buy_order, caplog) ) n = freqtrade.exit_positions(trades) assert n == 0 - assert log_has('Unable to sell trade ETH/BTC: ', caplog) + assert log_has('Unable to exit trade ETH/BTC: ', caplog) caplog.clear() @@ -3376,7 +3376,7 @@ def test__safe_exit_amount(default_conf, fee, caplog, mocker): caplog.clear() -def test__safe_exit_amount_error(default_conf, fee, caplog, mocker): +def test_safe_exit_amount_error(default_conf, fee, caplog, mocker): patch_RPCManager(mocker) patch_exchange(mocker) amount = 95.33 @@ -3393,7 +3393,7 @@ def test__safe_exit_amount_error(default_conf, fee, caplog, mocker): ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - with pytest.raises(DependencyException, match=r"Not enough amount to sell."): + with pytest.raises(DependencyException, match=r"Not enough amount to exit."): assert freqtrade._safe_exit_amount(trade.pair, trade.amount) From 5dda2273420a539c79365f2f7d633d8952043a7b Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 8 Sep 2021 01:53:42 -0600 Subject: [PATCH 042/134] comment change --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 63f7463d1..b97596b7f 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -955,7 +955,7 @@ class FreqtradeBot(LoggingMixin): Buy cancel - cancel order :return: True if order was fully cancelled """ - # TODO-lev: Pay back borrowed/interest and transfer back on margin trades + # TODO-lev: Pay back borrowed/interest and transfer back on leveraged trades was_trade_fully_canceled = False # Cancelled orders may have the status of 'canceled' or 'closed' From 83bd674ba7260e9e1af707ad31c1437e476cd6de Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 10 Sep 2021 03:25:54 -0600 Subject: [PATCH 043/134] Added side to execute_trade_exit --- freqtrade/freqtradebot.py | 27 +++++++++++++++++---------- freqtrade/rpc/rpc.py | 2 +- tests/test_freqtradebot.py | 34 +++++++++++++++++++++------------- 3 files changed, 39 insertions(+), 24 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 93f5fdf3d..bf3b62e85 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -756,7 +756,7 @@ class FreqtradeBot(LoggingMixin): logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.warning('Exiting the trade forcefully') self.execute_trade_exit(trade, trade.stop_loss, sell_reason=SellCheckTuple( - sell_type=SellType.EMERGENCY_SELL)) + sell_type=SellType.EMERGENCY_SELL), side=trade.exit_side) except ExchangeError: trade.stoploss_order_id = None @@ -876,7 +876,7 @@ class FreqtradeBot(LoggingMixin): if should_exit.sell_flag: logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.sell_type}') - self.execute_trade_exit(trade, exit_rate, should_exit) + self.execute_trade_exit(trade, exit_rate, should_exit, side=trade.exit_side) return True return False @@ -1081,21 +1081,28 @@ class FreqtradeBot(LoggingMixin): raise DependencyException( f"Not enough amount to exit trade. Trade-amount: {amount}, Wallet: {wallet_amount}") - def execute_trade_exit(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool: + def execute_trade_exit( + self, + trade: Trade, + limit: float, + sell_reason: SellCheckTuple, # TODO-lev update to exit_reason + side: str + ) -> bool: """ Executes a trade exit for the given trade and limit :param trade: Trade instance :param limit: limit rate for the sell order :param sell_reason: Reason the sell was triggered + :param side: "buy" or "sell" :return: True if it succeeds (supported) False (not supported) """ - sell_type = 'sell' # TODO-lev: Update to exit + exit_type = 'sell' # TODO-lev: Update to exit if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): - sell_type = 'stoploss' + exit_type = 'stoploss' # if stoploss is on exchange and we are on dry_run mode, # we consider the sell price stop price - if self.config['dry_run'] and sell_type == 'stoploss' \ + if self.config['dry_run'] and exit_type == 'stoploss' \ and self.strategy.order_types['stoploss_on_exchange']: limit = trade.stop_loss @@ -1119,7 +1126,7 @@ class FreqtradeBot(LoggingMixin): except InvalidOrderException: logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") - order_type = self.strategy.order_types[sell_type] + order_type = self.strategy.order_types[exit_type] if sell_reason.sell_type == SellType.EMERGENCY_SELL: # Emergency sells (default to market!) order_type = self.strategy.order_types.get("emergencysell", "market") @@ -1143,10 +1150,10 @@ class FreqtradeBot(LoggingMixin): order = self.exchange.create_order( pair=trade.pair, ordertype=order_type, - side="sell", amount=amount, rate=limit, - time_in_force=time_in_force + time_in_force=time_in_force, + side=trade.exit_side ) except InsufficientFundsError as e: logger.warning(f"Unable to place order {e}.") @@ -1154,7 +1161,7 @@ class FreqtradeBot(LoggingMixin): self.handle_insufficient_funds(trade) return False - order_obj = Order.parse_from_ccxt_object(order, trade.pair, 'sell') + order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side) trade.orders.append(order_obj) trade.open_order_id = order['id'] diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 7facacf97..8128313b7 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -561,7 +561,7 @@ class RPC: current_rate = self._freqtrade.exchange.get_rate( trade.pair, refresh=False, side="sell") sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL) - self._freqtrade.execute_trade_exit(trade, current_rate, sell_reason) + self._freqtrade.execute_trade_exit(trade, current_rate, sell_reason, side="sell") # ---- EOF def _exec_forcesell ---- if self._freqtrade.state != State.RUNNING: diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 0e92dbc84..37289888c 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2651,6 +2651,7 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf) -> None: assert freqtrade.handle_cancel_exit(trade, order, reason) == 'error cancelling order' +# TODO-lev: Add short tests def test_execute_trade_exit_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> None: rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) @@ -2679,15 +2680,16 @@ def test_execute_trade_exit_up(default_conf, ticker, fee, ticker_sell_up, mocker fetch_ticker=ticker_sell_up ) # Prevented sell ... - freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_up()['bid'], + # TODO-lev: side="buy" + freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_up()['bid'], side="sell", sell_reason=SellCheckTuple(sell_type=SellType.ROI)) assert rpc_mock.call_count == 0 assert freqtrade.strategy.confirm_trade_exit.call_count == 1 # Repatch with true freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True) - - freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_up()['bid'], + # TODO-lev: side="buy" + freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_up()['bid'], side="sell", sell_reason=SellCheckTuple(sell_type=SellType.ROI)) assert freqtrade.strategy.confirm_trade_exit.call_count == 1 @@ -2739,8 +2741,8 @@ def test_execute_trade_exit_down(default_conf, ticker, fee, ticker_sell_down, mo 'freqtrade.exchange.Exchange', fetch_ticker=ticker_sell_down ) - - freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_down()['bid'], + # TODO-lev: side="buy" + freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_down()['bid'], side="sell", sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) assert rpc_mock.call_count == 2 @@ -2800,8 +2802,8 @@ def test_execute_trade_exit_custom_exit_price(default_conf, ticker, fee, ticker_ # Set a custom exit price freqtrade.strategy.custom_exit_price = lambda **kwargs: 1.170e-05 - - freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_up()['bid'], + # TODO-lev: side="buy" + freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_up()['bid'], side="sell", sell_reason=SellCheckTuple(sell_type=SellType.SELL_SIGNAL)) # Sell price must be different to default bid price @@ -2863,7 +2865,8 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(default_conf, tick # Setting trade stoploss to 0.01 trade.stop_loss = 0.00001099 * 0.99 - freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_down()['bid'], + # TODO-lev: side="buy" + freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_down()['bid'], side="sell", sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) assert rpc_mock.call_count == 2 @@ -2919,7 +2922,8 @@ def test_execute_trade_exit_sloe_cancel_exception( freqtrade.config['dry_run'] = False trade.stoploss_order_id = "abcd" - freqtrade.execute_trade_exit(trade=trade, limit=1234, + # TODO-lev: side="buy" + freqtrade.execute_trade_exit(trade=trade, limit=1234, side="sell", sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) assert create_order_mock.call_count == 2 assert log_has('Could not cancel stoploss order abcd', caplog) @@ -2970,7 +2974,8 @@ def test_execute_trade_exit_with_stoploss_on_exchange(default_conf, ticker, fee, fetch_ticker=ticker_sell_up ) - freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_up()['bid'], + # TODO-lev: side="buy" + freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_up()['bid'], side="sell", sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) trade = Trade.query.first() @@ -3078,7 +3083,8 @@ def test_execute_trade_exit_market_order(default_conf, ticker, fee, ) freqtrade.config['order_types']['sell'] = 'market' - freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_up()['bid'], + # TODO-lev: side="buy" + freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_up()['bid'], side="sell", sell_reason=SellCheckTuple(sell_type=SellType.ROI)) assert not trade.is_open @@ -3137,8 +3143,9 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf, ticker, fee, ) sell_reason = SellCheckTuple(sell_type=SellType.ROI) + # TODO-lev: side="buy" assert not freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_up()['bid'], - sell_reason=sell_reason) + sell_reason=sell_reason, side="sell") assert mock_insuf.call_count == 1 @@ -3394,7 +3401,8 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo fetch_ticker=ticker_sell_down ) - freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_down()['bid'], + # TODO-lev: side="buy" + freqtrade.execute_trade_exit(trade=trade, limit=ticker_sell_down()['bid'], side="sell", sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) trade.close(ticker_sell_down()['bid']) assert freqtrade.strategy.is_pair_locked(trade.pair) From 9f16464b12d4dcd67ea33cbe50bfc31cac2e407c Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 10 Sep 2021 10:31:57 -0600 Subject: [PATCH 044/134] Removed unnecessary TODOs --- freqtrade/freqtradebot.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index bf3b62e85..f9448da42 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1141,7 +1141,7 @@ class FreqtradeBot(LoggingMixin): if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, time_in_force=time_in_force, sell_reason=sell_reason.sell_reason, - current_time=datetime.now(timezone.utc)): # TODO-lev: Update to exit + current_time=datetime.now(timezone.utc)): logger.info(f"User requested abortion of exiting {trade.pair}") return False @@ -1165,9 +1165,9 @@ class FreqtradeBot(LoggingMixin): trade.orders.append(order_obj) trade.open_order_id = order['id'] - trade.sell_order_status = '' # TODO-lev: Update to exit_order_status + trade.sell_order_status = '' trade.close_rate_requested = limit - trade.sell_reason = sell_reason.sell_reason # TODO-lev: Update to exit_reason + trade.sell_reason = sell_reason.sell_reason # In case of market sell orders the order can be closed immediately if order.get('status', 'unknown') in ('closed', 'expired'): self.update_trade_state(trade, trade.open_order_id, order) @@ -1208,7 +1208,7 @@ class FreqtradeBot(LoggingMixin): 'current_rate': current_rate, 'profit_amount': profit_trade, 'profit_ratio': profit_ratio, - 'sell_reason': trade.sell_reason, # TODO-lev: change to exit_reason + 'sell_reason': trade.sell_reason, 'open_date': trade.open_date, 'close_date': trade.close_date or datetime.utcnow(), 'stake_currency': self.config['stake_currency'], @@ -1227,10 +1227,10 @@ class FreqtradeBot(LoggingMixin): """ Sends rpc notification when a sell cancel occurred. """ - if trade.sell_order_status == reason: # TODO-lev: Update to exit_order_status + if trade.sell_order_status == reason: return else: - trade.sell_order_status = reason # TODO-lev: Update to exit_order_status + trade.sell_order_status = reason profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_trade = trade.calc_profit(rate=profit_rate) @@ -1251,7 +1251,7 @@ class FreqtradeBot(LoggingMixin): 'current_rate': current_rate, 'profit_amount': profit_trade, 'profit_ratio': profit_ratio, - 'sell_reason': trade.sell_reason, # TODO-lev: trade to exit_reason + 'sell_reason': trade.sell_reason, 'open_date': trade.open_date, 'close_date': trade.close_date, 'stake_currency': self.config['stake_currency'], @@ -1337,7 +1337,7 @@ class FreqtradeBot(LoggingMixin): self.wallets.update() if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount: # Eat into dust if we own more than base currency - # TODO-lev: won't be in "base"(quote) currency for shorts + # TODO-lev: won't be in (quote) currency for shorts logger.info(f"Fee amount for {trade} was in base currency - " f"Eating Fee {fee_abs} into dust.") elif fee_abs != 0: From cb155764eb97f5bf08ca33fa52719992bdeaa28c Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 10 Sep 2021 11:34:57 -0600 Subject: [PATCH 045/134] Short side options in freqtradebot --- freqtrade/freqtradebot.py | 216 +++-- tests/freqtradebot.py | 1516 ++++++++++++++++++++++++++++++++++++ tests/test_freqtradebot.py | 17 +- 3 files changed, 1669 insertions(+), 80 deletions(-) create mode 100644 tests/freqtradebot.py diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f9448da42..6919128ba 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -16,7 +16,7 @@ from freqtrade.configuration import validate_config_consistency from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge -from freqtrade.enums import RPCMessageType, SellType, State +from freqtrade.enums import RPCMessageType, SellType, SignalDirection, State from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds @@ -272,21 +272,26 @@ class FreqtradeBot(LoggingMixin): trades: List[Trade] = Trade.get_closed_trades_without_assigned_fees() for trade in trades: - - if not trade.is_open and not trade.fee_updated('sell'): + if not trade.is_open and not trade.fee_updated(trade.exit_side): # Get sell fee - order = trade.select_order('sell', False) + order = trade.select_order(trade.exit_side, False) if order: - logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.") + logger.info( + f"Updating {trade.exit_side}-fee on trade {trade}" + f"for order {order.order_id}." + ) self.update_trade_state(trade, order.order_id, stoploss_order=order.ft_order_side == 'stoploss') trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() for trade in trades: - if trade.is_open and not trade.fee_updated('buy'): - order = trade.select_order('buy', False) + if trade.is_open and not trade.fee_updated(trade.enter_side): + order = trade.select_order(trade.enter_side, False) if order: - logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") + logger.info( + f"Updating {trade.enter_side}-fee on trade {trade}" + f"for order {order.order_id}." + ) self.update_trade_state(trade, order.order_id) def handle_insufficient_funds(self, trade: Trade): @@ -294,8 +299,8 @@ class FreqtradeBot(LoggingMixin): Determine if we ever opened a exiting order for this trade. If not, try update entering fees - otherwise "refind" the open order we obviously lost. """ - sell_order = trade.select_order('sell', None) - if sell_order: + exit_order = trade.select_order(trade.exit_side, None) + if exit_order: self.refind_lost_order(trade) else: self.reupdate_enter_order_fees(trade) @@ -305,10 +310,11 @@ class FreqtradeBot(LoggingMixin): Get buy order from database, and try to reupdate. Handles trades where the initial fee-update did not work. """ - logger.info(f"Trying to reupdate buy fees for {trade}") - order = trade.select_order('buy', False) + logger.info(f"Trying to reupdate {trade.enter_side} fees for {trade}") + order = trade.select_order(trade.enter_side, False) if order: - logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") + logger.info( + f"Updating {trade.enter_side}-fee on trade {trade} for order {order.order_id}.") self.update_trade_state(trade, order.order_id) def refind_lost_order(self, trade): @@ -324,7 +330,7 @@ class FreqtradeBot(LoggingMixin): if not order.ft_is_open: logger.debug(f"Order {order} is no longer open.") continue - if order.ft_order_side == 'buy': + if order.ft_order_side == trade.enter_side: # Skip buy side - this is handled by reupdate_enter_order_fees continue try: @@ -334,7 +340,7 @@ class FreqtradeBot(LoggingMixin): if fo and fo['status'] == 'open': # Assume this as the open stoploss order trade.stoploss_order_id = order.order_id - elif order.ft_order_side == 'sell': + elif order.ft_order_side == trade.exit_side: if fo and fo['status'] == 'open': # Assume this as the open order trade.open_order_id = order.order_id @@ -433,8 +439,11 @@ class FreqtradeBot(LoggingMixin): if ((bid_check_dom.get('enabled', False)) and (bid_check_dom.get('bids_to_ask_delta', 0) > 0)): # TODO-lev: Does the below need to be adjusted for shorts? - if self._check_depth_of_market_buy(pair, bid_check_dom): - # TODO-lev: pass in "enter" as side. + if self._check_depth_of_market( + pair, + bid_check_dom, + side=side + ): return self.execute_entry(pair, stake_amount, enter_tag=enter_tag) else: @@ -444,7 +453,12 @@ class FreqtradeBot(LoggingMixin): else: return False - def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool: + def _check_depth_of_market( + self, + pair: str, + conf: Dict, + side: SignalDirection + ) -> bool: """ Checks depth of market before executing a buy """ @@ -454,9 +468,17 @@ class FreqtradeBot(LoggingMixin): order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks']) order_book_bids = order_book_data_frame['b_size'].sum() order_book_asks = order_book_data_frame['a_size'].sum() - bids_ask_delta = order_book_bids / order_book_asks + + enter_side = order_book_bids if side == SignalDirection.LONG else order_book_asks + exit_side = order_book_asks if side == SignalDirection.LONG else order_book_bids + bids_ask_delta = enter_side / exit_side + + bids = f"Bids: {order_book_bids}" + asks = f"Asks: {order_book_asks}" + delta = f"Delta: {bids_ask_delta}" + logger.info( - f"Bids: {order_book_bids}, Asks: {order_book_asks}, Delta: {bids_ask_delta}, " + f"{bids}, {asks}, {delta}, Direction: {side.value}" f"Bid Price: {order_book['bids'][0][0]}, Ask Price: {order_book['asks'][0][0]}, " f"Immediate Bid Quantity: {order_book['bids'][0][1]}, " f"Immediate Ask Quantity: {order_book['asks'][0][1]}." @@ -468,21 +490,32 @@ class FreqtradeBot(LoggingMixin): logger.info(f"Bids to asks delta for {pair} does not satisfy condition.") return False - def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None, - forcebuy: bool = False, enter_tag: Optional[str] = None) -> bool: + def execute_entry( + self, + pair: str, + stake_amount: float, + price: Optional[float] = None, + forcebuy: bool = False, + leverage: float = 1.0, + is_short: bool = False, + enter_tag: Optional[str] = None + ) -> bool: """ Executes a limit buy for the given pair :param pair: pair for which we want to create a LIMIT_BUY :param stake_amount: amount of stake-currency for the pair + :param leverage: amount of leverage applied to this trade :return: True if a buy order is created, false if it fails. """ time_in_force = self.strategy.order_time_in_force['buy'] + [side, name] = ['sell', 'Short'] if is_short else ['buy', 'Long'] + if price: enter_limit_requested = price else: # Calculate price - proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side="buy") + proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side=side) custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, default_retval=proposed_enter_rate)( pair=pair, current_time=datetime.now(timezone.utc), @@ -491,10 +524,14 @@ class FreqtradeBot(LoggingMixin): enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate) if not enter_limit_requested: - raise PricingError('Could not determine buy price.') + raise PricingError(f'Could not determine {side} price.') - min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, enter_limit_requested, - self.strategy.stoploss) + min_stake_amount = self.exchange.get_min_pair_stake_amount( + pair, + enter_limit_requested, + self.strategy.stoploss, + leverage=leverage + ) if not self.edge: max_stake_amount = self.wallets.get_available_stake_amount() @@ -508,10 +545,11 @@ class FreqtradeBot(LoggingMixin): if not stake_amount: return False - logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: " + log_type = f"{name} signal found" + logger.info(f"{log_type}: about create a new trade for {pair} with stake_amount: " f"{stake_amount} ...") - amount = stake_amount / enter_limit_requested + amount = (stake_amount / enter_limit_requested) * leverage order_type = self.strategy.order_types['buy'] if forcebuy: # Forcebuy can define a different ordertype @@ -522,13 +560,13 @@ class FreqtradeBot(LoggingMixin): 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)): - logger.info(f"User requested abortion of buying {pair}") + logger.info(f"User requested abortion of {name.lower()}ing {pair}") return False amount = self.exchange.amount_to_precision(pair, amount) - order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy", + order = self.exchange.create_order(pair=pair, ordertype=order_type, side=side, amount=amount, rate=enter_limit_requested, time_in_force=time_in_force) - order_obj = Order.parse_from_ccxt_object(order, pair, 'buy') + order_obj = Order.parse_from_ccxt_object(order, pair, side) order_id = order['id'] order_status = order.get('status', None) @@ -541,17 +579,17 @@ class FreqtradeBot(LoggingMixin): # return false if the order is not filled if float(order['filled']) == 0: - logger.warning('Buy %s order with time in force %s for %s is %s by %s.' + logger.warning('%s %s order with time in force %s for %s is %s by %s.' ' zero amount is fulfilled.', - order_tif, order_type, pair, order_status, self.exchange.name) + name, order_tif, order_type, pair, order_status, self.exchange.name) return False else: # the order is partially fulfilled # in case of IOC orders we can check immediately # if the order is fulfilled fully or partially - logger.warning('Buy %s order with time in force %s for %s is %s by %s.' + logger.warning('%s %s order with time in force %s for %s is %s by %s.' ' %s amount fulfilled out of %s (%s remaining which is canceled).', - order_tif, order_type, pair, order_status, self.exchange.name, + name, order_tif, order_type, pair, order_status, self.exchange.name, order['filled'], order['amount'], order['remaining'] ) stake_amount = order['cost'] @@ -582,7 +620,9 @@ class FreqtradeBot(LoggingMixin): strategy=self.strategy.get_strategy_name(), # TODO-lev: compatibility layer for buy_tag (!) buy_tag=enter_tag, - timeframe=timeframe_to_minutes(self.config['timeframe']) + timeframe=timeframe_to_minutes(self.config['timeframe']), + leverage=leverage, + is_short=is_short, ) trade.orders.append(order_obj) @@ -606,7 +646,7 @@ class FreqtradeBot(LoggingMixin): """ msg = { 'trade_id': trade.id, - 'type': RPCMessageType.BUY, + 'type': RPCMessageType.SHORT if trade.is_short else RPCMessageType.BUY, 'buy_tag': trade.buy_tag, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, @@ -627,11 +667,11 @@ class FreqtradeBot(LoggingMixin): """ Sends rpc notification when a buy/short cancel occurred. """ - current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="buy") - + current_rate = self.exchange.get_rate(trade.pair, refresh=False, side=trade.enter_side) + msg_type = RPCMessageType.SHORT_CANCEL if trade.is_short else RPCMessageType.BUY_CANCEL msg = { 'trade_id': trade.id, - 'type': RPCMessageType.BUY_CANCEL, + 'type': msg_type, 'buy_tag': trade.buy_tag, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, @@ -650,9 +690,10 @@ class FreqtradeBot(LoggingMixin): self.rpc.send_msg(msg) def _notify_enter_fill(self, trade: Trade) -> None: + msg_type = RPCMessageType.SHORT_FILL if trade.is_short else RPCMessageType.BUY_FILL msg = { 'trade_id': trade.id, - 'type': RPCMessageType.BUY_FILL, + 'type': msg_type, 'buy_tag': trade.buy_tag, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, @@ -706,6 +747,8 @@ class FreqtradeBot(LoggingMixin): logger.debug('Handling %s ...', trade) (enter, exit_) = (False, False) + exit_signal_type = "exit_short" if trade.is_short else "exit_long" + # TODO-lev: change to use_exit_signal, ignore_roi_if_enter_signal if (self.config.get('use_sell_signal', True) or self.config.get('ignore_roi_if_buy_signal', False)): @@ -715,15 +758,16 @@ class FreqtradeBot(LoggingMixin): (enter, exit_) = self.strategy.get_exit_signal( trade.pair, self.strategy.timeframe, - analyzed_df, is_short=trade.is_short + analyzed_df, + is_short=trade.is_short ) - logger.debug('checking sell') - exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") + logger.debug('checking exit') + exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side=trade.exit_side) if self._check_and_execute_exit(trade, exit_rate, enter, exit_): return True - logger.debug('Found no sell signal for %s.', trade) + logger.debug(f'Found no {exit_signal_type} signal for %s.', trade) return False def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool: @@ -807,7 +851,10 @@ class FreqtradeBot(LoggingMixin): # If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange if not stoploss_order: stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss - stop_price = trade.open_rate * (1 + stoploss) + if trade.is_short: + stop_price = trade.open_rate * (1 - stoploss) + else: + stop_price = trade.open_rate * (1 + stoploss) if self.create_stoploss_order(trade=trade, stop_price=stop_price): trade.stoploss_last_update = datetime.utcnow() @@ -844,7 +891,7 @@ class FreqtradeBot(LoggingMixin): :param order: Current on exchange stoploss order :return: None """ - if self.exchange.stoploss_adjust(trade.stop_loss, order, side): + if self.exchange.stoploss_adjust(trade.stop_loss, order, side=trade.exit_side): # we check if the update is necessary update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat: @@ -912,22 +959,38 @@ class FreqtradeBot(LoggingMixin): fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order) - if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and ( - fully_cancelled - or self._check_timed_out('buy', order) - or strategy_safe_wrapper(self.strategy.check_buy_timeout, - default_retval=False)(pair=trade.pair, - trade=trade, - order=order))): + if ( + order['side'] == trade.enter_side and + (order['status'] == 'open' or fully_cancelled) and + (fully_cancelled or + self._check_timed_out(trade.enter_side, order) or + strategy_safe_wrapper( + self.strategy.check_buy_timeout, + default_retval=False + )( + pair=trade.pair, + trade=trade, + order=order + ) + ) + ): self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) - elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and ( - fully_cancelled - or self._check_timed_out('sell', order) - or strategy_safe_wrapper(self.strategy.check_sell_timeout, - default_retval=False)(pair=trade.pair, - trade=trade, - order=order))): + elif ( + order['side'] == trade.exit_side and + (order['status'] == 'open' or fully_cancelled) and + (fully_cancelled or + self._check_timed_out(trade.exit_side, order) or + strategy_safe_wrapper( + self.strategy.check_sell_timeout, + default_retval=False + )( + pair=trade.pair, + trade=trade, + order=order + ) + ) + ): self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) def cancel_all_open_orders(self) -> None: @@ -943,10 +1006,10 @@ class FreqtradeBot(LoggingMixin): logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) continue - if order['side'] == 'buy': + if order['side'] == trade.enter_side: self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) - elif order['side'] == 'sell': + elif order['side'] == trade.exit_side: self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) Trade.commit() @@ -968,7 +1031,7 @@ class FreqtradeBot(LoggingMixin): if filled_val > 0 and filled_stake < minstake: logger.warning( f"Order {trade.open_order_id} for {trade.pair} not cancelled, " - f"as the filled amount of {filled_val} would result in an unsellable trade.") + f"as the filled amount of {filled_val} would result in an unexitable trade.") return False corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, trade.amount) @@ -983,12 +1046,16 @@ class FreqtradeBot(LoggingMixin): corder = order reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] - logger.info('Buy order %s for %s.', reason, trade) + side = trade.enter_side.capitalize() + logger.info('%s order %s for %s.', side, reason, trade) # Using filled to determine the filled amount filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled') if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC): - logger.info('Buy order fully cancelled. Removing %s from database.', trade) + logger.info( + '%s order fully cancelled. Removing %s from database.', + side, trade + ) # if trade is not partially completed, just delete the trade trade.delete() was_trade_fully_canceled = True @@ -1006,11 +1073,11 @@ class FreqtradeBot(LoggingMixin): self.update_trade_state(trade, trade.open_order_id, corder) trade.open_order_id = None - logger.info('Partial buy order timeout for %s.', trade) + logger.info('Partial %s order timeout for %s.', trade.enter_side, trade) reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}" self.wallets.update() - self._notify_enter_cancel(trade, order_type=self.strategy.order_types['buy'], + self._notify_enter_cancel(trade, order_type=self.strategy.order_types[trade.enter_side], reason=reason) return was_trade_fully_canceled @@ -1028,12 +1095,13 @@ class FreqtradeBot(LoggingMixin): trade.amount) trade.update_order(co) except InvalidOrderException: - logger.exception(f"Could not cancel sell order {trade.open_order_id}") + logger.exception( + f"Could not cancel {trade.exit_side} order {trade.open_order_id}") return 'error cancelling order' - logger.info('Sell order %s for %s.', reason, trade) + logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade) else: reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] - logger.info('Sell order %s for %s.', reason, trade) + logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade) trade.update_order(order) trade.close_rate = None @@ -1050,7 +1118,7 @@ class FreqtradeBot(LoggingMixin): self.wallets.update() self._notify_exit_cancel( trade, - order_type=self.strategy.order_types['sell'], + order_type=self.strategy.order_types[trade.exit_side], reason=reason ) return reason @@ -1189,7 +1257,7 @@ class FreqtradeBot(LoggingMixin): profit_trade = trade.calc_profit(rate=profit_rate) # Use cached rates here - it was updated seconds ago. current_rate = self.exchange.get_rate( - trade.pair, refresh=False, side="sell") if not fill else None + trade.pair, refresh=False, side=trade.exit_side) if not fill else None profit_ratio = trade.calc_profit_ratio(profit_rate) gain = "profit" if profit_ratio > 0 else "loss" @@ -1234,7 +1302,7 @@ class FreqtradeBot(LoggingMixin): profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_trade = trade.calc_profit(rate=profit_rate) - current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="sell") + current_rate = self.exchange.get_rate(trade.pair, refresh=False, side=trade.exit_side) profit_ratio = trade.calc_profit_ratio(profit_rate) gain = "profit" if profit_ratio > 0 else "loss" diff --git a/tests/freqtradebot.py b/tests/freqtradebot.py new file mode 100644 index 000000000..8aa60f887 --- /dev/null +++ b/tests/freqtradebot.py @@ -0,0 +1,1516 @@ +""" +Freqtrade is the main module of this bot. It contains the class Freqtrade() +""" +import copy +import logging +import traceback +from datetime import datetime, timezone +from math import isclose +from threading import Lock +from typing import Any, Dict, List, Optional + +import arrow + +from freqtrade import __version__, constants +from freqtrade.configuration import validate_config_consistency +from freqtrade.data.converter import order_book_to_dataframe +from freqtrade.data.dataprovider import DataProvider +from freqtrade.edge import Edge +from freqtrade.enums import RPCMessageType, SellType, SignalDirection, State +from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, + InvalidOrderException, PricingError) +from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds +from freqtrade.misc import safe_value_fallback, safe_value_fallback2 +from freqtrade.mixins import LoggingMixin +from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db +from freqtrade.plugins.pairlistmanager import PairListManager +from freqtrade.plugins.protectionmanager import ProtectionManager +from freqtrade.resolvers import ExchangeResolver, StrategyResolver +from freqtrade.rpc import RPCManager +from freqtrade.strategy.interface import IStrategy, SellCheckTuple +from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper +from freqtrade.wallets import Wallets + + +logger = logging.getLogger(__name__) + + +class FreqtradeBot(LoggingMixin): + """ + Freqtrade is the main class of the bot. + This is from here the bot start its logic. + """ + + def __init__(self, config: Dict[str, Any]) -> None: + """ + Init all variables and objects the bot needs to work + :param config: configuration dict, you can use Configuration.get_config() + to get the config dict. + """ + self.active_pair_whitelist: List[str] = [] + + logger.info('Starting freqtrade %s', __version__) + + # Init bot state + self.state = State.STOPPED + + # Init objects + self.config = config + + self.strategy: IStrategy = StrategyResolver.load_strategy(self.config) + + # Check config consistency here since strategies can set certain options + validate_config_consistency(config) + + self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) + + init_db(self.config.get('db_url', None), clean_open_orders=self.config['dry_run']) + + # TODO-lev: Do anything with this? + self.wallets = Wallets(self.config, self.exchange) + + PairLocks.timeframe = self.config['timeframe'] + + self.protections = ProtectionManager(self.config, self.strategy.protections) + + # RPC runs in separate threads, can start handling external commands just after + # initialization, even before Freqtradebot has a chance to start its throttling, + # so anything in the Freqtradebot instance should be ready (initialized), including + # the initial state of the bot. + # Keep this at the end of this initialization method. + # TODO-lev: Do I need to consider the rpc, pairlists or dataprovider? + self.rpc: RPCManager = RPCManager(self) + + self.pairlists = PairListManager(self.exchange, self.config) + + 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 + + # Initializing Edge only if enabled + self.edge = Edge(self.config, self.exchange, self.strategy) if \ + self.config.get('edge', {}).get('enabled', False) else None + + self.active_pair_whitelist = self._refresh_active_whitelist() + + # Set initial bot state from config + initial_state = self.config.get('initial_state') + self.state = State[initial_state.upper()] if initial_state else State.STOPPED + + # Protect exit-logic from forcesell and vice versa + self._exit_lock = Lock() + LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) + + def notify_status(self, msg: str) -> None: + """ + Public method for users of this class (worker, etc.) to send notifications + via RPC about changes in the bot status. + """ + self.rpc.send_msg({ + 'type': RPCMessageType.STATUS, + 'status': msg + }) + + def cleanup(self) -> None: + """ + Cleanup pending resources on an already stopped bot + :return: None + """ + logger.info('Cleaning up modules ...') + + if self.config['cancel_open_orders_on_exit']: + self.cancel_all_open_orders() + + self.check_for_open_trades() + + self.rpc.cleanup() + cleanup_db() + + def startup(self) -> None: + """ + Called on startup and after reloading the bot - triggers notifications and + performs startup tasks + """ + self.rpc.startup_messages(self.config, self.pairlists, self.protections) + if not self.edge: + # Adjust stoploss if it was changed + Trade.stoploss_reinitialization(self.strategy.stoploss) + + # Only update open orders on startup + # This will update the database after the initial migration + self.update_open_orders() + + def process(self) -> None: + """ + Queries the persistence layer for open trades and handles them, + otherwise a new trade is created. + :return: True if one or more trades has been created or closed, False otherwise + """ + + # Check whether markets have to be reloaded and reload them when it's needed + self.exchange.reload_markets() + + self.update_closed_trades_without_assigned_fees() + + # Query trades from persistence layer + trades = Trade.get_open_trades() + + self.active_pair_whitelist = self._refresh_active_whitelist(trades) + + # Refreshing candles + self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist), + self.strategy.informative_pairs()) + + strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)() + + self.strategy.analyze(self.active_pair_whitelist) + + with self._exit_lock: + # Check and handle any timed out open orders + self.check_handle_timedout() + + # Protect from collisions with forceexit. + # Without this, freqtrade my try to recreate stoploss_on_exchange orders + # while exiting is in process, since telegram messages arrive in an different thread. + with self._exit_lock: + trades = Trade.get_open_trades() + # First process current opened trades (positions) + self.exit_positions(trades) + + # Then looking for buy opportunities + if self.get_free_open_trades(): + self.enter_positions() + + Trade.commit() + + def process_stopped(self) -> None: + """ + Close all orders that were left open + """ + if self.config['cancel_open_orders_on_exit']: + self.cancel_all_open_orders() + + def check_for_open_trades(self): + """ + Notify the user when the bot is stopped + and there are still open trades active. + """ + open_trades = Trade.get_trades([Trade.is_open.is_(True)]).all() + + if len(open_trades) != 0: + msg = { + 'type': RPCMessageType.WARNING, + 'status': f"{len(open_trades)} open trades active.\n\n" + f"Handle these trades manually on {self.exchange.name}, " + f"or '/start' the bot again and use '/stopbuy' " + f"to handle open trades gracefully. \n" + f"{'Trades are simulated.' if self.config['dry_run'] else ''}", + } + self.rpc.send_msg(msg) + + def _refresh_active_whitelist(self, trades: List[Trade] = []) -> List[str]: + """ + Refresh active whitelist from pairlist or edge and extend it with + pairs that have open trades. + """ + # Refresh whitelist + self.pairlists.refresh_pairlist() + _whitelist = self.pairlists.whitelist + + # Calculating Edge positioning + if self.edge: + self.edge.calculate(_whitelist) + _whitelist = self.edge.adjust(_whitelist) + + if trades: + # Extend active-pair whitelist with pairs of open trades + # It ensures that candle (OHLCV) data are downloaded for open trades as well + _whitelist.extend([trade.pair for trade in trades if trade.pair not in _whitelist]) + return _whitelist + + def get_free_open_trades(self) -> int: + """ + Return the number of free open trades slots or 0 if + max number of open trades reached + """ + open_trades = len(Trade.get_open_trades()) + return max(0, self.config['max_open_trades'] - open_trades) + + def update_open_orders(self): + """ + Updates open orders based on order list kept in the database. + Mainly updates the state of orders - but may also close trades + """ + if self.config['dry_run'] or self.config['exchange'].get('skip_open_order_update', False): + # Updating open orders in dry-run does not make sense and will fail. + return + + orders = Order.get_open_orders() + logger.info(f"Updating {len(orders)} open orders.") + for order in orders: + try: + fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, + order.ft_order_side == 'stoploss') + + self.update_trade_state(order.trade, order.order_id, fo) + + except ExchangeError as e: + + logger.warning(f"Error updating Order {order.order_id} due to {e}") + + def update_closed_trades_without_assigned_fees(self): + """ + Update closed trades without close fees assigned. + Only acts when Orders are in the database, otherwise the last order-id is unknown. + """ + if self.config['dry_run']: + # Updating open orders in dry-run does not make sense and will fail. + return + + trades: List[Trade] = Trade.get_closed_trades_without_assigned_fees() + for trade in trades: + if not trade.is_open and not trade.fee_updated(trade.exit_side): + # Get sell fee + order = trade.select_order(trade.exit_side, False) + if order: + logger.info( + f"Updating {trade.exit_side}-fee on trade {trade}" + f"for order {order.order_id}." + ) + self.update_trade_state(trade, order.order_id, + stoploss_order=order.ft_order_side == 'stoploss') + + trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() + for trade in trades: + if trade.is_open and not trade.fee_updated(trade.enter_side): + order = trade.select_order(trade.enter_side, False) + if order: + logger.info( + f"Updating {trade.enter_side}-fee on trade {trade}" + f"for order {order.order_id}." + ) + self.update_trade_state(trade, order.order_id) + + def handle_insufficient_funds(self, trade: Trade): + """ + Determine if we ever opened a exiting order for this trade. + If not, try update entering fees - otherwise "refind" the open order we obviously lost. + """ + exit_order = trade.select_order(trade.exit_side, None) + if exit_order: + self.refind_lost_order(trade) + else: + self.reupdate_enter_order_fees(trade) + + def reupdate_enter_order_fees(self, trade: Trade): + """ + Get buy order from database, and try to reupdate. + Handles trades where the initial fee-update did not work. + """ + logger.info(f"Trying to reupdate {trade.enter_side} fees for {trade}") + order = trade.select_order(trade.enter_side, False) + if order: + logger.info( + f"Updating {trade.enter_side}-fee on trade {trade} for order {order.order_id}.") + self.update_trade_state(trade, order.order_id) + + def refind_lost_order(self, trade): + """ + Try refinding a lost trade. + Only used when InsufficientFunds appears on exit orders (stoploss or long sell/short buy). + Tries to walk the stored orders and sell them off eventually. + """ + logger.info(f"Trying to refind lost order for {trade}") + for order in trade.orders: + logger.info(f"Trying to refind {order}") + fo = None + if not order.ft_is_open: + logger.debug(f"Order {order} is no longer open.") + continue + if order.ft_order_side == trade.enter_side: + # Skip buy side - this is handled by reupdate_enter_order_fees + continue + try: + fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, + order.ft_order_side == 'stoploss') + if order.ft_order_side == 'stoploss': + if fo and fo['status'] == 'open': + # Assume this as the open stoploss order + trade.stoploss_order_id = order.order_id + elif order.ft_order_side == trade.exit_side: + if fo and fo['status'] == 'open': + # Assume this as the open order + trade.open_order_id = order.order_id + if fo: + logger.info(f"Found {order} for trade {trade}.") + self.update_trade_state(trade, order.order_id, fo, + stoploss_order=order.ft_order_side == 'stoploss') + + except ExchangeError: + logger.warning(f"Error updating {order.order_id}.") + +# +# BUY / enter positions / open trades logic and methods +# + + def enter_positions(self) -> int: + """ + Tries to execute long buy/short sell orders for new trades (positions) + """ + trades_created = 0 + + whitelist = copy.deepcopy(self.active_pair_whitelist) + if not whitelist: + logger.info("Active pair whitelist is empty.") + return trades_created + # Remove pairs for currently opened trades from the whitelist + for trade in Trade.get_open_trades(): + if trade.pair in whitelist: + whitelist.remove(trade.pair) + logger.debug('Ignoring %s in pair whitelist', trade.pair) + + if not whitelist: + logger.info("No currency pair in active pair whitelist, " + "but checking to exit open trades.") + return trades_created + if PairLocks.is_global_lock(): + lock = PairLocks.get_pair_longest_lock('*') + if lock: + self.log_once(f"Global pairlock active until " + f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)}. " + f"Not creating new trades, reason: {lock.reason}.", logger.info) + else: + self.log_once("Global pairlock active. Not creating new trades.", logger.info) + return trades_created + # Create entity and execute trade for each pair from whitelist + for pair in whitelist: + try: + trades_created += self.create_trade(pair) + except DependencyException as exception: + logger.warning('Unable to create trade for %s: %s', pair, exception) + + if not trades_created: + logger.debug("Found no enter signals for whitelisted currencies. Trying again...") + + return trades_created + + def create_trade(self, pair: str) -> bool: + """ + Check the implemented trading strategy for buy signals. + + If the pair triggers the buy signal a new trade record gets created + and the buy-order opening the trade gets issued towards the exchange. + + :return: True if a trade has been created. + """ + logger.debug(f"create_trade for pair {pair}") + + analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, self.strategy.timeframe) + nowtime = analyzed_df.iloc[-1]['date'] if len(analyzed_df) > 0 else None + if self.strategy.is_pair_locked(pair, nowtime): + lock = PairLocks.get_pair_longest_lock(pair, nowtime) + if lock: + self.log_once(f"Pair {pair} is still locked until " + f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)} " + f"due to {lock.reason}.", + logger.info) + else: + self.log_once(f"Pair {pair} is still locked.", logger.info) + return False + + # get_free_open_trades is checked before create_trade is called + # but it is still used here to prevent opening too many trades within one iteration + if not self.get_free_open_trades(): + logger.debug(f"Can't open a new trade for {pair}: max number of trades is reached.") + return False + + # running get_signal on historical data fetched + (side, enter_tag) = self.strategy.get_entry_signal( + pair, self.strategy.timeframe, analyzed_df + ) + + if side: + stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge) + + bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {}) + if ((bid_check_dom.get('enabled', False)) and + (bid_check_dom.get('bids_to_ask_delta', 0) > 0)): + # TODO-lev: Does the below need to be adjusted for shorts? + if self._check_depth_of_market( + pair, + bid_check_dom, + side=side + ): + + return self.execute_entry(pair, stake_amount, enter_tag=enter_tag) + else: + return False + + return self.execute_entry(pair, stake_amount, enter_tag=enter_tag) + else: + return False + + def _check_depth_of_market( + self, + pair: str, + conf: Dict, + side: SignalDirection + ) -> bool: + """ + Checks depth of market before executing a buy + """ + conf_bids_to_ask_delta = conf.get('bids_to_ask_delta', 0) + logger.info(f"Checking depth of market for {pair} ...") + order_book = self.exchange.fetch_l2_order_book(pair, 1000) + order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks']) + order_book_bids = order_book_data_frame['b_size'].sum() + order_book_asks = order_book_data_frame['a_size'].sum() + + enter_side = order_book_bids if side == SignalDirection.LONG else order_book_asks + exit_side = order_book_asks if side == SignalDirection.LONG else order_book_bids + bids_ask_delta = enter_side / exit_side + + bids = f"Bids: {order_book_bids}" + asks = f"Asks: {order_book_asks}" + delta = f"Delta: {bids_ask_delta}" + + logger.info( + f"{bids}, {asks}, {delta}, Direction: {side}" + f"Bid Price: {order_book['bids'][0][0]}, Ask Price: {order_book['asks'][0][0]}, " + f"Immediate Bid Quantity: {order_book['bids'][0][1]}, " + f"Immediate Ask Quantity: {order_book['asks'][0][1]}." + ) + if bids_ask_delta >= conf_bids_to_ask_delta: + logger.info(f"Bids to asks delta for {pair} DOES satisfy condition.") + return True + else: + logger.info(f"Bids to asks delta for {pair} does not satisfy condition.") + return False + + def execute_entry( + self, + pair: str, + stake_amount: float, + price: Optional[float] = None, + forcebuy: bool = False, + leverage: float = 1.0, + is_short: bool = False, + enter_tag: Optional[str] = None + ) -> bool: + """ + Executes a limit buy for the given pair + :param pair: pair for which we want to create a LIMIT_BUY + :param stake_amount: amount of stake-currency for the pair + :param leverage: amount of leverage applied to this trade + :return: True if a buy order is created, false if it fails. + """ + time_in_force = self.strategy.order_time_in_force['buy'] + + [side, name] = ['sell', 'Short'] if is_short else ['buy', 'Long'] + + if price: + enter_limit_requested = price + else: + # Calculate price + proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side=side) + custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, + default_retval=proposed_enter_rate)( + pair=pair, current_time=datetime.now(timezone.utc), + proposed_rate=proposed_enter_rate) + + enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate) + + if not enter_limit_requested: + raise PricingError(f'Could not determine {side} price.') + + min_stake_amount = self.exchange.get_min_pair_stake_amount( + pair, + enter_limit_requested, + self.strategy.stoploss, + leverage=leverage + ) + + if not self.edge: + max_stake_amount = self.wallets.get_available_stake_amount() + stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, + 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) + stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount) + + if not stake_amount: + return False + + log_type = f"{name} signal found" + logger.info(f"{log_type}: about create a new trade for {pair} with stake_amount: " + f"{stake_amount} ...") + + amount = (stake_amount / enter_limit_requested) * leverage + order_type = self.strategy.order_types['buy'] + if forcebuy: + # Forcebuy can define a different ordertype + # TODO-lev: get a forceshort? What is this + order_type = self.strategy.order_types.get('forcebuy', order_type) + # TODO-lev: Will this work for shorting? + + 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)): + logger.info(f"User requested abortion of {name.lower()}ing {pair}") + return False + amount = self.exchange.amount_to_precision(pair, amount) + order = self.exchange.create_order(pair=pair, ordertype=order_type, side=side, + amount=amount, rate=enter_limit_requested, + time_in_force=time_in_force) + order_obj = Order.parse_from_ccxt_object(order, pair, side) + order_id = order['id'] + order_status = order.get('status', None) + + # we assume the order is executed at the price requested + enter_limit_filled_price = enter_limit_requested + amount_requested = amount + + if order_status == 'expired' or order_status == 'rejected': + order_tif = self.strategy.order_time_in_force['buy'] + + # return false if the order is not filled + if float(order['filled']) == 0: + logger.warning('%s %s order with time in force %s for %s is %s by %s.' + ' zero amount is fulfilled.', + name, order_tif, order_type, pair, order_status, self.exchange.name) + return False + else: + # the order is partially fulfilled + # in case of IOC orders we can check immediately + # if the order is fulfilled fully or partially + logger.warning('%s %s order with time in force %s for %s is %s by %s.' + ' %s amount fulfilled out of %s (%s remaining which is canceled).', + name, order_tif, order_type, pair, order_status, self.exchange.name, + order['filled'], order['amount'], order['remaining'] + ) + stake_amount = order['cost'] + amount = safe_value_fallback(order, 'filled', 'amount') + enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') + + # in case of FOK the order may be filled immediately and fully + elif order_status == 'closed': + stake_amount = order['cost'] + amount = safe_value_fallback(order, 'filled', 'amount') + enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') + + # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL + fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') + trade = Trade( + pair=pair, + stake_amount=stake_amount, + amount=amount, + is_open=True, + amount_requested=amount_requested, + fee_open=fee, + fee_close=fee, + open_rate=enter_limit_filled_price, + open_rate_requested=enter_limit_requested, + open_date=datetime.utcnow(), + exchange=self.exchange.id, + open_order_id=order_id, + strategy=self.strategy.get_strategy_name(), + # TODO-lev: compatibility layer for buy_tag (!) + buy_tag=enter_tag, + timeframe=timeframe_to_minutes(self.config['timeframe']), + leverage=leverage, + is_short=is_short, + ) + trade.orders.append(order_obj) + + # Update fees if order is closed + if order_status == 'closed': + self.update_trade_state(trade, order_id, order) + + Trade.query.session.add(trade) + Trade.commit() + + # Updating wallets + self.wallets.update() + + self._notify_enter(trade, order_type) + + return True + + def _notify_enter(self, trade: Trade, order_type: str) -> None: + """ + Sends rpc notification when a buy/short occurred. + """ + msg = { + 'trade_id': trade.id, + 'type': RPCMessageType.SHORT if trade.is_short else RPCMessageType.BUY, + 'buy_tag': trade.buy_tag, + 'exchange': self.exchange.name.capitalize(), + 'pair': trade.pair, + 'limit': trade.open_rate, + 'order_type': order_type, + 'stake_amount': trade.stake_amount, + 'stake_currency': self.config['stake_currency'], + 'fiat_currency': self.config.get('fiat_display_currency', None), + 'amount': trade.amount, + 'open_date': trade.open_date or datetime.utcnow(), + 'current_rate': trade.open_rate_requested, + } + + # Send the message + self.rpc.send_msg(msg) + + def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str) -> None: + """ + Sends rpc notification when a buy/short cancel occurred. + """ + current_rate = self.exchange.get_rate(trade.pair, refresh=False, side=trade.enter_side) + msg_type = RPCMessageType.SHORT_CANCEL if trade.is_short else RPCMessageType.BUY_CANCEL + msg = { + 'trade_id': trade.id, + 'type': msg_type, + 'buy_tag': trade.buy_tag, + 'exchange': self.exchange.name.capitalize(), + 'pair': trade.pair, + 'limit': trade.open_rate, + 'order_type': order_type, + 'stake_amount': trade.stake_amount, + 'stake_currency': self.config['stake_currency'], + 'fiat_currency': self.config.get('fiat_display_currency', None), + 'amount': trade.amount, + 'open_date': trade.open_date, + 'current_rate': current_rate, + 'reason': reason, + } + + # Send the message + self.rpc.send_msg(msg) + + def _notify_enter_fill(self, trade: Trade) -> None: + msg_type = RPCMessageType.SHORT_FILL if trade.is_short else RPCMessageType.BUY_FILL + msg = { + 'trade_id': trade.id, + 'type': msg_type, + 'buy_tag': trade.buy_tag, + 'exchange': self.exchange.name.capitalize(), + 'pair': trade.pair, + 'open_rate': trade.open_rate, + 'stake_amount': trade.stake_amount, + 'stake_currency': self.config['stake_currency'], + 'fiat_currency': self.config.get('fiat_display_currency', None), + 'amount': trade.amount, + 'open_date': trade.open_date, + } + self.rpc.send_msg(msg) + +# +# SELL / exit positions / close trades logic and methods +# + + def exit_positions(self, trades: List[Any]) -> int: + """ + Tries to execute sell/exit_short orders for open trades (positions) + """ + trades_closed = 0 + for trade in trades: + try: + + if (self.strategy.order_types.get('stoploss_on_exchange') and + self.handle_stoploss_on_exchange(trade)): + trades_closed += 1 + Trade.commit() + continue + # Check if we can sell our current pair + if trade.open_order_id is None and trade.is_open and self.handle_trade(trade): + trades_closed += 1 + + except DependencyException as exception: + logger.warning('Unable to exit trade %s: %s', trade.pair, exception) + + # Updating wallets if any trade occurred + if trades_closed: + self.wallets.update() + + return trades_closed + + def handle_trade(self, trade: Trade) -> bool: + """ + Sells/exits_short the current pair if the threshold is reached and updates the trade record. + :return: True if trade has been sold/exited_short, False otherwise + """ + if not trade.is_open: + raise DependencyException(f'Attempt to handle closed trade: {trade}') + + logger.debug('Handling %s ...', trade) + + (enter, exit_) = (False, False) + exit_signal_type = "exit_short" if trade.is_short else "exit_long" + + # TODO-lev: change to use_exit_signal, ignore_roi_if_enter_signal + if (self.config.get('use_sell_signal', True) or + self.config.get('ignore_roi_if_buy_signal', False)): + analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, + self.strategy.timeframe) + + (enter, exit_) = self.strategy.get_exit_signal( + trade.pair, + self.strategy.timeframe, + analyzed_df, + is_short=trade.is_short + ) + + logger.debug('checking exit') + exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side=trade.exit_side) + if self._check_and_execute_exit(trade, exit_rate, enter, exit_): + return True + + logger.debug(f'Found no {exit_signal_type} signal for %s.', trade) + return False + + def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool: + """ + Abstracts creating stoploss orders from the logic. + Handles errors and updates the trade database object. + Force-sells the pair (using EmergencySell reason) in case of Problems creating the order. + :return: True if the order succeeded, and False in case of problems. + """ + try: + stoploss_order = self.exchange.stoploss( + pair=trade.pair, + amount=trade.amount, + stop_price=stop_price, + order_types=self.strategy.order_types, + side=trade.exit_side + ) + + order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss') + trade.orders.append(order_obj) + trade.stoploss_order_id = str(stoploss_order['id']) + return True + except InsufficientFundsError as e: + logger.warning(f"Unable to place stoploss order {e}.") + # Try to figure out what went wrong + self.handle_insufficient_funds(trade) + + except InvalidOrderException as e: + trade.stoploss_order_id = None + logger.error(f'Unable to place a stoploss order on exchange. {e}') + logger.warning('Exiting the trade forcefully') + self.execute_trade_exit(trade, trade.stop_loss, sell_reason=SellCheckTuple( + sell_type=SellType.EMERGENCY_SELL), side=trade.exit_side) + + except ExchangeError: + trade.stoploss_order_id = None + logger.exception('Unable to place a stoploss order on exchange.') + return False + + def handle_stoploss_on_exchange(self, trade: Trade) -> bool: + """ + Check if trade is fulfilled in which case the stoploss + on exchange should be added immediately if stoploss on exchange + is enabled. + # TODO-lev: liquidation price will always be on exchange, even though + # TODO-lev: stoploss_on_exchange might not be enabled + """ + + logger.debug('Handling stoploss on exchange %s ...', trade) + + stoploss_order = None + + try: + # First we check if there is already a stoploss on exchange + stoploss_order = self.exchange.fetch_stoploss_order( + trade.stoploss_order_id, trade.pair) if trade.stoploss_order_id else None + except InvalidOrderException as exception: + logger.warning('Unable to fetch stoploss order: %s', exception) + + if stoploss_order: + trade.update_order(stoploss_order) + + # We check if stoploss order is fulfilled + if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): + # TODO-lev: Update to exit reason + trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value + self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, + stoploss_order=True) + # Lock pair for one candle to prevent immediate rebuys + self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), + reason='Auto lock') + self._notify_exit(trade, "stoploss") + return True + + if trade.open_order_id or not trade.is_open: + # Trade has an open Buy or Sell order, Stoploss-handling can't happen in this case + # as the Amount on the exchange is tied up in another trade. + # The trade can be closed already (sell-order fill confirmation came in this iteration) + return False + + # If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange + if not stoploss_order: + stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss + if trade.is_short: + stop_price = trade.open_rate * (1 - stoploss) + else: + stop_price = trade.open_rate * (1 + stoploss) + + if self.create_stoploss_order(trade=trade, stop_price=stop_price): + trade.stoploss_last_update = datetime.utcnow() + return False + + # If stoploss order is canceled for some reason we add it + if stoploss_order and stoploss_order['status'] in ('canceled', 'cancelled'): + if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss): + return False + else: + trade.stoploss_order_id = None + logger.warning('Stoploss order was cancelled, but unable to recreate one.') + + # Finally we check if stoploss on exchange should be moved up because of trailing. + # Triggered Orders are now real orders - so don't replace stoploss anymore + if ( + stoploss_order + and stoploss_order.get('status_stop') != 'triggered' + and (self.config.get('trailing_stop', False) + or self.config.get('use_custom_stoploss', False)) + ): + # if trailing stoploss is enabled we check if stoploss value has changed + # in which case we cancel stoploss order and put another one with new + # value immediately + self.handle_trailing_stoploss_on_exchange(trade, stoploss_order, side=trade.exit_side) + + return False + + def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict, side: str) -> None: + """ + Check to see if stoploss on exchange should be updated + in case of trailing stoploss on exchange + :param trade: Corresponding Trade + :param order: Current on exchange stoploss order + :return: None + """ + if self.exchange.stoploss_adjust(trade.stop_loss, order, side=trade.exit_side): + # we check if the update is necessary + update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) + if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat: + # cancelling the current stoploss on exchange first + logger.info(f"Cancelling current stoploss on exchange for pair {trade.pair} " + f"(orderid:{order['id']}) in order to add another one ...") + try: + co = self.exchange.cancel_stoploss_order_with_result(order['id'], trade.pair, + trade.amount) + trade.update_order(co) + except InvalidOrderException: + logger.exception(f"Could not cancel stoploss order {order['id']} " + f"for pair {trade.pair}") + + # Create new stoploss order + if not self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss): + logger.warning(f"Could not create trailing stoploss order " + f"for pair {trade.pair}.") + + def _check_and_execute_exit(self, trade: Trade, exit_rate: float, + enter: bool, exit_: bool) -> bool: + """ + Check and execute trade exit + """ + should_exit: SellCheckTuple = self.strategy.should_exit( + trade, exit_rate, datetime.now(timezone.utc), enter=enter, exit_=exit_, + force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 + ) + + if should_exit.sell_flag: + logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.sell_type}') + self.execute_trade_exit(trade, exit_rate, should_exit, side=trade.exit_side) + return True + return False + + def _check_timed_out(self, side: str, order: dict) -> bool: + """ + Check if timeout is active, and if the order is still open and timed out + """ + timeout = self.config.get('unfilledtimeout', {}).get(side) + ordertime = arrow.get(order['datetime']).datetime + if timeout is not None: + timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes') + timeout_kwargs = {timeout_unit: -timeout} + timeout_threshold = arrow.utcnow().shift(**timeout_kwargs).datetime + return (order['status'] == 'open' and order['side'] == side + and ordertime < timeout_threshold) + return False + + def check_handle_timedout(self) -> None: + """ + Check if any orders are timed out and cancel if necessary + :param timeoutvalue: Number of minutes until order is considered timed out + :return: None + """ + + for trade in Trade.get_open_order_trades(): + try: + if not trade.open_order_id: + continue + order = self.exchange.fetch_order(trade.open_order_id, trade.pair) + except (ExchangeError): + logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) + continue + + fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order) + + if ( + order['side'] == trade.enter_side and + (order['status'] == 'open' or fully_cancelled) and + (fully_cancelled or + self._check_timed_out(trade.enter_side, order) or + strategy_safe_wrapper( + self.strategy.check_buy_timeout, + default_retval=False + )( + pair=trade.pair, + trade=trade, + order=order + ) + ) + ): + self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) + + elif ( + order['side'] == trade.exit_side and + (order['status'] == 'open' or fully_cancelled) and + (fully_cancelled or + self._check_timed_out(trade.exit_side, order) or + strategy_safe_wrapper( + self.strategy.check_sell_timeout, + default_retval=False + )( + pair=trade.pair, + trade=trade, + order=order + ) + ) + ): + self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) + + def cancel_all_open_orders(self) -> None: + """ + Cancel all orders that are currently open + :return: None + """ + + for trade in Trade.get_open_order_trades(): + try: + order = self.exchange.fetch_order(trade.open_order_id, trade.pair) + except (ExchangeError): + logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) + continue + + if order['side'] == trade.enter_side: + self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) + + elif order['side'] == trade.exit_side: + self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) + Trade.commit() + + def handle_cancel_enter(self, trade: Trade, order: Dict, reason: str) -> bool: + """ + Buy cancel - cancel order + :return: True if order was fully cancelled + """ + # TODO-lev: Pay back borrowed/interest and transfer back on leveraged trades + was_trade_fully_canceled = False + + # Cancelled orders may have the status of 'canceled' or 'closed' + if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES: + filled_val = order.get('filled', 0.0) or 0.0 + filled_stake = filled_val * trade.open_rate + minstake = self.exchange.get_min_pair_stake_amount( + trade.pair, trade.open_rate, self.strategy.stoploss) + + if filled_val > 0 and filled_stake < minstake: + logger.warning( + f"Order {trade.open_order_id} for {trade.pair} not cancelled, " + f"as the filled amount of {filled_val} would result in an unexitable trade.") + return False + corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, + trade.amount) + # Avoid race condition where the order could not be cancelled coz its already filled. + # Simply bailing here is the only safe way - as this order will then be + # handled in the next iteration. + if corder.get('status') not in constants.NON_OPEN_EXCHANGE_STATES: + logger.warning(f"Order {trade.open_order_id} for {trade.pair} not cancelled.") + return False + else: + # Order was cancelled already, so we can reuse the existing dict + corder = order + reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] + + side = trade.enter_side.capitalize() + logger.info('%s order %s for %s.', side, reason, trade) + + # Using filled to determine the filled amount + filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled') + if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC): + logger.info( + '%s order fully cancelled. Removing %s from database.', + side, trade + ) + # if trade is not partially completed, just delete the trade + trade.delete() + was_trade_fully_canceled = True + reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}" + else: + # if trade is partially complete, edit the stake details for the trade + # and close the order + # cancel_order may not contain the full order dict, so we need to fallback + # to the order dict acquired before cancelling. + # we need to fall back to the values from order if corder does not contain these keys. + trade.amount = filled_amount + # TODO-lev: Check edge cases, we don't want to make leverage > 1.0 if we don't have to + + trade.stake_amount = trade.amount * trade.open_rate + self.update_trade_state(trade, trade.open_order_id, corder) + + trade.open_order_id = None + logger.info('Partial %s order timeout for %s.', trade.enter_side, trade) + reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}" + + self.wallets.update() + self._notify_enter_cancel(trade, order_type=self.strategy.order_types[trade.enter_side], + reason=reason) + return was_trade_fully_canceled + + def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> str: + """ + Sell/exit_short cancel - cancel order and update trade + :return: Reason for cancel + """ + # if trade is not partially completed, just cancel the order + if order['remaining'] == order['amount'] or order.get('filled') == 0.0: + if not self.exchange.check_order_canceled_empty(order): + try: + # if trade is not partially completed, just delete the order + co = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, + trade.amount) + trade.update_order(co) + except InvalidOrderException: + logger.exception( + f"Could not cancel {trade.exit_side} order {trade.open_order_id}") + return 'error cancelling order' + logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade) + else: + reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] + logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade) + trade.update_order(order) + + trade.close_rate = None + trade.close_rate_requested = None + trade.close_profit = None + trade.close_profit_abs = None + trade.close_date = None + trade.is_open = True + trade.open_order_id = None + else: + # TODO: figure out how to handle partially complete sell orders + reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] + + self.wallets.update() + self._notify_exit_cancel( + trade, + order_type=self.strategy.order_types[trade.exit_side], + reason=reason + ) + return reason + + def _safe_exit_amount(self, pair: str, amount: float) -> float: + """ + Get sellable amount. + Should be trade.amount - but will fall back to the available amount if necessary. + This should cover cases where get_real_amount() was not able to update the amount + for whatever reason. + :param pair: Pair we're trying to sell + :param amount: amount we expect to be available + :return: amount to sell + :raise: DependencyException: if available balance is not within 2% of the available amount. + """ + # TODO-lev Maybe update? + # Update wallets to ensure amounts tied up in a stoploss is now free! + self.wallets.update() + trade_base_currency = self.exchange.get_pair_base_currency(pair) + wallet_amount = self.wallets.get_free(trade_base_currency) + logger.debug(f"{pair} - Wallet: {wallet_amount} - Trade-amount: {amount}") + if wallet_amount >= amount: + return amount + elif wallet_amount > amount * 0.98: + logger.info(f"{pair} - Falling back to wallet-amount {wallet_amount} -> {amount}.") + return wallet_amount + else: + raise DependencyException( + f"Not enough amount to exit trade. Trade-amount: {amount}, Wallet: {wallet_amount}") + + def execute_trade_exit( + self, + trade: Trade, + limit: float, + sell_reason: SellCheckTuple, # TODO-lev update to exit_reason + side: str + ) -> bool: + """ + Executes a trade exit for the given trade and limit + :param trade: Trade instance + :param limit: limit rate for the sell order + :param sell_reason: Reason the sell was triggered + :param side: "buy" or "sell" + :return: True if it succeeds (supported) False (not supported) + """ + exit_type = 'sell' # TODO-lev: Update to exit + if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): + exit_type = 'stoploss' + + # if stoploss is on exchange and we are on dry_run mode, + # we consider the sell price stop price + if self.config['dry_run'] and exit_type == 'stoploss' \ + and self.strategy.order_types['stoploss_on_exchange']: + limit = trade.stop_loss + + # set custom_exit_price if available + proposed_limit_rate = limit + current_profit = trade.calc_profit_ratio(limit) + custom_exit_price = strategy_safe_wrapper(self.strategy.custom_exit_price, + default_retval=proposed_limit_rate)( + pair=trade.pair, trade=trade, + current_time=datetime.now(timezone.utc), + proposed_rate=proposed_limit_rate, current_profit=current_profit) + + limit = self.get_valid_price(custom_exit_price, proposed_limit_rate) + + # First cancelling stoploss on exchange ... + if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: + try: + co = self.exchange.cancel_stoploss_order_with_result(trade.stoploss_order_id, + trade.pair, trade.amount) + trade.update_order(co) + except InvalidOrderException: + logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") + + order_type = self.strategy.order_types[exit_type] + if sell_reason.sell_type == SellType.EMERGENCY_SELL: + # Emergency sells (default to market!) + order_type = self.strategy.order_types.get("emergencysell", "market") + if sell_reason.sell_type == SellType.FORCE_SELL: + # Force sells (default to the sell_type defined in the strategy, + # but we allow this value to be changed) + order_type = self.strategy.order_types.get("forcesell", order_type) + + amount = self._safe_exit_amount(trade.pair, trade.amount) + time_in_force = self.strategy.order_time_in_force['sell'] # TODO-lev update to exit + + if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( + pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, + time_in_force=time_in_force, sell_reason=sell_reason.sell_reason, + current_time=datetime.now(timezone.utc)): + logger.info(f"User requested abortion of exiting {trade.pair}") + return False + + try: + # Execute sell and update trade record + order = self.exchange.create_order( + pair=trade.pair, + ordertype=order_type, + amount=amount, + rate=limit, + time_in_force=time_in_force, + side=trade.exit_side + ) + except InsufficientFundsError as e: + logger.warning(f"Unable to place order {e}.") + # Try to figure out what went wrong + self.handle_insufficient_funds(trade) + return False + + order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side) + trade.orders.append(order_obj) + + trade.open_order_id = order['id'] + trade.sell_order_status = '' + trade.close_rate_requested = limit + trade.sell_reason = sell_reason.sell_reason + # In case of market sell orders the order can be closed immediately + if order.get('status', 'unknown') in ('closed', 'expired'): + self.update_trade_state(trade, trade.open_order_id, order) + Trade.commit() + + # Lock pair for one candle to prevent immediate re-trading + self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), + reason='Auto lock') + + self._notify_exit(trade, order_type) + + return True + + def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False) -> None: + """ + Sends rpc notification when a sell occurred. + """ + profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested + profit_trade = trade.calc_profit(rate=profit_rate) + # Use cached rates here - it was updated seconds ago. + current_rate = self.exchange.get_rate( + trade.pair, refresh=False, side=trade.exit_side) if not fill else None + profit_ratio = trade.calc_profit_ratio(profit_rate) + gain = "profit" if profit_ratio > 0 else "loss" + + msg = { + 'type': (RPCMessageType.SELL_FILL if fill + else RPCMessageType.SELL), + 'trade_id': trade.id, + 'exchange': trade.exchange.capitalize(), + 'pair': trade.pair, + 'gain': gain, + 'limit': profit_rate, + 'order_type': order_type, + 'amount': trade.amount, + 'open_rate': trade.open_rate, + 'close_rate': trade.close_rate, + 'current_rate': current_rate, + 'profit_amount': profit_trade, + 'profit_ratio': profit_ratio, + 'sell_reason': trade.sell_reason, + 'open_date': trade.open_date, + 'close_date': trade.close_date or datetime.utcnow(), + 'stake_currency': self.config['stake_currency'], + 'fiat_currency': self.config.get('fiat_display_currency', None), + } + + if 'fiat_display_currency' in self.config: + msg.update({ + 'fiat_currency': self.config['fiat_display_currency'], + }) + + # Send the message + self.rpc.send_msg(msg) + + def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str) -> None: + """ + Sends rpc notification when a sell cancel occurred. + """ + if trade.sell_order_status == reason: + return + else: + trade.sell_order_status = reason + + profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested + profit_trade = trade.calc_profit(rate=profit_rate) + current_rate = self.exchange.get_rate(trade.pair, refresh=False, side=trade.exit_side) + profit_ratio = trade.calc_profit_ratio(profit_rate) + gain = "profit" if profit_ratio > 0 else "loss" + + msg = { + 'type': RPCMessageType.SELL_CANCEL, + 'trade_id': trade.id, + 'exchange': trade.exchange.capitalize(), + 'pair': trade.pair, + 'gain': gain, + 'limit': profit_rate, + 'order_type': order_type, + 'amount': trade.amount, + 'open_rate': trade.open_rate, + 'current_rate': current_rate, + 'profit_amount': profit_trade, + 'profit_ratio': profit_ratio, + 'sell_reason': trade.sell_reason, + 'open_date': trade.open_date, + 'close_date': trade.close_date, + 'stake_currency': self.config['stake_currency'], + 'fiat_currency': self.config.get('fiat_display_currency', None), + 'reason': reason, + } + + if 'fiat_display_currency' in self.config: + msg.update({ + 'fiat_currency': self.config['fiat_display_currency'], + }) + + # Send the message + self.rpc.send_msg(msg) + +# +# Common update trade state methods +# + + def update_trade_state(self, trade: Trade, order_id: str, action_order: Dict[str, Any] = None, + stoploss_order: bool = False) -> bool: + """ + Checks trades with open orders and updates the amount if necessary + Handles closing both buy and sell orders. + :param trade: Trade object of the trade we're analyzing + :param order_id: Order-id of the order we're analyzing + :param action_order: Already acquired order object + :return: True if order has been cancelled without being filled partially, False otherwise + """ + if not order_id: + logger.warning(f'Orderid for trade {trade} is empty.') + return False + + # Update trade with order values + logger.info('Found open order for %s', trade) + try: + order = action_order or self.exchange.fetch_order_or_stoploss_order(order_id, + trade.pair, + stoploss_order) + except InvalidOrderException as exception: + logger.warning('Unable to fetch order %s: %s', order_id, exception) + return False + + trade.update_order(order) + + # Try update amount (binance-fix) + try: + new_amount = self.get_real_amount(trade, order) + if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount, + abs_tol=constants.MATH_CLOSE_PREC): + order['amount'] = new_amount + order.pop('filled', None) + trade.recalc_open_trade_value() + except DependencyException as exception: + logger.warning("Could not update trade amount: %s", exception) + + if self.exchange.check_order_canceled_empty(order): + # Trade has been cancelled on exchange + # Handling of this will happen in check_handle_timeout. + return True + trade.update(order) + Trade.commit() + + # Updating wallets when order is closed + if not trade.is_open: + if not stoploss_order and not trade.open_order_id: + self._notify_exit(trade, '', True) + self.protections.stop_per_pair(trade.pair) + self.protections.global_stop() + self.wallets.update() + elif not trade.open_order_id: + # Buy fill + self._notify_enter_fill(trade) + + return False + + def apply_fee_conditional(self, trade: Trade, trade_base_currency: str, + amount: float, fee_abs: float) -> float: + """ + Applies the fee to amount (either from Order or from Trades). + Can eat into dust if more than the required asset is available. + """ + self.wallets.update() + if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount: + # Eat into dust if we own more than base currency + # TODO-lev: won't be in (quote) currency for shorts + logger.info(f"Fee amount for {trade} was in base currency - " + f"Eating Fee {fee_abs} into dust.") + elif fee_abs != 0: + real_amount = self.exchange.amount_to_precision(trade.pair, amount - fee_abs) + logger.info(f"Applying fee on amount for {trade} " + f"(from {amount} to {real_amount}).") + return real_amount + return amount + + def get_real_amount(self, trade: Trade, order: Dict) -> float: + """ + Detect and update trade fee. + Calls trade.update_fee() upon correct detection. + Returns modified amount if the fee was taken from the destination currency. + Necessary for exchanges which charge fees in base currency (e.g. binance) + :return: identical (or new) amount for the trade + """ + # Init variables + order_amount = safe_value_fallback(order, 'filled', 'amount') + # Only run for closed orders + if trade.fee_updated(order.get('side', '')) or order['status'] == 'open': + return order_amount + + trade_base_currency = self.exchange.get_pair_base_currency(trade.pair) + # use fee from order-dict if possible + if self.exchange.order_has_fee(order): + fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(order) + logger.info(f"Fee for Trade {trade} [{order.get('side')}]: " + f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}") + if fee_rate is None or fee_rate < 0.02: + # Reject all fees that report as > 2%. + # These are most likely caused by a parsing bug in ccxt + # due to multiple trades (https://github.com/ccxt/ccxt/issues/8025) + trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', '')) + if trade_base_currency == fee_currency: + # Apply fee to amount + return self.apply_fee_conditional(trade, trade_base_currency, + amount=order_amount, fee_abs=fee_cost) + return order_amount + return self.fee_detection_from_trades(trade, order, order_amount) + + def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float) -> float: + """ + fee-detection fallback to Trades. Parses result of fetch_my_trades to get correct fee. + """ + trades = self.exchange.get_trades_for_order(self.exchange.get_order_id_conditional(order), + trade.pair, trade.open_date) + + if len(trades) == 0: + logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade) + return order_amount + fee_currency = None + amount = 0 + fee_abs = 0.0 + fee_cost = 0.0 + trade_base_currency = self.exchange.get_pair_base_currency(trade.pair) + fee_rate_array: List[float] = [] + for exectrade in trades: + amount += exectrade['amount'] + if self.exchange.order_has_fee(exectrade): + fee_cost_, fee_currency, fee_rate_ = self.exchange.extract_cost_curr_rate(exectrade) + fee_cost += fee_cost_ + if fee_rate_ is not None: + fee_rate_array.append(fee_rate_) + # only applies if fee is in quote currency! + if trade_base_currency == fee_currency: + fee_abs += fee_cost_ + # Ensure at least one trade was found: + if fee_currency: + # fee_rate should use mean + fee_rate = sum(fee_rate_array) / float(len(fee_rate_array)) if fee_rate_array else None + if fee_rate is not None and fee_rate < 0.02: + # Only update if fee-rate is < 2% + trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', '')) + + if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC): + # TODO-lev: leverage? + logger.warning(f"Amount {amount} does not match amount {trade.amount}") + raise DependencyException("Half bought? Amounts don't match") + + if fee_abs != 0: + return self.apply_fee_conditional(trade, trade_base_currency, + amount=amount, fee_abs=fee_abs) + else: + return amount + + def get_valid_price(self, custom_price: float, proposed_price: float) -> float: + """ + Return the valid price. + Check if the custom price is of the good type if not return proposed_price + :return: valid price for the order + """ + if custom_price: + try: + valid_custom_price = float(custom_price) + except ValueError: + valid_custom_price = proposed_price + else: + valid_custom_price = proposed_price + + cust_p_max_dist_r = self.config.get('custom_price_max_distance_ratio', 0.02) + min_custom_price_allowed = proposed_price - (proposed_price * cust_p_max_dist_r) + max_custom_price_allowed = proposed_price + (proposed_price * cust_p_max_dist_r) + + # Bracket between min_custom_price_allowed and max_custom_price_allowed + return max( + min(valid_custom_price, max_custom_price_allowed), + min_custom_price_allowed) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 37289888c..51e55dfe0 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -11,7 +11,7 @@ import arrow import pytest from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT -from freqtrade.enums import RPCMessageType, RunMode, SellType, State +from freqtrade.enums import RPCMessageType, RunMode, SellType, SignalDirection, State from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, TemporaryError) @@ -631,7 +631,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, limit_buy assert trade.amount == 91.07468123 assert log_has( - 'Buy signal found: about create a new trade for ETH/BTC with stake_amount: 0.001 ...', + 'Long signal found: about create a new trade for ETH/BTC with stake_amount: 0.001 ...', caplog ) @@ -2508,6 +2508,8 @@ def test_handle_cancel_enter(mocker, caplog, default_conf, limit_buy_order) -> N trade = MagicMock() trade.pair = 'LTC/USDT' trade.open_rate = 200 + trade.is_short = False + trade.enter_side = "buy" limit_buy_order['filled'] = 0.0 limit_buy_order['status'] = 'open' reason = CANCEL_REASON['TIMEOUT'] @@ -2519,7 +2521,7 @@ def test_handle_cancel_enter(mocker, caplog, default_conf, limit_buy_order) -> N limit_buy_order['filled'] = 0.01 assert not freqtrade.handle_cancel_enter(trade, limit_buy_order, reason) assert cancel_order_mock.call_count == 0 - assert log_has_re("Order .* for .* not cancelled, as the filled amount.* unsellable.*", caplog) + assert log_has_re("Order .* for .* not cancelled, as the filled amount.* unexitable.*", caplog) caplog.clear() cancel_order_mock.reset_mock() @@ -2550,6 +2552,7 @@ def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf, reason = CANCEL_REASON['TIMEOUT'] trade = MagicMock() trade.pair = 'LTC/ETH' + trade.enter_side = "buy" assert freqtrade.handle_cancel_enter(trade, limit_buy_order_canceled_empty, reason) assert cancel_order_mock.call_count == 0 assert log_has_re(r'Buy order fully cancelled. Removing .* from database\.', caplog) @@ -2577,7 +2580,9 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf, limit_buy_order, trade = MagicMock() trade.pair = 'LTC/USDT' + trade.enter_side = "buy" trade.open_rate = 200 + trade.enter_side = "buy" limit_buy_order['filled'] = 0.0 limit_buy_order['status'] = 'open' reason = CANCEL_REASON['TIMEOUT'] @@ -3374,7 +3379,7 @@ def test__safe_exit_amount_error(default_conf, fee, caplog, mocker): ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - with pytest.raises(DependencyException, match=r"Not enough amount to exit."): + with pytest.raises(DependencyException, match=r"Not enough amount to exit trade."): assert freqtrade._safe_exit_amount(trade.pair, trade.amount) @@ -4210,7 +4215,7 @@ def test_order_book_bid_strategy_exception(mocker, default_conf, caplog) -> None assert log_has_re(r'Buy Price at location 1 from orderbook could not be determined.', caplog) -def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2) -> None: +def test_check_depth_of_market(default_conf, mocker, order_book_l2) -> None: """ test check depth of market """ @@ -4227,7 +4232,7 @@ def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2) -> None: freqtrade = FreqtradeBot(default_conf) conf = default_conf['bid_strategy']['check_depth_of_market'] - assert freqtrade._check_depth_of_market_buy('ETH/BTC', conf) is False + assert freqtrade._check_depth_of_market('ETH/BTC', conf, side=SignalDirection.LONG) is False def test_order_book_ask_strategy(default_conf, limit_buy_order_open, limit_buy_order, fee, From 9de946fdacacea430e2492ced16ee2fdb89e209b Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 10 Sep 2021 23:39:31 -0600 Subject: [PATCH 046/134] added collateral and trading mode to freqtradebot and leverage prep --- freqtrade/freqtradebot.py | 64 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 6919128ba..192152b5b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -7,7 +7,7 @@ import traceback from datetime import datetime, timezone from math import isclose from threading import Lock -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple import arrow @@ -16,7 +16,8 @@ from freqtrade.configuration import validate_config_consistency from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge -from freqtrade.enums import RPCMessageType, SellType, SignalDirection, State +from freqtrade.enums import (Collateral, RPCMessageType, SellType, SignalDirection, State, + TradingMode) from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, InvalidOrderException, PricingError) from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds @@ -104,6 +105,18 @@ class FreqtradeBot(LoggingMixin): self._exit_lock = Lock() LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) + self.trading_mode: TradingMode = TradingMode.SPOT + self.collateral_type: Optional[Collateral] = None + + trading_mode = self.config.get('trading_mode') + collateral_type = self.config.get('collateral_type') + + if trading_mode: + self.trading_mode = TradingMode(trading_mode) + + if collateral_type: + self.collateral_type = Collateral(collateral_type) + def notify_status(self, msg: str) -> None: """ Public method for users of this class (worker, etc.) to send notifications @@ -490,6 +503,43 @@ class FreqtradeBot(LoggingMixin): logger.info(f"Bids to asks delta for {pair} does not satisfy condition.") return False + def leverage_prep( + self, + pair: str, + open_rate: float, + amount: float, + leverage: float, + is_short: bool + ) -> Tuple[float, Optional[float]]: + + interest_rate = 0.0 + isolated_liq = None + + # TODO-lev: Uncomment once liq and interest merged in + # if TradingMode == TradingMode.MARGIN: + # interest_rate = self.exchange.get_interest_rate( + # pair=pair, + # open_rate=open_rate, + # is_short=is_short + # ) + + # if self.collateral_type == Collateral.ISOLATED: + + # isolated_liq = liquidation_price( + # exchange_name=self.exchange.name, + # trading_mode=self.trading_mode, + # open_rate=open_rate, + # amount=amount, + # leverage=leverage, + # is_short=is_short + # ) + + if self.trading_mode == TradingMode.FUTURES: + self.exchange.set_leverage(pair, leverage) + self.exchange.set_margin_mode(pair, self.collateral_type) + + return interest_rate, isolated_liq + def execute_entry( self, pair: str, @@ -602,6 +652,14 @@ class FreqtradeBot(LoggingMixin): amount = safe_value_fallback(order, 'filled', 'amount') enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') + interest_rate, isolated_liq = self.leverage_prep( + leverage=leverage, + pair=pair, + amount=amount, + open_rate=enter_limit_filled_price, + is_short=is_short + ) + # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') trade = Trade( @@ -623,6 +681,8 @@ class FreqtradeBot(LoggingMixin): timeframe=timeframe_to_minutes(self.config['timeframe']), leverage=leverage, is_short=is_short, + interest_rate=interest_rate, + isolated_liq=isolated_liq, ) trade.orders.append(order_obj) From b1067cee6c9aa8117a4f7ef9fdaefb9d0d7f0a40 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 11 Sep 2021 00:03:01 -0600 Subject: [PATCH 047/134] minor changes --- freqtrade/exchange/kraken.py | 2 +- tests/freqtradebot.py | 1516 ---------------------------------- tests/test_persistence.py | 2 +- 3 files changed, 2 insertions(+), 1518 deletions(-) delete mode 100644 tests/freqtradebot.py diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 17e728674..b72a92070 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -40,7 +40,7 @@ class Kraken(Exchange): return (parent_check and market.get('darkpool', False) is False) - @ retrier + @retrier def get_balances(self) -> dict: if self._config['dry_run']: return {} diff --git a/tests/freqtradebot.py b/tests/freqtradebot.py deleted file mode 100644 index 8aa60f887..000000000 --- a/tests/freqtradebot.py +++ /dev/null @@ -1,1516 +0,0 @@ -""" -Freqtrade is the main module of this bot. It contains the class Freqtrade() -""" -import copy -import logging -import traceback -from datetime import datetime, timezone -from math import isclose -from threading import Lock -from typing import Any, Dict, List, Optional - -import arrow - -from freqtrade import __version__, constants -from freqtrade.configuration import validate_config_consistency -from freqtrade.data.converter import order_book_to_dataframe -from freqtrade.data.dataprovider import DataProvider -from freqtrade.edge import Edge -from freqtrade.enums import RPCMessageType, SellType, SignalDirection, State -from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError, - InvalidOrderException, PricingError) -from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds -from freqtrade.misc import safe_value_fallback, safe_value_fallback2 -from freqtrade.mixins import LoggingMixin -from freqtrade.persistence import Order, PairLocks, Trade, cleanup_db, init_db -from freqtrade.plugins.pairlistmanager import PairListManager -from freqtrade.plugins.protectionmanager import ProtectionManager -from freqtrade.resolvers import ExchangeResolver, StrategyResolver -from freqtrade.rpc import RPCManager -from freqtrade.strategy.interface import IStrategy, SellCheckTuple -from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper -from freqtrade.wallets import Wallets - - -logger = logging.getLogger(__name__) - - -class FreqtradeBot(LoggingMixin): - """ - Freqtrade is the main class of the bot. - This is from here the bot start its logic. - """ - - def __init__(self, config: Dict[str, Any]) -> None: - """ - Init all variables and objects the bot needs to work - :param config: configuration dict, you can use Configuration.get_config() - to get the config dict. - """ - self.active_pair_whitelist: List[str] = [] - - logger.info('Starting freqtrade %s', __version__) - - # Init bot state - self.state = State.STOPPED - - # Init objects - self.config = config - - self.strategy: IStrategy = StrategyResolver.load_strategy(self.config) - - # Check config consistency here since strategies can set certain options - validate_config_consistency(config) - - self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) - - init_db(self.config.get('db_url', None), clean_open_orders=self.config['dry_run']) - - # TODO-lev: Do anything with this? - self.wallets = Wallets(self.config, self.exchange) - - PairLocks.timeframe = self.config['timeframe'] - - self.protections = ProtectionManager(self.config, self.strategy.protections) - - # RPC runs in separate threads, can start handling external commands just after - # initialization, even before Freqtradebot has a chance to start its throttling, - # so anything in the Freqtradebot instance should be ready (initialized), including - # the initial state of the bot. - # Keep this at the end of this initialization method. - # TODO-lev: Do I need to consider the rpc, pairlists or dataprovider? - self.rpc: RPCManager = RPCManager(self) - - self.pairlists = PairListManager(self.exchange, self.config) - - 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 - - # Initializing Edge only if enabled - self.edge = Edge(self.config, self.exchange, self.strategy) if \ - self.config.get('edge', {}).get('enabled', False) else None - - self.active_pair_whitelist = self._refresh_active_whitelist() - - # Set initial bot state from config - initial_state = self.config.get('initial_state') - self.state = State[initial_state.upper()] if initial_state else State.STOPPED - - # Protect exit-logic from forcesell and vice versa - self._exit_lock = Lock() - LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) - - def notify_status(self, msg: str) -> None: - """ - Public method for users of this class (worker, etc.) to send notifications - via RPC about changes in the bot status. - """ - self.rpc.send_msg({ - 'type': RPCMessageType.STATUS, - 'status': msg - }) - - def cleanup(self) -> None: - """ - Cleanup pending resources on an already stopped bot - :return: None - """ - logger.info('Cleaning up modules ...') - - if self.config['cancel_open_orders_on_exit']: - self.cancel_all_open_orders() - - self.check_for_open_trades() - - self.rpc.cleanup() - cleanup_db() - - def startup(self) -> None: - """ - Called on startup and after reloading the bot - triggers notifications and - performs startup tasks - """ - self.rpc.startup_messages(self.config, self.pairlists, self.protections) - if not self.edge: - # Adjust stoploss if it was changed - Trade.stoploss_reinitialization(self.strategy.stoploss) - - # Only update open orders on startup - # This will update the database after the initial migration - self.update_open_orders() - - def process(self) -> None: - """ - Queries the persistence layer for open trades and handles them, - otherwise a new trade is created. - :return: True if one or more trades has been created or closed, False otherwise - """ - - # Check whether markets have to be reloaded and reload them when it's needed - self.exchange.reload_markets() - - self.update_closed_trades_without_assigned_fees() - - # Query trades from persistence layer - trades = Trade.get_open_trades() - - self.active_pair_whitelist = self._refresh_active_whitelist(trades) - - # Refreshing candles - self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist), - self.strategy.informative_pairs()) - - strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)() - - self.strategy.analyze(self.active_pair_whitelist) - - with self._exit_lock: - # Check and handle any timed out open orders - self.check_handle_timedout() - - # Protect from collisions with forceexit. - # Without this, freqtrade my try to recreate stoploss_on_exchange orders - # while exiting is in process, since telegram messages arrive in an different thread. - with self._exit_lock: - trades = Trade.get_open_trades() - # First process current opened trades (positions) - self.exit_positions(trades) - - # Then looking for buy opportunities - if self.get_free_open_trades(): - self.enter_positions() - - Trade.commit() - - def process_stopped(self) -> None: - """ - Close all orders that were left open - """ - if self.config['cancel_open_orders_on_exit']: - self.cancel_all_open_orders() - - def check_for_open_trades(self): - """ - Notify the user when the bot is stopped - and there are still open trades active. - """ - open_trades = Trade.get_trades([Trade.is_open.is_(True)]).all() - - if len(open_trades) != 0: - msg = { - 'type': RPCMessageType.WARNING, - 'status': f"{len(open_trades)} open trades active.\n\n" - f"Handle these trades manually on {self.exchange.name}, " - f"or '/start' the bot again and use '/stopbuy' " - f"to handle open trades gracefully. \n" - f"{'Trades are simulated.' if self.config['dry_run'] else ''}", - } - self.rpc.send_msg(msg) - - def _refresh_active_whitelist(self, trades: List[Trade] = []) -> List[str]: - """ - Refresh active whitelist from pairlist or edge and extend it with - pairs that have open trades. - """ - # Refresh whitelist - self.pairlists.refresh_pairlist() - _whitelist = self.pairlists.whitelist - - # Calculating Edge positioning - if self.edge: - self.edge.calculate(_whitelist) - _whitelist = self.edge.adjust(_whitelist) - - if trades: - # Extend active-pair whitelist with pairs of open trades - # It ensures that candle (OHLCV) data are downloaded for open trades as well - _whitelist.extend([trade.pair for trade in trades if trade.pair not in _whitelist]) - return _whitelist - - def get_free_open_trades(self) -> int: - """ - Return the number of free open trades slots or 0 if - max number of open trades reached - """ - open_trades = len(Trade.get_open_trades()) - return max(0, self.config['max_open_trades'] - open_trades) - - def update_open_orders(self): - """ - Updates open orders based on order list kept in the database. - Mainly updates the state of orders - but may also close trades - """ - if self.config['dry_run'] or self.config['exchange'].get('skip_open_order_update', False): - # Updating open orders in dry-run does not make sense and will fail. - return - - orders = Order.get_open_orders() - logger.info(f"Updating {len(orders)} open orders.") - for order in orders: - try: - fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, - order.ft_order_side == 'stoploss') - - self.update_trade_state(order.trade, order.order_id, fo) - - except ExchangeError as e: - - logger.warning(f"Error updating Order {order.order_id} due to {e}") - - def update_closed_trades_without_assigned_fees(self): - """ - Update closed trades without close fees assigned. - Only acts when Orders are in the database, otherwise the last order-id is unknown. - """ - if self.config['dry_run']: - # Updating open orders in dry-run does not make sense and will fail. - return - - trades: List[Trade] = Trade.get_closed_trades_without_assigned_fees() - for trade in trades: - if not trade.is_open and not trade.fee_updated(trade.exit_side): - # Get sell fee - order = trade.select_order(trade.exit_side, False) - if order: - logger.info( - f"Updating {trade.exit_side}-fee on trade {trade}" - f"for order {order.order_id}." - ) - self.update_trade_state(trade, order.order_id, - stoploss_order=order.ft_order_side == 'stoploss') - - trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() - for trade in trades: - if trade.is_open and not trade.fee_updated(trade.enter_side): - order = trade.select_order(trade.enter_side, False) - if order: - logger.info( - f"Updating {trade.enter_side}-fee on trade {trade}" - f"for order {order.order_id}." - ) - self.update_trade_state(trade, order.order_id) - - def handle_insufficient_funds(self, trade: Trade): - """ - Determine if we ever opened a exiting order for this trade. - If not, try update entering fees - otherwise "refind" the open order we obviously lost. - """ - exit_order = trade.select_order(trade.exit_side, None) - if exit_order: - self.refind_lost_order(trade) - else: - self.reupdate_enter_order_fees(trade) - - def reupdate_enter_order_fees(self, trade: Trade): - """ - Get buy order from database, and try to reupdate. - Handles trades where the initial fee-update did not work. - """ - logger.info(f"Trying to reupdate {trade.enter_side} fees for {trade}") - order = trade.select_order(trade.enter_side, False) - if order: - logger.info( - f"Updating {trade.enter_side}-fee on trade {trade} for order {order.order_id}.") - self.update_trade_state(trade, order.order_id) - - def refind_lost_order(self, trade): - """ - Try refinding a lost trade. - Only used when InsufficientFunds appears on exit orders (stoploss or long sell/short buy). - Tries to walk the stored orders and sell them off eventually. - """ - logger.info(f"Trying to refind lost order for {trade}") - for order in trade.orders: - logger.info(f"Trying to refind {order}") - fo = None - if not order.ft_is_open: - logger.debug(f"Order {order} is no longer open.") - continue - if order.ft_order_side == trade.enter_side: - # Skip buy side - this is handled by reupdate_enter_order_fees - continue - try: - fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, - order.ft_order_side == 'stoploss') - if order.ft_order_side == 'stoploss': - if fo and fo['status'] == 'open': - # Assume this as the open stoploss order - trade.stoploss_order_id = order.order_id - elif order.ft_order_side == trade.exit_side: - if fo and fo['status'] == 'open': - # Assume this as the open order - trade.open_order_id = order.order_id - if fo: - logger.info(f"Found {order} for trade {trade}.") - self.update_trade_state(trade, order.order_id, fo, - stoploss_order=order.ft_order_side == 'stoploss') - - except ExchangeError: - logger.warning(f"Error updating {order.order_id}.") - -# -# BUY / enter positions / open trades logic and methods -# - - def enter_positions(self) -> int: - """ - Tries to execute long buy/short sell orders for new trades (positions) - """ - trades_created = 0 - - whitelist = copy.deepcopy(self.active_pair_whitelist) - if not whitelist: - logger.info("Active pair whitelist is empty.") - return trades_created - # Remove pairs for currently opened trades from the whitelist - for trade in Trade.get_open_trades(): - if trade.pair in whitelist: - whitelist.remove(trade.pair) - logger.debug('Ignoring %s in pair whitelist', trade.pair) - - if not whitelist: - logger.info("No currency pair in active pair whitelist, " - "but checking to exit open trades.") - return trades_created - if PairLocks.is_global_lock(): - lock = PairLocks.get_pair_longest_lock('*') - if lock: - self.log_once(f"Global pairlock active until " - f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)}. " - f"Not creating new trades, reason: {lock.reason}.", logger.info) - else: - self.log_once("Global pairlock active. Not creating new trades.", logger.info) - return trades_created - # Create entity and execute trade for each pair from whitelist - for pair in whitelist: - try: - trades_created += self.create_trade(pair) - except DependencyException as exception: - logger.warning('Unable to create trade for %s: %s', pair, exception) - - if not trades_created: - logger.debug("Found no enter signals for whitelisted currencies. Trying again...") - - return trades_created - - def create_trade(self, pair: str) -> bool: - """ - Check the implemented trading strategy for buy signals. - - If the pair triggers the buy signal a new trade record gets created - and the buy-order opening the trade gets issued towards the exchange. - - :return: True if a trade has been created. - """ - logger.debug(f"create_trade for pair {pair}") - - analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(pair, self.strategy.timeframe) - nowtime = analyzed_df.iloc[-1]['date'] if len(analyzed_df) > 0 else None - if self.strategy.is_pair_locked(pair, nowtime): - lock = PairLocks.get_pair_longest_lock(pair, nowtime) - if lock: - self.log_once(f"Pair {pair} is still locked until " - f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)} " - f"due to {lock.reason}.", - logger.info) - else: - self.log_once(f"Pair {pair} is still locked.", logger.info) - return False - - # get_free_open_trades is checked before create_trade is called - # but it is still used here to prevent opening too many trades within one iteration - if not self.get_free_open_trades(): - logger.debug(f"Can't open a new trade for {pair}: max number of trades is reached.") - return False - - # running get_signal on historical data fetched - (side, enter_tag) = self.strategy.get_entry_signal( - pair, self.strategy.timeframe, analyzed_df - ) - - if side: - stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge) - - bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {}) - if ((bid_check_dom.get('enabled', False)) and - (bid_check_dom.get('bids_to_ask_delta', 0) > 0)): - # TODO-lev: Does the below need to be adjusted for shorts? - if self._check_depth_of_market( - pair, - bid_check_dom, - side=side - ): - - return self.execute_entry(pair, stake_amount, enter_tag=enter_tag) - else: - return False - - return self.execute_entry(pair, stake_amount, enter_tag=enter_tag) - else: - return False - - def _check_depth_of_market( - self, - pair: str, - conf: Dict, - side: SignalDirection - ) -> bool: - """ - Checks depth of market before executing a buy - """ - conf_bids_to_ask_delta = conf.get('bids_to_ask_delta', 0) - logger.info(f"Checking depth of market for {pair} ...") - order_book = self.exchange.fetch_l2_order_book(pair, 1000) - order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks']) - order_book_bids = order_book_data_frame['b_size'].sum() - order_book_asks = order_book_data_frame['a_size'].sum() - - enter_side = order_book_bids if side == SignalDirection.LONG else order_book_asks - exit_side = order_book_asks if side == SignalDirection.LONG else order_book_bids - bids_ask_delta = enter_side / exit_side - - bids = f"Bids: {order_book_bids}" - asks = f"Asks: {order_book_asks}" - delta = f"Delta: {bids_ask_delta}" - - logger.info( - f"{bids}, {asks}, {delta}, Direction: {side}" - f"Bid Price: {order_book['bids'][0][0]}, Ask Price: {order_book['asks'][0][0]}, " - f"Immediate Bid Quantity: {order_book['bids'][0][1]}, " - f"Immediate Ask Quantity: {order_book['asks'][0][1]}." - ) - if bids_ask_delta >= conf_bids_to_ask_delta: - logger.info(f"Bids to asks delta for {pair} DOES satisfy condition.") - return True - else: - logger.info(f"Bids to asks delta for {pair} does not satisfy condition.") - return False - - def execute_entry( - self, - pair: str, - stake_amount: float, - price: Optional[float] = None, - forcebuy: bool = False, - leverage: float = 1.0, - is_short: bool = False, - enter_tag: Optional[str] = None - ) -> bool: - """ - Executes a limit buy for the given pair - :param pair: pair for which we want to create a LIMIT_BUY - :param stake_amount: amount of stake-currency for the pair - :param leverage: amount of leverage applied to this trade - :return: True if a buy order is created, false if it fails. - """ - time_in_force = self.strategy.order_time_in_force['buy'] - - [side, name] = ['sell', 'Short'] if is_short else ['buy', 'Long'] - - if price: - enter_limit_requested = price - else: - # Calculate price - proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side=side) - custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price, - default_retval=proposed_enter_rate)( - pair=pair, current_time=datetime.now(timezone.utc), - proposed_rate=proposed_enter_rate) - - enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate) - - if not enter_limit_requested: - raise PricingError(f'Could not determine {side} price.') - - min_stake_amount = self.exchange.get_min_pair_stake_amount( - pair, - enter_limit_requested, - self.strategy.stoploss, - leverage=leverage - ) - - if not self.edge: - max_stake_amount = self.wallets.get_available_stake_amount() - stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount, - 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) - stake_amount = self.wallets._validate_stake_amount(pair, stake_amount, min_stake_amount) - - if not stake_amount: - return False - - log_type = f"{name} signal found" - logger.info(f"{log_type}: about create a new trade for {pair} with stake_amount: " - f"{stake_amount} ...") - - amount = (stake_amount / enter_limit_requested) * leverage - order_type = self.strategy.order_types['buy'] - if forcebuy: - # Forcebuy can define a different ordertype - # TODO-lev: get a forceshort? What is this - order_type = self.strategy.order_types.get('forcebuy', order_type) - # TODO-lev: Will this work for shorting? - - 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)): - logger.info(f"User requested abortion of {name.lower()}ing {pair}") - return False - amount = self.exchange.amount_to_precision(pair, amount) - order = self.exchange.create_order(pair=pair, ordertype=order_type, side=side, - amount=amount, rate=enter_limit_requested, - time_in_force=time_in_force) - order_obj = Order.parse_from_ccxt_object(order, pair, side) - order_id = order['id'] - order_status = order.get('status', None) - - # we assume the order is executed at the price requested - enter_limit_filled_price = enter_limit_requested - amount_requested = amount - - if order_status == 'expired' or order_status == 'rejected': - order_tif = self.strategy.order_time_in_force['buy'] - - # return false if the order is not filled - if float(order['filled']) == 0: - logger.warning('%s %s order with time in force %s for %s is %s by %s.' - ' zero amount is fulfilled.', - name, order_tif, order_type, pair, order_status, self.exchange.name) - return False - else: - # the order is partially fulfilled - # in case of IOC orders we can check immediately - # if the order is fulfilled fully or partially - logger.warning('%s %s order with time in force %s for %s is %s by %s.' - ' %s amount fulfilled out of %s (%s remaining which is canceled).', - name, order_tif, order_type, pair, order_status, self.exchange.name, - order['filled'], order['amount'], order['remaining'] - ) - stake_amount = order['cost'] - amount = safe_value_fallback(order, 'filled', 'amount') - enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') - - # in case of FOK the order may be filled immediately and fully - elif order_status == 'closed': - stake_amount = order['cost'] - amount = safe_value_fallback(order, 'filled', 'amount') - enter_limit_filled_price = safe_value_fallback(order, 'average', 'price') - - # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL - fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') - trade = Trade( - pair=pair, - stake_amount=stake_amount, - amount=amount, - is_open=True, - amount_requested=amount_requested, - fee_open=fee, - fee_close=fee, - open_rate=enter_limit_filled_price, - open_rate_requested=enter_limit_requested, - open_date=datetime.utcnow(), - exchange=self.exchange.id, - open_order_id=order_id, - strategy=self.strategy.get_strategy_name(), - # TODO-lev: compatibility layer for buy_tag (!) - buy_tag=enter_tag, - timeframe=timeframe_to_minutes(self.config['timeframe']), - leverage=leverage, - is_short=is_short, - ) - trade.orders.append(order_obj) - - # Update fees if order is closed - if order_status == 'closed': - self.update_trade_state(trade, order_id, order) - - Trade.query.session.add(trade) - Trade.commit() - - # Updating wallets - self.wallets.update() - - self._notify_enter(trade, order_type) - - return True - - def _notify_enter(self, trade: Trade, order_type: str) -> None: - """ - Sends rpc notification when a buy/short occurred. - """ - msg = { - 'trade_id': trade.id, - 'type': RPCMessageType.SHORT if trade.is_short else RPCMessageType.BUY, - 'buy_tag': trade.buy_tag, - 'exchange': self.exchange.name.capitalize(), - 'pair': trade.pair, - 'limit': trade.open_rate, - 'order_type': order_type, - 'stake_amount': trade.stake_amount, - 'stake_currency': self.config['stake_currency'], - 'fiat_currency': self.config.get('fiat_display_currency', None), - 'amount': trade.amount, - 'open_date': trade.open_date or datetime.utcnow(), - 'current_rate': trade.open_rate_requested, - } - - # Send the message - self.rpc.send_msg(msg) - - def _notify_enter_cancel(self, trade: Trade, order_type: str, reason: str) -> None: - """ - Sends rpc notification when a buy/short cancel occurred. - """ - current_rate = self.exchange.get_rate(trade.pair, refresh=False, side=trade.enter_side) - msg_type = RPCMessageType.SHORT_CANCEL if trade.is_short else RPCMessageType.BUY_CANCEL - msg = { - 'trade_id': trade.id, - 'type': msg_type, - 'buy_tag': trade.buy_tag, - 'exchange': self.exchange.name.capitalize(), - 'pair': trade.pair, - 'limit': trade.open_rate, - 'order_type': order_type, - 'stake_amount': trade.stake_amount, - 'stake_currency': self.config['stake_currency'], - 'fiat_currency': self.config.get('fiat_display_currency', None), - 'amount': trade.amount, - 'open_date': trade.open_date, - 'current_rate': current_rate, - 'reason': reason, - } - - # Send the message - self.rpc.send_msg(msg) - - def _notify_enter_fill(self, trade: Trade) -> None: - msg_type = RPCMessageType.SHORT_FILL if trade.is_short else RPCMessageType.BUY_FILL - msg = { - 'trade_id': trade.id, - 'type': msg_type, - 'buy_tag': trade.buy_tag, - 'exchange': self.exchange.name.capitalize(), - 'pair': trade.pair, - 'open_rate': trade.open_rate, - 'stake_amount': trade.stake_amount, - 'stake_currency': self.config['stake_currency'], - 'fiat_currency': self.config.get('fiat_display_currency', None), - 'amount': trade.amount, - 'open_date': trade.open_date, - } - self.rpc.send_msg(msg) - -# -# SELL / exit positions / close trades logic and methods -# - - def exit_positions(self, trades: List[Any]) -> int: - """ - Tries to execute sell/exit_short orders for open trades (positions) - """ - trades_closed = 0 - for trade in trades: - try: - - if (self.strategy.order_types.get('stoploss_on_exchange') and - self.handle_stoploss_on_exchange(trade)): - trades_closed += 1 - Trade.commit() - continue - # Check if we can sell our current pair - if trade.open_order_id is None and trade.is_open and self.handle_trade(trade): - trades_closed += 1 - - except DependencyException as exception: - logger.warning('Unable to exit trade %s: %s', trade.pair, exception) - - # Updating wallets if any trade occurred - if trades_closed: - self.wallets.update() - - return trades_closed - - def handle_trade(self, trade: Trade) -> bool: - """ - Sells/exits_short the current pair if the threshold is reached and updates the trade record. - :return: True if trade has been sold/exited_short, False otherwise - """ - if not trade.is_open: - raise DependencyException(f'Attempt to handle closed trade: {trade}') - - logger.debug('Handling %s ...', trade) - - (enter, exit_) = (False, False) - exit_signal_type = "exit_short" if trade.is_short else "exit_long" - - # TODO-lev: change to use_exit_signal, ignore_roi_if_enter_signal - if (self.config.get('use_sell_signal', True) or - self.config.get('ignore_roi_if_buy_signal', False)): - analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, - self.strategy.timeframe) - - (enter, exit_) = self.strategy.get_exit_signal( - trade.pair, - self.strategy.timeframe, - analyzed_df, - is_short=trade.is_short - ) - - logger.debug('checking exit') - exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side=trade.exit_side) - if self._check_and_execute_exit(trade, exit_rate, enter, exit_): - return True - - logger.debug(f'Found no {exit_signal_type} signal for %s.', trade) - return False - - def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool: - """ - Abstracts creating stoploss orders from the logic. - Handles errors and updates the trade database object. - Force-sells the pair (using EmergencySell reason) in case of Problems creating the order. - :return: True if the order succeeded, and False in case of problems. - """ - try: - stoploss_order = self.exchange.stoploss( - pair=trade.pair, - amount=trade.amount, - stop_price=stop_price, - order_types=self.strategy.order_types, - side=trade.exit_side - ) - - order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss') - trade.orders.append(order_obj) - trade.stoploss_order_id = str(stoploss_order['id']) - return True - except InsufficientFundsError as e: - logger.warning(f"Unable to place stoploss order {e}.") - # Try to figure out what went wrong - self.handle_insufficient_funds(trade) - - except InvalidOrderException as e: - trade.stoploss_order_id = None - logger.error(f'Unable to place a stoploss order on exchange. {e}') - logger.warning('Exiting the trade forcefully') - self.execute_trade_exit(trade, trade.stop_loss, sell_reason=SellCheckTuple( - sell_type=SellType.EMERGENCY_SELL), side=trade.exit_side) - - except ExchangeError: - trade.stoploss_order_id = None - logger.exception('Unable to place a stoploss order on exchange.') - return False - - def handle_stoploss_on_exchange(self, trade: Trade) -> bool: - """ - Check if trade is fulfilled in which case the stoploss - on exchange should be added immediately if stoploss on exchange - is enabled. - # TODO-lev: liquidation price will always be on exchange, even though - # TODO-lev: stoploss_on_exchange might not be enabled - """ - - logger.debug('Handling stoploss on exchange %s ...', trade) - - stoploss_order = None - - try: - # First we check if there is already a stoploss on exchange - stoploss_order = self.exchange.fetch_stoploss_order( - trade.stoploss_order_id, trade.pair) if trade.stoploss_order_id else None - except InvalidOrderException as exception: - logger.warning('Unable to fetch stoploss order: %s', exception) - - if stoploss_order: - trade.update_order(stoploss_order) - - # We check if stoploss order is fulfilled - if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): - # TODO-lev: Update to exit reason - trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value - self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, - stoploss_order=True) - # Lock pair for one candle to prevent immediate rebuys - self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), - reason='Auto lock') - self._notify_exit(trade, "stoploss") - return True - - if trade.open_order_id or not trade.is_open: - # Trade has an open Buy or Sell order, Stoploss-handling can't happen in this case - # as the Amount on the exchange is tied up in another trade. - # The trade can be closed already (sell-order fill confirmation came in this iteration) - return False - - # If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange - if not stoploss_order: - stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss - if trade.is_short: - stop_price = trade.open_rate * (1 - stoploss) - else: - stop_price = trade.open_rate * (1 + stoploss) - - if self.create_stoploss_order(trade=trade, stop_price=stop_price): - trade.stoploss_last_update = datetime.utcnow() - return False - - # If stoploss order is canceled for some reason we add it - if stoploss_order and stoploss_order['status'] in ('canceled', 'cancelled'): - if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss): - return False - else: - trade.stoploss_order_id = None - logger.warning('Stoploss order was cancelled, but unable to recreate one.') - - # Finally we check if stoploss on exchange should be moved up because of trailing. - # Triggered Orders are now real orders - so don't replace stoploss anymore - if ( - stoploss_order - and stoploss_order.get('status_stop') != 'triggered' - and (self.config.get('trailing_stop', False) - or self.config.get('use_custom_stoploss', False)) - ): - # if trailing stoploss is enabled we check if stoploss value has changed - # in which case we cancel stoploss order and put another one with new - # value immediately - self.handle_trailing_stoploss_on_exchange(trade, stoploss_order, side=trade.exit_side) - - return False - - def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict, side: str) -> None: - """ - Check to see if stoploss on exchange should be updated - in case of trailing stoploss on exchange - :param trade: Corresponding Trade - :param order: Current on exchange stoploss order - :return: None - """ - if self.exchange.stoploss_adjust(trade.stop_loss, order, side=trade.exit_side): - # we check if the update is necessary - update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) - if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat: - # cancelling the current stoploss on exchange first - logger.info(f"Cancelling current stoploss on exchange for pair {trade.pair} " - f"(orderid:{order['id']}) in order to add another one ...") - try: - co = self.exchange.cancel_stoploss_order_with_result(order['id'], trade.pair, - trade.amount) - trade.update_order(co) - except InvalidOrderException: - logger.exception(f"Could not cancel stoploss order {order['id']} " - f"for pair {trade.pair}") - - # Create new stoploss order - if not self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss): - logger.warning(f"Could not create trailing stoploss order " - f"for pair {trade.pair}.") - - def _check_and_execute_exit(self, trade: Trade, exit_rate: float, - enter: bool, exit_: bool) -> bool: - """ - Check and execute trade exit - """ - should_exit: SellCheckTuple = self.strategy.should_exit( - trade, exit_rate, datetime.now(timezone.utc), enter=enter, exit_=exit_, - force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0 - ) - - if should_exit.sell_flag: - logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.sell_type}') - self.execute_trade_exit(trade, exit_rate, should_exit, side=trade.exit_side) - return True - return False - - def _check_timed_out(self, side: str, order: dict) -> bool: - """ - Check if timeout is active, and if the order is still open and timed out - """ - timeout = self.config.get('unfilledtimeout', {}).get(side) - ordertime = arrow.get(order['datetime']).datetime - if timeout is not None: - timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes') - timeout_kwargs = {timeout_unit: -timeout} - timeout_threshold = arrow.utcnow().shift(**timeout_kwargs).datetime - return (order['status'] == 'open' and order['side'] == side - and ordertime < timeout_threshold) - return False - - def check_handle_timedout(self) -> None: - """ - Check if any orders are timed out and cancel if necessary - :param timeoutvalue: Number of minutes until order is considered timed out - :return: None - """ - - for trade in Trade.get_open_order_trades(): - try: - if not trade.open_order_id: - continue - order = self.exchange.fetch_order(trade.open_order_id, trade.pair) - except (ExchangeError): - logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) - continue - - fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order) - - if ( - order['side'] == trade.enter_side and - (order['status'] == 'open' or fully_cancelled) and - (fully_cancelled or - self._check_timed_out(trade.enter_side, order) or - strategy_safe_wrapper( - self.strategy.check_buy_timeout, - default_retval=False - )( - pair=trade.pair, - trade=trade, - order=order - ) - ) - ): - self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT']) - - elif ( - order['side'] == trade.exit_side and - (order['status'] == 'open' or fully_cancelled) and - (fully_cancelled or - self._check_timed_out(trade.exit_side, order) or - strategy_safe_wrapper( - self.strategy.check_sell_timeout, - default_retval=False - )( - pair=trade.pair, - trade=trade, - order=order - ) - ) - ): - self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) - - def cancel_all_open_orders(self) -> None: - """ - Cancel all orders that are currently open - :return: None - """ - - for trade in Trade.get_open_order_trades(): - try: - order = self.exchange.fetch_order(trade.open_order_id, trade.pair) - except (ExchangeError): - logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc()) - continue - - if order['side'] == trade.enter_side: - self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) - - elif order['side'] == trade.exit_side: - self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED']) - Trade.commit() - - def handle_cancel_enter(self, trade: Trade, order: Dict, reason: str) -> bool: - """ - Buy cancel - cancel order - :return: True if order was fully cancelled - """ - # TODO-lev: Pay back borrowed/interest and transfer back on leveraged trades - was_trade_fully_canceled = False - - # Cancelled orders may have the status of 'canceled' or 'closed' - if order['status'] not in constants.NON_OPEN_EXCHANGE_STATES: - filled_val = order.get('filled', 0.0) or 0.0 - filled_stake = filled_val * trade.open_rate - minstake = self.exchange.get_min_pair_stake_amount( - trade.pair, trade.open_rate, self.strategy.stoploss) - - if filled_val > 0 and filled_stake < minstake: - logger.warning( - f"Order {trade.open_order_id} for {trade.pair} not cancelled, " - f"as the filled amount of {filled_val} would result in an unexitable trade.") - return False - corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, - trade.amount) - # Avoid race condition where the order could not be cancelled coz its already filled. - # Simply bailing here is the only safe way - as this order will then be - # handled in the next iteration. - if corder.get('status') not in constants.NON_OPEN_EXCHANGE_STATES: - logger.warning(f"Order {trade.open_order_id} for {trade.pair} not cancelled.") - return False - else: - # Order was cancelled already, so we can reuse the existing dict - corder = order - reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] - - side = trade.enter_side.capitalize() - logger.info('%s order %s for %s.', side, reason, trade) - - # Using filled to determine the filled amount - filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled') - if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC): - logger.info( - '%s order fully cancelled. Removing %s from database.', - side, trade - ) - # if trade is not partially completed, just delete the trade - trade.delete() - was_trade_fully_canceled = True - reason += f", {constants.CANCEL_REASON['FULLY_CANCELLED']}" - else: - # if trade is partially complete, edit the stake details for the trade - # and close the order - # cancel_order may not contain the full order dict, so we need to fallback - # to the order dict acquired before cancelling. - # we need to fall back to the values from order if corder does not contain these keys. - trade.amount = filled_amount - # TODO-lev: Check edge cases, we don't want to make leverage > 1.0 if we don't have to - - trade.stake_amount = trade.amount * trade.open_rate - self.update_trade_state(trade, trade.open_order_id, corder) - - trade.open_order_id = None - logger.info('Partial %s order timeout for %s.', trade.enter_side, trade) - reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}" - - self.wallets.update() - self._notify_enter_cancel(trade, order_type=self.strategy.order_types[trade.enter_side], - reason=reason) - return was_trade_fully_canceled - - def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> str: - """ - Sell/exit_short cancel - cancel order and update trade - :return: Reason for cancel - """ - # if trade is not partially completed, just cancel the order - if order['remaining'] == order['amount'] or order.get('filled') == 0.0: - if not self.exchange.check_order_canceled_empty(order): - try: - # if trade is not partially completed, just delete the order - co = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair, - trade.amount) - trade.update_order(co) - except InvalidOrderException: - logger.exception( - f"Could not cancel {trade.exit_side} order {trade.open_order_id}") - return 'error cancelling order' - logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade) - else: - reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] - logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade) - trade.update_order(order) - - trade.close_rate = None - trade.close_rate_requested = None - trade.close_profit = None - trade.close_profit_abs = None - trade.close_date = None - trade.is_open = True - trade.open_order_id = None - else: - # TODO: figure out how to handle partially complete sell orders - reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] - - self.wallets.update() - self._notify_exit_cancel( - trade, - order_type=self.strategy.order_types[trade.exit_side], - reason=reason - ) - return reason - - def _safe_exit_amount(self, pair: str, amount: float) -> float: - """ - Get sellable amount. - Should be trade.amount - but will fall back to the available amount if necessary. - This should cover cases where get_real_amount() was not able to update the amount - for whatever reason. - :param pair: Pair we're trying to sell - :param amount: amount we expect to be available - :return: amount to sell - :raise: DependencyException: if available balance is not within 2% of the available amount. - """ - # TODO-lev Maybe update? - # Update wallets to ensure amounts tied up in a stoploss is now free! - self.wallets.update() - trade_base_currency = self.exchange.get_pair_base_currency(pair) - wallet_amount = self.wallets.get_free(trade_base_currency) - logger.debug(f"{pair} - Wallet: {wallet_amount} - Trade-amount: {amount}") - if wallet_amount >= amount: - return amount - elif wallet_amount > amount * 0.98: - logger.info(f"{pair} - Falling back to wallet-amount {wallet_amount} -> {amount}.") - return wallet_amount - else: - raise DependencyException( - f"Not enough amount to exit trade. Trade-amount: {amount}, Wallet: {wallet_amount}") - - def execute_trade_exit( - self, - trade: Trade, - limit: float, - sell_reason: SellCheckTuple, # TODO-lev update to exit_reason - side: str - ) -> bool: - """ - Executes a trade exit for the given trade and limit - :param trade: Trade instance - :param limit: limit rate for the sell order - :param sell_reason: Reason the sell was triggered - :param side: "buy" or "sell" - :return: True if it succeeds (supported) False (not supported) - """ - exit_type = 'sell' # TODO-lev: Update to exit - if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): - exit_type = 'stoploss' - - # if stoploss is on exchange and we are on dry_run mode, - # we consider the sell price stop price - if self.config['dry_run'] and exit_type == 'stoploss' \ - and self.strategy.order_types['stoploss_on_exchange']: - limit = trade.stop_loss - - # set custom_exit_price if available - proposed_limit_rate = limit - current_profit = trade.calc_profit_ratio(limit) - custom_exit_price = strategy_safe_wrapper(self.strategy.custom_exit_price, - default_retval=proposed_limit_rate)( - pair=trade.pair, trade=trade, - current_time=datetime.now(timezone.utc), - proposed_rate=proposed_limit_rate, current_profit=current_profit) - - limit = self.get_valid_price(custom_exit_price, proposed_limit_rate) - - # First cancelling stoploss on exchange ... - if self.strategy.order_types.get('stoploss_on_exchange') and trade.stoploss_order_id: - try: - co = self.exchange.cancel_stoploss_order_with_result(trade.stoploss_order_id, - trade.pair, trade.amount) - trade.update_order(co) - except InvalidOrderException: - logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") - - order_type = self.strategy.order_types[exit_type] - if sell_reason.sell_type == SellType.EMERGENCY_SELL: - # Emergency sells (default to market!) - order_type = self.strategy.order_types.get("emergencysell", "market") - if sell_reason.sell_type == SellType.FORCE_SELL: - # Force sells (default to the sell_type defined in the strategy, - # but we allow this value to be changed) - order_type = self.strategy.order_types.get("forcesell", order_type) - - amount = self._safe_exit_amount(trade.pair, trade.amount) - time_in_force = self.strategy.order_time_in_force['sell'] # TODO-lev update to exit - - if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( - pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, - time_in_force=time_in_force, sell_reason=sell_reason.sell_reason, - current_time=datetime.now(timezone.utc)): - logger.info(f"User requested abortion of exiting {trade.pair}") - return False - - try: - # Execute sell and update trade record - order = self.exchange.create_order( - pair=trade.pair, - ordertype=order_type, - amount=amount, - rate=limit, - time_in_force=time_in_force, - side=trade.exit_side - ) - except InsufficientFundsError as e: - logger.warning(f"Unable to place order {e}.") - # Try to figure out what went wrong - self.handle_insufficient_funds(trade) - return False - - order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side) - trade.orders.append(order_obj) - - trade.open_order_id = order['id'] - trade.sell_order_status = '' - trade.close_rate_requested = limit - trade.sell_reason = sell_reason.sell_reason - # In case of market sell orders the order can be closed immediately - if order.get('status', 'unknown') in ('closed', 'expired'): - self.update_trade_state(trade, trade.open_order_id, order) - Trade.commit() - - # Lock pair for one candle to prevent immediate re-trading - self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), - reason='Auto lock') - - self._notify_exit(trade, order_type) - - return True - - def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False) -> None: - """ - Sends rpc notification when a sell occurred. - """ - profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested - profit_trade = trade.calc_profit(rate=profit_rate) - # Use cached rates here - it was updated seconds ago. - current_rate = self.exchange.get_rate( - trade.pair, refresh=False, side=trade.exit_side) if not fill else None - profit_ratio = trade.calc_profit_ratio(profit_rate) - gain = "profit" if profit_ratio > 0 else "loss" - - msg = { - 'type': (RPCMessageType.SELL_FILL if fill - else RPCMessageType.SELL), - 'trade_id': trade.id, - 'exchange': trade.exchange.capitalize(), - 'pair': trade.pair, - 'gain': gain, - 'limit': profit_rate, - 'order_type': order_type, - 'amount': trade.amount, - 'open_rate': trade.open_rate, - 'close_rate': trade.close_rate, - 'current_rate': current_rate, - 'profit_amount': profit_trade, - 'profit_ratio': profit_ratio, - 'sell_reason': trade.sell_reason, - 'open_date': trade.open_date, - 'close_date': trade.close_date or datetime.utcnow(), - 'stake_currency': self.config['stake_currency'], - 'fiat_currency': self.config.get('fiat_display_currency', None), - } - - if 'fiat_display_currency' in self.config: - msg.update({ - 'fiat_currency': self.config['fiat_display_currency'], - }) - - # Send the message - self.rpc.send_msg(msg) - - def _notify_exit_cancel(self, trade: Trade, order_type: str, reason: str) -> None: - """ - Sends rpc notification when a sell cancel occurred. - """ - if trade.sell_order_status == reason: - return - else: - trade.sell_order_status = reason - - profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested - profit_trade = trade.calc_profit(rate=profit_rate) - current_rate = self.exchange.get_rate(trade.pair, refresh=False, side=trade.exit_side) - profit_ratio = trade.calc_profit_ratio(profit_rate) - gain = "profit" if profit_ratio > 0 else "loss" - - msg = { - 'type': RPCMessageType.SELL_CANCEL, - 'trade_id': trade.id, - 'exchange': trade.exchange.capitalize(), - 'pair': trade.pair, - 'gain': gain, - 'limit': profit_rate, - 'order_type': order_type, - 'amount': trade.amount, - 'open_rate': trade.open_rate, - 'current_rate': current_rate, - 'profit_amount': profit_trade, - 'profit_ratio': profit_ratio, - 'sell_reason': trade.sell_reason, - 'open_date': trade.open_date, - 'close_date': trade.close_date, - 'stake_currency': self.config['stake_currency'], - 'fiat_currency': self.config.get('fiat_display_currency', None), - 'reason': reason, - } - - if 'fiat_display_currency' in self.config: - msg.update({ - 'fiat_currency': self.config['fiat_display_currency'], - }) - - # Send the message - self.rpc.send_msg(msg) - -# -# Common update trade state methods -# - - def update_trade_state(self, trade: Trade, order_id: str, action_order: Dict[str, Any] = None, - stoploss_order: bool = False) -> bool: - """ - Checks trades with open orders and updates the amount if necessary - Handles closing both buy and sell orders. - :param trade: Trade object of the trade we're analyzing - :param order_id: Order-id of the order we're analyzing - :param action_order: Already acquired order object - :return: True if order has been cancelled without being filled partially, False otherwise - """ - if not order_id: - logger.warning(f'Orderid for trade {trade} is empty.') - return False - - # Update trade with order values - logger.info('Found open order for %s', trade) - try: - order = action_order or self.exchange.fetch_order_or_stoploss_order(order_id, - trade.pair, - stoploss_order) - except InvalidOrderException as exception: - logger.warning('Unable to fetch order %s: %s', order_id, exception) - return False - - trade.update_order(order) - - # Try update amount (binance-fix) - try: - new_amount = self.get_real_amount(trade, order) - if not isclose(safe_value_fallback(order, 'filled', 'amount'), new_amount, - abs_tol=constants.MATH_CLOSE_PREC): - order['amount'] = new_amount - order.pop('filled', None) - trade.recalc_open_trade_value() - except DependencyException as exception: - logger.warning("Could not update trade amount: %s", exception) - - if self.exchange.check_order_canceled_empty(order): - # Trade has been cancelled on exchange - # Handling of this will happen in check_handle_timeout. - return True - trade.update(order) - Trade.commit() - - # Updating wallets when order is closed - if not trade.is_open: - if not stoploss_order and not trade.open_order_id: - self._notify_exit(trade, '', True) - self.protections.stop_per_pair(trade.pair) - self.protections.global_stop() - self.wallets.update() - elif not trade.open_order_id: - # Buy fill - self._notify_enter_fill(trade) - - return False - - def apply_fee_conditional(self, trade: Trade, trade_base_currency: str, - amount: float, fee_abs: float) -> float: - """ - Applies the fee to amount (either from Order or from Trades). - Can eat into dust if more than the required asset is available. - """ - self.wallets.update() - if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount: - # Eat into dust if we own more than base currency - # TODO-lev: won't be in (quote) currency for shorts - logger.info(f"Fee amount for {trade} was in base currency - " - f"Eating Fee {fee_abs} into dust.") - elif fee_abs != 0: - real_amount = self.exchange.amount_to_precision(trade.pair, amount - fee_abs) - logger.info(f"Applying fee on amount for {trade} " - f"(from {amount} to {real_amount}).") - return real_amount - return amount - - def get_real_amount(self, trade: Trade, order: Dict) -> float: - """ - Detect and update trade fee. - Calls trade.update_fee() upon correct detection. - Returns modified amount if the fee was taken from the destination currency. - Necessary for exchanges which charge fees in base currency (e.g. binance) - :return: identical (or new) amount for the trade - """ - # Init variables - order_amount = safe_value_fallback(order, 'filled', 'amount') - # Only run for closed orders - if trade.fee_updated(order.get('side', '')) or order['status'] == 'open': - return order_amount - - trade_base_currency = self.exchange.get_pair_base_currency(trade.pair) - # use fee from order-dict if possible - if self.exchange.order_has_fee(order): - fee_cost, fee_currency, fee_rate = self.exchange.extract_cost_curr_rate(order) - logger.info(f"Fee for Trade {trade} [{order.get('side')}]: " - f"{fee_cost:.8g} {fee_currency} - rate: {fee_rate}") - if fee_rate is None or fee_rate < 0.02: - # Reject all fees that report as > 2%. - # These are most likely caused by a parsing bug in ccxt - # due to multiple trades (https://github.com/ccxt/ccxt/issues/8025) - trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', '')) - if trade_base_currency == fee_currency: - # Apply fee to amount - return self.apply_fee_conditional(trade, trade_base_currency, - amount=order_amount, fee_abs=fee_cost) - return order_amount - return self.fee_detection_from_trades(trade, order, order_amount) - - def fee_detection_from_trades(self, trade: Trade, order: Dict, order_amount: float) -> float: - """ - fee-detection fallback to Trades. Parses result of fetch_my_trades to get correct fee. - """ - trades = self.exchange.get_trades_for_order(self.exchange.get_order_id_conditional(order), - trade.pair, trade.open_date) - - if len(trades) == 0: - logger.info("Applying fee on amount for %s failed: myTrade-Dict empty found", trade) - return order_amount - fee_currency = None - amount = 0 - fee_abs = 0.0 - fee_cost = 0.0 - trade_base_currency = self.exchange.get_pair_base_currency(trade.pair) - fee_rate_array: List[float] = [] - for exectrade in trades: - amount += exectrade['amount'] - if self.exchange.order_has_fee(exectrade): - fee_cost_, fee_currency, fee_rate_ = self.exchange.extract_cost_curr_rate(exectrade) - fee_cost += fee_cost_ - if fee_rate_ is not None: - fee_rate_array.append(fee_rate_) - # only applies if fee is in quote currency! - if trade_base_currency == fee_currency: - fee_abs += fee_cost_ - # Ensure at least one trade was found: - if fee_currency: - # fee_rate should use mean - fee_rate = sum(fee_rate_array) / float(len(fee_rate_array)) if fee_rate_array else None - if fee_rate is not None and fee_rate < 0.02: - # Only update if fee-rate is < 2% - trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', '')) - - if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC): - # TODO-lev: leverage? - logger.warning(f"Amount {amount} does not match amount {trade.amount}") - raise DependencyException("Half bought? Amounts don't match") - - if fee_abs != 0: - return self.apply_fee_conditional(trade, trade_base_currency, - amount=amount, fee_abs=fee_abs) - else: - return amount - - def get_valid_price(self, custom_price: float, proposed_price: float) -> float: - """ - Return the valid price. - Check if the custom price is of the good type if not return proposed_price - :return: valid price for the order - """ - if custom_price: - try: - valid_custom_price = float(custom_price) - except ValueError: - valid_custom_price = proposed_price - else: - valid_custom_price = proposed_price - - cust_p_max_dist_r = self.config.get('custom_price_max_distance_ratio', 0.02) - min_custom_price_allowed = proposed_price - (proposed_price * cust_p_max_dist_r) - max_custom_price_allowed = proposed_price + (proposed_price * cust_p_max_dist_r) - - # Bracket between min_custom_price_allowed and max_custom_price_allowed - return max( - min(valid_custom_price, max_custom_price_allowed), - min_custom_price_allowed) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 1250e7b92..911d7d6c2 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -90,7 +90,7 @@ def test_enter_exit_side(fee): @pytest.mark.usefixtures("init_persistence") -def test_set_stop_loss_isolated_liq(fee): +def test__set_stop_loss_isolated_liq(fee): trade = Trade( id=2, pair='ADA/USDT', From 83e1067af7a369343aef54b06de8b9579207ea60 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 12 Sep 2021 23:39:08 -0600 Subject: [PATCH 048/134] leverage to exchange.create_order --- freqtrade/freqtradebot.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 192152b5b..f94ba599b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -613,9 +613,15 @@ class FreqtradeBot(LoggingMixin): logger.info(f"User requested abortion of {name.lower()}ing {pair}") return False amount = self.exchange.amount_to_precision(pair, amount) - order = self.exchange.create_order(pair=pair, ordertype=order_type, side=side, - amount=amount, rate=enter_limit_requested, - time_in_force=time_in_force) + order = self.exchange.create_order( + pair=pair, + ordertype=order_type, + side=side, + amount=amount, + rate=enter_limit_requested, + time_in_force=time_in_force, + leverage=leverage + ) order_obj = Order.parse_from_ccxt_object(order, pair, side) order_id = order['id'] order_status = order.get('status', None) From 5f6384a9613c05e9b402ca2efcb2707ee851539f Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 14 Sep 2021 15:38:26 -0600 Subject: [PATCH 049/134] Added tests to freqtradebot --- freqtrade/exchange/exchange.py | 1 + freqtrade/freqtradebot.py | 2 +- tests/conftest.py | 27 ++ tests/test_freqtradebot.py | 620 ++++++++++++++++++++++----------- 4 files changed, 445 insertions(+), 205 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2fb63d201..a78460686 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -726,6 +726,7 @@ class Exchange: if not self.exchange_has('fetchL2OrderBook'): return True ob = self.fetch_l2_order_book(pair, 1) + breakpoint() if side == 'buy': price = ob['asks'][0][0] logger.debug(f"{pair} checking dry buy-order: price={price}, limit={limit}") diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4c2e908f2..ffd6f7546 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1467,7 +1467,7 @@ class FreqtradeBot(LoggingMixin): self.wallets.update() if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount: # Eat into dust if we own more than base currency - # TODO-lev: won't be in (quote) currency for shorts + # TODO-lev: won't be in base currency for shorts logger.info(f"Fee amount for {trade} was in base currency - " f"Eating Fee {fee_abs} into dust.") elif fee_abs != 0: diff --git a/tests/conftest.py b/tests/conftest.py index 993eeeed8..0c3998ab8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2115,3 +2115,30 @@ def market_sell_order_usdt(): 'remaining': 0.0, 'status': 'closed' } + + +@pytest.fixture(scope='function') +def open_order(limit_buy_order_open, limit_sell_order_open): + # limit_sell_order_open if is_short else limit_buy_order_open + return { + True: limit_sell_order_open, + False: limit_buy_order_open + } + + +@pytest.fixture(scope='function') +def limit_order(limit_sell_order, limit_buy_order): + # limit_sell_order if is_short else limit_buy_order + return { + True: limit_sell_order, + False: limit_buy_order + } + + +@pytest.fixture(scope='function') +def old_order(limit_sell_order_old, limit_buy_order_old): + # limit_sell_order_old if is_short else limit_buy_order_old + return { + True: limit_sell_order_old, + False: limit_buy_order_old + } diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index fa4f51077..d5c8566d4 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -39,8 +39,20 @@ def patch_RPCManager(mocker) -> MagicMock: return rpc_mock +def open_order(limit_buy_order_open, limit_sell_order_open, is_short): + return limit_sell_order_open if is_short else limit_buy_order_open + + +def limit_order(limit_sell_order, limit_buy_order, is_short): + return limit_sell_order if is_short else limit_buy_order + + +def old_order(limit_sell_order_old, limit_buy_order_old, is_short): + return limit_sell_order_old if is_short else limit_buy_order_old + # Unit tests + def test_freqtradebot_state(mocker, default_conf, markets) -> None: mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)) freqtrade = get_patched_freqtradebot(mocker, default_conf) @@ -327,7 +339,12 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker, fee) -> None: assert Trade.total_open_trades_stakes() == 1.97502e-03 -def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> None: +@pytest.mark.parametrize("is_short,open_rate", [ + (False, 0.00001099), + (True, 0.00001173) +]) +def test_create_trade(default_conf, ticker, limit_buy_order, limit_sell_order, + fee, mocker, is_short, open_rate) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -344,6 +361,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> Non freqtrade.create_trade('ETH/BTC') trade = Trade.query.first() + trade.is_short = is_short assert trade is not None assert trade.stake_amount == 0.001 assert trade.is_open @@ -351,9 +369,12 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> Non assert trade.exchange == 'binance' # Simulate fulfilled LIMIT_BUY order for trade - trade.update(limit_buy_order) + if is_short: + trade.update(limit_sell_order) + else: + trade.update(limit_buy_order) - assert trade.open_rate == 0.00001099 + assert trade.open_rate == open_rate assert trade.amount == 90.99181073 assert whitelist == default_conf['exchange']['pair_whitelist'] @@ -376,15 +397,20 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, freqtrade.create_trade('ETH/BTC') +@pytest.mark.parametrize("is_short", [False, True]) def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order_open, - fee, mocker) -> None: + limit_sell_order_open, fee, mocker, is_short) -> None: patch_RPCManager(mocker) patch_exchange(mocker) - buy_mock = MagicMock(return_value=limit_buy_order_open) + enter_mock = ( + MagicMock(return_value=limit_sell_order_open) + if is_short else + MagicMock(return_value=limit_buy_order_open) + ) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - create_order=buy_mock, + create_order=enter_mock, get_fee=fee, ) default_conf['stake_amount'] = 0.0005 @@ -392,9 +418,14 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order_open, patch_get_signal(freqtrade) freqtrade.create_trade('ETH/BTC') - rate, amount = buy_mock.call_args[1]['rate'], buy_mock.call_args[1]['amount'] + rate, amount = enter_mock.call_args[1]['rate'], enter_mock.call_args[1]['amount'] assert rate * amount <= default_conf['stake_amount'] +# TODO-lev: paramatrize and convert to USDT +# @pytest.mark.parametrize("stake_amount,leverage", [ +# "buy, sell" +# ]) + def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order_open, fee, mocker, caplog) -> None: @@ -778,147 +809,159 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None: assert ("ETH/BTC", default_conf["timeframe"]) in refresh_mock.call_args[0][0] -def test_execute_entry(mocker, default_conf, fee, limit_buy_order, limit_buy_order_open) -> None: +@pytest.mark.parametrize("is_short", [True, False]) +def test_execute_entry(mocker, default_conf, fee, limit_buy_order, limit_sell_order, + limit_buy_order_open, limit_sell_order_open, is_short) -> None: + + open_order = limit_sell_order_open if is_short else limit_buy_order_open + order = limit_sell_order if is_short else limit_buy_order + patch_RPCManager(mocker) patch_exchange(mocker) freqtrade = FreqtradeBot(default_conf) freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=False) stake_amount = 2 bid = 0.11 - buy_rate_mock = MagicMock(return_value=bid) - buy_mm = MagicMock(return_value=limit_buy_order_open) + enter_rate_mock = MagicMock(return_value=bid) + enter_mm = MagicMock(return_value=open_order) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - get_rate=buy_rate_mock, + get_rate=enter_rate_mock, fetch_ticker=MagicMock(return_value={ 'bid': 0.00001172, 'ask': 0.00001173, 'last': 0.00001172 }), - create_order=buy_mm, + create_order=enter_mm, get_min_pair_stake_amount=MagicMock(return_value=1), get_fee=fee, ) pair = 'ETH/BTC' - assert not freqtrade.execute_entry(pair, stake_amount) - assert buy_rate_mock.call_count == 1 - assert buy_mm.call_count == 0 + assert not freqtrade.execute_entry(pair, stake_amount, is_short=is_short) + assert enter_rate_mock.call_count == 1 + assert enter_mm.call_count == 0 assert freqtrade.strategy.confirm_trade_entry.call_count == 1 - buy_rate_mock.reset_mock() + enter_rate_mock.reset_mock() - limit_buy_order_open['id'] = '22' + open_order['id'] = '22' freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) assert freqtrade.execute_entry(pair, stake_amount) - assert buy_rate_mock.call_count == 1 - assert buy_mm.call_count == 1 - call_args = buy_mm.call_args_list[0][1] + assert enter_rate_mock.call_count == 1 + assert enter_mm.call_count == 1 + call_args = enter_mm.call_args_list[0][1] assert call_args['pair'] == pair assert call_args['rate'] == bid assert call_args['amount'] == round(stake_amount / bid, 8) - buy_rate_mock.reset_mock() + enter_rate_mock.reset_mock() # Should create an open trade with an open order id # As the order is not fulfilled yet trade = Trade.query.first() + trade.is_short = is_short assert trade assert trade.is_open is True assert trade.open_order_id == '22' # Test calling with price - limit_buy_order_open['id'] = '33' + open_order['id'] = '33' fix_price = 0.06 - assert freqtrade.execute_entry(pair, stake_amount, fix_price) + assert freqtrade.execute_entry(pair, stake_amount, fix_price, is_short=is_short) # Make sure get_rate wasn't called again - assert buy_rate_mock.call_count == 0 + assert enter_rate_mock.call_count == 0 - assert buy_mm.call_count == 2 - call_args = buy_mm.call_args_list[1][1] + assert enter_mm.call_count == 2 + call_args = enter_mm.call_args_list[1][1] assert call_args['pair'] == pair assert call_args['rate'] == fix_price assert call_args['amount'] == round(stake_amount / fix_price, 8) # In case of closed order - limit_buy_order['status'] = 'closed' - limit_buy_order['price'] = 10 - limit_buy_order['cost'] = 100 - limit_buy_order['id'] = '444' + order['status'] = 'closed' + order['price'] = 10 + order['cost'] = 100 + order['id'] = '444' mocker.patch('freqtrade.exchange.Exchange.create_order', - MagicMock(return_value=limit_buy_order)) - assert freqtrade.execute_entry(pair, stake_amount) + MagicMock(return_value=order)) + assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) trade = Trade.query.all()[2] + trade.is_short = is_short assert trade assert trade.open_order_id is None assert trade.open_rate == 10 assert trade.stake_amount == 100 # In case of rejected or expired order and partially filled - limit_buy_order['status'] = 'expired' - limit_buy_order['amount'] = 90.99181073 - limit_buy_order['filled'] = 80.99181073 - limit_buy_order['remaining'] = 10.00 - limit_buy_order['price'] = 0.5 - limit_buy_order['cost'] = 40.495905365 - limit_buy_order['id'] = '555' + order['status'] = 'expired' + order['amount'] = 90.99181073 + order['filled'] = 80.99181073 + order['remaining'] = 10.00 + order['price'] = 0.5 + order['cost'] = 40.495905365 + order['id'] = '555' mocker.patch('freqtrade.exchange.Exchange.create_order', - MagicMock(return_value=limit_buy_order)) - assert freqtrade.execute_entry(pair, stake_amount) + MagicMock(return_value=order)) + assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) trade = Trade.query.all()[3] + trade.is_short = is_short assert trade assert trade.open_order_id == '555' assert trade.open_rate == 0.5 assert trade.stake_amount == 40.495905365 # Test with custom stake - limit_buy_order['status'] = 'open' - limit_buy_order['id'] = '556' + order['status'] = 'open' + order['id'] = '556' freqtrade.strategy.custom_stake_amount = lambda **kwargs: 150.0 - assert freqtrade.execute_entry(pair, stake_amount) + assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) trade = Trade.query.all()[4] + trade.is_short = is_short assert trade assert trade.stake_amount == 150 # Exception case - limit_buy_order['id'] = '557' + order['id'] = '557' freqtrade.strategy.custom_stake_amount = lambda **kwargs: 20 / 0 - assert freqtrade.execute_entry(pair, stake_amount) + assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) trade = Trade.query.all()[5] + trade.is_short = is_short assert trade assert trade.stake_amount == 2.0 # In case of the order is rejected and not filled at all - limit_buy_order['status'] = 'rejected' - limit_buy_order['amount'] = 90.99181073 - limit_buy_order['filled'] = 0.0 - limit_buy_order['remaining'] = 90.99181073 - limit_buy_order['price'] = 0.5 - limit_buy_order['cost'] = 0.0 - limit_buy_order['id'] = '66' + order['status'] = 'rejected' + order['amount'] = 90.99181073 + order['filled'] = 0.0 + order['remaining'] = 90.99181073 + order['price'] = 0.5 + order['cost'] = 0.0 + order['id'] = '66' mocker.patch('freqtrade.exchange.Exchange.create_order', - MagicMock(return_value=limit_buy_order)) - assert not freqtrade.execute_entry(pair, stake_amount) + MagicMock(return_value=order)) + assert not freqtrade.execute_entry(pair, stake_amount, is_short=is_short) # Fail to get price... mocker.patch('freqtrade.exchange.Exchange.get_rate', MagicMock(return_value=0.0)) - with pytest.raises(PricingError, match="Could not determine buy price."): - freqtrade.execute_entry(pair, stake_amount) + with pytest.raises(PricingError, match=f"Could not determine {'sell' if is_short else 'buy'} price."): + freqtrade.execute_entry(pair, stake_amount, is_short=is_short) # In case of custom entry price mocker.patch('freqtrade.exchange.Exchange.get_rate', return_value=0.50) - limit_buy_order['status'] = 'open' - limit_buy_order['id'] = '5566' + order['status'] = 'open' + order['id'] = '5566' freqtrade.strategy.custom_entry_price = lambda **kwargs: 0.508 - assert freqtrade.execute_entry(pair, stake_amount) + assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) trade = Trade.query.all()[6] + trade.is_short = is_short assert trade assert trade.open_rate_requested == 0.508 # In case of custom entry price set to None - limit_buy_order['status'] = 'open' - limit_buy_order['id'] = '5567' + order['status'] = 'open' + order['id'] = '5567' freqtrade.strategy.custom_entry_price = lambda **kwargs: None mocker.patch.multiple( @@ -926,22 +969,27 @@ def test_execute_entry(mocker, default_conf, fee, limit_buy_order, limit_buy_ord get_rate=MagicMock(return_value=10), ) - assert freqtrade.execute_entry(pair, stake_amount) + assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) trade = Trade.query.all()[7] + trade.is_short = is_short assert trade assert trade.open_rate_requested == 10 # In case of custom entry price not float type - limit_buy_order['status'] = 'open' - limit_buy_order['id'] = '5568' + order['status'] = 'open' + order['id'] = '5568' freqtrade.strategy.custom_entry_price = lambda **kwargs: "string price" - assert freqtrade.execute_entry(pair, stake_amount) + assert freqtrade.execute_entry(pair, stake_amount, is_short=is_short) trade = Trade.query.all()[8] + trade.is_short = is_short assert trade assert trade.open_rate_requested == 10 -def test_execute_entry_confirm_error(mocker, default_conf, fee, limit_buy_order) -> None: +@pytest.mark.parametrize("is_short", [False, True]) +def test_execute_entry_confirm_error(mocker, default_conf, fee, limit_buy_order, + limit_sell_order, is_short) -> None: + order = limit_sell_order if is_short else limit_buy_order freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -950,7 +998,7 @@ def test_execute_entry_confirm_error(mocker, default_conf, fee, limit_buy_order) 'ask': 0.00001173, 'last': 0.00001172 }), - create_order=MagicMock(return_value=limit_buy_order), + create_order=MagicMock(return_value=order), get_rate=MagicMock(return_value=0.11), get_min_pair_stake_amount=MagicMock(return_value=1), get_fee=fee, @@ -959,13 +1007,14 @@ def test_execute_entry_confirm_error(mocker, default_conf, fee, limit_buy_order) pair = 'ETH/BTC' freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=ValueError) + # TODO-lev: KeyError happens on short, why? assert freqtrade.execute_entry(pair, stake_amount) - limit_buy_order['id'] = '222' + order['id'] = '222' freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=Exception) assert freqtrade.execute_entry(pair, stake_amount) - limit_buy_order['id'] = '2223' + order['id'] = '2223' freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) assert freqtrade.execute_entry(pair, stake_amount) @@ -973,11 +1022,14 @@ def test_execute_entry_confirm_error(mocker, default_conf, fee, limit_buy_order) assert not freqtrade.execute_entry(pair, stake_amount) -def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None: +@pytest.mark.parametrize("is_short", [False, True]) +def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order, + limit_sell_order, is_short) -> None: patch_RPCManager(mocker) patch_exchange(mocker) + order = limit_sell_order if is_short else limit_buy_order mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=limit_buy_order['amount']) @@ -989,6 +1041,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None freqtrade.strategy.order_types['stoploss_on_exchange'] = True trade = MagicMock() + trade.is_short = is_short trade.open_order_id = None trade.stoploss_order_id = None trade.is_open = True @@ -1000,9 +1053,12 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None assert trade.is_open is True +@pytest.mark.parametrize("is_short", [False, True]) def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, - limit_buy_order, limit_sell_order) -> None: + limit_buy_order, limit_sell_order, is_short) -> None: stoploss = MagicMock(return_value={'id': 13434334}) + enter_order = limit_sell_order if is_short else limit_buy_order + exit_order = limit_buy_order if is_short else limit_sell_order patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -1013,8 +1069,8 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, 'last': 0.00001172 }), create_order=MagicMock(side_effect=[ - {'id': limit_buy_order['id']}, - {'id': limit_sell_order['id']}, + {'id': enter_order['id']}, + {'id': exit_order['id']}, ]), get_fee=fee, ) @@ -1029,9 +1085,11 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, # should get the stoploss order id immediately # and should return false as no trade actually happened trade = MagicMock() + trade.is_short = is_short trade.is_open = True trade.open_order_id = None trade.stoploss_order_id = None + trade.is_short = is_short assert freqtrade.handle_stoploss_on_exchange(trade) is False assert stoploss.call_count == 1 @@ -1081,7 +1139,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, 'type': 'stop_loss_limit', 'price': 3, 'average': 2, - 'amount': limit_buy_order['amount'], + 'amount': enter_order['amount'], }) mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hit) assert freqtrade.handle_stoploss_on_exchange(trade) is True @@ -1120,9 +1178,12 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, assert stoploss.call_count == 0 -def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, +@pytest.mark.parametrize("is_short", [False, True]) +def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, is_short, limit_buy_order, limit_sell_order) -> None: # Sixth case: stoploss order was cancelled but couldn't create new one + enter_order = limit_sell_order if is_short else limit_buy_order + exit_order = limit_buy_order if is_short else limit_sell_order patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -1133,8 +1194,8 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, 'last': 0.00001172 }), create_order=MagicMock(side_effect=[ - {'id': limit_buy_order['id']}, - {'id': limit_sell_order['id']}, + {'id': enter_order['id']}, + {'id': exit_order['id']}, ]), get_fee=fee, ) @@ -1148,6 +1209,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, freqtrade.enter_positions() trade = Trade.query.first() + trade.is_short = is_short trade.is_open = True trade.open_order_id = None trade.stoploss_order_id = 100 @@ -1159,13 +1221,18 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, assert trade.is_open is True -def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, - limit_buy_order_open, limit_sell_order): +@pytest.mark.parametrize("is_short", [False, True]) +def test_create_stoploss_order_invalid_order( + mocker, default_conf, caplog, fee, limit_buy_order_open, + limit_sell_order_open, limit_buy_order, limit_sell_order, is_short +): + open_order = limit_sell_order_open if is_short else limit_buy_order_open + order = limit_buy_order if is_short else limit_sell_order rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) create_order_mock = MagicMock(side_effect=[ - limit_buy_order_open, - {'id': limit_sell_order['id']} + open_order, + {'id': order['id']} ]) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -1188,6 +1255,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, freqtrade.enter_positions() trade = Trade.query.first() + trade.is_short = is_short caplog.clear() freqtrade.create_stoploss_order(trade, 200) assert trade.stoploss_order_id is None @@ -1207,9 +1275,16 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, assert rpc_mock.call_args_list[1][0][0]['order_type'] == 'market' -def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog, fee, - limit_buy_order_open, limit_sell_order): - sell_mock = MagicMock(return_value={'id': limit_sell_order['id']}) +@pytest.mark.parametrize("is_short", [False, True]) +def test_create_stoploss_order_insufficient_funds( + mocker, default_conf, caplog, fee, limit_buy_order_open, limit_sell_order_open, + limit_buy_order, limit_sell_order, is_short +): + exit_order = ( + MagicMock(return_value={'id': limit_buy_order['id']}) + if is_short else + MagicMock(return_value={'id': limit_sell_order['id']}) + ) freqtrade = get_patched_freqtradebot(mocker, default_conf) mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds') @@ -1221,8 +1296,8 @@ def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog, 'last': 0.00001172 }), create_order=MagicMock(side_effect=[ - limit_buy_order_open, - sell_mock, + limit_sell_order_open if is_short else limit_buy_order_open, + exit_order, ]), get_fee=fee, fetch_order=MagicMock(return_value={'status': 'canceled'}), @@ -1236,6 +1311,7 @@ def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog, freqtrade.enter_positions() trade = Trade.query.first() + trade.is_short = is_short caplog.clear() freqtrade.create_stoploss_order(trade, 200) # stoploss_orderid was empty before @@ -1250,11 +1326,13 @@ def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog, assert mock_insuf.call_count == 1 +@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.usefixtures("init_persistence") -def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, +def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, is_short, limit_buy_order, limit_sell_order) -> None: - # TODO-lev: test for short # When trailing stoploss is set + enter_order = limit_sell_order if is_short else limit_buy_order + exit_order = limit_buy_order if is_short else limit_sell_order stoploss = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) mocker.patch.multiple( @@ -1265,8 +1343,8 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, 'last': 0.00001172 }), create_order=MagicMock(side_effect=[ - {'id': limit_buy_order['id']}, - {'id': limit_sell_order['id']}, + {'id': enter_order['id']}, + {'id': exit_order['id']}, ]), get_fee=fee, ) @@ -1300,6 +1378,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, trade.is_open = True trade.open_order_id = None trade.stoploss_order_id = 100 + trade.is_short = is_short stoploss_order_hanging = MagicMock(return_value={ 'id': 100, @@ -1362,9 +1441,12 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, assert freqtrade.handle_trade(trade) is True -def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog, +@pytest.mark.parametrize("is_short", [False, True]) +def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog, is_short, limit_buy_order, limit_sell_order) -> None: - # TODO-lev: test for short + + enter_order = limit_sell_order if is_short else limit_buy_order + exit_order = limit_buy_order if is_short else limit_sell_order # When trailing stoploss is set stoploss = MagicMock(return_value={'id': 13434334}) patch_exchange(mocker) @@ -1377,8 +1459,8 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c 'last': 0.00001172 }), create_order=MagicMock(side_effect=[ - {'id': limit_buy_order['id']}, - {'id': limit_sell_order['id']}, + {'id': enter_order['id']}, + {'id': exit_order['id']}, ]), get_fee=fee, ) @@ -1408,6 +1490,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c trade.stoploss_order_id = "abcd" trade.stop_loss = 0.2 trade.stoploss_last_update = arrow.utcnow().shift(minutes=-601).datetime.replace(tzinfo=None) + trade.is_short = is_short stoploss_order_hanging = { 'id': "abcd", @@ -1423,7 +1506,8 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c side_effect=InvalidOrderException()) mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', return_value=stoploss_order_hanging) - freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging, side="sell") + freqtrade.handle_trailing_stoploss_on_exchange( + trade, stoploss_order_hanging, side=("buy" if is_short else "sell")) assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog) # Still try to create order @@ -1433,16 +1517,21 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c caplog.clear() cancel_mock = mocker.patch("freqtrade.exchange.Binance.cancel_stoploss_order", MagicMock()) mocker.patch("freqtrade.exchange.Binance.stoploss", side_effect=ExchangeError()) - freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging, side="sell") + freqtrade.handle_trailing_stoploss_on_exchange( + trade, stoploss_order_hanging, side=("buy" if is_short else "sell")) assert cancel_mock.call_count == 1 assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog) +@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.usefixtures("init_persistence") -def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, +def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, is_short, limit_buy_order, limit_sell_order) -> None: + + enter_order = limit_sell_order if is_short else limit_buy_order + exit_order = limit_buy_order if is_short else limit_sell_order + # When trailing stoploss is set - # TODO-lev: test for short stoploss = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) mocker.patch.multiple( @@ -1453,8 +1542,8 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, 'last': 0.00001172 }), create_order=MagicMock(side_effect=[ - {'id': limit_buy_order['id']}, - {'id': limit_sell_order['id']}, + {'id': enter_order['id']}, + {'id': exit_order['id']}, ]), get_fee=fee, ) @@ -1550,9 +1639,13 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, assert freqtrade.handle_trade(trade) is True -def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, +@pytest.mark.parametrize("is_short", [False, True]) +def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, is_short, limit_buy_order, limit_sell_order) -> None: - # TODO-lev: test for short + + enter_order = limit_sell_order if is_short else limit_buy_order + exit_order = limit_buy_order if is_short else limit_sell_order + # When trailing stoploss is set stoploss = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) @@ -1569,8 +1662,8 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, 'last': 0.00001172 }), create_order=MagicMock(side_effect=[ - {'id': limit_buy_order['id']}, - {'id': limit_sell_order['id']}, + {'id': enter_order['id']}, + {'id': exit_order['id']}, ]), get_fee=fee, stoploss=stoploss, @@ -1604,6 +1697,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, trade.is_open = True trade.open_order_id = None trade.stoploss_order_id = 100 + trade.is_short = is_short stoploss_order_hanging = MagicMock(return_value={ 'id': 100, @@ -1692,7 +1786,8 @@ def test_enter_positions_exception(mocker, default_conf, caplog) -> None: assert log_has('Unable to create trade for ETH/BTC: ', caplog) -def test_exit_positions(mocker, default_conf, limit_buy_order, caplog) -> None: +@pytest.mark.parametrize("is_short", [False, True]) +def test_exit_positions(mocker, default_conf, limit_buy_order, caplog, is_short) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) @@ -1702,6 +1797,7 @@ def test_exit_positions(mocker, default_conf, limit_buy_order, caplog) -> None: return_value=limit_buy_order['amount']) trade = MagicMock() + trade.is_short = is_short trade.open_order_id = '123' trade.open_fee = 0.001 trades = [trade] @@ -1718,11 +1814,15 @@ def test_exit_positions(mocker, default_conf, limit_buy_order, caplog) -> None: assert n == 0 -def test_exit_positions_exception(mocker, default_conf, limit_buy_order, caplog) -> None: +@pytest.mark.parametrize("is_short", [False, True]) +def test_exit_positions_exception(mocker, default_conf, limit_buy_order, + limit_sell_order, caplog, is_short) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) + order = limit_sell_order if is_short else limit_buy_order + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order) trade = MagicMock() + trade.is_short = is_short trade.open_order_id = None trade.open_fee = 0.001 trade.pair = 'ETH/BTC' @@ -1738,14 +1838,17 @@ def test_exit_positions_exception(mocker, default_conf, limit_buy_order, caplog) assert log_has('Unable to exit trade ETH/BTC: ', caplog) -def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> None: +@pytest.mark.parametrize("is_short", [False, True]) +def test_update_trade_state(mocker, default_conf, limit_buy_order, + limit_sell_order, is_short, caplog) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) + order = limit_sell_order if is_short else limit_buy_order mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) - mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', - return_value=limit_buy_order['amount']) + return_value=order['amount']) trade = Trade( open_order_id=123, @@ -1755,6 +1858,7 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No open_date=arrow.utcnow().datetime, amount=11, exchange="binance", + is_short=is_short ) assert not freqtrade.update_trade_state(trade, None) assert log_has_re(r'Orderid for trade .* is empty.', caplog) @@ -1765,7 +1869,7 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No assert not log_has_re(r'Applying fee to .*', caplog) caplog.clear() assert trade.open_order_id is None - assert trade.amount == limit_buy_order['amount'] + assert trade.amount == order['amount'] trade.open_order_id = '123' mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81) @@ -1783,8 +1887,10 @@ 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("is_short", [False, True]) def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, fee, - mocker): + limit_sell_order, is_short, mocker): + order = limit_sell_order if is_short else limit_buy_order 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)) @@ -1801,15 +1907,20 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_ fee_close=fee.return_value, open_order_id="123456", is_open=True, + is_short=is_short ) - freqtrade.update_trade_state(trade, '123456', limit_buy_order) + freqtrade.update_trade_state(trade, '123456', order) assert trade.amount != amount - assert trade.amount == limit_buy_order['amount'] + assert trade.amount == 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 +@pytest.mark.parametrize("is_short", [False, True]) +def test_update_trade_state_withorderdict_rounding_fee( + default_conf, trades_for_order, fee, limit_buy_order, limit_sell_order, + mocker, caplog, is_short +): + order = limit_sell_order if is_short else limit_buy_order + trades_for_order[0]['amount'] = 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)) @@ -1826,21 +1937,25 @@ def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_ open_order_id='123456', is_open=True, open_date=arrow.utcnow().datetime, + is_short=is_short ) - freqtrade.update_trade_state(trade, '123456', limit_buy_order) + freqtrade.update_trade_state(trade, '123456', order) assert trade.amount != amount - assert trade.amount == limit_buy_order['amount'] + assert trade.amount == order['amount'] assert log_has_re(r'Applying fee on amount for .*', caplog) -def test_update_trade_state_exception(mocker, default_conf, - limit_buy_order, caplog) -> None: +@pytest.mark.parametrize("is_short", [False, True]) +def test_update_trade_state_exception(mocker, default_conf, limit_buy_order, + limit_sell_order, is_short, caplog) -> None: + order = limit_sell_order if is_short else limit_buy_order freqtrade = get_patched_freqtradebot(mocker, default_conf) - mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order) trade = MagicMock() trade.open_order_id = '123' trade.open_fee = 0.001 + trade.is_short = is_short # Test raise of OperationalException exception mocker.patch( @@ -1867,8 +1982,13 @@ def test_update_trade_state_orderexception(mocker, default_conf, caplog) -> None assert log_has(f'Unable to fetch order {trade.open_order_id}: ', caplog) -def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_order_open, - limit_sell_order, mocker): +@pytest.mark.parametrize("is_short", [False, True]) +def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_order_open, is_short, + limit_buy_order_open, limit_buy_order, limit_sell_order, mocker): + + open_order = limit_buy_order_open if is_short else limit_sell_order_open + order = limit_buy_order if is_short else limit_sell_order + 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)) @@ -1876,7 +1996,7 @@ def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_orde mocker.patch('freqtrade.wallets.Wallets.update', wallet_mock) patch_exchange(mocker) - amount = limit_sell_order["amount"] + amount = order["amount"] freqtrade = get_patched_freqtradebot(mocker, default_conf) wallet_mock.reset_mock() trade = Trade( @@ -1889,21 +2009,28 @@ def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_orde open_date=arrow.utcnow().datetime, open_order_id="123456", is_open=True, + is_short=is_short ) - order = Order.parse_from_ccxt_object(limit_sell_order_open, 'LTC/ETH', 'sell') + order = Order.parse_from_ccxt_object(open_order, 'LTC/ETH', ('buy' if is_short else 'sell')) trade.orders.append(order) assert order.status == 'open' - freqtrade.update_trade_state(trade, trade.open_order_id, limit_sell_order) - assert trade.amount == limit_sell_order['amount'] - # Wallet needs to be updated after closing a limit-sell order to reenable buying + freqtrade.update_trade_state(trade, trade.open_order_id, order) + assert trade.amount == order['amount'] + # Wallet needs to be updated after closing a limit order to reenable buying assert wallet_mock.call_count == 1 assert not trade.is_open # Order is updated by update_trade_state assert order.status == 'closed' +@pytest.mark.parametrize("is_short", [False, True]) def test_handle_trade(default_conf, limit_buy_order, limit_sell_order_open, limit_sell_order, - fee, mocker) -> None: + limit_buy_order_open, fee, mocker, is_short) -> None: + + open_order = limit_buy_order_open if is_short else limit_sell_order_open + enter_order = limit_buy_order if is_short else limit_sell_order + exit_order = limit_sell_order if is_short else limit_buy_order + patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -1914,8 +2041,8 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order_open, limi 'last': 0.00001172 }), create_order=MagicMock(side_effect=[ - limit_buy_order, - limit_sell_order_open, + enter_order, + open_order, ]), get_fee=fee, ) @@ -1925,19 +2052,20 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order_open, limi freqtrade.enter_positions() trade = Trade.query.first() + trade.is_short = is_short assert trade time.sleep(0.01) # Race condition fix - trade.update(limit_buy_order) + trade.update(enter_order) assert trade.is_open is True freqtrade.wallets.update() patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) is True - assert trade.open_order_id == limit_sell_order['id'] + assert trade.open_order_id == exit_order['id'] - # Simulate fulfilled LIMIT_SELL order for trade - trade.update(limit_sell_order) + # Simulate fulfilled LIMIT order for trade + trade.update(exit_order) assert trade.close_rate == 0.00001173 assert trade.close_profit == 0.06201058 @@ -1945,15 +2073,21 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order_open, limi assert trade.close_date is not None -def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, - fee, mocker) -> None: +@pytest.mark.parametrize("is_short", [False, True]) +def test_handle_overlapping_signals( + default_conf, ticker, limit_buy_order_open, + limit_sell_order_open, fee, mocker, is_short +) -> None: + + open_order = limit_buy_order_open if is_short else limit_sell_order_open + patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, create_order=MagicMock(side_effect=[ - limit_buy_order_open, + open_order, {'id': 1234553382}, ]), get_fee=fee, @@ -1967,6 +2101,7 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, # Buy and Sell triggering, so doing nothing ... trades = Trade.query.all() + nb_trades = len(trades) assert nb_trades == 0 @@ -1974,6 +2109,8 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, patch_get_signal(freqtrade) freqtrade.enter_positions() trades = Trade.query.all() + for trade in trades: + trades.is_short = is_short nb_trades = len(trades) assert nb_trades == 1 assert trades[0].is_open is True @@ -1982,6 +2119,8 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, patch_get_signal(freqtrade, enter_long=False) assert freqtrade.handle_trade(trades[0]) is False trades = Trade.query.all() + for trade in trades: + trades.is_short = is_short nb_trades = len(trades) assert nb_trades == 1 assert trades[0].is_open is True @@ -1990,6 +2129,8 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, patch_get_signal(freqtrade, enter_long=True, exit_long=True) assert freqtrade.handle_trade(trades[0]) is False trades = Trade.query.all() + for trade in trades: + trades.is_short = is_short nb_trades = len(trades) assert nb_trades == 1 assert trades[0].is_open is True @@ -1997,11 +2138,17 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, # Sell is triggering, guess what : we are Selling! patch_get_signal(freqtrade, enter_long=False, exit_long=True) trades = Trade.query.all() + for trade in trades: + trades.is_short = is_short assert freqtrade.handle_trade(trades[0]) is True +@pytest.mark.parametrize("is_short", [False, True]) def test_handle_trade_roi(default_conf, ticker, limit_buy_order_open, - fee, mocker, caplog) -> None: + limit_sell_order_open, fee, mocker, caplog, is_short) -> None: + + open_order = limit_sell_order_open if is_short else limit_buy_order_open + caplog.set_level(logging.DEBUG) patch_RPCManager(mocker) @@ -2009,7 +2156,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order_open, 'freqtrade.exchange.Exchange', fetch_ticker=ticker, create_order=MagicMock(side_effect=[ - limit_buy_order_open, + open_order, {'id': 1234553382}, ]), get_fee=fee, @@ -2022,6 +2169,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order_open, freqtrade.enter_positions() trade = Trade.query.first() + trade.is_short = is_short trade.is_open = True # FIX: sniffing logs, suggest handle_trade should not execute_trade_exit @@ -2029,14 +2177,22 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order_open, # we might just want to check if we are in a sell condition without # executing # if ROI is reached we must sell + # TODO-lev: Change the next line for shorts patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) assert log_has("ETH/BTC - Required profit reached. sell_type=SellType.ROI", caplog) -def test_handle_trade_use_sell_signal(default_conf, ticker, limit_buy_order_open, - limit_sell_order_open, fee, mocker, caplog) -> None: +@pytest.mark.parametrize("is_short", [False, True]) +def test_handle_trade_use_sell_signal( + default_conf, ticker, limit_buy_order_open, + limit_sell_order_open, fee, mocker, caplog, is_short +) -> None: + + enter_open_order = limit_buy_order_open if is_short else limit_sell_order_open + exit_open_order = limit_sell_order_open if is_short else limit_buy_order_open + # use_sell_signal is True buy default caplog.set_level(logging.DEBUG) patch_RPCManager(mocker) @@ -2044,8 +2200,8 @@ def test_handle_trade_use_sell_signal(default_conf, ticker, limit_buy_order_open 'freqtrade.exchange.Exchange', fetch_ticker=ticker, create_order=MagicMock(side_effect=[ - limit_buy_order_open, - limit_sell_order_open, + enter_open_order, + exit_open_order, ]), get_fee=fee, ) @@ -2058,23 +2214,31 @@ def test_handle_trade_use_sell_signal(default_conf, ticker, limit_buy_order_open trade = Trade.query.first() trade.is_open = True + # TODO-lev: patch for short patch_get_signal(freqtrade, enter_long=False, exit_long=False) assert not freqtrade.handle_trade(trade) + # TODO-lev: patch for short patch_get_signal(freqtrade, enter_long=False, exit_long=True) assert freqtrade.handle_trade(trade) assert log_has("ETH/BTC - Sell signal received. sell_type=SellType.SELL_SIGNAL", caplog) +@pytest.mark.parametrize("is_short", [False, True]) def test_close_trade(default_conf, ticker, limit_buy_order, limit_buy_order_open, limit_sell_order, - fee, mocker) -> None: + limit_sell_order_open, fee, mocker, is_short) -> None: + + open_order = limit_buy_order_open if is_short else limit_sell_order_open + enter_order = limit_buy_order if is_short else limit_sell_order + exit_order = limit_sell_order if is_short else limit_buy_order + patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - create_order=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(return_value=open_order), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -2086,8 +2250,8 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_buy_order_open trade = Trade.query.first() assert trade - trade.update(limit_buy_order) - trade.update(limit_sell_order) + trade.update(enter_order) + trade.update(exit_order) assert trade.is_open is False with pytest.raises(DependencyException, match=r'.*closed trade.*'): @@ -2107,21 +2271,24 @@ def test_bot_loop_start_called_once(mocker, default_conf, caplog): assert ftbot.strategy.analyze.call_count == 1 +@pytest.mark.parametrize("is_short", [False, True]) def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_order_old, open_trade, - fee, mocker) -> None: + limit_sell_order_old, fee, mocker, is_short) -> None: + + old_order = limit_sell_order_old if is_short else limit_buy_order_old default_conf["unfilledtimeout"] = {"buy": 1400, "sell": 30} rpc_mock = patch_RPCManager(mocker) - cancel_order_mock = MagicMock(return_value=limit_buy_order_old) - cancel_buy_order = deepcopy(limit_buy_order_old) - cancel_buy_order['status'] = 'canceled' - cancel_order_wr_mock = MagicMock(return_value=cancel_buy_order) + cancel_order_mock = MagicMock(return_value=old_order) + cancel_enter_order = deepcopy(old_order) + cancel_enter_order['status'] = 'canceled' + cancel_order_wr_mock = MagicMock(return_value=cancel_enter_order) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - fetch_order=MagicMock(return_value=limit_buy_order_old), + fetch_order=MagicMock(return_value=old_order), cancel_order_with_result=cancel_order_wr_mock, cancel_order=cancel_order_mock, get_fee=fee @@ -2163,17 +2330,19 @@ def test_check_handle_timedout_buy_usercustom(default_conf, ticker, limit_buy_or assert freqtrade.strategy.check_buy_timeout.call_count == 1 +@pytest.mark.parametrize("is_short", [False, True]) def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, open_trade, - fee, mocker) -> None: + limit_sell_order_old, fee, mocker, is_short) -> None: + old_order = limit_sell_order_old if is_short else limit_buy_order_old rpc_mock = patch_RPCManager(mocker) - limit_buy_cancel = deepcopy(limit_buy_order_old) + limit_buy_cancel = deepcopy(old_order) limit_buy_cancel['status'] = 'canceled' cancel_order_mock = MagicMock(return_value=limit_buy_cancel) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - fetch_order=MagicMock(return_value=limit_buy_order_old), + fetch_order=MagicMock(return_value=old_order), cancel_order_with_result=cancel_order_mock, get_fee=fee ) @@ -2193,17 +2362,19 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, op assert freqtrade.strategy.check_buy_timeout.call_count == 0 +@pytest.mark.parametrize("is_short", [False, True]) def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, open_trade, - fee, mocker, caplog) -> None: + limit_sell_order_old, fee, mocker, caplog, is_short) -> None: """ Handle Buy order cancelled on exchange""" + old_order = limit_sell_order_old if is_short else limit_buy_order_old rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock() patch_exchange(mocker) - limit_buy_order_old.update({"status": "canceled", 'filled': 0.0}) + old_order.update({"status": "canceled", 'filled': 0.0}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - fetch_order=MagicMock(return_value=limit_buy_order_old), + fetch_order=MagicMock(return_value=old_order), cancel_order=cancel_order_mock, get_fee=fee ) @@ -2218,10 +2389,12 @@ def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, o trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() nb_trades = len(trades) assert nb_trades == 0 - assert log_has_re("Buy order cancelled on exchange for Trade.*", caplog) + assert log_has_re( + f"{'Sell' if is_short else 'Buy'} order cancelled on exchange for Trade.*", caplog) -def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_order_old, open_trade, +@pytest.mark.parametrize("is_short", [False, True]) +def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_order_old, open_trade, is_short, fee, mocker) -> None: rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock() @@ -2247,7 +2420,8 @@ def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_ord assert nb_trades == 1 -def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_order_old, mocker, +@pytest.mark.parametrize("is_short", [False, True]) +def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_order_old, mocker, is_short, open_trade) -> None: default_conf["unfilledtimeout"] = {"buy": 1440, "sell": 1440} rpc_mock = patch_RPCManager(mocker) @@ -2296,7 +2470,8 @@ def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_ assert freqtrade.strategy.check_sell_timeout.call_count == 1 -def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker, +@pytest.mark.parametrize("is_short", [False, True]) +def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker, is_short, open_trade) -> None: rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock() @@ -2326,7 +2501,8 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, assert freqtrade.strategy.check_sell_timeout.call_count == 0 -def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old, open_trade, +@pytest.mark.parametrize("is_short", [False, True]) +def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old, open_trade, is_short, mocker, caplog) -> None: """ Handle sell order cancelled on exchange""" rpc_mock = patch_RPCManager(mocker) @@ -2355,7 +2531,8 @@ def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old, assert log_has_re("Sell order cancelled on exchange for Trade.*", caplog) -def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial, +@pytest.mark.parametrize("is_short", [False, True]) +def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial, is_short, open_trade, mocker) -> None: rpc_mock = patch_RPCManager(mocker) limit_buy_canceled = deepcopy(limit_buy_order_old_partial) @@ -2384,7 +2561,8 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old assert trades[0].stake_amount == open_trade.open_rate * trades[0].amount -def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, caplog, fee, +@pytest.mark.parametrize("is_short", [False, True]) +def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, caplog, fee, is_short, limit_buy_order_old_partial, trades_for_order, limit_buy_order_old_partial_canceled, mocker) -> None: rpc_mock = patch_RPCManager(mocker) @@ -2423,7 +2601,8 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap assert pytest.approx(trades[0].fee_open) == 0.001 -def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade, caplog, fee, +@pytest.mark.parametrize("is_short", [False, True]) +def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade, caplog, fee, is_short, limit_buy_order_old_partial, trades_for_order, limit_buy_order_old_partial_canceled, mocker) -> None: rpc_mock = patch_RPCManager(mocker) @@ -2463,7 +2642,7 @@ def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade, assert trades[0].fee_open == fee() -def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocker, caplog) -> None: +def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocker, caplog, is_short) -> None: patch_RPCManager(mocker) patch_exchange(mocker) cancel_order_mock = MagicMock() @@ -2492,7 +2671,8 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke caplog) -def test_handle_cancel_enter(mocker, caplog, default_conf, limit_buy_order) -> None: +@pytest.mark.parametrize("is_short", [False, True]) +def test_handle_cancel_enter(mocker, caplog, default_conf, limit_buy_order, is_short) -> None: patch_RPCManager(mocker) patch_exchange(mocker) cancel_buy_order = deepcopy(limit_buy_order) @@ -2537,9 +2717,10 @@ def test_handle_cancel_enter(mocker, caplog, default_conf, limit_buy_order) -> N assert log_has_re(r"Order .* for .* not cancelled.", caplog) +@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize("limit_buy_order_canceled_empty", ['binance', 'ftx', 'kraken', 'bittrex'], indirect=['limit_buy_order_canceled_empty']) -def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf, +def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf, is_short, limit_buy_order_canceled_empty) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -2559,13 +2740,14 @@ def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf, assert nofiy_mock.call_count == 1 +@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.parametrize('cancelorder', [ {}, {'remaining': None}, 'String Return value', 123 ]) -def test_handle_cancel_enter_corder_empty(mocker, default_conf, limit_buy_order, +def test_handle_cancel_enter_corder_empty(mocker, default_conf, limit_buy_order, is_short, cancelorder) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -2656,8 +2838,8 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf) -> None: assert freqtrade.handle_cancel_exit(trade, order, reason) == 'error cancelling order' -# TODO-lev: Add short tests -def test_execute_trade_exit_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> None: +@pytest.mark.parametrize("is_short", [False, True]) +def test_execute_trade_exit_up(default_conf, ticker, fee, ticker_sell_up, mocker, is_short) -> None: rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -2722,7 +2904,8 @@ def test_execute_trade_exit_up(default_conf, ticker, fee, ticker_sell_up, mocker } == last_msg -def test_execute_trade_exit_down(default_conf, ticker, fee, ticker_sell_down, mocker) -> None: +@pytest.mark.parametrize("is_short", [False, True]) +def test_execute_trade_exit_down(default_conf, ticker, fee, ticker_sell_down, mocker, is_short) -> None: rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -2774,7 +2957,8 @@ def test_execute_trade_exit_down(default_conf, ticker, fee, ticker_sell_down, mo } == last_msg -def test_execute_trade_exit_custom_exit_price(default_conf, ticker, fee, ticker_sell_up, +@pytest.mark.parametrize("is_short", [False, True]) +def test_execute_trade_exit_custom_exit_price(default_conf, ticker, fee, ticker_sell_up, is_short, mocker) -> None: rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) @@ -2839,7 +3023,8 @@ def test_execute_trade_exit_custom_exit_price(default_conf, ticker, fee, ticker_ } == last_msg -def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(default_conf, ticker, fee, +@pytest.mark.parametrize("is_short", [False, True]) +def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(default_conf, ticker, fee, is_short, ticker_sell_down, mocker) -> None: rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) @@ -2934,7 +3119,8 @@ def test_execute_trade_exit_sloe_cancel_exception( assert log_has('Could not cancel stoploss order abcd', caplog) -def test_execute_trade_exit_with_stoploss_on_exchange(default_conf, ticker, fee, ticker_sell_up, +@pytest.mark.parametrize("is_short", [False, True]) +def test_execute_trade_exit_with_stoploss_on_exchange(default_conf, ticker, fee, ticker_sell_up, is_short, mocker) -> None: default_conf['exchange']['name'] = 'binance' @@ -3061,7 +3247,8 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(default_conf, tic assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL -def test_execute_trade_exit_market_order(default_conf, ticker, fee, +@pytest.mark.parametrize("is_short", [False, True]) +def test_execute_trade_exit_market_order(default_conf, ticker, fee, is_short, ticker_sell_up, mocker) -> None: rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) @@ -3120,7 +3307,8 @@ def test_execute_trade_exit_market_order(default_conf, ticker, fee, } == last_msg -def test_execute_trade_exit_insufficient_funds_error(default_conf, ticker, fee, +@pytest.mark.parametrize("is_short", [False, True]) +def test_execute_trade_exit_insufficient_funds_error(default_conf, ticker, fee, is_short, ticker_sell_up, mocker) -> None: freqtrade = get_patched_freqtradebot(mocker, default_conf) mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds') @@ -3154,7 +3342,8 @@ 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, +@pytest.mark.parametrize("is_short", [False, True]) +def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy_order_open, is_short, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -3194,7 +3383,8 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy assert trade.sell_reason == SellType.SELL_SIGNAL.value -def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, limit_buy_order_open, +@pytest.mark.parametrize("is_short", [False, True]) +def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, limit_buy_order_open, is_short, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -3228,7 +3418,8 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, limit_bu assert trade.sell_reason == SellType.SELL_SIGNAL.value -def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_order_open, +@pytest.mark.parametrize("is_short", [False, True]) +def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_order_open, is_short, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -3261,7 +3452,8 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_o assert freqtrade.handle_trade(trade) is False -def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, limit_buy_order_open, +@pytest.mark.parametrize("is_short", [False, True]) +def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, limit_buy_order_open, is_short, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -3297,7 +3489,8 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, limit_buy_ assert trade.sell_reason == SellType.SELL_SIGNAL.value -def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_open, +@pytest.mark.parametrize("is_short", [False, True]) +def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_open, is_short, fee, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -3383,7 +3576,8 @@ def test__safe_exit_amount_error(default_conf, fee, caplog, mocker): assert freqtrade._safe_exit_amount(trade.pair, trade.amount) -def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplog) -> None: +@pytest.mark.parametrize("is_short", [False, True]) +def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplog, is_short) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -3419,7 +3613,8 @@ def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplo assert log_has_re(f"Pair {trade.pair} is still locked.*", caplog) -def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_buy_order_open, +@pytest.mark.parametrize("is_short", [False, True]) +def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_buy_order_open, is_short, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -3456,7 +3651,8 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_buy_order assert trade.sell_reason == SellType.ROI.value -def test_trailing_stop_loss(default_conf, limit_buy_order_open, limit_buy_order, +@pytest.mark.parametrize("is_short", [False, True]) +def test_trailing_stop_loss(default_conf, limit_buy_order_open, limit_buy_order, is_short, fee, caplog, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -3510,7 +3706,8 @@ def test_trailing_stop_loss(default_conf, limit_buy_order_open, limit_buy_order, assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value -def test_trailing_stop_loss_positive(default_conf, limit_buy_order, limit_buy_order_open, fee, +@pytest.mark.parametrize("is_short", [False, True]) +def test_trailing_stop_loss_positive(default_conf, limit_buy_order, limit_buy_order_open, fee, is_short, caplog, mocker) -> None: buy_price = limit_buy_order['price'] patch_RPCManager(mocker) @@ -3571,7 +3768,8 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, limit_buy_or f"initial stoploss was at 0.000010, trade opened at 0.000011", caplog) -def test_trailing_stop_loss_offset(default_conf, limit_buy_order, limit_buy_order_open, fee, +@pytest.mark.parametrize("is_short", [False, True]) +def test_trailing_stop_loss_offset(default_conf, limit_buy_order, limit_buy_order_open, fee, is_short, caplog, mocker) -> None: buy_price = limit_buy_order['price'] patch_RPCManager(mocker) @@ -3633,7 +3831,8 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, limit_buy_orde assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value -def test_tsl_only_offset_reached(default_conf, limit_buy_order, limit_buy_order_open, fee, +@pytest.mark.parametrize("is_short", [False, True]) +def test_tsl_only_offset_reached(default_conf, limit_buy_order, limit_buy_order_open, fee, is_short, caplog, mocker) -> None: buy_price = limit_buy_order['price'] # buy_price: 0.00001099 @@ -3697,7 +3896,8 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, limit_buy_order_ assert trade.stop_loss == 0.0000117705 -def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_buy_order_open, +@pytest.mark.parametrize("is_short", [False, True]) +def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_buy_order_open, is_short, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) @@ -4110,7 +4310,8 @@ def test_apply_fee_conditional(default_conf, fee, caplog, mocker, assert walletmock.call_count == 1 -def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order_open, limit_buy_order, +@pytest.mark.parametrize("is_short", [False, True]) +def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order_open, limit_buy_order, is_short, fee, mocker, order_book_l2): default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True default_conf['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = 0.1 @@ -4146,7 +4347,8 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order_open, assert whitelist == default_conf['exchange']['pair_whitelist'] -def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_order, +@pytest.mark.parametrize("is_short", [False, True]) +def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_order, is_short, fee, mocker, order_book_l2): default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True # delta is 100 which is impossible to reach. hence check_depth_of_market will return false @@ -4235,7 +4437,8 @@ def test_check_depth_of_market(default_conf, mocker, order_book_l2) -> None: assert freqtrade._check_depth_of_market('ETH/BTC', conf, side=SignalDirection.LONG) is False -def test_order_book_ask_strategy(default_conf, limit_buy_order_open, limit_buy_order, fee, +@pytest.mark.parametrize("is_short", [False, True]) +def test_order_book_ask_strategy(default_conf, limit_buy_order_open, limit_buy_order, fee, is_short, limit_sell_order_open, mocker, order_book_l2, caplog) -> None: """ test order book ask strategy @@ -4312,7 +4515,8 @@ def test_startup_trade_reinit(default_conf, edge_conf, mocker): @pytest.mark.usefixtures("init_persistence") -def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order_open, caplog): +@pytest.mark.parametrize("is_short", [False, True]) +def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order_open, caplog, is_short): default_conf['dry_run'] = True # Initialize to 2 times stake amount default_conf['dry_run_wallet'] = 0.002 @@ -4344,7 +4548,8 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order_ @pytest.mark.usefixtures("init_persistence") -def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limit_sell_order): +@pytest.mark.parametrize("is_short", [False, True]) +def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limit_sell_order, is_short): default_conf['cancel_open_orders_on_exit'] = True mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=[ @@ -4404,7 +4609,8 @@ def test_update_open_orders(mocker, default_conf, fee, caplog): @pytest.mark.usefixtures("init_persistence") -def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee): +@pytest.mark.parametrize("is_short", [False, True]) +def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee, is_short): freqtrade = get_patched_freqtradebot(mocker, default_conf) def patch_with_fee(order): @@ -4466,7 +4672,8 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee): @pytest.mark.usefixtures("init_persistence") -def test_reupdate_enter_order_fees(mocker, default_conf, fee, caplog): +@pytest.mark.parametrize("is_short", [False, True]) +def test_reupdate_enter_order_fees(mocker, default_conf, fee, caplog, is_short): freqtrade = get_patched_freqtradebot(mocker, default_conf) mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') @@ -4503,7 +4710,8 @@ def test_reupdate_enter_order_fees(mocker, default_conf, fee, caplog): @pytest.mark.usefixtures("init_persistence") -def test_handle_insufficient_funds(mocker, default_conf, fee): +@pytest.mark.parametrize("is_short", [False, True]) +def test_handle_insufficient_funds(mocker, default_conf, fee, is_short): freqtrade = get_patched_freqtradebot(mocker, default_conf) mock_rlo = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.refind_lost_order') mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_enter_order_fees') @@ -4541,7 +4749,8 @@ def test_handle_insufficient_funds(mocker, default_conf, fee): @pytest.mark.usefixtures("init_persistence") -def test_refind_lost_order(mocker, default_conf, fee, caplog): +@pytest.mark.parametrize("is_short", [False, True]) +def test_refind_lost_order(mocker, default_conf, fee, caplog, is_short): caplog.set_level(logging.DEBUG) freqtrade = get_patched_freqtradebot(mocker, default_conf) mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') @@ -4677,3 +4886,6 @@ def test_get_valid_price(mocker, default_conf) -> None: assert valid_price_at_min_alwd > custom_price_under_min_alwd assert valid_price_at_min_alwd < proposed_price + + +# TODO-lev def test_leverage_prep() From d60475705681ac9b53e2f8d1e0116d9c116e3e8b Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 14 Sep 2021 21:08:15 -0600 Subject: [PATCH 050/134] Added is_short to conf tests --- freqtrade/exchange/exchange.py | 1 - tests/conftest.py | 22 +++-- tests/conftest_trades.py | 131 +++++++++++++++------------- tests/test_freqtradebot.py | 152 +++++++++++++++++++-------------- 4 files changed, 179 insertions(+), 127 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 871a9dc73..0c95f50d0 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -727,7 +727,6 @@ class Exchange: if not self.exchange_has('fetchL2OrderBook'): return True ob = self.fetch_l2_order_book(pair, 1) - breakpoint() if side == 'buy': price = ob['asks'][0][0] logger.debug(f"{pair} checking dry buy-order: price={price}, limit={limit}") diff --git a/tests/conftest.py b/tests/conftest.py index 0c3998ab8..6cd0d5119 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -29,6 +29,14 @@ from tests.conftest_trades import (leverage_trade, mock_trade_1, mock_trade_2, m mock_trade_4, mock_trade_5, mock_trade_6, short_trade) +def enter_side(is_short: bool): + return "sell" if is_short else "buy" + + +def exit_side(is_short: bool): + return "buy" if is_short else "sell" + + logging.getLogger('').setLevel(logging.INFO) @@ -216,7 +224,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, enter_long=True, exit_long=False, freqtrade.exchange.refresh_latest_ohlcv = lambda p: None -def create_mock_trades(fee, use_db: bool = True): +def create_mock_trades(fee, is_short: bool, use_db: bool = True): """ Create some fake trades ... """ @@ -227,22 +235,22 @@ def create_mock_trades(fee, use_db: bool = True): LocalTrade.add_bt_trade(trade) # Simulate dry_run entries - trade = mock_trade_1(fee) + trade = mock_trade_1(fee, is_short) add_trade(trade) - trade = mock_trade_2(fee) + trade = mock_trade_2(fee, is_short) add_trade(trade) - trade = mock_trade_3(fee) + trade = mock_trade_3(fee, is_short) add_trade(trade) - trade = mock_trade_4(fee) + trade = mock_trade_4(fee, is_short) add_trade(trade) - trade = mock_trade_5(fee) + trade = mock_trade_5(fee, is_short) add_trade(trade) - trade = mock_trade_6(fee) + trade = mock_trade_6(fee, is_short) add_trade(trade) if use_db: diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 700cd3fa7..5ff5dc6de 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -6,12 +6,24 @@ from freqtrade.persistence.models import Order, Trade MOCK_TRADE_COUNT = 6 -def mock_order_1(): +def enter_side(is_short: bool): + return "sell" if is_short else "buy" + + +def exit_side(is_short: bool): + return "buy" if is_short else "sell" + + +def direc(is_short: bool): + return "short" if is_short else "long" + + +def mock_order_1(is_short: bool): return { - 'id': '1234', + 'id': f'1234_{direc(is_short)}', 'symbol': 'ETH/BTC', 'status': 'closed', - 'side': 'buy', + 'side': enter_side(is_short), 'type': 'limit', 'price': 0.123, 'amount': 123.0, @@ -20,7 +32,7 @@ def mock_order_1(): } -def mock_trade_1(fee): +def mock_trade_1(fee, is_short: bool): trade = Trade( pair='ETH/BTC', stake_amount=0.001, @@ -32,21 +44,22 @@ def mock_trade_1(fee): open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17), open_rate=0.123, exchange='binance', - open_order_id='dry_run_buy_12345', + open_order_id=f'dry_run_buy_{direc(is_short)}_12345', strategy='StrategyTestV2', timeframe=5, + is_short=is_short ) - o = Order.parse_from_ccxt_object(mock_order_1(), 'ETH/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_1(is_short), 'ETH/BTC', enter_side(is_short)) trade.orders.append(o) return trade -def mock_order_2(): +def mock_order_2(is_short: bool): return { - 'id': '1235', + 'id': f'1235_{direc(is_short)}', 'symbol': 'ETC/BTC', 'status': 'closed', - 'side': 'buy', + 'side': enter_side(is_short), 'type': 'limit', 'price': 0.123, 'amount': 123.0, @@ -55,12 +68,12 @@ def mock_order_2(): } -def mock_order_2_sell(): +def mock_order_2_sell(is_short: bool): return { - 'id': '12366', + 'id': f'12366_{direc(is_short)}', 'symbol': 'ETC/BTC', 'status': 'closed', - 'side': 'sell', + 'side': exit_side(is_short), 'type': 'limit', 'price': 0.128, 'amount': 123.0, @@ -69,7 +82,7 @@ def mock_order_2_sell(): } -def mock_trade_2(fee): +def mock_trade_2(fee, is_short: bool): """ Closed trade... """ @@ -82,30 +95,31 @@ def mock_trade_2(fee): fee_close=fee.return_value, open_rate=0.123, close_rate=0.128, - close_profit=0.005, - close_profit_abs=0.000584127, + close_profit=-0.005 if is_short else 0.005, + close_profit_abs=-0.005584127 if is_short else 0.000584127, exchange='binance', is_open=False, - open_order_id='dry_run_sell_12345', + open_order_id=f'dry_run_sell_{direc(is_short)}_12345', strategy='StrategyTestV2', timeframe=5, sell_reason='sell_signal', open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2), + is_short=is_short ) - o = Order.parse_from_ccxt_object(mock_order_2(), 'ETC/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_2(is_short), 'ETC/BTC', enter_side(is_short)) trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_2_sell(), 'ETC/BTC', 'sell') + o = Order.parse_from_ccxt_object(mock_order_2_sell(is_short), 'ETC/BTC', exit_side(is_short)) trade.orders.append(o) return trade -def mock_order_3(): +def mock_order_3(is_short: bool): return { - 'id': '41231a12a', + 'id': f'41231a12a_{direc(is_short)}', 'symbol': 'XRP/BTC', 'status': 'closed', - 'side': 'buy', + 'side': enter_side(is_short), 'type': 'limit', 'price': 0.05, 'amount': 123.0, @@ -114,12 +128,12 @@ def mock_order_3(): } -def mock_order_3_sell(): +def mock_order_3_sell(is_short: bool): return { - 'id': '41231a666a', + 'id': f'41231a666a_{direc(is_short)}', 'symbol': 'XRP/BTC', 'status': 'closed', - 'side': 'sell', + 'side': exit_side(is_short), 'type': 'stop_loss_limit', 'price': 0.06, 'average': 0.06, @@ -129,7 +143,7 @@ def mock_order_3_sell(): } -def mock_trade_3(fee): +def mock_trade_3(fee, is_short: bool): """ Closed trade """ @@ -142,8 +156,8 @@ def mock_trade_3(fee): fee_close=fee.return_value, open_rate=0.05, close_rate=0.06, - close_profit=0.01, - close_profit_abs=0.000155, + close_profit=-0.01 if is_short else 0.01, + close_profit_abs=-0.001155 if is_short else 0.000155, exchange='binance', is_open=False, strategy='StrategyTestV2', @@ -151,20 +165,21 @@ def mock_trade_3(fee): sell_reason='roi', open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20), close_date=datetime.now(tz=timezone.utc), + is_short=is_short ) - o = Order.parse_from_ccxt_object(mock_order_3(), 'XRP/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_3(is_short), 'XRP/BTC', enter_side(is_short)) trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_3_sell(), 'XRP/BTC', 'sell') + o = Order.parse_from_ccxt_object(mock_order_3_sell(is_short), 'XRP/BTC', exit_side(is_short)) trade.orders.append(o) return trade -def mock_order_4(): +def mock_order_4(is_short: bool): return { - 'id': 'prod_buy_12345', + 'id': f'prod_buy_{direc(is_short)}_12345', 'symbol': 'ETC/BTC', 'status': 'open', - 'side': 'buy', + 'side': enter_side(is_short), 'type': 'limit', 'price': 0.123, 'amount': 123.0, @@ -173,7 +188,7 @@ def mock_order_4(): } -def mock_trade_4(fee): +def mock_trade_4(fee, is_short: bool): """ Simulate prod entry """ @@ -188,21 +203,22 @@ def mock_trade_4(fee): is_open=True, open_rate=0.123, exchange='binance', - open_order_id='prod_buy_12345', + open_order_id=f'prod_buy_{direc(is_short)}_12345', strategy='StrategyTestV2', timeframe=5, + is_short=is_short ) - o = Order.parse_from_ccxt_object(mock_order_4(), 'ETC/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_4(is_short), 'ETC/BTC', enter_side(is_short)) trade.orders.append(o) return trade -def mock_order_5(): +def mock_order_5(is_short: bool): return { - 'id': 'prod_buy_3455', + 'id': f'prod_buy_{direc(is_short)}_3455', 'symbol': 'XRP/BTC', 'status': 'closed', - 'side': 'buy', + 'side': enter_side(is_short), 'type': 'limit', 'price': 0.123, 'amount': 123.0, @@ -211,12 +227,12 @@ def mock_order_5(): } -def mock_order_5_stoploss(): +def mock_order_5_stoploss(is_short: bool): return { - 'id': 'prod_stoploss_3455', + 'id': f'prod_stoploss_{direc(is_short)}_3455', 'symbol': 'XRP/BTC', 'status': 'open', - 'side': 'sell', + 'side': exit_side(is_short), 'type': 'stop_loss_limit', 'price': 0.123, 'amount': 123.0, @@ -225,7 +241,7 @@ def mock_order_5_stoploss(): } -def mock_trade_5(fee): +def mock_trade_5(fee, is_short: bool): """ Simulate prod entry with stoploss """ @@ -241,22 +257,23 @@ def mock_trade_5(fee): open_rate=0.123, exchange='binance', strategy='SampleStrategy', - stoploss_order_id='prod_stoploss_3455', + stoploss_order_id=f'prod_stoploss_{direc(is_short)}_3455', timeframe=5, + is_short=is_short ) - o = Order.parse_from_ccxt_object(mock_order_5(), 'XRP/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_5(is_short), 'XRP/BTC', enter_side(is_short)) trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_5_stoploss(), 'XRP/BTC', 'stoploss') + o = Order.parse_from_ccxt_object(mock_order_5_stoploss(is_short), 'XRP/BTC', 'stoploss') trade.orders.append(o) return trade -def mock_order_6(): +def mock_order_6(is_short: bool): return { - 'id': 'prod_buy_6', + 'id': f'prod_buy_{direc(is_short)}_6', 'symbol': 'LTC/BTC', 'status': 'closed', - 'side': 'buy', + 'side': enter_side(is_short), 'type': 'limit', 'price': 0.15, 'amount': 2.0, @@ -265,23 +282,23 @@ def mock_order_6(): } -def mock_order_6_sell(): +def mock_order_6_sell(is_short: bool): return { - 'id': 'prod_sell_6', + 'id': f'prod_sell_{direc(is_short)}_6', 'symbol': 'LTC/BTC', 'status': 'open', - 'side': 'sell', + 'side': exit_side(is_short), 'type': 'limit', - 'price': 0.20, + 'price': 0.15 if is_short else 0.20, 'amount': 2.0, 'filled': 0.0, 'remaining': 2.0, } -def mock_trade_6(fee): +def mock_trade_6(fee, is_short: bool): """ - Simulate prod entry with open sell order + Simulate prod entry with open exit order """ trade = Trade( pair='LTC/BTC', @@ -295,12 +312,12 @@ def mock_trade_6(fee): open_rate=0.15, exchange='binance', strategy='SampleStrategy', - open_order_id="prod_sell_6", + open_order_id=f"prod_sell_{direc(is_short)}_6", timeframe=5, ) - o = Order.parse_from_ccxt_object(mock_order_6(), 'LTC/BTC', 'buy') + o = Order.parse_from_ccxt_object(mock_order_6(is_short), 'LTC/BTC', enter_side(is_short)) trade.orders.append(o) - o = Order.parse_from_ccxt_object(mock_order_6_sell(), 'LTC/BTC', 'sell') + o = Order.parse_from_ccxt_object(mock_order_6_sell(is_short), 'LTC/BTC', exit_side(is_short)) trade.orders.append(o) return trade diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index d5c8566d4..cf7987ab0 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -28,6 +28,14 @@ from tests.conftest_trades import (MOCK_TRADE_COUNT, mock_order_1, mock_order_2, mock_order_5_stoploss, mock_order_6_sell) +def enter_side(is_short: bool): + return "sell" if is_short else "buy" + + +def exit_side(is_short: bool): + return "buy" if is_short else "sell" + + def patch_RPCManager(mocker) -> MagicMock: """ This function mock RPC manager to avoid repeating this code in almost every tests @@ -2394,8 +2402,8 @@ def test_check_handle_cancelled_buy(default_conf, ticker, limit_buy_order_old, o @pytest.mark.parametrize("is_short", [False, True]) -def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_order_old, open_trade, is_short, - fee, mocker) -> None: +def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_order_old, open_trade, + is_short, fee, mocker) -> None: rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock() patch_exchange(mocker) @@ -2421,8 +2429,8 @@ def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_ord @pytest.mark.parametrize("is_short", [False, True]) -def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_order_old, mocker, is_short, - open_trade) -> None: +def test_check_handle_timedout_sell_usercustom(default_conf, ticker, limit_sell_order_old, mocker, + is_short, open_trade) -> None: default_conf["unfilledtimeout"] = {"buy": 1440, "sell": 1440} rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock() @@ -2502,8 +2510,8 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, @pytest.mark.parametrize("is_short", [False, True]) -def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old, open_trade, is_short, - mocker, caplog) -> None: +def test_check_handle_cancelled_sell(default_conf, ticker, limit_sell_order_old, open_trade, + is_short, mocker, caplog) -> None: """ Handle sell order cancelled on exchange""" rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock() @@ -2602,9 +2610,10 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap @pytest.mark.parametrize("is_short", [False, True]) -def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade, caplog, fee, is_short, - limit_buy_order_old_partial, trades_for_order, - limit_buy_order_old_partial_canceled, mocker) -> None: +def test_check_handle_timedout_partial_except( + default_conf, ticker, open_trade, caplog, fee, is_short, limit_buy_order_old_partial, + trades_for_order, limit_buy_order_old_partial_canceled, mocker +) -> None: rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock(return_value=limit_buy_order_old_partial_canceled) patch_exchange(mocker) @@ -2642,7 +2651,8 @@ def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade, assert trades[0].fee_open == fee() -def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocker, caplog, is_short) -> None: +def test_check_handle_timedout_exception(default_conf, ticker, open_trade, + mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) cancel_order_mock = MagicMock() @@ -2664,7 +2674,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, open_trade, mocke freqtrade.check_handle_timedout() assert log_has_re(r"Cannot query order for Trade\(id=1, pair=ETH/BTC, amount=90.99181073, " - r"is_short=False, leverage=1.0, " + f"is_short=False, leverage=1.0, " r"open_rate=0.00001099, open_since=" f"{open_trade.open_date.strftime('%Y-%m-%d %H:%M:%S')}" r"\) due to Traceback \(most recent call last\):\n*", @@ -2905,7 +2915,8 @@ def test_execute_trade_exit_up(default_conf, ticker, fee, ticker_sell_up, mocker @pytest.mark.parametrize("is_short", [False, True]) -def test_execute_trade_exit_down(default_conf, ticker, fee, ticker_sell_down, mocker, is_short) -> None: +def test_execute_trade_exit_down(default_conf, ticker, fee, ticker_sell_down, + mocker, is_short) -> None: rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -3120,8 +3131,8 @@ def test_execute_trade_exit_sloe_cancel_exception( @pytest.mark.parametrize("is_short", [False, True]) -def test_execute_trade_exit_with_stoploss_on_exchange(default_conf, ticker, fee, ticker_sell_up, is_short, - mocker) -> None: +def test_execute_trade_exit_with_stoploss_on_exchange(default_conf, ticker, fee, ticker_sell_up, + is_short, mocker) -> None: default_conf['exchange']['name'] = 'binance' rpc_mock = patch_RPCManager(mocker) @@ -3343,8 +3354,8 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf, ticker, fee, @pytest.mark.parametrize("is_short", [False, True]) -def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy_order_open, is_short, - fee, mocker) -> None: +def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy_order_open, + is_short, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -3384,8 +3395,8 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy @pytest.mark.parametrize("is_short", [False, True]) -def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, limit_buy_order_open, is_short, - fee, mocker) -> None: +def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, limit_buy_order_open, + is_short, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -3419,8 +3430,8 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, limit_bu @pytest.mark.parametrize("is_short", [False, True]) -def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_order_open, is_short, - fee, mocker) -> None: +def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_order_open, + is_short, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -3453,8 +3464,8 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_o @pytest.mark.parametrize("is_short", [False, True]) -def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, limit_buy_order_open, is_short, - fee, mocker) -> None: +def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, limit_buy_order_open, + is_short, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -3577,7 +3588,8 @@ def test__safe_exit_amount_error(default_conf, fee, caplog, mocker): @pytest.mark.parametrize("is_short", [False, True]) -def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplog, is_short) -> None: +def test_locked_pairs(default_conf, ticker, fee, + ticker_sell_down, mocker, caplog, is_short) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -3707,8 +3719,8 @@ def test_trailing_stop_loss(default_conf, limit_buy_order_open, limit_buy_order, @pytest.mark.parametrize("is_short", [False, True]) -def test_trailing_stop_loss_positive(default_conf, limit_buy_order, limit_buy_order_open, fee, is_short, - caplog, mocker) -> None: +def test_trailing_stop_loss_positive(default_conf, limit_buy_order, limit_buy_order_open, fee, + is_short, caplog, mocker) -> None: buy_price = limit_buy_order['price'] patch_RPCManager(mocker) patch_exchange(mocker) @@ -3769,8 +3781,8 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, limit_buy_or @pytest.mark.parametrize("is_short", [False, True]) -def test_trailing_stop_loss_offset(default_conf, limit_buy_order, limit_buy_order_open, fee, is_short, - caplog, mocker) -> None: +def test_trailing_stop_loss_offset(default_conf, limit_buy_order, limit_buy_order_open, fee, + is_short, caplog, mocker) -> None: buy_price = limit_buy_order['price'] patch_RPCManager(mocker) patch_exchange(mocker) @@ -3897,8 +3909,8 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, limit_buy_order_ @pytest.mark.parametrize("is_short", [False, True]) -def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_buy_order_open, is_short, - fee, mocker) -> None: +def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_buy_order_open, + is_short, fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -4311,8 +4323,8 @@ def test_apply_fee_conditional(default_conf, fee, caplog, mocker, @pytest.mark.parametrize("is_short", [False, True]) -def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order_open, limit_buy_order, is_short, - fee, mocker, order_book_l2): +def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order_open, limit_buy_order, + is_short, fee, mocker, order_book_l2): default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True default_conf['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = 0.1 patch_RPCManager(mocker) @@ -4516,7 +4528,8 @@ def test_startup_trade_reinit(default_conf, edge_conf, mocker): @pytest.mark.usefixtures("init_persistence") @pytest.mark.parametrize("is_short", [False, True]) -def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order_open, caplog, is_short): +def test_sync_wallet_dry_run( + mocker, default_conf, ticker, fee, limit_buy_order_open, caplog, is_short): default_conf['dry_run'] = True # Initialize to 2 times stake amount default_conf['dry_run_wallet'] = 0.002 @@ -4549,7 +4562,8 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order_ @pytest.mark.usefixtures("init_persistence") @pytest.mark.parametrize("is_short", [False, True]) -def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limit_sell_order, is_short): +def test_cancel_all_open_orders( + mocker, default_conf, fee, limit_buy_order, limit_sell_order, is_short): default_conf['cancel_open_orders_on_exit'] = True mocker.patch('freqtrade.exchange.Exchange.fetch_order', side_effect=[ @@ -4558,7 +4572,7 @@ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limi sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit') freqtrade = get_patched_freqtradebot(mocker, default_conf) - create_mock_trades(fee) + create_mock_trades(fee, is_short=is_short) trades = Trade.query.all() assert len(trades) == MOCK_TRADE_COUNT freqtrade.cancel_all_open_orders() @@ -4567,13 +4581,14 @@ def test_cancel_all_open_orders(mocker, default_conf, fee, limit_buy_order, limi @pytest.mark.usefixtures("init_persistence") -def test_check_for_open_trades(mocker, default_conf, fee): +@pytest.mark.parametrize("is_short", [False, True]) +def test_check_for_open_trades(mocker, default_conf, fee, is_short): freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade.check_for_open_trades() assert freqtrade.rpc.send_msg.call_count == 0 - create_mock_trades(fee) + create_mock_trades(fee, is_short) trade = Trade.query.first() trade.is_open = True @@ -4582,10 +4597,11 @@ def test_check_for_open_trades(mocker, default_conf, fee): assert 'Handle these trades manually' in freqtrade.rpc.send_msg.call_args[0][0]['status'] +@pytest.mark.parametrize("is_short", [False, True]) @pytest.mark.usefixtures("init_persistence") -def test_update_open_orders(mocker, default_conf, fee, caplog): +def test_update_open_orders(mocker, default_conf, fee, caplog, is_short): freqtrade = get_patched_freqtradebot(mocker, default_conf) - create_mock_trades(fee) + create_mock_trades(fee, is_short=is_short) freqtrade.update_open_orders() assert not log_has_re(r"Error updating Order .*", caplog) @@ -4598,7 +4614,7 @@ def test_update_open_orders(mocker, default_conf, fee, caplog): caplog.clear() assert len(Order.get_open_orders()) == 3 - matching_buy_order = mock_order_4() + matching_buy_order = mock_order_4(is_short=is_short) matching_buy_order.update({ 'status': 'closed', }) @@ -4620,19 +4636,20 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee, i mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', side_effect=[ - patch_with_fee(mock_order_2_sell()), - patch_with_fee(mock_order_3_sell()), - patch_with_fee(mock_order_1()), - patch_with_fee(mock_order_2()), - patch_with_fee(mock_order_3()), - patch_with_fee(mock_order_4()), + patch_with_fee(mock_order_2_sell(is_short=is_short)), + patch_with_fee(mock_order_3_sell(is_short=is_short)), + patch_with_fee(mock_order_1(is_short=is_short)), + patch_with_fee(mock_order_2(is_short=is_short)), + patch_with_fee(mock_order_3(is_short=is_short)), + patch_with_fee(mock_order_4(is_short=is_short)), ] ) - create_mock_trades(fee) + create_mock_trades(fee, is_short=is_short) trades = Trade.get_trades().all() assert len(trades) == MOCK_TRADE_COUNT for trade in trades: + trade.is_short = is_short assert trade.fee_open_cost is None assert trade.fee_open_currency is None assert trade.fee_close_cost is None @@ -4659,7 +4676,7 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee, i for trade in trades: if trade.is_open: # Exclude Trade 4 - as the order is still open. - if trade.select_order('buy', False): + if trade.select_order(enter_side(is_short), False): assert trade.fee_open_cost is not None assert trade.fee_open_currency is not None else: @@ -4677,15 +4694,23 @@ def test_reupdate_enter_order_fees(mocker, default_conf, fee, caplog, is_short): freqtrade = get_patched_freqtradebot(mocker, default_conf) mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') - create_mock_trades(fee) + create_mock_trades(fee, is_short=is_short) trades = Trade.get_trades().all() freqtrade.reupdate_enter_order_fees(trades[0]) - assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) + assert log_has_re( + f"Trying to reupdate {enter_side(is_short)} " + r"fees for .*", + caplog + ) assert mock_uts.call_count == 1 assert mock_uts.call_args_list[0][0][0] == trades[0] - assert mock_uts.call_args_list[0][0][1] == mock_order_1()['id'] - assert log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog) + assert mock_uts.call_args_list[0][0][1] == mock_order_1(is_short=is_short)['id'] + assert log_has_re( + f"Updating {enter_side(is_short)}-fee on trade " + r".* for order .*\.", + caplog + ) mock_uts.reset_mock() caplog.clear() @@ -4700,22 +4725,24 @@ def test_reupdate_enter_order_fees(mocker, default_conf, fee, caplog, is_short): amount=20, open_rate=0.01, exchange='binance', + is_short=is_short ) Trade.query.session.add(trade) freqtrade.reupdate_enter_order_fees(trade) - assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) + assert log_has_re(f"Trying to reupdate {enter_side(is_short)} fees for " + r".*", caplog) assert mock_uts.call_count == 0 - assert not log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog) + assert not log_has_re(f"Updating {enter_side(is_short)}-fee on trade " + r".* for order .*\.", caplog) @pytest.mark.usefixtures("init_persistence") -@pytest.mark.parametrize("is_short", [False, True]) -def test_handle_insufficient_funds(mocker, default_conf, fee, is_short): +def test_handle_insufficient_funds(mocker, default_conf, fee): freqtrade = get_patched_freqtradebot(mocker, default_conf) mock_rlo = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.refind_lost_order') mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_enter_order_fees') - create_mock_trades(fee) + create_mock_trades(fee, is_short=False) trades = Trade.get_trades().all() # Trade 0 has only a open buy order, no closed order @@ -4761,8 +4788,9 @@ def test_refind_lost_order(mocker, default_conf, fee, caplog, is_short): def reset_open_orders(trade): trade.open_order_id = None trade.stoploss_order_id = None + trade.is_short = is_short - create_mock_trades(fee) + create_mock_trades(fee, is_short=is_short) trades = Trade.get_trades().all() caplog.clear() @@ -4774,7 +4802,7 @@ def test_refind_lost_order(mocker, default_conf, fee, caplog, is_short): assert trade.stoploss_order_id is None freqtrade.refind_lost_order(trade) - order = mock_order_1() + order = mock_order_1(is_short=is_short) assert log_has_re(r"Order Order(.*order_id=" + order['id'] + ".*) is no longer open.", caplog) assert mock_fo.call_count == 0 assert mock_uts.call_count == 0 @@ -4792,7 +4820,7 @@ def test_refind_lost_order(mocker, default_conf, fee, caplog, is_short): assert trade.stoploss_order_id is None freqtrade.refind_lost_order(trade) - order = mock_order_4() + order = mock_order_4(is_short=is_short) assert log_has_re(r"Trying to refind Order\(.*", caplog) assert mock_fo.call_count == 0 assert mock_uts.call_count == 0 @@ -4810,7 +4838,7 @@ def test_refind_lost_order(mocker, default_conf, fee, caplog, is_short): assert trade.stoploss_order_id is None freqtrade.refind_lost_order(trade) - order = mock_order_5_stoploss() + order = mock_order_5_stoploss(is_short=is_short) assert log_has_re(r"Trying to refind Order\(.*", caplog) assert mock_fo.call_count == 1 assert mock_uts.call_count == 1 @@ -4829,7 +4857,7 @@ def test_refind_lost_order(mocker, default_conf, fee, caplog, is_short): assert trade.stoploss_order_id is None freqtrade.refind_lost_order(trade) - order = mock_order_6_sell() + order = mock_order_6_sell(is_short=is_short) assert log_has_re(r"Trying to refind Order\(.*", caplog) assert mock_fo.call_count == 1 assert mock_uts.call_count == 1 @@ -4842,7 +4870,7 @@ def test_refind_lost_order(mocker, default_conf, fee, caplog, is_short): # Test error case mock_fo = mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', side_effect=ExchangeError()) - order = mock_order_5_stoploss() + order = mock_order_5_stoploss(is_short=is_short) freqtrade.refind_lost_order(trades[4]) assert log_has(f"Error updating {order['id']}.", caplog) From 4c91126c4978962cdf71bc26cc3cd447fb7ae99b Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 16 Sep 2021 03:23:45 -0600 Subject: [PATCH 051/134] some short freqtradebot parametrized tests --- freqtrade/freqtradebot.py | 6 ++++-- tests/test_freqtradebot.py | 11 +++-------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ffd6f7546..9f58e3c36 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -812,8 +812,10 @@ class FreqtradeBot(LoggingMixin): exit_signal_type = "exit_short" if trade.is_short else "exit_long" # TODO-lev: change to use_exit_signal, ignore_roi_if_enter_signal - if (self.config.get('use_sell_signal', True) or - self.config.get('ignore_roi_if_buy_signal', False)): + if ( + self.config.get('use_sell_signal', True) or + self.config.get('ignore_roi_if_buy_signal', False) + ): analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, self.strategy.timeframe) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index cf7987ab0..2028ec6f8 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4322,7 +4322,6 @@ def test_apply_fee_conditional(default_conf, fee, caplog, mocker, assert walletmock.call_count == 1 -@pytest.mark.parametrize("is_short", [False, True]) def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order_open, limit_buy_order, is_short, fee, mocker, order_book_l2): default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True @@ -4359,8 +4358,7 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order_open, assert whitelist == default_conf['exchange']['pair_whitelist'] -@pytest.mark.parametrize("is_short", [False, True]) -def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_order, is_short, +def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_order, fee, mocker, order_book_l2): default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True # delta is 100 which is impossible to reach. hence check_depth_of_market will return false @@ -4449,8 +4447,7 @@ def test_check_depth_of_market(default_conf, mocker, order_book_l2) -> None: assert freqtrade._check_depth_of_market('ETH/BTC', conf, side=SignalDirection.LONG) is False -@pytest.mark.parametrize("is_short", [False, True]) -def test_order_book_ask_strategy(default_conf, limit_buy_order_open, limit_buy_order, fee, is_short, +def test_order_book_ask_strategy(default_conf, limit_buy_order_open, limit_buy_order, fee, limit_sell_order_open, mocker, order_book_l2, caplog) -> None: """ test order book ask strategy @@ -4527,9 +4524,7 @@ def test_startup_trade_reinit(default_conf, edge_conf, mocker): @pytest.mark.usefixtures("init_persistence") -@pytest.mark.parametrize("is_short", [False, True]) -def test_sync_wallet_dry_run( - mocker, default_conf, ticker, fee, limit_buy_order_open, caplog, is_short): +def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order_open, caplog): default_conf['dry_run'] = True # Initialize to 2 times stake amount default_conf['dry_run_wallet'] = 0.002 From 2e8d00e87732344dcfadad4bdeeca024e4b4093a Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 17 Sep 2021 01:15:21 -0600 Subject: [PATCH 052/134] temp commit message --- tests/test_freqtradebot.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 2028ec6f8..9662118ec 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3844,21 +3844,24 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, limit_buy_orde @pytest.mark.parametrize("is_short", [False, True]) -def test_tsl_only_offset_reached(default_conf, limit_buy_order, limit_buy_order_open, fee, is_short, - caplog, mocker) -> None: - buy_price = limit_buy_order['price'] - # buy_price: 0.00001099 +def test_tsl_only_offset_reached(default_conf, limit_buy_order_usdt, limit_buy_order_usdt_open, + fee, is_short, limit_sell_order_usdt, + limit_sell_order_usdt_open, caplog, mocker) -> None: + limit_order = limit_sell_usdt_order if is_short else limit_buy_order_usdt + limit_order_open = limit_sell_order_usdt_open if is_short else limit_buy_order_open_usdt + enter_price = limit_order['price'] + # enter_price: 2.0 patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=MagicMock(return_value={ - 'bid': buy_price, - 'ask': buy_price, - 'last': buy_price + 'bid': enter_price, + 'ask': enter_price, + 'last': enter_price }), - create_order=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(return_value=limit_order_open), get_fee=fee, ) patch_whitelist(mocker, default_conf) @@ -3873,11 +3876,11 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, limit_buy_order_ freqtrade.enter_positions() trade = Trade.query.first() - trade.update(limit_buy_order) + trade.update(limit_order) caplog.set_level(logging.DEBUG) # stop-loss not reached assert freqtrade.handle_trade(trade) is False - assert trade.stop_loss == 0.0000098910 + assert trade.stop_loss == 2.20 if is_short else 1.80 # Raise ticker above buy price mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', @@ -3891,7 +3894,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, limit_buy_order_ assert freqtrade.handle_trade(trade) is False assert not log_has("ETH/BTC - Adjusting stoploss...", caplog) - assert trade.stop_loss == 0.0000098910 + assert trade.stop_loss == 2.20 if is_short else 1.80 caplog.clear() # price rises above the offset (rises 12% when the offset is 5.5%) @@ -3908,9 +3911,9 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, limit_buy_order_ assert trade.stop_loss == 0.0000117705 -@pytest.mark.parametrize("is_short", [False, True]) +# TODO-lev: @pytest.mark.parametrize("is_short", [False, True]) def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_buy_order_open, - is_short, fee, mocker) -> None: + fee, mocker) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( From 043bfcd5adf6d51975b8b78ffd9c9c259b5b8cc0 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 19 Sep 2021 20:24:22 -0600 Subject: [PATCH 053/134] Fixed a lot of failing tests" --- tests/commands/test_commands.py | 5 +- tests/conftest.py | 12 +-- tests/data/test_btanalysis.py | 5 +- tests/exchange/test_exchange.py | 140 +++++++++++++++++--------------- tests/plugins/test_pairlist.py | 3 +- tests/rpc/test_rpc.py | 10 ++- tests/rpc/test_rpc_apiserver.py | 32 +++++--- tests/rpc/test_rpc_telegram.py | 18 ++-- tests/test_freqtradebot.py | 21 ++--- tests/test_persistence.py | 25 ++++-- 10 files changed, 155 insertions(+), 116 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 135510b38..0737532e7 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -871,7 +871,7 @@ def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, tmpdir): mocker.patch( 'freqtrade.optimize.hyperopt_tools.HyperoptTools._test_hyperopt_results_exist', return_value=True - ) + ) def fake_iterator(*args, **kwargs): yield from [saved_hyperopt_results] @@ -1277,9 +1277,10 @@ def test_start_list_data(testdatadir, capsys): @pytest.mark.usefixtures("init_persistence") +# TODO-lev: Short trades? def test_show_trades(mocker, fee, capsys, caplog): mocker.patch("freqtrade.persistence.init_db") - create_mock_trades(fee) + create_mock_trades(fee, False) args = [ "show-trades", "--db-url", diff --git a/tests/conftest.py b/tests/conftest.py index 4a0ad4c97..c72c572f3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -285,22 +285,22 @@ def create_mock_trades_with_leverage(fee, use_db: bool = True): else: LocalTrade.add_bt_trade(trade) # Simulate dry_run entries - trade = mock_trade_1(fee) + trade = mock_trade_1(fee, False) add_trade(trade) - trade = mock_trade_2(fee) + trade = mock_trade_2(fee, False) add_trade(trade) - trade = mock_trade_3(fee) + trade = mock_trade_3(fee, False) add_trade(trade) - trade = mock_trade_4(fee) + trade = mock_trade_4(fee, False) add_trade(trade) - trade = mock_trade_5(fee) + trade = mock_trade_5(fee, False) add_trade(trade) - trade = mock_trade_6(fee) + trade = mock_trade_6(fee, False) add_trade(trade) trade = short_trade(fee) diff --git a/tests/data/test_btanalysis.py b/tests/data/test_btanalysis.py index 1dcd04a80..6d012f952 100644 --- a/tests/data/test_btanalysis.py +++ b/tests/data/test_btanalysis.py @@ -111,9 +111,10 @@ def test_load_backtest_data_multi(testdatadir): @pytest.mark.usefixtures("init_persistence") -def test_load_trades_from_db(default_conf, fee, mocker): +@pytest.mark.parametrize('is_short', [False, True]) +def test_load_trades_from_db(default_conf, fee, is_short, mocker): - create_mock_trades(fee) + create_mock_trades(fee, is_short) # remove init so it does not init again init_mock = mocker.patch('freqtrade.data.btanalysis.init_db', MagicMock()) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index cc9b5130d..950fdb6ff 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -135,7 +135,7 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog): assert ex._ccxt_config == {} Exchange._headers = {} - # TODO-lev: Test with options + # TODO-lev: Test with options in ccxt_config def test_destroy(default_conf, mocker, caplog): @@ -420,21 +420,25 @@ 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) + + # min amount and cost are set (cost is minimal) + markets["ETH/BTC"]["limits"] = { + 'cost': {'min': 2}, 'amount': {'min': 2} } mocker.patch( 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) ) - result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - expected_result=max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss)) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) + expected_result = max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss)) assert isclose(result, expected_result) # With Leverage - result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10) assert isclose(result, expected_result/10) # min amount and cost are set (amount is minial) - markets["ETH/BTC"]["limits"]={ + markets["ETH/BTC"]["limits"] = { 'cost': {'min': 8}, 'amount': {'min': 2} } @@ -442,28 +446,36 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) ) - result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - expected_result=max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss)) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) + expected_result = max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss)) assert isclose(result, expected_result) # With Leverage - result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 7.0) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 7.0) assert isclose(result, expected_result/7.0) - result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4) - expected_result=max(8, 2 * 2) * 1.5 + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4) + expected_result = max(8, 2 * 2) * 1.5 assert isclose(result, expected_result) # With Leverage - result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4, 8.0) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4, 8.0) assert isclose(result, expected_result/8.0) + + # Really big stoploss + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) + expected_result = max(8, 2 * 2) * 1.5 assert isclose(result, expected_result) # With Leverage - result=exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0) assert isclose(result, expected_result/12) - stoploss=-0.05 - markets={'ETH/BTC': {'symbol': 'ETH/BTC'}} + + +def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: + exchange = get_patched_exchange(mocker, default_conf, id="binance") + stoploss = -0.05 + markets = {'ETH/BTC': {'symbol': 'ETH/BTC'}} # Real Binance data - markets["ETH/BTC"]["limits"]={ + markets["ETH/BTC"]["limits"] = { 'cost': {'min': 0.0001}, 'amount': {'min': 0.001} } @@ -471,10 +483,10 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: 'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets) ) - result=exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) - expected_result=max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) + expected_result = max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)) assert round(result, 8) == round(expected_result, 8) - result=exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss, 3.0) + result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss, 3.0) assert round(result, 8) == round(expected_result/3, 8) @@ -482,16 +494,16 @@ def test_set_sandbox(default_conf, mocker): """ Test working scenario """ - api_mock=MagicMock() - api_mock.load_markets=MagicMock(return_value = { + api_mock = MagicMock() + api_mock.load_markets = MagicMock(return_value={ 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' }) - url_mock=PropertyMock(return_value = {'test': "api-public.sandbox.gdax.com", + url_mock = PropertyMock(return_value={'test': "api-public.sandbox.gdax.com", 'api': 'https://api.gdax.com'}) - type(api_mock).urls=url_mock - exchange=get_patched_exchange(mocker, default_conf, api_mock) - liveurl=exchange._api.urls['api'] - default_conf['exchange']['sandbox']=True + type(api_mock).urls = url_mock + exchange = get_patched_exchange(mocker, default_conf, api_mock) + liveurl = exchange._api.urls['api'] + default_conf['exchange']['sandbox'] = True exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname') assert exchange._api.urls['api'] != liveurl @@ -500,16 +512,16 @@ def test_set_sandbox_exception(default_conf, mocker): """ Test Fail scenario """ - api_mock=MagicMock() - api_mock.load_markets=MagicMock(return_value = { + api_mock = MagicMock() + api_mock.load_markets = MagicMock(return_value={ 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' }) - url_mock=PropertyMock(return_value = {'api': 'https://api.gdax.com'}) - type(api_mock).urls=url_mock + url_mock = PropertyMock(return_value={'api': 'https://api.gdax.com'}) + type(api_mock).urls = url_mock - with pytest.raises(OperationalException, match = r'does not provide a sandbox api'): - exchange=get_patched_exchange(mocker, default_conf, api_mock) - default_conf['exchange']['sandbox']=True + with pytest.raises(OperationalException, match=r'does not provide a sandbox api'): + exchange = get_patched_exchange(mocker, default_conf, api_mock) + default_conf['exchange']['sandbox'] = True exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname') @@ -519,13 +531,13 @@ def test__load_async_markets(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange._load_markets') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - exchange=Exchange(default_conf) - exchange._api_async.load_markets=get_mock_coro(None) + exchange = Exchange(default_conf) + exchange._api_async.load_markets = get_mock_coro(None) exchange._load_async_markets() assert exchange._api_async.load_markets.call_count == 1 caplog.set_level(logging.DEBUG) - exchange._api_async.load_markets=Mock(side_effect = ccxt.BaseError("deadbeef")) + exchange._api_async.load_markets = Mock(side_effect=ccxt.BaseError("deadbeef")) exchange._load_async_markets() assert log_has('Could not load async markets. Reason: deadbeef', caplog) @@ -533,8 +545,8 @@ def test__load_async_markets(default_conf, mocker, caplog): def test__load_markets(default_conf, mocker, caplog): caplog.set_level(logging.INFO) - api_mock=MagicMock() - api_mock.load_markets=MagicMock(side_effect = ccxt.BaseError("SomeError")) + api_mock = MagicMock() + api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError("SomeError")) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) mocker.patch('freqtrade.exchange.Exchange.validate_pairs') mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') @@ -543,28 +555,28 @@ def test__load_markets(default_conf, mocker, caplog): Exchange(default_conf) assert log_has('Unable to initialize markets.', caplog) - expected_return={'ETH/BTC': 'available'} - api_mock=MagicMock() - api_mock.load_markets=MagicMock(return_value = expected_return) + expected_return = {'ETH/BTC': 'available'} + api_mock = MagicMock() + api_mock.load_markets = MagicMock(return_value=expected_return) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - default_conf['exchange']['pair_whitelist']=['ETH/BTC'] - ex=Exchange(default_conf) + default_conf['exchange']['pair_whitelist'] = ['ETH/BTC'] + ex = Exchange(default_conf) assert ex.markets == expected_return def test_reload_markets(default_conf, mocker, caplog): caplog.set_level(logging.DEBUG) - initial_markets={'ETH/BTC': {}} - updated_markets={'ETH/BTC': {}, "LTC/BTC": {}} + initial_markets = {'ETH/BTC': {}} + updated_markets = {'ETH/BTC': {}, "LTC/BTC": {}} - api_mock=MagicMock() - api_mock.load_markets=MagicMock(return_value = initial_markets) - default_conf['exchange']['markets_refresh_interval']=10 - exchange=get_patched_exchange(mocker, default_conf, api_mock, id = "binance", - mock_markets = False) - exchange._load_async_markets=MagicMock() - exchange._last_markets_refresh=arrow.utcnow().int_timestamp + api_mock = MagicMock() + api_mock.load_markets = MagicMock(return_value=initial_markets) + default_conf['exchange']['markets_refresh_interval'] = 10 + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance", + mock_markets=False) + exchange._load_async_markets = MagicMock() + exchange._last_markets_refresh = arrow.utcnow().int_timestamp assert exchange.markets == initial_markets @@ -573,9 +585,9 @@ def test_reload_markets(default_conf, mocker, caplog): assert exchange.markets == initial_markets assert exchange._load_async_markets.call_count == 0 - api_mock.load_markets=MagicMock(return_value = updated_markets) + api_mock.load_markets = MagicMock(return_value=updated_markets) # more than 10 minutes have passed, reload is executed - exchange._last_markets_refresh=arrow.utcnow().int_timestamp - 15 * 60 + exchange._last_markets_refresh = arrow.utcnow().int_timestamp - 15 * 60 exchange.reload_markets() assert exchange.markets == updated_markets assert exchange._load_async_markets.call_count == 1 @@ -585,10 +597,10 @@ def test_reload_markets(default_conf, mocker, caplog): def test_reload_markets_exception(default_conf, mocker, caplog): caplog.set_level(logging.DEBUG) - api_mock=MagicMock() - api_mock.load_markets=MagicMock(side_effect = ccxt.NetworkError("LoadError")) - default_conf['exchange']['markets_refresh_interval']=10 - exchange=get_patched_exchange(mocker, default_conf, api_mock, id = "binance") + api_mock = MagicMock() + api_mock.load_markets = MagicMock(side_effect=ccxt.NetworkError("LoadError")) + default_conf['exchange']['markets_refresh_interval'] = 10 + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") # less than 10 minutes have passed, no reload exchange.reload_markets() @@ -596,11 +608,11 @@ def test_reload_markets_exception(default_conf, mocker, caplog): assert log_has_re(r"Could not reload markets.*", caplog) -@ pytest.mark.parametrize("stake_currency", ['ETH', 'BTC', 'USDT']) +@pytest.mark.parametrize("stake_currency", ['ETH', 'BTC', 'USDT']) def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog): - default_conf['stake_currency']=stake_currency - api_mock=MagicMock() - type(api_mock).load_markets=MagicMock(return_value = { + default_conf['stake_currency'] = stake_currency + api_mock = MagicMock() + type(api_mock).load_markets = MagicMock(return_value={ 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'}, 'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'}, }) @@ -612,9 +624,9 @@ def test_validate_stakecurrency(default_conf, stake_currency, mocker, caplog): def test_validate_stakecurrency_error(default_conf, mocker, caplog): - default_conf['stake_currency']='XRP' - api_mock=MagicMock() - type(api_mock).load_markets=MagicMock(return_value = { + default_conf['stake_currency'] = 'XRP' + api_mock = MagicMock() + type(api_mock).load_markets = MagicMock(return_value={ 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'}, 'XRP/ETH': {'quote': 'ETH'}, 'NEO/USDT': {'quote': 'USDT'}, }) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 1ce8d172c..8541d7008 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -665,6 +665,7 @@ def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None: @pytest.mark.usefixtures("init_persistence") +# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee) -> None: whitelist_conf['exchange']['pair_whitelist'].append('XRP/BTC') whitelist_conf['pairlists'] = [ @@ -679,7 +680,7 @@ def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee) -> None: 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) + create_mock_trades(fee, False) pm.refresh_pairlist() assert pm.whitelist == ['XRP/BTC', 'ETH/BTC', 'TKN/BTC'] diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 56e64db69..bb9b29f5f 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -285,7 +285,8 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, rpc._rpc_daily_profit(0, stake_currency, fiat_display_currency) -def test_rpc_trade_history(mocker, default_conf, markets, fee): +@pytest.mark.parametrize('is_short', [True, False]) +def test_rpc_trade_history(mocker, default_conf, markets, fee, is_short): mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -293,7 +294,7 @@ def test_rpc_trade_history(mocker, default_conf, markets, fee): ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) - create_mock_trades(fee) + create_mock_trades(fee, is_short) rpc = RPC(freqtradebot) rpc._fiat_converter = CryptoToFiatConverter() trades = rpc._rpc_trade_history(2) @@ -310,7 +311,8 @@ def test_rpc_trade_history(mocker, default_conf, markets, fee): assert trades['trades'][0]['pair'] == 'XRP/BTC' -def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog): +@pytest.mark.parametrize('is_short', [True, False]) +def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short): mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) stoploss_mock = MagicMock() cancel_mock = MagicMock() @@ -323,7 +325,7 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog): freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot.strategy.order_types['stoploss_on_exchange'] = True - create_mock_trades(fee) + create_mock_trades(fee, is_short) rpc = RPC(freqtradebot) with pytest.raises(RPCException, match='invalid argument'): rpc._rpc_delete('200') diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 7c98b2df7..eaad7128e 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -451,7 +451,8 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers): assert 'starting_capital_ratio' in response -def test_api_count(botclient, mocker, ticker, fee, markets): +@pytest.mark.parametrize('is_short', [True, False]) +def test_api_count(botclient, mocker, ticker, fee, markets, is_short): ftbot, client = botclient patch_get_signal(ftbot) mocker.patch.multiple( @@ -468,7 +469,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets): assert rc.json()["max"] == 1 # Create some test data - create_mock_trades(fee) + create_mock_trades(fee, is_short) rc = client_get(client, f"{BASE_URI}/count") assert_response(rc) assert rc.json()["current"] == 4 @@ -549,7 +550,8 @@ def test_api_daily(botclient, mocker, ticker, fee, markets): assert rc.json()['data'][0]['date'] == str(datetime.utcnow().date()) -def test_api_trades(botclient, mocker, fee, markets): +@pytest.mark.parametrize('is_short', [True, False]) +def test_api_trades(botclient, mocker, fee, markets, is_short): ftbot, client = botclient patch_get_signal(ftbot) mocker.patch.multiple( @@ -562,7 +564,7 @@ def test_api_trades(botclient, mocker, fee, markets): assert rc.json()['trades_count'] == 0 assert rc.json()['total_trades'] == 0 - create_mock_trades(fee) + create_mock_trades(fee, is_short) Trade.query.session.flush() rc = client_get(client, f"{BASE_URI}/trades") @@ -577,6 +579,7 @@ def test_api_trades(botclient, mocker, fee, markets): assert rc.json()['total_trades'] == 2 +# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) def test_api_trade_single(botclient, mocker, fee, ticker, markets): ftbot, client = botclient patch_get_signal(ftbot) @@ -589,7 +592,7 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets): assert_response(rc, 404) assert rc.json()['detail'] == 'Trade not found.' - create_mock_trades(fee) + create_mock_trades(fee, False) Trade.query.session.flush() rc = client_get(client, f"{BASE_URI}/trade/3") @@ -597,6 +600,7 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets): assert rc.json()['trade_id'] == 3 +# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) def test_api_delete_trade(botclient, mocker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot) @@ -612,7 +616,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets): # Error - trade won't exist yet. assert_response(rc, 502) - create_mock_trades(fee) + create_mock_trades(fee, False) Trade.query.session.flush() ftbot.strategy.order_types['stoploss_on_exchange'] = True trades = Trade.query.all() @@ -687,6 +691,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): @pytest.mark.usefixtures("init_persistence") +# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) def test_api_profit(botclient, mocker, ticker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot) @@ -702,7 +707,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets): assert_response(rc, 200) assert rc.json()['trade_count'] == 0 - create_mock_trades(fee) + create_mock_trades(fee, False) # Simulate fulfilled LIMIT_BUY order for trade rc = client_get(client, f"{BASE_URI}/profit") @@ -738,7 +743,8 @@ def test_api_profit(botclient, mocker, ticker, fee, markets): @pytest.mark.usefixtures("init_persistence") -def test_api_stats(botclient, mocker, ticker, fee, markets,): +# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) +def test_api_stats(botclient, mocker, ticker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot) mocker.patch.multiple( @@ -754,7 +760,7 @@ def test_api_stats(botclient, mocker, ticker, fee, markets,): assert 'durations' in rc.json() assert 'sell_reasons' in rc.json() - create_mock_trades(fee) + create_mock_trades(fee, False) rc = client_get(client, f"{BASE_URI}/stats") assert_response(rc, 200) @@ -812,6 +818,10 @@ def test_api_performance(botclient, fee): {'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57, 'profit_abs': -0.1150375}] +# TODO-lev: @pytest.mark.parametrize('is_short,side', [ +# (True, "short"), +# (False, "long") +# ]) def test_api_status(botclient, mocker, ticker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot) @@ -827,7 +837,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): rc = client_get(client, f"{BASE_URI}/status") assert_response(rc, 200) assert rc.json() == [] - create_mock_trades(fee) + create_mock_trades(fee, False) rc = client_get(client, f"{BASE_URI}/status") assert_response(rc) @@ -880,7 +890,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'is_open': True, 'max_rate': ANY, 'min_rate': ANY, - 'open_order_id': 'dry_run_buy_12345', + 'open_order_id': 'dry_run_buy_long_12345', 'open_rate_requested': ANY, 'open_trade_value': 15.1668225, 'sell_reason': None, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 21f1cd000..f52fc8d6c 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -33,6 +33,7 @@ class DummyCls(Telegram): """ Dummy class for testing the Telegram @authorized_only decorator """ + def __init__(self, rpc: RPC, config) -> None: super().__init__(rpc, config) self.state = {'called': False} @@ -479,8 +480,9 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0] +@pytest.mark.parametrize('is_short', [True, False]) def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee, - limit_buy_order, limit_sell_order, mocker) -> None: + limit_buy_order, limit_sell_order, mocker, is_short) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -496,7 +498,7 @@ def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee, msg_mock.reset_mock() # Create some test data - create_mock_trades(fee) + create_mock_trades(fee, is_short) telegram._stats(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -997,9 +999,9 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: msg = ('
  current    max    total stake\n---------  -----  -------------\n'
            '        1      {}          {}
').format( - default_conf['max_open_trades'], - default_conf['stake_amount'] - ) + default_conf['max_open_trades'], + default_conf['stake_amount'] + ) assert msg in msg_mock.call_args_list[0][0][0] @@ -1159,6 +1161,7 @@ def test_edge_enabled(edge_conf, update, mocker) -> None: assert 'Winrate' not in msg_mock.call_args_list[0][0][0] +# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) def test_telegram_trades(mocker, update, default_conf, fee): telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) @@ -1177,7 +1180,7 @@ def test_telegram_trades(mocker, update, default_conf, fee): assert "
" not in msg_mock.call_args_list[0][0][0]
     msg_mock.reset_mock()
 
-    create_mock_trades(fee)
+    create_mock_trades(fee, False)
 
     context = MagicMock()
     context.args = [5]
@@ -1191,6 +1194,7 @@ def test_telegram_trades(mocker, update, default_conf, fee):
                 msg_mock.call_args_list[0][0][0]))
 
 
+# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
 def test_telegram_delete_trade(mocker, update, default_conf, fee):
 
     telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
@@ -1201,7 +1205,7 @@ def test_telegram_delete_trade(mocker, update, default_conf, fee):
     assert "Trade-id not set." in msg_mock.call_args_list[0][0][0]
 
     msg_mock.reset_mock()
-    create_mock_trades(fee)
+    create_mock_trades(fee, False)
 
     context = MagicMock()
     context.args = [1]
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 3b72f55c1..1cc4a184b 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -875,10 +875,8 @@ def test_execute_entry(mocker, default_conf, fee, limit_buy_order, limit_sell_or
     assert trade.open_rate_requested == 10
 
 
-@pytest.mark.parametrize("is_short", [False, True])
-def test_execute_entry_confirm_error(mocker, default_conf, fee, limit_buy_order,
-                                     limit_sell_order, is_short) -> None:
-    order = limit_sell_order if is_short else limit_buy_order
+# TODO-lev: @pytest.mark.parametrize("is_short", [False, True])
+def test_execute_entry_confirm_error(mocker, default_conf, fee, limit_buy_order) -> None:
     freqtrade = get_patched_freqtradebot(mocker, default_conf)
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
@@ -887,7 +885,7 @@ def test_execute_entry_confirm_error(mocker, default_conf, fee, limit_buy_order,
             'ask': 0.00001173,
             'last': 0.00001172
         }),
-        create_order=MagicMock(return_value=order),
+        create_order=MagicMock(return_value=limit_buy_order),
         get_rate=MagicMock(return_value=0.11),
         get_min_pair_stake_amount=MagicMock(return_value=1),
         get_fee=fee,
@@ -899,11 +897,11 @@ def test_execute_entry_confirm_error(mocker, default_conf, fee, limit_buy_order,
     # TODO-lev: KeyError happens on short, why?
     assert freqtrade.execute_entry(pair, stake_amount)
 
-    order['id'] = '222'
+    limit_buy_order['id'] = '222'
     freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=Exception)
     assert freqtrade.execute_entry(pair, stake_amount)
 
-    order['id'] = '2223'
+    limit_buy_order['id'] = '2223'
     freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True)
     assert freqtrade.execute_entry(pair, stake_amount)
 
@@ -1319,7 +1317,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, is_shor
         pair='ETH/BTC',
         order_types=freqtrade.strategy.order_types,
         stop_price=0.00002346 * 0.95,
-        side="sell",
+        side="buy" if is_short else "sell",
         leverage=1.0
     )
 
@@ -1398,7 +1396,10 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
     mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order',
                  return_value=stoploss_order_hanging)
     freqtrade.handle_trailing_stoploss_on_exchange(
-        trade, stoploss_order_hanging, side=("buy" if is_short else "sell"))
+        trade,
+        stoploss_order_hanging,
+        side=("buy" if is_short else "sell")
+    )
     assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog)
 
     # Still try to create order
@@ -1519,7 +1520,7 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, is_s
         pair='ETH/BTC',
         order_types=freqtrade.strategy.order_types,
         stop_price=0.00002346 * 0.96,
-        side="sell",
+        side="buy" if is_short else "sell",
         leverage=1.0
     )
 
diff --git a/tests/test_persistence.py b/tests/test_persistence.py
index acdd79350..72d58dc67 100644
--- a/tests/test_persistence.py
+++ b/tests/test_persistence.py
@@ -1346,11 +1346,12 @@ def test_adjust_min_max_rates(fee):
 
 @pytest.mark.usefixtures("init_persistence")
 @pytest.mark.parametrize('use_db', [True, False])
-def test_get_open(fee, use_db):
+@pytest.mark.parametrize('is_short', [True, False])
+def test_get_open(fee, is_short, use_db):
     Trade.use_db = use_db
     Trade.reset_trades()
 
-    create_mock_trades(fee, use_db)
+    create_mock_trades(fee, is_short, use_db)
     assert len(Trade.get_open_trades()) == 4
 
     Trade.use_db = True
@@ -1702,14 +1703,15 @@ def test_fee_updated(fee):
 
 
 @pytest.mark.usefixtures("init_persistence")
+@pytest.mark.parametrize('is_short', [True, False])
 @pytest.mark.parametrize('use_db', [True, False])
-def test_total_open_trades_stakes(fee, use_db):
+def test_total_open_trades_stakes(fee, is_short, use_db):
 
     Trade.use_db = use_db
     Trade.reset_trades()
     res = Trade.total_open_trades_stakes()
     assert res == 0
-    create_mock_trades(fee, use_db)
+    create_mock_trades(fee, is_short, use_db)
     res = Trade.total_open_trades_stakes()
     assert res == 0.004
 
@@ -1717,6 +1719,7 @@ def test_total_open_trades_stakes(fee, use_db):
 
 
 @pytest.mark.usefixtures("init_persistence")
+# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
 @pytest.mark.parametrize('use_db', [True, False])
 def test_get_total_closed_profit(fee, use_db):
 
@@ -1724,7 +1727,7 @@ def test_get_total_closed_profit(fee, use_db):
     Trade.reset_trades()
     res = Trade.get_total_closed_profit()
     assert res == 0
-    create_mock_trades(fee, use_db)
+    create_mock_trades(fee, False, use_db)
     res = Trade.get_total_closed_profit()
     assert res == 0.000739127
 
@@ -1732,11 +1735,12 @@ def test_get_total_closed_profit(fee, use_db):
 
 
 @pytest.mark.usefixtures("init_persistence")
+# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
 @pytest.mark.parametrize('use_db', [True, False])
 def test_get_trades_proxy(fee, use_db):
     Trade.use_db = use_db
     Trade.reset_trades()
-    create_mock_trades(fee, use_db)
+    create_mock_trades(fee, False, use_db)
     trades = Trade.get_trades_proxy()
     assert len(trades) == 6
 
@@ -1765,9 +1769,10 @@ def test_get_trades_backtest():
 
 
 @pytest.mark.usefixtures("init_persistence")
+# @pytest.mark.parametrize('is_short', [True, False])
 def test_get_overall_performance(fee):
 
-    create_mock_trades(fee)
+    create_mock_trades(fee, False)
     res = Trade.get_overall_performance()
 
     assert len(res) == 2
@@ -1777,12 +1782,13 @@ def test_get_overall_performance(fee):
 
 
 @pytest.mark.usefixtures("init_persistence")
+# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
 def test_get_best_pair(fee):
 
     res = Trade.get_best_pair()
     assert res is None
 
-    create_mock_trades(fee)
+    create_mock_trades(fee, False)
     res = Trade.get_best_pair()
     assert len(res) == 2
     assert res[0] == 'XRP/BTC'
@@ -1864,8 +1870,9 @@ def test_update_order_from_ccxt(caplog):
 
 
 @pytest.mark.usefixtures("init_persistence")
+# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
 def test_select_order(fee):
-    create_mock_trades(fee)
+    create_mock_trades(fee, False)
 
     trades = Trade.get_trades().all()
 

From 5dd1088d8dae43847a09dc26fec010aa7c35ab8a Mon Sep 17 00:00:00 2001
From: Scott Lyons 
Date: Thu, 30 Sep 2021 00:44:26 -0700
Subject: [PATCH 054/134] Adding ignore unparameterized spaces flag

---
 freqtrade/commands/cli_options.py | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py
index e3c7fe464..ef1ec8515 100644
--- a/freqtrade/commands/cli_options.py
+++ b/freqtrade/commands/cli_options.py
@@ -552,4 +552,9 @@ AVAILABLE_CLI_OPTIONS = {
         help='Do not print epoch details header.',
         action='store_true',
     ),
+    "hyperopt_ignore_unparam_space": Arg(
+        "-u", "--ignore-unparameterized-spaces",
+        help="Suppress errors for any requested Hyperopt spaces that do not contain any parameters",
+        action="store_true",
+    ),
 }

From 08fcd1a0d4977716d466496171e4ae6a5c36b67f Mon Sep 17 00:00:00 2001
From: Scott Lyons 
Date: Thu, 30 Sep 2021 00:46:56 -0700
Subject: [PATCH 055/134] Adding ignore space errors to Hyperopt CLI

---
 freqtrade/commands/arguments.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py
index d424f3ce7..e58135895 100644
--- a/freqtrade/commands/arguments.py
+++ b/freqtrade/commands/arguments.py
@@ -31,7 +31,8 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
                                         "epochs", "spaces", "print_all",
                                         "print_colorized", "print_json", "hyperopt_jobs",
                                         "hyperopt_random_state", "hyperopt_min_trades",
-                                        "hyperopt_loss", "disableparamexport"]
+                                        "hyperopt_loss", "disableparamexport", 
+                                        "hyperopt_ignore_unparam_space"]
 
 ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
 

From 95227376b609a98b1577633861f90e9c9bb594f0 Mon Sep 17 00:00:00 2001
From: Scott Lyons 
Date: Thu, 30 Sep 2021 00:53:46 -0700
Subject: [PATCH 056/134] Adding IUS to optimize args

---
 freqtrade/configuration/configuration.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py
index 94b108f2b..723ad3795 100644
--- a/freqtrade/configuration/configuration.py
+++ b/freqtrade/configuration/configuration.py
@@ -368,6 +368,9 @@ class Configuration:
 
         self._args_to_config(config, argname='hyperopt_show_no_header',
                              logstring='Parameter --no-header detected: {}')
+        
+        self._args_to_config(config, argname="hyperopt_ignore_unparam_space",
+                             logstring="Paramter --ignore-unparameterized-spaces detected: {}")
 
     def _process_plot_options(self, config: Dict[str, Any]) -> None:
 

From df45f467c69e5b0dc64fe8cb5b5af716b42979d7 Mon Sep 17 00:00:00 2001
From: Scott Lyons 
Date: Thu, 30 Sep 2021 01:11:02 -0700
Subject: [PATCH 057/134] Adding ability to ignore unparameterized spaces

---
 freqtrade/optimize/hyperopt.py | 48 +++++++++++++++++++++++++++++-----
 1 file changed, 42 insertions(+), 6 deletions(-)

diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
index 9549b4054..f6c677a6e 100644
--- a/freqtrade/optimize/hyperopt.py
+++ b/freqtrade/optimize/hyperopt.py
@@ -237,27 +237,63 @@ class Hyperopt:
             logger.debug("Hyperopt has 'protection' space")
             # Enable Protections if protection space is selected.
             self.config['enable_protections'] = True
-            self.protection_space = self.custom_hyperopt.protection_space()
+            try:
+                self.protection_space = self.custom_hyperopt.protection_space()
+            except OperationalException as e:
+                if self.config["hyperopt_ignore_unparam_space"]:
+                    logger.warning(e)
+                else:
+                    raise
 
         if HyperoptTools.has_space(self.config, 'buy'):
             logger.debug("Hyperopt has 'buy' space")
-            self.buy_space = self.custom_hyperopt.buy_indicator_space()
+            try:
+                self.buy_space = self.custom_hyperopt.buy_indicator_space()
+            except OperationalException as e:
+                if self.config["hyperopt_ignore_unparam_space"]:
+                    logger.warning(e)
+                else:
+                    raise
 
         if HyperoptTools.has_space(self.config, 'sell'):
             logger.debug("Hyperopt has 'sell' space")
-            self.sell_space = self.custom_hyperopt.sell_indicator_space()
+            try:
+                self.sell_space = self.custom_hyperopt.sell_indicator_space()
+            except OperationalException as e:
+                if self.config["hyperopt_ignore_unparam_space"]:
+                    logger.warning(e)
+                else:
+                    raise
 
         if HyperoptTools.has_space(self.config, 'roi'):
             logger.debug("Hyperopt has 'roi' space")
-            self.roi_space = self.custom_hyperopt.roi_space()
+            try:
+                self.roi_space = self.custom_hyperopt.roi_space()
+            except OperationalException as e:
+                if self.config["hyperopt_ignore_unparam_space"]:
+                    logger.warning(e)
+                else:
+                    raise
 
         if HyperoptTools.has_space(self.config, 'stoploss'):
             logger.debug("Hyperopt has 'stoploss' space")
-            self.stoploss_space = self.custom_hyperopt.stoploss_space()
+            try:
+                self.stoploss_space = self.custom_hyperopt.stoploss_space()
+            except OperationalException as e:
+                if self.config["hyperopt_ignore_unparam_space"]:
+                    logger.warning(e)
+                else:
+                    raise
 
         if HyperoptTools.has_space(self.config, 'trailing'):
             logger.debug("Hyperopt has 'trailing' space")
-            self.trailing_space = self.custom_hyperopt.trailing_space()
+            try:
+                self.trailing_space = self.custom_hyperopt.trailing_space()
+            except OperationalException as e:
+                if self.config["hyperopt_ignore_unparam_space"]:
+                    logger.warning(e)
+                else:
+                    raise
         self.dimensions = (self.buy_space + self.sell_space + self.protection_space
                            + self.roi_space + self.stoploss_space + self.trailing_space)
 

From 87ff65d31e41c6a929c67de812bff382cb5ee449 Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Sat, 2 Oct 2021 03:58:02 -0600
Subject: [PATCH 058/134] Fixed failing test_handle_protections

---
 freqtrade/freqtradebot.py  |  2 +-
 tests/test_freqtradebot.py | 11 +++++------
 2 files changed, 6 insertions(+), 7 deletions(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 3a5fea580..45fd72d55 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -947,7 +947,7 @@ class FreqtradeBot(LoggingMixin):
             # if trailing stoploss is enabled we check if stoploss value has changed
             # in which case we cancel stoploss order and put another one with new
             # value immediately
-            self.handle_trailing_stoploss_on_exchange(trade, stoploss_order, side=trade.exit_side)
+            self.handle_trailing_stoploss_on_exchange(trade, stoploss_order)
 
         return False
 
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index b27c39e17..1c9b34e56 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -446,7 +446,8 @@ def test_enter_positions_global_pairlock(default_conf, ticker, limit_buy_order,
     assert log_has_re(message, caplog)
 
 
-def test_handle_protections(mocker, default_conf, fee):
+@pytest.mark.parametrize('is_short', [False, True])
+def test_handle_protections(mocker, default_conf, fee, is_short):
     default_conf['protections'] = [
         {"method": "CooldownPeriod", "stop_duration": 60},
         {
@@ -461,7 +462,7 @@ def test_handle_protections(mocker, default_conf, fee):
     freqtrade = get_patched_freqtradebot(mocker, default_conf)
     freqtrade.protections._protection_handlers[1].global_stop = MagicMock(
         return_value=(True, arrow.utcnow().shift(hours=1).datetime, "asdf"))
-    create_mock_trades(fee)
+    create_mock_trades(fee, is_short)
     freqtrade.handle_protections('ETC/BTC')
     send_msg_mock = freqtrade.rpc.send_msg
     assert send_msg_mock.call_count == 2
@@ -1420,8 +1421,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
                  return_value=stoploss_order_hanging)
     freqtrade.handle_trailing_stoploss_on_exchange(
         trade,
-        stoploss_order_hanging,
-        side=("buy" if is_short else "sell")
+        stoploss_order_hanging
     )
     assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog)
 
@@ -1432,8 +1432,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
     caplog.clear()
     cancel_mock = mocker.patch("freqtrade.exchange.Binance.cancel_stoploss_order", MagicMock())
     mocker.patch("freqtrade.exchange.Binance.stoploss", side_effect=ExchangeError())
-    freqtrade.handle_trailing_stoploss_on_exchange(
-        trade, stoploss_order_hanging, side=("buy" if is_short else "sell"))
+    freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
     assert cancel_mock.call_count == 1
     assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog)
 

From 09ef0781a165566dfe8314e201f375e6c3a8e019 Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Sat, 2 Oct 2021 23:35:50 -0600
Subject: [PATCH 059/134] switching limit_buy_order_usdt and
 limit_sell_order_usdt to limit_order(enter_side[is_short]) and
 limit_order(exit_side[is_short])

---
 freqtrade/freqtradebot.py  |   4 +-
 tests/conftest.py          |  21 +--
 tests/test_freqtradebot.py | 342 +++++++++++++++++--------------------
 3 files changed, 172 insertions(+), 195 deletions(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 12c8b1e37..024ae1996 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -16,8 +16,8 @@ from freqtrade.configuration import validate_config_consistency
 from freqtrade.data.converter import order_book_to_dataframe
 from freqtrade.data.dataprovider import DataProvider
 from freqtrade.edge import Edge
-from freqtrade.enums import (Collateral, RPCMessageType, SellType, SignalDirection, SignalTagType,
-                             State, TradingMode)
+from freqtrade.enums import (Collateral, RPCMessageType, SellType, SignalDirection, State,
+                             TradingMode)
 from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
                                   InvalidOrderException, PricingError)
 from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
diff --git a/tests/conftest.py b/tests/conftest.py
index f82e7e985..41e956a20 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -2349,27 +2349,24 @@ def market_sell_order_usdt():
 
 
 @pytest.fixture(scope='function')
-def open_order(limit_buy_order_open, limit_sell_order_open):
-    # limit_sell_order_open if is_short else limit_buy_order_open
+def limit_order(limit_buy_order_usdt, limit_sell_order_usdt):
     return {
-        True: limit_sell_order_open,
-        False: limit_buy_order_open
+        'buy': limit_buy_order_usdt,
+        'sell': limit_sell_order_usdt
     }
 
 
 @pytest.fixture(scope='function')
-def limit_order(limit_sell_order, limit_buy_order):
-    # limit_sell_order if is_short else limit_buy_order
+def market_order(market_buy_order_usdt, market_sell_order_usdt):
     return {
-        True: limit_sell_order,
-        False: limit_buy_order
+        'buy': market_buy_order_usdt,
+        'sell': market_sell_order_usdt
     }
 
 
 @pytest.fixture(scope='function')
-def old_order(limit_sell_order_old, limit_buy_order_old):
-    # limit_sell_order_old if is_short else limit_buy_order_old
+def limit_order_open(limit_buy_order_usdt_open, limit_sell_order_usdt_open):
     return {
-        True: limit_sell_order_old,
-        False: limit_buy_order_old
+        'buy': limit_buy_order_usdt_open,
+        'sell': limit_sell_order_usdt_open
     }
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 2b74860a2..62ca254c2 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -47,17 +47,6 @@ def patch_RPCManager(mocker) -> MagicMock:
     return rpc_mock
 
 
-def open_order(limit_buy_order_usdt_open, limit_sell_order_usdt_open, is_short):
-    return limit_sell_order_usdt_open if is_short else limit_buy_order_usdt_open
-
-
-def limit_order(limit_sell_order_usdt, limit_buy_order_usdt, is_short):
-    return limit_sell_order_usdt if is_short else limit_buy_order_usdt
-
-
-def old_order(limit_sell_order_old, limit_buy_order_old, is_short):
-    return limit_sell_order_old if is_short else limit_buy_order_old
-
 # Unit tests
 
 
@@ -214,13 +203,14 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None:
         'LTC/BTC', freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21
 
 
+@pytest.mark.parametrize('is_short', [False, True])
 @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_usdt, fee, caplog, mocker,
+def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker, is_short,
                                  buy_price_mult, ignore_strat_sl, edge_conf) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
@@ -231,13 +221,13 @@ def test_edge_overrides_stoploss(limit_buy_order_usdt, fee, caplog, mocker,
     # Thus, if price falls 21%, stoploss should be triggered
     #
     # mocking the ticker_usdt: price is falling ...
-    buy_price = limit_buy_order_usdt['price']
+    enter_price = limit_order[enter_side(is_short)]['price']
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
         fetch_ticker=MagicMock(return_value={
-            'bid': buy_price * buy_price_mult,
-            'ask': buy_price * buy_price_mult,
-            'last': buy_price * buy_price_mult,
+            'bid': enter_price * buy_price_mult,
+            'ask': enter_price * buy_price_mult,
+            'last': enter_price * buy_price_mult,
         }),
         get_fee=fee,
     )
@@ -250,7 +240,7 @@ def test_edge_overrides_stoploss(limit_buy_order_usdt, fee, caplog, mocker,
     freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
     freqtrade.enter_positions()
     trade = Trade.query.first()
-    trade.update(limit_buy_order_usdt)
+    trade.update(limit_order[enter_side(is_short)])
     #############################################
 
     # stoploss shoud be hit
@@ -296,7 +286,7 @@ def test_total_open_trades_stakes(mocker, default_conf_usdt, ticker_usdt, fee) -
     (False, 2.0),
     (True, 2.2)
 ])
-def test_create_trade(default_conf_usdt, ticker_usdt, limit_buy_order_usdt, limit_sell_order_usdt,
+def test_create_trade(default_conf_usdt, ticker_usdt, limit_order,
                       fee, mocker, is_short, open_rate) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
@@ -322,10 +312,7 @@ def test_create_trade(default_conf_usdt, ticker_usdt, limit_buy_order_usdt, limi
     assert trade.exchange == 'binance'
 
     # Simulate fulfilled LIMIT_BUY order for trade
-    if is_short:
-        trade.update(limit_sell_order_usdt)
-    else:
-        trade.update(limit_buy_order_usdt)
+    trade.update(limit_order[enter_side(is_short)])
 
     assert trade.open_rate == open_rate
     assert trade.amount == 30.0
@@ -357,16 +344,16 @@ def test_create_trade_no_stake_amount(default_conf_usdt, ticker_usdt, fee, mocke
     (UNLIMITED_STAKE_AMOUNT, False, True, 0),
 ])
 def test_create_trade_minimal_amount(
-    default_conf_usdt, ticker_usdt, limit_buy_order_usdt_open, fee, mocker,
+    default_conf_usdt, ticker_usdt, limit_order_open, fee, mocker,
     stake_amount, create, amount_enough, max_open_trades, caplog, is_short
 ) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
-    buy_mock = MagicMock(return_value=limit_buy_order_usdt_open)
+    enter_mock = MagicMock(return_value=limit_order_open[enter_side(is_short)])
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
         fetch_ticker=ticker_usdt,
-        create_order=buy_mock,
+        create_order=enter_mock,
         get_fee=fee,
     )
     default_conf_usdt['max_open_trades'] = max_open_trades
@@ -377,7 +364,7 @@ def test_create_trade_minimal_amount(
     if create:
         assert freqtrade.create_trade('ETH/USDT')
         if amount_enough:
-            rate, amount = buy_mock.call_args[1]['rate'], buy_mock.call_args[1]['amount']
+            rate, amount = enter_mock.call_args[1]['rate'], enter_mock.call_args[1]['amount']
             assert rate * amount <= default_conf_usdt['stake_amount']
         else:
             assert log_has_re(
@@ -549,15 +536,17 @@ def test_create_trades_preopen(default_conf_usdt, ticker_usdt, fee, mocker,
     assert len(trades) == 4
 
 
-def test_process_trade_creation(default_conf_usdt, ticker_usdt, limit_buy_order_usdt,
-                                limit_buy_order_usdt_open, fee, mocker, caplog) -> None:
+@pytest.mark.parametrize('is_short', [False, True])
+def test_process_trade_creation(default_conf_usdt, ticker_usdt, limit_order, limit_order_open,
+                                is_short, fee, mocker, caplog
+                                ) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
         fetch_ticker=ticker_usdt,
-        create_order=MagicMock(return_value=limit_buy_order_usdt_open),
-        fetch_order=MagicMock(return_value=limit_buy_order_usdt),
+        create_order=MagicMock(return_value=limit_order_open[enter_side(is_short)]),
+        fetch_order=MagicMock(return_value=limit_order[enter_side(is_short)]),
         get_fee=fee,
     )
     freqtrade = FreqtradeBot(default_conf_usdt)
@@ -729,11 +718,11 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker)
 
 
 @pytest.mark.parametrize("is_short", [True, False])
-def test_execute_entry(mocker, default_conf_usdt, fee, limit_buy_order_usdt, limit_sell_order_usdt,
-                       limit_buy_order_usdt_open, limit_sell_order_usdt_open, is_short) -> None:
+def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
+                       limit_order_open, is_short) -> None:
 
-    open_order = limit_sell_order_usdt_open if is_short else limit_buy_order_usdt_open
-    order = limit_sell_order_usdt if is_short else limit_buy_order_usdt
+    open_order = limit_order_open[enter_side(is_short)]
+    order = limit_order[enter_side(is_short)]
 
     patch_RPCManager(mocker)
     patch_exchange(mocker)
@@ -906,8 +895,8 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_buy_order_usdt, lim
     assert trade.open_rate_requested == 10
 
 
-# TODO-lev: @pytest.mark.parametrize("is_short", [False, True])
-def test_execute_entry_confirm_error(mocker, default_conf_usdt, fee, limit_buy_order_usdt) -> None:
+@pytest.mark.parametrize("is_short", [False, True])
+def test_execute_entry_confirm_error(mocker, default_conf_usdt, fee, limit_order, is_short) -> None:
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
@@ -916,7 +905,7 @@ def test_execute_entry_confirm_error(mocker, default_conf_usdt, fee, limit_buy_o
             'ask': 2.2,
             'last': 1.9
         }),
-        create_order=MagicMock(return_value=limit_buy_order_usdt),
+        create_order=MagicMock(return_value=limit_order[enter_side(is_short)]),
         get_rate=MagicMock(return_value=0.11),
         get_min_pair_stake_amount=MagicMock(return_value=1),
         get_fee=fee,
@@ -928,11 +917,11 @@ def test_execute_entry_confirm_error(mocker, default_conf_usdt, fee, limit_buy_o
     # TODO-lev: KeyError happens on short, why?
     assert freqtrade.execute_entry(pair, stake_amount)
 
-    limit_buy_order_usdt['id'] = '222'
+    limit_order[enter_side(is_short)]['id'] = '222'
     freqtrade.strategy.confirm_trade_entry = MagicMock(side_effect=Exception)
     assert freqtrade.execute_entry(pair, stake_amount)
 
-    limit_buy_order_usdt['id'] = '2223'
+    limit_order[enter_side(is_short)]['id'] = '2223'
     freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True)
     assert freqtrade.execute_entry(pair, stake_amount)
 
@@ -941,16 +930,15 @@ def test_execute_entry_confirm_error(mocker, default_conf_usdt, fee, limit_buy_o
 
 
 @pytest.mark.parametrize("is_short", [False, True])
-def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_buy_order_usdt,
-                                  limit_sell_order_usdt, is_short) -> None:
+def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_order, is_short) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
-    order = limit_sell_order_usdt if is_short else limit_buy_order_usdt
+    order = limit_order[enter_side(is_short)]
     mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True))
     mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order)
     mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
     mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
-                 return_value=limit_buy_order_usdt['amount'])
+                 return_value=order['amount'])
 
     stoploss = MagicMock(return_value={'id': 13434334})
     mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
@@ -973,10 +961,10 @@ def test_add_stoploss_on_exchange(mocker, default_conf_usdt, limit_buy_order_usd
 
 @pytest.mark.parametrize("is_short", [False, True])
 def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_short,
-                                     limit_buy_order_usdt, limit_sell_order_usdt) -> None:
+                                     limit_order) -> None:
     stoploss = MagicMock(return_value={'id': 13434334})
-    enter_order = limit_sell_order_usdt if is_short else limit_buy_order_usdt
-    exit_order = limit_buy_order_usdt if is_short else limit_sell_order_usdt
+    enter_order = limit_order[enter_side(is_short)]
+    exit_order = limit_order[exit_side(is_short)]
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
@@ -1098,10 +1086,10 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
 
 @pytest.mark.parametrize("is_short", [False, True])
 def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog, is_short,
-                                         limit_buy_order_usdt, limit_sell_order_usdt) -> None:
+                                         limit_order) -> None:
     # Sixth case: stoploss order was cancelled but couldn't create new one
-    enter_order = limit_sell_order_usdt if is_short else limit_buy_order_usdt
-    exit_order = limit_buy_order_usdt if is_short else limit_sell_order_usdt
+    enter_order = limit_order[enter_side(is_short)]
+    exit_order = limit_order[exit_side(is_short)]
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
@@ -1141,10 +1129,10 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog,
 
 @pytest.mark.parametrize("is_short", [False, True])
 def test_create_stoploss_order_invalid_order(
-        mocker, default_conf_usdt, caplog, fee, is_short, limit_buy_order_usdt, limit_sell_order_usdt,
-        limit_buy_order_usdt_open, limit_sell_order_usdt_open):
-    open_order = limit_sell_order_usdt_open if is_short else limit_buy_order_usdt_open
-    order = limit_buy_order_usdt if is_short else limit_sell_order_usdt
+    mocker, default_conf_usdt, caplog, fee, is_short, limit_order, limit_order_open
+):
+    open_order = limit_order_open[enter_side(is_short)]
+    order = limit_order[exit_side(is_short)]
     rpc_mock = patch_RPCManager(mocker)
     patch_exchange(mocker)
     create_order_mock = MagicMock(side_effect=[
@@ -1194,14 +1182,10 @@ def test_create_stoploss_order_invalid_order(
 
 @pytest.mark.parametrize("is_short", [False, True])
 def test_create_stoploss_order_insufficient_funds(
-    mocker, default_conf_usdt, caplog, fee, limit_buy_order_usdt_open, limit_sell_order_usdt_open,
-    limit_buy_order_usdt, limit_sell_order_usdt, is_short
+    mocker, default_conf_usdt, caplog, fee, limit_order_open,
+    limit_order, is_short
 ):
-    exit_order = (
-        MagicMock(return_value={'id': limit_buy_order_usdt['id']})
-        if is_short else
-        MagicMock(return_value={'id': limit_sell_order_usdt['id']})
-    )
+    exit_order = limit_order[exit_side(is_short)]['id']
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
 
     mock_insuf = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_insufficient_funds')
@@ -1213,7 +1197,7 @@ def test_create_stoploss_order_insufficient_funds(
             'last': 1.9
         }),
         create_order=MagicMock(side_effect=[
-            limit_sell_order_usdt_open if is_short else limit_buy_order_usdt_open,
+            limit_order[enter_side(is_short)],
             exit_order,
         ]),
         get_fee=fee,
@@ -1246,11 +1230,11 @@ def test_create_stoploss_order_insufficient_funds(
 @pytest.mark.parametrize("is_short", [False, True])
 @pytest.mark.usefixtures("init_persistence")
 def test_handle_stoploss_on_exchange_trailing(mocker, default_conf_usdt, fee, is_short,
-                                              limit_buy_order_usdt, limit_sell_order_usdt) -> None:
+                                              limit_order) -> None:
     # TODO-lev: test for short
     # When trailing stoploss is set
-    enter_order = limit_sell_order_usdt if is_short else limit_buy_order_usdt
-    exit_order = limit_buy_order_usdt if is_short else limit_sell_order_usdt
+    enter_order = limit_order[enter_side(is_short)]
+    exit_order = limit_order[exit_side(is_short)]
     stoploss = MagicMock(return_value={'id': 13434334})
     patch_RPCManager(mocker)
     mocker.patch.multiple(
@@ -1368,11 +1352,10 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf_usdt, fee, is
 
 @pytest.mark.parametrize("is_short", [False, True])
 def test_handle_stoploss_on_exchange_trailing_error(
-    mocker, default_conf_usdt, fee, caplog, limit_buy_order_usdt,
-    limit_sell_order_usdt, is_short
+    mocker, default_conf_usdt, fee, caplog, limit_order, is_short
 ) -> None:
-    enter_order = limit_sell_order_usdt if is_short else limit_buy_order_usdt
-    exit_order = limit_buy_order_usdt if is_short else limit_sell_order_usdt
+    enter_order = limit_order[enter_side(is_short)]
+    exit_order = limit_order[exit_side(is_short)]
     # When trailing stoploss is set
     stoploss = MagicMock(return_value={'id': 13434334})
     patch_exchange(mocker)
@@ -1450,11 +1433,10 @@ def test_handle_stoploss_on_exchange_trailing_error(
 @pytest.mark.parametrize("is_short", [False, True])
 @pytest.mark.usefixtures("init_persistence")
 def test_handle_stoploss_on_exchange_custom_stop(
-    mocker, default_conf_usdt, fee, is_short, limit_buy_order_usdt,
-    limit_sell_order_usdt
+    mocker, default_conf_usdt, fee, is_short, limit_order
 ) -> None:
-    enter_order = limit_sell_order_usdt if is_short else limit_buy_order_usdt
-    exit_order = limit_buy_order_usdt if is_short else limit_sell_order_usdt
+    enter_order = limit_order[enter_side(is_short)]
+    exit_order = limit_order[exit_side(is_short)]
     # When trailing stoploss is set
     # TODO-lev: test for short
     stoploss = MagicMock(return_value={'id': 13434334})
@@ -1573,10 +1555,10 @@ def test_handle_stoploss_on_exchange_custom_stop(
 
 @pytest.mark.parametrize("is_short", [False, True])
 def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, is_short,
-                                              limit_buy_order_usdt, limit_sell_order_usdt) -> None:
+                                              limit_order) -> None:
 
-    enter_order = limit_sell_order_usdt if is_short else limit_buy_order_usdt
-    exit_order = limit_buy_order_usdt if is_short else limit_sell_order_usdt
+    enter_order = limit_order[enter_side(is_short)]
+    exit_order = limit_order[exit_side(is_short)]
 
     # When trailing stoploss is set
     stoploss = MagicMock(return_value={'id': 13434334})
@@ -1717,15 +1699,16 @@ def test_enter_positions(mocker, default_conf_usdt, return_value, side_effect,
 
 @pytest.mark.parametrize("is_short", [False, True])
 def test_exit_positions(
-    mocker, default_conf_usdt, limit_buy_order_usdt, is_short, caplog
+    mocker, default_conf_usdt, limit_order, is_short, caplog
 ) -> None:
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
 
     mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True))
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order_usdt)
+    mocker.patch('freqtrade.exchange.Exchange.fetch_order',
+                 return_value=limit_order[enter_side(is_short)])
     mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
     mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
-                 return_value=limit_buy_order_usdt['amount'])
+                 return_value=limit_order[enter_side(is_short)]['amount'])
 
     trade = MagicMock()
     trade.is_short = is_short
@@ -1747,12 +1730,11 @@ def test_exit_positions(
 
 @pytest.mark.parametrize("is_short", [False, True])
 def test_exit_positions_exception(
-    mocker, default_conf_usdt, limit_buy_order_usdt,
-    limit_sell_order_usdt, caplog, is_short
+    mocker, default_conf_usdt, limit_order, caplog, is_short
 ) -> None:
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
-    order = limit_sell_order_usdt if is_short else limit_buy_order_usdt
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=limit_buy_order_usdt)
+    order = limit_order[enter_side(is_short)]
+    mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order)
 
     trade = MagicMock()
     trade.is_short = is_short
@@ -1774,11 +1756,10 @@ def test_exit_positions_exception(
 
 @pytest.mark.parametrize("is_short", [False, True])
 def test_update_trade_state(
-    mocker, default_conf_usdt, limit_buy_order_usdt,
-    limit_sell_order_usdt, is_short, caplog
+    mocker, default_conf_usdt, limit_order, is_short, caplog
 ) -> None:
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
-    order = limit_sell_order_usdt if is_short else limit_buy_order_usdt
+    order = limit_order[enter_side(is_short)]
 
     mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True))
     mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order)
@@ -1829,10 +1810,10 @@ def test_update_trade_state(
     (8.0, False)
 ])
 def test_update_trade_state_withorderdict(
-    default_conf_usdt, trades_for_order, limit_buy_order_usdt, fee, mocker, initial_amount,
-    has_rounding_fee, limit_sell_order_usdt, is_short, caplog
+    default_conf_usdt, trades_for_order, limit_order, fee, mocker, initial_amount,
+    has_rounding_fee, is_short, caplog
 ):
-    order = limit_sell_order_usdt if is_short else limit_buy_order_usdt
+    order = limit_order[enter_side(is_short)]
     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!!
@@ -1855,15 +1836,15 @@ def test_update_trade_state_withorderdict(
     )
     freqtrade.update_trade_state(trade, '123456', order)
     assert trade.amount != amount
-    assert trade.amount == limit_buy_order_usdt['amount']
+    assert trade.amount == order['amount']
     if has_rounding_fee:
         assert log_has_re(r'Applying fee on amount for .*', caplog)
 
 
 @pytest.mark.parametrize("is_short", [False, True])
-def test_update_trade_state_exception(mocker, default_conf_usdt, is_short, limit_sell_order_usdt,
-                                      limit_buy_order_usdt, caplog) -> None:
-    order = limit_sell_order_usdt if is_short else limit_buy_order_usdt
+def test_update_trade_state_exception(mocker, default_conf_usdt, is_short, limit_order,
+                                      caplog) -> None:
+    order = limit_order[enter_side(is_short)]
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
     mocker.patch('freqtrade.exchange.Exchange.fetch_order', return_value=order)
 
@@ -1899,11 +1880,10 @@ def test_update_trade_state_orderexception(mocker, default_conf_usdt, caplog) ->
 
 @pytest.mark.parametrize("is_short", [False, True])
 def test_update_trade_state_sell(
-    default_conf_usdt, trades_for_order, limit_sell_order_usdt_open, limit_buy_order_usdt_open,
-    limit_sell_order_usdt, is_short, mocker, limit_buy_order_usdt,
+    default_conf_usdt, trades_for_order, limit_order_open, limit_order, is_short, mocker,
 ):
-    open_order = limit_buy_order_usdt_open if is_short else limit_sell_order_usdt_open
-    order = limit_buy_order_usdt if is_short else limit_sell_order_usdt
+    open_order = limit_order_open[exit_side(is_short)]
+    l_order = limit_order[exit_side(is_short)]
     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))
@@ -1912,7 +1892,7 @@ def test_update_trade_state_sell(
 
     patch_exchange(mocker)
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
-    amount = order["amount"]
+    amount = l_order["amount"]
     wallet_mock.reset_mock()
     trade = Trade(
         pair='LTC/ETH',
@@ -1929,9 +1909,8 @@ def test_update_trade_state_sell(
     order = Order.parse_from_ccxt_object(open_order, 'LTC/ETH', (enter_side(is_short)))
     trade.orders.append(order)
     assert order.status == 'open'
-    freqtrade.update_trade_state(trade, trade.open_order_id,
-                                 limit_buy_order_usdt if is_short else limit_sell_order_usdt)
-    assert trade.amount == limit_buy_order_usdt['amount'] if is_short else limit_sell_order_usdt['amount']
+    freqtrade.update_trade_state(trade, trade.open_order_id, l_order)
+    assert trade.amount == l_order['amount']
     # Wallet needs to be updated after closing a limit-sell order to reenable buying
     assert wallet_mock.call_count == 1
     assert not trade.is_open
@@ -1941,12 +1920,11 @@ def test_update_trade_state_sell(
 
 @pytest.mark.parametrize('is_short', [False, True])
 def test_handle_trade(
-    default_conf_usdt, limit_buy_order_usdt_open, limit_buy_order_usdt,
-    limit_sell_order_usdt_open, limit_sell_order_usdt, fee, mocker, is_short
+    default_conf_usdt, limit_order_open, limit_order, fee, mocker, is_short
 ) -> None:
-    open_order = limit_buy_order_usdt_open if is_short else limit_sell_order_usdt_open
-    enter_order = limit_buy_order_usdt if is_short else limit_sell_order_usdt
-    exit_order = limit_sell_order_usdt if is_short else limit_buy_order_usdt
+    open_order = limit_order_open[exit_side(is_short)]
+    enter_order = limit_order[exit_side(is_short)]
+    exit_order = limit_order[enter_side(is_short)]
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
@@ -1990,10 +1968,9 @@ def test_handle_trade(
 
 @ pytest.mark.parametrize("is_short", [False, True])
 def test_handle_overlapping_signals(
-    default_conf_usdt, ticker_usdt, limit_buy_order_usdt_open,
-    limit_sell_order_usdt_open, fee, mocker, is_short
+    default_conf_usdt, ticker_usdt, limit_order_open, fee, mocker, is_short
 ) -> None:
-    open_order = limit_buy_order_usdt_open if is_short else limit_sell_order_usdt_open
+    open_order = limit_order_open[exit_side(is_short)]
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
@@ -2057,10 +2034,10 @@ def test_handle_overlapping_signals(
 
 
 @ pytest.mark.parametrize("is_short", [False, True])
-def test_handle_trade_roi(default_conf_usdt, ticker_usdt, limit_buy_order_usdt_open,
-                          limit_sell_order_usdt_open, fee, mocker, caplog, is_short) -> None:
+def test_handle_trade_roi(default_conf_usdt, ticker_usdt, limit_order_open, fee, mocker, caplog,
+                          is_short) -> None:
 
-    open_order = limit_sell_order_usdt_open if is_short else limit_buy_order_usdt_open
+    open_order = limit_order_open[enter_side(is_short)]
 
     caplog.set_level(logging.DEBUG)
 
@@ -2100,12 +2077,11 @@ def test_handle_trade_roi(default_conf_usdt, ticker_usdt, limit_buy_order_usdt_o
 
 @ pytest.mark.parametrize("is_short", [False, True])
 def test_handle_trade_use_sell_signal(
-    default_conf_usdt, ticker_usdt, limit_buy_order_usdt_open,
-    limit_sell_order_usdt_open, fee, mocker, caplog, is_short
+    default_conf_usdt, ticker_usdt, limit_order_open, fee, mocker, caplog, is_short
 ) -> None:
 
-    enter_open_order = limit_buy_order_usdt_open if is_short else limit_sell_order_usdt_open
-    exit_open_order = limit_sell_order_usdt_open if is_short else limit_buy_order_usdt_open
+    enter_open_order = limit_order_open[exit_side(is_short)]
+    exit_open_order = limit_order_open[enter_side(is_short)]
 
     # use_sell_signal is True buy default
     caplog.set_level(logging.DEBUG)
@@ -2141,12 +2117,12 @@ def test_handle_trade_use_sell_signal(
 
 @ pytest.mark.parametrize("is_short", [False, True])
 def test_close_trade(
-    default_conf_usdt, ticker_usdt, limit_buy_order_usdt, limit_sell_order_usdt_open,
-    limit_buy_order_usdt_open, limit_sell_order_usdt, fee, mocker, is_short
+    default_conf_usdt, ticker_usdt, limit_order_open,
+    limit_order, fee, mocker, is_short
 ) -> None:
-    open_order = limit_buy_order_usdt_open if is_short else limit_sell_order_usdt_open
-    enter_order = limit_buy_order_usdt if is_short else limit_sell_order_usdt
-    exit_order = limit_sell_order_usdt if is_short else limit_buy_order_usdt
+    open_order = limit_order_open[exit_side(is_short)]
+    enter_order = limit_order[exit_side(is_short)]
+    exit_order = limit_order[enter_side(is_short)]
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
@@ -2608,11 +2584,12 @@ def test_check_handle_timedout_exception(default_conf_usdt, ticker_usdt, open_tr
 
 
 @ pytest.mark.parametrize("is_short", [False, True])
-def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_buy_order_usdt,
+def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order,
                              is_short) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
-    cancel_buy_order = deepcopy(limit_buy_order_usdt)
+    l_order = limit_order[enter_side('is_short')]
+    cancel_buy_order = deepcopy(limit_order[enter_side('is_short')])
     cancel_buy_order['status'] = 'canceled'
     del cancel_buy_order['filled']
 
@@ -2627,30 +2604,30 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_buy_order_
     trade.open_rate = 200
     trade.is_short = False
     trade.enter_side = "buy"
-    limit_buy_order_usdt['filled'] = 0.0
-    limit_buy_order_usdt['status'] = 'open'
+    l_order['filled'] = 0.0
+    l_order['status'] = 'open'
     reason = CANCEL_REASON['TIMEOUT']
-    assert freqtrade.handle_cancel_enter(trade, limit_buy_order_usdt, reason)
+    assert freqtrade.handle_cancel_enter(trade, l_order, reason)
     assert cancel_order_mock.call_count == 1
 
     cancel_order_mock.reset_mock()
     caplog.clear()
-    limit_buy_order_usdt['filled'] = 0.01
-    assert not freqtrade.handle_cancel_enter(trade, limit_buy_order_usdt, reason)
+    l_order['filled'] = 0.01
+    assert not freqtrade.handle_cancel_enter(trade, l_order, reason)
     assert cancel_order_mock.call_count == 0
     assert log_has_re("Order .* for .* not cancelled, as the filled amount.* unexitable.*", caplog)
 
     caplog.clear()
     cancel_order_mock.reset_mock()
-    limit_buy_order_usdt['filled'] = 2
-    assert not freqtrade.handle_cancel_enter(trade, limit_buy_order_usdt, reason)
+    l_order['filled'] = 2
+    assert not freqtrade.handle_cancel_enter(trade, l_order, reason)
     assert cancel_order_mock.call_count == 1
 
     # Order remained open for some reason (cancel failed)
     cancel_buy_order['status'] = 'open'
     cancel_order_mock = MagicMock(return_value=cancel_buy_order)
     mocker.patch('freqtrade.exchange.Exchange.cancel_order_with_result', cancel_order_mock)
-    assert not freqtrade.handle_cancel_enter(trade, limit_buy_order_usdt, reason)
+    assert not freqtrade.handle_cancel_enter(trade, l_order, reason)
     assert log_has_re(r"Order .* for .* not cancelled.", caplog)
 
 
@@ -2684,10 +2661,11 @@ def test_handle_cancel_enter_exchanges(mocker, caplog, default_conf_usdt, is_sho
     'String Return value',
     123
 ])
-def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_buy_order_usdt, is_short,
+def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order, is_short,
                                           cancelorder) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
+    l_order = limit_order[enter_side('is_short')]
     cancel_order_mock = MagicMock(return_value=cancelorder)
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
@@ -2702,15 +2680,15 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_buy_o
     trade.enter_side = "buy"
     trade.open_rate = 200
     trade.enter_side = "buy"
-    limit_buy_order_usdt['filled'] = 0.0
-    limit_buy_order_usdt['status'] = 'open'
+    l_order['filled'] = 0.0
+    l_order['status'] = 'open'
     reason = CANCEL_REASON['TIMEOUT']
-    assert freqtrade.handle_cancel_enter(trade, limit_buy_order_usdt, reason)
+    assert freqtrade.handle_cancel_enter(trade, l_order, reason)
     assert cancel_order_mock.call_count == 1
 
     cancel_order_mock.reset_mock()
-    limit_buy_order_usdt['filled'] = 1.0
-    assert not freqtrade.handle_cancel_enter(trade, limit_buy_order_usdt, reason)
+    l_order['filled'] = 1.0
+    assert not freqtrade.handle_cancel_enter(trade, l_order, reason)
     assert cancel_order_mock.call_count == 1
 
 
@@ -3061,8 +3039,8 @@ def test_execute_trade_exit_sloe_cancel_exception(
 
 
 @ pytest.mark.parametrize("is_short", [False, True])
-def test_execute_trade_exit_with_stoploss_on_exchange(default_conf_usdt, ticker_usdt, fee,
-                                                      ticker_usdt_sell_up, is_short, mocker) -> None:
+def test_execute_trade_exit_with_stoploss_on_exchange(
+        default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_up, is_short, mocker) -> None:
 
     default_conf_usdt['exchange']['name'] = 'binance'
     rpc_mock = patch_RPCManager(mocker)
@@ -3296,7 +3274,7 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_u
     (False, 0.10, 0.22, True, False, SellType.SELL_SIGNAL.value),
 ])
 def test_sell_profit_only(
-        default_conf_usdt, limit_buy_order_usdt, limit_buy_order_usdt_open, is_short,
+        default_conf_usdt, limit_order, limit_order_open, is_short,
         fee, mocker, profit_only, bid, ask, handle_first, handle_second, sell_type) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
@@ -3308,7 +3286,7 @@ def test_sell_profit_only(
             'last': bid
         }),
         create_order=MagicMock(side_effect=[
-            limit_buy_order_usdt_open,
+            limit_order_open[enter_side['is_short']],
             {'id': 1234553382},
         ]),
         get_fee=fee,
@@ -3328,7 +3306,7 @@ def test_sell_profit_only(
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
-    trade.update(limit_buy_order_usdt)
+    trade.update(limit_order[enter_side('is_short')])
     freqtrade.wallets.update()
     patch_get_signal(freqtrade, enter_long=False, exit_long=True)
     assert freqtrade.handle_trade(trade) is handle_first
@@ -3339,7 +3317,7 @@ def test_sell_profit_only(
 
 
 @ pytest.mark.parametrize("is_short", [False, True])
-def test_sell_not_enough_balance(default_conf_usdt, limit_buy_order_usdt, limit_buy_order_usdt_open,
+def test_sell_not_enough_balance(default_conf_usdt, limit_order, limit_order_open,
                                  is_short, fee, mocker, caplog) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
@@ -3351,7 +3329,7 @@ def test_sell_not_enough_balance(default_conf_usdt, limit_buy_order_usdt, limit_
             'last': 0.00002172
         }),
         create_order=MagicMock(side_effect=[
-            limit_buy_order_usdt_open,
+            limit_order_open[enter_side(is_short)],
             {'id': 1234553382},
         ]),
         get_fee=fee,
@@ -3365,7 +3343,7 @@ def test_sell_not_enough_balance(default_conf_usdt, limit_buy_order_usdt, limit_
 
     trade = Trade.query.first()
     amnt = trade.amount
-    trade.update(limit_buy_order_usdt)
+    trade.update(limit_order[enter_side(is_short)])
     patch_get_signal(freqtrade, enter_long=False, exit_long=True)
     mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=trade.amount * 0.985))
 
@@ -3450,8 +3428,8 @@ def test_locked_pairs(default_conf_usdt, ticker_usdt, fee,
 
 
 @ pytest.mark.parametrize("is_short", [False, True])
-def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usdt,
-                                  limit_buy_order_usdt_open, is_short, fee, mocker) -> None:
+def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_order, limit_order_open, is_short,
+                                  fee, mocker) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
@@ -3462,7 +3440,7 @@ def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usdt,
             'last': 2.19
         }),
         create_order=MagicMock(side_effect=[
-            limit_buy_order_usdt_open,
+            limit_order_open[enter_side(is_short)],
             {'id': 1234553382},
         ]),
         get_fee=fee,
@@ -3476,7 +3454,7 @@ def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usdt,
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
-    trade.update(limit_buy_order_usdt)
+    trade.update(limit_order[enter_side(is_short)])
     freqtrade.wallets.update()
     patch_get_signal(freqtrade, enter_long=True, exit_long=True)
     assert freqtrade.handle_trade(trade) is False
@@ -3488,7 +3466,7 @@ def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usdt,
 
 
 @ pytest.mark.parametrize("is_short", [False, True])
-def test_trailing_stop_loss(default_conf_usdt, limit_buy_order_usdt_open,
+def test_trailing_stop_loss(default_conf_usdt, limit_order_open,
                             is_short, fee, caplog, mocker) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
@@ -3500,7 +3478,7 @@ def test_trailing_stop_loss(default_conf_usdt, limit_buy_order_usdt_open,
             'last': 2.0
         }),
         create_order=MagicMock(side_effect=[
-            limit_buy_order_usdt_open,
+            limit_order_open[enter_side(is_short)],
             {'id': 1234553382},
         ]),
         get_fee=fee,
@@ -3549,21 +3527,21 @@ def test_trailing_stop_loss(default_conf_usdt, limit_buy_order_usdt_open,
     (0.055, True, 1.8),
 ])
 def test_trailing_stop_loss_positive(
-    default_conf_usdt, limit_buy_order_usdt, limit_buy_order_usdt_open,
+    default_conf_usdt, limit_order, limit_order_open,
     offset, fee, caplog, mocker, trail_if_reached, second_sl, is_short
 ) -> None:
-    buy_price = limit_buy_order_usdt['price']
+    enter_price = limit_order[enter_side(is_short)]['price']
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
         fetch_ticker=MagicMock(return_value={
-            'bid': buy_price - 0.01,
-            'ask': buy_price - 0.01,
-            'last': buy_price - 0.01
+            'bid': enter_price - 0.01,
+            'ask': enter_price - 0.01,
+            'last': enter_price - 0.01
         }),
         create_order=MagicMock(side_effect=[
-            limit_buy_order_usdt_open,
+            limit_order_open[enter_side(is_short)],
             {'id': 1234553382},
         ]),
         get_fee=fee,
@@ -3581,7 +3559,7 @@ def test_trailing_stop_loss_positive(
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
-    trade.update(limit_buy_order_usdt)
+    trade.update(limit_order[enter_side(is_short)])
     caplog.set_level(logging.DEBUG)
     # stop-loss not reached
     assert freqtrade.handle_trade(trade) is False
@@ -3590,9 +3568,9 @@ def test_trailing_stop_loss_positive(
     mocker.patch(
         'freqtrade.exchange.Exchange.fetch_ticker',
         MagicMock(return_value={
-            'bid': buy_price + 0.06,
-            'ask': buy_price + 0.06,
-            'last': buy_price + 0.06
+            'bid': enter_price + 0.06,
+            'ask': enter_price + 0.06,
+            'last': enter_price + 0.06
         })
     )
     # stop-loss not reached, adjusted stoploss
@@ -3610,9 +3588,9 @@ def test_trailing_stop_loss_positive(
     mocker.patch(
         'freqtrade.exchange.Exchange.fetch_ticker',
         MagicMock(return_value={
-            'bid': buy_price + 0.125,
-            'ask': buy_price + 0.125,
-            'last': buy_price + 0.125,
+            'bid': enter_price + 0.125,
+            'ask': enter_price + 0.125,
+            'last': enter_price + 0.125,
         })
     )
     assert freqtrade.handle_trade(trade) is False
@@ -3625,23 +3603,23 @@ def test_trailing_stop_loss_positive(
     mocker.patch(
         'freqtrade.exchange.Exchange.fetch_ticker',
         MagicMock(return_value={
-            'bid': buy_price + 0.02,
-            'ask': buy_price + 0.02,
-            'last': buy_price + 0.02
+            'bid': enter_price + 0.02,
+            'ask': enter_price + 0.02,
+            'last': enter_price + 0.02
         })
     )
     # Lower price again (but still positive)
     assert freqtrade.handle_trade(trade) is True
     assert log_has(
-        f"ETH/USDT - HIT STOP: current price at {buy_price + 0.02:.6f}, "
+        f"ETH/USDT - HIT STOP: current price at {enter_price + 0.02:.6f}, "
         f"stoploss is {trade.stop_loss:.6f}, "
         f"initial stoploss was at 1.800000, trade opened at 2.000000", caplog)
     assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value
 
 
-# TODO-lev: @pytest.mark.parametrize("is_short", [False, True])
-def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usdt,
-                                          limit_buy_order_usdt_open, fee, mocker) -> None:
+@pytest.mark.parametrize("is_short", [False, True])
+def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_order, limit_order_open,
+                                          is_short, fee, mocker) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
@@ -3652,7 +3630,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usd
             'last': 0.00000172
         }),
         create_order=MagicMock(side_effect=[
-            limit_buy_order_usdt_open,
+            limit_order_open[enter_side(is_short)],
             {'id': 1234553382},
             {'id': 1234553383}
         ]),
@@ -3669,7 +3647,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usd
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
-    trade.update(limit_buy_order_usdt)
+    trade.update(limit_order[enter_side(is_short)])
     # Sell due to min_roi_reached
     patch_get_signal(freqtrade, enter_long=True, exit_long=True)
     assert freqtrade.handle_trade(trade) is True
@@ -3995,7 +3973,7 @@ def test_apply_fee_conditional(default_conf_usdt, fee, mocker,
 ])
 @ pytest.mark.parametrize('is_short', [False, True])
 def test_order_book_depth_of_market(
-    default_conf_usdt, ticker_usdt, limit_buy_order_usdt_open, limit_buy_order_usdt,
+    default_conf_usdt, ticker_usdt, limit_order, limit_order_open,
     fee, mocker, order_book_l2, delta, is_high_delta, is_short
 ):
     default_conf_usdt['bid_strategy']['check_depth_of_market']['enabled'] = True
@@ -4006,7 +3984,7 @@ def test_order_book_depth_of_market(
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
         fetch_ticker=ticker_usdt,
-        create_order=MagicMock(return_value=limit_buy_order_usdt_open),
+        create_order=MagicMock(return_value=limit_order_open[enter_side(is_short)]),
         get_fee=fee,
     )
 
@@ -4029,7 +4007,7 @@ def test_order_book_depth_of_market(
         assert len(Trade.query.all()) == 1
 
         # Simulate fulfilled LIMIT_BUY order for trade
-        trade.update(limit_buy_order_usdt)
+        trade.update(limit_order_open[enter_side(is_short)])
 
         assert trade.open_rate == 2.0
         assert whitelist == default_conf_usdt['exchange']['pair_whitelist']
@@ -4200,15 +4178,15 @@ def test_sync_wallet_dry_run(mocker, default_conf_usdt, ticker_usdt, fee, limit_
 
 @ pytest.mark.usefixtures("init_persistence")
 @ pytest.mark.parametrize("is_short", [False, True])
-def test_cancel_all_open_orders(mocker, default_conf_usdt, fee, limit_buy_order_usdt,
-                                limit_sell_order_usdt, is_short):
+def test_cancel_all_open_orders(mocker, default_conf_usdt, fee, limit_order, limit_order_open,
+                                is_short):
     default_conf_usdt['cancel_open_orders_on_exit'] = True
     mocker.patch('freqtrade.exchange.Exchange.fetch_order',
                  side_effect=[
                      ExchangeError(),
-                     limit_sell_order_usdt,
-                     limit_buy_order_usdt,
-                     limit_sell_order_usdt
+                     limit_order[exit_side(is_short)],
+                     limit_order_open[enter_side(is_short)],
+                     limit_order_open[exit_side(is_short)],
                  ])
     buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_enter')
     sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit')
@@ -4558,4 +4536,6 @@ def test_get_valid_price(mocker, default_conf_usdt) -> None:
     assert valid_price_at_min_alwd < proposed_price
 
 
-# TODO-lev def test_leverage_prep()
+def test_leverage_prep():
+    # TODO-lev
+    return

From d75934ce92bff0b6aec10ff0e3d43bf08de35a5f Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Sun, 3 Oct 2021 04:44:39 -0600
Subject: [PATCH 060/134] 'is_short' -> is_short: test_freqtradebot

---
 tests/test_freqtradebot.py | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 976b402f9..06dc9d0e1 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -2588,8 +2588,8 @@ def test_handle_cancel_enter(mocker, caplog, default_conf_usdt, limit_order,
                              is_short) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
-    l_order = limit_order[enter_side('is_short')]
-    cancel_buy_order = deepcopy(limit_order[enter_side('is_short')])
+    l_order = limit_order[enter_side(is_short)]
+    cancel_buy_order = deepcopy(limit_order[enter_side(is_short)])
     cancel_buy_order['status'] = 'canceled'
     del cancel_buy_order['filled']
 
@@ -2665,7 +2665,7 @@ def test_handle_cancel_enter_corder_empty(mocker, default_conf_usdt, limit_order
                                           cancelorder) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
-    l_order = limit_order[enter_side('is_short')]
+    l_order = limit_order[enter_side(is_short)]
     cancel_order_mock = MagicMock(return_value=cancelorder)
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
@@ -3286,7 +3286,7 @@ def test_sell_profit_only(
             'last': bid
         }),
         create_order=MagicMock(side_effect=[
-            limit_order_open[enter_side['is_short']],
+            limit_order_open[enter_side(is_short)],
             {'id': 1234553382},
         ]),
         get_fee=fee,
@@ -3306,7 +3306,7 @@ def test_sell_profit_only(
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
-    trade.update(limit_order[enter_side('is_short')])
+    trade.update(limit_order[enter_side(is_short)])
     freqtrade.wallets.update()
     patch_get_signal(freqtrade, enter_long=False, exit_long=True)
     assert freqtrade.handle_trade(trade) is handle_first

From 2a2b7594192d1036e810c284e0d777a610ce918e Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Sun, 3 Oct 2021 17:41:01 -0600
Subject: [PATCH 061/134] patch_get_signal test updates

---
 freqtrade/freqtradebot.py  |   6 +-
 tests/conftest.py          |  10 +++-
 tests/test_freqtradebot.py | 119 +++++++++++++++++++------------------
 3 files changed, 75 insertions(+), 60 deletions(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 024ae1996..5e87a02b2 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -986,7 +986,11 @@ class FreqtradeBot(LoggingMixin):
         Check and execute trade exit
         """
         should_exit: SellCheckTuple = self.strategy.should_exit(
-            trade, exit_rate, datetime.now(timezone.utc), enter=enter, exit_=exit_,
+            trade,
+            exit_rate,
+            datetime.now(timezone.utc),
+            enter=enter,
+            exit_=exit_,
             force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
         )
 
diff --git a/tests/conftest.py b/tests/conftest.py
index 0e3f2aebb..a0d6148db 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -217,8 +217,14 @@ def get_patched_worker(mocker, config) -> Worker:
     return Worker(args=None, config=config)
 
 
-def patch_get_signal(freqtrade: FreqtradeBot, enter_long=True, exit_long=False,
-                     enter_short=False, exit_short=False, enter_tag: Optional[str] = None) -> None:
+def patch_get_signal(
+    freqtrade: FreqtradeBot,
+    enter_long=True,
+    exit_long=False,
+    enter_short=False,
+    exit_short=False,
+    enter_tag: Optional[str] = None
+) -> None:
     """
     :param mocker: mocker to patch IStrategy class
     :param value: which value IStrategy.get_signal() must return
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 06dc9d0e1..71b2eabb3 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -203,12 +203,11 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None:
         'LTC/BTC', freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21
 
 
-@pytest.mark.parametrize('is_short', [False, True])
-@pytest.mark.parametrize('buy_price_mult,ignore_strat_sl', [
-    # Override stoploss
-    (0.79, False),
-    # Override strategy stoploss
-    (0.85, True)
+@pytest.mark.parametrize('buy_price_mult,ignore_strat_sl,is_short', [
+    (0.79, False, False),   # Override stoploss
+    (0.85, True, False),    # Override strategy stoploss
+    (0.85, False, True),    # Override stoploss
+    (0.79, True, True)      # Override strategy stoploss
 ])
 def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker, is_short,
                                  buy_price_mult, ignore_strat_sl, edge_conf) -> None:
@@ -220,7 +219,7 @@ def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker, is_short,
     # Strategy stoploss is -0.1 but Edge imposes a stoploss at -0.2
     # Thus, if price falls 21%, stoploss should be triggered
     #
-    # mocking the ticker_usdt: price is falling ...
+    # mocking the ticker: price is falling ...
     enter_price = limit_order[enter_side(is_short)]['price']
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
@@ -236,10 +235,12 @@ def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker, is_short,
     # Create a trade with "limit_buy_order_usdt" price
     freqtrade = FreqtradeBot(edge_conf)
     freqtrade.active_pair_whitelist = ['NEO/BTC']
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
     freqtrade.enter_positions()
     trade = Trade.query.first()
+    trade.is_short = is_short
+    caplog.clear()
     trade.update(limit_order[enter_side(is_short)])
     #############################################
 
@@ -253,7 +254,6 @@ def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker, is_short,
 def test_total_open_trades_stakes(mocker, default_conf_usdt, ticker_usdt, fee) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
-    default_conf_usdt['stake_amount'] = 10.0
     default_conf_usdt['max_open_trades'] = 2
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
@@ -300,7 +300,7 @@ def test_create_trade(default_conf_usdt, ticker_usdt, limit_order,
     # Save state of current whitelist
     whitelist = deepcopy(default_conf_usdt['exchange']['pair_whitelist'])
     freqtrade = FreqtradeBot(default_conf_usdt)
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     freqtrade.create_trade('ETH/USDT')
 
     trade = Trade.query.first()
@@ -359,7 +359,7 @@ def test_create_trade_minimal_amount(
     default_conf_usdt['max_open_trades'] = max_open_trades
     freqtrade = FreqtradeBot(default_conf_usdt)
     freqtrade.config['stake_amount'] = stake_amount
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
 
     if create:
         assert freqtrade.create_trade('ETH/USDT')
@@ -550,7 +550,7 @@ def test_process_trade_creation(default_conf_usdt, ticker_usdt, limit_order, lim
         get_fee=fee,
     )
     freqtrade = FreqtradeBot(default_conf_usdt)
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
 
     trades = Trade.query.filter(Trade.is_open.is_(True)).all()
     assert not trades
@@ -985,7 +985,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
         stoploss=stoploss
     )
     freqtrade = FreqtradeBot(default_conf_usdt)
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
 
     # First case: when stoploss is not yet set but the order is open
     # should get the stoploss order id immediately
@@ -1111,7 +1111,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf_usdt, fee, caplog,
         stoploss=MagicMock(side_effect=ExchangeError()),
     )
     freqtrade = FreqtradeBot(default_conf_usdt)
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
 
     freqtrade.enter_positions()
     trade = Trade.query.first()
@@ -1155,7 +1155,7 @@ def test_create_stoploss_order_invalid_order(
         stoploss=MagicMock(side_effect=InvalidOrderException()),
     )
     freqtrade = FreqtradeBot(default_conf_usdt)
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     freqtrade.strategy.order_types['stoploss_on_exchange'] = True
 
     freqtrade.enter_positions()
@@ -1207,7 +1207,7 @@ def test_create_stoploss_order_insufficient_funds(
         'freqtrade.exchange.Binance',
         stoploss=MagicMock(side_effect=InsufficientFundsError()),
     )
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     freqtrade.strategy.order_types['stoploss_on_exchange'] = True
 
     freqtrade.enter_positions()
@@ -1227,10 +1227,14 @@ def test_create_stoploss_order_insufficient_funds(
     assert mock_insuf.call_count == 1
 
 
-@pytest.mark.parametrize("is_short", [False, True])
+@pytest.mark.parametrize("is_short,bid,ask,stop_price,amt,hang_price", [
+    (False, [4.38, 4.16], [4.4, 4.17], ['2.0805', 4.4 * 0.95], 27.39726027, 3),
+    (True, [1.09, 1.21], [1.1, 1.22], ['2.321', 1.1 * 0.95], 27.39726027, 1.5),
+])
 @pytest.mark.usefixtures("init_persistence")
-def test_handle_stoploss_on_exchange_trailing(mocker, default_conf_usdt, fee, is_short,
-                                              limit_order) -> None:
+def test_handle_stoploss_on_exchange_trailing(
+    mocker, default_conf_usdt, fee, is_short, bid, ask, limit_order, stop_price, amt, hang_price
+) -> None:
     # TODO-lev: test for short
     # When trailing stoploss is set
     enter_order = limit_order[enter_side(is_short)]
@@ -1242,7 +1246,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf_usdt, fee, is
         fetch_ticker=MagicMock(return_value={
             'bid': 2.19,
             'ask': 2.2,
-            'last': 2.19
+            'last': 2.19,
         }),
         create_order=MagicMock(side_effect=[
             {'id': enter_order['id']},
@@ -1268,28 +1272,28 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf_usdt, fee, is
     freqtrade.strategy.order_types['stoploss_on_exchange'] = True
 
     # setting stoploss
-    freqtrade.strategy.stoploss = -0.05
+    freqtrade.strategy.stoploss = 0.05 if is_short else -0.05
 
     # setting stoploss_on_exchange_interval to 60 seconds
     freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60
 
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
 
     freqtrade.enter_positions()
     trade = Trade.query.first()
+    trade.is_short = is_short
     trade.is_open = True
     trade.open_order_id = None
     trade.stoploss_order_id = 100
-    trade.is_short = is_short
 
     stoploss_order_hanging = MagicMock(return_value={
         'id': 100,
         'status': 'open',
         'type': 'stop_loss_limit',
-        'price': 3,
+        'price': hang_price,
         'average': 2,
         'info': {
-            'stopPrice': '2.0805'
+            'stopPrice': stop_price[0]
         }
     })
 
@@ -1303,9 +1307,9 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf_usdt, fee, is
     mocker.patch(
         'freqtrade.exchange.Exchange.fetch_ticker',
         MagicMock(return_value={
-            'bid': 4.38,
-            'ask': 4.4,
-            'last': 4.38
+            'bid': bid[0],
+            'ask': ask[0],
+            'last': bid[0],
         })
     )
 
@@ -1321,7 +1325,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf_usdt, fee, is
     stoploss_order_mock.assert_not_called()
 
     assert freqtrade.handle_trade(trade) is False
-    assert trade.stop_loss == 4.4 * 0.95
+    assert trade.stop_loss == stop_price[1]
 
     # setting stoploss_on_exchange_interval to 0 seconds
     freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0
@@ -1333,7 +1337,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf_usdt, fee, is
         amount=27.39726027,
         pair='ETH/USDT',
         order_types=freqtrade.strategy.order_types,
-        stop_price=4.4 * 0.95,
+        stop_price=stop_price[1],
         side=exit_side(is_short),
         leverage=1.0
     )
@@ -1342,9 +1346,9 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf_usdt, fee, is
     mocker.patch(
         'freqtrade.exchange.Exchange.fetch_ticker',
         MagicMock(return_value={
-            'bid': 4.16,
-            'ask': 4.17,
-            'last': 4.16
+            'bid': bid[1],
+            'ask': ask[1],
+            'last': bid[1],
         })
     )
     assert freqtrade.handle_trade(trade) is True
@@ -1387,11 +1391,11 @@ def test_handle_stoploss_on_exchange_trailing_error(
     freqtrade.strategy.order_types['stoploss_on_exchange'] = True
 
     # setting stoploss
-    freqtrade.strategy.stoploss = -0.05
+    freqtrade.strategy.stoploss = 0.05 if is_short else -0.05
 
     # setting stoploss_on_exchange_interval to 60 seconds
     freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     freqtrade.enter_positions()
     trade = Trade.query.first()
     trade.is_open = True
@@ -1477,10 +1481,11 @@ def test_handle_stoploss_on_exchange_custom_stop(
     # setting stoploss_on_exchange_interval to 60 seconds
     freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 60
 
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
 
     freqtrade.enter_positions()
     trade = Trade.query.first()
+    trade.is_short = is_short
     trade.is_open = True
     trade.open_order_id = None
     trade.stoploss_order_id = 100
@@ -1602,7 +1607,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, is
     # setting stoploss_on_exchange_interval to 0 seconds
     freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0
 
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
 
     freqtrade.active_pair_whitelist = freqtrade.edge.adjust(freqtrade.active_pair_whitelist)
 
@@ -1941,7 +1946,7 @@ def test_handle_trade(
         get_fee=fee,
     )
     freqtrade = FreqtradeBot(default_conf_usdt)
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
 
     freqtrade.enter_positions()
 
@@ -1996,7 +2001,7 @@ def test_handle_overlapping_signals(
     assert nb_trades == 0
 
     # Buy is triggering, so buying ...
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     freqtrade.enter_positions()
     trades = Trade.query.all()
     for trade in trades:
@@ -2053,7 +2058,7 @@ def test_handle_trade_roi(default_conf_usdt, ticker_usdt, limit_order_open, fee,
     )
 
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     freqtrade.strategy.min_roi_reached = MagicMock(return_value=True)
 
     freqtrade.enter_positions()
@@ -2097,7 +2102,7 @@ def test_handle_trade_use_sell_signal(
     )
 
     freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
     freqtrade.enter_positions()
 
@@ -2132,7 +2137,7 @@ def test_close_trade(
         get_fee=fee,
     )
     freqtrade = FreqtradeBot(default_conf_usdt)
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
 
     # Create trade and sell it
     freqtrade.enter_positions()
@@ -2766,7 +2771,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
     )
     patch_whitelist(mocker, default_conf_usdt)
     freqtrade = FreqtradeBot(default_conf_usdt)
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=False)
 
     # Create some test data
@@ -2833,7 +2838,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd
     )
     patch_whitelist(mocker, default_conf_usdt)
     freqtrade = FreqtradeBot(default_conf_usdt)
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
 
     # Create some test data
     freqtrade.enter_positions()
@@ -2889,7 +2894,7 @@ def test_execute_trade_exit_custom_exit_price(default_conf_usdt, ticker_usdt, fe
     config['custom_price_max_distance_ratio'] = 0.1
     patch_whitelist(mocker, config)
     freqtrade = FreqtradeBot(config)
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=False)
 
     # Create some test data
@@ -2955,7 +2960,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
     )
     patch_whitelist(mocker, default_conf_usdt)
     freqtrade = FreqtradeBot(default_conf_usdt)
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
 
     # Create some test data
     freqtrade.enter_positions()
@@ -3066,7 +3071,7 @@ def test_execute_trade_exit_with_stoploss_on_exchange(
 
     freqtrade = FreqtradeBot(default_conf_usdt)
     freqtrade.strategy.order_types['stoploss_on_exchange'] = True
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
 
     # Create some test data
     freqtrade.enter_positions()
@@ -3179,7 +3184,7 @@ def test_execute_trade_exit_market_order(default_conf_usdt, ticker_usdt, fee, is
     )
     patch_whitelist(mocker, default_conf_usdt)
     freqtrade = FreqtradeBot(default_conf_usdt)
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
 
     # Create some test data
     freqtrade.enter_positions()
@@ -3240,7 +3245,7 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_u
             InsufficientFundsError(),
         ]),
     )
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
 
     # Create some test data
     freqtrade.enter_positions()
@@ -3297,7 +3302,7 @@ def test_sell_profit_only(
         'sell_profit_offset': 0.1,
     })
     freqtrade = FreqtradeBot(default_conf_usdt)
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     if sell_type == SellType.SELL_SIGNAL.value:
         freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
     else:
@@ -3336,7 +3341,7 @@ def test_sell_not_enough_balance(default_conf_usdt, limit_order, limit_order_ope
     )
 
     freqtrade = FreqtradeBot(default_conf_usdt)
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
 
     freqtrade.enter_positions()
@@ -3400,7 +3405,7 @@ def test_locked_pairs(default_conf_usdt, ticker_usdt, fee,
         get_fee=fee,
     )
     freqtrade = FreqtradeBot(default_conf_usdt)
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
 
     # Create some test data
     freqtrade.enter_positions()
@@ -3448,7 +3453,7 @@ def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_order, limit_order_op
     default_conf_usdt['ignore_roi_if_buy_signal'] = True
 
     freqtrade = FreqtradeBot(default_conf_usdt)
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     freqtrade.strategy.min_roi_reached = MagicMock(return_value=True)
 
     freqtrade.enter_positions()
@@ -3486,7 +3491,7 @@ def test_trailing_stop_loss(default_conf_usdt, limit_order_open,
     default_conf_usdt['trailing_stop'] = True
     patch_whitelist(mocker, default_conf_usdt)
     freqtrade = FreqtradeBot(default_conf_usdt)
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
 
     freqtrade.enter_positions()
@@ -3554,7 +3559,7 @@ def test_trailing_stop_loss_positive(
     patch_whitelist(mocker, default_conf_usdt)
 
     freqtrade = FreqtradeBot(default_conf_usdt)
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
     freqtrade.enter_positions()
 
@@ -3641,7 +3646,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_order, limit_
         'ignore_roi_if_buy_signal': False
     }
     freqtrade = FreqtradeBot(default_conf_usdt)
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     freqtrade.strategy.min_roi_reached = MagicMock(return_value=True)
 
     freqtrade.enter_positions()
@@ -3991,7 +3996,7 @@ def test_order_book_depth_of_market(
     # Save state of current whitelist
     whitelist = deepcopy(default_conf_usdt['exchange']['pair_whitelist'])
     freqtrade = FreqtradeBot(default_conf_usdt)
-    patch_get_signal(freqtrade)
+    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     freqtrade.enter_positions()
 
     trade = Trade.query.first()

From 9046caa27c24d7c487ba7029544e51c7bf90d753 Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Sun, 3 Oct 2021 23:13:34 -0600
Subject: [PATCH 062/134] fixed test_update_trade_state_sell

---
 freqtrade/exchange/exchange.py | 10 ++++++++--
 tests/test_freqtradebot.py     |  3 ++-
 2 files changed, 10 insertions(+), 3 deletions(-)

diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index bb31b84f2..ee0c1600c 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -800,8 +800,14 @@ class Exchange:
             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)
+            order = self._api.create_order(
+                pair,
+                ordertype,
+                side,
+                amount,
+                rate_for_order,
+                params
+            )
             self._log_exchange_response('create_order', order)
             return order
 
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 71b2eabb3..07b1108b7 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -717,7 +717,7 @@ def test_process_informative_pairs_added(default_conf_usdt, ticker_usdt, mocker)
     assert ("ETH/USDT", default_conf_usdt["timeframe"]) in refresh_mock.call_args[0][0]
 
 
-@pytest.mark.parametrize("is_short", [True, False])
+@pytest.mark.parametrize("is_short", [False, True])
 def test_execute_entry(mocker, default_conf_usdt, fee, limit_order,
                        limit_order_open, is_short) -> None:
 
@@ -1909,6 +1909,7 @@ def test_update_trade_state_sell(
         open_date=arrow.utcnow().datetime,
         open_order_id="123456",
         is_open=True,
+        interest_rate=0.0005,
         is_short=is_short
     )
     order = Order.parse_from_ccxt_object(open_order, 'LTC/ETH', (enter_side(is_short)))

From 928c4edace8d07dfd712ce1fc9e72e0c2e07c800 Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Sun, 3 Oct 2021 23:22:51 -0600
Subject: [PATCH 063/134] removed side from execute_trade_exit

---
 freqtrade/freqtradebot.py  |  5 ++---
 freqtrade/rpc/rpc.py       |  2 +-
 tests/test_freqtradebot.py | 20 ++++++++++----------
 3 files changed, 13 insertions(+), 14 deletions(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 5e87a02b2..5142af5e3 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -869,7 +869,7 @@ class FreqtradeBot(LoggingMixin):
             logger.error(f'Unable to place a stoploss order on exchange. {e}')
             logger.warning('Exiting the trade forcefully')
             self.execute_trade_exit(trade, trade.stop_loss, sell_reason=SellCheckTuple(
-                sell_type=SellType.EMERGENCY_SELL), side=trade.exit_side)
+                sell_type=SellType.EMERGENCY_SELL))
 
         except ExchangeError:
             trade.stoploss_order_id = None
@@ -996,7 +996,7 @@ class FreqtradeBot(LoggingMixin):
 
         if should_exit.sell_flag:
             logger.info(f'Exit for {trade.pair} detected. Reason: {should_exit.sell_type}')
-            self.execute_trade_exit(trade, exit_rate, should_exit, side=trade.exit_side)
+            self.execute_trade_exit(trade, exit_rate, should_exit)
             return True
         return False
 
@@ -1227,7 +1227,6 @@ class FreqtradeBot(LoggingMixin):
         trade: Trade,
         limit: float,
         sell_reason: SellCheckTuple,  # TODO-lev update to exit_reason
-        side: str
     ) -> bool:
         """
         Executes a trade exit for the given trade and limit
diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
index af850a89f..b50f90de8 100644
--- a/freqtrade/rpc/rpc.py
+++ b/freqtrade/rpc/rpc.py
@@ -574,7 +574,7 @@ class RPC:
                 current_rate = self._freqtrade.exchange.get_rate(
                     trade.pair, refresh=False, side="sell")
                 sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
-                self._freqtrade.execute_trade_exit(trade, current_rate, sell_reason, side="sell")
+                self._freqtrade.execute_trade_exit(trade, current_rate, sell_reason)
         # ---- EOF def _exec_forcesell ----
 
         if self._freqtrade.state != State.RUNNING:
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 07b1108b7..4a4e7b69f 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -2790,7 +2790,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
     )
     # Prevented sell ...
     # TODO-lev: side="buy"
-    freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['bid'], side="sell",
+    freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['bid'],
                                  sell_reason=SellCheckTuple(sell_type=SellType.ROI))
     assert rpc_mock.call_count == 0
     assert freqtrade.strategy.confirm_trade_exit.call_count == 1
@@ -2798,7 +2798,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
     # Repatch with true
     freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True)
     # TODO-lev: side="buy"
-    freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['bid'], side="sell",
+    freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['bid'],
                                  sell_reason=SellCheckTuple(sell_type=SellType.ROI))
     assert freqtrade.strategy.confirm_trade_exit.call_count == 1
 
@@ -2853,7 +2853,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd
         fetch_ticker=ticker_usdt_sell_down
     )
     # TODO-lev: side="buy"
-    freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_down()['bid'], side="sell",
+    freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_down()['bid'],
                                  sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
 
     assert rpc_mock.call_count == 2
@@ -2917,7 +2917,7 @@ def test_execute_trade_exit_custom_exit_price(default_conf_usdt, ticker_usdt, fe
     # Set a custom exit price
     freqtrade.strategy.custom_exit_price = lambda **kwargs: 2.25
     # TODO-lev: side="buy"
-    freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['bid'], side="sell",
+    freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['bid'],
                                  sell_reason=SellCheckTuple(sell_type=SellType.SELL_SIGNAL))
 
     # Sell price must be different to default bid price
@@ -2981,7 +2981,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
 
     trade.stop_loss = 2.0 * 0.99
     # TODO-lev: side="buy"
-    freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_down()['bid'], side="sell",
+    freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_down()['bid'],
                                  sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
 
     assert rpc_mock.call_count == 2
@@ -3038,7 +3038,7 @@ def test_execute_trade_exit_sloe_cancel_exception(
     trade.stoploss_order_id = "abcd"
 
     # TODO-lev: side="buy"
-    freqtrade.execute_trade_exit(trade=trade, limit=1234, side="sell",
+    freqtrade.execute_trade_exit(trade=trade, limit=1234,
                                  sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
     assert create_order_mock.call_count == 2
     assert log_has('Could not cancel stoploss order abcd', caplog)
@@ -3091,7 +3091,7 @@ def test_execute_trade_exit_with_stoploss_on_exchange(
     )
 
     # TODO-lev: side="buy"
-    freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['bid'], side="sell",
+    freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['bid'],
                                  sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
 
     trade = Trade.query.first()
@@ -3201,7 +3201,7 @@ def test_execute_trade_exit_market_order(default_conf_usdt, ticker_usdt, fee, is
     freqtrade.config['order_types']['sell'] = 'market'
 
     # TODO-lev: side="buy"
-    freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['bid'], side="sell",
+    freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['bid'],
                                  sell_reason=SellCheckTuple(sell_type=SellType.ROI))
 
     assert not trade.is_open
@@ -3263,7 +3263,7 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_u
     sell_reason = SellCheckTuple(sell_type=SellType.ROI)
     # TODO-lev: side="buy"
     assert not freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['bid'],
-                                            sell_reason=sell_reason, side="sell")
+                                            sell_reason=sell_reason)
     assert mock_insuf.call_count == 1
 
 
@@ -3421,7 +3421,7 @@ def test_locked_pairs(default_conf_usdt, ticker_usdt, fee,
     )
 
     # TODO-lev: side="buy"
-    freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_down()['bid'], side="sell",
+    freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_down()['bid'],
                                  sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
     trade.close(ticker_usdt_sell_down()['bid'])
     assert freqtrade.strategy.is_pair_locked(trade.pair)

From 07750518c3e50711a0b8965f2535537b07402ba5 Mon Sep 17 00:00:00 2001
From: Sergey Khliustin 
Date: Mon, 4 Oct 2021 18:49:57 +0300
Subject: [PATCH 064/134] Added min_profit param to PerformanceFilter

---
 freqtrade/plugins/pairlist/PerformanceFilter.py | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py
index 301ee57ab..f235816b8 100644
--- a/freqtrade/plugins/pairlist/PerformanceFilter.py
+++ b/freqtrade/plugins/pairlist/PerformanceFilter.py
@@ -21,6 +21,7 @@ class PerformanceFilter(IPairList):
         super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
 
         self._minutes = pairlistconfig.get('minutes', 0)
+        self._min_profit = pairlistconfig.get('min_profit', None)
 
     @property
     def needstickers(self) -> bool:
@@ -68,6 +69,8 @@ class PerformanceFilter(IPairList):
         sorted_df = list_df.merge(performance, on='pair', how='left')\
             .fillna(0).sort_values(by=['count', 'pair'], ascending=True)\
             .sort_values(by=['profit'], ascending=False)
+        if self._min_profit is not None:
+            sorted_df = sorted_df[sorted_df['profit'] >= self._min_profit]
         pairlist = sorted_df['pair'].tolist()
 
         return pairlist

From c72aac43568f1c26e080ec7104c48b7512e6987b Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Tue, 5 Oct 2021 02:13:29 -0600
Subject: [PATCH 065/134] Added trade.is_short = is_short a lot

---
 tests/test_freqtradebot.py | 5 +++++
 1 file changed, 5 insertions(+)

diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 4a4e7b69f..c25a010b1 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -3312,6 +3312,7 @@ def test_sell_profit_only(
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
+    trade.is_short = is_short
     trade.update(limit_order[enter_side(is_short)])
     freqtrade.wallets.update()
     patch_get_signal(freqtrade, enter_long=False, exit_long=True)
@@ -3565,6 +3566,7 @@ def test_trailing_stop_loss_positive(
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
+    trade.is_short = is_short
     trade.update(limit_order[enter_side(is_short)])
     caplog.set_level(logging.DEBUG)
     # stop-loss not reached
@@ -3653,6 +3655,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_order, limit_
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
+    trade.is_short = is_short
     trade.update(limit_order[enter_side(is_short)])
     # Sell due to min_roi_reached
     patch_get_signal(freqtrade, enter_long=True, exit_long=True)
@@ -4001,6 +4004,7 @@ def test_order_book_depth_of_market(
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
+    trade.is_short = is_short
     if is_high_delta:
         assert trade is None
     else:
@@ -4104,6 +4108,7 @@ def test_order_book_ask_strategy(
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
+    trade.is_short = is_short
     assert trade
 
     time.sleep(0.01)  # Race condition fix

From d8ba3d8cde1ebe26ce3b76f9a99a1dc56acc6dad Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Tue, 5 Oct 2021 02:16:17 -0600
Subject: [PATCH 066/134] Added trade.is_short = is_short a lot

---
 tests/test_freqtradebot.py | 21 ++++++++++++++++++++-
 1 file changed, 20 insertions(+), 1 deletion(-)

diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index c25a010b1..fa0748dc4 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -1034,6 +1034,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf_usdt, fee, caplog, is_
     caplog.clear()
     freqtrade.enter_positions()
     trade = Trade.query.first()
+    trade.is_short = is_short
     trade.is_open = True
     trade.open_order_id = None
     trade.stoploss_order_id = 100
@@ -1398,6 +1399,7 @@ def test_handle_stoploss_on_exchange_trailing_error(
     patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
     freqtrade.enter_positions()
     trade = Trade.query.first()
+    trade.is_short = is_short
     trade.is_open = True
     trade.open_order_id = None
     trade.stoploss_order_id = "abcd"
@@ -1613,6 +1615,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, is
 
     freqtrade.enter_positions()
     trade = Trade.query.first()
+    trade.is_short = is_short
     trade.is_open = True
     trade.open_order_id = None
     trade.stoploss_order_id = 100
@@ -1952,6 +1955,7 @@ def test_handle_trade(
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
+    trade.is_short = is_short
     assert trade
 
     time.sleep(0.01)  # Race condition fix
@@ -2108,6 +2112,7 @@ def test_handle_trade_use_sell_signal(
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
+    trade.is_short = is_short
     trade.is_open = True
 
     # TODO-lev: patch for short
@@ -2144,6 +2149,7 @@ def test_close_trade(
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
+    trade.is_short = is_short
     assert trade
 
     trade.update(enter_order)
@@ -2780,6 +2786,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
     rpc_mock.reset_mock()
 
     trade = Trade.query.first()
+    trade.is_short = is_short
     assert trade
     assert freqtrade.strategy.confirm_trade_exit.call_count == 0
 
@@ -2845,6 +2852,7 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
+    trade.is_short = is_short
     assert trade
 
     # Decrease the price and sell it
@@ -2903,6 +2911,7 @@ def test_execute_trade_exit_custom_exit_price(default_conf_usdt, ticker_usdt, fe
     rpc_mock.reset_mock()
 
     trade = Trade.query.first()
+    trade.is_short = is_short
     assert trade
     assert freqtrade.strategy.confirm_trade_exit.call_count == 0
 
@@ -2967,6 +2976,7 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
+    trade.is_short = is_short
     assert trade
 
     # Decrease the price and sell it
@@ -3078,6 +3088,7 @@ def test_execute_trade_exit_with_stoploss_on_exchange(
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
+    trade.is_short = is_short
     assert trade
     trades = [trade]
 
@@ -3095,6 +3106,7 @@ def test_execute_trade_exit_with_stoploss_on_exchange(
                                  sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
 
     trade = Trade.query.first()
+    trade.is_short = is_short
     assert trade
     assert cancel_order.call_count == 1
     assert rpc_mock.call_count == 3
@@ -3131,6 +3143,7 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(default_conf_usdt
     freqtrade.enter_positions()
     freqtrade.check_handle_timedout()
     trade = Trade.query.first()
+    trade.is_short = is_short
     trades = [trade]
     assert trade.stoploss_order_id is None
 
@@ -3191,6 +3204,7 @@ def test_execute_trade_exit_market_order(default_conf_usdt, ticker_usdt, fee, is
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
+    trade.is_short = is_short
     assert trade
 
     # Increase the price and sell it
@@ -3252,6 +3266,7 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_u
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
+    trade.is_short = is_short
     assert trade
 
     # Increase the price and sell it
@@ -3349,6 +3364,7 @@ def test_sell_not_enough_balance(default_conf_usdt, limit_order, limit_order_ope
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
+    trade.is_short = is_short
     amnt = trade.amount
     trade.update(limit_order[enter_side(is_short)])
     patch_get_signal(freqtrade, enter_long=False, exit_long=True)
@@ -3413,6 +3429,7 @@ def test_locked_pairs(default_conf_usdt, ticker_usdt, fee,
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
+    trade.is_short = is_short
     assert trade
 
     # Decrease the price and sell it
@@ -3461,6 +3478,7 @@ def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_order, limit_order_op
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
+    trade.is_short = is_short
     trade.update(limit_order[enter_side(is_short)])
     freqtrade.wallets.update()
     patch_get_signal(freqtrade, enter_long=True, exit_long=True)
@@ -3498,6 +3516,7 @@ def test_trailing_stop_loss(default_conf_usdt, limit_order_open,
 
     freqtrade.enter_positions()
     trade = Trade.query.first()
+    trade.is_short = is_short
     assert freqtrade.handle_trade(trade) is False
 
     # Raise ticker_usdt above buy price
@@ -4108,7 +4127,6 @@ def test_order_book_ask_strategy(
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
-    trade.is_short = is_short
     assert trade
 
     time.sleep(0.01)  # Race condition fix
@@ -4221,6 +4239,7 @@ def test_check_for_open_trades(mocker, default_conf_usdt, fee, is_short):
 
     create_mock_trades(fee, is_short)
     trade = Trade.query.first()
+    trade.is_short = is_short
     trade.is_open = True
 
     freqtrade.check_for_open_trades()

From 362c29c315eafea1a8fc56944b651798c6a7c28d Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Tue, 5 Oct 2021 03:15:28 -0600
Subject: [PATCH 067/134] Added patch_get_signal(freqtrade, enter_long=False,
 enter_short=True, exit_short=True) a bunch

---
 tests/test_freqtradebot.py | 54 +++++++++++++++++++++++++++++---------
 1 file changed, 41 insertions(+), 13 deletions(-)

diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index fa0748dc4..495a75c2d 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -1963,7 +1963,10 @@ def test_handle_trade(
     assert trade.is_open is True
     freqtrade.wallets.update()
 
-    patch_get_signal(freqtrade, enter_long=False, exit_long=True)
+    if is_short:
+        patch_get_signal(freqtrade, enter_long=False, exit_short=True)
+    else:
+        patch_get_signal(freqtrade, enter_long=False, exit_long=True)
     assert freqtrade.handle_trade(trade) is True
     assert trade.open_order_id == exit_order['id']
 
@@ -1994,7 +1997,10 @@ def test_handle_overlapping_signals(
     )
 
     freqtrade = FreqtradeBot(default_conf_usdt)
-    patch_get_signal(freqtrade, enter_long=True, exit_long=True)
+    if is_short:
+        patch_get_signal(freqtrade, enter_long=False, enter_short=True, exit_short=True)
+    else:
+        patch_get_signal(freqtrade, enter_long=True, exit_long=True)
     freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
 
     freqtrade.enter_positions()
@@ -2026,7 +2032,10 @@ def test_handle_overlapping_signals(
     assert trades[0].is_open is True
 
     # Buy and Sell are triggering, so doing nothing ...
-    patch_get_signal(freqtrade, enter_long=True, exit_long=True)
+    if is_short:
+        patch_get_signal(freqtrade, enter_long=False, enter_short=True, exit_short=True)
+    else:
+        patch_get_signal(freqtrade, enter_long=True, exit_long=True)
     assert freqtrade.handle_trade(trades[0]) is False
     trades = Trade.query.all()
     for trade in trades:
@@ -2036,7 +2045,10 @@ def test_handle_overlapping_signals(
     assert trades[0].is_open is True
 
     # Sell is triggering, guess what : we are Selling!
-    patch_get_signal(freqtrade, enter_long=False, exit_long=True)
+    if is_short:
+        patch_get_signal(freqtrade, enter_long=False, exit_short=True)
+    else:
+        patch_get_signal(freqtrade, enter_long=False, exit_long=True)
     trades = Trade.query.all()
     for trade in trades:
         trade.is_short = is_short
@@ -2115,12 +2127,13 @@ def test_handle_trade_use_sell_signal(
     trade.is_short = is_short
     trade.is_open = True
 
-    # TODO-lev: patch for short
     patch_get_signal(freqtrade, enter_long=False, exit_long=False)
     assert not freqtrade.handle_trade(trade)
 
-    # TODO-lev: patch for short
-    patch_get_signal(freqtrade, enter_long=False, exit_long=True)
+    if is_short:
+        patch_get_signal(freqtrade, enter_long=False, exit_short=True)
+    else:
+        patch_get_signal(freqtrade, enter_long=False, exit_long=True)
     assert freqtrade.handle_trade(trade)
     assert log_has("ETH/USDT - Sell signal received. sell_type=SellType.SELL_SIGNAL",
                    caplog)
@@ -3143,7 +3156,6 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(default_conf_usdt
     freqtrade.enter_positions()
     freqtrade.check_handle_timedout()
     trade = Trade.query.first()
-    trade.is_short = is_short
     trades = [trade]
     assert trade.stoploss_order_id is None
 
@@ -3481,11 +3493,18 @@ def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_order, limit_order_op
     trade.is_short = is_short
     trade.update(limit_order[enter_side(is_short)])
     freqtrade.wallets.update()
-    patch_get_signal(freqtrade, enter_long=True, exit_long=True)
+    if is_short:
+        patch_get_signal(freqtrade, enter_long=False, enter_short=True, exit_short=True)
+    else:
+        patch_get_signal(freqtrade, enter_long=True, exit_long=True)
+
     assert freqtrade.handle_trade(trade) is False
 
     # Test if buy-signal is absent (should sell due to roi = true)
-    patch_get_signal(freqtrade, enter_long=False, exit_long=True)
+    if is_short:
+        patch_get_signal(freqtrade, enter_long=False, exit_short=True)
+    else:
+        patch_get_signal(freqtrade, enter_long=False, exit_long=True)
     assert freqtrade.handle_trade(trade) is True
     assert trade.sell_reason == SellType.ROI.value
 
@@ -3677,11 +3696,17 @@ def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_order, limit_
     trade.is_short = is_short
     trade.update(limit_order[enter_side(is_short)])
     # Sell due to min_roi_reached
-    patch_get_signal(freqtrade, enter_long=True, exit_long=True)
+    if is_short:
+        patch_get_signal(freqtrade, enter_long=False, enter_short=True, exit_short=True)
+    else:
+        patch_get_signal(freqtrade, enter_long=True, exit_long=True)
     assert freqtrade.handle_trade(trade) is True
 
     # Test if buy-signal is absent
-    patch_get_signal(freqtrade, enter_long=False, exit_long=True)
+    if is_short:
+        patch_get_signal(freqtrade, enter_long=False, exit_long=True)
+    else:
+        patch_get_signal(freqtrade, enter_long=False, exit_short=True)
     assert freqtrade.handle_trade(trade) is True
     assert trade.sell_reason == SellType.SELL_SIGNAL.value
 
@@ -4134,7 +4159,10 @@ def test_order_book_ask_strategy(
     freqtrade.wallets.update()
     assert trade.is_open is True
 
-    patch_get_signal(freqtrade, enter_long=False, exit_long=True)
+    if is_short:
+        patch_get_signal(freqtrade, enter_long=False,  exit_short=True)
+    else:
+        patch_get_signal(freqtrade, enter_long=False, exit_long=True)
     assert freqtrade.handle_trade(trade) is True
     assert trade.close_rate_requested == order_book_l2.return_value['asks'][0][0]
 

From 7f7f377a90b38d7465a04e2ccecc4fe28f01cc9b Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Thu, 7 Oct 2021 05:03:38 -0600
Subject: [PATCH 068/134] updated a test, put in TODO-lev

---
 freqtrade/strategy/interface.py |  2 ++
 tests/test_freqtradebot.py      | 40 +++++++++++++++++----------------
 2 files changed, 23 insertions(+), 19 deletions(-)

diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py
index a22a0b6b8..3594346b5 100644
--- a/freqtrade/strategy/interface.py
+++ b/freqtrade/strategy/interface.py
@@ -840,6 +840,7 @@ class IStrategy(ABC, HyperStrategyMixin):
             else:
                 logger.warning("CustomStoploss function did not return valid stoploss")
 
+        # TODO-lev: short
         if self.trailing_stop and trade.stop_loss < (low or current_rate):
             # trailing stoploss handling
             sl_offset = self.trailing_stop_positive_offset
@@ -861,6 +862,7 @@ class IStrategy(ABC, HyperStrategyMixin):
         # evaluate if the stoploss was hit if stoploss is not on exchange
         # in Dry-Run, this handles stoploss logic as well, as the logic will not be different to
         # regular stoploss handling.
+        # TODO-lev: short
         if ((trade.stop_loss >= (low or current_rate)) and
                 (not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])):
 
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 495a75c2d..56aaeadf2 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -3565,11 +3565,13 @@ def test_trailing_stop_loss(default_conf_usdt, limit_order_open,
     assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value
 
 
-@ pytest.mark.parametrize("is_short", [False, True])
-@ pytest.mark.parametrize('offset,trail_if_reached,second_sl', [
-    (0, False, 2.0394),
-    (0.011, False, 2.0394),
-    (0.055, True, 1.8),
+@ pytest.mark.parametrize('offset,trail_if_reached,second_sl,is_short', [
+    # (0, False, 2.0394, False),
+    # (0.011, False, 2.0394, False),
+    # (0.055, True, 1.8, False),
+    (0, False, 2.1606, True),
+    (0.011, False, 2.1606, True),
+    (0.055, True, 2.4, True),
 ])
 def test_trailing_stop_loss_positive(
     default_conf_usdt, limit_order, limit_order_open,
@@ -3581,9 +3583,9 @@ def test_trailing_stop_loss_positive(
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
         fetch_ticker=MagicMock(return_value={
-            'bid': enter_price - 0.01,
-            'ask': enter_price - 0.01,
-            'last': enter_price - 0.01
+            'bid': enter_price - (-0.01 if is_short else 0.01),
+            'ask': enter_price - (-0.01 if is_short else 0.01),
+            'last': enter_price - (-0.01 if is_short else 0.01),
         }),
         create_order=MagicMock(side_effect=[
             limit_order_open[enter_side(is_short)],
@@ -3614,9 +3616,9 @@ def test_trailing_stop_loss_positive(
     mocker.patch(
         'freqtrade.exchange.Exchange.fetch_ticker',
         MagicMock(return_value={
-            'bid': enter_price + 0.06,
-            'ask': enter_price + 0.06,
-            'last': enter_price + 0.06
+            'bid': enter_price + (-0.06 if is_short else 0.06),
+            'ask': enter_price + (-0.06 if is_short else 0.06),
+            'last': enter_price + (-0.06 if is_short else 0.06),
         })
     )
     # stop-loss not reached, adjusted stoploss
@@ -3634,9 +3636,9 @@ def test_trailing_stop_loss_positive(
     mocker.patch(
         'freqtrade.exchange.Exchange.fetch_ticker',
         MagicMock(return_value={
-            'bid': enter_price + 0.125,
-            'ask': enter_price + 0.125,
-            'last': enter_price + 0.125,
+            'bid': enter_price + (-0.125 if is_short else 0.125),
+            'ask': enter_price + (-0.125 if is_short else 0.125),
+            'last': enter_price + (-0.125 if is_short else 0.125),
         })
     )
     assert freqtrade.handle_trade(trade) is False
@@ -3649,17 +3651,17 @@ def test_trailing_stop_loss_positive(
     mocker.patch(
         'freqtrade.exchange.Exchange.fetch_ticker',
         MagicMock(return_value={
-            'bid': enter_price + 0.02,
-            'ask': enter_price + 0.02,
-            'last': enter_price + 0.02
+            'bid': enter_price + (-0.02 if is_short else 0.02),
+            'ask': enter_price + (-0.02 if is_short else 0.02),
+            'last': enter_price + (-0.02 if is_short else 0.02),
         })
     )
     # Lower price again (but still positive)
     assert freqtrade.handle_trade(trade) is True
     assert log_has(
-        f"ETH/USDT - HIT STOP: current price at {enter_price + 0.02:.6f}, "
+        f"ETH/USDT - HIT STOP: current price at {enter_price + (-0.02 if is_short else 0.02):.6f}, "
         f"stoploss is {trade.stop_loss:.6f}, "
-        f"initial stoploss was at 1.800000, trade opened at 2.000000", caplog)
+        f"initial stoploss was at {2.2 if is_short else 1.8}00000, trade opened at 2.000000", caplog)
     assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value
 
 

From d7e6b842babc6bcc6dc829bb8e96fa3bbf39458f Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Sat, 9 Oct 2021 11:24:26 -0600
Subject: [PATCH 069/134] Fixed failing tests test_cancel_all_open_orders,
 test_order_book_ask_strategy, test_order_book_depth_of_market,
 test_disable_ignore_roi_if_buy_signal

---
 tests/test_freqtradebot.py | 48 +++++++++++++++++++-------------------
 1 file changed, 24 insertions(+), 24 deletions(-)

diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 56aaeadf2..d95f8ea9d 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -3566,9 +3566,9 @@ def test_trailing_stop_loss(default_conf_usdt, limit_order_open,
 
 
 @ pytest.mark.parametrize('offset,trail_if_reached,second_sl,is_short', [
-    # (0, False, 2.0394, False),
-    # (0.011, False, 2.0394, False),
-    # (0.055, True, 1.8, False),
+    (0, False, 2.0394, False),
+    (0.011, False, 2.0394, False),
+    (0.055, True, 1.8, False),
     (0, False, 2.1606, True),
     (0.011, False, 2.1606, True),
     (0.055, True, 2.4, True),
@@ -3698,17 +3698,11 @@ def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_order, limit_
     trade.is_short = is_short
     trade.update(limit_order[enter_side(is_short)])
     # Sell due to min_roi_reached
-    if is_short:
-        patch_get_signal(freqtrade, enter_long=False, enter_short=True, exit_short=True)
-    else:
-        patch_get_signal(freqtrade, enter_long=True, exit_long=True)
+    patch_get_signal(freqtrade, enter_long=not is_short, enter_short=is_short, exit_short=is_short)
     assert freqtrade.handle_trade(trade) is True
 
     # Test if buy-signal is absent
-    if is_short:
-        patch_get_signal(freqtrade, enter_long=False, exit_long=True)
-    else:
-        patch_get_signal(freqtrade, enter_long=False, exit_short=True)
+    patch_get_signal(freqtrade, enter_long=False, exit_long=not is_short, exit_short=is_short)
     assert freqtrade.handle_trade(trade) is True
     assert trade.sell_reason == SellType.SELL_SIGNAL.value
 
@@ -4050,10 +4044,10 @@ def test_order_book_depth_of_market(
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
-    trade.is_short = is_short
     if is_high_delta:
         assert trade is None
     else:
+        trade.is_short = is_short
         assert trade is not None
         assert trade.stake_amount == 60.0
         assert trade.is_open
@@ -4122,8 +4116,9 @@ def test_check_depth_of_market(default_conf_usdt, mocker, order_book_l2) -> None
     assert freqtrade._check_depth_of_market('ETH/BTC', conf, side=SignalDirection.LONG) is False
 
 
+@ pytest.mark.parametrize('is_short', [False, True])
 def test_order_book_ask_strategy(
-        default_conf_usdt, limit_buy_order_usdt_open, limit_buy_order_usdt, fee,
+        default_conf_usdt, limit_buy_order_usdt_open, limit_buy_order_usdt, fee, is_short,
         limit_sell_order_usdt_open, mocker, order_book_l2, caplog) -> None:
     """
     test order book ask strategy
@@ -4236,17 +4231,22 @@ def test_sync_wallet_dry_run(mocker, default_conf_usdt, ticker_usdt, fee, limit_
 
 
 @ pytest.mark.usefixtures("init_persistence")
-@ pytest.mark.parametrize("is_short", [False, True])
+@ pytest.mark.parametrize("is_short,buy_calls,sell_calls", [
+    (False, 1, 2),
+    (True, 2, 1),
+])
 def test_cancel_all_open_orders(mocker, default_conf_usdt, fee, limit_order, limit_order_open,
-                                is_short):
+                                is_short, buy_calls, sell_calls):
     default_conf_usdt['cancel_open_orders_on_exit'] = True
-    mocker.patch('freqtrade.exchange.Exchange.fetch_order',
-                 side_effect=[
-                     ExchangeError(),
-                     limit_order[exit_side(is_short)],
-                     limit_order_open[enter_side(is_short)],
-                     limit_order_open[exit_side(is_short)],
-                 ])
+    mocker.patch(
+        'freqtrade.exchange.Exchange.fetch_order',
+        side_effect=[
+            ExchangeError(),
+            limit_order[exit_side(is_short)],
+            limit_order_open[enter_side(is_short)],
+            limit_order_open[exit_side(is_short)],
+        ]
+    )
     buy_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_enter')
     sell_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit')
 
@@ -4255,8 +4255,8 @@ def test_cancel_all_open_orders(mocker, default_conf_usdt, fee, limit_order, lim
     trades = Trade.query.all()
     assert len(trades) == MOCK_TRADE_COUNT
     freqtrade.cancel_all_open_orders()
-    assert buy_mock.call_count == 1
-    assert sell_mock.call_count == 2
+    assert buy_mock.call_count == buy_calls
+    assert sell_mock.call_count == sell_calls
 
 
 @ pytest.mark.usefixtures("init_persistence")

From 729957572b3f7609464e522915ed0df8e5174f07 Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Sat, 9 Oct 2021 14:39:11 -0600
Subject: [PATCH 070/134] updated strategy stop_loss_reached to work for shorts

---
 freqtrade/strategy/interface.py | 39 +++++++++++++++++++++++----------
 1 file changed, 27 insertions(+), 12 deletions(-)

diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py
index 3594346b5..f4784133a 100644
--- a/freqtrade/strategy/interface.py
+++ b/freqtrade/strategy/interface.py
@@ -840,31 +840,40 @@ class IStrategy(ABC, HyperStrategyMixin):
             else:
                 logger.warning("CustomStoploss function did not return valid stoploss")
 
-        # TODO-lev: short
-        if self.trailing_stop and trade.stop_loss < (low or current_rate):
+        if self.trailing_stop and (
+            (trade.stop_loss < (low or current_rate) and not trade.is_short) or
+            (trade.stop_loss > (high or current_rate) and trade.is_short)
+        ):
             # trailing stoploss handling
             sl_offset = self.trailing_stop_positive_offset
 
             # Make sure current_profit is calculated using high for backtesting.
-            # TODO-lev: Check this function - high / low usage must be inversed for short trades!
-            high_profit = current_profit if not high else trade.calc_profit_ratio(high)
+            bound = low if trade.is_short else high
+            bound_profit = current_profit if not bound else trade.calc_profit_ratio(bound)
 
             # Don't update stoploss if trailing_only_offset_is_reached is true.
-            if not (self.trailing_only_offset_is_reached and high_profit < sl_offset):
+            if not (self.trailing_only_offset_is_reached and (
+                (bound_profit < sl_offset and not trade.is_short) or
+                (bound_profit > sl_offset and trade.is_short)
+            )):
                 # Specific handling for trailing_stop_positive
-                if self.trailing_stop_positive is not None and high_profit > sl_offset:
+                if self.trailing_stop_positive is not None and (
+                    (bound_profit > sl_offset and not trade.is_short) or
+                    (bound_profit < sl_offset and trade.is_short)
+                ):
                     stop_loss_value = self.trailing_stop_positive
                     logger.debug(f"{trade.pair} - Using positive stoploss: {stop_loss_value} "
                                  f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%")
 
-                trade.adjust_stop_loss(high or current_rate, stop_loss_value)
+                trade.adjust_stop_loss(bound or current_rate, stop_loss_value)
 
         # evaluate if the stoploss was hit if stoploss is not on exchange
         # in Dry-Run, this handles stoploss logic as well, as the logic will not be different to
         # regular stoploss handling.
-        # TODO-lev: short
-        if ((trade.stop_loss >= (low or current_rate)) and
-                (not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])):
+        if ((
+            (trade.stop_loss >= (low or current_rate) and not trade.is_short) or
+            ((trade.stop_loss <= (high or current_rate) and trade.is_short))
+        ) and (not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])):
 
             sell_type = SellType.STOP_LOSS
 
@@ -872,12 +881,18 @@ class IStrategy(ABC, HyperStrategyMixin):
             if trade.initial_stop_loss != trade.stop_loss:
                 sell_type = SellType.TRAILING_STOP_LOSS
                 logger.debug(
-                    f"{trade.pair} - HIT STOP: current price at {(low or current_rate):.6f}, "
+                    f"{trade.pair} - HIT STOP: current price at "
+                    f"{((high if trade.is_short else low) or current_rate):.6f}, "
                     f"stoploss is {trade.stop_loss:.6f}, "
                     f"initial stoploss was at {trade.initial_stop_loss:.6f}, "
                     f"trade opened at {trade.open_rate:.6f}")
+                new_stoploss = (
+                    trade.stop_loss + trade.initial_stop_loss
+                    if trade.is_short else
+                    trade.stop_loss - trade.initial_stop_loss
+                )
                 logger.debug(f"{trade.pair} - Trailing stop saved "
-                             f"{trade.stop_loss - trade.initial_stop_loss:.6f}")
+                             f"{new_stoploss:.6f}")
 
             return SellCheckTuple(sell_type=sell_type)
 

From 4fc40079751fb9dc60e08a89690f099d839ecbb0 Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Sat, 9 Oct 2021 14:57:10 -0600
Subject: [PATCH 071/134] Fixed failing test_check_handle_timedout_buy

---
 tests/test_freqtradebot.py | 17 ++++++++++++-----
 1 file changed, 12 insertions(+), 5 deletions(-)

diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index d95f8ea9d..a1ca95c46 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -2249,7 +2249,7 @@ def test_check_handle_timedout_buy_usercustom(
 
 @ pytest.mark.parametrize("is_short", [False, True])
 def test_check_handle_timedout_buy(
-    default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade_usdt,
+    default_conf_usdt, ticker_usdt, limit_buy_order_old, open_trade,
     limit_sell_order_old, fee, mocker, is_short
 ) -> None:
     old_order = limit_sell_order_old if is_short else limit_buy_order_old
@@ -2267,18 +2267,25 @@ def test_check_handle_timedout_buy(
     )
     freqtrade = FreqtradeBot(default_conf_usdt)
 
-    Trade.query.session.add(open_trade_usdt)
+    open_trade.is_short = is_short
+    Trade.query.session.add(open_trade)
 
-    freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False)
+    if is_short:
+        freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False)
+    else:
+        freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False)
     # check it does cancel buy orders over the time limit
     freqtrade.check_handle_timedout()
     assert cancel_order_mock.call_count == 1
     assert rpc_mock.call_count == 1
-    trades = Trade.query.filter(Trade.open_order_id.is_(open_trade_usdt.open_order_id)).all()
+    trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
     nb_trades = len(trades)
     assert nb_trades == 0
     # Custom user buy-timeout is never called
-    assert freqtrade.strategy.check_buy_timeout.call_count == 0
+    if is_short:
+        assert freqtrade.strategy.check_sell_timeout.call_count == 0
+    else:
+        assert freqtrade.strategy.check_buy_timeout.call_count == 0
 
 
 @ pytest.mark.parametrize("is_short", [False, True])

From 85e86ec09db035e075c44803ce2d49beb8d5fa4e Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Sat, 9 Oct 2021 15:11:48 -0600
Subject: [PATCH 072/134] Fixed failing
 test_check_handle_timedout_buy_usercustom

---
 freqtrade/freqtradebot.py  |  6 +++++-
 tests/test_freqtradebot.py | 36 +++++++++++++++++++++++++-----------
 2 files changed, 30 insertions(+), 12 deletions(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 5142af5e3..e1117908c 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -1038,7 +1038,11 @@ class FreqtradeBot(LoggingMixin):
                 (fully_cancelled or
                     self._check_timed_out(trade.enter_side, order) or
                     strategy_safe_wrapper(
-                        self.strategy.check_buy_timeout,
+                        (
+                            self.strategy.check_sell_timeout
+                            if trade.is_short else
+                            self.strategy.check_buy_timeout
+                        ),
                         default_retval=False
                     )(
                         pair=trade.pair,
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index a1ca95c46..4405c788a 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -2193,7 +2193,8 @@ def test_check_handle_timedout_buy_usercustom(
 ) -> None:
 
     old_order = limit_sell_order_old if is_short else limit_buy_order_old
-    default_conf_usdt["unfilledtimeout"] = {"buy": 1400, "sell": 30}
+    default_conf_usdt["unfilledtimeout"] = {"buy": 30,
+                                            "sell": 1400} if is_short else {"buy": 1400, "sell": 30}
 
     rpc_mock = patch_RPCManager(mocker)
     cancel_order_mock = MagicMock(return_value=old_order)
@@ -2211,7 +2212,7 @@ def test_check_handle_timedout_buy_usercustom(
         get_fee=fee
     )
     freqtrade = FreqtradeBot(default_conf_usdt)
-
+    open_trade.is_short = is_short
     Trade.query.session.add(open_trade)
 
     # Ensure default is to return empty (so not mocked yet)
@@ -2219,24 +2220,34 @@ def test_check_handle_timedout_buy_usercustom(
     assert cancel_order_mock.call_count == 0
 
     # Return false - trade remains open
-    freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False)
+    if is_short:
+        freqtrade.strategy.check_sell_timeout = MagicMock(return_value=False)
+    else:
+        freqtrade.strategy.check_buy_timeout = MagicMock(return_value=False)
     freqtrade.check_handle_timedout()
     assert cancel_order_mock.call_count == 0
     trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
     nb_trades = len(trades)
     assert nb_trades == 1
-    assert freqtrade.strategy.check_buy_timeout.call_count == 1
+    if is_short:
+        assert freqtrade.strategy.check_sell_timeout.call_count == 1
+        # Raise Keyerror ... (no impact on trade)
+        freqtrade.strategy.check_sell_timeout = MagicMock(side_effect=KeyError)
+    else:
+        assert freqtrade.strategy.check_buy_timeout.call_count == 1
+        freqtrade.strategy.check_buy_timeout = MagicMock(side_effect=KeyError)
 
-    # Raise Keyerror ... (no impact on trade)
-    freqtrade.strategy.check_buy_timeout = MagicMock(side_effect=KeyError)
     freqtrade.check_handle_timedout()
     assert cancel_order_mock.call_count == 0
     trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
     nb_trades = len(trades)
     assert nb_trades == 1
-    assert freqtrade.strategy.check_buy_timeout.call_count == 1
-
-    freqtrade.strategy.check_buy_timeout = MagicMock(return_value=True)
+    if is_short:
+        assert freqtrade.strategy.check_sell_timeout.call_count == 1
+        freqtrade.strategy.check_sell_timeout = MagicMock(return_value=True)
+    else:
+        assert freqtrade.strategy.check_buy_timeout.call_count == 1
+        freqtrade.strategy.check_buy_timeout = MagicMock(return_value=True)
     # Trade should be closed since the function returns true
     freqtrade.check_handle_timedout()
     assert cancel_order_wr_mock.call_count == 1
@@ -2244,7 +2255,10 @@ def test_check_handle_timedout_buy_usercustom(
     trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
     nb_trades = len(trades)
     assert nb_trades == 0
-    assert freqtrade.strategy.check_buy_timeout.call_count == 1
+    if is_short:
+        assert freqtrade.strategy.check_sell_timeout.call_count == 1
+    else:
+        assert freqtrade.strategy.check_buy_timeout.call_count == 1
 
 
 @ pytest.mark.parametrize("is_short", [False, True])
@@ -2307,7 +2321,7 @@ def test_check_handle_cancelled_buy(
         get_fee=fee
     )
     freqtrade = FreqtradeBot(default_conf_usdt)
-
+    open_trade.is_short = is_short
     Trade.query.session.add(open_trade)
 
     # check it does cancel buy orders over the time limit

From 9a6ffff5eb7a72f459bbdff12281c139368b2fc6 Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Sat, 9 Oct 2021 15:50:18 -0600
Subject: [PATCH 073/134] Added cost to limit_sell_order_usdt_open, fixing some
 tests

---
 tests/conftest.py          | 1 +
 tests/test_freqtradebot.py | 9 ++++++---
 2 files changed, 7 insertions(+), 3 deletions(-)

diff --git a/tests/conftest.py b/tests/conftest.py
index a0d6148db..b97e0dfad 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -2309,6 +2309,7 @@ def limit_sell_order_usdt_open():
         'timestamp': arrow.utcnow().int_timestamp,
         'price': 2.20,
         'amount': 30.0,
+        'cost': 66.0,
         'filled': 0.0,
         'remaining': 30.0,
         'status': 'open'
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 4405c788a..1f14be306 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -1927,13 +1927,16 @@ def test_update_trade_state_sell(
     assert order.status == 'closed'
 
 
-@pytest.mark.parametrize('is_short', [False, True])
+@pytest.mark.parametrize('is_short', [
+    False,
+    True
+])
 def test_handle_trade(
     default_conf_usdt, limit_order_open, limit_order, fee, mocker, is_short
 ) -> None:
     open_order = limit_order_open[exit_side(is_short)]
-    enter_order = limit_order[exit_side(is_short)]
-    exit_order = limit_order[enter_side(is_short)]
+    enter_order = limit_order[enter_side(is_short)]
+    exit_order = limit_order[exit_side(is_short)]
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(

From 9513650ffed4780ba8682f89177997a88323c70b Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Sat, 9 Oct 2021 16:20:25 -0600
Subject: [PATCH 074/134] Fixed failing
 test_handle_stoploss_on_exchange_trailing

---
 tests/test_freqtradebot.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 1f14be306..531b5df1c 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -1230,7 +1230,8 @@ def test_create_stoploss_order_insufficient_funds(
 
 @pytest.mark.parametrize("is_short,bid,ask,stop_price,amt,hang_price", [
     (False, [4.38, 4.16], [4.4, 4.17], ['2.0805', 4.4 * 0.95], 27.39726027, 3),
-    (True, [1.09, 1.21], [1.1, 1.22], ['2.321', 1.1 * 0.95], 27.39726027, 1.5),
+    # TODO-lev: Should the stoploss be based off the bid for shorts? (1.09)
+    (True, [1.09, 1.21], [1.1, 1.22], ['2.321', 1.09 * 1.05], 27.39726027, 1.5),
 ])
 @pytest.mark.usefixtures("init_persistence")
 def test_handle_stoploss_on_exchange_trailing(

From 94f0be1fa9b1c96939725abf9e4b763f3021299b Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Sat, 9 Oct 2021 16:32:22 -0600
Subject: [PATCH 075/134] Added is_short=(signal == SignalDirection.SHORT)
 inside freqtradebot.create_trade

---
 freqtrade/freqtradebot.py | 15 ++++++++++++---
 1 file changed, 12 insertions(+), 3 deletions(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index e1117908c..0deed053a 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -453,17 +453,26 @@ class FreqtradeBot(LoggingMixin):
             bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {})
             if ((bid_check_dom.get('enabled', False)) and
                     (bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
-                # TODO-lev: Does the below need to be adjusted for shorts?
                 if self._check_depth_of_market(
                     pair,
                     bid_check_dom,
                     side=signal
                 ):
-                    return self.execute_entry(pair, stake_amount, enter_tag=enter_tag)
+                    return self.execute_entry(
+                        pair,
+                        stake_amount,
+                        enter_tag=enter_tag,
+                        is_short=(signal == SignalDirection.SHORT)
+                    )
                 else:
                     return False
 
-            return self.execute_entry(pair, stake_amount, enter_tag=enter_tag)
+            return self.execute_entry(
+                pair,
+                stake_amount,
+                enter_tag=enter_tag,
+                is_short=(signal == SignalDirection.SHORT)
+            )
         else:
             return False
 

From 81cf4653a9e5af041f6679515e8397b3a6df4432 Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Sat, 9 Oct 2021 16:53:42 -0600
Subject: [PATCH 076/134] Fixed failing test_process_trade_creation,
 test_order_book_depth_of_market, test_handle_stoploss_on_exchange_trailing

---
 freqtrade/freqtradebot.py  |  2 +-
 tests/test_freqtradebot.py | 59 ++++++++++++++++++++++----------------
 2 files changed, 35 insertions(+), 26 deletions(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 0deed053a..f4a36e3d8 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -1303,7 +1303,7 @@ class FreqtradeBot(LoggingMixin):
             order = self.exchange.create_order(
                 pair=trade.pair,
                 ordertype=order_type,
-                side="sell",
+                side=trade.exit_side,
                 amount=amount,
                 rate=limit,
                 time_in_force=time_in_force
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 531b5df1c..63c9cd8f9 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -4,7 +4,7 @@
 import logging
 import time
 from copy import deepcopy
-from math import isclose
+from math import floor, isclose
 from unittest.mock import ANY, MagicMock, PropertyMock
 
 import arrow
@@ -536,9 +536,12 @@ def test_create_trades_preopen(default_conf_usdt, ticker_usdt, fee, mocker,
     assert len(trades) == 4
 
 
-@pytest.mark.parametrize('is_short', [False, True])
+@pytest.mark.parametrize('is_short, open_rate', [
+    (False, 2.0),
+    (True, 2.02)
+])
 def test_process_trade_creation(default_conf_usdt, ticker_usdt, limit_order, limit_order_open,
-                                is_short, fee, mocker, caplog
+                                is_short, open_rate, fee, mocker, caplog
                                 ) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
@@ -565,11 +568,12 @@ def test_process_trade_creation(default_conf_usdt, ticker_usdt, limit_order, lim
     assert trade.is_open
     assert trade.open_date is not None
     assert trade.exchange == 'binance'
-    assert trade.open_rate == 2.0
-    assert trade.amount == 30.0
+    assert trade.open_rate == open_rate  # TODO-lev: I think? That's what the ticker ask price is
+    assert isclose(trade.amount, 60 / open_rate)
 
     assert log_has(
-        'Long signal found: about create a new trade for ETH/USDT with stake_amount: 60.0 ...',
+        f'{"Short" if is_short else "Long"} signal found: about create a new trade for ETH/USDT '
+        'with stake_amount: 60.0 ...',
         caplog
     )
 
@@ -1230,8 +1234,7 @@ def test_create_stoploss_order_insufficient_funds(
 
 @pytest.mark.parametrize("is_short,bid,ask,stop_price,amt,hang_price", [
     (False, [4.38, 4.16], [4.4, 4.17], ['2.0805', 4.4 * 0.95], 27.39726027, 3),
-    # TODO-lev: Should the stoploss be based off the bid for shorts? (1.09)
-    (True, [1.09, 1.21], [1.1, 1.22], ['2.321', 1.09 * 1.05], 27.39726027, 1.5),
+    (True, [1.09, 1.21], [1.1, 1.22], ['2.321', 1.09 * 1.05], 27.27272727, 1.5),
 ])
 @pytest.mark.usefixtures("init_persistence")
 def test_handle_stoploss_on_exchange_trailing(
@@ -1336,7 +1339,7 @@ def test_handle_stoploss_on_exchange_trailing(
 
     cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
     stoploss_order_mock.assert_called_once_with(
-        amount=27.39726027,
+        amount=amt,
         pair='ETH/USDT',
         order_types=freqtrade.strategy.order_types,
         stop_price=stop_price[1],
@@ -1943,9 +1946,9 @@ def test_handle_trade(
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
         fetch_ticker=MagicMock(return_value={
-            'bid': 1.9,
+            'bid': 2.19,
             'ask': 2.2,
-            'last': 1.9
+            'last': 2.19
         }),
         create_order=MagicMock(side_effect=[
             enter_order,
@@ -1967,17 +1970,14 @@ def test_handle_trade(
     assert trade.is_open is True
     freqtrade.wallets.update()
 
-    if is_short:
-        patch_get_signal(freqtrade, enter_long=False, exit_short=True)
-    else:
-        patch_get_signal(freqtrade, enter_long=False, exit_long=True)
+    patch_get_signal(freqtrade, enter_long=False, exit_short=is_short, exit_long=not is_short)
     assert freqtrade.handle_trade(trade) is True
     assert trade.open_order_id == exit_order['id']
 
     # Simulate fulfilled LIMIT order for trade
     trade.update(exit_order)
 
-    assert trade.close_rate == 2.2
+    assert trade.close_rate == 2.0 if is_short else 2.2
     assert trade.close_profit == 0.09451372
     assert trade.calc_profit() == 5.685
     assert trade.close_date is not None
@@ -2803,9 +2803,12 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None:
     assert freqtrade.handle_cancel_exit(trade, order, reason) == 'error cancelling order'
 
 
-@ pytest.mark.parametrize("is_short", [False, True])
+@ pytest.mark.parametrize("is_short, open_rate, amt", [
+    (False, 2.0, 30.0),
+    (True, 2.02, 29.7029703),
+])
 def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_up, mocker,
-                               is_short) -> None:
+                               is_short, open_rate, amt) -> None:
     rpc_mock = patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
@@ -2856,9 +2859,9 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
         'pair': 'ETH/USDT',
         'gain': 'profit',
         'limit': 2.2,
-        'amount': 30.0,
+        'amount': amt,
         'order_type': 'limit',
-        'open_rate': 2.0,
+        'open_rate': open_rate,
         'current_rate': 2.3,
         'profit_amount': 5.685,
         'profit_ratio': 0.09451372,
@@ -3252,8 +3255,11 @@ def test_execute_trade_exit_market_order(default_conf_usdt, ticker_usdt, fee, is
     freqtrade.config['order_types']['sell'] = 'market'
 
     # TODO-lev: side="buy"
-    freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['bid'],
-                                 sell_reason=SellCheckTuple(sell_type=SellType.ROI))
+    freqtrade.execute_trade_exit(
+        trade=trade,
+        limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'],
+        sell_reason=SellCheckTuple(sell_type=SellType.ROI)
+    )
 
     assert not trade.is_open
     assert trade.close_profit == 0.09451372
@@ -4045,10 +4051,13 @@ def test_apply_fee_conditional(default_conf_usdt, fee, mocker,
     (0.1, False),
     (100, True),
 ])
-@ pytest.mark.parametrize('is_short', [False, True])
+@ pytest.mark.parametrize('is_short, open_rate', [
+    (False, 2.0),
+    (True, 2.02),
+])
 def test_order_book_depth_of_market(
     default_conf_usdt, ticker_usdt, limit_order, limit_order_open,
-    fee, mocker, order_book_l2, delta, is_high_delta, is_short
+    fee, mocker, order_book_l2, delta, is_high_delta, is_short, open_rate
 ):
     default_conf_usdt['bid_strategy']['check_depth_of_market']['enabled'] = True
     default_conf_usdt['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = delta
@@ -4084,7 +4093,7 @@ def test_order_book_depth_of_market(
         # Simulate fulfilled LIMIT_BUY order for trade
         trade.update(limit_order_open[enter_side(is_short)])
 
-        assert trade.open_rate == 2.0
+        assert trade.open_rate == open_rate  # TODO-lev: double check
         assert whitelist == default_conf_usdt['exchange']['pair_whitelist']
 
 

From 2e7adb99daa9308229454d3bc70c9229b95d514f Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Mon, 11 Oct 2021 08:26:15 -0600
Subject: [PATCH 077/134] Fixed some breaking tests

---
 tests/test_freqtradebot.py | 70 +++++++++++++++++---------------------
 1 file changed, 32 insertions(+), 38 deletions(-)

diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 63c9cd8f9..6c51e7fe2 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -203,13 +203,11 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None:
         'LTC/BTC', freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21
 
 
-@pytest.mark.parametrize('buy_price_mult,ignore_strat_sl,is_short', [
-    (0.79, False, False),   # Override stoploss
-    (0.85, True, False),    # Override strategy stoploss
-    (0.85, False, True),    # Override stoploss
-    (0.79, True, True)      # Override strategy stoploss
+@pytest.mark.parametrize('buy_price_mult,ignore_strat_sl', [
+    (0.79, False),   # Override stoploss
+    (0.85, True),    # Override strategy stoploss
 ])
-def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker, is_short,
+def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker,
                                  buy_price_mult, ignore_strat_sl, edge_conf) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
@@ -220,7 +218,7 @@ def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker, is_short,
     # Thus, if price falls 21%, stoploss should be triggered
     #
     # mocking the ticker: price is falling ...
-    enter_price = limit_order[enter_side(is_short)]['price']
+    enter_price = limit_order['buy']['price']
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
         fetch_ticker=MagicMock(return_value={
@@ -235,13 +233,12 @@ def test_edge_overrides_stoploss(limit_order, fee, caplog, mocker, is_short,
     # Create a trade with "limit_buy_order_usdt" price
     freqtrade = FreqtradeBot(edge_conf)
     freqtrade.active_pair_whitelist = ['NEO/BTC']
-    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
+    patch_get_signal(freqtrade)
     freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
     freqtrade.enter_positions()
     trade = Trade.query.first()
-    trade.is_short = is_short
     caplog.clear()
-    trade.update(limit_order[enter_side(is_short)])
+    trade.update(limit_order['buy'])
     #############################################
 
     # stoploss shoud be hit
@@ -1564,12 +1561,11 @@ def test_handle_stoploss_on_exchange_custom_stop(
     assert freqtrade.handle_trade(trade) is True
 
 
-@pytest.mark.parametrize("is_short", [False, True])
-def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, is_short,
+def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
                                               limit_order) -> None:
 
-    enter_order = limit_order[enter_side(is_short)]
-    exit_order = limit_order[exit_side(is_short)]
+    enter_order = limit_order['buy']
+    exit_order = limit_order['sell']
 
     # When trailing stoploss is set
     stoploss = MagicMock(return_value={'id': 13434334})
@@ -1613,17 +1609,15 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, is
     # setting stoploss_on_exchange_interval to 0 seconds
     freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0
 
-    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
+    patch_get_signal(freqtrade)
 
     freqtrade.active_pair_whitelist = freqtrade.edge.adjust(freqtrade.active_pair_whitelist)
 
     freqtrade.enter_positions()
     trade = Trade.query.first()
-    trade.is_short = is_short
     trade.is_open = True
     trade.open_order_id = None
     trade.stoploss_order_id = 100
-    trade.is_short = is_short
 
     stoploss_order_hanging = MagicMock(return_value={
         'id': 100,
@@ -1681,7 +1675,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, is
         pair='NEO/BTC',
         order_types=freqtrade.strategy.order_types,
         stop_price=4.4 * 0.99,
-        side=exit_side(is_short),
+        side='sell',
         leverage=1.0
     )
 
@@ -1931,12 +1925,12 @@ def test_update_trade_state_sell(
     assert order.status == 'closed'
 
 
-@pytest.mark.parametrize('is_short', [
-    False,
-    True
+@pytest.mark.parametrize('is_short,close_profit,profit', [
+    (False, 0.09451372, 5.685),
+    (True, 0.08675799087, 5.7),
 ])
 def test_handle_trade(
-    default_conf_usdt, limit_order_open, limit_order, fee, mocker, is_short
+    default_conf_usdt, limit_order_open, limit_order, fee, mocker, is_short, close_profit, profit
 ) -> None:
     open_order = limit_order_open[exit_side(is_short)]
     enter_order = limit_order[enter_side(is_short)]
@@ -1978,8 +1972,8 @@ def test_handle_trade(
     trade.update(exit_order)
 
     assert trade.close_rate == 2.0 if is_short else 2.2
-    assert trade.close_profit == 0.09451372
-    assert trade.calc_profit() == 5.685
+    assert trade.close_profit == close_profit
+    assert trade.calc_profit() == profit
     assert trade.close_date is not None
 
 
@@ -2838,7 +2832,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
     )
     # Prevented sell ...
     # TODO-lev: side="buy"
-    freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['bid'],
+    freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'],
                                  sell_reason=SellCheckTuple(sell_type=SellType.ROI))
     assert rpc_mock.call_count == 0
     assert freqtrade.strategy.confirm_trade_exit.call_count == 1
@@ -2846,7 +2840,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
     # Repatch with true
     freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True)
     # TODO-lev: side="buy"
-    freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['bid'],
+    freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'],
                                  sell_reason=SellCheckTuple(sell_type=SellType.ROI))
     assert freqtrade.strategy.confirm_trade_exit.call_count == 1
 
@@ -3328,12 +3322,12 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_u
 @ pytest.mark.parametrize("is_short", [False, True])
 @ pytest.mark.parametrize('profit_only,bid,ask,handle_first,handle_second,sell_type', [
     # Enable profit
-    (True, 1.9, 2.2, False, True, SellType.SELL_SIGNAL.value),
+    (True, 2.19, 2.2, False, True, SellType.SELL_SIGNAL.value),
     # Disable profit
-    (False, 2.9, 3.2, True,  False, SellType.SELL_SIGNAL.value),
+    (False, 3.19, 3.2, True,  False, SellType.SELL_SIGNAL.value),
     # Enable loss
     # * Shouldn't this be SellType.STOP_LOSS.value
-    (True, 0.19, 0.22, False, False, None),
+    (True, 0.21, 0.22, False, False, None),
     # Disable loss
     (False, 0.10, 0.22, True, False, SellType.SELL_SIGNAL.value),
 ])
@@ -3373,7 +3367,7 @@ def test_sell_profit_only(
     trade.is_short = is_short
     trade.update(limit_order[enter_side(is_short)])
     freqtrade.wallets.update()
-    patch_get_signal(freqtrade, enter_long=False, exit_long=True)
+    patch_get_signal(freqtrade, enter_long=False, exit_short=is_short, exit_long=not is_short)
     assert freqtrade.handle_trade(trade) is handle_first
 
     if handle_second:
@@ -3381,9 +3375,8 @@ def test_sell_profit_only(
         assert freqtrade.handle_trade(trade) is True
 
 
-@ pytest.mark.parametrize("is_short", [False, True])
 def test_sell_not_enough_balance(default_conf_usdt, limit_order, limit_order_open,
-                                 is_short, fee, mocker, caplog) -> None:
+                                 fee, mocker, caplog) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
@@ -3394,22 +3387,21 @@ def test_sell_not_enough_balance(default_conf_usdt, limit_order, limit_order_ope
             'last': 0.00002172
         }),
         create_order=MagicMock(side_effect=[
-            limit_order_open[enter_side(is_short)],
+            limit_order_open['buy'],
             {'id': 1234553382},
         ]),
         get_fee=fee,
     )
 
     freqtrade = FreqtradeBot(default_conf_usdt)
-    patch_get_signal(freqtrade, enter_short=is_short, enter_long=not is_short)
+    patch_get_signal(freqtrade)
     freqtrade.strategy.min_roi_reached = MagicMock(return_value=False)
 
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
-    trade.is_short = is_short
     amnt = trade.amount
-    trade.update(limit_order[enter_side(is_short)])
+    trade.update(limit_order['buy'])
     patch_get_signal(freqtrade, enter_long=False, exit_long=True)
     mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=trade.amount * 0.985))
 
@@ -3692,7 +3684,8 @@ def test_trailing_stop_loss_positive(
     assert log_has(
         f"ETH/USDT - HIT STOP: current price at {enter_price + (-0.02 if is_short else 0.02):.6f}, "
         f"stoploss is {trade.stop_loss:.6f}, "
-        f"initial stoploss was at {2.2 if is_short else 1.8}00000, trade opened at 2.000000", caplog)
+        f"initial stoploss was at {2.2 if is_short else 1.8}00000, trade opened at 2.000000",
+        caplog)
     assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value
 
 
@@ -3729,7 +3722,8 @@ def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_order, limit_
     trade.is_short = is_short
     trade.update(limit_order[enter_side(is_short)])
     # Sell due to min_roi_reached
-    patch_get_signal(freqtrade, enter_long=not is_short, enter_short=is_short, exit_short=is_short)
+    patch_get_signal(freqtrade, enter_long=not is_short, exit_long=not is_short,
+                     enter_short=is_short, exit_short=is_short)
     assert freqtrade.handle_trade(trade) is True
 
     # Test if buy-signal is absent

From 437fadc2588c19fce6dc1edc957d7dbb3b0a8f3a Mon Sep 17 00:00:00 2001
From: Rokas Kupstys 
Date: Tue, 12 Oct 2021 10:49:07 +0300
Subject: [PATCH 078/134] Fix profitable trade registering as a loss due to
 fees.

---
 tests/test_freqtradebot.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 6c51e7fe2..53ef18733 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -3322,7 +3322,7 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_u
 @ pytest.mark.parametrize("is_short", [False, True])
 @ pytest.mark.parametrize('profit_only,bid,ask,handle_first,handle_second,sell_type', [
     # Enable profit
-    (True, 2.19, 2.2, False, True, SellType.SELL_SIGNAL.value),
+    (True, 2.18, 2.2, False, True, SellType.SELL_SIGNAL.value),
     # Disable profit
     (False, 3.19, 3.2, True,  False, SellType.SELL_SIGNAL.value),
     # Enable loss

From aed919a05f352de482caddc1dc199d0ba2bd8e85 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Wed, 13 Oct 2021 19:54:35 +0200
Subject: [PATCH 079/134] Simplify "no-space-configured" error handling by
 moving it to hyperopt_auto

---
 freqtrade/commands/arguments.py          |  4 +-
 freqtrade/commands/cli_options.py        |  4 +-
 freqtrade/configuration/configuration.py |  6 +--
 freqtrade/optimize/hyperopt.py           | 49 ++++--------------------
 freqtrade/optimize/hyperopt_auto.py      | 27 +++++++++----
 5 files changed, 33 insertions(+), 57 deletions(-)

diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py
index e58135895..167b79afb 100644
--- a/freqtrade/commands/arguments.py
+++ b/freqtrade/commands/arguments.py
@@ -31,8 +31,8 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
                                         "epochs", "spaces", "print_all",
                                         "print_colorized", "print_json", "hyperopt_jobs",
                                         "hyperopt_random_state", "hyperopt_min_trades",
-                                        "hyperopt_loss", "disableparamexport", 
-                                        "hyperopt_ignore_unparam_space"]
+                                        "hyperopt_loss", "disableparamexport",
+                                        "hyperopt_ignore_missing_space"]
 
 ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
 
diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py
index ef1ec8515..1f49b779b 100644
--- a/freqtrade/commands/cli_options.py
+++ b/freqtrade/commands/cli_options.py
@@ -552,8 +552,8 @@ AVAILABLE_CLI_OPTIONS = {
         help='Do not print epoch details header.',
         action='store_true',
     ),
-    "hyperopt_ignore_unparam_space": Arg(
-        "-u", "--ignore-unparameterized-spaces",
+    "hyperopt_ignore_missing_space": Arg(
+        "--ignore-missing-spaces", "--ignore-unparameterized-spaces",
         help="Suppress errors for any requested Hyperopt spaces that do not contain any parameters",
         action="store_true",
     ),
diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py
index 723ad3795..12dcff46a 100644
--- a/freqtrade/configuration/configuration.py
+++ b/freqtrade/configuration/configuration.py
@@ -368,9 +368,9 @@ class Configuration:
 
         self._args_to_config(config, argname='hyperopt_show_no_header',
                              logstring='Parameter --no-header detected: {}')
-        
-        self._args_to_config(config, argname="hyperopt_ignore_unparam_space",
-                             logstring="Paramter --ignore-unparameterized-spaces detected: {}")
+
+        self._args_to_config(config, argname="hyperopt_ignore_missing_space",
+                             logstring="Paramter --ignore-missing-space detected: {}")
 
     def _process_plot_options(self, config: Dict[str, Any]) -> None:
 
diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
index f6c677a6e..6397bbacb 100644
--- a/freqtrade/optimize/hyperopt.py
+++ b/freqtrade/optimize/hyperopt.py
@@ -237,63 +237,28 @@ class Hyperopt:
             logger.debug("Hyperopt has 'protection' space")
             # Enable Protections if protection space is selected.
             self.config['enable_protections'] = True
-            try:
-                self.protection_space = self.custom_hyperopt.protection_space()
-            except OperationalException as e:
-                if self.config["hyperopt_ignore_unparam_space"]:
-                    logger.warning(e)
-                else:
-                    raise
+            self.protection_space = self.custom_hyperopt.protection_space()
 
         if HyperoptTools.has_space(self.config, 'buy'):
             logger.debug("Hyperopt has 'buy' space")
-            try:
-                self.buy_space = self.custom_hyperopt.buy_indicator_space()
-            except OperationalException as e:
-                if self.config["hyperopt_ignore_unparam_space"]:
-                    logger.warning(e)
-                else:
-                    raise
+            self.buy_space = self.custom_hyperopt.buy_indicator_space()
 
         if HyperoptTools.has_space(self.config, 'sell'):
             logger.debug("Hyperopt has 'sell' space")
-            try:
-                self.sell_space = self.custom_hyperopt.sell_indicator_space()
-            except OperationalException as e:
-                if self.config["hyperopt_ignore_unparam_space"]:
-                    logger.warning(e)
-                else:
-                    raise
+            self.sell_space = self.custom_hyperopt.sell_indicator_space()
 
         if HyperoptTools.has_space(self.config, 'roi'):
             logger.debug("Hyperopt has 'roi' space")
-            try:
-                self.roi_space = self.custom_hyperopt.roi_space()
-            except OperationalException as e:
-                if self.config["hyperopt_ignore_unparam_space"]:
-                    logger.warning(e)
-                else:
-                    raise
+            self.roi_space = self.custom_hyperopt.roi_space()
 
         if HyperoptTools.has_space(self.config, 'stoploss'):
             logger.debug("Hyperopt has 'stoploss' space")
-            try:
-                self.stoploss_space = self.custom_hyperopt.stoploss_space()
-            except OperationalException as e:
-                if self.config["hyperopt_ignore_unparam_space"]:
-                    logger.warning(e)
-                else:
-                    raise
+            self.stoploss_space = self.custom_hyperopt.stoploss_space()
 
         if HyperoptTools.has_space(self.config, 'trailing'):
             logger.debug("Hyperopt has 'trailing' space")
-            try:
-                self.trailing_space = self.custom_hyperopt.trailing_space()
-            except OperationalException as e:
-                if self.config["hyperopt_ignore_unparam_space"]:
-                    logger.warning(e)
-                else:
-                    raise
+            self.trailing_space = self.custom_hyperopt.trailing_space()
+
         self.dimensions = (self.buy_space + self.sell_space + self.protection_space
                            + self.roi_space + self.stoploss_space + self.trailing_space)
 
diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py
index c1c769c72..63b4b14e1 100644
--- a/freqtrade/optimize/hyperopt_auto.py
+++ b/freqtrade/optimize/hyperopt_auto.py
@@ -3,6 +3,7 @@ HyperOptAuto class.
 This module implements a convenience auto-hyperopt class, which can be used together with strategies
  that implement IHyperStrategy interface.
 """
+import logging
 from contextlib import suppress
 from typing import Callable, Dict, List
 
@@ -15,12 +16,19 @@ with suppress(ImportError):
 from freqtrade.optimize.hyperopt_interface import EstimatorType, IHyperOpt
 
 
-def _format_exception_message(space: str) -> str:
-    raise OperationalException(
-        f"The '{space}' space is included into the hyperoptimization "
-        f"but no parameter for this space was not found in your Strategy. "
-        f"Please make sure to have parameters for this space enabled for optimization "
-        f"or remove the '{space}' space from hyperoptimization.")
+logger = logging.getLogger(__name__)
+
+
+def _format_exception_message(space: str, ignore_missing_space: bool) -> None:
+    msg = (f"The '{space}' space is included into the hyperoptimization "
+           f"but no parameter for this space was not found in your Strategy. "
+           )
+    if ignore_missing_space:
+        logger.warning(msg + "This space will be ignored.")
+    else:
+        raise OperationalException(
+            msg + f"Please make sure to have parameters for this space enabled for optimization "
+            f"or remove the '{space}' space from hyperoptimization.")
 
 
 class HyperOptAuto(IHyperOpt):
@@ -48,13 +56,16 @@ class HyperOptAuto(IHyperOpt):
             if attr.optimize:
                 yield attr.get_space(attr_name)
 
-    def _get_indicator_space(self, category):
+    def _get_indicator_space(self, category) -> List:
         # TODO: is this necessary, or can we call "generate_space" directly?
         indicator_space = list(self._generate_indicator_space(category))
         if len(indicator_space) > 0:
             return indicator_space
         else:
-            _format_exception_message(category)
+            _format_exception_message(
+                category,
+                self.config.get("hyperopt_ignore_missing_space", False))
+            return []
 
     def buy_indicator_space(self) -> List['Dimension']:
         return self._get_indicator_space('buy')

From 3279ea568c0b2da3d5a05a7aa3e011964838d849 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Wed, 13 Oct 2021 19:56:34 +0200
Subject: [PATCH 080/134] Add new parameter to hyperopt docs

---
 docs/hyperopt.md                  | 4 ++++
 freqtrade/commands/cli_options.py | 3 ++-
 2 files changed, 6 insertions(+), 1 deletion(-)

diff --git a/docs/hyperopt.md b/docs/hyperopt.md
index 09d43939a..a693513d0 100644
--- a/docs/hyperopt.md
+++ b/docs/hyperopt.md
@@ -51,6 +51,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
                           [--print-all] [--no-color] [--print-json] [-j JOBS]
                           [--random-state INT] [--min-trades INT]
                           [--hyperopt-loss NAME] [--disable-param-export]
+                          [--ignore-missing-spaces]
 
 optional arguments:
   -h, --help            show this help message and exit
@@ -117,6 +118,9 @@ optional arguments:
                         SortinoHyperOptLoss, SortinoHyperOptLossDaily
   --disable-param-export
                         Disable automatic hyperopt parameter export.
+  --ignore-missing-spaces, --ignore-unparameterized-spaces
+                        Suppress errors for any requested Hyperopt spaces that
+                        do not contain any parameters.
 
 Common arguments:
   -v, --verbose         Verbose mode (-vv for more, -vvv to get all messages).
diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py
index 1f49b779b..f8338bf6a 100644
--- a/freqtrade/commands/cli_options.py
+++ b/freqtrade/commands/cli_options.py
@@ -554,7 +554,8 @@ AVAILABLE_CLI_OPTIONS = {
     ),
     "hyperopt_ignore_missing_space": Arg(
         "--ignore-missing-spaces", "--ignore-unparameterized-spaces",
-        help="Suppress errors for any requested Hyperopt spaces that do not contain any parameters",
+        help=("Suppress errors for any requested Hyperopt spaces "
+              "that do not contain any parameters."),
         action="store_true",
     ),
 }

From fe8374f2a489418a8628ebeb1387df617d89b211 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Thu, 14 Oct 2021 07:06:51 +0200
Subject: [PATCH 081/134] Test for non-failing missing hyperopt space

---
 tests/optimize/test_hyperopt.py | 10 ++++++++--
 1 file changed, 8 insertions(+), 2 deletions(-)

diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py
index e4ce29d44..b123fec21 100644
--- a/tests/optimize/test_hyperopt.py
+++ b/tests/optimize/test_hyperopt.py
@@ -702,7 +702,7 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non
     assert hasattr(hyperopt, "position_stacking")
 
 
-def test_simplified_interface_all_failed(mocker, hyperopt_conf) -> None:
+def test_simplified_interface_all_failed(mocker, hyperopt_conf, caplog) -> None:
     mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
     mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
     mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
@@ -724,7 +724,13 @@ def test_simplified_interface_all_failed(mocker, hyperopt_conf) -> None:
     hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
 
     with pytest.raises(OperationalException, match=r"The 'protection' space is included into *"):
-        hyperopt.start()
+        hyperopt.init_spaces()
+
+    hyperopt.config['hyperopt_ignore_missing_space'] = True
+    caplog.clear()
+    hyperopt.init_spaces()
+    assert log_has_re(r"The 'protection' space is included into *", caplog)
+    assert hyperopt.protection_space == []
 
 
 def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:

From b0ce9612f87fdef6a4c9bb029b99e3cba3011fa3 Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Thu, 14 Oct 2021 03:52:29 -0600
Subject: [PATCH 082/134] Fixed sell_profit_only failing

---
 tests/test_freqtradebot.py | 21 ++++++++++++---------
 1 file changed, 12 insertions(+), 9 deletions(-)

diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 403d2f2fd..cebd59f8f 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -3325,17 +3325,20 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf_usdt, ticker_u
     assert mock_insuf.call_count == 1
 
 
-@ pytest.mark.parametrize("is_short", [False, True])
-@ pytest.mark.parametrize('profit_only,bid,ask,handle_first,handle_second,sell_type', [
+@ pytest.mark.parametrize('profit_only,bid,ask,handle_first,handle_second,sell_type,is_short', [
     # Enable profit
-    (True, 2.18, 2.2, False, True, SellType.SELL_SIGNAL.value),
-    # Disable profit
-    (False, 3.19, 3.2, True,  False, SellType.SELL_SIGNAL.value),
-    # Enable loss
-    # * Shouldn't this be SellType.STOP_LOSS.value
-    (True, 0.21, 0.22, False, False, None),
+    (True, 2.18, 2.2, False, True, SellType.SELL_SIGNAL.value, False),
+    (True, 2.18, 2.2, False, True, SellType.SELL_SIGNAL.value, True),
+    # # Disable profit
+    (False, 3.19, 3.2, True,  False, SellType.SELL_SIGNAL.value, False),
+    (False, 3.19, 3.2, True,  False, SellType.SELL_SIGNAL.value, True),
+    # # Enable loss
+    # # * Shouldn't this be SellType.STOP_LOSS.value
+    (True, 0.21, 0.22, False, False, None, False),
+    (True, 2.41, 2.42, False, False, None, True),
     # Disable loss
-    (False, 0.10, 0.22, True, False, SellType.SELL_SIGNAL.value),
+    (False, 0.10, 0.22, True, False, SellType.SELL_SIGNAL.value, False),
+    (False, 0.10, 0.22, True, False, SellType.SELL_SIGNAL.value, True),
 ])
 def test_sell_profit_only(
         default_conf_usdt, limit_order, limit_order_open, is_short,

From 2dc402fbf79f09f2bf31ce4afac7e7b2c556844d Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Thu, 14 Oct 2021 04:05:50 -0600
Subject: [PATCH 083/134] Fixed failing test_handle_trade

---
 tests/test_freqtradebot.py | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index cebd59f8f..2e63fea6c 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -1925,12 +1925,12 @@ def test_update_trade_state_sell(
     assert order.status == 'closed'
 
 
-@pytest.mark.parametrize('is_short,close_profit,profit', [
-    (False, 0.09451372, 5.685),
-    (True, 0.08675799087, 5.7),
+@pytest.mark.parametrize('is_short,close_profit', [
+    (False, 0.09451372),
+    (True, 0.08635224),
 ])
 def test_handle_trade(
-    default_conf_usdt, limit_order_open, limit_order, fee, mocker, is_short, close_profit, profit
+    default_conf_usdt, limit_order_open, limit_order, fee, mocker, is_short, close_profit
 ) -> None:
     open_order = limit_order_open[exit_side(is_short)]
     enter_order = limit_order[enter_side(is_short)]
@@ -1973,7 +1973,7 @@ def test_handle_trade(
 
     assert trade.close_rate == 2.0 if is_short else 2.2
     assert trade.close_profit == close_profit
-    assert trade.calc_profit() == profit
+    assert trade.calc_profit() == 5.685
     assert trade.close_date is not None
 
 

From 0afd76c18319ff8d19c02576eec6c38add07f195 Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Thu, 14 Oct 2021 04:45:48 -0600
Subject: [PATCH 084/134] Fixed failing test_execute_trade_exit_market_order

---
 tests/test_freqtradebot.py | 46 +++++++++++++++++++++++++++++---------
 1 file changed, 35 insertions(+), 11 deletions(-)

diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 2e63fea6c..d5b820d2b 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -3225,9 +3225,33 @@ def test_may_execute_trade_exit_after_stoploss_on_exchange_hit(default_conf_usdt
     assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL
 
 
-@ pytest.mark.parametrize("is_short", [False, True])
-def test_execute_trade_exit_market_order(default_conf_usdt, ticker_usdt, fee, is_short,
-                                         ticker_usdt_sell_up, mocker) -> None:
+@pytest.mark.parametrize(
+    "is_short,amount,open_rate,current_rate,limit,profit_amount,profit_ratio,profit_or_loss", [
+        (False, 30, 2.0, 2.3, 2.2, 5.685, 0.09451372, 'profit'),
+        # TODO-lev: Should the current rate be 2.2 for shorts?
+        (True, 29.70297029, 2.02, 2.2, 2.3, -8.63762376, -0.1443212, 'loss'),
+    ])
+def test_execute_trade_exit_market_order(
+    default_conf_usdt, ticker_usdt, fee, is_short, current_rate, amount, open_rate,
+    limit, profit_amount, profit_ratio, profit_or_loss, ticker_usdt_sell_up, mocker
+) -> None:
+    """
+    amount
+        long: 60 / 2.0 = 30
+        short: 60 / 2.02 = 29.70297029 
+    open_value 
+        long: (30 * 2.0) + (30 * 2.0 * 0.0025) = 60.15
+        short: (29.702970297029704 * 2.02) - (29.702970297029704 * 2.02 * 0.0025) = 59.85
+    close_value 
+        long: (30 * 2.2) - (30 * 2.2 * 0.0025) = 65.835
+        short: (29.702970297029704 * 2.3) + (29.702970297029704 * 2.3 * 0.0025) = 68.48762376237624
+    profit
+        long: 65.835 - 60.15 = 5.684999999999995
+        short: 59.85 - 68.48762376237624 = -8.637623762376244
+    profit_ratio
+        long: (65.835/60.15) - 1 = 0.0945137157107232
+        short: 1 - (68.48762376237624/59.85) = -0.1443211990371971
+    """
     rpc_mock = patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
@@ -3262,7 +3286,7 @@ def test_execute_trade_exit_market_order(default_conf_usdt, ticker_usdt, fee, is
     )
 
     assert not trade.is_open
-    assert trade.close_profit == 0.09451372
+    assert trade.close_profit == profit_ratio
 
     assert rpc_mock.call_count == 3
     last_msg = rpc_mock.call_args_list[-1][0][0]
@@ -3271,14 +3295,14 @@ def test_execute_trade_exit_market_order(default_conf_usdt, ticker_usdt, fee, is
         'trade_id': 1,
         'exchange': 'Binance',
         'pair': 'ETH/USDT',
-        'gain': 'profit',
-        'limit': 2.2,
-        'amount': 30.0,
+        'gain': profit_or_loss,
+        'limit': limit,
+        'amount': round(amount, 9),
         'order_type': 'market',
-        'open_rate': 2.0,
-        'current_rate': 2.3,
-        'profit_amount': 5.685,
-        'profit_ratio': 0.09451372,
+        'open_rate': open_rate,
+        'current_rate': current_rate,
+        'profit_amount': profit_amount,
+        'profit_ratio': profit_ratio,
         'stake_currency': 'USDT',
         'fiat_currency': 'USD',
         'sell_reason': SellType.ROI.value,

From 5fbe76cd7ed01ca5eeee26dc93649cbe4415d305 Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Thu, 14 Oct 2021 05:10:28 -0600
Subject: [PATCH 085/134] isolated conditionals in interface stoploss method

---
 freqtrade/strategy/interface.py | 15 +++++++--------
 tests/test_freqtradebot.py      |  6 +++---
 2 files changed, 10 insertions(+), 11 deletions(-)

diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py
index f4784133a..05df0c6fb 100644
--- a/freqtrade/strategy/interface.py
+++ b/freqtrade/strategy/interface.py
@@ -840,10 +840,9 @@ class IStrategy(ABC, HyperStrategyMixin):
             else:
                 logger.warning("CustomStoploss function did not return valid stoploss")
 
-        if self.trailing_stop and (
-            (trade.stop_loss < (low or current_rate) and not trade.is_short) or
-            (trade.stop_loss > (high or current_rate) and trade.is_short)
-        ):
+        sl_lower_short = (trade.stop_loss < (low or current_rate) and not trade.is_short)
+        sl_higher_long = (trade.stop_loss > (high or current_rate) and trade.is_short)
+        if self.trailing_stop and (sl_lower_short or sl_higher_long):
             # trailing stoploss handling
             sl_offset = self.trailing_stop_positive_offset
 
@@ -867,13 +866,13 @@ class IStrategy(ABC, HyperStrategyMixin):
 
                 trade.adjust_stop_loss(bound or current_rate, stop_loss_value)
 
+        sl_higher_short = (trade.stop_loss >= (low or current_rate) and not trade.is_short)
+        sl_lower_long = ((trade.stop_loss <= (high or current_rate) and trade.is_short))
         # evaluate if the stoploss was hit if stoploss is not on exchange
         # in Dry-Run, this handles stoploss logic as well, as the logic will not be different to
         # regular stoploss handling.
-        if ((
-            (trade.stop_loss >= (low or current_rate) and not trade.is_short) or
-            ((trade.stop_loss <= (high or current_rate) and trade.is_short))
-        ) and (not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])):
+        if ((sl_higher_short or sl_lower_long) and
+                (not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])):
 
             sell_type = SellType.STOP_LOSS
 
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index d5b820d2b..9d817bc91 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -3238,11 +3238,11 @@ def test_execute_trade_exit_market_order(
     """
     amount
         long: 60 / 2.0 = 30
-        short: 60 / 2.02 = 29.70297029 
-    open_value 
+        short: 60 / 2.02 = 29.70297029
+    open_value
         long: (30 * 2.0) + (30 * 2.0 * 0.0025) = 60.15
         short: (29.702970297029704 * 2.02) - (29.702970297029704 * 2.02 * 0.0025) = 59.85
-    close_value 
+    close_value
         long: (30 * 2.2) - (30 * 2.2 * 0.0025) = 65.835
         short: (29.702970297029704 * 2.3) + (29.702970297029704 * 2.3 * 0.0025) = 68.48762376237624
     profit

From 962f63a19a13060697423348cda1aeae2b753e27 Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Thu, 14 Oct 2021 05:25:26 -0600
Subject: [PATCH 086/134] fixed failing
 test_execute_trade_exit_custom_exit_price

---
 tests/test_freqtradebot.py | 25 +++++++++++++++----------
 1 file changed, 15 insertions(+), 10 deletions(-)

diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 9d817bc91..376b2e920 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -2929,9 +2929,14 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd
     } == last_msg
 
 
-@ pytest.mark.parametrize("is_short", [False, True])
-def test_execute_trade_exit_custom_exit_price(default_conf_usdt, ticker_usdt, fee,
-                                              ticker_usdt_sell_up, is_short, mocker) -> None:
+@pytest.mark.parametrize(
+    "is_short,amount,open_rate,current_rate,limit,profit_amount,profit_ratio,profit_or_loss", [
+        (False, 30, 2.0, 2.3, 2.25, 7.18125, 0.11938903, 'profit'),
+        (True, 29.70297029, 2.02, 2.2, 2.25, -7.14876237, -0.11944465, 'loss'),  # TODO-lev
+    ])
+def test_execute_trade_exit_custom_exit_price(
+        default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_up, is_short, amount, open_rate,
+        current_rate, limit, profit_amount, profit_ratio, profit_or_loss, mocker) -> None:
     rpc_mock = patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
@@ -2981,14 +2986,14 @@ def test_execute_trade_exit_custom_exit_price(default_conf_usdt, ticker_usdt, fe
         'type': RPCMessageType.SELL,
         'exchange': 'Binance',
         'pair': 'ETH/USDT',
-        'gain': 'profit',
-        'limit': 2.25,
-        'amount': 30.0,
+        'gain': profit_or_loss,
+        'limit': limit,
+        'amount': amount,
         'order_type': 'limit',
-        'open_rate': 2.0,
-        'current_rate': 2.3,
-        'profit_amount': 7.18125,
-        'profit_ratio': 0.11938903,
+        'open_rate': open_rate,
+        'current_rate': current_rate,
+        'profit_amount': profit_amount,
+        'profit_ratio': profit_ratio,
         'stake_currency': 'USDT',
         'fiat_currency': 'USD',
         'sell_reason': SellType.SELL_SIGNAL.value,

From c02a538187340fffe3515396e052c18d4116e467 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Thu, 14 Oct 2021 19:33:32 +0200
Subject: [PATCH 087/134] Add documentation and log to PerformanceFilter

---
 docs/includes/pairlists.md                      | 9 +++++++--
 freqtrade/plugins/pairlist/PerformanceFilter.py | 6 ++++++
 tests/plugins/test_pairlist.py                  | 7 ++++---
 3 files changed, 17 insertions(+), 5 deletions(-)

diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md
index b612a4ddf..3d10747d3 100644
--- a/docs/includes/pairlists.md
+++ b/docs/includes/pairlists.md
@@ -194,17 +194,22 @@ Trade count is used as a tie breaker.
 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.
 
+The optional `min_profit` parameter defines the minimum profit a pair must have to be considered.
+Pairs below this level will be filtered out.
+Using this parameter without `minutes` is highly discouraged, as it can lead to an empty pairlist without without a way to recover.
+
 ```json
 "pairlists": [
     // ...
     {
         "method": "PerformanceFilter",
-        "minutes": 1440  // rolling 24h
+        "minutes": 1440,  // rolling 24h
+        "min_profit": 0.01
     }
 ],
 ```
 
-!!! Note
+!!! Warning "Backtesting"
     `PerformanceFilter` does not support backtesting mode.
 
 #### PrecisionFilter
diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py
index f235816b8..671b6362b 100644
--- a/freqtrade/plugins/pairlist/PerformanceFilter.py
+++ b/freqtrade/plugins/pairlist/PerformanceFilter.py
@@ -70,7 +70,13 @@ class PerformanceFilter(IPairList):
             .fillna(0).sort_values(by=['count', 'pair'], ascending=True)\
             .sort_values(by=['profit'], ascending=False)
         if self._min_profit is not None:
+            removed = sorted_df[sorted_df['profit'] < self._min_profit]
+            for _, row in removed.iterrows():
+                self.log_once(
+                    f"Removing pair {row['pair']} since {row['profit']} is "
+                    f"below {self._min_profit}", logger.info)
             sorted_df = sorted_df[sorted_df['profit'] >= self._min_profit]
+
         pairlist = sorted_df['pair'].tolist()
 
         return pairlist
diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py
index cf918e2a0..c6246dccb 100644
--- a/tests/plugins/test_pairlist.py
+++ b/tests/plugins/test_pairlist.py
@@ -665,11 +665,11 @@ def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None:
 
 
 @pytest.mark.usefixtures("init_persistence")
-def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee) -> None:
+def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee, caplog) -> None:
     whitelist_conf['exchange']['pair_whitelist'].append('XRP/BTC')
     whitelist_conf['pairlists'] = [
         {"method": "StaticPairList"},
-        {"method": "PerformanceFilter", "minutes": 60}
+        {"method": "PerformanceFilter", "minutes": 60, "min_profit": 0.01}
     ]
     mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
     exchange = get_patched_exchange(mocker, whitelist_conf)
@@ -681,7 +681,8 @@ def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee) -> None:
     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']
+        assert pm.whitelist == ['XRP/BTC']
+        assert log_has_re(r'Removing pair .* since .* is below .*', caplog)
 
         # Move to "outside" of lookback window, so original sorting is restored.
         t.move_to("2021-09-01 07:00:00 +00:00")

From fe9f597eab044f06ae1f68036eded72046db7d4f Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Fri, 15 Oct 2021 10:11:25 +0200
Subject: [PATCH 088/134] Don't build ta-lib in parallel, this causes failures

---
 build_helpers/install_ta-lib.sh | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/build_helpers/install_ta-lib.sh b/build_helpers/install_ta-lib.sh
index d12b16364..00c4417ae 100755
--- a/build_helpers/install_ta-lib.sh
+++ b/build_helpers/install_ta-lib.sh
@@ -11,8 +11,13 @@ if [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then
   && curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess \
   && curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub \
   && ./configure --prefix=${INSTALL_LOC}/ \
-  && make -j$(nproc) \
-  && which sudo && sudo make install || make install
+  && make
+  if [ $? -ne 0 ]; then
+    echo "Failed building ta-lib."
+    cd .. && rm -rf ./ta-lib/
+    exit 1
+  fi
+  which sudo && sudo make install || make install
   if [ -x "$(command -v apt-get)" ]; then
     echo "Updating library path using ldconfig"
     sudo ldconfig

From de5657a91b88eff9e65ff6e24a432d792c539002 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 12 Oct 2021 07:00:07 +0200
Subject: [PATCH 089/134] Fix test failing when UI is installed

---
 tests/rpc/test_rpc_apiserver.py | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py
index ac76bbd11..7aa057d09 100644
--- a/tests/rpc/test_rpc_apiserver.py
+++ b/tests/rpc/test_rpc_apiserver.py
@@ -95,7 +95,7 @@ def test_api_not_found(botclient):
     assert rc.json() == {"detail": "Not Found"}
 
 
-def test_api_ui_fallback(botclient):
+def test_api_ui_fallback(botclient, mocker):
     ftbot, client = botclient
 
     rc = client_get(client, "/favicon.ico")
@@ -109,9 +109,16 @@ def test_api_ui_fallback(botclient):
     rc = client_get(client, "/something")
     assert rc.status_code == 200
 
-    # Test directory traversal
+    # Test directory traversal without mock
     rc = client_get(client, '%2F%2F%2Fetc/passwd')
     assert rc.status_code == 200
+    # Allow both fallback or real UI
+    assert '`freqtrade install-ui`' in rc.text or '' in rc.text
+
+    mocker.patch.object(Path, 'is_file', MagicMock(side_effect=[True, False]))
+    rc = client_get(client, '%2F%2F%2Fetc/passwd')
+    assert rc.status_code == 200
+
     assert '`freqtrade install-ui`' in rc.text
 
 

From 7f1080368b224d9ec7b8485cdf21a6b055494318 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 12 Oct 2021 06:53:15 +0200
Subject: [PATCH 090/134] Commit mock-trades to avoid errors in tests

---
 tests/conftest.py | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/tests/conftest.py b/tests/conftest.py
index 49534c88d..470eaa6d8 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -226,6 +226,7 @@ def create_mock_trades(fee, use_db: bool = True):
     add_trade(trade)
 
     if use_db:
+        Trade.commit()
         Trade.query.session.flush()
 
 
@@ -259,6 +260,7 @@ def create_mock_trades_usdt(fee, use_db: bool = True):
     add_trade(trade)
 
     if use_db:
+        Trade.commit()
         Trade.query.session.flush()
 
 

From dcefb3eb9c91479ea17f343c339ffc3554c3166d Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 12 Oct 2021 07:11:34 +0200
Subject: [PATCH 091/134] Fix delete_Trade api test

---
 tests/rpc/test_rpc_apiserver.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py
index 7aa057d09..9a03158ae 100644
--- a/tests/rpc/test_rpc_apiserver.py
+++ b/tests/rpc/test_rpc_apiserver.py
@@ -620,10 +620,11 @@ def test_api_delete_trade(botclient, mocker, fee, markets):
     assert_response(rc, 502)
 
     create_mock_trades(fee)
-    Trade.query.session.flush()
+
     ftbot.strategy.order_types['stoploss_on_exchange'] = True
     trades = Trade.query.all()
     trades[1].stoploss_order_id = '1234'
+    Trade.commit()
     assert len(trades) > 2
 
     rc = client_delete(client, f"{BASE_URI}/trades/1")

From 5ba1d66be7f86ad9bf467e3238fd45b9e709b2e0 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sat, 16 Oct 2021 17:57:51 +0200
Subject: [PATCH 092/134] Make sure transactions are reset

closes #5719
---
 freqtrade/rpc/api_server/deps.py | 2 ++
 freqtrade/rpc/telegram.py        | 4 +++-
 2 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/freqtrade/rpc/api_server/deps.py b/freqtrade/rpc/api_server/deps.py
index d2459010f..77870722d 100644
--- a/freqtrade/rpc/api_server/deps.py
+++ b/freqtrade/rpc/api_server/deps.py
@@ -1,5 +1,6 @@
 from typing import Any, Dict, Optional
 
+from freqtrade.persistence import Trade
 from freqtrade.rpc.rpc import RPC, RPCException
 
 from .webserver import ApiServer
@@ -14,6 +15,7 @@ def get_rpc_optional() -> Optional[RPC]:
 def get_rpc() -> Optional[RPC]:
     _rpc = get_rpc_optional()
     if _rpc:
+        Trade.query.session.rollback()
         return _rpc
     else:
         raise RPCException('Bot is not in the correct state')
diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 059ba9c41..846747f40 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -25,6 +25,7 @@ from freqtrade.constants import DUST_PER_COIN
 from freqtrade.enums import RPCMessageType
 from freqtrade.exceptions import OperationalException
 from freqtrade.misc import chunks, plural, round_coin_value
+from freqtrade.persistence import Trade
 from freqtrade.rpc import RPC, RPCException, RPCHandler
 
 
@@ -59,7 +60,8 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
                 update.message.chat_id
             )
             return wrapper
-
+        # Rollback session to avoid getting data stored in a transaction.
+        Trade.query.session.rollback()
         logger.debug(
             'Executing handler: %s for chat_id: %s',
             command_handler.__name__,

From 89ca8abea9cab3efee896e6afd5e9e2c38a67f40 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 11 Oct 2021 06:16:06 +0000
Subject: [PATCH 093/134] Bump fastapi from 0.68.1 to 0.70.0

Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.68.1 to 0.70.0.
- [Release notes](https://github.com/tiangolo/fastapi/releases)
- [Commits](https://github.com/tiangolo/fastapi/compare/0.68.1...0.70.0)

---
updated-dependencies:
- dependency-name: fastapi
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index af6ef974e..f3eb65c59 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -32,7 +32,7 @@ python-rapidjson==1.4
 sdnotify==0.3.2
 
 # API Server
-fastapi==0.68.1
+fastapi==0.70.0
 uvicorn==0.15.0
 pyjwt==2.2.0
 aiofiles==0.7.0

From 5a9983086a75275f2fbeb7f0569e7a3a09277b71 Mon Sep 17 00:00:00 2001
From: daniila 
Date: Sun, 17 Oct 2021 00:24:00 +0300
Subject: [PATCH 094/134] How to run multiple instances with docker

Basic guide on how to run multiple instances using docker.
---
 docs/advanced-setup.md | 65 ++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 65 insertions(+)

diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md
index f03bc10c0..e7e5b6cec 100644
--- a/docs/advanced-setup.md
+++ b/docs/advanced-setup.md
@@ -52,6 +52,71 @@ freqtrade trade -c MyConfigUSDT.json -s MyCustomStrategy --db-url sqlite:///user
 
 For more information regarding usage of the sqlite databases, for example to manually enter or remove trades, please refer to the [SQL Cheatsheet](sql_cheatsheet.md).
 
+### Multiple instances using docker
+
+To run multiple instances of freqtrade using docker you will need to edit the docker-compose.yml file and add all the instances you want as separate services. Remember, you can separate your configuration into multiple files, so it's a good idea to think about making them modular, then if you need to edit something common to all bots, you can do that in a single config file. 
+```
+---
+version: '3'
+services:
+  freqtrade1:
+    image: freqtradeorg/freqtrade:stable
+    # image: freqtradeorg/freqtrade:develop
+    # Use plotting image
+    # image: freqtradeorg/freqtrade:develop_plot
+    # Build step - only needed when additional dependencies are needed
+    # build:
+    #   context: .
+    #   dockerfile: "./docker/Dockerfile.custom"
+    restart: always
+    container_name: freqtrade1
+    volumes:
+      - "./user_data:/freqtrade/user_data"
+    # Expose api on port 8080 (localhost only)
+    # Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation
+    # before enabling this.
+     ports:
+     - "127.0.0.1:8080:8080"
+    # Default command used when running `docker compose up`
+    command: >
+      trade
+      --logfile /freqtrade/user_data/logs/freqtrade1.log
+      --db-url sqlite:////freqtrade/user_data/tradesv3_freqtrade1.sqlite
+      --config /freqtrade/user_data/config.json
+      --config /freqtrade/user_data/config.freqtrade1.json
+      --strategy SampleStrategy
+  
+  freqtrade2:
+    image: freqtradeorg/freqtrade:stable
+    # image: freqtradeorg/freqtrade:develop
+    # Use plotting image
+    # image: freqtradeorg/freqtrade:develop_plot
+    # Build step - only needed when additional dependencies are needed
+    # build:
+    #   context: .
+    #   dockerfile: "./docker/Dockerfile.custom"
+    restart: always
+    container_name: freqtrade2
+    volumes:
+      - "./user_data:/freqtrade/user_data"
+    # Expose api on port 8080 (localhost only)
+    # Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation
+    # before enabling this.
+    ports:
+      - "127.0.0.1:8081:8081"
+    # Default command used when running `docker compose up`
+    command: >
+      trade
+      --logfile /freqtrade/user_data/logs/freqtrade2.log
+      --db-url sqlite:////freqtrade/user_data/tradesv3_freqtrade2.sqlite
+      --config /freqtrade/user_data/config.json
+      --config /freqtrade/user_data/config.freqtrade2.json
+      --strategy SampleStrategy
+
+```
+You can use whatever naming convention you want, freqtrade1 and 2 are arbitrary.
+
+
 ## Configure the bot running as a systemd service
 
 Copy the `freqtrade.service` file to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup.

From f61dc6d95ada089055e0e12c7927c58350b409ef Mon Sep 17 00:00:00 2001
From: Rik Helsen 
Date: Sun, 17 Oct 2021 00:14:09 +0200
Subject: [PATCH 095/134] =?UTF-8?q?=F0=9F=93=9D=20`mkdocs.yml`=20-=20Fixed?=
 =?UTF-8?q?=20darktheme=20toggle?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 mkdocs.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/mkdocs.yml b/mkdocs.yml
index 05156168f..0daf462c2 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -54,8 +54,8 @@ theme:
       primary: 'blue grey'
       accent: 'tear'
       toggle:
-        icon: material/toggle-switch-off-outline
-        name: Switch to dark mode
+        icon: material/toggle-switch
+        name: Switch to light mode
 extra_css:
   - 'stylesheets/ft.extra.css'
 extra_javascript:

From e19d95b63e49d51b1a2347fdcb908f73b9ab5620 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 17 Oct 2021 09:00:10 +0200
Subject: [PATCH 096/134] Fix stoploss test

---
 tests/test_freqtradebot.py | 16 ++++++++--------
 1 file changed, 8 insertions(+), 8 deletions(-)

diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 376b2e920..86cac8b82 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -1438,7 +1438,6 @@ def test_handle_stoploss_on_exchange_trailing_error(
 
 
 @pytest.mark.parametrize("is_short", [False, True])
-@pytest.mark.usefixtures("init_persistence")
 def test_handle_stoploss_on_exchange_custom_stop(
     mocker, default_conf_usdt, fee, is_short, limit_order
 ) -> None:
@@ -1513,9 +1512,9 @@ def test_handle_stoploss_on_exchange_custom_stop(
     mocker.patch(
         'freqtrade.exchange.Exchange.fetch_ticker',
         MagicMock(return_value={
-            'bid': 4.38,
-            'ask': 4.4,
-            'last': 4.38
+            'bid': 4.38 if not is_short else 1.9 / 2,
+            'ask': 4.4 if not is_short else 2.2 / 2,
+            'last': 4.38 if not is_short else 1.9 / 2,
         })
     )
 
@@ -1531,8 +1530,8 @@ def test_handle_stoploss_on_exchange_custom_stop(
     stoploss_order_mock.assert_not_called()
 
     assert freqtrade.handle_trade(trade) is False
-    assert trade.stop_loss == 4.4 * 0.96
-    assert trade.stop_loss_pct == -0.04
+    assert trade.stop_loss == 4.4 * 0.96 if not is_short else 1.1
+    assert trade.stop_loss_pct == -0.04 if not is_short else 0.04
 
     # setting stoploss_on_exchange_interval to 0 seconds
     freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0
@@ -1540,11 +1539,12 @@ def test_handle_stoploss_on_exchange_custom_stop(
     assert freqtrade.handle_stoploss_on_exchange(trade) is False
 
     cancel_order_mock.assert_called_once_with(100, 'ETH/USDT')
+    # Long uses modified ask - offset, short modified bid + offset
     stoploss_order_mock.assert_called_once_with(
-        amount=31.57894736,
+        amount=trade.amount,
         pair='ETH/USDT',
         order_types=freqtrade.strategy.order_types,
-        stop_price=4.4 * 0.96,
+        stop_price=4.4 * 0.96 if not is_short else 0.95 * 1.04,
         side=exit_side(is_short),
         leverage=1.0
     )

From bc10b451fec47f8fbc616f62e4489160b20e28ce Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 17 Oct 2021 09:46:39 +0200
Subject: [PATCH 097/134] Revert wrong condition

---
 freqtrade/strategy/interface.py | 16 +++++-----------
 1 file changed, 5 insertions(+), 11 deletions(-)

diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py
index 05df0c6fb..94541218c 100644
--- a/freqtrade/strategy/interface.py
+++ b/freqtrade/strategy/interface.py
@@ -840,9 +840,9 @@ class IStrategy(ABC, HyperStrategyMixin):
             else:
                 logger.warning("CustomStoploss function did not return valid stoploss")
 
-        sl_lower_short = (trade.stop_loss < (low or current_rate) and not trade.is_short)
-        sl_higher_long = (trade.stop_loss > (high or current_rate) and trade.is_short)
-        if self.trailing_stop and (sl_lower_short or sl_higher_long):
+        sl_lower_long = (trade.stop_loss < (low or current_rate) and not trade.is_short)
+        sl_higher_short = (trade.stop_loss > (high or current_rate) and trade.is_short)
+        if self.trailing_stop and (sl_lower_long or sl_higher_short):
             # trailing stoploss handling
             sl_offset = self.trailing_stop_positive_offset
 
@@ -851,15 +851,9 @@ class IStrategy(ABC, HyperStrategyMixin):
             bound_profit = current_profit if not bound else trade.calc_profit_ratio(bound)
 
             # Don't update stoploss if trailing_only_offset_is_reached is true.
-            if not (self.trailing_only_offset_is_reached and (
-                (bound_profit < sl_offset and not trade.is_short) or
-                (bound_profit > sl_offset and trade.is_short)
-            )):
+            if not (self.trailing_only_offset_is_reached and bound_profit < sl_offset):
                 # Specific handling for trailing_stop_positive
-                if self.trailing_stop_positive is not None and (
-                    (bound_profit > sl_offset and not trade.is_short) or
-                    (bound_profit < sl_offset and trade.is_short)
-                ):
+                if self.trailing_stop_positive is not None and bound_profit > sl_offset:
                     stop_loss_value = self.trailing_stop_positive
                     logger.debug(f"{trade.pair} - Using positive stoploss: {stop_loss_value} "
                                  f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%")

From 41b5e5627b1ec7a412cf2dc238df1edab84b0129 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 17 Oct 2021 09:54:38 +0200
Subject: [PATCH 098/134] Update stoploss test

---
 tests/test_freqtradebot.py | 23 +++++++++++++----------
 1 file changed, 13 insertions(+), 10 deletions(-)

diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 86cac8b82..dd6a1e257 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -3630,9 +3630,9 @@ def test_trailing_stop_loss(default_conf_usdt, limit_order_open,
     (0, False, 2.0394, False),
     (0.011, False, 2.0394, False),
     (0.055, True, 1.8, False),
-    (0, False, 2.1606, True),
-    (0.011, False, 2.1606, True),
-    (0.055, True, 2.4, True),
+    (0, False, 2.1614, True),
+    (0.011, False, 2.1614, True),
+    (0.055, True, 2.42, True),
 ])
 def test_trailing_stop_loss_positive(
     default_conf_usdt, limit_order, limit_order_open,
@@ -3684,27 +3684,29 @@ def test_trailing_stop_loss_positive(
     )
     # stop-loss not reached, adjusted stoploss
     assert freqtrade.handle_trade(trade) is False
-    caplog_text = f"ETH/USDT - Using positive stoploss: 0.01 offset: {offset} profit: 0.0249%"
+    caplog_text = (f"ETH/USDT - Using positive stoploss: 0.01 offset: {offset} profit: "
+                   f"{'0.0249' if not is_short else '0.0224'}%")
     if trail_if_reached:
         assert not log_has(caplog_text, caplog)
         assert not log_has("ETH/USDT - Adjusting stoploss...", caplog)
     else:
         assert log_has(caplog_text, caplog)
         assert log_has("ETH/USDT - Adjusting stoploss...", caplog)
-    assert trade.stop_loss == second_sl
+    assert pytest.approx(trade.stop_loss) == second_sl
     caplog.clear()
 
     mocker.patch(
         'freqtrade.exchange.Exchange.fetch_ticker',
         MagicMock(return_value={
-            'bid': enter_price + (-0.125 if is_short else 0.125),
-            'ask': enter_price + (-0.125 if is_short else 0.125),
-            'last': enter_price + (-0.125 if is_short else 0.125),
+            'bid': enter_price + (-0.135 if is_short else 0.125),
+            'ask': enter_price + (-0.135 if is_short else 0.125),
+            'last': enter_price + (-0.135 if is_short else 0.125),
         })
     )
     assert freqtrade.handle_trade(trade) is False
     assert log_has(
-        f"ETH/USDT - Using positive stoploss: 0.01 offset: {offset} profit: 0.0572%",
+        f"ETH/USDT - Using positive stoploss: 0.01 offset: {offset} profit: "
+        f"{'0.0572' if not is_short else '0.0567'}%",
         caplog
     )
     assert log_has("ETH/USDT - Adjusting stoploss...", caplog)
@@ -3722,7 +3724,8 @@ def test_trailing_stop_loss_positive(
     assert log_has(
         f"ETH/USDT - HIT STOP: current price at {enter_price + (-0.02 if is_short else 0.02):.6f}, "
         f"stoploss is {trade.stop_loss:.6f}, "
-        f"initial stoploss was at {2.2 if is_short else 1.8}00000, trade opened at 2.000000",
+        f"initial stoploss was at {'2.42' if is_short else '1.80'}0000, "
+        f"trade opened at {2.2 if is_short else 2.0}00000",
         caplog)
     assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value
 

From fb2c8f7621b4d3a9d9644e9f499e21442cd1c258 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 17 Oct 2021 08:36:11 +0200
Subject: [PATCH 099/134] Rollback after each request

This closes the transaction and avoids "sticking" transactions.
---
 freqtrade/rpc/api_server/deps.py | 7 ++++---
 tests/conftest.py                | 2 --
 tests/rpc/test_rpc_apiserver.py  | 6 +-----
 3 files changed, 5 insertions(+), 10 deletions(-)

diff --git a/freqtrade/rpc/api_server/deps.py b/freqtrade/rpc/api_server/deps.py
index 77870722d..16f9a78c0 100644
--- a/freqtrade/rpc/api_server/deps.py
+++ b/freqtrade/rpc/api_server/deps.py
@@ -1,4 +1,4 @@
-from typing import Any, Dict, Optional
+from typing import Any, Dict, Iterator, Optional
 
 from freqtrade.persistence import Trade
 from freqtrade.rpc.rpc import RPC, RPCException
@@ -12,11 +12,12 @@ def get_rpc_optional() -> Optional[RPC]:
     return None
 
 
-def get_rpc() -> Optional[RPC]:
+def get_rpc() -> Optional[Iterator[RPC]]:
     _rpc = get_rpc_optional()
     if _rpc:
         Trade.query.session.rollback()
-        return _rpc
+        yield _rpc
+        Trade.query.session.rollback()
     else:
         raise RPCException('Bot is not in the correct state')
 
diff --git a/tests/conftest.py b/tests/conftest.py
index 470eaa6d8..b35a220df 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -227,7 +227,6 @@ def create_mock_trades(fee, use_db: bool = True):
 
     if use_db:
         Trade.commit()
-        Trade.query.session.flush()
 
 
 def create_mock_trades_usdt(fee, use_db: bool = True):
@@ -261,7 +260,6 @@ def create_mock_trades_usdt(fee, use_db: bool = True):
 
     if use_db:
         Trade.commit()
-        Trade.query.session.flush()
 
 
 @pytest.fixture(autouse=True)
diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py
index 9a03158ae..02ed26459 100644
--- a/tests/rpc/test_rpc_apiserver.py
+++ b/tests/rpc/test_rpc_apiserver.py
@@ -570,7 +570,6 @@ def test_api_trades(botclient, mocker, fee, markets):
     assert rc.json()['total_trades'] == 0
 
     create_mock_trades(fee)
-    Trade.query.session.flush()
 
     rc = client_get(client, f"{BASE_URI}/trades")
     assert_response(rc)
@@ -597,7 +596,6 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets):
     assert rc.json()['detail'] == 'Trade not found.'
 
     create_mock_trades(fee)
-    Trade.query.session.flush()
 
     rc = client_get(client, f"{BASE_URI}/trade/3")
     assert_response(rc)
@@ -694,7 +692,6 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
     assert rc.json() == {"error": "Error querying /api/v1/edge: Edge is not enabled."}
 
 
-@pytest.mark.usefixtures("init_persistence")
 def test_api_profit(botclient, mocker, ticker, fee, markets):
     ftbot, client = botclient
     patch_get_signal(ftbot)
@@ -745,7 +742,6 @@ def test_api_profit(botclient, mocker, ticker, fee, markets):
                          }
 
 
-@pytest.mark.usefixtures("init_persistence")
 def test_api_stats(botclient, mocker, ticker, fee, markets,):
     ftbot, client = botclient
     patch_get_signal(ftbot)
@@ -811,7 +807,7 @@ def test_api_performance(botclient, fee):
     trade.close_profit_abs = trade.calc_profit()
 
     Trade.query.session.add(trade)
-    Trade.query.session.flush()
+    Trade.commit()
 
     rc = client_get(client, f"{BASE_URI}/performance")
     assert_response(rc)

From abd5c4f27855c3486badc00b5efb9330e3aeb52b Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 17 Oct 2021 10:39:53 +0200
Subject: [PATCH 100/134] Convert additional test to USDT

---
 tests/test_freqtradebot.py | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index f57e35ca1..838a158e0 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -3378,9 +3378,9 @@ def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usd
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
         fetch_ticker=MagicMock(return_value={
-            'bid': 0.00000172,
-            'ask': 0.00000173,
-            'last': 0.00000172
+            'bid': 2.0,
+            'ask': 2.0,
+            'last': 2.0
         }),
         create_order=MagicMock(side_effect=[
             limit_buy_order_usdt_open,
@@ -3408,7 +3408,7 @@ def test_disable_ignore_roi_if_buy_signal(default_conf_usdt, limit_buy_order_usd
     # Test if buy-signal is absent
     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 == SellType.ROI.value
 
 
 def test_get_real_amount_quote(default_conf_usdt, trades_for_order, buy_order_fee, fee, caplog,

From e8f98e473d6406dc8589d82731abdfa99d9d64de Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 17 Oct 2021 10:55:20 +0200
Subject: [PATCH 101/134] Fix a few more tests

---
 tests/test_freqtradebot.py | 101 +++++++++++++++++++------------------
 1 file changed, 53 insertions(+), 48 deletions(-)

diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 3849731e3..f381caba4 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -2799,10 +2799,10 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None:
 
 @ pytest.mark.parametrize("is_short, open_rate, amt", [
     (False, 2.0, 30.0),
-    (True, 2.02, 29.7029703),
+    (True, 2.02, 29.70297029),
 ])
 def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_up, mocker,
-                               is_short, open_rate, amt) -> None:
+                               ticker_usdt_sell_down, is_short, open_rate, amt) -> None:
     rpc_mock = patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
@@ -2821,20 +2821,19 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
     rpc_mock.reset_mock()
 
     trade = Trade.query.first()
-    trade.is_short = is_short
+    assert trade.is_short == is_short
     assert trade
     assert freqtrade.strategy.confirm_trade_exit.call_count == 0
 
     # Increase the price and sell it
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
-        fetch_ticker=ticker_usdt_sell_up
+        fetch_ticker=ticker_usdt_sell_down if is_short else ticker_usdt_sell_up
     )
     # Prevented sell ...
-    # TODO-lev: side="buy"
     freqtrade.execute_trade_exit(
         trade=trade,
-        limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'],
+        limit=(ticker_usdt_sell_down()['ask'] if is_short else ticker_usdt_sell_up()['bid']),
         sell_reason=SellCheckTuple(sell_type=SellType.ROI)
     )
     assert rpc_mock.call_count == 0
@@ -2842,10 +2841,9 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
 
     # Repatch with true
     freqtrade.strategy.confirm_trade_exit = MagicMock(return_value=True)
-    # TODO-lev: side="buy"
     freqtrade.execute_trade_exit(
         trade=trade,
-        limit=ticker_usdt_sell_up()['ask' if is_short else 'bid'],
+        limit=(ticker_usdt_sell_down()['ask'] if is_short else ticker_usdt_sell_up()['bid']),
         sell_reason=SellCheckTuple(sell_type=SellType.ROI)
     )
     assert freqtrade.strategy.confirm_trade_exit.call_count == 1
@@ -2858,13 +2856,13 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
         'exchange': 'Binance',
         'pair': 'ETH/USDT',
         'gain': 'profit',
-        'limit': 2.2,
+        'limit': 2.0 if is_short else 2.2,
         'amount': amt,
         'order_type': 'limit',
         'open_rate': open_rate,
-        'current_rate': 2.3,
-        'profit_amount': 5.685,
-        'profit_ratio': 0.09451372,
+        'current_rate': 2.01 if is_short else 2.3,
+        'profit_amount': 0.29554455 if is_short else 5.685,
+        'profit_ratio': 0.00493809 if is_short else 0.09451372,
         'stake_currency': 'USDT',
         'fiat_currency': 'USD',
         'sell_reason': SellType.ROI.value,
@@ -2876,7 +2874,7 @@ def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_
 
 @ pytest.mark.parametrize("is_short", [False, True])
 def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_down,
-                                 mocker, is_short) -> None:
+                                 ticker_usdt_sell_up, mocker, is_short) -> None:
     rpc_mock = patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
@@ -2899,11 +2897,11 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd
     # Decrease the price and sell it
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
-        fetch_ticker=ticker_usdt_sell_down
+        fetch_ticker=ticker_usdt_sell_up if is_short else ticker_usdt_sell_down
     )
-    # TODO-lev: side="buy"
-    freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_down()['bid'],
-                                 sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
+    freqtrade.execute_trade_exit(
+        trade=trade, limit=(ticker_usdt_sell_up if is_short else ticker_usdt_sell_down)()['bid'],
+        sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
 
     assert rpc_mock.call_count == 2
     last_msg = rpc_mock.call_args_list[-1][0][0]
@@ -2913,13 +2911,13 @@ def test_execute_trade_exit_down(default_conf_usdt, ticker_usdt, fee, ticker_usd
         'exchange': 'Binance',
         'pair': 'ETH/USDT',
         'gain': 'loss',
-        'limit': 2.01,
-        'amount': 30.0,
+        'limit': 2.2 if is_short else 2.01,
+        'amount': 29.70297029 if is_short else 30.0,
         'order_type': 'limit',
-        'open_rate': 2.0,
-        'current_rate': 2.0,
-        'profit_amount': -0.00075,
-        'profit_ratio': -1.247e-05,
+        'open_rate': 2.02 if is_short else 2.0,
+        'current_rate': 2.2 if is_short else 2.0,
+        'profit_amount': -5.65990099 if is_short else -0.00075,
+        'profit_ratio': -0.0945681 if is_short else -1.247e-05,
         'stake_currency': 'USDT',
         'fiat_currency': 'USD',
         'sell_reason': SellType.STOP_LOSS.value,
@@ -3005,7 +3003,8 @@ def test_execute_trade_exit_custom_exit_price(
 
 @ pytest.mark.parametrize("is_short", [False, True])
 def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
-        default_conf_usdt, ticker_usdt, fee, is_short, ticker_usdt_sell_down, mocker) -> None:
+        default_conf_usdt, ticker_usdt, fee, is_short, ticker_usdt_sell_down,
+        ticker_usdt_sell_up, mocker) -> None:
     rpc_mock = patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
@@ -3022,23 +3021,23 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
     freqtrade.enter_positions()
 
     trade = Trade.query.first()
-    trade.is_short = is_short
+    assert trade.is_short == is_short
     assert trade
 
     # Decrease the price and sell it
     mocker.patch.multiple(
         'freqtrade.exchange.Exchange',
-        fetch_ticker=ticker_usdt_sell_down
+        fetch_ticker=ticker_usdt_sell_up if is_short else ticker_usdt_sell_down
     )
 
     default_conf_usdt['dry_run'] = True
     freqtrade.strategy.order_types['stoploss_on_exchange'] = True
     # Setting trade stoploss to 0.01
 
-    trade.stop_loss = 2.0 * 0.99
-    # TODO-lev: side="buy"
-    freqtrade.execute_trade_exit(trade=trade, limit=ticker_usdt_sell_down()['bid'],
-                                 sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
+    trade.stop_loss = 2.0 * 1.01 if is_short else 2.0 * 0.99
+    freqtrade.execute_trade_exit(
+        trade=trade, limit=(ticker_usdt_sell_up if is_short else ticker_usdt_sell_down())['bid'],
+        sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS))
 
     assert rpc_mock.call_count == 2
     last_msg = rpc_mock.call_args_list[-1][0][0]
@@ -3049,13 +3048,13 @@ def test_execute_trade_exit_down_stoploss_on_exchange_dry_run(
         'exchange': 'Binance',
         'pair': 'ETH/USDT',
         'gain': 'loss',
-        'limit': 1.98,
-        'amount': 30.0,
+        'limit': 2.02 if is_short else 1.98,
+        'amount': 29.70297029 if is_short else 30.0,
         'order_type': 'limit',
-        'open_rate': 2.0,
-        'current_rate': 2.0,
-        'profit_amount': -0.8985,
-        'profit_ratio': -0.01493766,
+        'open_rate': 2.02 if is_short else 2.0,
+        'current_rate': 2.2 if is_short else 2.0,
+        'profit_amount': -0.3 if is_short else -0.8985,
+        'profit_ratio': -0.00501253 if is_short else -0.01493766,
         'stake_currency': 'USDT',
         'fiat_currency': 'USD',
         'sell_reason': SellType.STOP_LOSS.value,
@@ -3570,9 +3569,12 @@ def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_order, limit_order_op
     assert trade.sell_reason == SellType.ROI.value
 
 
-@ pytest.mark.parametrize("is_short", [False, True])
+@ pytest.mark.parametrize("is_short,val1,val2", [
+    (False, 1.5, 1.1),
+    (True, 0.5, 0.9)
+    ])
 def test_trailing_stop_loss(default_conf_usdt, limit_order_open,
-                            is_short, fee, caplog, mocker) -> None:
+                            is_short, val1, val2, fee, caplog, mocker) -> None:
     patch_RPCManager(mocker)
     patch_exchange(mocker)
     mocker.patch.multiple(
@@ -3596,15 +3598,15 @@ def test_trailing_stop_loss(default_conf_usdt, limit_order_open,
 
     freqtrade.enter_positions()
     trade = Trade.query.first()
-    trade.is_short = is_short
+    assert trade.is_short == is_short
     assert freqtrade.handle_trade(trade) is False
 
-    # Raise ticker_usdt above buy price
+    # Raise praise into profits
     mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
                  MagicMock(return_value={
-                     'bid': 2.0 * 1.5,
-                     'ask': 2.0 * 1.5,
-                     'last': 2.0 * 1.5
+                     'bid': 2.0 * val1,
+                     'ask': 2.0 * val1,
+                     'last': 2.0 * val1
                  }))
 
     # Stoploss should be adjusted
@@ -3613,16 +3615,19 @@ def test_trailing_stop_loss(default_conf_usdt, limit_order_open,
     # Price fell
     mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
                  MagicMock(return_value={
-                     'bid': 2.0 * 1.1,
-                     'ask': 2.0 * 1.1,
-                     'last': 2.0 * 1.1
+                     'bid': 2.0 * val2,
+                     'ask': 2.0 * val2,
+                     'last': 2.0 * val2
                  }))
 
     caplog.set_level(logging.DEBUG)
     # Sell as trailing-stop is reached
     assert freqtrade.handle_trade(trade) is True
-    assert log_has("ETH/USDT - HIT STOP: current price at 2.200000, stoploss is 2.700000, "
-                   "initial stoploss was at 1.800000, trade opened at 2.000000", caplog)
+    stop_multi = 1.1 if is_short else 0.9
+    assert log_has(f"ETH/USDT - HIT STOP: current price at {(2.0 * val2):6f}, "
+                   f"stoploss is {(2.0 * val1 * stop_multi):6f}, "
+                   f"initial stoploss was at {(2.0 * stop_multi):6f}, trade opened at 2.000000",
+                   caplog)
     assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value
 
 

From e23eb99abf4bd19465f89192329100b4b7e2381d Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 17 Oct 2021 11:23:58 +0200
Subject: [PATCH 102/134] Disable ability to use lookahead-biased vwap

closes #5782
---
 freqtrade/vendor/qtpylib/indicators.py | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/freqtrade/vendor/qtpylib/indicators.py b/freqtrade/vendor/qtpylib/indicators.py
index 4c0fb5b5c..4f14ae13c 100644
--- a/freqtrade/vendor/qtpylib/indicators.py
+++ b/freqtrade/vendor/qtpylib/indicators.py
@@ -339,11 +339,13 @@ def vwap(bars):
     (input can be pandas series or numpy array)
     bars are usually mid [ (h+l)/2 ] or typical [ (h+l+c)/3 ]
     """
-    typical = ((bars['high'] + bars['low'] + bars['close']) / 3).values
-    volume = bars['volume'].values
+    raise ValueError("using `qtpylib.vwap` facilitates lookahead bias. Please use "
+                     "`qtpylib.rolling_vwap` instead, which calculates vwap in a rolling manner.")
+    # typical = ((bars['high'] + bars['low'] + bars['close']) / 3).values
+    # volume = bars['volume'].values
 
-    return pd.Series(index=bars.index,
-                     data=np.cumsum(volume * typical) / np.cumsum(volume))
+    # return pd.Series(index=bars.index,
+    #                  data=np.cumsum(volume * typical) / np.cumsum(volume))
 
 
 # ---------------------------------------------

From d4d57f00027d056ab94ce1eb7c3410f0370d8dcd Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 17 Oct 2021 16:09:56 +0200
Subject: [PATCH 103/134] Document expansion of `--pairs`, add
 download-inactive

---
 docs/data-download.md                    | 141 +++++++++++++----------
 freqtrade/commands/arguments.py          |   6 +-
 freqtrade/commands/cli_options.py        |   5 +
 freqtrade/commands/data_commands.py      |   9 +-
 freqtrade/configuration/configuration.py |   3 +
 tests/commands/test_commands.py          |  40 +++++++
 6 files changed, 138 insertions(+), 66 deletions(-)

diff --git a/docs/data-download.md b/docs/data-download.md
index 5f605c404..6c7d5312d 100644
--- a/docs/data-download.md
+++ b/docs/data-download.md
@@ -22,6 +22,7 @@ usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
                                [-d PATH] [--userdir PATH]
                                [-p PAIRS [PAIRS ...]] [--pairs-file FILE]
                                [--days INT] [--new-pairs-days INT]
+                               [--include-inactive-pairs]
                                [--timerange TIMERANGE] [--dl-trades]
                                [--exchange EXCHANGE]
                                [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]]
@@ -38,6 +39,8 @@ optional arguments:
   --days INT            Download data for given number of days.
   --new-pairs-days INT  Download data of new pairs for given number of days.
                         Default: `None`.
+  --include-inactive-pairs
+                        Also download data from inactive pairs.
   --timerange TIMERANGE
                         Specify what timerange of data to use.
   --dl-trades           Download trades instead of OHLCV data. The bot will
@@ -52,10 +55,10 @@ optional arguments:
                         exchange/pairs/timeframes.
   --data-format-ohlcv {json,jsongz,hdf5}
                         Storage format for downloaded candle (OHLCV) data.
-                        (default: `None`).
+                        (default: `json`).
   --data-format-trades {json,jsongz,hdf5}
                         Storage format for downloaded trades data. (default:
-                        `None`).
+                        `jsongz`).
 
 Common arguments:
   -v, --verbose         Verbose mode (-vv for more, -vvv to get all messages).
@@ -80,6 +83,82 @@ Common arguments:
 
     For that reason, `download-data` does not care about the "startup-period" defined in a strategy. It's up to the user to download additional days if the backtest should start at a specific point in time (while respecting startup period).
 
+### Pairs file
+
+In alternative to the whitelist from `config.json`, a `pairs.json` file can be used.
+If you are using Binance for example:
+
+- create a directory `user_data/data/binance` and copy or create the `pairs.json` file in that directory.
+- update the `pairs.json` file to contain the currency pairs you are interested in.
+
+```bash
+mkdir -p user_data/data/binance
+touch user_data/data/binance/pairs.json
+```
+
+The format of the `pairs.json` file is a simple json list.
+Mixing different stake-currencies is allowed for this file, since it's only used for downloading.
+
+``` json
+[
+    "ETH/BTC",
+    "ETH/USDT",
+    "BTC/USDT",
+    "XRP/ETH"
+]
+```
+
+!!! Tip "Downloading all data for one quote currency"
+    Often, you'll want to download data for all pairs of a specific quote-currency. In such cases, you can use the following shorthand:
+    `freqtrade download-data --exchange binance --pairs .*/USDT <...>`. The provided "pairs" string will be expanded to contain all active pairs on the exchange.
+    To also download data for inactive (delisted) pairs, add `--include-inactive-pairs` to the command.
+
+??? Note "Permission denied errors"
+    If your configuration directory `user_data` was made by docker, you may get the following error:
+
+    ```
+    cp: cannot create regular file 'user_data/data/binance/pairs.json': Permission denied
+    ```
+
+    You can fix the permissions of your user-data directory as follows:
+
+    ```
+    sudo chown -R $UID:$GID user_data
+    ```
+
+### Start download
+
+Then run:
+
+```bash
+freqtrade download-data --exchange binance
+```
+
+This will download historical candle (OHLCV) data for all the currency pairs you defined in `pairs.json`.
+
+Alternatively, specify the pairs directly
+
+```bash
+freqtrade download-data --exchange binance --pairs ETH/USDT XRP/USDT BTC/USDT
+```
+
+or as regex (to download all active USDT pairs)
+
+```bash
+freqtrade download-data --exchange binance --pairs .*/USDT
+```
+
+### Other Notes
+
+- To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`.
+- To change the exchange used to download the historical data from, please use a different configuration file (you'll probably need to adjust rate limits etc.)
+- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`.
+- To download historical candle (OHLCV) data for only 10 days, use `--days 10` (defaults to 30 days).
+- To download historical candle (OHLCV) data from a fixed starting point, use `--timerange 20200101-` - which will download all data from January 1st, 2020. Eventually set end dates are ignored.
+- Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data.
+- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
+
+
 ### Data format
 
 Freqtrade currently supports 3 data-formats for both OHLCV and trades data:
@@ -312,64 +391,6 @@ ETH/BTC     5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d
 ETH/USDT    5m, 15m, 30m, 1h, 2h, 4h
 ```
 
-### Pairs file
-
-In alternative to the whitelist from `config.json`, a `pairs.json` file can be used.
-
-If you are using Binance for example:
-
-- create a directory `user_data/data/binance` and copy or create the `pairs.json` file in that directory.
-- update the `pairs.json` file to contain the currency pairs you are interested in.
-
-```bash
-mkdir -p user_data/data/binance
-cp tests/testdata/pairs.json user_data/data/binance
-```
-
-If your configuration directory `user_data` was made by docker, you may get the following error:
-
-```
-cp: cannot create regular file 'user_data/data/binance/pairs.json': Permission denied
-```
-
-You can fix the permissions of your user-data directory as follows:
-
-```
-sudo chown -R $UID:$GID user_data
-```
-
-The format of the `pairs.json` file is a simple json list.
-Mixing different stake-currencies is allowed for this file, since it's only used for downloading.
-
-``` json
-[
-    "ETH/BTC",
-    "ETH/USDT",
-    "BTC/USDT",
-    "XRP/ETH"
-]
-```
-
-### Start download
-
-Then run:
-
-```bash
-freqtrade download-data --exchange binance
-```
-
-This will download historical candle (OHLCV) data for all the currency pairs you defined in `pairs.json`.
-
-### Other Notes
-
-- To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`.
-- To change the exchange used to download the historical data from, please use a different configuration file (you'll probably need to adjust rate limits etc.)
-- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`.
-- To download historical candle (OHLCV) data for only 10 days, use `--days 10` (defaults to 30 days).
-- To download historical candle (OHLCV) data from a fixed starting point, use `--timerange 20200101-` - which will download all data from January 1st, 2020. Eventually set end dates are ignored.
-- Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data.
-- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
-
 ### Trades (tick) data
 
 By default, `download-data` sub-command downloads Candles (OHLCV) data. Some exchanges also provide historic trade-data via their API.
diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py
index 5675eb096..86d7a1923 100644
--- a/freqtrade/commands/arguments.py
+++ b/freqtrade/commands/arguments.py
@@ -63,9 +63,9 @@ ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "d
 
 ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"]
 
-ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "timerange",
-                      "download_trades", "exchange", "timeframes", "erase", "dataformat_ohlcv",
-                      "dataformat_trades"]
+ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "include_inactive",
+                      "timerange", "download_trades", "exchange", "timeframes",
+                      "erase", "dataformat_ohlcv", "dataformat_trades"]
 
 ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
                        "db_url", "trade_source", "export", "exportfilename",
diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py
index 0e08adb47..b60692c67 100644
--- a/freqtrade/commands/cli_options.py
+++ b/freqtrade/commands/cli_options.py
@@ -355,6 +355,11 @@ AVAILABLE_CLI_OPTIONS = {
         type=check_int_positive,
         metavar='INT',
     ),
+    "include_inactive": Arg(
+        '--include-inactive-pairs',
+        help='Also download data from inactive pairs.',
+        action='store_true',
+    ),
     "new_pairs_days": Arg(
         '--new-pairs-days',
         help='Download data of new pairs for given number of days. Default: `%(default)s`.',
diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py
index ee05e6c69..5dc5fe7ea 100644
--- a/freqtrade/commands/data_commands.py
+++ b/freqtrade/commands/data_commands.py
@@ -11,6 +11,7 @@ from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_oh
 from freqtrade.enums import RunMode
 from freqtrade.exceptions import OperationalException
 from freqtrade.exchange import timeframe_to_minutes
+from freqtrade.exchange.exchange import market_is_active
 from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
 from freqtrade.resolvers import ExchangeResolver
 
@@ -47,11 +48,13 @@ def start_download_data(args: Dict[str, Any]) -> None:
 
     # Init exchange
     exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
+    markets = [p for p, m in exchange.markets.items() if market_is_active(m)
+               or config.get('include_inactive')]
+    expanded_pairs = expand_pairlist(config['pairs'], markets)
+
     # Manual validations of relevant settings
     if not config['exchange'].get('skip_pair_validation', False):
-        exchange.validate_pairs(config['pairs'])
-    expanded_pairs = expand_pairlist(config['pairs'], list(exchange.markets))
-
+        exchange.validate_pairs(expanded_pairs)
     logger.info(f"About to download pairs: {expanded_pairs}, "
                 f"intervals: {config['timeframes']} to {config['datadir']}")
 
diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py
index 12dcff46a..5db3379d2 100644
--- a/freqtrade/configuration/configuration.py
+++ b/freqtrade/configuration/configuration.py
@@ -407,6 +407,9 @@ class Configuration:
         self._args_to_config(config, argname='days',
                              logstring='Detected --days: {}')
 
+        self._args_to_config(config, argname='include_inactive',
+                             logstring='Detected --include-inactive-pairs: {}')
+
         self._args_to_config(config, argname='download_trades',
                              logstring='Detected --dl-trades: {}')
 
diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py
index 6a0e741d9..6e717afdf 100644
--- a/tests/commands/test_commands.py
+++ b/tests/commands/test_commands.py
@@ -754,6 +754,46 @@ def test_download_data_no_pairs(mocker, caplog):
         start_download_data(pargs)
 
 
+def test_download_data_all_pairs(mocker, markets):
+
+    mocker.patch.object(Path, "exists", MagicMock(return_value=False))
+
+    dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data',
+                           MagicMock(return_value=["ETH/BTC", "XRP/BTC"]))
+    patch_exchange(mocker)
+    mocker.patch(
+        'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
+    )
+    args = [
+        "download-data",
+        "--exchange",
+        "binance",
+        "--pairs",
+        ".*/USDT"
+    ]
+    pargs = get_args(args)
+    pargs['config'] = None
+    start_download_data(pargs)
+    expected = set(['ETH/USDT', 'XRP/USDT', 'NEO/USDT', 'TKN/USDT'])
+    assert set(dl_mock.call_args_list[0][1]['pairs']) == expected
+    assert dl_mock.call_count == 1
+
+    dl_mock.reset_mock()
+    args = [
+        "download-data",
+        "--exchange",
+        "binance",
+        "--pairs",
+        ".*/USDT",
+        "--include-inactive-pairs",
+    ]
+    pargs = get_args(args)
+    pargs['config'] = None
+    start_download_data(pargs)
+    expected = set(['ETH/USDT', 'LTC/USDT', 'XRP/USDT', 'NEO/USDT', 'TKN/USDT'])
+    assert set(dl_mock.call_args_list[0][1]['pairs']) == expected
+
+
 def test_download_data_trades(mocker, caplog):
     dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_trades_data',
                            MagicMock(return_value=[]))

From 28483a795224ff6ac27383768136101fdc795ffb Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 17 Oct 2021 16:10:15 +0200
Subject: [PATCH 104/134] Fix doc-link in developer docs

---
 docs/developer.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/developer.md b/docs/developer.md
index bd138212b..a6c9ec322 100644
--- a/docs/developer.md
+++ b/docs/developer.md
@@ -8,7 +8,7 @@ All contributions, bug reports, bug fixes, documentation improvements, enhanceme
 
 Documentation is available at [https://freqtrade.io](https://www.freqtrade.io/) and needs to be provided with every new feature PR.
 
-Special fields for the documentation (like Note boxes, ...) can be found [here](https://squidfunk.github.io/mkdocs-material/extensions/admonition/).
+Special fields for the documentation (like Note boxes, ...) can be found [here](https://squidfunk.github.io/mkdocs-material/reference/admonitions/).
 
 To test the documentation locally use the following commands.
 

From ad2c88b991ea7e82d2074ccf29eca3d2c7364004 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 17 Oct 2021 17:00:25 +0200
Subject: [PATCH 105/134] Reduce test-code duplication by importing functions

---
 tests/conftest.py          |  8 --------
 tests/test_freqtradebot.py | 14 +++-----------
 2 files changed, 3 insertions(+), 19 deletions(-)

diff --git a/tests/conftest.py b/tests/conftest.py
index ff31a9965..2c6297d57 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -31,14 +31,6 @@ from tests.conftest_trades_usdt import (mock_trade_usdt_1, mock_trade_usdt_2, mo
                                         mock_trade_usdt_4, mock_trade_usdt_5, mock_trade_usdt_6)
 
 
-def enter_side(is_short: bool):
-    return "sell" if is_short else "buy"
-
-
-def exit_side(is_short: bool):
-    return "buy" if is_short else "sell"
-
-
 logging.getLogger('').setLevel(logging.INFO)
 
 
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index f381caba4..319e25e71 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -23,17 +23,9 @@ from freqtrade.worker import Worker
 from tests.conftest import (create_mock_trades, get_patched_freqtradebot, get_patched_worker,
                             log_has, log_has_re, patch_edge, patch_exchange, patch_get_signal,
                             patch_wallet, patch_whitelist)
-from tests.conftest_trades import (MOCK_TRADE_COUNT, mock_order_1, mock_order_2, mock_order_2_sell,
-                                   mock_order_3, mock_order_3_sell, mock_order_4,
-                                   mock_order_5_stoploss, mock_order_6_sell)
-
-
-def enter_side(is_short: bool):
-    return "sell" if is_short else "buy"
-
-
-def exit_side(is_short: bool):
-    return "buy" if is_short else "sell"
+from tests.conftest_trades import (MOCK_TRADE_COUNT, enter_side, exit_side, mock_order_1,
+                                   mock_order_2, mock_order_2_sell, mock_order_3, mock_order_3_sell,
+                                   mock_order_4, mock_order_5_stoploss, mock_order_6_sell)
 
 
 def patch_RPCManager(mocker) -> MagicMock:

From 00fc38a5dccf2efb999e92cd8e50ba0a13ea1d51 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 17 Oct 2021 19:23:51 +0200
Subject: [PATCH 106/134] Update setup.sh to correctly exit if ta-lib fails

part of #5734
---
 setup.sh | 20 ++++++++++++++------
 1 file changed, 14 insertions(+), 6 deletions(-)

diff --git a/setup.sh b/setup.sh
index aee7c80b5..1173b59b9 100755
--- a/setup.sh
+++ b/setup.sh
@@ -30,7 +30,7 @@ function check_installed_python() {
             check_installed_pip
             return
         fi
-    done 
+    done
 
     echo "No usable python found. Please make sure to have python3.7 or newer installed"
     exit 1
@@ -95,11 +95,19 @@ function install_talib() {
         return
     fi
 
-    cd build_helpers && ./install_ta-lib.sh && cd ..
+    cd build_helpers && ./install_ta-lib.sh
+
+    if [ $? -ne 0 ]; then
+        echo "Quitting. Please fix the above error before continuing."
+        cd ..
+        exit 1
+    fi;
+
+    cd ..
 }
 
-function install_mac_newer_python_dependencies() {    
-    
+function install_mac_newer_python_dependencies() {
+
     if [ ! $(brew --prefix --installed hdf5 2>/dev/null) ]
     then
         echo "-------------------------"
@@ -115,7 +123,7 @@ function install_mac_newer_python_dependencies() {
         echo "Installing c-blosc"
         echo "-------------------------"
         brew install c-blosc
-    fi    
+    fi
     export CBLOSC_DIR=$(brew --prefix)
 }
 
@@ -130,7 +138,7 @@ function install_macos() {
     fi
     #Gets number after decimal in python version
     version=$(egrep -o 3.\[0-9\]+ <<< $PYTHON | sed 's/3.//g')
-    
+
     if [[ $version -ge 9 ]]; then               #Checks if python version >= 3.9
         install_mac_newer_python_dependencies
     fi

From 6be40cb7c3d680303591de81bb895ed8cc3f4d52 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 Oct 2021 03:01:07 +0000
Subject: [PATCH 107/134] Bump types-requests from 2.25.9 to 2.25.11

Bumps [types-requests](https://github.com/python/typeshed) from 2.25.9 to 2.25.11.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-requests
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
---
 requirements-dev.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements-dev.txt b/requirements-dev.txt
index 74ebee479..7627e1022 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -22,5 +22,5 @@ nbconvert==6.2.0
 # mypy types
 types-cachetools==4.2.2
 types-filelock==3.2.0
-types-requests==2.25.9
+types-requests==2.25.11
 types-tabulate==0.8.2

From 12a041b46665caad023f7bbef1849b148cc7ef8e Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 Oct 2021 03:01:10 +0000
Subject: [PATCH 108/134] Bump pytest-asyncio from 0.15.1 to 0.16.0

Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.15.1 to 0.16.0.
- [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases)
- [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.15.1...v0.16.0)

---
updated-dependencies:
- dependency-name: pytest-asyncio
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 
---
 requirements-dev.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements-dev.txt b/requirements-dev.txt
index 74ebee479..8daaa0524 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -8,7 +8,7 @@ flake8==4.0.0
 flake8-tidy-imports==4.5.0
 mypy==0.910
 pytest==6.2.5
-pytest-asyncio==0.15.1
+pytest-asyncio==0.16.0
 pytest-cov==3.0.0
 pytest-mock==3.6.1
 pytest-random-order==1.0.4

From 9b0171ef3748b310b681fcd1262d16f21b6d2dc7 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 Oct 2021 03:01:14 +0000
Subject: [PATCH 109/134] Bump flake8 from 4.0.0 to 4.0.1

Bumps [flake8](https://github.com/pycqa/flake8) from 4.0.0 to 4.0.1.
- [Release notes](https://github.com/pycqa/flake8/releases)
- [Commits](https://github.com/pycqa/flake8/compare/4.0.0...4.0.1)

---
updated-dependencies:
- dependency-name: flake8
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
---
 requirements-dev.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements-dev.txt b/requirements-dev.txt
index 74ebee479..83bd12843 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -4,7 +4,7 @@
 -r requirements-hyperopt.txt
 
 coveralls==3.2.0
-flake8==4.0.0
+flake8==4.0.1
 flake8-tidy-imports==4.5.0
 mypy==0.910
 pytest==6.2.5

From e7a2672f07ca15aa1a42b834f92f91e6ec8812ff Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 Oct 2021 03:01:17 +0000
Subject: [PATCH 110/134] Bump filelock from 3.3.0 to 3.3.1

Bumps [filelock](https://github.com/tox-dev/py-filelock) from 3.3.0 to 3.3.1.
- [Release notes](https://github.com/tox-dev/py-filelock/releases)
- [Changelog](https://github.com/tox-dev/py-filelock/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/py-filelock/compare/3.3.0...3.3.1)

---
updated-dependencies:
- dependency-name: filelock
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
---
 requirements-hyperopt.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt
index e97e78638..b0f4343ae 100644
--- a/requirements-hyperopt.txt
+++ b/requirements-hyperopt.txt
@@ -5,7 +5,7 @@
 scipy==1.7.1
 scikit-learn==1.0
 scikit-optimize==0.9.0
-filelock==3.3.0
+filelock==3.3.1
 joblib==1.1.0
 psutil==5.8.0
 progressbar2==3.53.3

From b60371822f9b14bff45e16b9de7f8e8fe338afd3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 Oct 2021 03:01:19 +0000
Subject: [PATCH 111/134] Bump pyjwt from 2.2.0 to 2.3.0

Bumps [pyjwt](https://github.com/jpadilla/pyjwt) from 2.2.0 to 2.3.0.
- [Release notes](https://github.com/jpadilla/pyjwt/releases)
- [Changelog](https://github.com/jpadilla/pyjwt/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jpadilla/pyjwt/commits)

---
updated-dependencies:
- dependency-name: pyjwt
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index f3eb65c59..f85b9bb8e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -34,7 +34,7 @@ sdnotify==0.3.2
 # API Server
 fastapi==0.70.0
 uvicorn==0.15.0
-pyjwt==2.2.0
+pyjwt==2.3.0
 aiofiles==0.7.0
 psutil==5.8.0
 

From d7756efe8b4e9b5aebb5534597b396bee5f94b5d Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 Oct 2021 03:01:28 +0000
Subject: [PATCH 112/134] Bump wrapt from 1.13.1 to 1.13.2

Bumps [wrapt](https://github.com/GrahamDumpleton/wrapt) from 1.13.1 to 1.13.2.
- [Release notes](https://github.com/GrahamDumpleton/wrapt/releases)
- [Changelog](https://github.com/GrahamDumpleton/wrapt/blob/develop/docs/changes.rst)
- [Commits](https://github.com/GrahamDumpleton/wrapt/compare/1.13.1...1.13.2)

---
updated-dependencies:
- dependency-name: wrapt
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index f3eb65c59..858452e82 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,7 +12,7 @@ arrow==1.2.0
 cachetools==4.2.2
 requests==2.26.0
 urllib3==1.26.7
-wrapt==1.13.1
+wrapt==1.13.2
 jsonschema==4.1.0
 TA-Lib==0.4.21
 technical==1.3.0

From 035380d8a4b7f2826e2d33d9f36078a44c973231 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 Oct 2021 05:18:42 +0000
Subject: [PATCH 113/134] Bump types-cachetools from 4.2.2 to 4.2.4

Bumps [types-cachetools](https://github.com/python/typeshed) from 4.2.2 to 4.2.4.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-cachetools
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
---
 requirements-dev.txt | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/requirements-dev.txt b/requirements-dev.txt
index 74ebee479..93df93695 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -4,7 +4,7 @@
 -r requirements-hyperopt.txt
 
 coveralls==3.2.0
-flake8==4.0.0
+flake8==4.0.1
 flake8-tidy-imports==4.5.0
 mypy==0.910
 pytest==6.2.5
@@ -20,7 +20,7 @@ time-machine==2.4.0
 nbconvert==6.2.0
 
 # mypy types
-types-cachetools==4.2.2
+types-cachetools==4.2.4
 types-filelock==3.2.0
 types-requests==2.25.9
 types-tabulate==0.8.2

From 69c98c4141b1e9d1e74dfd55162fded3120625bf Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 Oct 2021 05:18:47 +0000
Subject: [PATCH 114/134] Bump python-rapidjson from 1.4 to 1.5

Bumps [python-rapidjson](https://github.com/python-rapidjson/python-rapidjson) from 1.4 to 1.5.
- [Release notes](https://github.com/python-rapidjson/python-rapidjson/releases)
- [Changelog](https://github.com/python-rapidjson/python-rapidjson/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-rapidjson/python-rapidjson/compare/v1.4...v1.5)

---
updated-dependencies:
- dependency-name: python-rapidjson
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index f3eb65c59..a78e8c7a3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -26,7 +26,7 @@ blosc==1.10.6
 py_find_1st==1.1.5
 
 # Load ticker files 30% faster
-python-rapidjson==1.4
+python-rapidjson==1.5
 
 # Notify systemd
 sdnotify==0.3.2

From 82684f5de90eb5c1bfd8cd5c6ef8af631905b1c9 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 Oct 2021 05:19:03 +0000
Subject: [PATCH 115/134] Bump progressbar2 from 3.53.3 to 3.55.0

Bumps [progressbar2](https://github.com/WoLpH/python-progressbar) from 3.53.3 to 3.55.0.
- [Release notes](https://github.com/WoLpH/python-progressbar/releases)
- [Changelog](https://github.com/WoLpH/python-progressbar/blob/develop/CHANGES.rst)
- [Commits](https://github.com/WoLpH/python-progressbar/compare/v3.53.3...v3.55.0)

---
updated-dependencies:
- dependency-name: progressbar2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 
---
 requirements-hyperopt.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt
index e97e78638..8e6ca9769 100644
--- a/requirements-hyperopt.txt
+++ b/requirements-hyperopt.txt
@@ -8,4 +8,4 @@ scikit-optimize==0.9.0
 filelock==3.3.0
 joblib==1.1.0
 psutil==5.8.0
-progressbar2==3.53.3
+progressbar2==3.55.0

From 4b02749019394ffef0046e53e0187f37cd06aa90 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 Oct 2021 06:15:52 +0000
Subject: [PATCH 116/134] Bump mkdocs from 1.2.2 to 1.2.3

Bumps [mkdocs](https://github.com/mkdocs/mkdocs) from 1.2.2 to 1.2.3.
- [Release notes](https://github.com/mkdocs/mkdocs/releases)
- [Commits](https://github.com/mkdocs/mkdocs/compare/1.2.2...1.2.3)

---
updated-dependencies:
- dependency-name: mkdocs
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
---
 docs/requirements-docs.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt
index 9a733d8f7..e4bcf3c79 100644
--- a/docs/requirements-docs.txt
+++ b/docs/requirements-docs.txt
@@ -1,4 +1,4 @@
-mkdocs==1.2.2
+mkdocs==1.2.3
 mkdocs-material==7.3.2
 mdx_truly_sane_lists==1.2
 pymdown-extensions==9.0

From 44e6e134297301f97901a18e08f2bf4f22e4bf27 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 Oct 2021 06:16:09 +0000
Subject: [PATCH 117/134] Bump ccxt from 1.57.94 to 1.58.47

Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.57.94 to 1.58.47.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg)
- [Commits](https://github.com/ccxt/ccxt/compare/1.57.94...1.58.47)

---
updated-dependencies:
- dependency-name: ccxt
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/requirements.txt b/requirements.txt
index 858452e82..d38a0afce 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,7 +2,7 @@ numpy==1.21.2
 pandas==1.3.3
 pandas-ta==0.3.14b
 
-ccxt==1.57.94
+ccxt==1.58.47
 # Pin cryptography for now due to rust build errors with piwheels
 cryptography==35.0.0
 aiohttp==3.7.4.post0
@@ -26,7 +26,7 @@ blosc==1.10.6
 py_find_1st==1.1.5
 
 # Load ticker files 30% faster
-python-rapidjson==1.4
+python-rapidjson==1.5
 
 # Notify systemd
 sdnotify==0.3.2
@@ -34,7 +34,7 @@ sdnotify==0.3.2
 # API Server
 fastapi==0.70.0
 uvicorn==0.15.0
-pyjwt==2.2.0
+pyjwt==2.3.0
 aiofiles==0.7.0
 psutil==5.8.0
 

From e4682b78c5f0e9d3ea90f3038371c9d7530ea082 Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Mon, 18 Oct 2021 00:16:49 -0600
Subject: [PATCH 118/134] updates suggested on github

---
 freqtrade/freqtradebot.py      | 14 +++++---------
 tests/plugins/test_pairlist.py |  1 -
 2 files changed, 5 insertions(+), 10 deletions(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index dafb3b106..a84e64898 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -479,11 +479,7 @@ class FreqtradeBot(LoggingMixin):
             bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {})
             if ((bid_check_dom.get('enabled', False)) and
                     (bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
-                if self._check_depth_of_market(
-                    pair,
-                    bid_check_dom,
-                    side=signal
-                ):
+                if self._check_depth_of_market(pair, bid_check_dom, side=signal):
                     return self.execute_entry(
                         pair,
                         stake_amount,
@@ -629,9 +625,10 @@ class FreqtradeBot(LoggingMixin):
         if not stake_amount:
             return False
 
-        log_type = f"{name} signal found"
-        logger.info(f"{log_type}: about create a new trade for {pair} with stake_amount: "
-                    f"{stake_amount} ...")
+        logger.info(
+            f"{name} signal found: about create a new trade for {pair} with stake_amount: "
+            f"{stake_amount} ..."
+        )
 
         amount = (stake_amount / enter_limit_requested) * leverage
         order_type = self.strategy.order_types['buy']
@@ -1280,7 +1277,6 @@ class FreqtradeBot(LoggingMixin):
         :param trade: Trade instance
         :param limit: limit rate for the sell order
         :param sell_reason: Reason the sell was triggered
-        :param side: "buy" or "sell"
         :return: True if it succeeds (supported) False (not supported)
         """
         exit_type = 'sell'  # TODO-lev: Update to exit
diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py
index fc8b20f02..93eebde82 100644
--- a/tests/plugins/test_pairlist.py
+++ b/tests/plugins/test_pairlist.py
@@ -665,7 +665,6 @@ def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None:
 
 
 @pytest.mark.usefixtures("init_persistence")
-# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
 def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee, caplog) -> None:
     whitelist_conf['exchange']['pair_whitelist'].append('XRP/BTC')
     whitelist_conf['pairlists'] = [

From 618f0ffe68882a80677e36d41ab72bbf77dba8f0 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 Oct 2021 06:38:42 +0000
Subject: [PATCH 119/134] Bump types-tabulate from 0.8.2 to 0.8.3

Bumps [types-tabulate](https://github.com/python/typeshed) from 0.8.2 to 0.8.3.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-tabulate
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
---
 requirements-dev.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements-dev.txt b/requirements-dev.txt
index da12e5fcc..3a6913f53 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -23,4 +23,4 @@ nbconvert==6.2.0
 types-cachetools==4.2.4
 types-filelock==3.2.0
 types-requests==2.25.11
-types-tabulate==0.8.2
+types-tabulate==0.8.3

From 75e6a2d276912ab0b50630a6854276266e20da2c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 Oct 2021 06:47:32 +0000
Subject: [PATCH 120/134] Bump mkdocs-material from 7.3.2 to 7.3.4

Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.3.2 to 7.3.4.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.3.2...7.3.4)

---
updated-dependencies:
- dependency-name: mkdocs-material
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
---
 docs/requirements-docs.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt
index e4bcf3c79..72d1d0494 100644
--- a/docs/requirements-docs.txt
+++ b/docs/requirements-docs.txt
@@ -1,4 +1,4 @@
 mkdocs==1.2.3
-mkdocs-material==7.3.2
+mkdocs-material==7.3.4
 mdx_truly_sane_lists==1.2
 pymdown-extensions==9.0

From 3af55cc8c775ea3d28fcc0207097e97f49d6b5e3 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 Oct 2021 06:58:47 +0000
Subject: [PATCH 121/134] Bump pandas from 1.3.3 to 1.3.4

Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.3.3 to 1.3.4.
- [Release notes](https://github.com/pandas-dev/pandas/releases)
- [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md)
- [Commits](https://github.com/pandas-dev/pandas/compare/v1.3.3...v1.3.4)

---
updated-dependencies:
- dependency-name: pandas
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
---
 requirements.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements.txt b/requirements.txt
index d38a0afce..9f7a055e8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,5 @@
 numpy==1.21.2
-pandas==1.3.3
+pandas==1.3.4
 pandas-ta==0.3.14b
 
 ccxt==1.58.47

From 053aecf111e55e79501ba9fb708aa6705aeba2e1 Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Mon, 18 Oct 2021 00:45:48 -0600
Subject: [PATCH 122/134] reformatted check_handle_timedout

---
 freqtrade/freqtradebot.py | 53 ++++++++++++---------------------------
 1 file changed, 16 insertions(+), 37 deletions(-)

diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index a84e64898..bb7e06e8a 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -1071,44 +1071,23 @@ class FreqtradeBot(LoggingMixin):
                 continue
 
             fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order)
+            is_entering = order['side'] == trade.enter_side
+            not_closed = order['status'] == 'open' or fully_cancelled
+            side = trade.enter_side if is_entering else trade.exit_side
+            timed_out = self._check_timed_out(side, order)
+            time_method = 'check_sell_timeout' if order['side'] == 'sell' else 'check_buy_timeout'
 
-            if (
-                order['side'] == trade.enter_side and
-                (order['status'] == 'open' or fully_cancelled) and
-                (fully_cancelled or
-                    self._check_timed_out(trade.enter_side, order) or
-                    strategy_safe_wrapper(
-                        (
-                            self.strategy.check_sell_timeout
-                            if trade.is_short else
-                            self.strategy.check_buy_timeout
-                        ),
-                        default_retval=False
-                    )(
-                        pair=trade.pair,
-                        trade=trade,
-                        order=order
-                    )
-                 )
-            ):
-                self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
-
-            elif (
-                order['side'] == trade.exit_side and
-                (order['status'] == 'open' or fully_cancelled) and
-                (fully_cancelled or
-                    self._check_timed_out(trade.exit_side, order) or
-                    strategy_safe_wrapper(
-                        self.strategy.check_sell_timeout,
-                        default_retval=False
-                    )(
-                        pair=trade.pair,
-                        trade=trade,
-                        order=order
-                    )
-                 )
-            ):
-                self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
+            if not_closed and (fully_cancelled or timed_out or (
+                strategy_safe_wrapper(getattr(self.strategy, time_method), default_retval=False)(
+                    pair=trade.pair,
+                    trade=trade,
+                    order=order
+                )
+            )):
+                if is_entering:
+                    self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
+                else:
+                    self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
 
     def cancel_all_open_orders(self) -> None:
         """

From 8a7ea655313b64b6dc5f0009b728c9436c2ffcc8 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 18 Oct 2021 07:06:05 +0000
Subject: [PATCH 123/134] Bump types-filelock from 3.2.0 to 3.2.1

Bumps [types-filelock](https://github.com/python/typeshed) from 3.2.0 to 3.2.1.
- [Release notes](https://github.com/python/typeshed/releases)
- [Commits](https://github.com/python/typeshed/commits)

---
updated-dependencies:
- dependency-name: types-filelock
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] 
---
 requirements-dev.txt | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/requirements-dev.txt b/requirements-dev.txt
index 3a6913f53..0e68e18a3 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -21,6 +21,6 @@ nbconvert==6.2.0
 
 # mypy types
 types-cachetools==4.2.4
-types-filelock==3.2.0
+types-filelock==3.2.1
 types-requests==2.25.11
 types-tabulate==0.8.3

From faaa3ae9b12930cec1062a6324e9d9863bfcc367 Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Mon, 18 Oct 2021 01:08:12 -0600
Subject: [PATCH 124/134] Removed exit_short rpcmessagetype

---
 freqtrade/enums/rpcmessagetype.py | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/freqtrade/enums/rpcmessagetype.py b/freqtrade/enums/rpcmessagetype.py
index a17fa3d64..663b37b83 100644
--- a/freqtrade/enums/rpcmessagetype.py
+++ b/freqtrade/enums/rpcmessagetype.py
@@ -20,10 +20,6 @@ class RPCMessageType(Enum):
     SHORT_FILL = 'short_fill'
     SHORT_CANCEL = 'short_cancel'
 
-    EXIT_SHORT = 'exit_short'
-    EXIT_SHORT_FILL = 'exit_short_fill'
-    EXIT_SHORT_CANCEL = 'exit_short_cancel'
-
     def __repr__(self):
         return self.value
 

From 57d7009fd9762b8b2bf09e81e9e584c4cf335b82 Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Mon, 18 Oct 2021 01:15:44 -0600
Subject: [PATCH 125/134] Added trading mode and collateral to constants.py

---
 freqtrade/constants.py     |  6 +++++-
 tests/test_freqtradebot.py | 30 +++++++++++++++---------------
 2 files changed, 20 insertions(+), 16 deletions(-)

diff --git a/freqtrade/constants.py b/freqtrade/constants.py
index c6b8f0e62..ee104325b 100644
--- a/freqtrade/constants.py
+++ b/freqtrade/constants.py
@@ -39,6 +39,8 @@ DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume']
 # Don't modify sequence of DEFAULT_TRADES_COLUMNS
 # it has wide consequences for stored trades files
 DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost']
+TRADING_MODES = ['spot', 'margin', 'futures']
+COLLATERAL_TYPES = ['cross', 'isolated']
 
 LAST_BT_RESULT_FN = '.last_result.json'
 FTHYPT_FILEVERSION = 'fthypt_fileversion'
@@ -146,6 +148,8 @@ CONF_SCHEMA = {
         'sell_profit_offset': {'type': 'number'},
         'ignore_roi_if_buy_signal': {'type': 'boolean'},
         'ignore_buying_expired_candle_after': {'type': 'number'},
+        'trading_mode': {'type': 'string', 'enum': TRADING_MODES},
+        'collateral_type': {'type': 'string', 'enum': COLLATERAL_TYPES},
         'bot_name': {'type': 'string'},
         'unfilledtimeout': {
             'type': 'object',
@@ -193,7 +197,7 @@ CONF_SCHEMA = {
             'required': ['price_side']
         },
         'custom_price_max_distance_ratio': {
-           'type': 'number', 'minimum': 0.0
+            'type': 'number', 'minimum': 0.0
         },
         'order_types': {
             'type': 'object',
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index 319e25e71..6d784d9d1 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -11,7 +11,7 @@ import arrow
 import pytest
 
 from freqtrade.constants import CANCEL_REASON, MATH_CLOSE_PREC, UNLIMITED_STAKE_AMOUNT
-from freqtrade.enums import RPCMessageType, RunMode, SellType, SignalDirection, State, TradingMode
+from freqtrade.enums import RPCMessageType, RunMode, SellType, SignalDirection, State
 from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
                                   InvalidOrderException, OperationalException, PricingError,
                                   TemporaryError)
@@ -3564,7 +3564,7 @@ def test_ignore_roi_if_buy_signal(default_conf_usdt, limit_order, limit_order_op
 @ pytest.mark.parametrize("is_short,val1,val2", [
     (False, 1.5, 1.1),
     (True, 0.5, 0.9)
-    ])
+])
 def test_trailing_stop_loss(default_conf_usdt, limit_order_open,
                             is_short, val1, val2, fee, caplog, mocker) -> None:
     patch_RPCManager(mocker)
@@ -4668,19 +4668,19 @@ def test_leverage_prep():
 
 
 @pytest.mark.parametrize('trading_mode,calls,t1,t2', [
-    (TradingMode.SPOT, 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"),
-    (TradingMode.MARGIN, 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"),
-    (TradingMode.FUTURES, 31, "2021-09-01 00:00:02", "2021-09-01 08:00:01"),
-    (TradingMode.FUTURES, 32, "2021-09-01 00:00:00", "2021-09-01 08:00:01"),
-    (TradingMode.FUTURES, 32, "2021-09-01 00:00:02", "2021-09-01 08:00:02"),
-    (TradingMode.FUTURES, 33, "2021-09-01 00:00:00", "2021-09-01 08:00:02"),
-    (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:02"),
-    (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:03"),
-    (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:04"),
-    (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:05"),
-    (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:06"),
-    (TradingMode.FUTURES, 33, "2021-08-31 23:59:59", "2021-09-01 08:00:07"),
-    (TradingMode.FUTURES, 33, "2021-08-31 23:59:58", "2021-09-01 08:00:07"),
+    ('spot', 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"),
+    ('margin', 0, "2021-09-01 00:00:00", "2021-09-01 08:00:00"),
+    ('futures', 31, "2021-09-01 00:00:02", "2021-09-01 08:00:01"),
+    ('futures', 32, "2021-09-01 00:00:00", "2021-09-01 08:00:01"),
+    ('futures', 32, "2021-09-01 00:00:02", "2021-09-01 08:00:02"),
+    ('futures', 33, "2021-09-01 00:00:00", "2021-09-01 08:00:02"),
+    ('futures', 33, "2021-08-31 23:59:59", "2021-09-01 08:00:02"),
+    ('futures', 33, "2021-08-31 23:59:59", "2021-09-01 08:00:03"),
+    ('futures', 33, "2021-08-31 23:59:59", "2021-09-01 08:00:04"),
+    ('futures', 33, "2021-08-31 23:59:59", "2021-09-01 08:00:05"),
+    ('futures', 33, "2021-08-31 23:59:59", "2021-09-01 08:00:06"),
+    ('futures', 33, "2021-08-31 23:59:59", "2021-09-01 08:00:07"),
+    ('futures', 33, "2021-08-31 23:59:58", "2021-09-01 08:00:07"),
 ])
 def test_update_funding_fees(mocker, default_conf, trading_mode, calls, time_machine,
                              t1, t2):

From ddba4e32d7b904d6a54b273fd3ddee67b3a260f5 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Mon, 18 Oct 2021 16:04:24 +0200
Subject: [PATCH 126/134] Fully remove flake8-type-annotations

---
 environment.yml | 1 -
 setup.py        | 1 -
 2 files changed, 2 deletions(-)

diff --git a/environment.yml b/environment.yml
index f58434c15..f62ac8105 100644
--- a/environment.yml
+++ b/environment.yml
@@ -64,7 +64,6 @@ dependencies:
         - py_find_1st
         - tables
         - pytest-random-order
-        - flake8-type-annotations
         - ccxt
         - flake8-tidy-imports
         - -e .
diff --git a/setup.py b/setup.py
index cf381bdd3..445155687 100644
--- a/setup.py
+++ b/setup.py
@@ -16,7 +16,6 @@ hyperopt = [
 develop = [
     'coveralls',
     'flake8',
-    'flake8-type-annotations',
     'flake8-tidy-imports',
     'mypy',
     'pytest',

From 0da5ef16e6170efad32d8b82533f620f129b04f7 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Mon, 18 Oct 2021 19:16:56 +0200
Subject: [PATCH 127/134] Remove unnecessary dependency

---
 environment.yml  | 1 -
 requirements.txt | 1 -
 setup.py         | 1 -
 3 files changed, 3 deletions(-)

diff --git a/environment.yml b/environment.yml
index f62ac8105..84ab5ff6f 100644
--- a/environment.yml
+++ b/environment.yml
@@ -16,7 +16,6 @@ dependencies:
     - cachetools
     - requests
     - urllib3
-    - wrapt
     - jsonschema
     - TA-Lib
     - tabulate
diff --git a/requirements.txt b/requirements.txt
index 9f7a055e8..b10bbabf6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -12,7 +12,6 @@ arrow==1.2.0
 cachetools==4.2.2
 requests==2.26.0
 urllib3==1.26.7
-wrapt==1.13.2
 jsonschema==4.1.0
 TA-Lib==0.4.21
 technical==1.3.0
diff --git a/setup.py b/setup.py
index 445155687..b23fa814d 100644
--- a/setup.py
+++ b/setup.py
@@ -50,7 +50,6 @@ setup(
         'cachetools',
         'requests',
         'urllib3',
-        'wrapt',
         'jsonschema',
         'TA-Lib',
         'pandas-ta',

From f9b166747847c529c16900904a80e07284f46108 Mon Sep 17 00:00:00 2001
From: daniila 
Date: Mon, 18 Oct 2021 23:36:47 +0300
Subject: [PATCH 128/134] Update docs/advanced-setup.md

Co-authored-by: Matthias 
---
 docs/advanced-setup.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md
index e7e5b6cec..79e17fb4e 100644
--- a/docs/advanced-setup.md
+++ b/docs/advanced-setup.md
@@ -103,7 +103,7 @@ services:
     # Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation
     # before enabling this.
     ports:
-      - "127.0.0.1:8081:8081"
+      - "127.0.0.1:8081:8080"
     # Default command used when running `docker compose up`
     command: >
       trade

From 5d2e37409962970a45cbffd255ea9080c595fc52 Mon Sep 17 00:00:00 2001
From: daniila 
Date: Mon, 18 Oct 2021 23:38:45 +0300
Subject: [PATCH 129/134] Update docs/advanced-setup.md

Co-authored-by: Matthias 
---
 docs/advanced-setup.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md
index 79e17fb4e..6eda8489b 100644
--- a/docs/advanced-setup.md
+++ b/docs/advanced-setup.md
@@ -55,7 +55,7 @@ For more information regarding usage of the sqlite databases, for example to man
 ### Multiple instances using docker
 
 To run multiple instances of freqtrade using docker you will need to edit the docker-compose.yml file and add all the instances you want as separate services. Remember, you can separate your configuration into multiple files, so it's a good idea to think about making them modular, then if you need to edit something common to all bots, you can do that in a single config file. 
-```
+``` yml
 ---
 version: '3'
 services:

From f863f4fdfca815bd5c8a20f3915b948b90f7d753 Mon Sep 17 00:00:00 2001
From: daniila 
Date: Mon, 18 Oct 2021 23:49:59 +0300
Subject: [PATCH 130/134] Update advanced-setup.md

A note on having to use different database files, ports and telegram configs for each bot.
---
 docs/advanced-setup.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md
index 6eda8489b..02b0307e5 100644
--- a/docs/advanced-setup.md
+++ b/docs/advanced-setup.md
@@ -114,7 +114,7 @@ services:
       --strategy SampleStrategy
 
 ```
-You can use whatever naming convention you want, freqtrade1 and 2 are arbitrary.
+You can use whatever naming convention you want, freqtrade1 and 2 are arbitrary. Note, that you will need to use different database files, port mappings and telegram configurations for each instance, as mentioned above. 
 
 
 ## Configure the bot running as a systemd service

From 42a4dfed28be1425016836ae5c09b4e2a883662d Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 19 Oct 2021 19:11:17 +0200
Subject: [PATCH 131/134] Reallow bitstamp

revert #1984, related to #1983
---
 docs/utils.md                | 2 +-
 freqtrade/exchange/common.py | 2 --
 2 files changed, 1 insertion(+), 3 deletions(-)

diff --git a/docs/utils.md b/docs/utils.md
index d8fbcacb7..e915528ec 100644
--- a/docs/utils.md
+++ b/docs/utils.md
@@ -281,7 +281,7 @@ bitmax              True     missing opt: fetchMyTrades
 bitmex              False    Various reasons.
 bitpanda            True
 bitso               False    missing: fetchOHLCV
-bitstamp            False    Does not provide history. Details in https://github.com/freqtrade/freqtrade/issues/1983
+bitstamp            True     missing opt: fetchTickers
 bitstamp1           False    missing: fetchOrder, fetchOHLCV
 bittrex             True
 bitvavo             True
diff --git a/freqtrade/exchange/common.py b/freqtrade/exchange/common.py
index 7b89adf06..644a13e93 100644
--- a/freqtrade/exchange/common.py
+++ b/freqtrade/exchange/common.py
@@ -16,8 +16,6 @@ API_FETCH_ORDER_RETRY_COUNT = 5
 
 BAD_EXCHANGES = {
     "bitmex": "Various reasons.",
-    "bitstamp": "Does not provide history. "
-                "Details in https://github.com/freqtrade/freqtrade/issues/1983",
     "phemex": "Does not provide history. ",
     "poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders.",
 }

From 55b021618067d1c3183611027f4963e48394b6ed Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Tue, 19 Oct 2021 19:48:56 +0200
Subject: [PATCH 132/134] Allow StaticPairlist in non-first position

closes #5754
---
 docs/includes/pairlists.md                   |  2 ++
 freqtrade/plugins/pairlist/StaticPairList.py | 12 ++++++------
 tests/plugins/test_pairlist.py               | 13 +++----------
 3 files changed, 11 insertions(+), 16 deletions(-)

diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md
index 3d10747d3..589bc23b2 100644
--- a/docs/includes/pairlists.md
+++ b/docs/includes/pairlists.md
@@ -52,6 +52,8 @@ To skip pair validation against active markets, set `"allow_inactive": true` wit
 This can be useful for backtesting expired pairs (like quarterly spot-markets).
 This option must be configured along with `exchange.skip_pair_validation` in the exchange configuration.
 
+When used in a "follow-up" position (e.g. after VolumePairlist), all pairs in `'pair_whitelist'` will be added to the end of the pairlist.
+
 #### Volume Pair List
 
 `VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`).
diff --git a/freqtrade/plugins/pairlist/StaticPairList.py b/freqtrade/plugins/pairlist/StaticPairList.py
index d8623e13d..30fa474e4 100644
--- a/freqtrade/plugins/pairlist/StaticPairList.py
+++ b/freqtrade/plugins/pairlist/StaticPairList.py
@@ -4,9 +4,9 @@ Static Pair List provider
 Provides pair white list as it configured in config
 """
 import logging
+from copy import deepcopy
 from typing import Any, Dict, List
 
-from freqtrade.exceptions import OperationalException
 from freqtrade.plugins.pairlist.IPairList import IPairList
 
 
@@ -20,10 +20,6 @@ class StaticPairList(IPairList):
                  pairlist_pos: int) -> None:
         super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
 
-        if self._pairlist_pos != 0:
-            raise OperationalException(f"{self.name} can only be used in the first position "
-                                       "in the list of Pairlist Handlers.")
-
         self._allow_inactive = self._pairlistconfig.get('allow_inactive', False)
 
     @property
@@ -64,4 +60,8 @@ class StaticPairList(IPairList):
         :param tickers: Tickers (from exchange.get_tickers()). May be cached.
         :return: new whitelist
         """
-        return pairlist
+        pairlist_ = deepcopy(pairlist)
+        for pair in self._config['exchange']['pair_whitelist']:
+            if pair not in pairlist_:
+                pairlist_.append(pair)
+        return pairlist_
diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py
index c6246dccb..6333266aa 100644
--- a/tests/plugins/test_pairlist.py
+++ b/tests/plugins/test_pairlist.py
@@ -415,10 +415,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
     # SpreadFilter only
     ([{"method": "SpreadFilter", "max_spread_ratio": 0.005}],
      "BTC", 'filter_at_the_beginning'),  # OperationalException expected
-    # Static Pairlist after VolumePairList, on a non-first position
-    ([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
+    # Static Pairlist after VolumePairList, on a non-first position (appends pairs)
+    ([{"method": "VolumePairList", "number_assets": 2, "sort_key": "quoteVolume"},
       {"method": "StaticPairList"}],
-        "BTC", 'static_in_the_middle'),
+        "BTC", ['ETH/BTC', 'TKN/BTC', 'TRST/BTC', 'SWT/BTC', 'BCC/BTC', 'HOT/BTC']),
     ([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
       {"method": "PriceFilter", "low_price_ratio": 0.02}],
         "USDT", ['ETH/USDT', 'NANO/USDT']),
@@ -469,13 +469,6 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
 
     mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
 
-    if whitelist_result == 'static_in_the_middle':
-        with pytest.raises(OperationalException,
-                           match=r"StaticPairList can only be used in the first position "
-                                 r"in the list of Pairlist Handlers."):
-            freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
-        return
-
     freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
     mocker.patch.multiple('freqtrade.exchange.Exchange',
                           get_tickers=tickers,

From 5454460227dcf63b578096c3862dd2269673ec85 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Wed, 20 Oct 2021 07:46:15 +0200
Subject: [PATCH 133/134] Revert initial_points to 30

closes #5760
---
 freqtrade/optimize/hyperopt.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
index 6397bbacb..2c7cc0ea7 100644
--- a/freqtrade/optimize/hyperopt.py
+++ b/freqtrade/optimize/hyperopt.py
@@ -45,7 +45,7 @@ progressbar.streams.wrap_stdout()
 logger = logging.getLogger(__name__)
 
 
-INITIAL_POINTS = 5
+INITIAL_POINTS = 30
 
 # Keep no more than SKOPT_MODEL_QUEUE_SIZE models
 # in the skopt model queue, to optimize memory consumption

From 8c80fb46c829f7f79cc0952a60af032f16a4b9f4 Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Wed, 20 Oct 2021 05:33:09 -0600
Subject: [PATCH 134/134] test__ccxt_config

---
 tests/exchange/test_exchange.py | 27 +++++++++++++++++++++++++++
 1 file changed, 27 insertions(+)

diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py
index f7627450e..430c648d0 100644
--- a/tests/exchange/test_exchange.py
+++ b/tests/exchange/test_exchange.py
@@ -3240,3 +3240,30 @@ def test_validate_trading_mode_and_collateral(
             exchange.validate_trading_mode_and_collateral(trading_mode, collateral)
     else:
         exchange.validate_trading_mode_and_collateral(trading_mode, collateral)
+
+
+@pytest.mark.parametrize("exchange_name,trading_mode,ccxt_config", [
+    ("binance", "spot", {}),
+    ("binance", "margin", {"options": {"defaultType": "margin"}}),
+    ("binance", "futures", {"options": {"defaultType": "future"}}),
+    ("kraken", "spot", {}),
+    ("kraken", "margin", {}),
+    ("kraken", "futures", {}),
+    ("ftx", "spot", {}),
+    ("ftx", "margin", {}),
+    ("ftx", "futures", {}),
+    ("bittrex", "spot", {}),
+    ("bittrex", "margin", {}),
+    ("bittrex", "futures", {}),
+])
+def test__ccxt_config(
+    default_conf,
+    mocker,
+    exchange_name,
+    trading_mode,
+    ccxt_config
+):
+    default_conf['trading_mode'] = trading_mode
+    default_conf['collateral'] = 'isolated'
+    exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
+    assert exchange._ccxt_config == ccxt_config