From 120cad88af542aa761b75e1f9e4e9203bcfe4dfe Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 24 Jul 2021 01:32:42 -0600 Subject: [PATCH 01/87] 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 02/87] 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 03/87] 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 04/87] 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 05/87] 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 06/87] 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 07/87] 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 08/87] 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 09/87] 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 10/87] 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 11/87] 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 12/87] 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 13/87] 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 14/87] 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 15/87] 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 16/87] 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 17/87] 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 18/87] 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 19/87] 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 20/87] 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 21/87] 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 22/87] 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 23/87] 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 24/87] 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 25/87] 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 26/87] 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 27/87] 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 28/87] 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 29/87] 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 30/87] 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 31/87] 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 32/87] 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 33/87] 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 34/87] 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 35/87] 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 36/87] 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 37/87] 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 38/87] 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 39/87] 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 40/87] 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 41/87] 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 42/87] 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 43/87] 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 44/87] 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 45/87] 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 46/87] 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 47/87] 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 48/87] 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 49/87] 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 50/87] 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 51/87] 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 52/87] 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 53/87] 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 87ff65d31e41c6a929c67de812bff382cb5ee449 Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Sat, 2 Oct 2021 03:58:02 -0600
Subject: [PATCH 54/87] 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 55/87] 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 56/87] '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 57/87] 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 58/87] 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 59/87] 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 c72aac43568f1c26e080ec7104c48b7512e6987b Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Tue, 5 Oct 2021 02:13:29 -0600
Subject: [PATCH 60/87] 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 61/87] 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 62/87] 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 63/87] 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 64/87] 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 65/87] 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 66/87] 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 67/87] 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 68/87] 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 69/87] 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 70/87] 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 71/87] 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 72/87] 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 73/87] 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 b0ce9612f87fdef6a4c9bb029b99e3cba3011fa3 Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Thu, 14 Oct 2021 03:52:29 -0600
Subject: [PATCH 74/87] 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 75/87] 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 76/87] 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 77/87] 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 78/87] 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 e19d95b63e49d51b1a2347fdcb908f73b9ab5620 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 17 Oct 2021 09:00:10 +0200
Subject: [PATCH 79/87] 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 80/87] 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 81/87] 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 e8f98e473d6406dc8589d82731abdfa99d9d64de Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 17 Oct 2021 10:55:20 +0200
Subject: [PATCH 82/87] 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 ad2c88b991ea7e82d2074ccf29eca3d2c7364004 Mon Sep 17 00:00:00 2001
From: Matthias 
Date: Sun, 17 Oct 2021 17:00:25 +0200
Subject: [PATCH 83/87] 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 e4682b78c5f0e9d3ea90f3038371c9d7530ea082 Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Mon, 18 Oct 2021 00:16:49 -0600
Subject: [PATCH 84/87] 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 053aecf111e55e79501ba9fb708aa6705aeba2e1 Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Mon, 18 Oct 2021 00:45:48 -0600
Subject: [PATCH 85/87] 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 faaa3ae9b12930cec1062a6324e9d9863bfcc367 Mon Sep 17 00:00:00 2001
From: Sam Germain 
Date: Mon, 18 Oct 2021 01:08:12 -0600
Subject: [PATCH 86/87] 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 87/87] 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):