From 5d3261e92fad6e4b7d5a843877ea3db2d646866a Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Tue, 7 Sep 2021 12:24:39 +0530 Subject: [PATCH 001/102] Added Ftx interest rate calculation --- freqtrade/leverage/interest.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/leverage/interest.py b/freqtrade/leverage/interest.py index aacbb3532..c687c8b5b 100644 --- a/freqtrade/leverage/interest.py +++ b/freqtrade/leverage/interest.py @@ -20,7 +20,7 @@ def interest( :param exchange_name: The exchanged being trading on :param borrowed: The amount of currency being borrowed - :param rate: The rate of interest + :param rate: The rate of interest (i.e daily interest rate) :param hours: The time in hours that the currency has been borrowed for Raises: @@ -36,7 +36,8 @@ def interest( # Rounded based on https://kraken-fees-calculator.github.io/ return borrowed * rate * (one+ceil(hours/four)) elif exchange_name == "ftx": - # TODO-lev: Add FTX interest formula - raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") + # As Explained under #Interest rates section in + # https://help.ftx.com/hc/en-us/articles/360053007671-Spot-Margin-Trading-Explainer + return borrowed * rate * ceil(hours)/twenty_four else: - raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") + raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") \ No newline at end of file From d07c7f7f275af5169b4ba3ac6ca25974c687998f Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Tue, 7 Sep 2021 12:28:23 +0530 Subject: [PATCH 002/102] Added Ftx interest rate calculation --- freqtrade/leverage/interest.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/freqtrade/leverage/interest.py b/freqtrade/leverage/interest.py index aacbb3532..c687c8b5b 100644 --- a/freqtrade/leverage/interest.py +++ b/freqtrade/leverage/interest.py @@ -20,7 +20,7 @@ def interest( :param exchange_name: The exchanged being trading on :param borrowed: The amount of currency being borrowed - :param rate: The rate of interest + :param rate: The rate of interest (i.e daily interest rate) :param hours: The time in hours that the currency has been borrowed for Raises: @@ -36,7 +36,8 @@ def interest( # Rounded based on https://kraken-fees-calculator.github.io/ return borrowed * rate * (one+ceil(hours/four)) elif exchange_name == "ftx": - # TODO-lev: Add FTX interest formula - raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") + # As Explained under #Interest rates section in + # https://help.ftx.com/hc/en-us/articles/360053007671-Spot-Margin-Trading-Explainer + return borrowed * rate * ceil(hours)/twenty_four else: - raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") + raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") \ No newline at end of file From 54dd9ce7ad385083aaf374e4a976108f3514923a Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 24 Jul 2021 01:32:42 -0600 Subject: [PATCH 003/102] 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 189f5f481..33f22f970 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 @@ -90,3 +90,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 80f20b17e..e976a266f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -189,6 +189,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) @@ -540,8 +541,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: @@ -575,7 +577,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 @@ -713,6 +728,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, @@ -737,6 +761,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: @@ -757,6 +782,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) @@ -1525,6 +1570,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 ebf531081755592e58da0ee89ecadbe31b9cc717 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 25 Jul 2021 23:40:38 -0600 Subject: [PATCH 004/102] Added get_interest template method in exchange --- freqtrade/exchange/exchange.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e976a266f..fba673c63 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -584,7 +584,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 @@ -1573,6 +1573,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) From f4e26a616f9127ef7f15679b8b8649b64d6a9c65 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 26 Jul 2021 00:01:57 -0600 Subject: [PATCH 005/102] 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 33f22f970..c285cec21 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -25,20 +25,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 fba673c63..0471c0149 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -802,14 +802,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 78f6da9ec..e2586ed28 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -728,9 +728,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) @@ -819,11 +823,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 @@ -831,7 +835,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 144063c07..d03316ea5 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2581,10 +2581,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 e0880db8f..3106c3e00 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1343,10 +1343,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={ @@ -1417,7 +1420,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 @@ -1427,7 +1430,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) @@ -1526,10 +1529,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={ @@ -1647,10 +1653,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 d262af35cafd007f23f436c8474274f647eab2e8 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 2 Aug 2021 06:14:30 -0600 Subject: [PATCH 006/102] 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 c285cec21..675f85e62 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 @@ -96,103 +96,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 0471c0149..87d920fba 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -730,8 +730,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") @@ -782,26 +781,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) @@ -1571,15 +1550,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 add7e74632f260cf375afa7bef1372e59a6f95ba Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 8 Aug 2021 19:34:33 -0600 Subject: [PATCH 007/102] 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 87d920fba..bf5fc4de3 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -760,7 +760,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: @@ -1554,6 +1553,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 455bcf5389e1756983e5e9cc45b378c01ee0a4ae Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 8 Aug 2021 23:13:35 -0600 Subject: [PATCH 008/102] 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 675f85e62..8c70fdb1f 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -31,7 +31,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) @@ -43,7 +43,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 bf5fc4de3..b7b7151c2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -543,7 +543,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: @@ -1550,7 +1550,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 d03316ea5..0c1e027b7 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -116,6 +116,8 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog): assert ex._api.headers == {'hello': 'world'} Exchange._headers = {} + # TODO-lev: Test with options + def test_destroy(default_conf, mocker, caplog): caplog.set_level(logging.DEBUG) @@ -307,6 +309,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'}} @@ -425,6 +428,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'}} @@ -445,6 +449,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 @@ -2933,3 +2942,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 134a7ec59b0e3f42f0fdad87b883dcc88fa01a6c Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 20 Aug 2021 02:40:22 -0600 Subject: [PATCH 009/102] 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 8c70fdb1f..1339677d2 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 @@ -96,5 +96,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 b7b7151c2..340a63ab5 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -74,6 +74,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, @@ -161,6 +163,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 @@ -355,6 +367,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.") @@ -577,12 +590,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 @@ -728,14 +741,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, @@ -1553,13 +1558,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 0c1e027b7..518629531 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -449,11 +449,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 @@ -2944,7 +2939,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 c256dc3745127f1959fc7d7155ea57ee68e6f9c6 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 20 Aug 2021 18:50:02 -0600 Subject: [PATCH 010/102] 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 340a63ab5..0f7bf6b39 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -556,7 +556,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: @@ -1584,8 +1583,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 16db8d70a5b680d2c295daa9763a3041bfc62748 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 21 Aug 2021 01:13:51 -0600 Subject: [PATCH 011/102] 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 1339677d2..15599eff9 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -104,17 +104,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: """ @@ -135,4 +144,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 4ef1f0a977b3ac1ef7aef13a4de3e991423b6edc Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 21 Aug 2021 16:26:04 -0600 Subject: [PATCH 012/102] 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 15599eff9..1177a4409 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -134,22 +134,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 0f7bf6b39..ffcf1d401 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1575,13 +1575,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 5748c9bc13915903d1128ace05882b937afbefe6 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 21 Aug 2021 21:10:03 -0600 Subject: [PATCH 013/102] 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 1177a4409..3117f5ee1 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -31,8 +31,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, @@ -43,7 +46,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 @@ -52,14 +54,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: @@ -70,7 +74,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) @@ -78,21 +82,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 b73611c1b..630078ab3 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 8a5bad7c3ed3efb61023b6d2efbf3a13f96dc6e7 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 22 Aug 2021 20:58:22 -0600 Subject: [PATCH 014/102] 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 f950f039a8a20bf62488709a086723364db3ec45 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 22 Aug 2021 23:28:03 -0600 Subject: [PATCH 015/102] 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 518629531..1bfdd376b 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -309,7 +309,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'}} @@ -381,7 +380,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"] = { @@ -393,7 +397,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"] = { @@ -405,7 +414,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"] = { @@ -417,18 +431,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'}} @@ -443,10 +471,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 3a4d247b64b47fb22c942318d7a7aefe5ee40f61 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 22 Aug 2021 23:36:36 -0600 Subject: [PATCH 016/102] 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 3106c3e00..a841744b7 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1420,7 +1420,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 @@ -1430,7 +1430,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 39fe3814735ed4009c93ef51e2e47f6872446ffe Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 1 Sep 2021 23:40:32 -0600 Subject: [PATCH 017/102] 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 ffcf1d401..558417332 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1590,6 +1590,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 e6c9b8ffe5c9c47ac9abb81be09b759a69535fba Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 3 Sep 2021 18:11:39 -0600 Subject: [PATCH 018/102] 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 558417332..b9c2db152 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -22,6 +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.exceptions import (DDosProtection, ExchangeError, InsufficientFundsError, InvalidOrderException, OperationalException, PricingError, RetryableOrderError, TemporaryError) @@ -1590,8 +1591,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 5708fee0e69d7fcfdac2b9cc66d8259fd2403528 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 3 Sep 2021 19:00:04 -0600 Subject: [PATCH 019/102] 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 b9c2db152..4aaa5bc0b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1606,7 +1606,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 1bfdd376b..d76ac1bfe 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 @@ -3023,6 +3024,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 @@ -3032,6 +3098,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 607e403eb2325ef3c29c027eecae39cbce2801d0 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 3 Sep 2021 19:25:16 -0600 Subject: [PATCH 020/102] 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 d76ac1bfe..9c580ea51 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2978,10 +2978,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, @@ -2995,101 +2994,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 8264cc546d4b14ef3971eaef37c7920304d9f767 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 3 Sep 2021 19:56:13 -0600 Subject: [PATCH 021/102] 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 4aaa5bc0b..7c983e58e 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1554,9 +1554,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 9c580ea51..bf89f745f 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2998,9 +2998,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 8d74233aa51d53bf48d5d3561d372b4c29700776 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 3 Sep 2021 19:56:53 -0600 Subject: [PATCH 022/102] 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 7c983e58e..6689731d8 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -75,7 +75,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 0232f0fa18d92616fa67aed1fddbebc29db3248a Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 3 Sep 2021 20:20:42 -0600 Subject: [PATCH 023/102] 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 2b7d94a8551fbe64dd9225eea7ea6fcec09fb3fc Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 3 Sep 2021 20:30:19 -0600 Subject: [PATCH 024/102] 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 cd33f69c7e6030ed79a0e7d9a4b4a87c67c0f08a Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 3 Sep 2021 20:30:52 -0600 Subject: [PATCH 025/102] 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 97d1306e34285c2a2a69791ff130a23cbcd60795 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 4 Sep 2021 19:16:17 -0600 Subject: [PATCH 026/102] 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 6689731d8..e96a9e324 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1554,10 +1554,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 @@ -1576,6 +1589,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 @@ -1591,7 +1605,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") @@ -1601,7 +1616,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 bf89f745f..bcb27c8ed 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3049,8 +3049,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 ) @@ -3072,6 +3072,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 619ecc9728f42c8d6c7f027659182f7904116e20 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 4 Sep 2021 19:47:04 -0600 Subject: [PATCH 027/102] 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 e96a9e324..4c11937b2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1562,13 +1562,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 bcb27c8ed..f7c0b0f38 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3031,6 +3031,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 d1c4030b88b13f3e645d95a4f0ed7876bb746b1b Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 4 Sep 2021 19:58:42 -0600 Subject: [PATCH 028/102] 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 9e73d026637acc1893a37e0dde7b4322f3a6cbe0 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 4 Sep 2021 21:55:55 -0600 Subject: [PATCH 029/102] 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 3117f5ee1..3d491c867 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 @@ -25,6 +26,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 4c11937b2..d6004760a 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) @@ -77,6 +77,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, @@ -142,6 +146,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: @@ -159,21 +183,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 @@ -384,7 +398,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( @@ -496,6 +510,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 f7c0b0f38..1915b3f34 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 @@ -3034,10 +3034,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}) @@ -3099,3 +3105,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 93da13212c6cb46534a2725739b9c47dd225b3ec Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 5 Sep 2021 22:27:14 -0600 Subject: [PATCH 030/102] 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 3d491c867..9a96e1f19 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -111,6 +111,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 @@ -118,8 +119,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 d6004760a..6e25689f3 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -158,13 +158,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 1915b3f34..348aa3290 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3092,7 +3092,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( @@ -3137,8 +3137,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 9f96b977f6a1ea08d036dc53ebacd2aed2cea976 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 9 Sep 2021 13:42:35 -0600 Subject: [PATCH 031/102] removed interest method from exchange, will create a separate interest PR --- freqtrade/exchange/exchange.py | 25 ------------- tests/exchange/test_exchange.py | 63 --------------------------------- 2 files changed, 88 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 6e25689f3..dcc6e513a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1581,31 +1581,6 @@ class Exchange: self._async_get_trade_history(pair=pair, since=since, until=until, from_id=from_id)) - @retrier - def get_interest_rate( - self, - pair: str, - maker_or_taker: str, - 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) - """ - 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 348aa3290..5d7901420 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2998,69 +2998,6 @@ def test_fill_leverage_brackets(): 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("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 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 785b71aec1ca6a5468380db2801dc0fea24117fd Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 9 Sep 2021 13:42:43 -0600 Subject: [PATCH 032/102] formatting --- freqtrade/exchange/kraken.py | 4 ++-- freqtrade/persistence/models.py | 1 + tests/exchange/test_exchange.py | 4 ---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 5207018ad..861063b3f 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 {} @@ -84,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/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 630078ab3..b73611c1b 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -499,6 +499,7 @@ 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): diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 5d7901420..239704bdd 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2994,10 +2994,6 @@ def test_apply_leverage_to_stake_amount( assert exchange._apply_leverage_to_stake_amount(stake_amount, leverage) == min_stake_with_lev -def test_fill_leverage_brackets(): - return - - @pytest.mark.parametrize("collateral", [ (Collateral.CROSS), (Collateral.ISOLATED) From 2c7cf794f58b67fd290d32442e4b786d130fc79a Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 9 Sep 2021 14:24:07 -0600 Subject: [PATCH 033/102] Test for short exchange.stoploss exchange.stoploss_adjust --- freqtrade/exchange/binance.py | 5 ++- freqtrade/exchange/exchange.py | 1 + freqtrade/exchange/ftx.py | 9 +++--- tests/exchange/test_binance.py | 51 ++++++++++++++++++++---------- tests/exchange/test_ftx.py | 57 ++++++++++++++++++++++------------ tests/exchange/test_kraken.py | 39 +++++++++++++---------- 6 files changed, 105 insertions(+), 57 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 9a96e1f19..5680a7b47 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -55,7 +55,10 @@ class Binance(Exchange): :param side: "buy" or "sell" """ # 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) + limit_price_pct = order_types.get( + 'stoploss_on_exchange_limit_ratio', + 0.99 if side == 'sell' else 1.01 + ) rate = stop_price * limit_price_pct ordertype = "stop_loss_limit" diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index dcc6e513a..b07ee95ce 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -624,6 +624,7 @@ class Exchange: def _apply_leverage_to_stake_amount(self, stake_amount: float, leverage: float): """ + #TODO-lev: Find out how this works on Kraken and FTX # * Should be implemented by child classes if leverage affects the stake_amount Takes the minimum stake amount for a pair with no leverage and returns the minimum stake amount when leverage is considered diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 3e6ff01a3..870791cf5 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -57,7 +57,10 @@ 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_price_pct = order_types.get( + 'stoploss_on_exchange_limit_ratio', + 0.99 if side == "sell" else 1.01 + ) limit_rate = stop_price * limit_price_pct ordertype = "stop" @@ -164,10 +167,6 @@ class Ftx(Exchange): 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, diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index f2bd68154..ad55ede9b 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -9,12 +9,22 @@ from tests.conftest import get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers -@pytest.mark.parametrize('limitratio,expected', [ - (None, 220 * 0.99), - (0.99, 220 * 0.99), - (0.98, 220 * 0.98), +@pytest.mark.parametrize('limitratio,exchangelimitratio,expected,side', [ + (None, 1.05, 220 * 0.99, "sell"), + (0.99, 1.05, 220 * 0.99, "sell"), + (0.98, 1.05, 220 * 0.98, "sell"), + (None, 0.95, 220 * 1.01, "buy"), + (1.01, 0.95, 220 * 1.01, "buy"), + (1.02, 0.95, 220 * 1.02, "buy"), ]) -def test_stoploss_order_binance(default_conf, mocker, limitratio, expected): +def test_stoploss_order_binance( + default_conf, + mocker, + limitratio, + exchangelimitratio, + expected, + side +): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) order_type = 'stop_loss_limit' @@ -32,20 +42,25 @@ 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, side="sell", - order_types={'stoploss_on_exchange_limit_ratio': 1.05}) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=190, + side=side, + order_types={'stoploss_on_exchange_limit_ratio': exchangelimitratio} + ) api_mock.create_order.reset_mock() order_types = {} if limitratio is None else {'stoploss_on_exchange_limit_ratio': limitratio} order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, - order_types=order_types, side="sell") + order_types=order_types, side=side) assert 'id' in order assert 'info' in order assert order['id'] == order_id assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' assert api_mock.create_order.call_args_list[0][1]['type'] == order_type - assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['side'] == side assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 # Price should be 1% below stopprice assert api_mock.create_order.call_args_list[0][1]['price'] == expected @@ -55,17 +70,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={}, side="sell") + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("binance Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance", "stoploss", "create_order", retries=1, - pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") + pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) def test_stoploss_order_dry_run_binance(default_conf, mocker): @@ -94,18 +109,22 @@ def test_stoploss_order_dry_run_binance(default_conf, mocker): assert order['amount'] == 1 -def test_stoploss_adjust_binance(mocker, default_conf): +@pytest.mark.parametrize('sl1,sl2,sl3,side', [ + (1501, 1499, 1501, "sell"), + (1499, 1501, 1499, "buy") +]) +def test_stoploss_adjust_binance(mocker, default_conf, sl1, sl2, sl3, side): exchange = get_patched_exchange(mocker, default_conf, id='binance') order = { 'type': 'stop_loss_limit', 'price': 1500, 'info': {'stopPrice': 1500}, } - assert exchange.stoploss_adjust(1501, order, side="sell") - assert not exchange.stoploss_adjust(1499, order, side="sell") + assert exchange.stoploss_adjust(sl1, order, side=side) + assert not exchange.stoploss_adjust(sl2, order, side=side) # Test with invalid order case order['type'] = 'stop_loss' - assert not exchange.stoploss_adjust(1501, order, side="sell") + assert not exchange.stoploss_adjust(sl3, order, side=side) @pytest.mark.parametrize('pair,nominal_value,max_lev', [ diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 1ed528dd9..33eb0e3c4 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -16,7 +16,11 @@ STOPLOSS_ORDERTYPE = 'stop' # TODO-lev: All these stoploss tests with shorts -def test_stoploss_order_ftx(default_conf, mocker): +@pytest.mark.parametrize('order_price,exchangelimitratio,side', [ + (217.8, 1.05, "sell"), + (222.2, 0.95, "buy"), +]) +def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitratio, side): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) @@ -34,12 +38,12 @@ 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, side="sell", - order_types={'stoploss_on_exchange_limit_ratio': 1.05}) + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, side=side, + order_types={'stoploss_on_exchange_limit_ratio': exchangelimitratio}) assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE - assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['side'] == side assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params'] assert 'stopPrice' in api_mock.create_order.call_args_list[0][1]['params'] @@ -49,51 +53,52 @@ 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={}, side="sell") + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) assert 'id' in order assert 'info' in order assert order['id'] == order_id assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE - assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['side'] == side assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 assert 'orderPrice' not in api_mock.create_order.call_args_list[0][1]['params'] assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220 api_mock.create_order.reset_mock() order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, - order_types={'stoploss': 'limit'}, side="sell") + order_types={'stoploss': 'limit'}, side=side) assert 'id' in order assert 'info' in order assert order['id'] == order_id assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE - assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['side'] == side assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 assert 'orderPrice' in api_mock.create_order.call_args_list[0][1]['params'] - assert api_mock.create_order.call_args_list[0][1]['params']['orderPrice'] == 217.8 + assert api_mock.create_order.call_args_list[0][1]['params']['orderPrice'] == order_price assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220 # test exception handling with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("ftx Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) ccxt_exceptionhandlers(mocker, default_conf, api_mock, "ftx", "stoploss", "create_order", retries=1, - pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") + pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) -def test_stoploss_order_dry_run_ftx(default_conf, mocker): +@pytest.mark.parametrize('side', [("sell"), ("buy")]) +def test_stoploss_order_dry_run_ftx(default_conf, mocker, side): api_mock = MagicMock() default_conf['dry_run'] = True mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) @@ -103,7 +108,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={}, side="sell") + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) assert 'id' in order assert 'info' in order @@ -114,20 +119,24 @@ def test_stoploss_order_dry_run_ftx(default_conf, mocker): assert order['amount'] == 1 -def test_stoploss_adjust_ftx(mocker, default_conf): +@pytest.mark.parametrize('sl1,sl2,sl3,side', [ + (1501, 1499, 1501, "sell"), + (1499, 1501, 1499, "buy") +]) +def test_stoploss_adjust_ftx(mocker, default_conf, sl1, sl2, sl3, side): exchange = get_patched_exchange(mocker, default_conf, id='ftx') order = { 'type': STOPLOSS_ORDERTYPE, 'price': 1500, } - assert exchange.stoploss_adjust(1501, order, side="sell") - assert not exchange.stoploss_adjust(1499, order, side="sell") + assert exchange.stoploss_adjust(sl1, order, side=side) + assert not exchange.stoploss_adjust(sl2, order, side=side) # Test with invalid order case ... order['type'] = 'stop_loss_limit' - assert not exchange.stoploss_adjust(1501, order, side="sell") + assert not exchange.stoploss_adjust(sl3, order, side=side) -def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order): +def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order, limit_buy_order): default_conf['dry_run'] = True order = MagicMock() order.myid = 123 @@ -160,6 +169,16 @@ def test_fetch_stoploss_order(default_conf, mocker, limit_sell_order): assert resp['type'] == 'stop' assert resp['status_stop'] == 'triggered' + api_mock.fetch_order = MagicMock(return_value=limit_buy_order) + + resp = exchange.fetch_stoploss_order('X', 'TKN/BTC') + assert resp + assert api_mock.fetch_order.call_count == 1 + assert resp['id_stop'] == 'mocked_limit_buy' + assert resp['id'] == 'X' + assert resp['type'] == 'stop' + assert resp['status_stop'] == 'triggered' + with pytest.raises(InvalidOrderException): api_mock.fetch_orders = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id='ftx') diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 8222f5ce8..01f27997c 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -164,11 +164,13 @@ 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): +@pytest.mark.parametrize('side,limitratio,adjustedprice', [ + ("buy", 0.99, 217.8), + ("sell", 1.01, 222.2), +]) +def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, limitratio, adjustedprice): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) @@ -185,9 +187,9 @@ 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, side="sell", + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, side=side, order_types={'stoploss': ordertype, - 'stoploss_on_exchange_limit_ratio': 0.99 + 'stoploss_on_exchange_limit_ratio': limitratio }) assert 'id' in order @@ -197,12 +199,12 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype): if ordertype == 'limit': assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_LIMIT_ORDERTYPE assert api_mock.create_order.call_args_list[0][1]['params'] == { - 'trading_agreement': 'agree', 'price2': 217.8} + 'trading_agreement': 'agree', 'price2': adjustedprice} else: assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE assert api_mock.create_order.call_args_list[0][1]['params'] == { 'trading_agreement': 'agree'} - assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['side'] == side assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 assert api_mock.create_order.call_args_list[0][1]['price'] == 220 @@ -210,20 +212,21 @@ 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={}, side="sell") + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("kraken Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken", "stoploss", "create_order", retries=1, - pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") + pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) -def test_stoploss_order_dry_run_kraken(default_conf, mocker): +@pytest.mark.parametrize('side', ['buy', 'sell']) +def test_stoploss_order_dry_run_kraken(default_conf, mocker, side): api_mock = MagicMock() default_conf['dry_run'] = True mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) @@ -233,7 +236,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={}, side="sell") + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) assert 'id' in order assert 'info' in order @@ -244,17 +247,21 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker): assert order['amount'] == 1 -def test_stoploss_adjust_kraken(mocker, default_conf): +@pytest.mark.parametrize('sl1,sl2,sl3,side', [ + (1501, 1499, 1501, "sell"), + (1499, 1501, 1499, "buy") +]) +def test_stoploss_adjust_kraken(mocker, default_conf, sl1, sl2, sl3, side): exchange = get_patched_exchange(mocker, default_conf, id='kraken') order = { 'type': STOPLOSS_ORDERTYPE, 'price': 1500, } - assert exchange.stoploss_adjust(1501, order, side="sell") - assert not exchange.stoploss_adjust(1499, order, side="sell") + assert exchange.stoploss_adjust(sl1, order, side=side) + assert not exchange.stoploss_adjust(sl2, order, side=side) # Test with invalid order case ... order['type'] = 'stop_loss_limit' - assert not exchange.stoploss_adjust(1501, order, side="sell") + assert not exchange.stoploss_adjust(sl3, order, side=side) @pytest.mark.parametrize('pair,nominal_value,max_lev', [ From 063861ada35d7f91711921d6769877635efe3263 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 9 Sep 2021 15:38:32 -0600 Subject: [PATCH 034/102] Added todos for short stoploss --- tests/test_freqtradebot.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index a841744b7..c4be69cd1 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1252,6 +1252,7 @@ def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog, @pytest.mark.usefixtures("init_persistence") def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, limit_buy_order, limit_sell_order) -> None: + # TODO-lev: test for short # When trailing stoploss is set stoploss = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) @@ -1362,6 +1363,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog, limit_buy_order, limit_sell_order) -> None: + # TODO-lev: test for short # When trailing stoploss is set stoploss = MagicMock(return_value={'id': 13434334}) patch_exchange(mocker) @@ -1439,6 +1441,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, limit_buy_order, limit_sell_order) -> None: # When trailing stoploss is set + # TODO-lev: test for short stoploss = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) mocker.patch.multiple( @@ -1548,7 +1551,7 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, limit_buy_order, limit_sell_order) -> None: - + # TODO-lev: test for short # When trailing stoploss is set stoploss = MagicMock(return_value={'id': 13434334}) patch_RPCManager(mocker) From 77fc21a16b0b56587ba5bf5b755d8ed9e11b5f95 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 9 Sep 2021 23:58:10 -0600 Subject: [PATCH 035/102] Patched test_fill_leverage_brackets_ftx so that exchange._leverage_brackets doesn't retain the values from binance --- tests/exchange/test_ftx.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 33eb0e3c4..a98e46b27 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -227,5 +227,8 @@ def test_get_max_leverage_ftx(default_conf, mocker, 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") + # TODO: This is a patch, develop a real solution + # TODO: _leverage_brackets retains it's value from the binance tests, but shouldn't + exchange._leverage_brackets = {} exchange.fill_leverage_brackets() assert exchange._leverage_brackets == {} From 77aa372909899ec0f823fd322acf4e71d933baa2 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 10 Sep 2021 02:09:27 -0600 Subject: [PATCH 036/102] Fixed test_ftx patch --- freqtrade/exchange/exchange.py | 3 +-- tests/exchange/test_ftx.py | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b07ee95ce..b9da0cf7c 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -75,8 +75,6 @@ class Exchange: } _ft_has: Dict = {} - _leverage_brackets: Dict = {} - _supported_trading_mode_collateral_pairs: List[Tuple[TradingMode, Collateral]] = [ # TradingMode.SPOT always supported and not required in this list ] @@ -90,6 +88,7 @@ class Exchange: self._api: ccxt.Exchange = None self._api_async: ccxt_async.Exchange = None self._markets: Dict = {} + self._leverage_brackets: Dict = {} self._config.update(config) diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index a98e46b27..33eb0e3c4 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -227,8 +227,5 @@ def test_get_max_leverage_ftx(default_conf, mocker, 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") - # TODO: This is a patch, develop a real solution - # TODO: _leverage_brackets retains it's value from the binance tests, but shouldn't - exchange._leverage_brackets = {} exchange.fill_leverage_brackets() assert exchange._leverage_brackets == {} From b0e05b92d3aa16404798edc2c657d904c2242c04 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 10 Sep 2021 13:39:42 -0600 Subject: [PATCH 037/102] Added minor changes from lev-exchange review --- freqtrade/exchange/binance.py | 15 ++++++++------- freqtrade/exchange/exchange.py | 7 +++++-- freqtrade/exchange/ftx.py | 10 +++++----- freqtrade/exchange/kraken.py | 15 +++++++++------ tests/exchange/test_binance.py | 17 ++++++++--------- tests/exchange/test_kraken.py | 25 ++++++++++++++++--------- 6 files changed, 51 insertions(+), 38 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 5680a7b47..f5a222d2d 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -41,8 +41,8 @@ class Binance(Exchange): """ 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']) + (side == "sell" and stop_loss > float(order['info']['stopPrice'])) or + (side == "buy" and stop_loss < float(order['info']['stopPrice'])) ) @retrier(retries=0) @@ -55,11 +55,12 @@ class Binance(Exchange): :param side: "buy" or "sell" """ # 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 if side == 'sell' else 1.01 - ) - rate = stop_price * limit_price_pct + limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) + if side == "sell": + # TODO: Name limit_rate in other exchange subclasses + rate = stop_price * limit_price_pct + else: + rate = stop_price * (2 - limit_price_pct) ordertype = "stop_loss_limit" diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b9da0cf7c..03ab281c9 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1597,8 +1597,7 @@ class Exchange: :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.") + return 1.0 @retrier def set_leverage(self, leverage: float, pair: Optional[str]): @@ -1606,6 +1605,10 @@ class Exchange: Set's the leverage before making a trade, in order to not have the same leverage on every trade """ + if not self.exchange_has("setLeverage"): + # Some exchanges only support one collateral type + return + try: self._api.set_leverage(symbol=pair, leverage=leverage) except ccxt.DDoSProtection as e: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 870791cf5..095d8eaa1 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -57,11 +57,11 @@ 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 if side == "sell" else 1.01 - ) - limit_rate = stop_price * limit_price_pct + limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) + if side == "sell": + limit_rate = stop_price * limit_price_pct + else: + limit_rate = stop_price * (2 - limit_price_pct) ordertype = "stop" diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 861063b3f..b72a92070 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -55,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: @@ -96,7 +96,10 @@ class Kraken(Exchange): if order_types.get('stoploss', 'market') == 'limit': ordertype = "stop-loss-limit" limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) - limit_rate = stop_price * limit_price_pct + if side == "sell": + limit_rate = stop_price * limit_price_pct + else: + limit_rate = stop_price * (2 - limit_price_pct) params['price2'] = self.price_to_precision(pair, limit_rate) else: ordertype = "stop-loss" @@ -144,13 +147,13 @@ class Kraken(Exchange): 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 [] + leverage_buy = info.get('leverage_buy', []) + leverage_sell = info.get('leverage_sell', []) 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" + "for {pair}. Please notify freqtrade because this has never happened before" ) if max(leverage_buy) < max(leverage_sell): leverages[pair] = leverage_buy diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index ad55ede9b..96287da44 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -9,19 +9,18 @@ from tests.conftest import get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers -@pytest.mark.parametrize('limitratio,exchangelimitratio,expected,side', [ - (None, 1.05, 220 * 0.99, "sell"), - (0.99, 1.05, 220 * 0.99, "sell"), - (0.98, 1.05, 220 * 0.98, "sell"), - (None, 0.95, 220 * 1.01, "buy"), - (1.01, 0.95, 220 * 1.01, "buy"), - (1.02, 0.95, 220 * 1.02, "buy"), +@pytest.mark.parametrize('limitratio,expected,side', [ + (None, 220 * 0.99, "sell"), + (0.99, 220 * 0.99, "sell"), + (0.98, 220 * 0.98, "sell"), + (None, 220 * 1.01, "buy"), + (0.99, 220 * 1.01, "buy"), + (0.98, 220 * 1.02, "buy"), ]) def test_stoploss_order_binance( default_conf, mocker, limitratio, - exchangelimitratio, expected, side ): @@ -47,7 +46,7 @@ def test_stoploss_order_binance( amount=1, stop_price=190, side=side, - order_types={'stoploss_on_exchange_limit_ratio': exchangelimitratio} + order_types={'stoploss_on_exchange_limit_ratio': 1.05} ) api_mock.create_order.reset_mock() diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 01f27997c..66e7f4f0b 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -166,11 +166,11 @@ def test_get_balances_prod(default_conf, mocker): @pytest.mark.parametrize('ordertype', ['market', 'limit']) -@pytest.mark.parametrize('side,limitratio,adjustedprice', [ - ("buy", 0.99, 217.8), - ("sell", 1.01, 222.2), +@pytest.mark.parametrize('side,adjustedprice', [ + ("sell", 217.8), + ("buy", 222.2), ]) -def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, limitratio, adjustedprice): +def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedprice): api_mock = MagicMock() order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) @@ -187,10 +187,15 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, limitratio exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, side=side, - order_types={'stoploss': ordertype, - 'stoploss_on_exchange_limit_ratio': limitratio - }) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + side=side, + order_types={ + 'stoploss': ordertype, + 'stoploss_on_exchange_limit_ratio': 0.99 + }) assert 'id' in order assert 'info' in order @@ -199,7 +204,9 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, limitratio if ordertype == 'limit': assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_LIMIT_ORDERTYPE assert api_mock.create_order.call_args_list[0][1]['params'] == { - 'trading_agreement': 'agree', 'price2': adjustedprice} + 'trading_agreement': 'agree', + 'price2': adjustedprice + } else: assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE assert api_mock.create_order.call_args_list[0][1]['params'] == { From 5b84298e0305c84d1ff2dee56e150a05c030f320 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 12 Sep 2021 00:03:02 -0600 Subject: [PATCH 038/102] kraken._apply_leverage_to_stake_amount --- freqtrade/exchange/kraken.py | 9 +++++++++ tests/exchange/test_exchange.py | 6 +++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index b72a92070..14ebedef0 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -163,6 +163,15 @@ class Kraken(Exchange): leverages[pair] = leverage_buy self._leverage_brackets = leverages + def _apply_leverage_to_stake_amount(self, stake_amount: float, leverage: float): + """ + Takes the minimum stake amount for a pair with no leverage and returns the minimum + stake amount when leverage is considered + :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 / leverage + def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: """ Returns the maximum leverage that a pair can be traded at diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 239704bdd..330793822 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2974,9 +2974,9 @@ def test_calculate_backoff(retrycount, max_retries, expected): ('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), + ('kraken', 9.0, 3.0, 3.0), + ('kraken', 20.0, 5.0, 4.0), + ('kraken', 100.0, 100.0, 1.0), # FTX ('ftx', 9.0, 3.0, 9.0), ('ftx', 20.0, 5.0, 20.0), From 1344c9f7fc3ae103baf207e1d67f5fe3ac11d57b Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 12 Sep 2021 01:30:26 -0600 Subject: [PATCH 039/102] _apply_leverage_to_min_stake_amount --- freqtrade/exchange/binance.py | 3 --- freqtrade/exchange/exchange.py | 2 +- freqtrade/exchange/kraken.py | 9 --------- tests/exchange/test_exchange.py | 15 ++++----------- 4 files changed, 5 insertions(+), 24 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index f5a222d2d..1fcdc0ab4 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -112,9 +112,6 @@ 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): - return stake_amount / leverage - @retrier def fill_leverage_brackets(self): """ diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 03ab281c9..dfee82d7b 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -630,7 +630,7 @@ class Exchange: :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 + return stake_amount / leverage # Dry-run methods diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 14ebedef0..b72a92070 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -163,15 +163,6 @@ class Kraken(Exchange): leverages[pair] = leverage_buy self._leverage_brackets = leverages - def _apply_leverage_to_stake_amount(self, stake_amount: float, leverage: float): - """ - Takes the minimum stake amount for a pair with no leverage and returns the minimum - stake amount when leverage is considered - :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 / leverage - def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: """ Returns the maximum leverage that a pair can be traded at diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 330793822..5c0323915 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2969,18 +2969,11 @@ def test_calculate_backoff(retrycount, max_retries, expected): assert calculate_backoff(retrycount, max_retries) == expected +@pytest.mark.parametrize('exchange', ['binance', 'kraken', 'ftx']) @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, 3.0), - ('kraken', 20.0, 5.0, 4.0), - ('kraken', 100.0, 100.0, 1.0), - # FTX - ('ftx', 9.0, 3.0, 9.0), - ('ftx', 20.0, 5.0, 20.0), - ('ftx', 100.0, 100.0, 100.0) + (9.0, 3.0, 3.0), + (20.0, 5.0, 4.0), + (100.0, 100.0, 1.0) ]) def test_apply_leverage_to_stake_amount( exchange, From 09418938fe920ae9f9dc335d54705f2347f102f9 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 12 Sep 2021 01:51:09 -0600 Subject: [PATCH 040/102] Updated kraken fill leverage brackets and set_leverage --- freqtrade/exchange/kraken.py | 15 ++++++++------- tests/exchange/test_kraken.py | 15 ++++++++++++--- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index b72a92070..97125f7ec 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -146,6 +146,7 @@ class Kraken(Exchange): leverages = {} for pair, market in self.markets.items(): + leverages[pair] = [1] info = market['info'] leverage_buy = info.get('leverage_buy', []) leverage_sell = info.get('leverage_sell', []) @@ -155,12 +156,12 @@ class Kraken(Exchange): f"The buy({leverage_buy}) and sell({leverage_sell}) leverage are not equal" "for {pair}. Please notify freqtrade because this has never happened before" ) - if max(leverage_buy) < max(leverage_sell): - leverages[pair] = leverage_buy + if max(leverage_buy) <= max(leverage_sell): + leverages[pair] += [int(lev) for lev in leverage_buy] else: - leverages[pair] = leverage_sell + leverages[pair] += [int(lev) for lev in leverage_sell] else: - leverages[pair] = leverage_buy + leverages[pair] += [int(lev) for lev in leverage_buy] self._leverage_brackets = leverages def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: @@ -173,7 +174,7 @@ class Kraken(Exchange): def set_leverage(self, pair, leverage): """ - Kraken set's the leverage as an option in the order object, so it doesn't do - anything in this function + Kraken set's the leverage as an option in the order object, so we need to + add it to params """ - return + self._params['leverage'] = leverage diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 66e7f4f0b..74a06c96c 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -292,7 +292,16 @@ def test_fill_leverage_brackets_kraken(default_conf, mocker): exchange.fill_leverage_brackets() assert exchange._leverage_brackets == { - 'BLK/BTC': ['2', '3'], - 'TKN/BTC': ['2', '3', '4', '5'], - 'ETH/BTC': ['2'] + 'BLK/BTC': [1, 2, 3], + 'TKN/BTC': [1, 2, 3, 4, 5], + 'ETH/BTC': [1, 2], + 'LTC/BTC': [1], + 'XRP/BTC': [1], + 'NEO/BTC': [1], + 'BTT/BTC': [1], + 'ETH/USDT': [1], + 'LTC/USDT': [1], + 'LTC/USD': [1], + 'XLTCUSDT': [1], + 'LTC/ETH': [1] } From 0c1e5afc91384c88e4a3bf6d7aba9894780ef6e3 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 12 Sep 2021 02:02:10 -0600 Subject: [PATCH 041/102] Added set leverage to create_order --- freqtrade/exchange/exchange.py | 5 +++-- freqtrade/exchange/kraken.py | 2 +- tests/exchange/test_exchange.py | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index dfee82d7b..a5778432a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -771,12 +771,13 @@ class Exchange: # Order handling def create_order(self, pair: str, ordertype: str, side: str, amount: float, - rate: float, time_in_force: str = 'gtc') -> Dict: + rate: float, time_in_force: str = 'gtc', leverage=1.0) -> Dict: if self._config['dry_run']: dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate) return dry_order + self._set_leverage(pair, leverage) params = self._params.copy() if time_in_force != 'gtc' and ordertype != 'market': param = self._ft_has.get('time_in_force_parameter', '') @@ -1600,7 +1601,7 @@ class Exchange: return 1.0 @retrier - def set_leverage(self, leverage: float, pair: Optional[str]): + 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 diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 97125f7ec..6981204a4 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -172,7 +172,7 @@ class Kraken(Exchange): """ return float(max(self._leverage_brackets[pair])) - def set_leverage(self, pair, leverage): + def _set_leverage(self, pair, leverage): """ Kraken set's the leverage as an option in the order object, so we need to add it to params diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 5c0323915..939e45d63 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2970,7 +2970,7 @@ def test_calculate_backoff(retrycount, max_retries, expected): @pytest.mark.parametrize('exchange', ['binance', 'kraken', 'ftx']) -@pytest.mark.parametrize('exchange,stake_amount,leverage,min_stake_with_lev', [ +@pytest.mark.parametrize('stake_amount,leverage,min_stake_with_lev', [ (9.0, 3.0, 3.0), (20.0, 5.0, 4.0), (100.0, 100.0, 1.0) @@ -2992,7 +2992,7 @@ def test_apply_leverage_to_stake_amount( (Collateral.ISOLATED) ]) @pytest.mark.parametrize("exchange_name", [("ftx"), ("binance")]) -def test_set_leverage(mocker, default_conf, exchange_name, collateral): +def test__set_leverage(mocker, default_conf, exchange_name, collateral): api_mock = MagicMock() api_mock.set_leverage = MagicMock() @@ -3003,7 +3003,7 @@ def test_set_leverage(mocker, default_conf, exchange_name, collateral): default_conf, api_mock, exchange_name, - "set_leverage", + "_set_leverage", "set_leverage", pair="XRP/USDT", leverage=5.0 From bc102d57c91ff225c9ca3cd1745d7b8460efcce0 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 12 Sep 2021 02:06:18 -0600 Subject: [PATCH 042/102] Updated set leverage to check trading mode --- freqtrade/exchange/binance.py | 21 +++++++++++++++++++++ freqtrade/exchange/exchange.py | 9 +++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 1fcdc0ab4..178fa49da 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -151,3 +151,24 @@ class Binance(Exchange): if nominal_value >= min_amount: max_lev = 1/margin_req return max_lev + + @retrier + 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 + """ + if not self.exchange_has("setLeverage"): + # Some exchanges only support one collateral type + return + + try: + if self.trading_mode == TradingMode.FUTURES: + 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 a5778432a..bef8f5e57 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -145,7 +145,7 @@ class Exchange: self._api_async = self._init_ccxt( exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config) - trading_mode: TradingMode = ( + self.trading_mode: TradingMode = ( TradingMode(config.get('trading_mode')) if config.get('trading_mode') else TradingMode.SPOT @@ -156,7 +156,7 @@ class Exchange: else None ) - if trading_mode != TradingMode.SPOT: + if self.trading_mode != TradingMode.SPOT: self.fill_leverage_brackets() logger.info('Using Exchange "%s"', self.name) @@ -176,7 +176,7 @@ class Exchange: self.validate_order_time_in_force(config.get('order_time_in_force', {})) self.validate_required_startup_candles(config.get('startup_candle_count', 0), config.get('timeframe', '')) - self.validate_trading_mode_and_collateral(trading_mode, collateral) + self.validate_trading_mode_and_collateral(self.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 @@ -777,7 +777,8 @@ class Exchange: dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate) return dry_order - self._set_leverage(pair, leverage) + if self.trading_mode != TradingMode.SPOT: + self._set_leverage(pair, leverage) params = self._params.copy() if time_in_force != 'gtc' and ordertype != 'market': param = self._ft_has.get('time_in_force_parameter', '') From ad44048e29adc83518bdf2f5a8b9aaa2bc721897 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 12 Sep 2021 02:42:13 -0600 Subject: [PATCH 043/102] customized set_leverage for different exchanges --- freqtrade/exchange/binance.py | 13 ++++++++----- freqtrade/exchange/exchange.py | 9 +++++++-- freqtrade/exchange/kraken.py | 12 ++++++++++-- tests/exchange/test_exchange.py | 18 +++++++++--------- tests/exchange/test_kraken.py | 6 ++++++ 5 files changed, 40 insertions(+), 18 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 178fa49da..4315585b6 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -153,17 +153,20 @@ class Binance(Exchange): return max_lev @retrier - def _set_leverage(self, leverage: float, pair: Optional[str]): + def _set_leverage( + self, + leverage: float, + pair: Optional[str], + trading_mode: Optional[TradingMode] + ): """ Set's the leverage before making a trade, in order to not have the same leverage on every trade """ - if not self.exchange_has("setLeverage"): - # Some exchanges only support one collateral type - return + trading_mode = trading_mode or self.trading_mode try: - if self.trading_mode == TradingMode.FUTURES: + if trading_mode == TradingMode.FUTURES: self._api.set_leverage(symbol=pair, leverage=leverage) except ccxt.DDoSProtection as e: raise DDosProtection(e) from e diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index bef8f5e57..dd3304921 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -778,7 +778,7 @@ class Exchange: return dry_order if self.trading_mode != TradingMode.SPOT: - self._set_leverage(pair, leverage) + self._set_leverage(leverage, pair) params = self._params.copy() if time_in_force != 'gtc' and ordertype != 'market': param = self._ft_has.get('time_in_force_parameter', '') @@ -1602,7 +1602,12 @@ class Exchange: return 1.0 @retrier - def _set_leverage(self, leverage: float, pair: Optional[str]): + def _set_leverage( + self, + leverage: float, + pair: Optional[str], + trading_mode: Optional[TradingMode] + ): """ Set's the leverage before making a trade, in order to not have the same leverage on every trade diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 6981204a4..46f1ab934 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -172,9 +172,17 @@ class Kraken(Exchange): """ return float(max(self._leverage_brackets[pair])) - def _set_leverage(self, pair, leverage): + def _set_leverage( + self, + leverage: float, + pair: Optional[str], + trading_mode: Optional[TradingMode] + ): """ Kraken set's the leverage as an option in the order object, so we need to add it to params """ - self._params['leverage'] = leverage + if leverage > 1.0: + self._params['leverage'] = leverage + else: + del self._params['leverage'] diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 939e45d63..3231d9811 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -2987,12 +2987,12 @@ def test_apply_leverage_to_stake_amount( assert exchange._apply_leverage_to_stake_amount(stake_amount, leverage) == min_stake_with_lev -@pytest.mark.parametrize("collateral", [ - (Collateral.CROSS), - (Collateral.ISOLATED) +@pytest.mark.parametrize("exchange_name,trading_mode", [ + ("binance", TradingMode.FUTURES), + ("ftx", TradingMode.MARGIN), + ("ftx", TradingMode.FUTURES) ]) -@pytest.mark.parametrize("exchange_name", [("ftx"), ("binance")]) -def test__set_leverage(mocker, default_conf, exchange_name, collateral): +def test__set_leverage(mocker, default_conf, exchange_name, trading_mode): api_mock = MagicMock() api_mock.set_leverage = MagicMock() @@ -3006,7 +3006,8 @@ def test__set_leverage(mocker, default_conf, exchange_name, collateral): "_set_leverage", "set_leverage", pair="XRP/USDT", - leverage=5.0 + leverage=5.0, + trading_mode=trading_mode ) @@ -3014,8 +3015,7 @@ def test__set_leverage(mocker, default_conf, exchange_name, collateral): (Collateral.CROSS), (Collateral.ISOLATED) ]) -@pytest.mark.parametrize("exchange_name", [("ftx"), ("binance")]) -def test_set_margin_mode(mocker, default_conf, exchange_name, collateral): +def test_set_margin_mode(mocker, default_conf, collateral): api_mock = MagicMock() api_mock.set_margin_mode = MagicMock() @@ -3025,7 +3025,7 @@ def test_set_margin_mode(mocker, default_conf, exchange_name, collateral): mocker, default_conf, api_mock, - exchange_name, + "binance", "set_margin_mode", "set_margin_mode", pair="XRP/USDT", diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 74a06c96c..1a712fd3f 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -305,3 +305,9 @@ def test_fill_leverage_brackets_kraken(default_conf, mocker): 'XLTCUSDT': [1], 'LTC/ETH': [1] } + + +def test_kraken__set_leverage(default_conf, mocker): + exchange = get_patched_exchange(mocker, default_conf, id="kraken") + exchange._set_leverage(3) + assert exchange.params['leverage'] == 3 From e070bdd161a3b433c1afe0f0552f02632a03f47b Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 12 Sep 2021 03:09:51 -0600 Subject: [PATCH 044/102] set leverage more thorough tests --- freqtrade/exchange/binance.py | 4 ++-- freqtrade/exchange/exchange.py | 4 ++-- freqtrade/exchange/kraken.py | 7 ++++--- tests/exchange/test_binance.py | 23 +++++++++++++++++++++++ tests/exchange/test_ftx.py | 26 +++++++++++++++++++++++++- tests/exchange/test_kraken.py | 10 ++++++++-- 6 files changed, 64 insertions(+), 10 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 4315585b6..fcd027d52 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -156,8 +156,8 @@ class Binance(Exchange): def _set_leverage( self, leverage: float, - pair: Optional[str], - trading_mode: Optional[TradingMode] + pair: Optional[str] = None, + trading_mode: Optional[TradingMode] = None ): """ Set's the leverage before making a trade, in order to not diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index dd3304921..2fb63d201 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1605,8 +1605,8 @@ class Exchange: def _set_leverage( self, leverage: float, - pair: Optional[str], - trading_mode: Optional[TradingMode] + pair: Optional[str] = None, + trading_mode: Optional[TradingMode] = None ): """ Set's the leverage before making a trade, in order to not diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 46f1ab934..661000d4d 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -175,8 +175,8 @@ class Kraken(Exchange): def _set_leverage( self, leverage: float, - pair: Optional[str], - trading_mode: Optional[TradingMode] + pair: Optional[str] = None, + trading_mode: Optional[TradingMode] = None ): """ Kraken set's the leverage as an option in the order object, so we need to @@ -185,4 +185,5 @@ class Kraken(Exchange): if leverage > 1.0: self._params['leverage'] = leverage else: - del self._params['leverage'] + if 'leverage' in self._params: + del self._params['leverage'] diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 96287da44..dd012f4ab 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -4,6 +4,7 @@ from unittest.mock import MagicMock, PropertyMock import ccxt import pytest +from freqtrade.enums import TradingMode from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException from tests.conftest import get_patched_exchange from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -232,3 +233,25 @@ def test_fill_leverage_brackets_binance(default_conf, mocker): "fill_leverage_brackets", "load_leverage_brackets" ) + + +def test__set_leverage_binance(mocker, default_conf): + + api_mock = MagicMock() + api_mock.set_leverage = MagicMock() + type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) + + exchange = get_patched_exchange(mocker, default_conf, id="binance") + exchange._set_leverage(3.0, trading_mode=TradingMode.MARGIN) + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + "binance", + "_set_leverage", + "set_leverage", + pair="XRP/USDT", + leverage=5.0, + trading_mode=TradingMode.FUTURES + ) diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 33eb0e3c4..88c4c069b 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -1,9 +1,10 @@ from random import randint -from unittest.mock import MagicMock +from unittest.mock import MagicMock, PropertyMock import ccxt import pytest +from freqtrade.enums import TradingMode from freqtrade.exceptions import DependencyException, InvalidOrderException from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT from tests.conftest import get_patched_exchange @@ -229,3 +230,26 @@ def test_fill_leverage_brackets_ftx(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, id="ftx") exchange.fill_leverage_brackets() assert exchange._leverage_brackets == {} + + +@pytest.mark.parametrize("trading_mode", [ + (TradingMode.MARGIN), + (TradingMode.FUTURES) +]) +def test__set_leverage(mocker, default_conf, trading_mode): + + api_mock = MagicMock() + api_mock.set_leverage = MagicMock() + type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + "ftx", + "_set_leverage", + "set_leverage", + pair="XRP/USDT", + leverage=5.0, + trading_mode=trading_mode + ) diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 1a712fd3f..374b054a6 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -307,7 +307,13 @@ def test_fill_leverage_brackets_kraken(default_conf, mocker): } -def test_kraken__set_leverage(default_conf, mocker): +def test__set_leverage_kraken(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, id="kraken") + exchange._set_leverage(1) + assert 'leverage' not in exchange._params exchange._set_leverage(3) - assert exchange.params['leverage'] == 3 + assert exchange._params['leverage'] == 3 + exchange._set_leverage(1.0) + assert 'leverage' not in exchange._params + exchange._set_leverage(3.0) + assert exchange._params['leverage'] == 3 From 17a5cc96feb7058f7831cd0cbc5663654adfca13 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Mon, 13 Sep 2021 00:09:55 -0600 Subject: [PATCH 045/102] Added set_margin_mode to create_order --- freqtrade/exchange/binance.py | 4 ++++ freqtrade/exchange/exchange.py | 10 +++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index fcd027d52..d079d4ad6 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -152,6 +152,10 @@ class Binance(Exchange): max_lev = 1/margin_req return max_lev + def lev_prep(self, pair: str, leverage: float): + self.set_margin_mode(pair, self.collateral) + self._set_leverage(leverage, pair, self.trading_mode) + @retrier def _set_leverage( self, diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 2fb63d201..07a817006 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -150,7 +150,7 @@ class Exchange: if config.get('trading_mode') else TradingMode.SPOT ) - collateral: Optional[Collateral] = ( + self.collateral: Optional[Collateral] = ( Collateral(config.get('collateral')) if config.get('collateral') else None @@ -176,7 +176,7 @@ class Exchange: self.validate_order_time_in_force(config.get('order_time_in_force', {})) self.validate_required_startup_candles(config.get('startup_candle_count', 0), config.get('timeframe', '')) - self.validate_trading_mode_and_collateral(self.trading_mode, collateral) + self.validate_trading_mode_and_collateral(self.trading_mode, self.collateral) # Converts the interval provided in minutes in config to seconds self.markets_refresh_interval: int = exchange_config.get( "markets_refresh_interval", 60) * 60 @@ -770,6 +770,10 @@ class Exchange: # Order handling + def lev_prep(self, pair: str, leverage: float): + self.set_margin_mode(pair, self.collateral) + self._set_leverage(leverage, pair) + def create_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, time_in_force: str = 'gtc', leverage=1.0) -> Dict: @@ -778,7 +782,7 @@ class Exchange: return dry_order if self.trading_mode != TradingMode.SPOT: - self._set_leverage(leverage, pair) + self.lev_prep(pair, leverage) params = self._params.copy() if time_in_force != 'gtc' and ordertype != 'market': param = self._ft_has.get('time_in_force_parameter', '') From 3cdd06f5627b7baee7b5ec3ead09e036080e9b66 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Sep 2021 19:32:51 +0200 Subject: [PATCH 046/102] Add PeriodicCache --- freqtrade/configuration/PeriodicCache.py | 19 +++++++++++++++ freqtrade/configuration/__init__.py | 1 + requirements-dev.txt | 2 ++ tests/test_periodiccache.py | 31 ++++++++++++++++++++++++ 4 files changed, 53 insertions(+) create mode 100644 freqtrade/configuration/PeriodicCache.py create mode 100644 tests/test_periodiccache.py diff --git a/freqtrade/configuration/PeriodicCache.py b/freqtrade/configuration/PeriodicCache.py new file mode 100644 index 000000000..25c0c47f3 --- /dev/null +++ b/freqtrade/configuration/PeriodicCache.py @@ -0,0 +1,19 @@ +from datetime import datetime, timezone + +from cachetools.ttl import TTLCache + + +class PeriodicCache(TTLCache): + """ + Special cache that expires at "straight" times + A timer with ttl of 3600 (1h) will expire at every full hour (:00). + """ + + def __init__(self, maxsize, ttl, getsizeof=None): + def local_timer(): + ts = datetime.now(timezone.utc).timestamp() + offset = (ts % ttl) + return ts - offset + + # Init with smlight offset + super().__init__(maxsize=maxsize, ttl=ttl-1e-5, timer=local_timer, getsizeof=getsizeof) diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py index 607f9cdef..b1b268092 100644 --- a/freqtrade/configuration/__init__.py +++ b/freqtrade/configuration/__init__.py @@ -1,5 +1,6 @@ # flake8: noqa: F401 +from freqtrade.configuration.PeriodicCache import PeriodicCache from freqtrade.configuration.check_exchange import check_exchange, remove_credentials from freqtrade.configuration.config_setup import setup_utils_configuration from freqtrade.configuration.config_validation import validate_config_consistency diff --git a/requirements-dev.txt b/requirements-dev.txt index 34d5607f3..4859e1cc6 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -14,6 +14,8 @@ pytest-cov==2.12.1 pytest-mock==3.6.1 pytest-random-order==1.0.4 isort==5.9.3 +# For datetime mocking +time-machine==2.4.0 # Convert jupyter notebooks to markdown documents nbconvert==6.1.0 diff --git a/tests/test_periodiccache.py b/tests/test_periodiccache.py new file mode 100644 index 000000000..ff9b53684 --- /dev/null +++ b/tests/test_periodiccache.py @@ -0,0 +1,31 @@ +from freqtrade.configuration import PeriodicCache +import time_machine + + +def test_ttl_cache(): + + with time_machine.travel("2021-09-01 05:00:00 +00:00") as t: + + cache = PeriodicCache(5, ttl=60) + cache1h = PeriodicCache(5, ttl=3600) + + assert cache.timer() == 1630472400.0 + cache['a'] = 1235 + cache1h['a'] = 555123 + assert 'a' in cache + assert 'a' in cache1h + + t.move_to("2021-09-01 05:00:59 +00:00") + assert 'a' in cache + assert 'a' in cache1h + + # Cache expired + t.move_to("2021-09-01 05:01:00 +00:00") + assert 'a' not in cache + assert 'a' in cache1h + + t.move_to("2021-09-01 05:59:59 +00:00") + assert 'a' in cache1h + + t.move_to("2021-09-01 06:00:00 +00:00") + assert 'a' not in cache1h From 8afb3c4b70c3458772953c8be23976bd90a264ba Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 13 Sep 2021 19:33:28 +0200 Subject: [PATCH 047/102] Move AgeFilter cache to instance level --- freqtrade/plugins/pairlist/AgeFilter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/plugins/pairlist/AgeFilter.py b/freqtrade/plugins/pairlist/AgeFilter.py index dc5cab31e..32fef5fb0 100644 --- a/freqtrade/plugins/pairlist/AgeFilter.py +++ b/freqtrade/plugins/pairlist/AgeFilter.py @@ -18,14 +18,14 @@ logger = logging.getLogger(__name__) class AgeFilter(IPairList): - # Checked symbols cache (dictionary of ticker symbol => timestamp) - _symbolsChecked: Dict[str, int] = {} - def __init__(self, exchange, pairlistmanager, config: Dict[str, Any], pairlistconfig: Dict[str, Any], pairlist_pos: int) -> None: super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + # Checked symbols cache (dictionary of ticker symbol => timestamp) + self._symbolsChecked: Dict[str, int] = {} + self._min_days_listed = pairlistconfig.get('min_days_listed', 10) self._max_days_listed = pairlistconfig.get('max_days_listed', None) From c9ba52d7321b2cae17776c66723c837a75c3bfdf Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Sep 2021 06:30:18 +0200 Subject: [PATCH 048/102] Expire cached pairs in age-filter once per day --- freqtrade/plugins/pairlist/AgeFilter.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/freqtrade/plugins/pairlist/AgeFilter.py b/freqtrade/plugins/pairlist/AgeFilter.py index 32fef5fb0..1fba00123 100644 --- a/freqtrade/plugins/pairlist/AgeFilter.py +++ b/freqtrade/plugins/pairlist/AgeFilter.py @@ -11,6 +11,7 @@ from pandas import DataFrame from freqtrade.exceptions import OperationalException from freqtrade.misc import plural from freqtrade.plugins.pairlist.IPairList import IPairList +from freqtrade.configuration import PeriodicCache logger = logging.getLogger(__name__) @@ -25,6 +26,7 @@ class AgeFilter(IPairList): # Checked symbols cache (dictionary of ticker symbol => timestamp) self._symbolsChecked: Dict[str, int] = {} + self._too_young_pairs = PeriodicCache(maxsize=1000, ttl=86_400) self._min_days_listed = pairlistconfig.get('min_days_listed', 10) self._max_days_listed = pairlistconfig.get('max_days_listed', None) @@ -69,10 +71,12 @@ class AgeFilter(IPairList): :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: new allowlist """ - needed_pairs = [(p, '1d') for p in pairlist if p not in self._symbolsChecked] + needed_pairs = [ + (p, '1d') for p in pairlist + if p not in self._symbolsChecked and p not in self._too_young_pairs] if not needed_pairs: return pairlist - + logger.info(f"needed pairs {needed_pairs}") since_days = -( self._max_days_listed if self._max_days_listed else self._min_days_listed ) - 1 @@ -118,5 +122,6 @@ class AgeFilter(IPairList): " or more than " f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}" ) if self._max_days_listed else ''), logger.info) + self._too_young_pairs[pair] = arrow.utcnow().int_timestamp * 1000 return False return False From 3ce5197e8d07471e75be14df435c3d291b3b69c4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Sep 2021 06:45:26 +0200 Subject: [PATCH 049/102] Add Tests for AgeFilter caching closes #5552 --- freqtrade/configuration/__init__.py | 2 +- freqtrade/plugins/pairlist/AgeFilter.py | 11 ++-- tests/plugins/test_pairlist.py | 69 ++++++++++++++++--------- tests/test_periodiccache.py | 3 +- 4 files changed, 54 insertions(+), 31 deletions(-) diff --git a/freqtrade/configuration/__init__.py b/freqtrade/configuration/__init__.py index b1b268092..dccbb14b3 100644 --- a/freqtrade/configuration/__init__.py +++ b/freqtrade/configuration/__init__.py @@ -1,8 +1,8 @@ # flake8: noqa: F401 -from freqtrade.configuration.PeriodicCache import PeriodicCache from freqtrade.configuration.check_exchange import check_exchange, remove_credentials from freqtrade.configuration.config_setup import setup_utils_configuration from freqtrade.configuration.config_validation import validate_config_consistency from freqtrade.configuration.configuration import Configuration +from freqtrade.configuration.PeriodicCache import PeriodicCache from freqtrade.configuration.timerange import TimeRange diff --git a/freqtrade/plugins/pairlist/AgeFilter.py b/freqtrade/plugins/pairlist/AgeFilter.py index 1fba00123..c43bd0c4c 100644 --- a/freqtrade/plugins/pairlist/AgeFilter.py +++ b/freqtrade/plugins/pairlist/AgeFilter.py @@ -8,10 +8,10 @@ from typing import Any, Dict, List, Optional import arrow from pandas import DataFrame +from freqtrade.configuration import PeriodicCache from freqtrade.exceptions import OperationalException from freqtrade.misc import plural from freqtrade.plugins.pairlist.IPairList import IPairList -from freqtrade.configuration import PeriodicCache logger = logging.getLogger(__name__) @@ -26,7 +26,7 @@ class AgeFilter(IPairList): # Checked symbols cache (dictionary of ticker symbol => timestamp) self._symbolsChecked: Dict[str, int] = {} - self._too_young_pairs = PeriodicCache(maxsize=1000, ttl=86_400) + self._symbolsCheckFailed = PeriodicCache(maxsize=1000, ttl=86_400) self._min_days_listed = pairlistconfig.get('min_days_listed', 10) self._max_days_listed = pairlistconfig.get('max_days_listed', None) @@ -73,9 +73,10 @@ class AgeFilter(IPairList): """ needed_pairs = [ (p, '1d') for p in pairlist - if p not in self._symbolsChecked and p not in self._too_young_pairs] + if p not in self._symbolsChecked and p not in self._symbolsCheckFailed] if not needed_pairs: - return pairlist + # Remove pairs that have been removed before + return [p for p in pairlist if p not in self._symbolsCheckFailed] logger.info(f"needed pairs {needed_pairs}") since_days = -( self._max_days_listed if self._max_days_listed else self._min_days_listed @@ -122,6 +123,6 @@ class AgeFilter(IPairList): " or more than " f"{self._max_days_listed} {plural(self._max_days_listed, 'day')}" ) if self._max_days_listed else ''), logger.info) - self._too_young_pairs[pair] = arrow.utcnow().int_timestamp * 1000 + self._symbolsCheckFailed[pair] = arrow.utcnow().int_timestamp * 1000 return False return False diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 5f0701a22..3cdf6d341 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -4,6 +4,7 @@ import time from unittest.mock import MagicMock, PropertyMock import pytest +import time_machine from freqtrade.constants import AVAILABLE_PAIRLISTS from freqtrade.exceptions import OperationalException @@ -815,32 +816,52 @@ def test_agefilter_min_days_listed_too_large(mocker, default_conf, markets, tick def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, ohlcv_history): - ohlcv_data = { - ('ETH/BTC', '1d'): ohlcv_history, - ('TKN/BTC', '1d'): ohlcv_history, - ('LTC/BTC', '1d'): ohlcv_history, - } - mocker.patch.multiple('freqtrade.exchange.Exchange', - markets=PropertyMock(return_value=markets), - exchange_has=MagicMock(return_value=True), - get_tickers=tickers - ) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data), - ) + with time_machine.travel("2021-09-01 05:00:00 +00:00") as t: + ohlcv_data = { + ('ETH/BTC', '1d'): ohlcv_history, + ('TKN/BTC', '1d'): ohlcv_history, + ('LTC/BTC', '1d'): ohlcv_history, + ('XRP/BTC', '1d'): ohlcv_history.iloc[[0]], + } + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + exchange_has=MagicMock(return_value=True), + get_tickers=tickers, + refresh_latest_ohlcv=MagicMock(return_value=ohlcv_data), + ) - freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_agefilter) - assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 0 - freqtrade.pairlists.refresh_pairlist() - assert len(freqtrade.pairlists.whitelist) == 3 - assert freqtrade.exchange.refresh_latest_ohlcv.call_count > 0 + freqtrade = get_patched_freqtradebot(mocker, whitelist_conf_agefilter) + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 0 + freqtrade.pairlists.refresh_pairlist() + assert len(freqtrade.pairlists.whitelist) == 3 + assert freqtrade.exchange.refresh_latest_ohlcv.call_count > 0 - previous_call_count = freqtrade.exchange.refresh_latest_ohlcv.call_count - freqtrade.pairlists.refresh_pairlist() - assert len(freqtrade.pairlists.whitelist) == 3 - # Called once for XRP/BTC - assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count + 1 + previous_call_count = freqtrade.exchange.refresh_latest_ohlcv.call_count + freqtrade.pairlists.refresh_pairlist() + assert len(freqtrade.pairlists.whitelist) == 3 + # Call to XRP/BTC cached + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count + # Move to next day + t.move_to("2021-09-02 01:00:00 +00:00") + freqtrade.pairlists.refresh_pairlist() + assert len(freqtrade.pairlists.whitelist) == 3 + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count + 1 + + # Move another day with fresh mocks (now the pair is old enough) + t.move_to("2021-09-03 01:00:00 +00:00") + # Called once for XRP/BTC + ohlcv_data = { + ('ETH/BTC', '1d'): ohlcv_history, + ('TKN/BTC', '1d'): ohlcv_history, + ('LTC/BTC', '1d'): ohlcv_history, + ('XRP/BTC', '1d'): ohlcv_history, + } + mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', return_value=ohlcv_data) + freqtrade.pairlists.refresh_pairlist() + assert len(freqtrade.pairlists.whitelist) == 4 + # Called once (only for XRP/BTC) + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 1 def test_OffsetFilter_error(mocker, whitelist_conf) -> None: diff --git a/tests/test_periodiccache.py b/tests/test_periodiccache.py index ff9b53684..f874f9041 100644 --- a/tests/test_periodiccache.py +++ b/tests/test_periodiccache.py @@ -1,6 +1,7 @@ -from freqtrade.configuration import PeriodicCache import time_machine +from freqtrade.configuration import PeriodicCache + def test_ttl_cache(): From 35eda8c8c7c304051cae6b2a22453f56aa0ea4a3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 14 Sep 2021 07:07:20 +0200 Subject: [PATCH 050/102] Improve agefilter test --- tests/plugins/test_pairlist.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 3cdf6d341..34770c03d 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -821,7 +821,6 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o ('ETH/BTC', '1d'): ohlcv_history, ('TKN/BTC', '1d'): ohlcv_history, ('LTC/BTC', '1d'): ohlcv_history, - ('XRP/BTC', '1d'): ohlcv_history.iloc[[0]], } mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -837,16 +836,28 @@ def test_agefilter_caching(mocker, markets, whitelist_conf_agefilter, tickers, o assert len(freqtrade.pairlists.whitelist) == 3 assert freqtrade.exchange.refresh_latest_ohlcv.call_count > 0 - previous_call_count = freqtrade.exchange.refresh_latest_ohlcv.call_count freqtrade.pairlists.refresh_pairlist() assert len(freqtrade.pairlists.whitelist) == 3 # Call to XRP/BTC cached - assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count - # Move to next day - t.move_to("2021-09-02 01:00:00 +00:00") + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 2 + + ohlcv_data = { + ('ETH/BTC', '1d'): ohlcv_history, + ('TKN/BTC', '1d'): ohlcv_history, + ('LTC/BTC', '1d'): ohlcv_history, + ('XRP/BTC', '1d'): ohlcv_history.iloc[[0]], + } + mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', return_value=ohlcv_data) freqtrade.pairlists.refresh_pairlist() assert len(freqtrade.pairlists.whitelist) == 3 - assert freqtrade.exchange.refresh_latest_ohlcv.call_count == previous_call_count + 1 + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 1 + + # Move to next day + t.move_to("2021-09-02 01:00:00 +00:00") + mocker.patch('freqtrade.exchange.Exchange.refresh_latest_ohlcv', return_value=ohlcv_data) + freqtrade.pairlists.refresh_pairlist() + assert len(freqtrade.pairlists.whitelist) == 3 + assert freqtrade.exchange.refresh_latest_ohlcv.call_count == 1 # Move another day with fresh mocks (now the pair is old enough) t.move_to("2021-09-03 01:00:00 +00:00") From f7bae81d968a20e815d97d1dc72f86f0573edad2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Sep 2021 19:56:12 +0200 Subject: [PATCH 051/102] Dataframe should be copied after populate_indicator Without that, PerformanceWarnings can appear throughout hyperopt which are unnecessary and missleading for users closes #5408 --- freqtrade/strategy/interface.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 91963f223..00ad3faf0 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -777,10 +777,11 @@ class IStrategy(ABC, HyperStrategyMixin): Does not run advise_buy or advise_sell! Used by optimize operations only, not during dry / live runs. Using .copy() to get a fresh copy of the dataframe for every strategy run. + Also copy on output to avoid PerformanceWarnings pandas 1.3.0 started to show. Has positive effects on memory usage for whatever reason - also when using only one strategy. """ - return {pair: self.advise_indicators(pair_data.copy(), {'pair': pair}) + return {pair: self.advise_indicators(pair_data.copy(), {'pair': pair}).copy() for pair, pair_data in data.items()} def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: From 57ea0c322f8018992cb6c4303a43af29da310227 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Sep 2021 20:20:31 +0200 Subject: [PATCH 052/102] Rename indicator_space to buy_indicator_space --- docs/hyperopt.md | 2 +- freqtrade/optimize/hyperopt.py | 2 +- freqtrade/optimize/hyperopt_auto.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index e69b761c4..09d43939a 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -677,7 +677,7 @@ If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace f These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used. -If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. +If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 14b155546..d047b7311 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -241,7 +241,7 @@ class Hyperopt: if HyperoptTools.has_space(self.config, 'buy'): logger.debug("Hyperopt has 'buy' space") - self.buy_space = self.custom_hyperopt.indicator_space() + self.buy_space = self.custom_hyperopt.buy_indicator_space() if HyperoptTools.has_space(self.config, 'sell'): logger.debug("Hyperopt has 'sell' space") diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 1f11cec80..80fe090c2 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -56,7 +56,7 @@ class HyperOptAuto(IHyperOpt): else: _format_exception_message(category) - def indicator_space(self) -> List['Dimension']: + def buy_indicator_space(self) -> List['Dimension']: return self._get_indicator_space('buy') def sell_indicator_space(self) -> List['Dimension']: From 90ad1789323e2c3061110f34bd5d3c54d60b65d7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Sep 2021 21:04:25 +0200 Subject: [PATCH 053/102] Remove verbosity of edge --- freqtrade/plugins/pairlist/AgeFilter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/AgeFilter.py b/freqtrade/plugins/pairlist/AgeFilter.py index c43bd0c4c..5627d82ce 100644 --- a/freqtrade/plugins/pairlist/AgeFilter.py +++ b/freqtrade/plugins/pairlist/AgeFilter.py @@ -77,7 +77,7 @@ class AgeFilter(IPairList): if not needed_pairs: # Remove pairs that have been removed before return [p for p in pairlist if p not in self._symbolsCheckFailed] - logger.info(f"needed pairs {needed_pairs}") + since_days = -( self._max_days_listed if self._max_days_listed else self._min_days_listed ) - 1 From c0811ae8969799857e987ed7e93d1c1e78dd3a2f Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 15 Sep 2021 21:36:53 +0200 Subject: [PATCH 054/102] Add possibility to override estimator from within hyperopt --- docs/advanced-hyperopt.md | 32 ++++++++++++++++++++++++ freqtrade/optimize/hyperopt.py | 8 ++++-- freqtrade/optimize/hyperopt_auto.py | 5 +++- freqtrade/optimize/hyperopt_interface.py | 13 +++++++++- 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index f2f52b7dd..f5a52ff49 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -98,6 +98,38 @@ class MyAwesomeStrategy(IStrategy): !!! Note All overrides are optional and can be mixed/matched as necessary. +### Overriding Base estimator + +You can define your own estimator for Hyperopt by implementing `generate_estimator()` in the Hyperopt subclass. + +```python +class MyAwesomeStrategy(IStrategy): + class HyperOpt: + def generate_estimator(): + return "RF" + +``` + +Possible values are either one of "GP", "RF", "ET", "GBRT" (Details can be found in the [scikit-optimize documentation](https://scikit-optimize.github.io/)), or "an instance of a class that inherits from `RegressorMixin` (from sklearn) and where the `predict` method has an optional `return_std` argument, which returns `std(Y | x)` along with `E[Y | x]`". + +Some research will be necessary to find additional Regressors. + +Example for `ExtraTreesRegressor` ("ET") with additional parameters: + +```python +class MyAwesomeStrategy(IStrategy): + class HyperOpt: + def generate_estimator(): + from skopt.learning import ExtraTreesRegressor + # Corresponds to "ET" - but allows additional parameters. + return ExtraTreesRegressor(n_estimators=100) + +``` + +!!! Note + While custom estimators can be provided, it's up to you as User to do research on possible parameters and analyze / understand which ones should be used. + If you're unsure about this, best use one of the Defaults (`"ET"` has proven to be the most versatile) without further parameters. + ## Space options For the additional spaces, scikit-optimize (in combination with Freqtrade) provides the following space types: diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index d047b7311..56d11934a 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -365,10 +365,14 @@ class Hyperopt: } def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer: + estimator = self.custom_hyperopt.generate_estimator() + logger.info(f"Using estimator {estimator}.") + # TODO: Impact of changing acq_optimizer to "sampling" is unclear + # (other than that it fails with other optimizers when using custom sklearn regressors) return Optimizer( dimensions, - base_estimator="ET", - acq_optimizer="auto", + base_estimator=estimator, + acq_optimizer="sampling", n_initial_points=INITIAL_POINTS, acq_optimizer_kwargs={'n_jobs': cpu_count}, random_state=self.random_state, diff --git a/freqtrade/optimize/hyperopt_auto.py b/freqtrade/optimize/hyperopt_auto.py index 80fe090c2..c1c769c72 100644 --- a/freqtrade/optimize/hyperopt_auto.py +++ b/freqtrade/optimize/hyperopt_auto.py @@ -12,7 +12,7 @@ from freqtrade.exceptions import OperationalException with suppress(ImportError): from skopt.space import Dimension -from freqtrade.optimize.hyperopt_interface import IHyperOpt +from freqtrade.optimize.hyperopt_interface import EstimatorType, IHyperOpt def _format_exception_message(space: str) -> str: @@ -79,3 +79,6 @@ class HyperOptAuto(IHyperOpt): def trailing_space(self) -> List['Dimension']: return self._get_func('trailing_space')() + + def generate_estimator(self) -> EstimatorType: + return self._get_func('generate_estimator')() diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 8fb40f557..53b4f087c 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -5,8 +5,9 @@ This module defines the interface to apply for hyperopt import logging import math from abc import ABC -from typing import Dict, List +from typing import Dict, List, Union +from sklearn.base import RegressorMixin from skopt.space import Categorical, Dimension, Integer from freqtrade.exchange import timeframe_to_minutes @@ -17,6 +18,8 @@ from freqtrade.strategy import IStrategy logger = logging.getLogger(__name__) +EstimatorType = Union[RegressorMixin, str] + class IHyperOpt(ABC): """ @@ -37,6 +40,14 @@ class IHyperOpt(ABC): IHyperOpt.ticker_interval = str(config['timeframe']) # DEPRECATED IHyperOpt.timeframe = str(config['timeframe']) + def generate_estimator(self) -> EstimatorType: + """ + Return base_estimator. + Can be any of "GP", "RF", "ET", "GBRT" or an instance of a class + inheriting from RegressorMixin (from sklearn). + """ + return 'ET' + def generate_roi_table(self, params: Dict) -> Dict[int, float]: """ Create a ROI table. From cbaf477bec00071877eb3946b8b2c89ec15c7ac4 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Wed, 15 Sep 2021 21:55:19 -0600 Subject: [PATCH 055/102] changed kraken set lev implementation --- freqtrade/exchange/exchange.py | 13 +++++++++---- freqtrade/exchange/kraken.py | 10 ++++++---- tests/exchange/test_exchange.py | 8 +++++++- tests/exchange/test_kraken.py | 12 ------------ 4 files changed, 22 insertions(+), 21 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 0c3b29e1a..554873100 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -775,6 +775,13 @@ class Exchange: self.set_margin_mode(pair, self.collateral) self._set_leverage(leverage, pair) + def _get_params(self, time_in_force: str, ordertype: str, leverage: float) -> Dict: + params = self._params.copy() + if time_in_force != 'gtc' and ordertype != 'market': + param = self._ft_has.get('time_in_force_parameter', '') + params.update({param: time_in_force}) + return params + def create_order(self, pair: str, ordertype: str, side: str, amount: float, rate: float, time_in_force: str = 'gtc', leverage=1.0) -> Dict: @@ -784,10 +791,8 @@ class Exchange: if self.trading_mode != TradingMode.SPOT: self.lev_prep(pair, leverage) - params = self._params.copy() - if time_in_force != 'gtc' and ordertype != 'market': - param = self._ft_has.get('time_in_force_parameter', '') - params.update({param: time_in_force}) + + params = self._get_params(time_in_force, ordertype, leverage) try: # Set the precision for amount and price(rate) as accepted by the exchange diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 661000d4d..60af42c69 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -182,8 +182,10 @@ class Kraken(Exchange): Kraken set's the leverage as an option in the order object, so we need to add it to params """ + return + + def _get_params(self, time_in_force: str, ordertype: str, leverage: float) -> Dict: + params = super()._get_params(time_in_force, ordertype, leverage) if leverage > 1.0: - self._params['leverage'] = leverage - else: - if 'leverage' in self._params: - del self._params['leverage'] + params['leverage'] = leverage + return params diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 535726b4b..8c7f908b2 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1110,7 +1110,13 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) order = exchange.create_order( - pair='ETH/BTC', ordertype=ordertype, side=side, amount=1, rate=200) + pair='ETH/BTC', + ordertype=ordertype, + side=side, + amount=1, + rate=200, + leverage=3.0 + ) assert 'id' in order assert 'info' in order diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 374b054a6..74a06c96c 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -305,15 +305,3 @@ def test_fill_leverage_brackets_kraken(default_conf, mocker): 'XLTCUSDT': [1], 'LTC/ETH': [1] } - - -def test__set_leverage_kraken(default_conf, mocker): - exchange = get_patched_exchange(mocker, default_conf, id="kraken") - exchange._set_leverage(1) - assert 'leverage' not in exchange._params - exchange._set_leverage(3) - assert exchange._params['leverage'] == 3 - exchange._set_leverage(1.0) - assert 'leverage' not in exchange._params - exchange._set_leverage(3.0) - assert exchange._params['leverage'] == 3 From 994c3c3a4c5f36c02d249f4c13466b284c4991af Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 16 Sep 2021 07:13:25 +0200 Subject: [PATCH 056/102] Add some errorhandling for custom estimator --- freqtrade/optimize/hyperopt.py | 14 ++++++++++---- tests/optimize/test_hyperopt.py | 4 ++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 56d11934a..9549b4054 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -45,7 +45,7 @@ progressbar.streams.wrap_stdout() logger = logging.getLogger(__name__) -INITIAL_POINTS = 30 +INITIAL_POINTS = 5 # Keep no more than SKOPT_MODEL_QUEUE_SIZE models # in the skopt model queue, to optimize memory consumption @@ -366,13 +366,19 @@ class Hyperopt: def get_optimizer(self, dimensions: List[Dimension], cpu_count) -> Optimizer: estimator = self.custom_hyperopt.generate_estimator() + + acq_optimizer = "sampling" + if isinstance(estimator, str): + if estimator not in ("GP", "RF", "ET", "GBRT"): + raise OperationalException(f"Estimator {estimator} not supported.") + else: + acq_optimizer = "auto" + logger.info(f"Using estimator {estimator}.") - # TODO: Impact of changing acq_optimizer to "sampling" is unclear - # (other than that it fails with other optimizers when using custom sklearn regressors) return Optimizer( dimensions, base_estimator=estimator, - acq_optimizer="sampling", + acq_optimizer=acq_optimizer, n_initial_points=INITIAL_POINTS, acq_optimizer_kwargs={'n_jobs': cpu_count}, random_state=self.random_state, diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index b34c3a916..e4ce29d44 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -884,6 +884,10 @@ def test_in_strategy_auto_hyperopt(mocker, hyperopt_conf, tmpdir, fee) -> None: assert hyperopt.backtesting.strategy.buy_rsi.value != 35 assert hyperopt.backtesting.strategy.sell_rsi.value != 74 + hyperopt.custom_hyperopt.generate_estimator = lambda *args, **kwargs: 'ET1' + with pytest.raises(OperationalException, match="Estimator ET1 not supported."): + hyperopt.get_optimizer([], 2) + def test_SKDecimal(): space = SKDecimal(1, 2, decimals=2) From 457e738b4a049cbc522987612dc2e4bd91b272e9 Mon Sep 17 00:00:00 2001 From: Sergey Khliustin Date: Thu, 16 Sep 2021 14:48:02 +0300 Subject: [PATCH 057/102] Added days parameter to PerformanceFilter --- freqtrade/persistence/models.py | 28 ++++++++++++++++++- .../plugins/pairlist/PerformanceFilter.py | 15 ++++++++-- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 8c8c1e0a9..91a26eba7 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -2,7 +2,7 @@ This module contains the class to persist trades into SQLite """ import logging -from datetime import datetime, timezone +from datetime import datetime, timezone, timedelta from decimal import Decimal from typing import Any, Dict, List, Optional @@ -856,6 +856,32 @@ class Trade(_DECL_BASE, LocalTrade): for pair, profit, profit_abs, count in pair_rates ] + @staticmethod + def get_performance(days: int) -> List[Dict[str, Any]]: + """ + Returns List of dicts containing all Trades, including profit and trade count + NOTE: Not supported in Backtesting. + """ + start_date = datetime.today() - timedelta(days) + pair_rates = Trade.query.with_entities( + Trade.pair, + func.sum(Trade.close_profit).label('profit_sum'), + func.sum(Trade.close_profit_abs).label('profit_sum_abs'), + func.count(Trade.pair).label('count') + ).filter(Trade.is_open.is_(False) & (Trade.close_date >= start_date))\ + .group_by(Trade.pair) \ + .order_by(desc('profit_sum_abs')) \ + .all() + return [ + { + 'pair': pair, + 'profit': profit, + 'profit_abs': profit_abs, + 'count': count + } + for pair, profit, profit_abs, count in pair_rates + ] + @staticmethod def get_best_pair(start_date: datetime = datetime.fromtimestamp(0)): """ diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index 46a289ae6..4d530fe88 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -2,11 +2,12 @@ Performance pair list filter """ import logging -from typing import Dict, List +from typing import Dict, List, Any import pandas as pd from freqtrade.persistence import Trade +from freqtrade.exceptions import OperationalException from freqtrade.plugins.pairlist.IPairList import IPairList @@ -15,6 +16,13 @@ logger = logging.getLogger(__name__) class PerformanceFilter(IPairList): + def __init__(self, exchange, pairlistmanager, + config: Dict[str, Any], pairlistconfig: Dict[str, Any], + pairlist_pos: int) -> None: + super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) + + self._days = pairlistconfig.get('days', 0) + @property def needstickers(self) -> bool: """ @@ -40,7 +48,10 @@ class PerformanceFilter(IPairList): """ # Get the trading performance for pairs from database try: - performance = pd.DataFrame(Trade.get_overall_performance()) + if self._days > 0: + performance = pd.DataFrame(Trade.get_performance(self._days)) + else: + performance = pd.DataFrame(Trade.get_overall_performance()) except AttributeError: # Performancefilter does not work in backtesting. self.log_once("PerformanceFilter is not available in this mode.", logger.warning) From 57c7926515b9973a3a6f767963f5e0c52e2b44c2 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 16 Sep 2021 23:05:13 -0600 Subject: [PATCH 058/102] leverage updates on exchange classes --- freqtrade/data/leverage_brackets.json | 1214 +++++++++++++++++++++++++ freqtrade/exchange/binance.py | 74 +- freqtrade/exchange/exchange.py | 44 +- freqtrade/exchange/ftx.py | 16 +- freqtrade/exchange/kraken.py | 17 +- freqtrade/freqtradebot.py | 3 +- tests/exchange/test_binance.py | 52 +- tests/exchange/test_exchange.py | 44 +- tests/exchange/test_ftx.py | 86 +- tests/exchange/test_kraken.py | 34 +- tests/test_freqtradebot.py | 9 +- 11 files changed, 1467 insertions(+), 126 deletions(-) create mode 100644 freqtrade/data/leverage_brackets.json diff --git a/freqtrade/data/leverage_brackets.json b/freqtrade/data/leverage_brackets.json new file mode 100644 index 000000000..4450b015e --- /dev/null +++ b/freqtrade/data/leverage_brackets.json @@ -0,0 +1,1214 @@ +{ + "1000SHIB/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "1INCH/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "AAVE/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"] + ], + "ADA/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "ADA/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "AKRO/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ALGO/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [1000000.0, "0.25"], + [2000000.0, "0.5"] + ], + "ALICE/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ALPHA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ANKR/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ATA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ATOM/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [1000000.0, "0.25"], + [2000000.0, "0.5"] + ], + "AUDIO/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "AVAX/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "AXS/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"], + [15000000.0, "0.5"] + ], + "BAKE/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BAL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BAND/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BAT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BCH/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "BEL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BLZ/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BNB/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "BNB/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "BTC/BUSD": [ + [0.0, "0.004"], + [25000.0, "0.005"], + [100000.0, "0.01"], + [500000.0, "0.025"], + [1000000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"], + [30000000.0, "0.5"] + ], + "BTC/USDT": [ + [0.0, "0.004"], + [50000.0, "0.005"], + [250000.0, "0.01"], + [1000000.0, "0.025"], + [5000000.0, "0.05"], + [20000000.0, "0.1"], + [50000000.0, "0.125"], + [100000000.0, "0.15"], + [200000000.0, "0.25"], + [300000000.0, "0.5"] + ], + "BTCBUSD_210129": [ + [0.0, "0.004"], + [5000.0, "0.005"], + [25000.0, "0.01"], + [100000.0, "0.025"], + [500000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"] + ], + "BTCBUSD_210226": [ + [0.0, "0.004"], + [5000.0, "0.005"], + [25000.0, "0.01"], + [100000.0, "0.025"], + [500000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"] + ], + "BTCDOM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BTCSTUSDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BTCUSDT_210326": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "BTCUSDT_210625": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "BTCUSDT_210924": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"], + [20000000.0, "0.5"] + ], + "BTS/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BTT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "BZRX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "C98/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CELR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CHR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CHZ/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "COMP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "COTI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CRV/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CTK/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "CVC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DASH/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DEFI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DENT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DGB/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DODO/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DOGE/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "DOGE/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "DOT/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "DOTECOUSDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "DYDX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "EGLD/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ENJ/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "EOS/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "ETC/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "ETH/BUSD": [ + [0.0, "0.004"], + [25000.0, "0.005"], + [100000.0, "0.01"], + [500000.0, "0.025"], + [1000000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"], + [30000000.0, "0.5"] + ], + "ETH/USDT": [ + [0.0, "0.005"], + [10000.0, "0.0065"], + [100000.0, "0.01"], + [500000.0, "0.02"], + [1000000.0, "0.05"], + [2000000.0, "0.1"], + [5000000.0, "0.125"], + [10000000.0, "0.15"], + [20000000.0, "0.25"] + ], + "ETHUSDT_210326": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "ETHUSDT_210625": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "ETHUSDT_210924": [ + [0.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"], + [20000000.0, "0.5"] + ], + "FIL/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"] + ], + "FLM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "FTM/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "FTT/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "GRT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "GTC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "HBAR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "HNT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "HOT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ICP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ICX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "IOST/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "IOTA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "IOTX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "KAVA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "KEEP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "KNC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "KSM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LENDUSDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LINA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LINK/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "LIT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LRC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "LTC/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "LUNA/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"], + [15000000.0, "0.5"] + ], + "MANA/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "MASK/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "MATIC/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [150000.0, "0.05"], + [250000.0, "0.1"], + [500000.0, "0.125"], + [750000.0, "0.25"], + [1000000.0, "0.5"] + ], + "MKR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "MTL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "NEAR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "NEO/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "NKN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "OCEAN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "OGN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "OMG/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ONE/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ONT/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "QTUM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RAY/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "REEF/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "REN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RLC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RSR/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RUNE/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "RVN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SAND/USDT": [ + [0.0, "0.012"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SFP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SKL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SNX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SOL/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "SOL/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.25"], + [10000000.0, "0.5"] + ], + "SRM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "STMX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "STORJ/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SUSHI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "SXP/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "THETA/USDT": [ + [0.0, "0.01"], + [50000.0, "0.025"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"] + ], + "TLM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "TOMO/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "TRB/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "TRX/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "UNFI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "UNI/USDT": [ + [0.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.1665"], + [10000000.0, "0.25"] + ], + "VET/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "WAVES/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "XEM/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "XLM/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "XMR/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "XRP/BUSD": [ + [0.0, "0.025"], + [100000.0, "0.05"], + [500000.0, "0.1"], + [1000000.0, "0.15"], + [2000000.0, "0.25"], + [5000000.0, "0.5"] + ], + "XRP/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "XTZ/USDT": [ + [0.0, "0.0065"], + [10000.0, "0.01"], + [50000.0, "0.02"], + [250000.0, "0.05"], + [1000000.0, "0.1"], + [2000000.0, "0.125"], + [5000000.0, "0.15"], + [10000000.0, "0.25"] + ], + "YFI/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "YFII/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ZEC/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ZEN/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ZIL/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ], + "ZRX/USDT": [ + [0.0, "0.01"], + [5000.0, "0.025"], + [25000.0, "0.05"], + [100000.0, "0.1"], + [250000.0, "0.125"], + [1000000.0, "0.5"] + ] +} diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index f4998d9a7..17e865d64 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -1,5 +1,7 @@ """ Binance exchange subclass """ +import json import logging +from pathlib import Path from typing import Dict, List, Optional, Tuple import arrow @@ -47,8 +49,8 @@ class Binance(Exchange): ) @retrier(retries=0) - def stoploss(self, pair: str, amount: float, - stop_price: float, order_types: Dict, side: str) -> Dict: + def stoploss(self, pair: str, amount: float, stop_price: float, + order_types: Dict, side: str, leverage: float) -> Dict: """ creates a stoploss limit order. this stoploss-limit is binance-specific. @@ -76,7 +78,7 @@ class Binance(Exchange): if self._config['dry_run']: dry_order = self.create_dry_run_order( - pair, ordertype, side, amount, stop_price) + pair, ordertype, side, amount, stop_price, leverage) return dry_order try: @@ -87,8 +89,15 @@ class Binance(Exchange): rate = self.price_to_precision(pair, rate) - order = self._api.create_order(symbol=pair, type=ordertype, side=side, - amount=amount, price=rate, params=params) + order = self._api.create_order( + symbol=pair, + type=ordertype, + side=side, + amount=amount, + price=rate, + params=params, + leverage=leverage + ) logger.info('stoploss limit order added for %s. ' 'stop price: %s. limit: %s', pair, stop_price, rate) self._log_exchange_response('create_stoploss_order', order) @@ -119,26 +128,33 @@ class Binance(Exchange): Assigns property _leverage_brackets to a dictionary of information about the leverage allowed on each pair """ - try: - leverage_brackets = self._api.load_leverage_brackets() - for pair, brackets in leverage_brackets.items(): - self._leverage_brackets[pair] = [ - [ - min_amount, - float(margin_req) - ] for [ - min_amount, - margin_req - ] in brackets - ] + if self.trading_mode == TradingMode.FUTURES: + try: + if self._config['dry_run']: + leverage_brackets_path = Path('data') / 'leverage_brackets.json' + with open(leverage_brackets_path) as json_file: + leverage_brackets = json.load(json_file) + else: + leverage_brackets = self._api.load_leverage_brackets() - except ccxt.DDoSProtection as e: - raise DDosProtection(e) from e - except (ccxt.NetworkError, ccxt.ExchangeError) as e: - raise TemporaryError(f'Could not fetch leverage amounts due to' - f'{e.__class__.__name__}. Message: {e}') from e - except ccxt.BaseError as e: - raise OperationalException(e) from e + for pair, brackets in leverage_brackets.items(): + self._leverage_brackets[pair] = [ + [ + min_amount, + float(margin_req) + ] for [ + min_amount, + margin_req + ] in brackets + ] + + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError(f'Could not fetch leverage amounts due to' + f'{e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: """ @@ -153,10 +169,6 @@ class Binance(Exchange): max_lev = 1/margin_req return max_lev - def lev_prep(self, pair: str, leverage: float): - self.set_margin_mode(pair, self.collateral) - self._set_leverage(leverage, pair, self.trading_mode) - @retrier def _set_leverage( self, @@ -170,9 +182,11 @@ class Binance(Exchange): """ trading_mode = trading_mode or self.trading_mode + if self._config['dry_run'] or trading_mode != TradingMode.FUTURES: + return + try: - if trading_mode == TradingMode.FUTURES: - self._api.set_leverage(symbol=pair, leverage=leverage) + self._api.set_leverage(symbol=pair, leverage=leverage) except ccxt.DDoSProtection as e: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 554873100..8bbc88235 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -258,6 +258,13 @@ class Exchange: """exchange ccxt precisionMode""" return self._api.precisionMode + @property + def running_live_mode(self) -> bool: + return ( + self._config['runmode'].value not in ('backtest', 'hyperopt') and + not self._config['dry_run'] + ) + def _log_exchange_response(self, endpoint, response) -> None: """ Log exchange responses """ if self.log_responses: @@ -617,15 +624,13 @@ class Exchange: # The value returned should satisfy both limits: for amount (base currency) and # for cost (quote, stake currency), so max() is used here. # See also #2575 at github. - return self._apply_leverage_to_stake_amount( + return self._divide_stake_amount_by_leverage( max(min_stake_amounts) * amount_reserve_percent, leverage or 1.0 ) - def _apply_leverage_to_stake_amount(self, stake_amount: float, leverage: float): + def _divide_stake_amount_by_leverage(self, stake_amount: float, leverage: float): """ - #TODO-lev: Find out how this works on Kraken and FTX - # * Should be implemented by child classes if leverage affects the stake_amount Takes the minimum stake amount for a pair with no leverage and returns the minimum stake amount when leverage is considered :param stake_amount: The stake amount for a pair before leverage is considered @@ -636,7 +641,7 @@ class Exchange: # Dry-run methods def create_dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, - rate: float, params: Dict = {}) -> Dict[str, Any]: + rate: float, leverage: float, params: Dict = {}) -> Dict[str, Any]: order_id = f'dry_run_{side}_{datetime.now().timestamp()}' _amount = self.amount_to_precision(pair, amount) dry_order: Dict[str, Any] = { @@ -653,7 +658,8 @@ class Exchange: 'timestamp': arrow.utcnow().int_timestamp * 1000, 'status': "closed" if ordertype == "market" else "open", 'fee': None, - 'info': {} + 'info': {}, + 'leverage': leverage } if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]: dry_order["info"] = {"stopPrice": dry_order["price"]} @@ -663,7 +669,7 @@ class Exchange: average = self.get_dry_market_fill_price(pair, side, amount, rate) dry_order.update({ 'average': average, - 'cost': dry_order['amount'] * average, + 'cost': (dry_order['amount'] * average) / leverage }) dry_order = self.add_dry_order_fee(pair, dry_order) @@ -771,7 +777,7 @@ class Exchange: # Order handling - def lev_prep(self, pair: str, leverage: float): + def _lev_prep(self, pair: str, leverage: float): self.set_margin_mode(pair, self.collateral) self._set_leverage(leverage, pair) @@ -783,14 +789,14 @@ class Exchange: return params def create_order(self, pair: str, ordertype: str, side: str, amount: float, - rate: float, time_in_force: str = 'gtc', leverage=1.0) -> Dict: - + rate: float, leverage: float = 1.0, time_in_force: str = 'gtc') -> Dict: + # TODO-lev: remove default for leverage if self._config['dry_run']: - dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate) + dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate, leverage) return dry_order if self.trading_mode != TradingMode.SPOT: - self.lev_prep(pair, leverage) + self._lev_prep(pair, leverage) params = self._get_params(time_in_force, ordertype, leverage) @@ -831,8 +837,8 @@ class Exchange: """ raise OperationalException(f"stoploss is not implemented for {self.name}.") - def stoploss(self, pair: str, amount: float, - stop_price: float, order_types: Dict, side: str) -> Dict: + def stoploss(self, pair: str, amount: float, stop_price: float, + order_types: Dict, side: str, leverage: float) -> Dict: """ creates a stoploss order. The precise ordertype is determined by the order_types dict or exchange default. @@ -1595,15 +1601,13 @@ class Exchange: self._async_get_trade_history(pair=pair, since=since, until=until, from_id=from_id)) - @retrier def fill_leverage_brackets(self): """ #TODO-lev: Should maybe be renamed, leverage_brackets might not be accurate for kraken Assigns property _leverage_brackets to a dictionary of information about the leverage allowed on each pair """ - raise OperationalException( - f"{self.name.capitalize()}.fill_leverage_brackets has not been implemented.") + return def get_max_leverage(self, pair: Optional[str], nominal_value: Optional[float]) -> float: """ @@ -1624,7 +1628,9 @@ class Exchange: Set's the leverage before making a trade, in order to not have the same leverage on every trade """ - if not self.exchange_has("setLeverage"): + # TODO-lev: Make a documentation page that says you can't run 2 bots + # TODO-lev: on the same account with leverage + if self._config['dry_run'] or not self.exchange_has("setLeverage"): # Some exchanges only support one collateral type return @@ -1644,7 +1650,7 @@ class Exchange: Set's the margin mode on the exchange to cross or isolated for a specific pair :param symbol: base/quote currency pair (e.g. "ADA/USDT") ''' - if not self.exchange_has("setMarginMode"): + if self._config['dry_run'] or not self.exchange_has("setMarginMode"): # Some exchanges only support one collateral type return diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 095d8eaa1..eaf9a0477 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -49,8 +49,8 @@ class Ftx(Exchange): ) @retrier(retries=0) - def stoploss(self, pair: str, amount: float, - stop_price: float, order_types: Dict, side: str) -> Dict: + def stoploss(self, pair: str, amount: float, stop_price: float, + order_types: Dict, side: str, leverage: float) -> Dict: """ Creates a stoploss order. depending on order_types.stoploss configuration, uses 'market' or limit order. @@ -69,7 +69,7 @@ class Ftx(Exchange): if self._config['dry_run']: dry_order = self.create_dry_run_order( - pair, ordertype, side, amount, stop_price) + pair, ordertype, side, amount, stop_price, leverage) return dry_order try: @@ -81,8 +81,14 @@ class Ftx(Exchange): params['stopPrice'] = stop_price amount = self.amount_to_precision(pair, amount) - order = self._api.create_order(symbol=pair, type=ordertype, side=side, - amount=amount, params=params) + order = self._api.create_order( + symbol=pair, + type=ordertype, + side=side, + amount=amount, + leverage=leverage, + params=params + ) self._log_exchange_response('create_stoploss_order', order) logger.info('stoploss order added for %s. ' 'stop price: %s.', pair, stop_price) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 60af42c69..d6a816c9e 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -85,8 +85,8 @@ class Kraken(Exchange): )) @retrier(retries=0) - def stoploss(self, pair: str, amount: float, - stop_price: float, order_types: Dict, side: str) -> Dict: + def stoploss(self, pair: str, amount: float, stop_price: float, + order_types: Dict, side: str, leverage: float) -> Dict: """ Creates a stoploss market order. Stoploss market orders is the only stoploss type supported by kraken. @@ -108,14 +108,21 @@ class Kraken(Exchange): if self._config['dry_run']: dry_order = self.create_dry_run_order( - pair, ordertype, side, amount, stop_price) + pair, ordertype, side, amount, stop_price, leverage) return dry_order try: amount = self.amount_to_precision(pair, amount) - order = self._api.create_order(symbol=pair, type=ordertype, side=side, - amount=amount, price=stop_price, params=params) + order = self._api.create_order( + symbol=pair, + type=ordertype, + side=side, + amount=amount, + price=stop_price, + leverage=leverage, + params=params + ) self._log_exchange_response('create_stoploss_order', order) logger.info('stoploss order added for %s. ' 'stop price: %s.', pair, stop_price) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ca1e9f9b0..2738ec634 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -737,7 +737,8 @@ class FreqtradeBot(LoggingMixin): amount=trade.amount, stop_price=stop_price, order_types=self.strategy.order_types, - side=trade.exit_side + side=trade.exit_side, + leverage=trade.leverage ) order_obj = Order.parse_from_ccxt_object(stoploss_order, trade.pair, 'stoploss') diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 5a1087534..f0642fda9 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -48,13 +48,20 @@ def test_stoploss_order_binance( amount=1, stop_price=190, side=side, - order_types={'stoploss_on_exchange_limit_ratio': 1.05} + order_types={'stoploss_on_exchange_limit_ratio': 1.05}, + leverage=1.0 ) api_mock.create_order.reset_mock() order_types = {} if limitratio is None else {'stoploss_on_exchange_limit_ratio': limitratio} - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, - order_types=order_types, side=side) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types=order_types, + side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -71,17 +78,31 @@ def test_stoploss_order_binance( with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0) with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("binance Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) ccxt_exceptionhandlers(mocker, default_conf, api_mock, "binance", "stoploss", "create_order", retries=1, - pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + pair='ETH/BTC', amount=1, stop_price=220, order_types={}, + side=side, leverage=1.0) def test_stoploss_order_dry_run_binance(default_conf, mocker): @@ -94,12 +115,25 @@ def test_stoploss_order_dry_run_binance(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance') with pytest.raises(OperationalException): - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, side="sell", - order_types={'stoploss_on_exchange_limit_ratio': 1.05}) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=190, + side="sell", + order_types={'stoploss_on_exchange_limit_ratio': 1.05}, + leverage=1.0 + ) api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side="sell", + leverage=1.0 + ) assert 'id' in order assert 'info' in order diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 8c7f908b2..d641b0a63 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -403,7 +403,6 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: # With Leverage result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss, 3.0) assert isclose(result, expected_result/3) - # TODO-lev: Min stake for base, kraken and ftx # min amount is set markets["ETH/BTC"]["limits"] = { @@ -420,7 +419,6 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: # With Leverage result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 5.0) assert isclose(result, expected_result/5) - # TODO-lev: Min stake for base, kraken and ftx # min amount and cost are set (cost is minimal) markets["ETH/BTC"]["limits"] = { @@ -437,7 +435,6 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: # With Leverage result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 10) assert isclose(result, expected_result/10) - # TODO-lev: Min stake for base, kraken and ftx # min amount and cost are set (amount is minial) markets["ETH/BTC"]["limits"] = { @@ -454,7 +451,6 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: # With Leverage result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss, 7.0) assert isclose(result, expected_result/7.0) - # TODO-lev: Min stake for base, kraken and ftx result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4) expected_result = max(8, 2 * 2) * 1.5 @@ -462,7 +458,6 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: # With Leverage result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4, 8.0) assert isclose(result, expected_result/8.0) - # TODO-lev: Min stake for base, kraken and ftx # Really big stoploss result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) @@ -471,7 +466,6 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: # With Leverage result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1, 12.0) assert isclose(result, expected_result/12) - # TODO-lev: Min stake for base, kraken and ftx def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: @@ -493,7 +487,6 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: assert round(result, 8) == round(expected_result, 8) result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss, 3.0) assert round(result, 8) == round(expected_result/3, 8) - # TODO-lev: Min stake for base, kraken and ftx def test_set_sandbox(default_conf, mocker): @@ -1004,7 +997,13 @@ def test_create_dry_run_order(default_conf, mocker, side, exchange_name): exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) order = exchange.create_dry_run_order( - pair='ETH/BTC', ordertype='limit', side=side, amount=1, rate=200) + pair='ETH/BTC', + ordertype='limit', + side=side, + amount=1, + rate=200, + leverage=1.0 + ) assert 'id' in order assert f'dry_run_{side}_' in order["id"] assert order["side"] == side @@ -1027,7 +1026,13 @@ def test_create_dry_run_order_limit_fill(default_conf, mocker, side, startprice, ) order = exchange.create_dry_run_order( - pair='LTC/USDT', ordertype='limit', side=side, amount=1, rate=startprice) + pair='LTC/USDT', + ordertype='limit', + side=side, + amount=1, + rate=startprice, + leverage=1.0 + ) assert order_book_l2_usd.call_count == 1 assert 'id' in order assert f'dry_run_{side}_' in order["id"] @@ -1073,7 +1078,13 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou ) order = exchange.create_dry_run_order( - pair='LTC/USDT', ordertype='market', side=side, amount=amount, rate=rate) + pair='LTC/USDT', + ordertype='market', + side=side, + amount=amount, + rate=rate, + leverage=1.0 + ) assert 'id' in order assert f'dry_run_{side}_' in order["id"] assert order["side"] == side @@ -2664,7 +2675,14 @@ def test_get_fee(default_conf, mocker, exchange_name): def test_stoploss_order_unsupported_exchange(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, id='bittrex') with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side="sell") + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side="sell", + leverage=1.0 + ) with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"): exchange.stoploss_adjust(1, {}, side="sell") @@ -3024,7 +3042,7 @@ def test_calculate_backoff(retrycount, max_retries, expected): (20.0, 5.0, 4.0), (100.0, 100.0, 1.0) ]) -def test_apply_leverage_to_stake_amount( +def test_divide_stake_amount_by_leverage( exchange, stake_amount, leverage, @@ -3033,7 +3051,7 @@ def test_apply_leverage_to_stake_amount( default_conf ): exchange = get_patched_exchange(mocker, default_conf, id=exchange) - assert exchange._apply_leverage_to_stake_amount(stake_amount, leverage) == min_stake_with_lev + assert exchange._divide_stake_amount_by_leverage(stake_amount, leverage) == min_stake_with_lev @pytest.mark.parametrize("exchange_name,trading_mode", [ diff --git a/tests/exchange/test_ftx.py b/tests/exchange/test_ftx.py index 88c4c069b..ca6b24d64 100644 --- a/tests/exchange/test_ftx.py +++ b/tests/exchange/test_ftx.py @@ -1,10 +1,9 @@ from random import randint -from unittest.mock import MagicMock, PropertyMock +from unittest.mock import MagicMock import ccxt import pytest -from freqtrade.enums import TradingMode from freqtrade.exceptions import DependencyException, InvalidOrderException from freqtrade.exchange.common import API_FETCH_ORDER_RETRY_COUNT from tests.conftest import get_patched_exchange @@ -14,8 +13,6 @@ from .test_exchange import ccxt_exceptionhandlers STOPLOSS_ORDERTYPE = 'stop' -# TODO-lev: All these stoploss tests with shorts - @pytest.mark.parametrize('order_price,exchangelimitratio,side', [ (217.8, 1.05, "sell"), @@ -39,8 +36,14 @@ def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitrati exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') # stoploss_on_exchange_limit_ratio is irrelevant for ftx market orders - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, side=side, - order_types={'stoploss_on_exchange_limit_ratio': exchangelimitratio}) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=190, + side=side, + order_types={'stoploss_on_exchange_limit_ratio': exchangelimitratio}, + leverage=1.0 + ) assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' assert api_mock.create_order.call_args_list[0][1]['type'] == STOPLOSS_ORDERTYPE @@ -54,7 +57,14 @@ def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitrati api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -67,8 +77,13 @@ def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitrati assert api_mock.create_order.call_args_list[0][1]['params']['stopPrice'] == 220 api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, - order_types={'stoploss': 'limit'}, side=side) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={'stoploss': 'limit'}, side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -85,17 +100,32 @@ def test_stoploss_order_ftx(default_conf, mocker, order_price, exchangelimitrati with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("ftx Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'ftx') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) ccxt_exceptionhandlers(mocker, default_conf, api_mock, "ftx", "stoploss", "create_order", retries=1, - pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + pair='ETH/BTC', amount=1, stop_price=220, order_types={}, + side=side, leverage=1.0) @pytest.mark.parametrize('side', [("sell"), ("buy")]) @@ -109,7 +139,14 @@ def test_stoploss_order_dry_run_ftx(default_conf, mocker, side): api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -230,26 +267,3 @@ def test_fill_leverage_brackets_ftx(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, id="ftx") exchange.fill_leverage_brackets() assert exchange._leverage_brackets == {} - - -@pytest.mark.parametrize("trading_mode", [ - (TradingMode.MARGIN), - (TradingMode.FUTURES) -]) -def test__set_leverage(mocker, default_conf, trading_mode): - - api_mock = MagicMock() - api_mock.set_leverage = MagicMock() - type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) - - ccxt_exceptionhandlers( - mocker, - default_conf, - api_mock, - "ftx", - "_set_leverage", - "set_leverage", - pair="XRP/USDT", - leverage=5.0, - trading_mode=trading_mode - ) diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index 74a06c96c..a8cd8d8ef 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -195,7 +195,9 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedpr order_types={ 'stoploss': ordertype, 'stoploss_on_exchange_limit_ratio': 0.99 - }) + }, + leverage=1.0 + ) assert 'id' in order assert 'info' in order @@ -219,17 +221,32 @@ def test_stoploss_order_kraken(default_conf, mocker, ordertype, side, adjustedpr with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) with pytest.raises(InvalidOrderException): api_mock.create_order = MagicMock( side_effect=ccxt.InvalidOrder("kraken Order would trigger immediately.")) exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken') - exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken", "stoploss", "create_order", retries=1, - pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + pair='ETH/BTC', amount=1, stop_price=220, order_types={}, + side=side, leverage=1.0) @pytest.mark.parametrize('side', ['buy', 'sell']) @@ -243,7 +260,14 @@ def test_stoploss_order_dry_run_kraken(default_conf, mocker, side): api_mock.create_order.reset_mock() - order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}, side=side) + order = exchange.stoploss( + pair='ETH/BTC', + amount=1, + stop_price=220, + order_types={}, + side=side, + leverage=1.0 + ) assert 'id' in order assert 'info' in order diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index f87841fe8..28ca0ee49 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1349,7 +1349,8 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, pair='ETH/BTC', order_types=freqtrade.strategy.order_types, stop_price=0.00002346 * 0.95, - side="sell" + side="sell", + leverage=1.0 ) # price fell below stoploss, so dry-run sells trade. @@ -1537,7 +1538,8 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, pair='ETH/BTC', order_types=freqtrade.strategy.order_types, stop_price=0.00002346 * 0.96, - side="sell" + side="sell", + leverage=1.0 ) # price fell below stoploss, so dry-run sells trade. @@ -1661,7 +1663,8 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, pair='NEO/BTC', order_types=freqtrade.strategy.order_types, stop_price=0.00002346 * 0.99, - side="sell" + side="sell", + leverage=1.0 ) From dced167ea2d93a7c0d46960b0ace6f4625809500 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Thu, 16 Sep 2021 23:23:36 -0600 Subject: [PATCH 059/102] fixed some stuff in the leverage brackets binance test --- freqtrade/exchange/binance.py | 2 +- tests/exchange/test_binance.py | 117 +++++++++++++++++++-------------- 2 files changed, 70 insertions(+), 49 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 17e865d64..769073052 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -131,7 +131,7 @@ class Binance(Exchange): if self.trading_mode == TradingMode.FUTURES: try: if self._config['dry_run']: - leverage_brackets_path = Path('data') / 'leverage_brackets.json' + leverage_brackets_path = Path('freqtrade/data') / 'leverage_brackets.json' with open(leverage_brackets_path) as json_file: leverage_brackets = json.load(json_file) else: diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index f0642fda9..03b1d5044 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -2,6 +2,9 @@ from datetime import datetime, timezone from random import randint from unittest.mock import MagicMock, PropertyMock +import json +from pathlib import Path + import ccxt import pytest @@ -203,58 +206,76 @@ def test_get_max_leverage_binance(default_conf, mocker, pair, nominal_value, max def test_fill_leverage_brackets_binance(default_conf, mocker): api_mock = MagicMock() - api_mock.load_leverage_brackets = MagicMock(return_value={ - 'ADA/BUSD': [[0.0, 0.025], - [100000.0, 0.05], - [500000.0, 0.1], - [1000000.0, 0.15], - [2000000.0, 0.25], - [5000000.0, 0.5]], - 'BTC/USDT': [[0.0, 0.004], - [50000.0, 0.005], - [250000.0, 0.01], - [1000000.0, 0.025], - [5000000.0, 0.05], - [20000000.0, 0.1], - [50000000.0, 0.125], - [100000000.0, 0.15], - [200000000.0, 0.25], - [300000000.0, 0.5]], - "ZEC/USDT": [[0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5]], + # api_mock.load_leverage_brackets = MagicMock(return_value={ + # 'ADA/BUSD': [[0.0, 0.025], + # [100000.0, 0.05], + # [500000.0, 0.1], + # [1000000.0, 0.15], + # [2000000.0, 0.25], + # [5000000.0, 0.5]], + # 'BTC/USDT': [[0.0, 0.004], + # [50000.0, 0.005], + # [250000.0, 0.01], + # [1000000.0, 0.025], + # [5000000.0, 0.05], + # [20000000.0, 0.1], + # [50000000.0, 0.125], + # [100000000.0, 0.15], + # [200000000.0, 0.25], + # [300000000.0, 0.5]], + # "ZEC/USDT": [[0.0, 0.01], + # [5000.0, 0.025], + # [25000.0, 0.05], + # [100000.0, 0.1], + # [250000.0, 0.125], + # [1000000.0, 0.5]], - }) + # }) exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + exchange.trading_mode = TradingMode.FUTURES exchange.fill_leverage_brackets() - assert exchange._leverage_brackets == { - 'ADA/BUSD': [[0.0, 0.025], - [100000.0, 0.05], - [500000.0, 0.1], - [1000000.0, 0.15], - [2000000.0, 0.25], - [5000000.0, 0.5]], - 'BTC/USDT': [[0.0, 0.004], - [50000.0, 0.005], - [250000.0, 0.01], - [1000000.0, 0.025], - [5000000.0, 0.05], - [20000000.0, 0.1], - [50000000.0, 0.125], - [100000000.0, 0.15], - [200000000.0, 0.25], - [300000000.0, 0.5]], - "ZEC/USDT": [[0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5]], - } + leverage_brackets_path = Path('freqtrade/data') / 'leverage_brackets.json' + with open(leverage_brackets_path) as json_file: + leverage_brackets = json.load(json_file) + + for pair, brackets in leverage_brackets.items(): + leverage_brackets[pair] = [ + [ + min_amount, + float(margin_req) + ] for [ + min_amount, + margin_req + ] in brackets + ] + + assert exchange._leverage_brackets == leverage_brackets + + # assert exchange._leverage_brackets == { + # 'ADA/BUSD': [[0.0, 0.025], + # [100000.0, 0.05], + # [500000.0, 0.1], + # [1000000.0, 0.15], + # [2000000.0, 0.25], + # [5000000.0, 0.5]], + # 'BTC/USDT': [[0.0, 0.004], + # [50000.0, 0.005], + # [250000.0, 0.01], + # [1000000.0, 0.025], + # [5000000.0, 0.05], + # [20000000.0, 0.1], + # [50000000.0, 0.125], + # [100000000.0, 0.15], + # [200000000.0, 0.25], + # [300000000.0, 0.5]], + # "ZEC/USDT": [[0.0, 0.01], + # [5000.0, 0.025], + # [25000.0, 0.05], + # [100000.0, 0.1], + # [250000.0, 0.125], + # [1000000.0, 0.5]], + # } api_mock = MagicMock() api_mock.load_leverage_brackets = MagicMock() From 798a0c9827a72e7b8abd6ecef6fe0a6531c78a60 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 17 Sep 2021 00:10:53 -0600 Subject: [PATCH 060/102] Tried to add call count to test_create_order --- tests/exchange/test_exchange.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index d641b0a63..8448819aa 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1094,10 +1094,7 @@ def test_create_dry_run_order_market_fill(default_conf, mocker, side, rate, amou assert round(order["average"], 4) == round(endprice, 4) -@pytest.mark.parametrize("side", [ - ("buy"), - ("sell") -]) +@pytest.mark.parametrize("side", ["buy", "sell"]) @pytest.mark.parametrize("ordertype,rate,marketprice", [ ("market", None, None), ("market", 200, True), @@ -1126,7 +1123,7 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, side=side, amount=1, rate=200, - leverage=3.0 + leverage=1.0 ) assert 'id' in order @@ -1138,6 +1135,21 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, assert api_mock.create_order.call_args[0][3] == 1 assert api_mock.create_order.call_args[0][4] is rate + assert api_mock._set_leverage.call_count == 0 if side == "buy" else 1 + assert api_mock.set_margin_mode.call_count == 0 if side == "buy" else 1 + + order = exchange.create_order( + pair='ETH/BTC', + ordertype=ordertype, + side=side, + amount=1, + rate=200, + leverage=3.0 + ) + + assert api_mock._set_leverage.call_count == 1 + assert api_mock.set_margin_mode.call_count == 1 + def test_buy_dry_run(default_conf, mocker): default_conf['dry_run'] = True From 32e52cd4606ce3a687a5e3640ddd4fcc64c46aaf Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 17 Sep 2021 00:41:00 -0600 Subject: [PATCH 061/102] Added leverage brackets dry run test --- freqtrade/exchange/binance.py | 2 +- tests/exchange/test_binance.py | 135 ++-- tests/exchange/test_exchange.py | 5 +- tests/leverage_brackets.py | 1215 +++++++++++++++++++++++++++++++ 4 files changed, 1283 insertions(+), 74 deletions(-) create mode 100644 tests/leverage_brackets.py diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 769073052..572fa2141 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -33,7 +33,7 @@ class Binance(Exchange): # TradingMode.SPOT always supported and not required in this list # (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported # (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported - # (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported + (TradingMode.FUTURES, Collateral.ISOLATED) ] def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 03b1d5044..4999e94af 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -2,16 +2,14 @@ from datetime import datetime, timezone from random import randint from unittest.mock import MagicMock, PropertyMock -import json -from pathlib import Path - import ccxt import pytest -from freqtrade.enums import TradingMode +from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException from tests.conftest import get_mock_coro, get_patched_exchange, log_has_re from tests.exchange.test_exchange import ccxt_exceptionhandlers +from tests.leverage_brackets import leverage_brackets @pytest.mark.parametrize('limitratio,expected,side', [ @@ -206,76 +204,61 @@ def test_get_max_leverage_binance(default_conf, mocker, pair, nominal_value, max def test_fill_leverage_brackets_binance(default_conf, mocker): api_mock = MagicMock() - # api_mock.load_leverage_brackets = MagicMock(return_value={ - # 'ADA/BUSD': [[0.0, 0.025], - # [100000.0, 0.05], - # [500000.0, 0.1], - # [1000000.0, 0.15], - # [2000000.0, 0.25], - # [5000000.0, 0.5]], - # 'BTC/USDT': [[0.0, 0.004], - # [50000.0, 0.005], - # [250000.0, 0.01], - # [1000000.0, 0.025], - # [5000000.0, 0.05], - # [20000000.0, 0.1], - # [50000000.0, 0.125], - # [100000000.0, 0.15], - # [200000000.0, 0.25], - # [300000000.0, 0.5]], - # "ZEC/USDT": [[0.0, 0.01], - # [5000.0, 0.025], - # [25000.0, 0.05], - # [100000.0, 0.1], - # [250000.0, 0.125], - # [1000000.0, 0.5]], + api_mock.load_leverage_brackets = MagicMock(return_value={ + 'ADA/BUSD': [[0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5]], + 'BTC/USDT': [[0.0, 0.004], + [50000.0, 0.005], + [250000.0, 0.01], + [1000000.0, 0.025], + [5000000.0, 0.05], + [20000000.0, 0.1], + [50000000.0, 0.125], + [100000000.0, 0.15], + [200000000.0, 0.25], + [300000000.0, 0.5]], + "ZEC/USDT": [[0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5]], - # }) + }) + default_conf['dry_run'] = False + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['collateral'] = Collateral.ISOLATED exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") - exchange.trading_mode = TradingMode.FUTURES exchange.fill_leverage_brackets() - leverage_brackets_path = Path('freqtrade/data') / 'leverage_brackets.json' - with open(leverage_brackets_path) as json_file: - leverage_brackets = json.load(json_file) - - for pair, brackets in leverage_brackets.items(): - leverage_brackets[pair] = [ - [ - min_amount, - float(margin_req) - ] for [ - min_amount, - margin_req - ] in brackets - ] - - assert exchange._leverage_brackets == leverage_brackets - - # assert exchange._leverage_brackets == { - # 'ADA/BUSD': [[0.0, 0.025], - # [100000.0, 0.05], - # [500000.0, 0.1], - # [1000000.0, 0.15], - # [2000000.0, 0.25], - # [5000000.0, 0.5]], - # 'BTC/USDT': [[0.0, 0.004], - # [50000.0, 0.005], - # [250000.0, 0.01], - # [1000000.0, 0.025], - # [5000000.0, 0.05], - # [20000000.0, 0.1], - # [50000000.0, 0.125], - # [100000000.0, 0.15], - # [200000000.0, 0.25], - # [300000000.0, 0.5]], - # "ZEC/USDT": [[0.0, 0.01], - # [5000.0, 0.025], - # [25000.0, 0.05], - # [100000.0, 0.1], - # [250000.0, 0.125], - # [1000000.0, 0.5]], - # } + assert exchange._leverage_brackets == { + 'ADA/BUSD': [[0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5]], + 'BTC/USDT': [[0.0, 0.004], + [50000.0, 0.005], + [250000.0, 0.01], + [1000000.0, 0.025], + [5000000.0, 0.05], + [20000000.0, 0.1], + [50000000.0, 0.125], + [100000000.0, 0.15], + [200000000.0, 0.25], + [300000000.0, 0.5]], + "ZEC/USDT": [[0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5]], + } api_mock = MagicMock() api_mock.load_leverage_brackets = MagicMock() @@ -291,12 +274,22 @@ def test_fill_leverage_brackets_binance(default_conf, mocker): ) +def test_fill_leverage_brackets_binance_dryrun(default_conf, mocker): + api_mock = MagicMock() + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['collateral'] = Collateral.ISOLATED + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") + exchange.fill_leverage_brackets() + + assert exchange._leverage_brackets == leverage_brackets() + + def test__set_leverage_binance(mocker, default_conf): api_mock = MagicMock() api_mock.set_leverage = MagicMock() type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) - + default_conf['dry_run'] = False exchange = get_patched_exchange(mocker, default_conf, id="binance") exchange._set_leverage(3.0, trading_mode=TradingMode.MARGIN) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 8448819aa..ce09e31e7 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -3076,6 +3076,7 @@ def test__set_leverage(mocker, default_conf, exchange_name, trading_mode): api_mock = MagicMock() api_mock.set_leverage = MagicMock() type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) + default_conf['dry_run'] = False ccxt_exceptionhandlers( mocker, @@ -3099,6 +3100,7 @@ def test_set_margin_mode(mocker, default_conf, collateral): api_mock = MagicMock() api_mock.set_margin_mode = MagicMock() type(api_mock).has = PropertyMock(return_value={'setMarginMode': True}) + default_conf['dry_run'] = False ccxt_exceptionhandlers( mocker, @@ -3130,7 +3132,6 @@ def test_set_margin_mode(mocker, default_conf, collateral): # TODO-lev: Remove once implemented ("binance", TradingMode.MARGIN, Collateral.CROSS, True), ("binance", TradingMode.FUTURES, Collateral.CROSS, True), - ("binance", TradingMode.FUTURES, Collateral.ISOLATED, True), ("kraken", TradingMode.MARGIN, Collateral.CROSS, True), ("kraken", TradingMode.FUTURES, Collateral.CROSS, True), ("ftx", TradingMode.MARGIN, Collateral.CROSS, True), @@ -3139,7 +3140,7 @@ def test_set_margin_mode(mocker, default_conf, collateral): # TODO-lev: Uncomment once implemented # ("binance", TradingMode.MARGIN, Collateral.CROSS, False), # ("binance", TradingMode.FUTURES, Collateral.CROSS, False), - # ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False), + ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False), # ("kraken", TradingMode.MARGIN, Collateral.CROSS, False), # ("kraken", TradingMode.FUTURES, Collateral.CROSS, False), # ("ftx", TradingMode.MARGIN, Collateral.CROSS, False), diff --git a/tests/leverage_brackets.py b/tests/leverage_brackets.py new file mode 100644 index 000000000..aa60a7af2 --- /dev/null +++ b/tests/leverage_brackets.py @@ -0,0 +1,1215 @@ +def leverage_brackets(): + return { + "1000SHIB/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "1INCH/USDT": [ + [0.0, 0.012], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "AAVE/USDT": [ + [0.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.1665], + [10000000.0, 0.25] + ], + "ADA/BUSD": [ + [0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5] + ], + "ADA/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "AKRO/USDT": [ + [0.0, 0.012], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ALGO/USDT": [ + [0.0, 0.01], + [50000.0, 0.025], + [150000.0, 0.05], + [250000.0, 0.1], + [500000.0, 0.125], + [1000000.0, 0.25], + [2000000.0, 0.5] + ], + "ALICE/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ALPHA/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ANKR/USDT": [ + [0.0, 0.012], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ATA/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ATOM/USDT": [ + [0.0, 0.01], + [50000.0, 0.025], + [150000.0, 0.05], + [250000.0, 0.1], + [500000.0, 0.125], + [1000000.0, 0.25], + [2000000.0, 0.5] + ], + "AUDIO/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "AVAX/USDT": [ + [0.0, 0.01], + [50000.0, 0.025], + [150000.0, 0.05], + [250000.0, 0.1], + [500000.0, 0.125], + [750000.0, 0.25], + [1000000.0, 0.5] + ], + "AXS/USDT": [ + [0.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.1665], + [10000000.0, 0.25], + [15000000.0, 0.5] + ], + "BAKE/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "BAL/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "BAND/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "BAT/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "BCH/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "BEL/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "BLZ/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "BNB/BUSD": [ + [0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5] + ], + "BNB/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "BTC/BUSD": [ + [0.0, 0.004], + [25000.0, 0.005], + [100000.0, 0.01], + [500000.0, 0.025], + [1000000.0, 0.05], + [2000000.0, 0.1], + [5000000.0, 0.125], + [10000000.0, 0.15], + [20000000.0, 0.25], + [30000000.0, 0.5] + ], + "BTC/USDT": [ + [0.0, 0.004], + [50000.0, 0.005], + [250000.0, 0.01], + [1000000.0, 0.025], + [5000000.0, 0.05], + [20000000.0, 0.1], + [50000000.0, 0.125], + [100000000.0, 0.15], + [200000000.0, 0.25], + [300000000.0, 0.5] + ], + "BTCBUSD_210129": [ + [0.0, 0.004], + [5000.0, 0.005], + [25000.0, 0.01], + [100000.0, 0.025], + [500000.0, 0.05], + [2000000.0, 0.1], + [5000000.0, 0.125], + [10000000.0, 0.15], + [20000000.0, 0.25] + ], + "BTCBUSD_210226": [ + [0.0, 0.004], + [5000.0, 0.005], + [25000.0, 0.01], + [100000.0, 0.025], + [500000.0, 0.05], + [2000000.0, 0.1], + [5000000.0, 0.125], + [10000000.0, 0.15], + [20000000.0, 0.25] + ], + "BTCDOM/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "BTCSTUSDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "BTCUSDT_210326": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "BTCUSDT_210625": [ + [0.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "BTCUSDT_210924": [ + [0.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25], + [20000000.0, 0.5] + ], + "BTS/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "BTT/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "BZRX/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "C98/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "CELR/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "CHR/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "CHZ/USDT": [ + [0.0, 0.012], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "COMP/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "COTI/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "CRV/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "CTK/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "CVC/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "DASH/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "DEFI/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "DENT/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "DGB/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "DODO/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "DOGE/BUSD": [ + [0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5] + ], + "DOGE/USDT": [ + [0.0, 0.01], + [50000.0, 0.025], + [150000.0, 0.05], + [250000.0, 0.1], + [500000.0, 0.125], + [750000.0, 0.25], + [1000000.0, 0.5] + ], + "DOT/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "DOTECOUSDT": [ + [0.0, 0.012], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "DYDX/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "EGLD/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ENJ/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "EOS/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "ETC/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "ETH/BUSD": [ + [0.0, 0.004], + [25000.0, 0.005], + [100000.0, 0.01], + [500000.0, 0.025], + [1000000.0, 0.05], + [2000000.0, 0.1], + [5000000.0, 0.125], + [10000000.0, 0.15], + [20000000.0, 0.25], + [30000000.0, 0.5] + ], + "ETH/USDT": [ + [0.0, 0.005], + [10000.0, 0.0065], + [100000.0, 0.01], + [500000.0, 0.02], + [1000000.0, 0.05], + [2000000.0, 0.1], + [5000000.0, 0.125], + [10000000.0, 0.15], + [20000000.0, 0.25] + ], + "ETHUSDT_210326": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "ETHUSDT_210625": [ + [0.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "ETHUSDT_210924": [ + [0.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25], + [20000000.0, 0.5] + ], + "FIL/USDT": [ + [0.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.1665], + [10000000.0, 0.25] + ], + "FLM/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "FTM/USDT": [ + [0.0, 0.01], + [50000.0, 0.025], + [150000.0, 0.05], + [250000.0, 0.1], + [500000.0, 0.125], + [750000.0, 0.25], + [1000000.0, 0.5] + ], + "FTT/BUSD": [ + [0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5] + ], + "GRT/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "GTC/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "HBAR/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "HNT/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "HOT/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ICP/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ICX/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "IOST/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "IOTA/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "IOTX/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "KAVA/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "KEEP/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "KNC/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "KSM/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "LENDUSDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "LINA/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "LINK/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "LIT/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "LRC/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "LTC/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "LUNA/USDT": [ + [0.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.1665], + [10000000.0, 0.25], + [15000000.0, 0.5] + ], + "MANA/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "MASK/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "MATIC/USDT": [ + [0.0, 0.01], + [50000.0, 0.025], + [150000.0, 0.05], + [250000.0, 0.1], + [500000.0, 0.125], + [750000.0, 0.25], + [1000000.0, 0.5] + ], + "MKR/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "MTL/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "NEAR/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "NEO/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "NKN/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "OCEAN/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "OGN/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "OMG/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ONE/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ONT/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "QTUM/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "RAY/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "REEF/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "REN/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "RLC/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "RSR/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "RUNE/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "RVN/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "SAND/USDT": [ + [0.0, 0.012], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "SC/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "SFP/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "SKL/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "SNX/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "SOL/BUSD": [ + [0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5] + ], + "SOL/USDT": [ + [0.0, 0.01], + [50000.0, 0.025], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.25], + [10000000.0, 0.5] + ], + "SRM/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "STMX/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "STORJ/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "SUSHI/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "SXP/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "THETA/USDT": [ + [0.0, 0.01], + [50000.0, 0.025], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.1665], + [10000000.0, 0.25] + ], + "TLM/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "TOMO/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "TRB/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "TRX/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "UNFI/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "UNI/USDT": [ + [0.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.1665], + [10000000.0, 0.25] + ], + "VET/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "WAVES/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "XEM/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "XLM/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "XMR/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "XRP/BUSD": [ + [0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5] + ], + "XRP/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "XTZ/USDT": [ + [0.0, 0.0065], + [10000.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.15], + [10000000.0, 0.25] + ], + "YFI/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "YFII/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ZEC/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ZEN/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ZIL/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "ZRX/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ] + } From 54ef36a4971d77467a10fa343857aec27a312e5a Mon Sep 17 00:00:00 2001 From: Sergey Khliustin Date: Fri, 17 Sep 2021 13:45:44 +0300 Subject: [PATCH 062/102] Updates after review to PerformanceFilter days param --- freqtrade/persistence/models.py | 34 ++++--------------- .../plugins/pairlist/PerformanceFilter.py | 5 +-- 2 files changed, 7 insertions(+), 32 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 91a26eba7..222be7169 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -832,43 +832,21 @@ class Trade(_DECL_BASE, LocalTrade): return total_open_stake_amount or 0 @staticmethod - def get_overall_performance() -> List[Dict[str, Any]]: + def get_overall_performance(days=None) -> List[Dict[str, Any]]: """ Returns List of dicts containing all Trades, including profit and trade count NOTE: Not supported in Backtesting. """ + filters = [Trade.is_open.is_(False)] + if days: + start_date = datetime.today() - timedelta(days) + filters.append((Trade.close_date >= start_date)) pair_rates = Trade.query.with_entities( Trade.pair, func.sum(Trade.close_profit).label('profit_sum'), func.sum(Trade.close_profit_abs).label('profit_sum_abs'), func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False))\ - .group_by(Trade.pair) \ - .order_by(desc('profit_sum_abs')) \ - .all() - return [ - { - 'pair': pair, - 'profit': profit, - 'profit_abs': profit_abs, - 'count': count - } - for pair, profit, profit_abs, count in pair_rates - ] - - @staticmethod - def get_performance(days: int) -> List[Dict[str, Any]]: - """ - Returns List of dicts containing all Trades, including profit and trade count - NOTE: Not supported in Backtesting. - """ - start_date = datetime.today() - timedelta(days) - pair_rates = Trade.query.with_entities( - Trade.pair, - func.sum(Trade.close_profit).label('profit_sum'), - func.sum(Trade.close_profit_abs).label('profit_sum_abs'), - func.count(Trade.pair).label('count') - ).filter(Trade.is_open.is_(False) & (Trade.close_date >= start_date))\ + ).filter(filters)\ .group_by(Trade.pair) \ .order_by(desc('profit_sum_abs')) \ .all() diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index 4d530fe88..920b884a0 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -48,10 +48,7 @@ class PerformanceFilter(IPairList): """ # Get the trading performance for pairs from database try: - if self._days > 0: - performance = pd.DataFrame(Trade.get_performance(self._days)) - else: - performance = pd.DataFrame(Trade.get_overall_performance()) + performance = pd.DataFrame(Trade.get_overall_performance(self._days)) except AttributeError: # Performancefilter does not work in backtesting. self.log_once("PerformanceFilter is not available in this mode.", logger.warning) From 982deeedf04cb1be9c5ae7b1ec3598a9d09a4b9a Mon Sep 17 00:00:00 2001 From: sergeykhliustin <51409210+sergeykhliustin@users.noreply.github.com> Date: Fri, 17 Sep 2021 18:23:13 +0300 Subject: [PATCH 063/102] Update freqtrade/persistence/models.py Co-authored-by: Matthias --- freqtrade/persistence/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 222be7169..a2e30d58c 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -846,7 +846,7 @@ class Trade(_DECL_BASE, LocalTrade): func.sum(Trade.close_profit).label('profit_sum'), func.sum(Trade.close_profit_abs).label('profit_sum_abs'), func.count(Trade.pair).label('count') - ).filter(filters)\ + ).filter(*filters)\ .group_by(Trade.pair) \ .order_by(desc('profit_sum_abs')) \ .all() From 3a98fb72a491536cef7b601573c3ca45f64e8fc5 Mon Sep 17 00:00:00 2001 From: raphael Date: Fri, 17 Sep 2021 11:42:33 -0400 Subject: [PATCH 064/102] Update prepare_trials_columns() return type Was returning str, updated to pd.DataFrame --- freqtrade/optimize/hyperopt_tools.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index b2e024f65..2fcab0b0f 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -7,6 +7,7 @@ from pathlib import Path from typing import Any, Dict, Iterator, List, Optional, Tuple import numpy as np +import pandas as pd import rapidjson import tabulate from colorama import Fore, Style @@ -298,7 +299,7 @@ class HyperoptTools(): f"Objective: {results['loss']:.5f}") @staticmethod - def prepare_trials_columns(trials, legacy_mode: bool, has_drawdown: bool) -> str: + def prepare_trials_columns(trials, legacy_mode: bool, has_drawdown: bool) -> pd.DataFrame: trials['Best'] = '' From 5fc993231a2d138e9ff994f1d4548888551dd73c Mon Sep 17 00:00:00 2001 From: raphael Date: Fri, 17 Sep 2021 11:51:55 -0400 Subject: [PATCH 065/102] Update HyperoptTools.export_csv_file usage --- freqtrade/commands/hyperopt_commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/commands/hyperopt_commands.py b/freqtrade/commands/hyperopt_commands.py index 089529d15..614c4b3f5 100755 --- a/freqtrade/commands/hyperopt_commands.py +++ b/freqtrade/commands/hyperopt_commands.py @@ -53,7 +53,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None: if epochs and export_csv: HyperoptTools.export_csv_file( - config, epochs, total_epochs, not config.get('hyperopt_list_best', False), export_csv + config, epochs, export_csv ) From 124e97f3b9a7006cfcf2337255f10c8bcc3e0038 Mon Sep 17 00:00:00 2001 From: raphael Date: Fri, 17 Sep 2021 11:57:36 -0400 Subject: [PATCH 066/102] Remove ununsed variables from export_csv_file --- freqtrade/optimize/hyperopt_tools.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index b2e024f65..6a5c31d56 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -435,8 +435,7 @@ class HyperoptTools(): return table @staticmethod - def export_csv_file(config: dict, results: list, total_epochs: int, highlight_best: bool, - csv_file: str) -> None: + def export_csv_file(config: dict, results: list, csv_file: str) -> None: """ Log result to csv-file """ From fb6beb90e87e75d2997651324daaf0c5954e661a Mon Sep 17 00:00:00 2001 From: Ottavio Miele Date: Fri, 17 Sep 2021 18:03:54 +0200 Subject: [PATCH 067/102] Include Raspberry Pi armv6 (0, 0W,...) --- setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index 217500569..aee7c80b5 100755 --- a/setup.sh +++ b/setup.sh @@ -62,7 +62,7 @@ function updateenv() { then REQUIREMENTS_PLOT="-r requirements-plot.txt" fi - if [ "${SYS_ARCH}" == "armv7l" ]; then + if [ "${SYS_ARCH}" == "armv7l" ] || [ "${SYS_ARCH}" == "armv6l" ]; then echo "Detected Raspberry, installing cython, skipping hyperopt installation." ${PYTHON} -m pip install --upgrade cython else From 9525a5b96cf779dfa80abdae355e2d5c9382f505 Mon Sep 17 00:00:00 2001 From: raphael Date: Fri, 17 Sep 2021 14:10:37 -0400 Subject: [PATCH 068/102] Add type to "trials" parameter --- freqtrade/optimize/hyperopt_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 2fcab0b0f..1874b2949 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -299,7 +299,7 @@ class HyperoptTools(): f"Objective: {results['loss']:.5f}") @staticmethod - def prepare_trials_columns(trials, legacy_mode: bool, has_drawdown: bool) -> pd.DataFrame: + def prepare_trials_columns(trials: pd.DataFrame, legacy_mode: bool, has_drawdown: bool) -> pd.DataFrame: trials['Best'] = '' From e715f2a253afe2ef4332166c0357595976dd7dfc Mon Sep 17 00:00:00 2001 From: raphael Date: Fri, 17 Sep 2021 14:23:26 -0400 Subject: [PATCH 069/102] Update formatting Line 302 was too long --- freqtrade/optimize/hyperopt_tools.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index 1874b2949..ac92ad682 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -299,7 +299,8 @@ class HyperoptTools(): f"Objective: {results['loss']:.5f}") @staticmethod - def prepare_trials_columns(trials: pd.DataFrame, legacy_mode: bool, has_drawdown: bool) -> pd.DataFrame: + def prepare_trials_columns(trials: pd.DataFrame, legacy_mode: bool, + has_drawdown: bool) -> pd.DataFrame: trials['Best'] = '' From 4b2c1a9b8e0e487a60454125e8916026b8443c5c Mon Sep 17 00:00:00 2001 From: raphael Date: Fri, 17 Sep 2021 14:39:15 -0400 Subject: [PATCH 070/102] Remove trailing whitespace --- freqtrade/optimize/hyperopt_tools.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/optimize/hyperopt_tools.py b/freqtrade/optimize/hyperopt_tools.py index ac92ad682..7a4ce5f23 100755 --- a/freqtrade/optimize/hyperopt_tools.py +++ b/freqtrade/optimize/hyperopt_tools.py @@ -299,9 +299,8 @@ class HyperoptTools(): f"Objective: {results['loss']:.5f}") @staticmethod - def prepare_trials_columns(trials: pd.DataFrame, legacy_mode: bool, + def prepare_trials_columns(trials: pd.DataFrame, legacy_mode: bool, has_drawdown: bool) -> pd.DataFrame: - trials['Best'] = '' if 'results_metrics.winsdrawslosses' not in trials.columns: From d7395e873be5560932752e00200c70fc2cd169c3 Mon Sep 17 00:00:00 2001 From: Sergey Khliustin Date: Fri, 17 Sep 2021 22:05:57 +0300 Subject: [PATCH 071/102] Removed unused OperationalException --- freqtrade/plugins/pairlist/PerformanceFilter.py | 1 - 1 file changed, 1 deletion(-) diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index 920b884a0..5750b721c 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -7,7 +7,6 @@ from typing import Dict, List, Any import pandas as pd from freqtrade.persistence import Trade -from freqtrade.exceptions import OperationalException from freqtrade.plugins.pairlist.IPairList import IPairList From 2c21bbfa0c6c914ec77610be1b118eb9defbdc45 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Fri, 17 Sep 2021 14:16:52 -0600 Subject: [PATCH 072/102] Fixed create order margin call count tests and made _ccxt_config a computed property --- freqtrade/exchange/bibox.py | 5 +++- freqtrade/exchange/binance.py | 20 ++++++++++++- freqtrade/exchange/exchange.py | 53 +++++++++++++++------------------ tests/conftest.py | 26 +++++++++++++--- tests/exchange/test_binance.py | 12 ++++++++ tests/exchange/test_exchange.py | 27 +++++++++-------- 6 files changed, 96 insertions(+), 47 deletions(-) diff --git a/freqtrade/exchange/bibox.py b/freqtrade/exchange/bibox.py index f0c2dd00b..074dd2b10 100644 --- a/freqtrade/exchange/bibox.py +++ b/freqtrade/exchange/bibox.py @@ -20,4 +20,7 @@ class Bibox(Exchange): # fetchCurrencies API point requires authentication for Bibox, # so switch it off for Freqtrade load_markets() - _ccxt_config: Dict = {"has": {"fetchCurrencies": False}} + @property + def _ccxt_config(self) -> Dict: + # Parameters to add directly to ccxt sync/async initialization. + return {"has": {"fetchCurrencies": False}} diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 572fa2141..60a1b8019 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -33,9 +33,27 @@ class Binance(Exchange): # TradingMode.SPOT always supported and not required in this list # (TradingMode.MARGIN, Collateral.CROSS), # TODO-lev: Uncomment once supported # (TradingMode.FUTURES, Collateral.CROSS), # TODO-lev: Uncomment once supported - (TradingMode.FUTURES, Collateral.ISOLATED) + # (TradingMode.FUTURES, Collateral.ISOLATED) # TODO-lev: Uncomment once supported ] + @property + def _ccxt_config(self) -> Dict: + # Parameters to add directly to ccxt sync/async initialization. + if self.trading_mode == TradingMode.MARGIN: + return { + "options": { + "defaultType": "margin" + } + } + elif self.trading_mode == TradingMode.FUTURES: + return { + "options": { + "defaultType": "future" + } + } + else: + return {} + def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: """ Verify stop_loss against stoploss-order value (limit or price) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 8bbc88235..4021e7d02 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -49,9 +49,6 @@ class Exchange: _config: Dict = {} - # Parameters to add directly to ccxt sync/async initialization. - _ccxt_config: Dict = {} - # Parameters to add directly to buy/sell calls (like agreeing to trading agreement) _params: Dict = {} @@ -131,21 +128,6 @@ class Exchange: self._trades_pagination = self._ft_has['trades_pagination'] self._trades_pagination_arg = self._ft_has['trades_pagination_arg'] - # Initialize ccxt objects - ccxt_config = self._ccxt_config.copy() - ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_config) - ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_sync_config', {}), ccxt_config) - - self._api = self._init_ccxt(exchange_config, ccxt_kwargs=ccxt_config) - - ccxt_async_config = self._ccxt_config.copy() - ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), - ccxt_async_config) - ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_async_config', {}), - ccxt_async_config) - self._api_async = self._init_ccxt( - exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config) - self.trading_mode: TradingMode = ( TradingMode(config.get('trading_mode')) if config.get('trading_mode') @@ -157,6 +139,21 @@ class Exchange: else None ) + # Initialize ccxt objects + ccxt_config = self._ccxt_config + ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), ccxt_config) + ccxt_config = deep_merge_dicts(exchange_config.get('ccxt_sync_config', {}), ccxt_config) + + self._api = self._init_ccxt(exchange_config, ccxt_kwargs=ccxt_config) + + ccxt_async_config = self._ccxt_config + ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_config', {}), + ccxt_async_config) + ccxt_async_config = deep_merge_dicts(exchange_config.get('ccxt_async_config', {}), + ccxt_async_config) + self._api_async = self._init_ccxt( + exchange_config, ccxt_async, ccxt_kwargs=ccxt_async_config) + if self.trading_mode != TradingMode.SPOT: self.fill_leverage_brackets() @@ -210,7 +207,7 @@ class Exchange: 'secret': exchange_config.get('secret'), 'password': exchange_config.get('password'), 'uid': exchange_config.get('uid', ''), - 'options': exchange_config.get('options', {}) + # 'options': exchange_config.get('options', {}) } if ccxt_kwargs: logger.info('Applying additional ccxt config: %s', ccxt_kwargs) @@ -231,6 +228,11 @@ class Exchange: return api + @property + def _ccxt_config(self) -> Dict: + # Parameters to add directly to ccxt sync/async initialization. + return {} + @property def name(self) -> str: """exchange Name (from ccxt)""" @@ -258,13 +260,6 @@ class Exchange: """exchange ccxt precisionMode""" return self._api.precisionMode - @property - def running_live_mode(self) -> bool: - return ( - self._config['runmode'].value not in ('backtest', 'hyperopt') and - not self._config['dry_run'] - ) - def _log_exchange_response(self, endpoint, response) -> None: """ Log exchange responses """ if self.log_responses: @@ -624,12 +619,12 @@ class Exchange: # The value returned should satisfy both limits: for amount (base currency) and # for cost (quote, stake currency), so max() is used here. # See also #2575 at github. - return self._divide_stake_amount_by_leverage( + return self._get_stake_amount_considering_leverage( max(min_stake_amounts) * amount_reserve_percent, leverage or 1.0 ) - def _divide_stake_amount_by_leverage(self, stake_amount: float, leverage: float): + def _get_stake_amount_considering_leverage(self, stake_amount: float, leverage: float): """ Takes the minimum stake amount for a pair with no leverage and returns the minimum stake amount when leverage is considered @@ -1603,7 +1598,7 @@ class Exchange: def fill_leverage_brackets(self): """ - #TODO-lev: Should maybe be renamed, leverage_brackets might not be accurate for kraken + # TODO-lev: Should maybe be renamed, leverage_brackets might not be accurate for kraken Assigns property _leverage_brackets to a dictionary of information about the leverage allowed on each pair """ diff --git a/tests/conftest.py b/tests/conftest.py index 3de299752..d2f24fa69 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -18,7 +18,7 @@ from freqtrade import constants from freqtrade.commands import Arguments from freqtrade.data.converter import ohlcv_to_dataframe from freqtrade.edge import Edge, PairInfo -from freqtrade.enums import RunMode +from freqtrade.enums import Collateral, RunMode, TradingMode from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import LocalTrade, Trade, init_db @@ -81,7 +81,13 @@ def patched_configuration_load_config_file(mocker, config) -> None: ) -def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> None: +def patch_exchange( + mocker, + api_mock=None, + id='binance', + mock_markets=True, + mock_supported_modes=True +) -> None: mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) @@ -90,10 +96,22 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value=id)) mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value=id.title())) mocker.patch('freqtrade.exchange.Exchange.precisionMode', PropertyMock(return_value=2)) + if mock_markets: mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=get_markets())) + if mock_supported_modes: + mocker.patch( + f'freqtrade.exchange.{id.capitalize()}._supported_trading_mode_collateral_pairs', + PropertyMock(return_value=[ + (TradingMode.MARGIN, Collateral.CROSS), + (TradingMode.MARGIN, Collateral.ISOLATED), + (TradingMode.FUTURES, Collateral.CROSS), + (TradingMode.FUTURES, Collateral.ISOLATED) + ]) + ) + if api_mock: mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) else: @@ -101,8 +119,8 @@ def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> No def get_patched_exchange(mocker, config, api_mock=None, id='binance', - mock_markets=True) -> Exchange: - patch_exchange(mocker, api_mock, id, mock_markets) + mock_markets=True, mock_supported_modes=True) -> Exchange: + patch_exchange(mocker, api_mock, id, mock_markets, mock_supported_modes) config['exchange']['name'] = id try: exchange = ExchangeResolver.load_exchange(id, config) diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index 4999e94af..cbbace1db 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -336,3 +336,15 @@ async def test__async_get_historic_ohlcv_binance(default_conf, mocker, caplog): assert exchange._api_async.fetch_ohlcv.call_count == 2 assert res == ohlcv assert log_has_re(r"Candle-data for ETH/BTC available starting with .*", caplog) + + +@pytest.mark.parametrize("trading_mode,collateral,config", [ + ("", "", {}), + ("margin", "cross", {"options": {"defaultType": "margin"}}), + ("futures", "isolated", {"options": {"defaultType": "future"}}), +]) +def test__ccxt_config(default_conf, mocker, trading_mode, collateral, config): + default_conf['trading_mode'] = trading_mode + default_conf['collateral'] = collateral + exchange = get_patched_exchange(mocker, default_conf, id="binance") + assert exchange._ccxt_config == config diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index ce09e31e7..8b16a9f12 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -132,10 +132,9 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog): assert log_has("Applying additional ccxt config: {'TestKWARG': 11, 'TestKWARG44': 11}", caplog) assert ex._api.headers == {'hello': 'world'} + assert ex._ccxt_config == {} Exchange._headers = {} - # TODO-lev: Test with options - def test_destroy(default_conf, mocker, caplog): caplog.set_level(logging.DEBUG) @@ -1116,6 +1115,8 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + exchange._set_leverage = MagicMock() + exchange.set_margin_mode = MagicMock() order = exchange.create_order( pair='ETH/BTC', @@ -1134,10 +1135,10 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, assert api_mock.create_order.call_args[0][2] == side assert api_mock.create_order.call_args[0][3] == 1 assert api_mock.create_order.call_args[0][4] is rate + assert exchange._set_leverage.call_count == 0 + assert exchange.set_margin_mode.call_count == 0 - assert api_mock._set_leverage.call_count == 0 if side == "buy" else 1 - assert api_mock.set_margin_mode.call_count == 0 if side == "buy" else 1 - + exchange.trading_mode = TradingMode.FUTURES order = exchange.create_order( pair='ETH/BTC', ordertype=ordertype, @@ -1147,8 +1148,8 @@ def test_create_order(default_conf, mocker, side, ordertype, rate, marketprice, leverage=3.0 ) - assert api_mock._set_leverage.call_count == 1 - assert api_mock.set_margin_mode.call_count == 1 + assert exchange._set_leverage.call_count == 1 + assert exchange.set_margin_mode.call_count == 1 def test_buy_dry_run(default_conf, mocker): @@ -3042,7 +3043,6 @@ def test_calculate_fee_rate(mocker, default_conf, order, expected) -> None: (3, 5, 5), (4, 5, 2), (5, 5, 1), - ]) def test_calculate_backoff(retrycount, max_retries, expected): assert calculate_backoff(retrycount, max_retries) == expected @@ -3054,7 +3054,7 @@ def test_calculate_backoff(retrycount, max_retries, expected): (20.0, 5.0, 4.0), (100.0, 100.0, 1.0) ]) -def test_divide_stake_amount_by_leverage( +def test_get_stake_amount_considering_leverage( exchange, stake_amount, leverage, @@ -3063,7 +3063,8 @@ def test_divide_stake_amount_by_leverage( default_conf ): exchange = get_patched_exchange(mocker, default_conf, id=exchange) - assert exchange._divide_stake_amount_by_leverage(stake_amount, leverage) == min_stake_with_lev + assert exchange._get_stake_amount_considering_leverage( + stake_amount, leverage) == min_stake_with_lev @pytest.mark.parametrize("exchange_name,trading_mode", [ @@ -3132,6 +3133,7 @@ def test_set_margin_mode(mocker, default_conf, collateral): # TODO-lev: Remove once implemented ("binance", TradingMode.MARGIN, Collateral.CROSS, True), ("binance", TradingMode.FUTURES, Collateral.CROSS, True), + ("binance", TradingMode.FUTURES, Collateral.ISOLATED, True), ("kraken", TradingMode.MARGIN, Collateral.CROSS, True), ("kraken", TradingMode.FUTURES, Collateral.CROSS, True), ("ftx", TradingMode.MARGIN, Collateral.CROSS, True), @@ -3140,7 +3142,7 @@ def test_set_margin_mode(mocker, default_conf, collateral): # TODO-lev: Uncomment once implemented # ("binance", TradingMode.MARGIN, Collateral.CROSS, False), # ("binance", TradingMode.FUTURES, Collateral.CROSS, False), - ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False), + # ("binance", TradingMode.FUTURES, Collateral.ISOLATED, False), # ("kraken", TradingMode.MARGIN, Collateral.CROSS, False), # ("kraken", TradingMode.FUTURES, Collateral.CROSS, False), # ("ftx", TradingMode.MARGIN, Collateral.CROSS, False), @@ -3154,7 +3156,8 @@ def test_validate_trading_mode_and_collateral( collateral, exception_thrown ): - exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) + exchange = get_patched_exchange( + mocker, default_conf, id=exchange_name, mock_supported_modes=False) if (exception_thrown): with pytest.raises(OperationalException): exchange.validate_trading_mode_and_collateral(trading_mode, collateral) From 12c12d42df5e0f838aad1ef971ca8530345d2d00 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 08:26:48 +0200 Subject: [PATCH 073/102] Add documentation for days parameter in PerformanceFilter --- docs/includes/pairlists.md | 14 ++++++++++++++ freqtrade/persistence/models.py | 4 ++-- freqtrade/plugins/pairlist/PerformanceFilter.py | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 69e12d5dc..71165f93b 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -165,6 +165,7 @@ Example to remove the first 10 pairs from the pairlist: ```json "pairlists": [ + // ... { "method": "OffsetFilter", "offset": 10 @@ -190,6 +191,19 @@ Sorts pairs by past trade performance, as follows: Trade count is used as a tie breaker. +You can use the `days` parameter to only consider performance of the past X days. +Not defining this parameter (or setting it to 0) will use all-time performance. + +```json +"pairlists": [ + // ... + { + "method": "PerformanceFilter", + "days": 10 + } +], +``` + !!! Note `PerformanceFilter` does not support backtesting mode. diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index a2e30d58c..f5f4e3a26 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -2,7 +2,7 @@ This module contains the class to persist trades into SQLite """ import logging -from datetime import datetime, timezone, timedelta +from datetime import datetime, timedelta, timezone from decimal import Decimal from typing import Any, Dict, List, Optional @@ -840,7 +840,7 @@ class Trade(_DECL_BASE, LocalTrade): filters = [Trade.is_open.is_(False)] if days: start_date = datetime.today() - timedelta(days) - filters.append((Trade.close_date >= start_date)) + filters.append(Trade.close_date >= start_date) pair_rates = Trade.query.with_entities( Trade.pair, func.sum(Trade.close_profit).label('profit_sum'), diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index 5750b721c..ee443b0fe 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -2,7 +2,7 @@ Performance pair list filter """ import logging -from typing import Dict, List, Any +from typing import Any, Dict, List import pandas as pd From 564e0b9a1ac89c19f2a9c1e94c04cafd4fd96afe Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 08:36:06 +0200 Subject: [PATCH 074/102] Switch performanceFilter to use Minutes lookback resolution closes #5060 --- docs/includes/pairlists.md | 4 ++-- freqtrade/persistence/models.py | 6 +++--- freqtrade/plugins/pairlist/PerformanceFilter.py | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 71165f93b..b612a4ddf 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -191,7 +191,7 @@ Sorts pairs by past trade performance, as follows: Trade count is used as a tie breaker. -You can use the `days` parameter to only consider performance of the past X days. +You can use the `minutes` parameter to only consider performance of the past X minutes (rolling window). Not defining this parameter (or setting it to 0) will use all-time performance. ```json @@ -199,7 +199,7 @@ Not defining this parameter (or setting it to 0) will use all-time performance. // ... { "method": "PerformanceFilter", - "days": 10 + "minutes": 1440 // rolling 24h } ], ``` diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index f5f4e3a26..bc5ef961a 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -832,14 +832,14 @@ class Trade(_DECL_BASE, LocalTrade): return total_open_stake_amount or 0 @staticmethod - def get_overall_performance(days=None) -> List[Dict[str, Any]]: + def get_overall_performance(minutes=None) -> List[Dict[str, Any]]: """ Returns List of dicts containing all Trades, including profit and trade count NOTE: Not supported in Backtesting. """ filters = [Trade.is_open.is_(False)] - if days: - start_date = datetime.today() - timedelta(days) + if minutes: + start_date = datetime.now(timezone.utc) - timedelta(minutes=minutes) filters.append(Trade.close_date >= start_date) pair_rates = Trade.query.with_entities( Trade.pair, diff --git a/freqtrade/plugins/pairlist/PerformanceFilter.py b/freqtrade/plugins/pairlist/PerformanceFilter.py index ee443b0fe..301ee57ab 100644 --- a/freqtrade/plugins/pairlist/PerformanceFilter.py +++ b/freqtrade/plugins/pairlist/PerformanceFilter.py @@ -20,7 +20,7 @@ class PerformanceFilter(IPairList): pairlist_pos: int) -> None: super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos) - self._days = pairlistconfig.get('days', 0) + self._minutes = pairlistconfig.get('minutes', 0) @property def needstickers(self) -> bool: @@ -47,7 +47,7 @@ class PerformanceFilter(IPairList): """ # Get the trading performance for pairs from database try: - performance = pd.DataFrame(Trade.get_overall_performance(self._days)) + performance = pd.DataFrame(Trade.get_overall_performance(self._minutes)) except AttributeError: # Performancefilter does not work in backtesting. self.log_once("PerformanceFilter is not available in this mode.", logger.warning) From 56fb25c5e521415c9880e47c4938bfe357550bec Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 09:10:37 +0200 Subject: [PATCH 075/102] Add test for PerformanceFilter lookback --- tests/plugins/test_pairlist.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index 34770c03d..1ce8d172c 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -12,7 +12,8 @@ from freqtrade.persistence import Trade from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist from freqtrade.plugins.pairlistmanager import PairListManager from freqtrade.resolvers import PairListResolver -from tests.conftest import get_patched_exchange, get_patched_freqtradebot, log_has, log_has_re +from tests.conftest import (create_mock_trades, get_patched_exchange, get_patched_freqtradebot, + log_has, log_has_re) @pytest.fixture(scope="function") @@ -663,6 +664,31 @@ def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None: assert log_has("PerformanceFilter is not available in this mode.", caplog) +@pytest.mark.usefixtures("init_persistence") +def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee) -> None: + whitelist_conf['exchange']['pair_whitelist'].append('XRP/BTC') + whitelist_conf['pairlists'] = [ + {"method": "StaticPairList"}, + {"method": "PerformanceFilter", "minutes": 60} + ] + mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) + exchange = get_patched_exchange(mocker, whitelist_conf) + pm = PairListManager(exchange, whitelist_conf) + pm.refresh_pairlist() + + assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC'] + + with time_machine.travel("2021-09-01 05:00:00 +00:00") as t: + create_mock_trades(fee) + pm.refresh_pairlist() + assert pm.whitelist == ['XRP/BTC', 'ETH/BTC', 'TKN/BTC'] + + # Move to "outside" of lookback window, so original sorting is restored. + t.move_to("2021-09-01 07:00:00 +00:00") + pm.refresh_pairlist() + assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC'] + + def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None: default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}] From d84ef34740a65df4922a13b8e8e523167637ae2e Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 17 Jul 2021 18:09:08 +0300 Subject: [PATCH 076/102] A helper to calculate stoploss value from absolute price. --- docs/strategy-advanced.md | 6 ++++ docs/strategy-customization.md | 41 +++++++++++++++++++++++++++ freqtrade/strategy/__init__.py | 3 +- freqtrade/strategy/strategy_helper.py | 11 +++++++ 4 files changed, 60 insertions(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 4409af6ea..2b9517f3b 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -288,6 +288,12 @@ Stoploss values returned from `custom_stoploss()` always specify a percentage re The helper function [`stoploss_from_open()`](strategy-customization.md#stoploss_from_open) can be used to convert from an open price relative stop, to a current price relative stop which can be returned from `custom_stoploss()`. +### Calculating stoploss percentage from absolute price + +Stoploss values returned from `custom_stoploss()` always specify a percentage relative to `current_rate`. In order to set a stoploss at specified absolute price level, we need to use `stop_rate` to calculate what percentage relative to the `current_rate` will give you the same result as if the percentage was specified from the open price. + +The helper function [`stoploss_from_absolute()`](strategy-customization.md#stoploss_from_absolute) can be used to convert from an absolute price, to a current price relative stop which can be returned from `custom_stoploss()`. + #### Stepped stoploss Instead of continuously trailing behind the current price, this example sets fixed stoploss price levels based on the current profit. diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index cfea60d22..1f8116deb 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -639,6 +639,47 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation. +!!! Note + Providing invalid input to `stoploss_from_open()` may produce "CustomStoploss function did not return valid stoploss" warnings. + This may happen if `current_profit` parameter is below specified `open_relative_stop`. Such situations may arise when closing trade + is blocked by `confirm_trade_exit()` method. Warnings can be solved by never blocking stop loss sells by checking `sell_reason` in + `confirm_trade_exit()`, or by using `return stoploss_from_open(...) or 1` idiom, which will request to not change stop loss when + `current_profit < open_relative_stop`. + +### *stoploss_from_absolute()* + +In some situations it may be confusing to deal with stops relative to current rate. Instead, you may define a stoploss level using an absolute price. + +??? Example "Returning a stoploss using absolute price from the custom stoploss function" + + Say the open price was $100, and `current_price` is $121 (`current_profit` will be `0.21`). + + If we want a stop price at $107 price we can call `stoploss_from_absolute(107, current_rate)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100. + + ``` python + + from datetime import datetime + from freqtrade.persistence import Trade + from freqtrade.strategy import IStrategy, stoploss_from_open + + class AwesomeStrategy(IStrategy): + + # ... populate_* methods + + use_custom_stoploss = True + + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, + current_rate: float, current_profit: float, **kwargs) -> float: + + # once the profit has risen above 10%, keep the stoploss at 7% above the open price + if current_profit > 0.10: + return stoploss_from_absolute(trade.open_rate * 1.07, current_rate) + + return 1 + + ``` + + Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation. ## Additional data (Wallets) diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index be655fc33..703cdabc1 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -4,4 +4,5 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timefr from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter, IntParameter, RealParameter) from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.strategy_helper import merge_informative_pair, stoploss_from_open +from freqtrade.strategy.strategy_helper import (merge_informative_pair, + stoploss_from_absolute, stoploss_from_open) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index e089ebf31..32f7a9886 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -83,3 +83,14 @@ def stoploss_from_open(open_relative_stop: float, current_profit: float) -> floa # negative stoploss values indicate the requested stop price is higher than the current price return max(stoploss, 0.0) + + +def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float: + """ + Given current price and desired stop price, return a stop loss value that is relative to current + price. + :param stop_rate: Stop loss price. + :param current_rate: Current asset price. + :return: Positive stop loss value relative to current price + """ + return 1 - (stop_rate / current_rate) From 1fdb656334208f57916fe4539ab83e9363d7b984 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 17 Jul 2021 19:19:49 +0300 Subject: [PATCH 077/102] Add a decorator which can be used to declare populate_indicators() functions for informative pairs. --- docs/strategy-customization.md | 84 +++++++++- freqtrade/edge/edge_positioning.py | 2 +- freqtrade/freqtradebot.py | 2 +- freqtrade/strategy/__init__.py | 2 +- freqtrade/strategy/interface.py | 53 ++++++- freqtrade/strategy/strategy_helper.py | 148 +++++++++++++++++- tests/rpc/test_rpc_apiserver.py | 1 + .../strats/informative_decorator_strategy.py | 75 +++++++++ tests/strategy/test_interface.py | 2 +- tests/strategy/test_strategy_helpers.py | 55 +++++++ tests/strategy/test_strategy_loading.py | 6 +- 11 files changed, 414 insertions(+), 16 deletions(-) create mode 100644 tests/strategy/strats/informative_decorator_strategy.py diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 1f8116deb..526c111c5 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -679,7 +679,89 @@ In some situations it may be confusing to deal with stops relative to current ra ``` - Full examples can be found in the [Custom stoploss](strategy-advanced.md#custom-stoploss) section of the Documentation. +### *@informative()* + +In most common case it is possible to easily define informative pairs by using a decorator. All decorated `populate_indicators_*` methods run in isolation, +not having access to data from other informative pairs, in the end all informative dataframes are merged and passed to main `populate_indicators()` method. +When hyperopting, please follow instructions of [optimizing an indicator parameter](hyperopt.md#optimizing-an-indicator-parameter). + +??? Example "Fast and easy way to define informative pairs" + + Most of the time we do not need power and flexibility offered by `merge_informative_pair()`, therefore we can use a decorator to quickly define informative pairs. + + ``` python + + from datetime import datetime + from freqtrade.persistence import Trade + from freqtrade.strategy import IStrategy, informative + + class AwesomeStrategy(IStrategy): + + # This method is not required. + # def informative_pairs(self): ... + + # Define informative upper timeframe for each pair. Decorators can be stacked on same + # method. Available in populate_indicators as 'rsi_30m' and 'rsi_1h'. + @informative('30m') + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + # Define BTC/STAKE informative pair. Available in populate_indicators and other methods as + # 'btc_rsi_1h'. Current stake currency should be specified as {stake} format variable + # instead of hardcoding actual stake currency. Available in populate_indicators and other + # methods as 'btc_rsi_1h'. + @informative('1h', 'BTC/{stake}') + def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + # Define BTC/ETH informative pair. You must specify quote currency if it is different from + # stake currency. Available in populate_indicators and other methods as 'eth_btc_rsi_1h'. + @informative('1h', 'ETH/BTC') + def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + # Define BTC/STAKE informative pair. A custom formatter may be specified for formatting + # column names. Format string supports these format variables: + # * {asset} - full name of the asset, for example 'BTC/USDT'. + # * {base} - base currency in lower case, for example 'eth'. + # * {BASE} - same as {base}, except in upper case. + # * {quote} - quote currency in lower case, for example 'usdt'. + # * {QUOTE} - same as {quote}, except in upper case. + # * {column} - name of dataframe column. + # * {timeframe} - timeframe of informative dataframe. + # A callable `fmt(**kwargs) -> str` may be specified, to implement custom formatting. + # Available in populate_indicators and other methods as 'rsi_upper'. + @informative('1h', 'BTC/{stake}', '{name}') + def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # Strategy timeframe indicators for current pair. + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + # Informative pairs are available in this method. + dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h'] + return dataframe + + ``` + + See docstring of `@informative()` decorator for more information. + +!!! Note + Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs + manually as described [in the DataProvider section](#complete-data-provider-sample). + +!!! Warning + Methods tagged with `@informative()` decorator must always have unique names! Re-using same name (for example when copy-pasting already defined informative method) + will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators + created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique! + +!!! Warning + When using a legacy hyperopt implementation informative pairs defined with a decorator will not be executed. Please update your strategy if necessary. ## Additional data (Wallets) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index f12b1b37d..1950f0d08 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -119,7 +119,7 @@ class Edge: ) # Download informative pairs too res = defaultdict(list) - for p, t in self.strategy.informative_pairs(): + for p, t in self.strategy.gather_informative_pairs(): res[t].append(p) for timeframe, inf_pairs in res.items(): timerange_startup = deepcopy(self._timerange) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 7f668273c..bdc438c9a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -160,7 +160,7 @@ class FreqtradeBot(LoggingMixin): # Refreshing candles self.dataprovider.refresh(self.pairlists.create_pair_list(self.active_pair_whitelist), - self.strategy.informative_pairs()) + self.strategy.gather_informative_pairs()) strategy_safe_wrapper(self.strategy.bot_loop_start, supress_error=True)() diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index 703cdabc1..a7de34916 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -4,5 +4,5 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timefr from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter, IntParameter, RealParameter) from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.strategy_helper import (merge_informative_pair, +from freqtrade.strategy.strategy_helper import (informative, merge_informative_pair, stoploss_from_absolute, stoploss_from_open) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 00ad3faf0..8e8b8b404 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -6,7 +6,7 @@ import logging import warnings from abc import ABC, abstractmethod from datetime import datetime, timedelta, timezone -from typing import Dict, List, Optional, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Tuple, Union import arrow from pandas import DataFrame @@ -19,6 +19,8 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.persistence import PairLocks, Trade from freqtrade.strategy.hyper import HyperStrategyMixin +from freqtrade.strategy.strategy_helper import (InformativeData, _create_and_merge_informative_pair, + _format_pair_name) from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -134,6 +136,39 @@ class IStrategy(ABC, HyperStrategyMixin): self._last_candle_seen_per_pair: Dict[str, datetime] = {} super().__init__(config) + # Gather informative pairs from @informative-decorated methods. + self._ft_informative: Dict[ + Tuple[str, str], Tuple[InformativeData, + Callable[[Any, DataFrame, dict], DataFrame]]] = {} + for attr_name in dir(self.__class__): + cls_method = getattr(self.__class__, attr_name) + if not callable(cls_method): + continue + ft_informative = getattr(cls_method, '_ft_informative', []) + if not isinstance(ft_informative, list): + # Type check is required because mocker would return a mock object that evaluates to + # True, confusing this code. + continue + for informative_data in ft_informative: + asset = informative_data.asset + timeframe = informative_data.timeframe + if asset: + pair = _format_pair_name(self.config, asset) + if (pair, timeframe) in self._ft_informative: + raise OperationalException(f'Informative pair {pair} {timeframe} can not ' + f'be defined more than once!') + self._ft_informative[(pair, timeframe)] = (informative_data, cls_method) + elif self.dp is not None: + for pair in self.dp.current_whitelist(): + if (pair, timeframe) in self._ft_informative: + raise OperationalException(f'Informative pair {pair} {timeframe} can ' + f'not be defined more than once!') + self._ft_informative[(pair, timeframe)] = (informative_data, cls_method) + + def _format_pair(self, pair: str) -> str: + return pair.format(stake_currency=self.config['stake_currency'], + stake=self.config['stake_currency']).upper() + @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ @@ -377,6 +412,14 @@ class IStrategy(ABC, HyperStrategyMixin): # END - Intended to be overridden by strategy ### + def gather_informative_pairs(self) -> ListPairsWithTimeframes: + """ + Internal method which gathers all informative pairs (user or automatically defined). + """ + informative_pairs = self.informative_pairs() + informative_pairs += list(self._ft_informative.keys()) + return list(set(informative_pairs)) + def get_strategy_name(self) -> str: """ Returns strategy class name @@ -793,6 +836,14 @@ class IStrategy(ABC, HyperStrategyMixin): :return: a Dataframe with all mandatory indicators for the strategies """ logger.debug(f"Populating indicators for pair {metadata.get('pair')}.") + + # call populate_indicators_Nm() which were tagged with @informative decorator. + for (pair, timeframe), (informative_data, populate_fn) in self._ft_informative.items(): + if not informative_data.asset and pair != metadata['pair']: + continue + dataframe = _create_and_merge_informative_pair( + self, dataframe, metadata, informative_data, populate_fn) + if self._populate_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " "the current function headers!", DeprecationWarning) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 32f7a9886..aa828d330 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -1,10 +1,24 @@ -import pandas as pd +from typing import Any, Callable, NamedTuple, Optional, Union +import pandas as pd +from mypy_extensions import KwArg +from pandas import DataFrame + +from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes +class InformativeData(NamedTuple): + asset: Optional[str] + timeframe: str + fmt: Union[str, Callable[[KwArg(str)], str], None] + ffill: bool + + def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, - timeframe: str, timeframe_inf: str, ffill: bool = True) -> pd.DataFrame: + timeframe: str, timeframe_inf: str, ffill: bool = True, + append_timeframe: bool = True, + date_column: str = 'date') -> pd.DataFrame: """ Correctly merge informative samples to the original dataframe, avoiding lookahead bias. @@ -24,6 +38,8 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, :param timeframe: Timeframe of the original pair sample. :param timeframe_inf: Timeframe of the informative pair sample. :param ffill: Forwardfill missing values - optional but usually required + :param append_timeframe: Rename columns by appending timeframe. + :param date_column: A custom date column name. :return: Merged dataframe :raise: ValueError if the secondary timeframe is shorter than the dataframe timeframe """ @@ -32,25 +48,29 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, minutes = timeframe_to_minutes(timeframe) if minutes == minutes_inf: # No need to forwardshift if the timeframes are identical - informative['date_merge'] = informative["date"] + informative['date_merge'] = informative[date_column] elif minutes < minutes_inf: # Subtract "small" timeframe so merging is not delayed by 1 small candle # Detailed explanation in https://github.com/freqtrade/freqtrade/issues/4073 informative['date_merge'] = ( - informative["date"] + pd.to_timedelta(minutes_inf, 'm') - pd.to_timedelta(minutes, 'm') + informative[date_column] + pd.to_timedelta(minutes_inf, 'm') - + pd.to_timedelta(minutes, 'm') ) else: raise ValueError("Tried to merge a faster timeframe to a slower timeframe." "This would create new rows, and can throw off your regular indicators.") # Rename columns to be unique - informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns] + date_merge = 'date_merge' + if append_timeframe: + date_merge = f'date_merge_{timeframe_inf}' + informative.columns = [f"{col}_{timeframe_inf}" for col in informative.columns] # Combine the 2 dataframes # all indicators on the informative sample MUST be calculated before this point dataframe = pd.merge(dataframe, informative, left_on='date', - right_on=f'date_merge_{timeframe_inf}', how='left') - dataframe = dataframe.drop(f'date_merge_{timeframe_inf}', axis=1) + right_on=date_merge, how='left') + dataframe = dataframe.drop(date_merge, axis=1) if ffill: dataframe = dataframe.ffill() @@ -94,3 +114,117 @@ def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float: :return: Positive stop loss value relative to current price """ return 1 - (stop_rate / current_rate) + + +def informative(timeframe: str, asset: str = '', + fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None, + ffill: bool = True) -> Callable[[Callable[[Any, DataFrame, dict], DataFrame]], + Callable[[Any, DataFrame, dict], DataFrame]]: + """ + A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to + define informative indicators. + + Example usage: + + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + :param timeframe: Informative timeframe. Must always be higher than strategy timeframe. + :param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use + current pair. + :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not + specified, defaults to {asset}_{name}_{timeframe} if asset is specified, or {name}_{timeframe} + otherwise. + * {asset}: name of informative asset, provided in lower-case, with / replaced with _. Stake + currency is not included in this string. + * {name}: user-specified dataframe column name. + * {timeframe}: informative timeframe. + :param ffill: ffill dataframe after mering informative pair. + """ + _asset = asset + _timeframe = timeframe + _fmt = fmt + _ffill = ffill + + def decorator(fn: Callable[[Any, DataFrame, dict], DataFrame]): + informative_pairs = getattr(fn, '_ft_informative', []) + informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill)) + setattr(fn, '_ft_informative', informative_pairs) + return fn + return decorator + + +def _format_pair_name(config, pair: str) -> str: + return pair.format(stake_currency=config['stake_currency'], + stake=config['stake_currency']).upper() + + +def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, + metadata: dict, informative_data: InformativeData, + populate_indicators: Callable[[Any, DataFrame, dict], + DataFrame]): + asset = informative_data.asset or '' + timeframe = informative_data.timeframe + fmt = informative_data.fmt + ffill = informative_data.ffill + config = strategy.config + dp = strategy.dp + + if asset: + # Insert stake currency if needed. + asset = _format_pair_name(config, asset) + else: + # Not specifying an asset will define informative dataframe for current pair. + asset = metadata['pair'] + + if '/' in asset: + base, quote = asset.split('/') + else: + # When futures are supported this may need reevaluation. + # base, quote = asset, None + raise OperationalException('Not implemented.') + + # Default format. This optimizes for the common case: informative pairs using same stake + # currency. When quote currency matches stake currency, column name will omit base currency. + # This allows easily reconfiguring strategy to use different base currency. In a rare case + # where it is desired to keep quote currency in column name at all times user should specify + # fmt='{base}_{quote}_{column}_{timeframe}' format or similar. + if not fmt: + fmt = '{column}_{timeframe}' # Informatives of current pair + if asset != metadata['pair']: + if quote == config['stake_currency']: + fmt = '{base}_' + fmt # Informatives of other pair + else: + fmt = '{base}_{quote}_' + fmt # Informatives of different quote currency + + inf_metadata = {'pair': asset, 'timeframe': timeframe} + inf_dataframe = dp.get_pair_dataframe(asset, timeframe) + inf_dataframe = populate_indicators(strategy, inf_dataframe, inf_metadata) + + formatter: Any = None + if callable(fmt): + formatter = fmt # A custom user-specified formatter function. + else: + formatter = fmt.format # A default string formatter. + + fmt_args = { + 'BASE': base.upper(), + 'QUOTE': quote.upper(), + 'base': base.lower(), + 'quote': quote.lower(), + 'asset': asset, + 'timeframe': timeframe, + } + inf_dataframe.rename(columns=lambda column: formatter(column=column, **fmt_args), + inplace=True) + + date_column = formatter(column='date', **fmt_args) + if date_column in dataframe.columns: + raise OperationalException(f'Duplicate column name {date_column} exists in ' + f'dataframe! Ensure column names are unique!') + dataframe = merge_informative_pair(dataframe, inf_dataframe, strategy.timeframe, timeframe, + ffill=ffill, append_timeframe=False, + date_column=date_column) + return dataframe diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 2852486ed..43eb70938 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1218,6 +1218,7 @@ def test_api_strategies(botclient): assert_response(rc) assert rc.json() == {'strategies': [ 'HyperoptableStrategy', + 'InformativeDecoratorTest', 'StrategyTestV2', 'TestStrategyLegacyV1' ]} diff --git a/tests/strategy/strats/informative_decorator_strategy.py b/tests/strategy/strats/informative_decorator_strategy.py new file mode 100644 index 000000000..a32ad79e8 --- /dev/null +++ b/tests/strategy/strats/informative_decorator_strategy.py @@ -0,0 +1,75 @@ +# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement + +from pandas import DataFrame + +from freqtrade.strategy import informative, merge_informative_pair +from freqtrade.strategy.interface import IStrategy + + +class InformativeDecoratorTest(IStrategy): + """ + Strategy used by tests freqtrade bot. + Please do not modify this strategy, it's intended for internal use only. + Please look at the SampleStrategy in the user_data/strategy directory + or strategy repository https://github.com/freqtrade/freqtrade-strategies + for samples and inspiration. + """ + INTERFACE_VERSION = 2 + stoploss = -0.10 + timeframe = '5m' + startup_candle_count: int = 20 + + def informative_pairs(self): + return [('BTC/USDT', '5m')] + + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['buy'] = 0 + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['sell'] = 0 + return dataframe + + # Decorator stacking test. + @informative('30m') + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + # Simple informative test. + @informative('1h', 'BTC/{stake}') + def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + # Quote currency different from stake currency test. + @informative('1h', 'ETH/BTC') + def populate_indicators_eth_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + # Formatting test. + @informative('30m', 'BTC/{stake}', '{column}_{BASE}_{QUOTE}_{base}_{quote}_{asset}_{timeframe}') + def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + # Custom formatter test + @informative('30m', 'ETH/{stake}', fmt=lambda column, **kwargs: column + '_from_callable') + def populate_indicators_eth_30m(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = 14 + return dataframe + + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + # Strategy timeframe indicators for current pair. + dataframe['rsi'] = 14 + # Informative pairs are available in this method. + dataframe['rsi_less'] = dataframe['rsi'] < dataframe['rsi_1h'] + + # Mixing manual informative pairs with decorators. + informative = self.dp.get_pair_dataframe('BTC/USDT', '5m') + informative['rsi'] = 14 + dataframe = merge_informative_pair(dataframe, informative, self.timeframe, '5m', ffill=True) + + return dataframe diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 250dcf63d..dcb9e3e64 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -607,7 +607,7 @@ def test_is_informative_pairs_callback(default_conf): strategy = StrategyResolver.load_strategy(default_conf) # Should return empty # Uses fallback to base implementation - assert [] == strategy.informative_pairs() + assert [] == strategy.gather_informative_pairs() @pytest.mark.parametrize('error', [ diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 3b84fc254..7784f3f77 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -4,6 +4,7 @@ import numpy as np import pandas as pd import pytest +from freqtrade.data.dataprovider import DataProvider from freqtrade.strategy import merge_informative_pair, stoploss_from_open, timeframe_to_minutes @@ -132,3 +133,57 @@ def test_stoploss_from_open(): assert stoploss == 0 else: assert isclose(stop_price, expected_stop_price, rel_tol=0.00001) + + +def test_informative_decorator(mocker, default_conf): + test_data_5m = generate_test_data('5m', 40) + test_data_30m = generate_test_data('30m', 40) + test_data_1h = generate_test_data('1h', 40) + data = { + ('XRP/USDT', '5m'): test_data_5m, + ('XRP/USDT', '30m'): test_data_30m, + ('XRP/USDT', '1h'): test_data_1h, + ('LTC/USDT', '5m'): test_data_5m, + ('LTC/USDT', '30m'): test_data_30m, + ('LTC/USDT', '1h'): test_data_1h, + ('BTC/USDT', '30m'): test_data_30m, + ('BTC/USDT', '5m'): test_data_5m, + ('BTC/USDT', '1h'): test_data_1h, + ('ETH/USDT', '1h'): test_data_1h, + ('ETH/USDT', '30m'): test_data_30m, + ('ETH/BTC', '1h'): test_data_1h, + } + from .strats.informative_decorator_strategy import InformativeDecoratorTest + default_conf['stake_currency'] = 'USDT' + InformativeDecoratorTest.dp = DataProvider({}, None, None) + mocker.patch.object(InformativeDecoratorTest.dp, 'current_whitelist', return_value=[ + 'XRP/USDT', 'LTC/USDT' + ]) + strategy = InformativeDecoratorTest(config=default_conf) + + assert len(strategy._ft_informative) == 8 + informative_pairs = [('XRP/USDT', '1h'), ('LTC/USDT', '1h'), ('XRP/USDT', '30m'), + ('LTC/USDT', '30m'), ('BTC/USDT', '1h'), ('BTC/USDT', '30m'), + ('BTC/USDT', '5m'), ('ETH/BTC', '1h'), ('ETH/USDT', '30m')] + for inf_pair in informative_pairs: + assert inf_pair in strategy.gather_informative_pairs() + + def test_historic_ohlcv(pair, timeframe): + return data[(pair, timeframe or strategy.timeframe)].copy() + mocker.patch('freqtrade.data.dataprovider.DataProvider.historic_ohlcv', + side_effect=test_historic_ohlcv) + + analyzed = strategy.advise_all_indicators( + {p: data[(p, strategy.timeframe)] for p in ('XRP/USDT', 'LTC/USDT')}) + expected_columns = [ + 'rsi_1h', 'rsi_30m', # Stacked informative decorators + 'btc_rsi_1h', # BTC 1h informative + 'rsi_BTC_USDT_btc_usdt_BTC/USDT_30m', # Column formatting + 'rsi_from_callable', # Custom column formatter + 'eth_btc_rsi_1h', # Quote currency not matching stake currency + 'rsi', 'rsi_less', # Non-informative columns + 'rsi_5m', # Manual informative dataframe + ] + for _, dataframe in analyzed.items(): + for col in expected_columns: + assert col in dataframe.columns diff --git a/tests/strategy/test_strategy_loading.py b/tests/strategy/test_strategy_loading.py index 2cbc9d0c6..3a30a824a 100644 --- a/tests/strategy/test_strategy_loading.py +++ b/tests/strategy/test_strategy_loading.py @@ -35,7 +35,7 @@ def test_search_all_strategies_no_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=False) assert isinstance(strategies, list) - assert len(strategies) == 3 + assert len(strategies) == 4 assert isinstance(strategies[0], dict) @@ -43,10 +43,10 @@ def test_search_all_strategies_with_failed(): directory = Path(__file__).parent / "strats" strategies = StrategyResolver.search_all_objects(directory, enum_failed=True) assert isinstance(strategies, list) - assert len(strategies) == 4 + assert len(strategies) == 5 # with enum_failed=True search_all_objects() shall find 2 good strategies # and 1 which fails to load - assert len([x for x in strategies if x['class'] is not None]) == 3 + assert len([x for x in strategies if x['class'] is not None]) == 4 assert len([x for x in strategies if x['class'] is None]) == 1 From f2a1d9d2fc8efe238e47f5404dd6c924511e683b Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 5 Sep 2021 09:54:05 +0300 Subject: [PATCH 078/102] [SQUASH] Address PR comments. --- docs/strategy-customization.md | 56 ++++++++++++++++++++------- freqtrade/strategy/interface.py | 18 ++++----- freqtrade/strategy/strategy_helper.py | 32 +++++++++------ 3 files changed, 72 insertions(+), 34 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 526c111c5..f2bf6cf7c 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -681,9 +681,47 @@ In some situations it may be confusing to deal with stops relative to current ra ### *@informative()* +``` python +def informative(timeframe: str, asset: str = '', + fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None, + ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]: + """ + A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to + define informative indicators. + + Example usage: + + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + :param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe. + :param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use + current pair. + :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not + specified, defaults to: + * {base}_{column}_{timeframe} if asset is specified and quote currency does match stake + curerncy. + * {base}_{quote}_{column}_{timeframe} if asset is specified and quote currency does not match + stake curerncy. + * {column}_{timeframe} if asset is not specified. + Format string supports these format variables: + * {asset} - full name of the asset, for example 'BTC/USDT'. + * {base} - base currency in lower case, for example 'eth'. + * {BASE} - same as {base}, except in upper case. + * {quote} - quote currency in lower case, for example 'usdt'. + * {QUOTE} - same as {quote}, except in upper case. + * {column} - name of dataframe column. + * {timeframe} - timeframe of informative dataframe. + :param ffill: ffill dataframe after merging informative pair. + """ +``` + In most common case it is possible to easily define informative pairs by using a decorator. All decorated `populate_indicators_*` methods run in isolation, not having access to data from other informative pairs, in the end all informative dataframes are merged and passed to main `populate_indicators()` method. -When hyperopting, please follow instructions of [optimizing an indicator parameter](hyperopt.md#optimizing-an-indicator-parameter). +When hyperopting, use of hyperoptable parameter `.value` attribute is not supported. Please use `.range` attribute. See [optimizing an indicator parameter](hyperopt.md#optimizing-an-indicator-parameter) +for more information. ??? Example "Fast and easy way to define informative pairs" @@ -725,17 +763,9 @@ When hyperopting, please follow instructions of [optimizing an indicator paramet return dataframe # Define BTC/STAKE informative pair. A custom formatter may be specified for formatting - # column names. Format string supports these format variables: - # * {asset} - full name of the asset, for example 'BTC/USDT'. - # * {base} - base currency in lower case, for example 'eth'. - # * {BASE} - same as {base}, except in upper case. - # * {quote} - quote currency in lower case, for example 'usdt'. - # * {QUOTE} - same as {quote}, except in upper case. - # * {column} - name of dataframe column. - # * {timeframe} - timeframe of informative dataframe. - # A callable `fmt(**kwargs) -> str` may be specified, to implement custom formatting. - # Available in populate_indicators and other methods as 'rsi_upper'. - @informative('1h', 'BTC/{stake}', '{name}') + # column names. A callable `fmt(**kwargs) -> str` may be specified, to implement custom + # formatting. Available in populate_indicators and other methods as 'rsi_upper'. + @informative('1h', 'BTC/{stake}', '{column}') def populate_indicators_btc_1h_2(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['rsi_upper'] = ta.RSI(dataframe, timeperiod=14) return dataframe @@ -749,8 +779,6 @@ When hyperopting, please follow instructions of [optimizing an indicator paramet ``` - See docstring of `@informative()` decorator for more information. - !!! Note Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs manually as described [in the DataProvider section](#complete-data-provider-sample). diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 8e8b8b404..0546deb01 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -6,7 +6,7 @@ import logging import warnings from abc import ABC, abstractmethod from datetime import datetime, timedelta, timezone -from typing import Any, Callable, Dict, List, Optional, Tuple, Union +from typing import Dict, List, Optional, Tuple, Union import arrow from pandas import DataFrame @@ -19,7 +19,8 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.persistence import PairLocks, Trade from freqtrade.strategy.hyper import HyperStrategyMixin -from freqtrade.strategy.strategy_helper import (InformativeData, _create_and_merge_informative_pair, +from freqtrade.strategy.strategy_helper import (InformativeData, PopulateIndicators, + _create_and_merge_informative_pair, _format_pair_name) from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -138,20 +139,23 @@ class IStrategy(ABC, HyperStrategyMixin): # Gather informative pairs from @informative-decorated methods. self._ft_informative: Dict[ - Tuple[str, str], Tuple[InformativeData, - Callable[[Any, DataFrame, dict], DataFrame]]] = {} + Tuple[str, str], Tuple[InformativeData, PopulateIndicators]] = {} for attr_name in dir(self.__class__): cls_method = getattr(self.__class__, attr_name) if not callable(cls_method): continue - ft_informative = getattr(cls_method, '_ft_informative', []) + ft_informative = getattr(cls_method, '_ft_informative', None) if not isinstance(ft_informative, list): # Type check is required because mocker would return a mock object that evaluates to # True, confusing this code. continue + strategy_timeframe_minutes = timeframe_to_minutes(self.timeframe) for informative_data in ft_informative: asset = informative_data.asset timeframe = informative_data.timeframe + if timeframe_to_minutes(timeframe) < strategy_timeframe_minutes: + raise OperationalException('Informative timeframe must be equal or higher than ' + 'strategy timeframe!') if asset: pair = _format_pair_name(self.config, asset) if (pair, timeframe) in self._ft_informative: @@ -165,10 +169,6 @@ class IStrategy(ABC, HyperStrategyMixin): f'not be defined more than once!') self._ft_informative[(pair, timeframe)] = (informative_data, cls_method) - def _format_pair(self, pair: str) -> str: - return pair.format(stake_currency=self.config['stake_currency'], - stake=self.config['stake_currency']).upper() - @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index aa828d330..64d9bdea8 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -8,6 +8,9 @@ from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes +PopulateIndicators = Callable[[Any, DataFrame, dict], DataFrame] + + class InformativeData(NamedTuple): asset: Optional[str] timeframe: str @@ -118,8 +121,7 @@ def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float: def informative(timeframe: str, asset: str = '', fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None, - ffill: bool = True) -> Callable[[Callable[[Any, DataFrame, dict], DataFrame]], - Callable[[Any, DataFrame, dict], DataFrame]]: + ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]: """ A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to define informative indicators. @@ -131,24 +133,32 @@ def informative(timeframe: str, asset: str = '', dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) return dataframe - :param timeframe: Informative timeframe. Must always be higher than strategy timeframe. + :param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe. :param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use current pair. :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not - specified, defaults to {asset}_{name}_{timeframe} if asset is specified, or {name}_{timeframe} - otherwise. - * {asset}: name of informative asset, provided in lower-case, with / replaced with _. Stake - currency is not included in this string. - * {name}: user-specified dataframe column name. - * {timeframe}: informative timeframe. - :param ffill: ffill dataframe after mering informative pair. + specified, defaults to: + * {base}_{column}_{timeframe} if asset is specified and quote currency does match stake + curerncy. + * {base}_{quote}_{column}_{timeframe} if asset is specified and quote currency does not match + stake curerncy. + * {column}_{timeframe} if asset is not specified. + Format string supports these format variables: + * {asset} - full name of the asset, for example 'BTC/USDT'. + * {base} - base currency in lower case, for example 'eth'. + * {BASE} - same as {base}, except in upper case. + * {quote} - quote currency in lower case, for example 'usdt'. + * {QUOTE} - same as {quote}, except in upper case. + * {column} - name of dataframe column. + * {timeframe} - timeframe of informative dataframe. + :param ffill: ffill dataframe after merging informative pair. """ _asset = asset _timeframe = timeframe _fmt = fmt _ffill = ffill - def decorator(fn: Callable[[Any, DataFrame, dict], DataFrame]): + def decorator(fn: PopulateIndicators): informative_pairs = getattr(fn, '_ft_informative', []) informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill)) setattr(fn, '_ft_informative', informative_pairs) From dfa61b7ad2297a94b0b26309ca74c64a7c1d08a7 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Tue, 7 Sep 2021 15:40:53 +0300 Subject: [PATCH 079/102] [SQUASH] Fix informatives for each pair not being created because dataprovider was not available. Fix not being able to have informative dataframe of a pair in whitelist. --- docs/strategy-customization.md | 4 +-- freqtrade/freqtradebot.py | 10 ++++--- freqtrade/optimize/backtesting.py | 3 +- freqtrade/optimize/edge_cli.py | 3 ++ freqtrade/strategy/interface.py | 38 +++++++++++++++---------- freqtrade/strategy/strategy_helper.py | 13 ++++----- tests/strategy/test_strategy_helpers.py | 9 +++--- 7 files changed, 47 insertions(+), 33 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index f2bf6cf7c..a994d9acd 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -702,9 +702,9 @@ def informative(timeframe: str, asset: str = '', :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not specified, defaults to: * {base}_{column}_{timeframe} if asset is specified and quote currency does match stake - curerncy. + currency. * {base}_{quote}_{column}_{timeframe} if asset is specified and quote currency does not match - stake curerncy. + stake currency. * {column}_{timeframe} if asset is not specified. Format string supports these format variables: * {asset} - full name of the asset, for example 'BTC/USDT'. diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index bdc438c9a..b79916639 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -83,10 +83,12 @@ class FreqtradeBot(LoggingMixin): self.dataprovider = DataProvider(self.config, self.exchange, self.pairlists) - # Attach Dataprovider to Strategy baseclass - IStrategy.dp = self.dataprovider - # Attach Wallets to Strategy baseclass - IStrategy.wallets = self.wallets + # Attach Dataprovider to strategy instance + self.strategy.dp = self.dataprovider + # Attach Wallets to strategy instance + self.strategy.wallets = self.wallets + # Late initialization (may depend on dp/wallets) + self.strategy._initialize() # Initializing Edge only if enabled self.edge = Edge(self.config, self.exchange, self.strategy) if \ diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 3e06bfa1b..ef491ae5e 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -154,11 +154,12 @@ class Backtesting: self.strategy: IStrategy = strategy strategy.dp = self.dataprovider # Attach Wallets to Strategy baseclass - IStrategy.wallets = self.wallets + strategy.wallets = self.wallets # Set stoploss_on_exchange to false for backtesting, # since a "perfect" stoploss-sell is assumed anyway # And the regular "stoploss" function would not apply to that case self.strategy.order_types['stoploss_on_exchange'] = False + strategy._initialize() def _load_protections(self, strategy: IStrategy): if self.config.get('enable_protections', False): diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index 417faa685..abb5ca635 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -8,6 +8,7 @@ from typing import Any, Dict from freqtrade import constants from freqtrade.configuration import TimeRange, validate_config_consistency +from freqtrade.data.dataprovider import DataProvider from freqtrade.edge import Edge from freqtrade.optimize.optimize_reports import generate_edge_table from freqtrade.resolvers import ExchangeResolver, StrategyResolver @@ -33,6 +34,8 @@ class EdgeCli: self.config['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) self.strategy = StrategyResolver.load_strategy(self.config) + self.strategy.dp = DataProvider(config, None) + self.strategy._initialize() validate_config_consistency(self.config) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 0546deb01..951979212 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -137,9 +137,13 @@ class IStrategy(ABC, HyperStrategyMixin): self._last_candle_seen_per_pair: Dict[str, datetime] = {} super().__init__(config) + def _initialize(self): + """ + Late initialization tasks, which may depend on availability of dataprovider/wallets/etc. + """ # Gather informative pairs from @informative-decorated methods. self._ft_informative: Dict[ - Tuple[str, str], Tuple[InformativeData, PopulateIndicators]] = {} + Tuple[str, str], List[Tuple[InformativeData, PopulateIndicators]]] = {} for attr_name in dir(self.__class__): cls_method = getattr(self.__class__, attr_name) if not callable(cls_method): @@ -158,16 +162,19 @@ class IStrategy(ABC, HyperStrategyMixin): 'strategy timeframe!') if asset: pair = _format_pair_name(self.config, asset) - if (pair, timeframe) in self._ft_informative: - raise OperationalException(f'Informative pair {pair} {timeframe} can not ' - f'be defined more than once!') - self._ft_informative[(pair, timeframe)] = (informative_data, cls_method) - elif self.dp is not None: + try: + self._ft_informative[(pair, timeframe)].append( + (informative_data, cls_method)) + except KeyError: + self._ft_informative[(pair, timeframe)] = [(informative_data, cls_method)] + else: for pair in self.dp.current_whitelist(): - if (pair, timeframe) in self._ft_informative: - raise OperationalException(f'Informative pair {pair} {timeframe} can ' - f'not be defined more than once!') - self._ft_informative[(pair, timeframe)] = (informative_data, cls_method) + try: + self._ft_informative[(pair, timeframe)].append( + (informative_data, cls_method)) + except KeyError: + self._ft_informative[(pair, timeframe)] = \ + [(informative_data, cls_method)] @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -838,11 +845,12 @@ class IStrategy(ABC, HyperStrategyMixin): logger.debug(f"Populating indicators for pair {metadata.get('pair')}.") # call populate_indicators_Nm() which were tagged with @informative decorator. - for (pair, timeframe), (informative_data, populate_fn) in self._ft_informative.items(): - if not informative_data.asset and pair != metadata['pair']: - continue - dataframe = _create_and_merge_informative_pair( - self, dataframe, metadata, informative_data, populate_fn) + for (pair, timeframe), informatives in self._ft_informative.items(): + for (informative_data, populate_fn) in informatives: + if not informative_data.asset and pair != metadata['pair']: + continue + dataframe = _create_and_merge_informative_pair( + self, dataframe, metadata, informative_data, populate_fn) if self._populate_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 64d9bdea8..a4023f953 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -139,9 +139,9 @@ def informative(timeframe: str, asset: str = '', :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not specified, defaults to: * {base}_{column}_{timeframe} if asset is specified and quote currency does match stake - curerncy. + currency. * {base}_{quote}_{column}_{timeframe} if asset is specified and quote currency does not match - stake curerncy. + stake currency. * {column}_{timeframe} if asset is not specified. Format string supports these format variables: * {asset} - full name of the asset, for example 'BTC/USDT'. @@ -203,11 +203,10 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, # fmt='{base}_{quote}_{column}_{timeframe}' format or similar. if not fmt: fmt = '{column}_{timeframe}' # Informatives of current pair - if asset != metadata['pair']: - if quote == config['stake_currency']: - fmt = '{base}_' + fmt # Informatives of other pair - else: - fmt = '{base}_{quote}_' + fmt # Informatives of different quote currency + if quote != config['stake_currency']: + fmt = '{quote}_' + fmt # Informatives of different quote currency + if informative_data.asset: + fmt = '{base}_' + fmt # Informatives of other pair inf_metadata = {'pair': asset, 'timeframe': timeframe} inf_dataframe = dp.get_pair_dataframe(asset, timeframe) diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 7784f3f77..0ee554ede 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -155,11 +155,12 @@ def test_informative_decorator(mocker, default_conf): } from .strats.informative_decorator_strategy import InformativeDecoratorTest default_conf['stake_currency'] = 'USDT' - InformativeDecoratorTest.dp = DataProvider({}, None, None) - mocker.patch.object(InformativeDecoratorTest.dp, 'current_whitelist', return_value=[ - 'XRP/USDT', 'LTC/USDT' - ]) strategy = InformativeDecoratorTest(config=default_conf) + strategy.dp = DataProvider({}, None, None) + mocker.patch.object(strategy.dp, 'current_whitelist', return_value=[ + 'XRP/USDT', 'LTC/USDT', 'BTC/USDT' + ]) + strategy._initialize() assert len(strategy._ft_informative) == 8 informative_pairs = [('XRP/USDT', '1h'), ('LTC/USDT', '1h'), ('XRP/USDT', '30m'), From f81df19b934b738bf148f51fdb5a9c7ab1dfb34f Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Tue, 7 Sep 2021 15:53:12 +0300 Subject: [PATCH 080/102] [TMP] Make tests not fail for now. --- freqtrade/strategy/interface.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 951979212..6e312e15d 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -424,7 +424,8 @@ class IStrategy(ABC, HyperStrategyMixin): Internal method which gathers all informative pairs (user or automatically defined). """ informative_pairs = self.informative_pairs() - informative_pairs += list(self._ft_informative.keys()) + if hasattr(self, '_ft_informative'): + informative_pairs += list(self._ft_informative.keys()) return list(set(informative_pairs)) def get_strategy_name(self) -> str: @@ -844,13 +845,14 @@ class IStrategy(ABC, HyperStrategyMixin): """ logger.debug(f"Populating indicators for pair {metadata.get('pair')}.") - # call populate_indicators_Nm() which were tagged with @informative decorator. - for (pair, timeframe), informatives in self._ft_informative.items(): - for (informative_data, populate_fn) in informatives: - if not informative_data.asset and pair != metadata['pair']: - continue - dataframe = _create_and_merge_informative_pair( - self, dataframe, metadata, informative_data, populate_fn) + if hasattr(self, '_ft_informative'): + # call populate_indicators_Nm() which were tagged with @informative decorator. + for (pair, timeframe), informatives in self._ft_informative.items(): + for (informative_data, populate_fn) in informatives: + if not informative_data.asset and pair != metadata['pair']: + continue + dataframe = _create_and_merge_informative_pair( + self, dataframe, metadata, informative_data, populate_fn) if self._populate_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " From 5dc78a0c66f385edd14db16a806e1f75bd453e83 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Fri, 10 Sep 2021 09:36:52 +0300 Subject: [PATCH 081/102] [SQUASH] Get rid of _initialize() and fix informatives for dynamic pairlists. --- freqtrade/freqtradebot.py | 2 - freqtrade/optimize/backtesting.py | 1 - freqtrade/optimize/edge_cli.py | 1 - freqtrade/strategy/interface.py | 56 ++++++++----------------- freqtrade/strategy/strategy_helper.py | 17 ++++---- tests/strategy/test_strategy_helpers.py | 3 +- 6 files changed, 27 insertions(+), 53 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index b79916639..1cb8988ff 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -87,8 +87,6 @@ class FreqtradeBot(LoggingMixin): self.strategy.dp = self.dataprovider # Attach Wallets to strategy instance self.strategy.wallets = self.wallets - # Late initialization (may depend on dp/wallets) - self.strategy._initialize() # Initializing Edge only if enabled self.edge = Edge(self.config, self.exchange, self.strategy) if \ diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ef491ae5e..79c861ee8 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -159,7 +159,6 @@ class Backtesting: # since a "perfect" stoploss-sell is assumed anyway # And the regular "stoploss" function would not apply to that case self.strategy.order_types['stoploss_on_exchange'] = False - strategy._initialize() def _load_protections(self, strategy: IStrategy): if self.config.get('enable_protections', False): diff --git a/freqtrade/optimize/edge_cli.py b/freqtrade/optimize/edge_cli.py index abb5ca635..f211da750 100644 --- a/freqtrade/optimize/edge_cli.py +++ b/freqtrade/optimize/edge_cli.py @@ -35,7 +35,6 @@ class EdgeCli: self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config) self.strategy = StrategyResolver.load_strategy(self.config) self.strategy.dp = DataProvider(config, None) - self.strategy._initialize() validate_config_consistency(self.config) diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 6e312e15d..00c56f5df 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -121,7 +121,7 @@ class IStrategy(ABC, HyperStrategyMixin): # Class level variables (intentional) containing # the dataprovider (dp) (access to other candles, historic data, ...) # and wallets - access to the current balance. - dp: Optional[DataProvider] = None + dp: DataProvider wallets: Optional[Wallets] = None # Filled from configuration stake_currency: str @@ -137,44 +137,23 @@ class IStrategy(ABC, HyperStrategyMixin): self._last_candle_seen_per_pair: Dict[str, datetime] = {} super().__init__(config) - def _initialize(self): - """ - Late initialization tasks, which may depend on availability of dataprovider/wallets/etc. - """ # Gather informative pairs from @informative-decorated methods. - self._ft_informative: Dict[ - Tuple[str, str], List[Tuple[InformativeData, PopulateIndicators]]] = {} + self._ft_informative: List[Tuple[InformativeData, PopulateIndicators]] = [] for attr_name in dir(self.__class__): cls_method = getattr(self.__class__, attr_name) if not callable(cls_method): continue - ft_informative = getattr(cls_method, '_ft_informative', None) - if not isinstance(ft_informative, list): + informative_data_list = getattr(cls_method, '_ft_informative', None) + if not isinstance(informative_data_list, list): # Type check is required because mocker would return a mock object that evaluates to # True, confusing this code. continue strategy_timeframe_minutes = timeframe_to_minutes(self.timeframe) - for informative_data in ft_informative: - asset = informative_data.asset - timeframe = informative_data.timeframe - if timeframe_to_minutes(timeframe) < strategy_timeframe_minutes: + for informative_data in informative_data_list: + if timeframe_to_minutes(informative_data.timeframe) < strategy_timeframe_minutes: raise OperationalException('Informative timeframe must be equal or higher than ' 'strategy timeframe!') - if asset: - pair = _format_pair_name(self.config, asset) - try: - self._ft_informative[(pair, timeframe)].append( - (informative_data, cls_method)) - except KeyError: - self._ft_informative[(pair, timeframe)] = [(informative_data, cls_method)] - else: - for pair in self.dp.current_whitelist(): - try: - self._ft_informative[(pair, timeframe)].append( - (informative_data, cls_method)) - except KeyError: - self._ft_informative[(pair, timeframe)] = \ - [(informative_data, cls_method)] + self._ft_informative.append((informative_data, cls_method)) @abstractmethod def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: @@ -424,8 +403,13 @@ class IStrategy(ABC, HyperStrategyMixin): Internal method which gathers all informative pairs (user or automatically defined). """ informative_pairs = self.informative_pairs() - if hasattr(self, '_ft_informative'): - informative_pairs += list(self._ft_informative.keys()) + for inf_data, _ in self._ft_informative: + if inf_data.asset: + pair_tf = (_format_pair_name(self.config, inf_data.asset), inf_data.timeframe) + informative_pairs.append(pair_tf) + else: + for pair in self.dp.current_whitelist(): + informative_pairs.append((pair, inf_data.timeframe)) return list(set(informative_pairs)) def get_strategy_name(self) -> str: @@ -845,14 +829,10 @@ class IStrategy(ABC, HyperStrategyMixin): """ logger.debug(f"Populating indicators for pair {metadata.get('pair')}.") - if hasattr(self, '_ft_informative'): - # call populate_indicators_Nm() which were tagged with @informative decorator. - for (pair, timeframe), informatives in self._ft_informative.items(): - for (informative_data, populate_fn) in informatives: - if not informative_data.asset and pair != metadata['pair']: - continue - dataframe = _create_and_merge_informative_pair( - self, dataframe, metadata, informative_data, populate_fn) + # call populate_indicators_Nm() which were tagged with @informative decorator. + for inf_data, populate_fn in self._ft_informative: + dataframe = _create_and_merge_informative_pair( + self, dataframe, metadata, inf_data, populate_fn) if self._populate_fun_len == 2: warnings.warn("deprecated - check out the Sample strategy to see " diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index a4023f953..15c6d8a69 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -171,14 +171,13 @@ def _format_pair_name(config, pair: str) -> str: stake=config['stake_currency']).upper() -def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, - metadata: dict, informative_data: InformativeData, - populate_indicators: Callable[[Any, DataFrame, dict], - DataFrame]): - asset = informative_data.asset or '' - timeframe = informative_data.timeframe - fmt = informative_data.fmt - ffill = informative_data.ffill +def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: dict, + inf_data: InformativeData, + populate_indicators: PopulateIndicators): + asset = inf_data.asset or '' + timeframe = inf_data.timeframe + fmt = inf_data.fmt + ffill = inf_data.ffill config = strategy.config dp = strategy.dp @@ -205,7 +204,7 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, fmt = '{column}_{timeframe}' # Informatives of current pair if quote != config['stake_currency']: fmt = '{quote}_' + fmt # Informatives of different quote currency - if informative_data.asset: + if inf_data.asset: fmt = '{base}_' + fmt # Informatives of other pair inf_metadata = {'pair': asset, 'timeframe': timeframe} diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 0ee554ede..95ca0416f 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -160,9 +160,8 @@ def test_informative_decorator(mocker, default_conf): mocker.patch.object(strategy.dp, 'current_whitelist', return_value=[ 'XRP/USDT', 'LTC/USDT', 'BTC/USDT' ]) - strategy._initialize() - assert len(strategy._ft_informative) == 8 + assert len(strategy._ft_informative) == 6 # Equal to number of decorators used informative_pairs = [('XRP/USDT', '1h'), ('LTC/USDT', '1h'), ('XRP/USDT', '30m'), ('LTC/USDT', '30m'), ('BTC/USDT', '1h'), ('BTC/USDT', '30m'), ('BTC/USDT', '5m'), ('ETH/BTC', '1h'), ('ETH/USDT', '30m')] From bb6ae682fc7d82175196741e31723c5f797ffd2f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 12 Sep 2021 09:59:10 +0200 Subject: [PATCH 082/102] Small simplifications --- docs/strategy-customization.md | 2 +- freqtrade/strategy/strategy_helper.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index a994d9acd..800dd9326 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -783,7 +783,7 @@ for more information. Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs manually as described [in the DataProvider section](#complete-data-provider-sample). -!!! Warning +!!! Warning "Duplicate method names" Methods tagged with `@informative()` decorator must always have unique names! Re-using same name (for example when copy-pasting already defined informative method) will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique! diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 15c6d8a69..746d656df 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -177,9 +177,7 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: asset = inf_data.asset or '' timeframe = inf_data.timeframe fmt = inf_data.fmt - ffill = inf_data.ffill config = strategy.config - dp = strategy.dp if asset: # Insert stake currency if needed. @@ -208,7 +206,7 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: fmt = '{base}_' + fmt # Informatives of other pair inf_metadata = {'pair': asset, 'timeframe': timeframe} - inf_dataframe = dp.get_pair_dataframe(asset, timeframe) + inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe) inf_dataframe = populate_indicators(strategy, inf_dataframe, inf_metadata) formatter: Any = None @@ -233,6 +231,6 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: raise OperationalException(f'Duplicate column name {date_column} exists in ' f'dataframe! Ensure column names are unique!') dataframe = merge_informative_pair(dataframe, inf_dataframe, strategy.timeframe, timeframe, - ffill=ffill, append_timeframe=False, + ffill=inf_data.ffill, append_timeframe=False, date_column=date_column) return dataframe From e88c4701bb2c321ed7cd5d6fed7fa6db6f4f41f6 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sun, 12 Sep 2021 18:26:41 +0300 Subject: [PATCH 083/102] [SQUASH] Address PR comments. --- docs/strategy-customization.md | 19 ++- freqtrade/strategy/__init__.py | 5 +- freqtrade/strategy/informative_decorator.py | 134 ++++++++++++++++++++ freqtrade/strategy/interface.py | 11 +- freqtrade/strategy/strategy_helper.py | 132 ------------------- 5 files changed, 152 insertions(+), 149 deletions(-) create mode 100644 freqtrade/strategy/informative_decorator.py diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 800dd9326..671768bfa 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -652,9 +652,7 @@ In some situations it may be confusing to deal with stops relative to current ra ??? Example "Returning a stoploss using absolute price from the custom stoploss function" - Say the open price was $100, and `current_price` is $121 (`current_profit` will be `0.21`). - - If we want a stop price at $107 price we can call `stoploss_from_absolute(107, current_rate)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100. + If we want to trail a stop price at 2xATR below current proce we can call `stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate)`. ``` python @@ -664,18 +662,17 @@ In some situations it may be confusing to deal with stops relative to current ra class AwesomeStrategy(IStrategy): - # ... populate_* methods - use_custom_stoploss = True + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['atr'] = ta.ATR(dataframe, timeperiod=14) + return dataframe + def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> float: - - # once the profit has risen above 10%, keep the stoploss at 7% above the open price - if current_profit > 0.10: - return stoploss_from_absolute(trade.open_rate * 1.07, current_rate) - - return 1 + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + candle = dataframe.iloc[-1].squeeze() + return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate) ``` diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index a7de34916..2ea0ad2b4 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -3,6 +3,7 @@ from freqtrade.exchange import (timeframe_to_minutes, timeframe_to_msecs, timefr timeframe_to_prev_date, timeframe_to_seconds) from freqtrade.strategy.hyper import (BooleanParameter, CategoricalParameter, DecimalParameter, IntParameter, RealParameter) +from freqtrade.strategy.informative_decorator import informative from freqtrade.strategy.interface import IStrategy -from freqtrade.strategy.strategy_helper import (informative, merge_informative_pair, - stoploss_from_absolute, stoploss_from_open) +from freqtrade.strategy.strategy_helper import (merge_informative_pair, stoploss_from_absolute, + stoploss_from_open) diff --git a/freqtrade/strategy/informative_decorator.py b/freqtrade/strategy/informative_decorator.py new file mode 100644 index 000000000..f09e634b0 --- /dev/null +++ b/freqtrade/strategy/informative_decorator.py @@ -0,0 +1,134 @@ +from typing import Any, Callable, NamedTuple, Optional, Union + +from mypy_extensions import KwArg +from pandas import DataFrame + +from freqtrade.exceptions import OperationalException +from freqtrade.strategy.strategy_helper import merge_informative_pair + + +PopulateIndicators = Callable[[Any, DataFrame, dict], DataFrame] + + +class InformativeData(NamedTuple): + asset: Optional[str] + timeframe: str + fmt: Union[str, Callable[[KwArg(str)], str], None] + ffill: bool + + +def informative(timeframe: str, asset: str = '', + fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None, + ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]: + """ + A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to + define informative indicators. + + Example usage: + + @informative('1h') + def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) + return dataframe + + :param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe. + :param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use + current pair. + :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not + specified, defaults to: + * {base}_{column}_{timeframe} if asset is specified and quote currency does match stake + currency. + * {base}_{quote}_{column}_{timeframe} if asset is specified and quote currency does not match + stake currency. + * {column}_{timeframe} if asset is not specified. + Format string supports these format variables: + * {asset} - full name of the asset, for example 'BTC/USDT'. + * {base} - base currency in lower case, for example 'eth'. + * {BASE} - same as {base}, except in upper case. + * {quote} - quote currency in lower case, for example 'usdt'. + * {QUOTE} - same as {quote}, except in upper case. + * {column} - name of dataframe column. + * {timeframe} - timeframe of informative dataframe. + :param ffill: ffill dataframe after merging informative pair. + """ + _asset = asset + _timeframe = timeframe + _fmt = fmt + _ffill = ffill + + def decorator(fn: PopulateIndicators): + informative_pairs = getattr(fn, '_ft_informative', []) + informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill)) + setattr(fn, '_ft_informative', informative_pairs) + return fn + return decorator + + +def _format_pair_name(config, pair: str) -> str: + return pair.format(stake_currency=config['stake_currency'], + stake=config['stake_currency']).upper() + + +def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: dict, + inf_data: InformativeData, + populate_indicators: PopulateIndicators): + asset = inf_data.asset or '' + timeframe = inf_data.timeframe + fmt = inf_data.fmt + config = strategy.config + + if asset: + # Insert stake currency if needed. + asset = _format_pair_name(config, asset) + else: + # Not specifying an asset will define informative dataframe for current pair. + asset = metadata['pair'] + + if '/' in asset: + base, quote = asset.split('/') + else: + # When futures are supported this may need reevaluation. + # base, quote = asset, None + raise OperationalException('Not implemented.') + + # Default format. This optimizes for the common case: informative pairs using same stake + # currency. When quote currency matches stake currency, column name will omit base currency. + # This allows easily reconfiguring strategy to use different base currency. In a rare case + # where it is desired to keep quote currency in column name at all times user should specify + # fmt='{base}_{quote}_{column}_{timeframe}' format or similar. + if not fmt: + fmt = '{column}_{timeframe}' # Informatives of current pair + if quote != config['stake_currency']: + fmt = '{quote}_' + fmt # Informatives of different quote currency + if inf_data.asset: + fmt = '{base}_' + fmt # Informatives of other pair + + inf_metadata = {'pair': asset, 'timeframe': timeframe} + inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe) + inf_dataframe = populate_indicators(strategy, inf_dataframe, inf_metadata) + + formatter: Any = None + if callable(fmt): + formatter = fmt # A custom user-specified formatter function. + else: + formatter = fmt.format # A default string formatter. + + fmt_args = { + 'BASE': base.upper(), + 'QUOTE': quote.upper(), + 'base': base.lower(), + 'quote': quote.lower(), + 'asset': asset, + 'timeframe': timeframe, + } + inf_dataframe.rename(columns=lambda column: formatter(column=column, **fmt_args), + inplace=True) + + date_column = formatter(column='date', **fmt_args) + if date_column in dataframe.columns: + raise OperationalException(f'Duplicate column name {date_column} exists in ' + f'dataframe! Ensure column names are unique!') + dataframe = merge_informative_pair(dataframe, inf_dataframe, strategy.timeframe, timeframe, + ffill=inf_data.ffill, append_timeframe=False, + date_column=date_column) + return dataframe diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index 00c56f5df..7420bd9fd 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -19,9 +19,9 @@ from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_next_date from freqtrade.persistence import PairLocks, Trade from freqtrade.strategy.hyper import HyperStrategyMixin -from freqtrade.strategy.strategy_helper import (InformativeData, PopulateIndicators, - _create_and_merge_informative_pair, - _format_pair_name) +from freqtrade.strategy.informative_decorator import (InformativeData, PopulateIndicators, + _create_and_merge_informative_pair, + _format_pair_name) from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper from freqtrade.wallets import Wallets @@ -121,7 +121,7 @@ class IStrategy(ABC, HyperStrategyMixin): # Class level variables (intentional) containing # the dataprovider (dp) (access to other candles, historic data, ...) # and wallets - access to the current balance. - dp: DataProvider + dp: Optional[DataProvider] wallets: Optional[Wallets] = None # Filled from configuration stake_currency: str @@ -408,6 +408,9 @@ class IStrategy(ABC, HyperStrategyMixin): pair_tf = (_format_pair_name(self.config, inf_data.asset), inf_data.timeframe) informative_pairs.append(pair_tf) else: + if not self.dp: + raise OperationalException('@informative decorator with unspecified asset ' + 'requires DataProvider instance.') for pair in self.dp.current_whitelist(): informative_pairs.append((pair, inf_data.timeframe)) return list(set(informative_pairs)) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index 746d656df..f813b39c5 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -1,23 +1,8 @@ -from typing import Any, Callable, NamedTuple, Optional, Union - import pandas as pd -from mypy_extensions import KwArg -from pandas import DataFrame -from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes -PopulateIndicators = Callable[[Any, DataFrame, dict], DataFrame] - - -class InformativeData(NamedTuple): - asset: Optional[str] - timeframe: str - fmt: Union[str, Callable[[KwArg(str)], str], None] - ffill: bool - - def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame, timeframe: str, timeframe_inf: str, ffill: bool = True, append_timeframe: bool = True, @@ -117,120 +102,3 @@ def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float: :return: Positive stop loss value relative to current price """ return 1 - (stop_rate / current_rate) - - -def informative(timeframe: str, asset: str = '', - fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None, - ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]: - """ - A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to - define informative indicators. - - Example usage: - - @informative('1h') - def populate_indicators_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: - dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) - return dataframe - - :param timeframe: Informative timeframe. Must always be equal or higher than strategy timeframe. - :param asset: Informative asset, for example BTC, BTC/USDT, ETH/BTC. Do not specify to use - current pair. - :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not - specified, defaults to: - * {base}_{column}_{timeframe} if asset is specified and quote currency does match stake - currency. - * {base}_{quote}_{column}_{timeframe} if asset is specified and quote currency does not match - stake currency. - * {column}_{timeframe} if asset is not specified. - Format string supports these format variables: - * {asset} - full name of the asset, for example 'BTC/USDT'. - * {base} - base currency in lower case, for example 'eth'. - * {BASE} - same as {base}, except in upper case. - * {quote} - quote currency in lower case, for example 'usdt'. - * {QUOTE} - same as {quote}, except in upper case. - * {column} - name of dataframe column. - * {timeframe} - timeframe of informative dataframe. - :param ffill: ffill dataframe after merging informative pair. - """ - _asset = asset - _timeframe = timeframe - _fmt = fmt - _ffill = ffill - - def decorator(fn: PopulateIndicators): - informative_pairs = getattr(fn, '_ft_informative', []) - informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill)) - setattr(fn, '_ft_informative', informative_pairs) - return fn - return decorator - - -def _format_pair_name(config, pair: str) -> str: - return pair.format(stake_currency=config['stake_currency'], - stake=config['stake_currency']).upper() - - -def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: dict, - inf_data: InformativeData, - populate_indicators: PopulateIndicators): - asset = inf_data.asset or '' - timeframe = inf_data.timeframe - fmt = inf_data.fmt - config = strategy.config - - if asset: - # Insert stake currency if needed. - asset = _format_pair_name(config, asset) - else: - # Not specifying an asset will define informative dataframe for current pair. - asset = metadata['pair'] - - if '/' in asset: - base, quote = asset.split('/') - else: - # When futures are supported this may need reevaluation. - # base, quote = asset, None - raise OperationalException('Not implemented.') - - # Default format. This optimizes for the common case: informative pairs using same stake - # currency. When quote currency matches stake currency, column name will omit base currency. - # This allows easily reconfiguring strategy to use different base currency. In a rare case - # where it is desired to keep quote currency in column name at all times user should specify - # fmt='{base}_{quote}_{column}_{timeframe}' format or similar. - if not fmt: - fmt = '{column}_{timeframe}' # Informatives of current pair - if quote != config['stake_currency']: - fmt = '{quote}_' + fmt # Informatives of different quote currency - if inf_data.asset: - fmt = '{base}_' + fmt # Informatives of other pair - - inf_metadata = {'pair': asset, 'timeframe': timeframe} - inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe) - inf_dataframe = populate_indicators(strategy, inf_dataframe, inf_metadata) - - formatter: Any = None - if callable(fmt): - formatter = fmt # A custom user-specified formatter function. - else: - formatter = fmt.format # A default string formatter. - - fmt_args = { - 'BASE': base.upper(), - 'QUOTE': quote.upper(), - 'base': base.lower(), - 'quote': quote.lower(), - 'asset': asset, - 'timeframe': timeframe, - } - inf_dataframe.rename(columns=lambda column: formatter(column=column, **fmt_args), - inplace=True) - - date_column = formatter(column='date', **fmt_args) - if date_column in dataframe.columns: - raise OperationalException(f'Duplicate column name {date_column} exists in ' - f'dataframe! Ensure column names are unique!') - dataframe = merge_informative_pair(dataframe, inf_dataframe, strategy.timeframe, timeframe, - ffill=inf_data.ffill, append_timeframe=False, - date_column=date_column) - return dataframe From 7e6aa9390ae9c970f8b22afb591161492f833807 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 18 Sep 2021 10:17:50 +0300 Subject: [PATCH 084/102] [SQUASH] Unconditionally include quote currency when asset is explicitly specified. Added docs suggesting to use string formatting to make strategy independent of configured stake currency. --- docs/strategy-customization.md | 26 +++++++++++++++++---- freqtrade/strategy/informative_decorator.py | 11 +++------ tests/strategy/test_strategy_helpers.py | 2 +- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 671768bfa..2b22dd274 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -698,10 +698,7 @@ def informative(timeframe: str, asset: str = '', current pair. :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not specified, defaults to: - * {base}_{column}_{timeframe} if asset is specified and quote currency does match stake - currency. - * {base}_{quote}_{column}_{timeframe} if asset is specified and quote currency does not match - stake currency. + * {base}_{quote}_{column}_{timeframe} if asset is specified. * {column}_{timeframe} if asset is not specified. Format string supports these format variables: * {asset} - full name of the asset, for example 'BTC/USDT'. @@ -746,7 +743,7 @@ for more information. # Define BTC/STAKE informative pair. Available in populate_indicators and other methods as # 'btc_rsi_1h'. Current stake currency should be specified as {stake} format variable # instead of hardcoding actual stake currency. Available in populate_indicators and other - # methods as 'btc_rsi_1h'. + # methods as 'btc_usdt_rsi_1h' (when stake currency is USDT). @informative('1h', 'BTC/{stake}') def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14) @@ -780,6 +777,25 @@ for more information. Do not use `@informative` decorator if you need to use data of one informative pair when generating another informative pair. Instead, define informative pairs manually as described [in the DataProvider section](#complete-data-provider-sample). +!!! Note + Use string formatting when accessing informative dataframes of other pairs. This will allow easily changing stake currency in config without having to adjust strategy code. + + ``` python + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + stake = self.config['stake_currency'] + dataframe.loc[ + ( + (dataframe[f'btc_{stake}_rsi_1h'] < 35) + & + (dataframe['volume'] > 0) + ), + ['buy', 'buy_tag']] = (1, 'buy_signal_rsi') + + return dataframe + ``` + + Alternatively column renaming may be used to remove stake currency from column names: `@informative('1h', 'BTC/{stake}', fmt='{base}_{column}_{timeframe}')`. + !!! Warning "Duplicate method names" Methods tagged with `@informative()` decorator must always have unique names! Re-using same name (for example when copy-pasting already defined informative method) will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators diff --git a/freqtrade/strategy/informative_decorator.py b/freqtrade/strategy/informative_decorator.py index f09e634b0..c8ebf5989 100644 --- a/freqtrade/strategy/informative_decorator.py +++ b/freqtrade/strategy/informative_decorator.py @@ -36,10 +36,7 @@ def informative(timeframe: str, asset: str = '', current pair. :param fmt: Column format (str) or column formatter (callable(name, asset, timeframe)). When not specified, defaults to: - * {base}_{column}_{timeframe} if asset is specified and quote currency does match stake - currency. - * {base}_{quote}_{column}_{timeframe} if asset is specified and quote currency does not match - stake currency. + * {base}_{quote}_{column}_{timeframe} if asset is specified. * {column}_{timeframe} if asset is not specified. Format string supports these format variables: * {asset} - full name of the asset, for example 'BTC/USDT'. @@ -88,7 +85,7 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: base, quote = asset.split('/') else: # When futures are supported this may need reevaluation. - # base, quote = asset, None + # base, quote = asset, '' raise OperationalException('Not implemented.') # Default format. This optimizes for the common case: informative pairs using same stake @@ -98,10 +95,8 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata: # fmt='{base}_{quote}_{column}_{timeframe}' format or similar. if not fmt: fmt = '{column}_{timeframe}' # Informatives of current pair - if quote != config['stake_currency']: - fmt = '{quote}_' + fmt # Informatives of different quote currency if inf_data.asset: - fmt = '{base}_' + fmt # Informatives of other pair + fmt = '{base}_{quote}_' + fmt # Informatives of other pairs inf_metadata = {'pair': asset, 'timeframe': timeframe} inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe) diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 95ca0416f..d4206ba8c 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -177,7 +177,7 @@ def test_informative_decorator(mocker, default_conf): {p: data[(p, strategy.timeframe)] for p in ('XRP/USDT', 'LTC/USDT')}) expected_columns = [ 'rsi_1h', 'rsi_30m', # Stacked informative decorators - 'btc_rsi_1h', # BTC 1h informative + 'btc_usdt_rsi_1h', # BTC 1h informative 'rsi_BTC_USDT_btc_usdt_BTC/USDT_30m', # Column formatting 'rsi_from_callable', # Custom column formatter 'eth_btc_rsi_1h', # Quote currency not matching stake currency From e4ca42faec580a3c336298d42402fec0d8e56f93 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 18 Sep 2021 10:18:33 +0300 Subject: [PATCH 085/102] [SQUASH] Update stoploss_from_absolute to behave more like stoploss_from_open and add a test for it. --- freqtrade/strategy/strategy_helper.py | 16 +++++++++++++++- tests/strategy/test_strategy_helpers.py | 11 ++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/freqtrade/strategy/strategy_helper.py b/freqtrade/strategy/strategy_helper.py index f813b39c5..175bcaccb 100644 --- a/freqtrade/strategy/strategy_helper.py +++ b/freqtrade/strategy/strategy_helper.py @@ -97,8 +97,22 @@ def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float: """ Given current price and desired stop price, return a stop loss value that is relative to current price. + + The requested stop can be positive for a stop above the open price, or negative for + a stop below the open price. The return value is always >= 0. + + Returns 0 if the resulting stop price would be above the current price. + :param stop_rate: Stop loss price. :param current_rate: Current asset price. :return: Positive stop loss value relative to current price """ - return 1 - (stop_rate / current_rate) + + # formula is undefined for current_rate 0, return maximum value + if current_rate == 0: + return 1 + + stoploss = 1 - (stop_rate / current_rate) + + # negative stoploss values indicate the requested stop price is higher than the current price + return max(stoploss, 0.0) diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index d4206ba8c..9132382fa 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -5,7 +5,8 @@ import pandas as pd import pytest from freqtrade.data.dataprovider import DataProvider -from freqtrade.strategy import merge_informative_pair, stoploss_from_open, timeframe_to_minutes +from freqtrade.strategy import (merge_informative_pair, stoploss_from_open, timeframe_to_minutes, + stoploss_from_absolute) def generate_test_data(timeframe: str, size: int): @@ -135,6 +136,14 @@ def test_stoploss_from_open(): assert isclose(stop_price, expected_stop_price, rel_tol=0.00001) +def test_stoploss_from_absolute(): + assert stoploss_from_absolute(90, 100) == 1 - (90 / 100) + assert stoploss_from_absolute(100, 100) == 0 + assert stoploss_from_absolute(110, 100) == 0 + assert stoploss_from_absolute(100, 0) == 1 + assert stoploss_from_absolute(0, 100) == 1 + + def test_informative_decorator(mocker, default_conf): test_data_5m = generate_test_data('5m', 40) test_data_30m = generate_test_data('30m', 40) From 216f75bbb91b8d72790f386dfc8b05340ade3c3d Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 18 Sep 2021 02:53:34 -0600 Subject: [PATCH 086/102] parametrized test_sell_profit_only , test__safe_exit_amount, test_order_book_bid_strategy1 --- tests/test_freqtradebot.py | 244 ++++++++++--------------------------- 1 file changed, 65 insertions(+), 179 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index eb3c77cc7..5268d40ec 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -3128,16 +3128,28 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf, ticker, fee, assert mock_insuf.call_count == 1 -def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy_order_open, - fee, mocker) -> None: +@pytest.mark.parametrize('profit_only,bid,ask,handle_first,handle_second,sell_type', [ + # Enable profit + (True, 0.00001172, 0.00001173, False, True, SellType.SELL_SIGNAL.value), + # Disable profit + (False, 0.00002172, 0.00002173, True, False, SellType.SELL_SIGNAL.value), + # Enable loss + # * Shouldn't this be SellType.STOP_LOSS.value + (True, 0.00000172, 0.00000173, False, False, None), + # Disable loss + (False, 0.00000172, 0.00000173, True, False, SellType.SELL_SIGNAL.value), +]) +def test_sell_profit_only_enable_profit( + default_conf, limit_buy_order, limit_buy_order_open, + fee, mocker, profit_only, bid, ask, handle_first, handle_second, sell_type) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=MagicMock(return_value={ - 'bid': 0.00001172, - 'ask': 0.00001173, - 'last': 0.00001172 + 'bid': bid, + 'ask': ask, + 'last': bid }), create_order=MagicMock(side_effect=[ limit_buy_order_open, @@ -3147,128 +3159,29 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy ) default_conf.update({ 'use_sell_signal': True, - 'sell_profit_only': True, + 'sell_profit_only': profit_only, 'sell_profit_offset': 0.1, }) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - + if sell_type == SellType.SELL_SIGNAL.value: + freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) + else: + freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple( + sell_type=SellType.NONE)) freqtrade.enter_positions() trade = Trade.query.first() trade.update(limit_buy_order) freqtrade.wallets.update() patch_get_signal(freqtrade, value=(False, True, None)) - assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_trade(trade) is handle_first - freqtrade.strategy.sell_profit_offset = 0.0 - assert freqtrade.handle_trade(trade) is True + if handle_second: + freqtrade.strategy.sell_profit_offset = 0.0 + assert freqtrade.handle_trade(trade) is True - assert trade.sell_reason == SellType.SELL_SIGNAL.value - - -def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, limit_buy_order_open, - fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=MagicMock(return_value={ - 'bid': 0.00002172, - 'ask': 0.00002173, - 'last': 0.00002172 - }), - create_order=MagicMock(side_effect=[ - limit_buy_order_open, - {'id': 1234553382}, - ]), - get_fee=fee, - ) - default_conf.update({ - 'use_sell_signal': True, - 'sell_profit_only': False, - }) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.enter_positions() - - trade = Trade.query.first() - trade.update(limit_buy_order) - freqtrade.wallets.update() - patch_get_signal(freqtrade, value=(False, True, None)) - assert freqtrade.handle_trade(trade) is True - assert trade.sell_reason == SellType.SELL_SIGNAL.value - - -def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_order_open, - fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=MagicMock(return_value={ - 'bid': 0.00000172, - 'ask': 0.00000173, - 'last': 0.00000172 - }), - create_order=MagicMock(side_effect=[ - limit_buy_order_open, - {'id': 1234553382}, - ]), - get_fee=fee, - ) - default_conf.update({ - 'use_sell_signal': True, - 'sell_profit_only': True, - }) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - freqtrade.strategy.stop_loss_reached = MagicMock(return_value=SellCheckTuple( - sell_type=SellType.NONE)) - freqtrade.enter_positions() - - trade = Trade.query.first() - trade.update(limit_buy_order) - patch_get_signal(freqtrade, value=(False, True, None)) - assert freqtrade.handle_trade(trade) is False - - -def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, limit_buy_order_open, - fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=MagicMock(return_value={ - 'bid': 0.0000172, - 'ask': 0.0000173, - 'last': 0.0000172 - }), - create_order=MagicMock(side_effect=[ - limit_buy_order_open, - {'id': 1234553382}, - ]), - get_fee=fee, - ) - default_conf.update({ - 'use_sell_signal': True, - 'sell_profit_only': False, - }) - - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - - freqtrade.enter_positions() - - trade = Trade.query.first() - trade.update(limit_buy_order) - freqtrade.wallets.update() - patch_get_signal(freqtrade, value=(False, True, None)) - assert freqtrade.handle_trade(trade) is True - assert trade.sell_reason == SellType.SELL_SIGNAL.value + assert trade.sell_reason == sell_type def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_open, @@ -3306,11 +3219,15 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_ assert trade.amount != amnt -def test__safe_exit_amount(default_conf, fee, caplog, mocker): +@pytest.mark.parametrize('amount_wallet,has_err', [ + (95.29, False), + (91.29, True) +]) +def test__safe_exit_amount(default_conf, fee, caplog, mocker, amount_wallet, has_err): patch_RPCManager(mocker) patch_exchange(mocker) amount = 95.33 - amount_wallet = 95.29 + amount_wallet = amount_wallet mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet)) wallet_update = mocker.patch('freqtrade.wallets.Wallets.update') trade = Trade( @@ -3324,37 +3241,19 @@ def test__safe_exit_amount(default_conf, fee, caplog, mocker): ) freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) - - wallet_update.reset_mock() - assert freqtrade._safe_exit_amount(trade.pair, trade.amount) == amount_wallet - assert log_has_re(r'.*Falling back to wallet-amount.', caplog) - assert wallet_update.call_count == 1 - caplog.clear() - wallet_update.reset_mock() - assert freqtrade._safe_exit_amount(trade.pair, amount_wallet) == amount_wallet - assert not log_has_re(r'.*Falling back to wallet-amount.', caplog) - assert wallet_update.call_count == 1 - - -def test__safe_exit_amount_error(default_conf, fee, caplog, mocker): - patch_RPCManager(mocker) - patch_exchange(mocker) - amount = 95.33 - amount_wallet = 91.29 - mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=amount_wallet)) - trade = Trade( - pair='LTC/ETH', - amount=amount, - exchange='binance', - open_rate=0.245441, - open_order_id="123456", - fee_open=fee.return_value, - fee_close=fee.return_value, - ) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - with pytest.raises(DependencyException, match=r"Not enough amount to sell."): - assert freqtrade._safe_exit_amount(trade.pair, trade.amount) + if has_err: + with pytest.raises(DependencyException, match=r"Not enough amount to sell."): + assert freqtrade._safe_exit_amount(trade.pair, trade.amount) + else: + wallet_update.reset_mock() + assert freqtrade._safe_exit_amount(trade.pair, trade.amount) == amount_wallet + assert log_has_re(r'.*Falling back to wallet-amount.', caplog) + assert wallet_update.call_count == 1 + caplog.clear() + wallet_update.reset_mock() + assert freqtrade._safe_exit_amount(trade.pair, amount_wallet) == amount_wallet + assert not log_has_re(r'.*Falling back to wallet-amount.', caplog) + assert wallet_update.call_count == 1 def test_locked_pairs(default_conf, ticker, fee, ticker_sell_down, mocker, caplog) -> None: @@ -4131,50 +4030,37 @@ def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_o assert trade is None -def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2) -> None: +@pytest.mark.parametrize('exception_thrown,ask,last,order_book_top,order_book', [ + (False, 0.045, 0.046, 2, None), + (True, 0.042, 0.046, 1, {'bids': [[]], 'asks': [[]]}) +]) +def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2, exception_thrown, + ask, last, order_book_top, order_book, caplog) -> None: """ - test if function get_rate will return the order book price - instead of the ask rate + test if function get_rate will return the order book price instead of the ask rate """ patch_exchange(mocker) - ticker_mock = MagicMock(return_value={'ask': 0.045, 'last': 0.046}) + ticker_mock = MagicMock(return_value={'ask': ask, 'last': last}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', - fetch_l2_order_book=order_book_l2, + fetch_l2_order_book=MagicMock(return_value=order_book) if order_book else order_book_l2, fetch_ticker=ticker_mock, - ) default_conf['exchange']['name'] = 'binance' default_conf['bid_strategy']['use_order_book'] = True - default_conf['bid_strategy']['order_book_top'] = 2 + default_conf['bid_strategy']['order_book_top'] = order_book_top default_conf['bid_strategy']['ask_last_balance'] = 0 default_conf['telegram']['enabled'] = False freqtrade = FreqtradeBot(default_conf) - assert freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") == 0.043935 - assert ticker_mock.call_count == 0 - - -def test_order_book_bid_strategy_exception(mocker, default_conf, caplog) -> None: - patch_exchange(mocker) - ticker_mock = MagicMock(return_value={'ask': 0.042, 'last': 0.046}) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_l2_order_book=MagicMock(return_value={'bids': [[]], 'asks': [[]]}), - fetch_ticker=ticker_mock, - - ) - default_conf['exchange']['name'] = 'binance' - default_conf['bid_strategy']['use_order_book'] = True - default_conf['bid_strategy']['order_book_top'] = 1 - default_conf['bid_strategy']['ask_last_balance'] = 0 - default_conf['telegram']['enabled'] = False - - freqtrade = FreqtradeBot(default_conf) - # orderbook shall be used even if tickers would be lower. - with pytest.raises(PricingError): - freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") - assert log_has_re(r'Buy Price at location 1 from orderbook could not be determined.', caplog) + if exception_thrown: + with pytest.raises(PricingError): + freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") + assert log_has_re( + r'Buy Price at location 1 from orderbook could not be determined.', caplog) + else: + assert freqtrade.exchange.get_rate('ETH/BTC', refresh=True, side="buy") == 0.043935 + assert ticker_mock.call_count == 0 def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2) -> None: From 42a2fdc1c520c97b27cf937e25aa7bd85a581a21 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 18 Sep 2021 03:01:08 -0600 Subject: [PATCH 087/102] parametrized test_order_dict --- tests/test_freqtradebot.py | 47 ++++++++------------------------------ 1 file changed, 10 insertions(+), 37 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 5268d40ec..8e8ae4da2 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -78,11 +78,15 @@ def test_bot_cleanup(mocker, default_conf, caplog) -> None: assert coo_mock.call_count == 1 -def test_order_dict_dry_run(default_conf, mocker, caplog) -> None: +@pytest.mark.parametrize('runmode', [ + RunMode.DRY_RUN, + RunMode.LIVE +]) +def test_order_dict(default_conf, mocker, runmode, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) conf = default_conf.copy() - conf['runmode'] = RunMode.DRY_RUN + conf['runmode'] = runmode conf['order_types'] = { 'buy': 'market', 'sell': 'limit', @@ -92,45 +96,14 @@ def test_order_dict_dry_run(default_conf, mocker, caplog) -> None: conf['bid_strategy']['price_side'] = 'ask' freqtrade = FreqtradeBot(conf) + if runmode == RunMode.LIVE: + assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog) assert freqtrade.strategy.order_types['stoploss_on_exchange'] caplog.clear() # is left untouched conf = default_conf.copy() - conf['runmode'] = RunMode.DRY_RUN - conf['order_types'] = { - 'buy': 'market', - 'sell': 'limit', - 'stoploss': 'limit', - 'stoploss_on_exchange': False, - } - freqtrade = FreqtradeBot(conf) - assert not freqtrade.strategy.order_types['stoploss_on_exchange'] - assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog) - - -def test_order_dict_live(default_conf, mocker, caplog) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - - conf = default_conf.copy() - conf['runmode'] = RunMode.LIVE - conf['order_types'] = { - 'buy': 'market', - 'sell': 'limit', - 'stoploss': 'limit', - 'stoploss_on_exchange': True, - } - conf['bid_strategy']['price_side'] = 'ask' - - freqtrade = FreqtradeBot(conf) - assert not log_has_re(".*stoploss_on_exchange .* dry-run", caplog) - assert freqtrade.strategy.order_types['stoploss_on_exchange'] - - caplog.clear() - # is left untouched - conf = default_conf.copy() - conf['runmode'] = RunMode.LIVE + conf['runmode'] = runmode conf['order_types'] = { 'buy': 'market', 'sell': 'limit', @@ -3139,7 +3112,7 @@ def test_execute_trade_exit_insufficient_funds_error(default_conf, ticker, fee, # Disable loss (False, 0.00000172, 0.00000173, True, False, SellType.SELL_SIGNAL.value), ]) -def test_sell_profit_only_enable_profit( +def test_sell_profit_only( default_conf, limit_buy_order, limit_buy_order_open, fee, mocker, profit_only, bid, ask, handle_first, handle_second, sell_type) -> None: patch_RPCManager(mocker) From 518a59ad41daa50c51deb8d7e70e75b3b9a8bad0 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 18 Sep 2021 03:20:00 -0600 Subject: [PATCH 088/102] parametrized test_edge_overrides_stoploss --- tests/test_freqtradebot.py | 60 ++++++++++---------------------------- 1 file changed, 15 insertions(+), 45 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 8e8ae4da2..debe2bee0 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -192,8 +192,14 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None: 'LTC/BTC', freqtrade.edge) == (999.9 * 0.5 * 0.01) / 0.21 -def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf) -> None: - +@pytest.mark.parametrize('buy_price_mult,ignore_strat_sl', [ + # Override stoploss + (0.79, False), + # Override strategy stoploss + (0.85, True) +]) +def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, + buy_price_mult, ignore_strat_sl, edge_conf) -> None: patch_RPCManager(mocker) patch_exchange(mocker) patch_edge(mocker) @@ -207,9 +213,9 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=MagicMock(return_value={ - 'bid': buy_price * 0.79, - 'ask': buy_price * 0.79, - 'last': buy_price * 0.79 + 'bid': buy_price * buy_price_mult, + 'ask': buy_price * buy_price_mult, + 'last': buy_price * buy_price_mult, }), get_fee=fee, ) @@ -226,46 +232,10 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf ############################################# # stoploss shoud be hit - assert freqtrade.handle_trade(trade) is True - assert log_has('Executing Sell for NEO/BTC. Reason: stop_loss', caplog) - assert trade.sell_reason == SellType.STOP_LOSS.value - - -def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, - mocker, edge_conf) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - patch_edge(mocker) - edge_conf['max_open_trades'] = float('inf') - - # Strategy stoploss is -0.1 but Edge imposes a stoploss at -0.2 - # Thus, if price falls 15%, stoploss should not be triggered - # - # mocking the ticker: price is falling ... - buy_price = limit_buy_order['price'] - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=MagicMock(return_value={ - 'bid': buy_price * 0.85, - 'ask': buy_price * 0.85, - 'last': buy_price * 0.85 - }), - get_fee=fee, - ) - ############################################# - - # Create a trade with "limit_buy_order" price - freqtrade = FreqtradeBot(edge_conf) - freqtrade.active_pair_whitelist = ['NEO/BTC'] - patch_get_signal(freqtrade) - freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) - freqtrade.enter_positions() - trade = Trade.query.first() - trade.update(limit_buy_order) - ############################################# - - # stoploss shoud not be hit - assert freqtrade.handle_trade(trade) is False + assert freqtrade.handle_trade(trade) is not ignore_strat_sl + if not ignore_strat_sl: + assert log_has('Executing Sell for NEO/BTC. Reason: stop_loss', caplog) + assert trade.sell_reason == SellType.STOP_LOSS.value def test_total_open_trades_stakes(mocker, default_conf, ticker, fee) -> None: From 979c6f2f263404ae78f4002262e7a2b3cc8497b3 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 18 Sep 2021 03:49:15 -0600 Subject: [PATCH 089/102] moved leverage_brackets.json to exchange/binance_leverage_brackets.json --- freqtrade/exchange/binance.py | 6 ++++-- .../binance_leverage_brackets.json} | 0 2 files changed, 4 insertions(+), 2 deletions(-) rename freqtrade/{data/leverage_brackets.json => exchange/binance_leverage_brackets.json} (100%) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 60a1b8019..69d781395 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -149,7 +149,9 @@ class Binance(Exchange): if self.trading_mode == TradingMode.FUTURES: try: if self._config['dry_run']: - leverage_brackets_path = Path('freqtrade/data') / 'leverage_brackets.json' + leverage_brackets_path = ( + Path(__file__).parent / 'binance_leverage_brackets.json' + ) with open(leverage_brackets_path) as json_file: leverage_brackets = json.load(json_file) else: @@ -187,7 +189,7 @@ class Binance(Exchange): max_lev = 1/margin_req return max_lev - @retrier + @ retrier def _set_leverage( self, leverage: float, diff --git a/freqtrade/data/leverage_brackets.json b/freqtrade/exchange/binance_leverage_brackets.json similarity index 100% rename from freqtrade/data/leverage_brackets.json rename to freqtrade/exchange/binance_leverage_brackets.json From 713e7819f7e534a36984f9f4c76a11bbfd948896 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 18 Sep 2021 15:27:58 +0300 Subject: [PATCH 090/102] [SQUASH] Remove mypy import. --- freqtrade/strategy/informative_decorator.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/strategy/informative_decorator.py b/freqtrade/strategy/informative_decorator.py index c8ebf5989..4c5f21108 100644 --- a/freqtrade/strategy/informative_decorator.py +++ b/freqtrade/strategy/informative_decorator.py @@ -1,6 +1,5 @@ from typing import Any, Callable, NamedTuple, Optional, Union -from mypy_extensions import KwArg from pandas import DataFrame from freqtrade.exceptions import OperationalException @@ -13,12 +12,12 @@ PopulateIndicators = Callable[[Any, DataFrame, dict], DataFrame] class InformativeData(NamedTuple): asset: Optional[str] timeframe: str - fmt: Union[str, Callable[[KwArg(str)], str], None] + fmt: Union[str, Callable[[Any], str], None] ffill: bool def informative(timeframe: str, asset: str = '', - fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None, + fmt: Optional[Union[str, Callable[[Any], str]]] = None, ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]: """ A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to From eab7f8f6944e77fa7ae69bf99fe885aa76aea5a5 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Sat, 18 Sep 2021 15:44:21 +0300 Subject: [PATCH 091/102] [SQUASH] Doh. --- tests/strategy/test_strategy_helpers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 9132382fa..a01b55050 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -5,8 +5,8 @@ import pandas as pd import pytest from freqtrade.data.dataprovider import DataProvider -from freqtrade.strategy import (merge_informative_pair, stoploss_from_open, timeframe_to_minutes, - stoploss_from_absolute) +from freqtrade.strategy import (merge_informative_pair, stoploss_from_absolute, stoploss_from_open, + timeframe_to_minutes) def generate_test_data(timeframe: str, size: int): From c1895a0fc230f14e9de79a3042d77d3379a61c1f Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 18 Sep 2021 15:30:33 +0200 Subject: [PATCH 092/102] Remove warning related to legacy hyperopt --- docs/strategy-customization.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/strategy-customization.md b/docs/strategy-customization.md index 2b22dd274..725252b30 100644 --- a/docs/strategy-customization.md +++ b/docs/strategy-customization.md @@ -801,9 +801,6 @@ for more information. will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique! -!!! Warning - When using a legacy hyperopt implementation informative pairs defined with a decorator will not be executed. Please update your strategy if necessary. - ## Additional data (Wallets) The strategy provides access to the `Wallets` object. This contains the current balances on the exchange. From ec9dbc550e66c2694bfe32a43c3442952d8b685c Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 18 Sep 2021 19:19:53 -0600 Subject: [PATCH 093/102] parametrized test_create_trade_minimal_amount --- tests/test_freqtradebot.py | 93 +++++++++++--------------------------- 1 file changed, 26 insertions(+), 67 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index debe2bee0..209c8a2a2 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -319,8 +319,16 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, freqtrade.create_trade('ETH/BTC') -def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order_open, - fee, mocker) -> None: +@pytest.mark.parametrize('stake_amount,create,amount_enough,max_open_trades', [ + (0.0005, True, True, 99), + (0.000000005, True, False, 99), + (0, False, True, 99), + (UNLIMITED_STAKE_AMOUNT, False, True, 0), +]) +def test_create_trade_minimal_amount( + default_conf, ticker, limit_buy_order_open, fee, mocker, + stake_amount, create, amount_enough, max_open_trades, caplog +) -> None: patch_RPCManager(mocker) patch_exchange(mocker) buy_mock = MagicMock(return_value=limit_buy_order_open) @@ -330,74 +338,25 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order_open, create_order=buy_mock, get_fee=fee, ) - default_conf['stake_amount'] = 0.0005 + default_conf['max_open_trades'] = max_open_trades freqtrade = FreqtradeBot(default_conf) + freqtrade.config['stake_amount'] = stake_amount patch_get_signal(freqtrade) - freqtrade.create_trade('ETH/BTC') - rate, amount = buy_mock.call_args[1]['rate'], buy_mock.call_args[1]['amount'] - assert rate * amount <= default_conf['stake_amount'] - - -def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order_open, - fee, mocker, caplog) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - buy_mock = MagicMock(return_value=limit_buy_order_open) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - create_order=buy_mock, - get_fee=fee, - ) - - freqtrade = FreqtradeBot(default_conf) - freqtrade.config['stake_amount'] = 0.000000005 - - patch_get_signal(freqtrade) - - assert freqtrade.create_trade('ETH/BTC') - assert log_has_re(r"Stake amount for pair .* is too small.*", caplog) - - -def test_create_trade_zero_stake_amount(default_conf, ticker, limit_buy_order_open, - fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - buy_mock = MagicMock(return_value=limit_buy_order_open) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - create_order=buy_mock, - get_fee=fee, - ) - - freqtrade = FreqtradeBot(default_conf) - freqtrade.config['stake_amount'] = 0 - - patch_get_signal(freqtrade) - - assert not freqtrade.create_trade('ETH/BTC') - - -def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order_open, - fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - create_order=MagicMock(return_value=limit_buy_order_open), - get_fee=fee, - ) - default_conf['max_open_trades'] = 0 - default_conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT - - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - assert not freqtrade.create_trade('ETH/BTC') - assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0 + if create: + assert freqtrade.create_trade('ETH/BTC') + if amount_enough: + rate, amount = buy_mock.call_args[1]['rate'], buy_mock.call_args[1]['amount'] + assert rate * amount <= default_conf['stake_amount'] + else: + assert log_has_re( + r"Stake amount for pair .* is too small.*", + caplog + ) + else: + assert not freqtrade.create_trade('ETH/BTC') + if not max_open_trades: + assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0 def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee, From cee4ed541b8e4bec16ce8a23598531fb4d64ec57 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sat, 18 Sep 2021 19:46:46 -0600 Subject: [PATCH 094/102] parametrized test_update_trade_state_withorderdict --- tests/test_freqtradebot.py | 104 +++++++++++++------------------------ 1 file changed, 35 insertions(+), 69 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 209c8a2a2..72d1f6150 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -359,8 +359,12 @@ def test_create_trade_minimal_amount( assert freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.edge) == 0 +@pytest.mark.parametrize('whitelist,positions', [ + (["ETH/BTC"], 1), # No pairs left + ([], 0), # No pairs in whitelist +]) def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_open, fee, - mocker, caplog) -> None: + whitelist, positions, mocker, caplog) -> None: patch_RPCManager(mocker) patch_exchange(mocker) mocker.patch.multiple( @@ -369,36 +373,20 @@ def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_ope create_order=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) - - default_conf['exchange']['pair_whitelist'] = ["ETH/BTC"] + default_conf['exchange']['pair_whitelist'] = whitelist freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) n = freqtrade.enter_positions() - assert n == 1 - assert not log_has_re(r"No currency pair in active pair whitelist.*", caplog) - n = freqtrade.enter_positions() - assert n == 0 - assert log_has_re(r"No currency pair in active pair whitelist.*", caplog) - - -def test_enter_positions_no_pairs_in_whitelist(default_conf, ticker, limit_buy_order, fee, - mocker, caplog) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - create_order=MagicMock(return_value={'id': limit_buy_order['id']}), - get_fee=fee, - ) - default_conf['exchange']['pair_whitelist'] = [] - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - n = freqtrade.enter_positions() - assert n == 0 - assert log_has("Active pair whitelist is empty.", caplog) + assert n == positions + if positions: + assert not log_has_re(r"No currency pair in active pair whitelist.*", caplog) + n = freqtrade.enter_positions() + assert n == 0 + assert log_has_re(r"No currency pair in active pair whitelist.*", caplog) + else: + assert n == 0 + assert log_has("Active pair whitelist is empty.", caplog) @pytest.mark.usefixtures("init_persistence") @@ -1555,30 +1543,27 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, stop_price=0.00002346 * 0.99) -def test_enter_positions(mocker, default_conf, caplog) -> None: +@pytest.mark.parametrize('return_value,side_effect,log_message', [ + (False, None, 'Found no buy signals for whitelisted currencies. Trying again...'), + (None, DependencyException, 'Unable to create trade for ETH/BTC: ') +]) +def test_enter_positions(mocker, default_conf, return_value, side_effect, + log_message, caplog) -> None: caplog.set_level(logging.DEBUG) freqtrade = get_patched_freqtradebot(mocker, default_conf) - mock_ct = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trade', - MagicMock(return_value=False)) - n = freqtrade.enter_positions() - assert n == 0 - assert log_has('Found no buy signals for whitelisted currencies. Trying again...', caplog) - # create_trade should be called once for every pair in the whitelist. - assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist']) - - -def test_enter_positions_exception(mocker, default_conf, caplog) -> None: - freqtrade = get_patched_freqtradebot(mocker, default_conf) - mock_ct = mocker.patch( 'freqtrade.freqtradebot.FreqtradeBot.create_trade', - MagicMock(side_effect=DependencyException) + MagicMock( + return_value=return_value, + side_effect=side_effect + ) ) n = freqtrade.enter_positions() assert n == 0 + assert log_has(log_message, caplog) + # create_trade should be called once for every pair in the whitelist. assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist']) - assert log_has('Unable to create trade for ETH/BTC: ', caplog) def test_exit_positions(mocker, default_conf, limit_buy_order, caplog) -> None: @@ -1672,8 +1657,13 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No assert log_has_re('Found open order for.*', caplog) +@pytest.mark.parametrize('initial_amount,has_rounding_fee', [ + (90.99181073 + 1e-14, True), + (8.0, False) +]) def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_buy_order, fee, - mocker): + mocker, initial_amount, has_rounding_fee, caplog): + trades_for_order[0]['amount'] = initial_amount mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) # fetch_order should not be called!! mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) @@ -1694,32 +1684,8 @@ def test_update_trade_state_withorderdict(default_conf, trades_for_order, limit_ freqtrade.update_trade_state(trade, '123456', limit_buy_order) assert trade.amount != amount assert trade.amount == limit_buy_order['amount'] - - -def test_update_trade_state_withorderdict_rounding_fee(default_conf, trades_for_order, fee, - limit_buy_order, mocker, caplog): - trades_for_order[0]['amount'] = limit_buy_order['amount'] + 1e-14 - mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) - # fetch_order should not be called!! - mocker.patch('freqtrade.exchange.Exchange.fetch_order', MagicMock(side_effect=ValueError)) - patch_exchange(mocker) - amount = sum(x['amount'] for x in trades_for_order) - freqtrade = get_patched_freqtradebot(mocker, default_conf) - trade = Trade( - pair='LTC/ETH', - amount=amount, - exchange='binance', - open_rate=0.245441, - fee_open=fee.return_value, - fee_close=fee.return_value, - open_order_id='123456', - is_open=True, - open_date=arrow.utcnow().datetime, - ) - freqtrade.update_trade_state(trade, '123456', limit_buy_order) - assert trade.amount != amount - assert trade.amount == limit_buy_order['amount'] - assert log_has_re(r'Applying fee on amount for .*', caplog) + if has_rounding_fee: + assert log_has_re(r'Applying fee on amount for .*', caplog) def test_update_trade_state_exception(mocker, default_conf, From c54259b4c55598bbc8579bc4f65d322f7180faac Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Sun, 19 Sep 2021 11:35:29 +0530 Subject: [PATCH 095/102] Added ftx interest formula tests --- tests/leverage/{test_leverage.py => test_interest.py} | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) rename tests/leverage/{test_leverage.py => test_interest.py} (83%) diff --git a/tests/leverage/test_leverage.py b/tests/leverage/test_interest.py similarity index 83% rename from tests/leverage/test_leverage.py rename to tests/leverage/test_interest.py index 7b7ca0f9b..c7e787bdb 100644 --- a/tests/leverage/test_leverage.py +++ b/tests/leverage/test_interest.py @@ -22,9 +22,10 @@ twentyfive_hours = Decimal(25.0) ('kraken', 0.00025, five_hours, 0.045), ('kraken', 0.00025, twentyfive_hours, 0.12), # FTX - # TODO-lev: - implement FTX tests - # ('ftx', Decimal(0.0005), ten_mins, 0.06), - # ('ftx', Decimal(0.0005), five_hours, 0.045), + ('ftx', 0.0005, ten_mins, 0.00125), + ('ftx', 0.00025, ten_mins, 0.000625), + ('ftx', 0.00025, five_hours, 0.003125), + ('ftx', 0.00025, twentyfive_hours, 0.015625), ]) def test_interest(exchange, interest_rate, hours, expected): borrowed = Decimal(60.0) From 27bd30d266b121c0ead4c08c302dfa776e5596bd Mon Sep 17 00:00:00 2001 From: Arunavo Ray Date: Sun, 19 Sep 2021 11:42:29 +0530 Subject: [PATCH 096/102] fixed formatting issues --- freqtrade/leverage/interest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/leverage/interest.py b/freqtrade/leverage/interest.py index c687c8b5b..2878ad784 100644 --- a/freqtrade/leverage/interest.py +++ b/freqtrade/leverage/interest.py @@ -40,4 +40,4 @@ def interest( # https://help.ftx.com/hc/en-us/articles/360053007671-Spot-Margin-Trading-Explainer return borrowed * rate * ceil(hours)/twenty_four else: - raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") \ No newline at end of file + raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade") From 835e0e69fcabadc658327950baac6fcb3c4dcfc5 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 19 Sep 2021 02:23:05 -0600 Subject: [PATCH 097/102] removed leverage from create order api call --- docs/leverage.md | 4 ++++ freqtrade/exchange/binance.py | 11 ++--------- freqtrade/exchange/ftx.py | 10 ++-------- freqtrade/exchange/kraken.py | 11 ++--------- 4 files changed, 10 insertions(+), 26 deletions(-) diff --git a/docs/leverage.md b/docs/leverage.md index c4b975a0b..9448c64c3 100644 --- a/docs/leverage.md +++ b/docs/leverage.md @@ -15,3 +15,7 @@ For longs, the currency which pays the interest fee for the `borrowed` will alre Rollover fee = P (borrowed money) * R (quat_hourly_interest) * ceiling(T/4) (in hours) I (interest) = Opening fee + Rollover fee [source](https://support.kraken.com/hc/en-us/articles/206161568-What-are-the-fees-for-margin-trading-) + +# TODO-lev: Mention that says you can't run 2 bots on the same account with leverage, + +#TODO-lev: Create a huge risk disclaimer \ No newline at end of file diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 69d781395..7d83e971b 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -107,15 +107,8 @@ class Binance(Exchange): rate = self.price_to_precision(pair, rate) - order = self._api.create_order( - symbol=pair, - type=ordertype, - side=side, - amount=amount, - price=rate, - params=params, - leverage=leverage - ) + order = self._api.create_order(symbol=pair, type=ordertype, side=side, + amount=amount, price=rate, params=params) logger.info('stoploss limit order added for %s. ' 'stop price: %s. limit: %s', pair, stop_price, rate) self._log_exchange_response('create_stoploss_order', order) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index eaf9a0477..0f572dee9 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -81,14 +81,8 @@ class Ftx(Exchange): params['stopPrice'] = stop_price amount = self.amount_to_precision(pair, amount) - order = self._api.create_order( - symbol=pair, - type=ordertype, - side=side, - amount=amount, - leverage=leverage, - params=params - ) + order = self._api.create_order(symbol=pair, type=ordertype, side=side, + amount=amount, params=params) self._log_exchange_response('create_stoploss_order', order) logger.info('stoploss order added for %s. ' 'stop price: %s.', pair, stop_price) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index d6a816c9e..ec49c963f 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -114,15 +114,8 @@ class Kraken(Exchange): try: amount = self.amount_to_precision(pair, amount) - order = self._api.create_order( - symbol=pair, - type=ordertype, - side=side, - amount=amount, - price=stop_price, - leverage=leverage, - params=params - ) + order = self._api.create_order(symbol=pair, type=ordertype, side=side, + amount=amount, price=stop_price, params=params) self._log_exchange_response('create_stoploss_order', order) logger.info('stoploss order added for %s. ' 'stop price: %s.', pair, stop_price) From fa74b95a013e44ce1289b2c3cc231a75c54601d4 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 19 Sep 2021 02:33:28 -0600 Subject: [PATCH 098/102] reduced amount of code for leverage_brackets test --- tests/exchange/test_binance.py | 40 +- tests/leverage_brackets.py | 1215 -------------------------------- 2 files changed, 38 insertions(+), 1217 deletions(-) delete mode 100644 tests/leverage_brackets.py diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index cbbace1db..0c3e86fdd 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -9,7 +9,6 @@ from freqtrade.enums import Collateral, TradingMode from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException from tests.conftest import get_mock_coro, get_patched_exchange, log_has_re from tests.exchange.test_exchange import ccxt_exceptionhandlers -from tests.leverage_brackets import leverage_brackets @pytest.mark.parametrize('limitratio,expected,side', [ @@ -281,7 +280,44 @@ def test_fill_leverage_brackets_binance_dryrun(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf, api_mock, id="binance") exchange.fill_leverage_brackets() - assert exchange._leverage_brackets == leverage_brackets() + leverage_brackets = { + "1000SHIB/USDT": [ + [0.0, 0.01], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "1INCH/USDT": [ + [0.0, 0.012], + [5000.0, 0.025], + [25000.0, 0.05], + [100000.0, 0.1], + [250000.0, 0.125], + [1000000.0, 0.5] + ], + "AAVE/USDT": [ + [0.0, 0.01], + [50000.0, 0.02], + [250000.0, 0.05], + [1000000.0, 0.1], + [2000000.0, 0.125], + [5000000.0, 0.1665], + [10000000.0, 0.25] + ], + "ADA/BUSD": [ + [0.0, 0.025], + [100000.0, 0.05], + [500000.0, 0.1], + [1000000.0, 0.15], + [2000000.0, 0.25], + [5000000.0, 0.5] + ] + } + + for key, value in leverage_brackets.items(): + assert exchange._leverage_brackets[key] == value def test__set_leverage_binance(mocker, default_conf): diff --git a/tests/leverage_brackets.py b/tests/leverage_brackets.py deleted file mode 100644 index aa60a7af2..000000000 --- a/tests/leverage_brackets.py +++ /dev/null @@ -1,1215 +0,0 @@ -def leverage_brackets(): - return { - "1000SHIB/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "1INCH/USDT": [ - [0.0, 0.012], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "AAVE/USDT": [ - [0.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.1665], - [10000000.0, 0.25] - ], - "ADA/BUSD": [ - [0.0, 0.025], - [100000.0, 0.05], - [500000.0, 0.1], - [1000000.0, 0.15], - [2000000.0, 0.25], - [5000000.0, 0.5] - ], - "ADA/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "AKRO/USDT": [ - [0.0, 0.012], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ALGO/USDT": [ - [0.0, 0.01], - [50000.0, 0.025], - [150000.0, 0.05], - [250000.0, 0.1], - [500000.0, 0.125], - [1000000.0, 0.25], - [2000000.0, 0.5] - ], - "ALICE/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ALPHA/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ANKR/USDT": [ - [0.0, 0.012], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ATA/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ATOM/USDT": [ - [0.0, 0.01], - [50000.0, 0.025], - [150000.0, 0.05], - [250000.0, 0.1], - [500000.0, 0.125], - [1000000.0, 0.25], - [2000000.0, 0.5] - ], - "AUDIO/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "AVAX/USDT": [ - [0.0, 0.01], - [50000.0, 0.025], - [150000.0, 0.05], - [250000.0, 0.1], - [500000.0, 0.125], - [750000.0, 0.25], - [1000000.0, 0.5] - ], - "AXS/USDT": [ - [0.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.1665], - [10000000.0, 0.25], - [15000000.0, 0.5] - ], - "BAKE/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "BAL/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "BAND/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "BAT/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "BCH/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "BEL/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "BLZ/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "BNB/BUSD": [ - [0.0, 0.025], - [100000.0, 0.05], - [500000.0, 0.1], - [1000000.0, 0.15], - [2000000.0, 0.25], - [5000000.0, 0.5] - ], - "BNB/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "BTC/BUSD": [ - [0.0, 0.004], - [25000.0, 0.005], - [100000.0, 0.01], - [500000.0, 0.025], - [1000000.0, 0.05], - [2000000.0, 0.1], - [5000000.0, 0.125], - [10000000.0, 0.15], - [20000000.0, 0.25], - [30000000.0, 0.5] - ], - "BTC/USDT": [ - [0.0, 0.004], - [50000.0, 0.005], - [250000.0, 0.01], - [1000000.0, 0.025], - [5000000.0, 0.05], - [20000000.0, 0.1], - [50000000.0, 0.125], - [100000000.0, 0.15], - [200000000.0, 0.25], - [300000000.0, 0.5] - ], - "BTCBUSD_210129": [ - [0.0, 0.004], - [5000.0, 0.005], - [25000.0, 0.01], - [100000.0, 0.025], - [500000.0, 0.05], - [2000000.0, 0.1], - [5000000.0, 0.125], - [10000000.0, 0.15], - [20000000.0, 0.25] - ], - "BTCBUSD_210226": [ - [0.0, 0.004], - [5000.0, 0.005], - [25000.0, 0.01], - [100000.0, 0.025], - [500000.0, 0.05], - [2000000.0, 0.1], - [5000000.0, 0.125], - [10000000.0, 0.15], - [20000000.0, 0.25] - ], - "BTCDOM/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "BTCSTUSDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "BTCUSDT_210326": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "BTCUSDT_210625": [ - [0.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "BTCUSDT_210924": [ - [0.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25], - [20000000.0, 0.5] - ], - "BTS/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "BTT/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "BZRX/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "C98/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "CELR/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "CHR/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "CHZ/USDT": [ - [0.0, 0.012], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "COMP/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "COTI/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "CRV/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "CTK/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "CVC/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "DASH/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "DEFI/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "DENT/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "DGB/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "DODO/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "DOGE/BUSD": [ - [0.0, 0.025], - [100000.0, 0.05], - [500000.0, 0.1], - [1000000.0, 0.15], - [2000000.0, 0.25], - [5000000.0, 0.5] - ], - "DOGE/USDT": [ - [0.0, 0.01], - [50000.0, 0.025], - [150000.0, 0.05], - [250000.0, 0.1], - [500000.0, 0.125], - [750000.0, 0.25], - [1000000.0, 0.5] - ], - "DOT/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "DOTECOUSDT": [ - [0.0, 0.012], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "DYDX/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "EGLD/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ENJ/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "EOS/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "ETC/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "ETH/BUSD": [ - [0.0, 0.004], - [25000.0, 0.005], - [100000.0, 0.01], - [500000.0, 0.025], - [1000000.0, 0.05], - [2000000.0, 0.1], - [5000000.0, 0.125], - [10000000.0, 0.15], - [20000000.0, 0.25], - [30000000.0, 0.5] - ], - "ETH/USDT": [ - [0.0, 0.005], - [10000.0, 0.0065], - [100000.0, 0.01], - [500000.0, 0.02], - [1000000.0, 0.05], - [2000000.0, 0.1], - [5000000.0, 0.125], - [10000000.0, 0.15], - [20000000.0, 0.25] - ], - "ETHUSDT_210326": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "ETHUSDT_210625": [ - [0.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "ETHUSDT_210924": [ - [0.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25], - [20000000.0, 0.5] - ], - "FIL/USDT": [ - [0.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.1665], - [10000000.0, 0.25] - ], - "FLM/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "FTM/USDT": [ - [0.0, 0.01], - [50000.0, 0.025], - [150000.0, 0.05], - [250000.0, 0.1], - [500000.0, 0.125], - [750000.0, 0.25], - [1000000.0, 0.5] - ], - "FTT/BUSD": [ - [0.0, 0.025], - [100000.0, 0.05], - [500000.0, 0.1], - [1000000.0, 0.15], - [2000000.0, 0.25], - [5000000.0, 0.5] - ], - "GRT/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "GTC/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "HBAR/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "HNT/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "HOT/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ICP/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ICX/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "IOST/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "IOTA/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "IOTX/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "KAVA/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "KEEP/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "KNC/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "KSM/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "LENDUSDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "LINA/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "LINK/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "LIT/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "LRC/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "LTC/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "LUNA/USDT": [ - [0.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.1665], - [10000000.0, 0.25], - [15000000.0, 0.5] - ], - "MANA/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "MASK/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "MATIC/USDT": [ - [0.0, 0.01], - [50000.0, 0.025], - [150000.0, 0.05], - [250000.0, 0.1], - [500000.0, 0.125], - [750000.0, 0.25], - [1000000.0, 0.5] - ], - "MKR/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "MTL/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "NEAR/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "NEO/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "NKN/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "OCEAN/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "OGN/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "OMG/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ONE/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ONT/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "QTUM/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "RAY/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "REEF/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "REN/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "RLC/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "RSR/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "RUNE/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "RVN/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "SAND/USDT": [ - [0.0, 0.012], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "SC/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "SFP/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "SKL/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "SNX/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "SOL/BUSD": [ - [0.0, 0.025], - [100000.0, 0.05], - [500000.0, 0.1], - [1000000.0, 0.15], - [2000000.0, 0.25], - [5000000.0, 0.5] - ], - "SOL/USDT": [ - [0.0, 0.01], - [50000.0, 0.025], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.25], - [10000000.0, 0.5] - ], - "SRM/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "STMX/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "STORJ/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "SUSHI/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "SXP/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "THETA/USDT": [ - [0.0, 0.01], - [50000.0, 0.025], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.1665], - [10000000.0, 0.25] - ], - "TLM/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "TOMO/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "TRB/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "TRX/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "UNFI/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "UNI/USDT": [ - [0.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.1665], - [10000000.0, 0.25] - ], - "VET/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "WAVES/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "XEM/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "XLM/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "XMR/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "XRP/BUSD": [ - [0.0, 0.025], - [100000.0, 0.05], - [500000.0, 0.1], - [1000000.0, 0.15], - [2000000.0, 0.25], - [5000000.0, 0.5] - ], - "XRP/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "XTZ/USDT": [ - [0.0, 0.0065], - [10000.0, 0.01], - [50000.0, 0.02], - [250000.0, 0.05], - [1000000.0, 0.1], - [2000000.0, 0.125], - [5000000.0, 0.15], - [10000000.0, 0.25] - ], - "YFI/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "YFII/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ZEC/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ZEN/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ZIL/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ], - "ZRX/USDT": [ - [0.0, 0.01], - [5000.0, 0.025], - [25000.0, 0.05], - [100000.0, 0.1], - [250000.0, 0.125], - [1000000.0, 0.5] - ] - } From 2d679177e506067753c3a5475c80a8f69ce70f2f Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 19 Sep 2021 03:05:58 -0600 Subject: [PATCH 099/102] Added in lev prep before creating api order --- freqtrade/exchange/binance.py | 1 + freqtrade/exchange/exchange.py | 13 ++++++------- freqtrade/exchange/ftx.py | 1 + freqtrade/exchange/kraken.py | 4 ++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 7d83e971b..35f427c34 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -107,6 +107,7 @@ class Binance(Exchange): rate = self.price_to_precision(pair, rate) + self._lev_prep(pair, leverage) order = self._api.create_order(symbol=pair, type=ordertype, side=side, amount=amount, price=rate, params=params) logger.info('stoploss limit order added for %s. ' diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 4021e7d02..4617fd4c2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -773,10 +773,11 @@ class Exchange: # Order handling def _lev_prep(self, pair: str, leverage: float): - self.set_margin_mode(pair, self.collateral) - self._set_leverage(leverage, pair) + if self.trading_mode != TradingMode.SPOT: + self.set_margin_mode(pair, self.collateral) + self._set_leverage(leverage, pair) - def _get_params(self, time_in_force: str, ordertype: str, leverage: float) -> Dict: + def _get_params(self, ordertype: str, leverage: float, time_in_force: str = 'gtc') -> Dict: params = self._params.copy() if time_in_force != 'gtc' and ordertype != 'market': param = self._ft_has.get('time_in_force_parameter', '') @@ -790,10 +791,7 @@ class Exchange: dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate, leverage) return dry_order - if self.trading_mode != TradingMode.SPOT: - self._lev_prep(pair, leverage) - - params = self._get_params(time_in_force, ordertype, leverage) + params = self._get_params(ordertype, leverage, time_in_force) try: # Set the precision for amount and price(rate) as accepted by the exchange @@ -802,6 +800,7 @@ class Exchange: or self._api.options.get("createMarketBuyOrderRequiresPrice", False)) rate_for_order = self.price_to_precision(pair, rate) if needs_price else None + self._lev_prep(pair, leverage) order = self._api.create_order(pair, ordertype, side, amount, rate_for_order, params) self._log_exchange_response('create_order', order) diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index 0f572dee9..62adea04c 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -81,6 +81,7 @@ class Ftx(Exchange): params['stopPrice'] = stop_price amount = self.amount_to_precision(pair, amount) + self._lev_prep(pair, leverage) order = self._api.create_order(symbol=pair, type=ordertype, side=side, amount=amount, params=params) self._log_exchange_response('create_stoploss_order', order) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index ec49c963f..19d0a4967 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -184,8 +184,8 @@ class Kraken(Exchange): """ return - def _get_params(self, time_in_force: str, ordertype: str, leverage: float) -> Dict: - params = super()._get_params(time_in_force, ordertype, leverage) + def _get_params(self, ordertype: str, leverage: float, time_in_force: str = 'gtc') -> Dict: + params = super()._get_params(ordertype, leverage, time_in_force) if leverage > 1.0: params['leverage'] = leverage return params From ab88217186afa23718421ad67867a9d624024878 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Sep 2021 13:16:30 +0200 Subject: [PATCH 100/102] Improve /balance output to include starting balance and percentual change closes #5503 --- freqtrade/rpc/api_server/api_schemas.py | 7 +++++++ freqtrade/rpc/rpc.py | 24 ++++++++++++++++++++---- freqtrade/rpc/telegram.py | 23 +++++++++++++++-------- tests/rpc/test_rpc_apiserver.py | 14 ++++++++++---- tests/rpc/test_rpc_telegram.py | 2 ++ 5 files changed, 54 insertions(+), 16 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 3adbebc16..eb6082087 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -46,6 +46,13 @@ class Balances(BaseModel): value: float stake: str note: str + starting_capital: float + starting_capital_ratio: float + starting_capital_pct: float + starting_capital_fiat: float + starting_capital_fiat_ratio: float + starting_capital_fiat_pct: float + class Count(BaseModel): diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b7b1fe603..12a444b80 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -458,6 +458,9 @@ class RPC: raise RPCException('Error getting current tickers.') self._freqtrade.wallets.update(require_update=False) + starting_capital = self._freqtrade.wallets.get_starting_balance() + starting_capital_fiat = self._fiat_converter.convert_amount( + starting_capital, stake_currency, fiat_display_currency) if self._fiat_converter else 0 for coin, balance in self._freqtrade.wallets.get_all_balances().items(): if not balance.total: @@ -493,15 +496,28 @@ class RPC: else: raise RPCException('All balances are zero.') - symbol = fiat_display_currency - value = self._fiat_converter.convert_amount(total, stake_currency, - symbol) if self._fiat_converter else 0 + value = self._fiat_converter.convert_amount( + total, stake_currency, fiat_display_currency) if self._fiat_converter else 0 + + starting_capital_ratio = 0.0 + starting_capital_fiat_ratio = 0.0 + if starting_capital: + starting_capital_ratio = (total / starting_capital) - 1 + if starting_capital_fiat: + starting_capital_fiat_ratio = (value / starting_capital_fiat) - 1 + return { 'currencies': output, 'total': total, - 'symbol': symbol, + 'symbol': fiat_display_currency, 'value': value, 'stake': stake_currency, + 'starting_capital': starting_capital, + 'starting_capital_ratio': starting_capital_ratio, + 'starting_capital_pct': round(starting_capital_ratio * 100, 2), + 'starting_capital_fiat': starting_capital_fiat, + 'starting_capital_fiat_ratio': starting_capital_fiat_ratio, + 'starting_capital_fiat_pct': round(starting_capital_fiat_ratio * 100, 2), 'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else '' } diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a988d2b60..ffb7385da 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -603,12 +603,15 @@ class Telegram(RPCHandler): output = '' if self._config['dry_run']: - output += ( - f"*Warning:* Simulated balances in Dry Mode.\n" - "This mode is still experimental!\n" - "Starting capital: " - f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n" - ) + output += "*Warning:* Simulated balances in Dry Mode.\n" + + output += ("Starting capital: " + f"`{result['starting_capital']}` {self._config['stake_currency']}" + ) + output += (f" `{result['starting_capital_fiat']}` " + f"{self._config['fiat_display_currency']}.\n" + ) if result['starting_capital_fiat'] > 0 else '.\n' + total_dust_balance = 0 total_dust_currencies = 0 for curr in result['currencies']: @@ -641,9 +644,13 @@ class Telegram(RPCHandler): f"{round_coin_value(total_dust_balance, result['stake'], False)}`\n") output += ("\n*Estimated Value*:\n" - f"\t`{result['stake']}: {result['total']: .8f}`\n" + f"\t`{result['stake']}: " + f"{round_coin_value(result['total'], result['stake'], False)}`" + f" `({result['starting_capital_pct']}%)`\n" f"\t`{result['symbol']}: " - f"{round_coin_value(result['value'], result['symbol'], False)}`\n") + f"{round_coin_value(result['value'], result['symbol'], False)}`" + f" `({result['starting_capital_fiat_pct']}%)`\n" + ) self._send_msg(output, reload_able=True, callback_path="update_balance", query=update.callback_query) except RPCException as e: diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 43eb70938..7c98b2df7 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -422,20 +422,22 @@ def test_api_stopbuy(botclient): assert ftbot.config['max_open_trades'] == 0 -def test_api_balance(botclient, mocker, rpc_balance): +def test_api_balance(botclient, mocker, rpc_balance, tickers): ftbot, client = botclient ftbot.config['dry_run'] = False mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=rpc_balance) + mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) mocker.patch('freqtrade.exchange.Exchange.get_valid_pair_combination', side_effect=lambda a, b: f"{a}/{b}") ftbot.wallets.update() rc = client_get(client, f"{BASE_URI}/balance") assert_response(rc) - assert "currencies" in rc.json() - assert len(rc.json()["currencies"]) == 5 - assert rc.json()['currencies'][0] == { + response = rc.json() + assert "currencies" in response + assert len(response["currencies"]) == 5 + assert response['currencies'][0] == { 'currency': 'BTC', 'free': 12.0, 'balance': 12.0, @@ -443,6 +445,10 @@ def test_api_balance(botclient, mocker, rpc_balance): 'est_stake': 12.0, 'stake': 'BTC', } + assert 'starting_capital' in response + assert 'starting_capital_fiat' in response + assert 'starting_capital_pct' in response + assert 'starting_capital_ratio' in response def test_api_count(botclient, mocker, ticker, fee, markets): diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 2013dad7d..21f1cd000 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -576,6 +576,8 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None 'total': 100.0, 'symbol': 100.0, 'value': 1000.0, + 'starting_capital': 1000, + 'starting_capital_fiat': 1000, }) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) From ec03531771904a3a20dc31e4c64d0f13156fdcce Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 19 Sep 2021 13:29:09 +0200 Subject: [PATCH 101/102] Improve naming of variables --- freqtrade/rpc/api_server/api_schemas.py | 1 - freqtrade/rpc/rpc.py | 15 ++++++--------- freqtrade/rpc/telegram.py | 3 +-- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index eb6082087..46187f571 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -54,7 +54,6 @@ class Balances(BaseModel): starting_capital_fiat_pct: float - class Count(BaseModel): current: int max: int diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 12a444b80..f6599b429 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -459,7 +459,7 @@ class RPC: self._freqtrade.wallets.update(require_update=False) starting_capital = self._freqtrade.wallets.get_starting_balance() - starting_capital_fiat = self._fiat_converter.convert_amount( + starting_cap_fiat = self._fiat_converter.convert_amount( starting_capital, stake_currency, fiat_display_currency) if self._fiat_converter else 0 for coin, balance in self._freqtrade.wallets.get_all_balances().items(): @@ -500,11 +500,8 @@ class RPC: total, stake_currency, fiat_display_currency) if self._fiat_converter else 0 starting_capital_ratio = 0.0 - starting_capital_fiat_ratio = 0.0 - if starting_capital: - starting_capital_ratio = (total / starting_capital) - 1 - if starting_capital_fiat: - starting_capital_fiat_ratio = (value / starting_capital_fiat) - 1 + starting_capital_ratio = (total / starting_capital) - 1 if starting_capital else 0.0 + starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0 return { 'currencies': output, @@ -515,9 +512,9 @@ class RPC: 'starting_capital': starting_capital, 'starting_capital_ratio': starting_capital_ratio, 'starting_capital_pct': round(starting_capital_ratio * 100, 2), - 'starting_capital_fiat': starting_capital_fiat, - 'starting_capital_fiat_ratio': starting_capital_fiat_ratio, - 'starting_capital_fiat_pct': round(starting_capital_fiat_ratio * 100, 2), + 'starting_capital_fiat': starting_cap_fiat, + 'starting_capital_fiat_ratio': starting_cap_fiat_ratio, + 'starting_capital_fiat_pct': round(starting_cap_fiat_ratio * 100, 2), 'note': 'Simulated balances' if self._freqtrade.config['dry_run'] else '' } diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ffb7385da..19c58b63d 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -649,8 +649,7 @@ class Telegram(RPCHandler): f" `({result['starting_capital_pct']}%)`\n" f"\t`{result['symbol']}: " f"{round_coin_value(result['value'], result['symbol'], False)}`" - f" `({result['starting_capital_fiat_pct']}%)`\n" - ) + f" `({result['starting_capital_fiat_pct']}%)`\n") self._send_msg(output, reload_able=True, callback_path="update_balance", query=update.callback_query) except RPCException as e: From d8d6f245a71c45ded9bc9926d2ad04995ec1d8a2 Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Sun, 19 Sep 2021 16:44:02 -0600 Subject: [PATCH 102/102] Fixed breaking tests in test_freqtradebot.py --- tests/test_freqtradebot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index cd9dd6103..bb9527011 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1559,7 +1559,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, @pytest.mark.parametrize('return_value,side_effect,log_message', [ - (False, None, 'Found no buy signals for whitelisted currencies. Trying again...'), + (False, None, 'Found no enter signals for whitelisted currencies. Trying again...'), (None, DependencyException, 'Unable to create trade for ETH/BTC: ') ]) def test_enter_positions(mocker, default_conf, return_value, side_effect, @@ -3126,7 +3126,7 @@ def test__safe_exit_amount(default_conf, fee, caplog, mocker, amount_wallet, has freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) if has_err: - with pytest.raises(DependencyException, match=r"Not enough amount to sell."): + with pytest.raises(DependencyException, match=r"Not enough amount to exit trade."): assert freqtrade._safe_exit_amount(trade.pair, trade.amount) else: wallet_update.reset_mock()