From df726a54f89af8f99cb5dd3969ba280187d22d58 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Fri, 25 Feb 2022 00:20:53 +0000 Subject: [PATCH 01/37] cater for case where sell limit order expired --- freqtrade/rpc/telegram.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index da613fab8..e71b5a9b9 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -448,7 +448,8 @@ class Telegram(RPCHandler): "*Current Pair:* {pair}", "*Amount:* `{amount} ({stake_amount} {base_currency})`", "*Entry Tag:* `{buy_tag}`" if r['buy_tag'] else "", - "*Exit Reason:* `{sell_reason}`" if r['sell_reason'] else "", + "*Exit Reason:* `{sell_reason}`" + if (r['sell_reason'] and not r['is_open']) else "", ] if position_adjust: From a0b42c7aa2ef018188e5a437a203065f9f25b900 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Feb 2022 20:47:15 +0100 Subject: [PATCH 02/37] gitignore sqlite temporary files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 34c751242..97f77f779 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ # Freqtrade rules config*.json *.sqlite +*.sqlite-shm +*.sqlite-wal logfile.txt user_data/* !user_data/strategy/sample_strategy.py From 018c6200578ac8702a40c53cf0142291d85908d6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Feb 2022 08:19:45 +0100 Subject: [PATCH 03/37] Fix 0 Division error on exchanges without average closes #6461 --- freqtrade/persistence/models.py | 1 + freqtrade/rpc/telegram.py | 21 +++++++++++++-------- tests/rpc/test_rpc.py | 4 ++-- tests/rpc/test_rpc_telegram.py | 4 +++- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index e674890d3..559c7e94a 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -195,6 +195,7 @@ class Order(_DECL_BASE): return { 'amount': self.amount, 'average': round(self.average, 8) if self.average else 0, + 'safe_price': self.safe_price, 'cost': self.cost if self.cost else 0, 'filled': self.filled, 'ft_order_side': self.ft_order_side, diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index da613fab8..eb9dd94e3 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -370,15 +370,18 @@ class Telegram(RPCHandler): else: return "\N{CROSS MARK}" - def _prepare_entry_details(self, filled_orders, base_currency, is_open): + def _prepare_entry_details(self, filled_orders: List, base_currency: str, is_open: bool): """ Prepare details of trade with entry adjustment enabled """ - lines = [] + lines: List[str] = [] + if len(filled_orders) > 0: + first_avg = filled_orders[0]["safe_price"] + for x, order in enumerate(filled_orders): cur_entry_datetime = arrow.get(order["order_filled_date"]) cur_entry_amount = order["amount"] - cur_entry_average = order["average"] + cur_entry_average = order["safe_price"] lines.append(" ") if x == 0: lines.append("*Entry #{}:*".format(x+1)) @@ -389,12 +392,14 @@ class Telegram(RPCHandler): sumA = 0 sumB = 0 for y in range(x): - sumA += (filled_orders[y]["amount"] * filled_orders[y]["average"]) + sumA += (filled_orders[y]["amount"] * filled_orders[y]["safe_price"]) sumB += filled_orders[y]["amount"] - prev_avg_price = sumA/sumB - price_to_1st_entry = ((cur_entry_average - filled_orders[0]["average"]) - / filled_orders[0]["average"]) - minus_on_entry = (cur_entry_average - prev_avg_price)/prev_avg_price + prev_avg_price = sumA / sumB + price_to_1st_entry = ((cur_entry_average - first_avg) / first_avg) + minus_on_entry = 0 + if prev_avg_price: + minus_on_entry = (cur_entry_average - prev_avg_price) / prev_avg_price + dur_entry = cur_entry_datetime - arrow.get(filled_orders[x-1]["order_filled_date"]) days = dur_entry.days hours, remainder = divmod(dur_entry.seconds, 3600) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index e7b09ab74..dd6c969ed 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -110,7 +110,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'open_order': None, 'exchange': 'binance', 'filled_entry_orders': [{ - 'amount': 91.07468123, 'average': 1.098e-05, + 'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05, 'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy', 'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY, 'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05, @@ -185,7 +185,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'open_order': None, 'exchange': 'binance', 'filled_entry_orders': [{ - 'amount': 91.07468123, 'average': 1.098e-05, + 'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05, 'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy', 'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY, 'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 5894b9a0f..ccf61f91b 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -236,6 +236,8 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: create_mock_trades(fee) trades = Trade.get_open_trades() trade = trades[0] + # Average may be empty on some exchanges + trade.orders[0].average = 0 trade.orders.append(Order( order_id='5412vbb', ft_order_side='buy', @@ -246,7 +248,7 @@ def test_telegram_status_multi_entry(default_conf, update, mocker, fee) -> None: order_type="market", side="buy", price=trade.open_rate * 0.95, - average=trade.open_rate * 0.95, + average=0, filled=trade.amount, remaining=0, cost=trade.amount, From 7883160ce0040b57758a07c2eb0bcf78b07969a5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Feb 2022 08:23:13 +0100 Subject: [PATCH 04/37] Update to fstrings --- freqtrade/rpc/telegram.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index eb9dd94e3..69f7f2858 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -384,10 +384,10 @@ class Telegram(RPCHandler): cur_entry_average = order["safe_price"] lines.append(" ") if x == 0: - lines.append("*Entry #{}:*".format(x+1)) - lines.append("*Entry Amount:* {} ({:.8f} {})" - .format(cur_entry_amount, order["cost"], base_currency)) - lines.append("*Average Entry Price:* {}".format(cur_entry_average)) + lines.append(f"*Entry #{x+1}:*") + lines.append( + f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {base_currency})") + lines.append(f"*Average Entry Price:* {cur_entry_average}") else: sumA = 0 sumB = 0 @@ -404,17 +404,16 @@ class Telegram(RPCHandler): days = dur_entry.days hours, remainder = divmod(dur_entry.seconds, 3600) minutes, seconds = divmod(remainder, 60) - lines.append("*Entry #{}:* at {:.2%} avg profit".format(x+1, minus_on_entry)) + lines.append(f"*Entry #{x+1}:* at {minus_on_entry:.2%} avg profit") if is_open: lines.append("({})".format(cur_entry_datetime .humanize(granularity=["day", "hour", "minute"]))) - lines.append("*Entry Amount:* {} ({:.8f} {})" - .format(cur_entry_amount, order["cost"], base_currency)) - lines.append("*Average Entry Price:* {} ({:.2%} from 1st entry rate)" - .format(cur_entry_average, price_to_1st_entry)) - lines.append("*Order filled at:* {}".format(order["order_filled_date"])) - lines.append("({}d {}h {}m {}s from previous entry)" - .format(days, hours, minutes, seconds)) + lines.append( + f"*Entry Amount:* {cur_entry_amount} ({order['cost']:.8f} {base_currency})") + lines.append(f"*Average Entry Price:* {cur_entry_average} " + f"({price_to_1st_entry:.2%} from 1st entry rate)") + lines.append(f"*Order filled at:* {order['order_filled_date']}") + lines.append(f"({days}d {hours}h {minutes}m {seconds}s from previous entry)") return lines @authorized_only From 1d57ce19ebf3ba7be55aaebecb7db9db8d00c4cb Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Feb 2022 19:45:49 +0100 Subject: [PATCH 05/37] Move stoploss -limit implemenentation to exchange class, as this seems to be used by multiple exchanges. --- freqtrade/exchange/binance.py | 64 +----------------------------- freqtrade/exchange/exchange.py | 71 +++++++++++++++++++++++++++++++--- 2 files changed, 67 insertions(+), 68 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index 4ba30b626..a195788dd 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -3,12 +3,8 @@ import logging from typing import Dict, List, Tuple import arrow -import ccxt -from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, - OperationalException, TemporaryError) from freqtrade.exchange import Exchange -from freqtrade.exchange.common import retrier logger = logging.getLogger(__name__) @@ -18,6 +14,7 @@ class Binance(Exchange): _ft_has: Dict = { "stoploss_on_exchange": True, + "stoploss_order_type": "stop_loss_limit", "order_time_in_force": ['gtc', 'fok', 'ioc'], "time_in_force_parameter": "timeInForce", "ohlcv_candle_limit": 1000, @@ -33,65 +30,6 @@ class Binance(Exchange): """ 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: - """ - 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. - """ - # 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 - - ordertype = "stop_loss_limit" - - stop_price = self.price_to_precision(pair, stop_price) - - # Ensure rate is less than stop price - if stop_price <= rate: - raise OperationalException( - 'In stoploss limit order, stop price should be more than limit price') - - if self._config['dry_run']: - dry_order = self.create_dry_run_order( - pair, ordertype, "sell", amount, stop_price) - return dry_order - - try: - params = self._params.copy() - params.update({'stopPrice': stop_price}) - - amount = self.amount_to_precision(pair, amount) - - rate = self.price_to_precision(pair, rate) - - order = self._api.create_order(symbol=pair, type=ordertype, side='sell', - 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) - 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'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'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 - except ccxt.BaseError as e: - raise OperationalException(e) from e - async def _async_get_historic_ohlcv(self, pair: str, timeframe: str, since_ms: int, is_new_pair: bool = False, raise_: bool = False diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index a2217a02e..cd4c2ce83 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -791,18 +791,79 @@ class Exchange: """ raise OperationalException(f"stoploss is not implemented for {self.name}.") + @retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ creates a stoploss order. + creates a stoploss limit order. + Should an exchange support more ordertypes, the exchange should implement this method, + using `order_types.get('stoploss', 'market')` to get the correct ordertype (e.g. FTX). + The precise ordertype is determined by the order_types dict or exchange default. - Since ccxt does not unify stoploss-limit orders yet, this needs to be implemented in each - exchange's subclass. + The exception below should never raise, since we disallow starting the bot in validate_ordertypes() - Note: Changes to this interface need to be applied to all sub-classes too. - """ - raise OperationalException(f"stoploss is not implemented for {self.name}.") + This may work with a limited number of other exchanges, but correct working + needs to be tested individually. + WARNING: setting `stoploss_on_exchange` to True will NOT auto-enable stoploss on exchange. + `stoploss_adjust` must still be implemented for this to work. + """ + if not self._ft_has['stoploss_on_exchange']: + raise OperationalException(f"stoploss is not implemented for {self.name}.") + + # 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 + + ordertype = self._ft_has["stoploss_order_type"] + + stop_price = self.price_to_precision(pair, stop_price) + + # Ensure rate is less than stop price + if stop_price <= rate: + raise OperationalException( + 'In stoploss limit order, stop price should be more than limit price') + + if self._config['dry_run']: + dry_order = self.create_dry_run_order( + pair, ordertype, "sell", amount, stop_price) + return dry_order + + try: + params = self._params.copy() + # Verify if stopPrice works for your exchange! + params.update({'stopPrice': stop_price}) + + amount = self.amount_to_precision(pair, amount) + + rate = self.price_to_precision(pair, rate) + + order = self._api.create_order(symbol=pair, type=ordertype, side='sell', + amount=amount, price=rate, params=params) + logger.info(f"stoploss limit order added for {pair}. " + f"stop price: {stop_price}. limit: {rate}") + self._log_exchange_response('create_stoploss_order', order) + 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'Message: {e}') from e + except ccxt.InvalidOrder as e: + # Errors: + # `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'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 + except ccxt.BaseError as e: + raise OperationalException(e) from e @retrier(retries=API_FETCH_ORDER_RETRY_COUNT) def fetch_order(self, order_id: str, pair: str) -> Dict: From ea197b79caaceb7f4f72ff9cfe2b2e069e4a16e2 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Feb 2022 20:40:40 +0100 Subject: [PATCH 06/37] Add some more logic to stoploss --- freqtrade/exchange/binance.py | 2 +- freqtrade/exchange/exchange.py | 28 +++++++++++++++++++--------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/freqtrade/exchange/binance.py b/freqtrade/exchange/binance.py index a195788dd..37ead6dd8 100644 --- a/freqtrade/exchange/binance.py +++ b/freqtrade/exchange/binance.py @@ -14,7 +14,7 @@ class Binance(Exchange): _ft_has: Dict = { "stoploss_on_exchange": True, - "stoploss_order_type": "stop_loss_limit", + "stoploss_order_types": {"limit": "stop_loss_limit"}, "order_time_in_force": ['gtc', 'fok', 'ioc'], "time_in_force_parameter": "timeInForce", "ohlcv_candle_limit": 1000, diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index cd4c2ce83..d8644dcb9 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -791,13 +791,18 @@ class Exchange: """ raise OperationalException(f"stoploss is not implemented for {self.name}.") + def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict: + params = self._params.copy() + # Verify if stopPrice works for your exchange! + params.update({'stopPrice': stop_price}) + return params + @retrier(retries=0) def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: """ creates a stoploss order. - creates a stoploss limit order. - Should an exchange support more ordertypes, the exchange should implement this method, - using `order_types.get('stoploss', 'market')` to get the correct ordertype (e.g. FTX). + requires `_ft_has['stoploss_order_types']` to be set as a dict mapping limit and market + to the corresponding exchange type. The precise ordertype is determined by the order_types dict or exchange default. @@ -812,12 +817,18 @@ class Exchange: if not self._ft_has['stoploss_on_exchange']: raise OperationalException(f"stoploss is not implemented for {self.name}.") - # Limit price threshold: As limit price should always be below stop-price + user_order_type = order_types.get('stoploss', 'market') + if user_order_type in self._ft_has["stoploss_order_types"].keys(): + ordertype = self._ft_has["stoploss_order_types"][user_order_type] + else: + # Otherwise pick only one available + ordertype = list(self._ft_has["stoploss_order_types"].values())[0] + + # if user_order_type == 'limit': + # 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 - ordertype = self._ft_has["stoploss_order_type"] - stop_price = self.price_to_precision(pair, stop_price) # Ensure rate is less than stop price @@ -826,14 +837,13 @@ class Exchange: 'In stoploss limit order, stop price should be more than limit price') if self._config['dry_run']: + # TODO: will this work if ordertype is limit?? dry_order = self.create_dry_run_order( pair, ordertype, "sell", amount, stop_price) return dry_order try: - params = self._params.copy() - # Verify if stopPrice works for your exchange! - params.update({'stopPrice': stop_price}) + params = self._get_stop_params(ordertype=ordertype, stop_price=stop_price) amount = self.amount_to_precision(pair, amount) From 7ba92086c96a8b453b881825be1c2f789ff7b8f4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 3 Feb 2022 06:55:58 +0100 Subject: [PATCH 07/37] Make stoploss method more flexible --- freqtrade/exchange/exchange.py | 27 ++++++++++++++------------- freqtrade/freqtradebot.py | 2 +- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index d8644dcb9..60fd1ded4 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -823,27 +823,28 @@ class Exchange: else: # Otherwise pick only one available ordertype = list(self._ft_has["stoploss_order_types"].values())[0] + user_order_type = list(self._ft_has["stoploss_order_types"].keys())[0] - # if user_order_type == 'limit': + stop_price_norm = self.price_to_precision(pair, stop_price) + rate = None + if user_order_type == 'limit': # 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 + limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) + rate = stop_price * limit_price_pct - stop_price = self.price_to_precision(pair, stop_price) - - # Ensure rate is less than stop price - if stop_price <= rate: - raise OperationalException( - 'In stoploss limit order, stop price should be more than limit price') + # Ensure rate is less than stop price + if stop_price_norm <= rate: + raise OperationalException( + 'In stoploss limit order, stop price should be more than limit price') if self._config['dry_run']: # TODO: will this work if ordertype is limit?? dry_order = self.create_dry_run_order( - pair, ordertype, "sell", amount, stop_price) + pair, ordertype, "sell", amount, stop_price_norm) return dry_order try: - params = self._get_stop_params(ordertype=ordertype, stop_price=stop_price) + params = self._get_stop_params(ordertype=ordertype, stop_price=stop_price_norm) amount = self.amount_to_precision(pair, amount) @@ -851,7 +852,7 @@ class Exchange: order = self._api.create_order(symbol=pair, type=ordertype, side='sell', amount=amount, price=rate, params=params) - logger.info(f"stoploss limit order added for {pair}. " + logger.info(f"stoploss {user_order_type} order added for {pair}. " f"stop price: {stop_price}. limit: {rate}") self._log_exchange_response('create_stoploss_order', order) return order @@ -871,7 +872,7 @@ class Exchange: 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 stoploss order due to {e.__class__.__name__}. Message: {e}') from e except ccxt.BaseError as e: raise OperationalException(e) from e diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 20fd833eb..70cbc32b7 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -900,7 +900,7 @@ class FreqtradeBot(LoggingMixin): 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) -> None: """ Check to see if stoploss on exchange should be updated in case of trailing stoploss on exchange From 768b526c3867758c00234653d750eb9944f4eb8c Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 3 Feb 2022 07:57:58 +0100 Subject: [PATCH 08/37] Add kucoin stoploss on exchange --- freqtrade/exchange/kucoin.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/freqtrade/exchange/kucoin.py b/freqtrade/exchange/kucoin.py index 2884669a6..efb76f0e3 100644 --- a/freqtrade/exchange/kucoin.py +++ b/freqtrade/exchange/kucoin.py @@ -19,8 +19,27 @@ class Kucoin(Exchange): """ _ft_has: Dict = { + "stoploss_on_exchange": True, + "stoploss_order_types": {"limit": "limit", "market": "market"}, "l2_limit_range": [20, 100], "l2_limit_range_required": False, "order_time_in_force": ['gtc', 'fok', 'ioc'], "time_in_force_parameter": "timeInForce", } + + def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + """ + Verify stop_loss against stoploss-order value (limit or price) + Returns True if adjustment is necessary. + """ + # TODO: since kucoin uses Limit orders, changes to models will be required. + return order['info']['stop'] is not None and stop_loss > float(order['stopPrice']) + + def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict: + + params = self._params.copy() + params.update({ + 'stopPrice': stop_price, + 'stop': 'loss' + }) + return params From 020729cf50b51eae5da8a77f0d97ea5e1a48b6d7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Feb 2022 06:53:51 +0100 Subject: [PATCH 09/37] update docs about kucoin stoploss --- docs/exchanges.md | 4 ++++ docs/stoploss.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index e79abf220..a758245d2 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -177,6 +177,10 @@ Kucoin requires a passphrase for each api key, you will therefore need to add th Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force). +!!! Tip "Stoploss on Exchange" + Kucoin supports `stoploss_on_exchange` and can use both stop-loss-market and stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it. + You can use either `"limit"` or `"market"` in the `order_types.stoploss` configuration setting to decide which type of stoploss shall be used. + ### Kucoin Blacklists For Kucoin, please add `"KCS/"` to your blacklist to avoid issues. diff --git a/docs/stoploss.md b/docs/stoploss.md index 4d28846f1..0158e0365 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -24,7 +24,7 @@ These modes can be configured with these values: ``` !!! Note - Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market, stop-loss-limit) and FTX (stop limit and stop-market) as of now. + Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market, stop-loss-limit), FTX (stop limit and stop-market) and kucoin (stop-limit and stop-market) as of now. Do not set too low/tight stoploss value if using stop loss on exchange! If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work. From 07491990976ee885c7db63d469935cf83aa8cb38 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 24 Feb 2022 07:08:15 +0100 Subject: [PATCH 10/37] Add stoploss tests for kucoin --- freqtrade/exchange/exchange.py | 6 +- freqtrade/exchange/kucoin.py | 2 +- tests/exchange/test_kucoin.py | 120 +++++++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 tests/exchange/test_kucoin.py diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 60fd1ded4..b470f8ff2 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -836,6 +836,7 @@ class Exchange: if stop_price_norm <= rate: raise OperationalException( 'In stoploss limit order, stop price should be more than limit price') + rate = self.price_to_precision(pair, rate) if self._config['dry_run']: # TODO: will this work if ordertype is limit?? @@ -848,8 +849,6 @@ class Exchange: amount = self.amount_to_precision(pair, amount) - rate = self.price_to_precision(pair, rate) - order = self._api.create_order(symbol=pair, type=ordertype, side='sell', amount=amount, price=rate, params=params) logger.info(f"stoploss {user_order_type} order added for {pair}. " @@ -872,7 +871,8 @@ class Exchange: raise DDosProtection(e) from e except (ccxt.NetworkError, ccxt.ExchangeError) as e: raise TemporaryError( - f'Could not place stoploss order due to {e.__class__.__name__}. Message: {e}') from e + f"Could not place stoploss order due to {e.__class__.__name__}. " + f"Message: {e}") from e except ccxt.BaseError as e: raise OperationalException(e) from e diff --git a/freqtrade/exchange/kucoin.py b/freqtrade/exchange/kucoin.py index efb76f0e3..037ca5f9a 100644 --- a/freqtrade/exchange/kucoin.py +++ b/freqtrade/exchange/kucoin.py @@ -33,7 +33,7 @@ class Kucoin(Exchange): Returns True if adjustment is necessary. """ # TODO: since kucoin uses Limit orders, changes to models will be required. - return order['info']['stop'] is not None and stop_loss > float(order['stopPrice']) + return order['info'].get('stop') is not None and stop_loss > float(order['stopPrice']) def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict: diff --git a/tests/exchange/test_kucoin.py b/tests/exchange/test_kucoin.py new file mode 100644 index 000000000..87f9ae8d9 --- /dev/null +++ b/tests/exchange/test_kucoin.py @@ -0,0 +1,120 @@ +from random import randint +from unittest.mock import MagicMock + +import ccxt +import pytest + +from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException +from tests.conftest import get_patched_exchange +from tests.exchange.test_exchange import ccxt_exceptionhandlers + + +@pytest.mark.parametrize('order_type', ['market', 'limit']) +@pytest.mark.parametrize('limitratio,expected', [ + (None, 220 * 0.99), + (0.99, 220 * 0.99), + (0.98, 220 * 0.98), +]) +def test_stoploss_order_kucoin(default_conf, mocker, limitratio, expected, order_type): + api_mock = MagicMock() + order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + + api_mock.create_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + } + }) + default_conf['dry_run'] = False + 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, 'kucoin') + if order_type == 'limit': + with pytest.raises(OperationalException): + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, + order_types={ + 'stoploss': order_type, + 'stoploss_on_exchange_limit_ratio': 1.05}) + + api_mock.create_order.reset_mock() + order_types = {'stoploss': order_type} + if limitratio is not None: + order_types.update({'stoploss_on_exchange_limit_ratio': limitratio}) + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types=order_types) + + 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]['amount'] == 1 + # Price should be 1% below stopprice + if order_type == 'limit': + assert api_mock.create_order.call_args_list[0][1]['price'] == expected + else: + assert api_mock.create_order.call_args_list[0][1]['price'] is None + + assert api_mock.create_order.call_args_list[0][1]['params'] == { + 'stopPrice': 220, + 'stop': 'loss' + } + + # 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, 'kucoin') + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + with pytest.raises(InvalidOrderException): + api_mock.create_order = MagicMock( + side_effect=ccxt.InvalidOrder("kucoin Order would trigger immediately.")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kucoin') + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kucoin", + "stoploss", "create_order", retries=1, + pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + +def test_stoploss_order_dry_run_kucoin(default_conf, mocker): + api_mock = MagicMock() + order_type = 'market' + default_conf['dry_run'] = True + 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, 'kucoin') + + with pytest.raises(OperationalException): + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, + order_types={'stoploss': 'limit', + '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={}) + + assert 'id' in order + assert 'info' in order + assert 'type' in order + + assert order['type'] == order_type + assert order['price'] == 220 + assert order['amount'] == 1 + + +def test_stoploss_adjust_kucoin(mocker, default_conf): + exchange = get_patched_exchange(mocker, default_conf, id='kucoin') + order = { + 'type': 'limit', + 'price': 1500, + 'stopPrice': 1500, + 'info': {'stopPrice': 1500, 'stop': "limit"}, + } + assert exchange.stoploss_adjust(1501, order) + assert not exchange.stoploss_adjust(1499, order) + # Test with invalid order case + order['info']['stop'] = None + assert not exchange.stoploss_adjust(1501, order) From 6caa5f7131eedc1f2cbc6b760e487c1064952af7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Feb 2022 19:10:16 +0100 Subject: [PATCH 11/37] Update dry-run behaviour --- freqtrade/exchange/exchange.py | 19 ++++++++++++------- freqtrade/exchange/ftx.py | 2 +- freqtrade/exchange/kraken.py | 2 +- freqtrade/exchange/kucoin.py | 1 - freqtrade/freqtradebot.py | 4 ++-- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index b470f8ff2..760a1dd32 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -600,7 +600,8 @@ 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, params: Dict = {}, + stop_loss: bool = False) -> Dict[str, Any]: order_id = f'dry_run_{side}_{datetime.now().timestamp()}' _amount = self.amount_to_precision(pair, amount) dry_order: Dict[str, Any] = { @@ -616,14 +617,17 @@ class Exchange: 'remaining': _amount, 'datetime': arrow.utcnow().strftime('%Y-%m-%dT%H:%M:%S.%fZ'), 'timestamp': arrow.utcnow().int_timestamp * 1000, - 'status': "closed" if ordertype == "market" else "open", + 'status': "closed" if ordertype == "market" and not stop_loss else "open", 'fee': None, 'info': {} } - if dry_order["type"] in ["stop_loss_limit", "stop-loss-limit"]: + if stop_loss: dry_order["info"] = {"stopPrice": dry_order["price"]} + dry_order["stopPrice"] = dry_order["price"] + # Workaround to avoid filling stoploss orders immediately + dry_order["ft_order_type"] = "stoploss" - if dry_order["type"] == "market": + if dry_order["type"] == "market" and not dry_order.get("ft_order_type"): # Update market order pricing average = self.get_dry_market_fill_price(pair, side, amount, rate) dry_order.update({ @@ -714,7 +718,9 @@ class Exchange: """ Check dry-run limit order fill and update fee (if it filled). """ - if order['status'] != "closed" and order['type'] in ["limit"]: + if (order['status'] != "closed" + and order['type'] in ["limit"] + and not order.get('ft_order_type')): pair = order['symbol'] if self._is_dry_limit_order_filled(pair, order['side'], order['price']): order.update({ @@ -839,9 +845,8 @@ class Exchange: rate = self.price_to_precision(pair, rate) if self._config['dry_run']: - # TODO: will this work if ordertype is limit?? dry_order = self.create_dry_run_order( - pair, ordertype, "sell", amount, stop_price_norm) + pair, ordertype, "sell", amount, stop_price_norm, stop_loss=True) return dry_order try: diff --git a/freqtrade/exchange/ftx.py b/freqtrade/exchange/ftx.py index a8bf9abac..a346216b3 100644 --- a/freqtrade/exchange/ftx.py +++ b/freqtrade/exchange/ftx.py @@ -56,7 +56,7 @@ class Ftx(Exchange): if self._config['dry_run']: dry_order = self.create_dry_run_order( - pair, ordertype, "sell", amount, stop_price) + pair, ordertype, "sell", amount, stop_price, stop_loss=True) return dry_order try: diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index f4c8ca275..6a033f133 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -101,7 +101,7 @@ class Kraken(Exchange): if self._config['dry_run']: dry_order = self.create_dry_run_order( - pair, ordertype, "sell", amount, stop_price) + pair, ordertype, "sell", amount, stop_price, stop_loss=True) return dry_order try: diff --git a/freqtrade/exchange/kucoin.py b/freqtrade/exchange/kucoin.py index 037ca5f9a..e55f49cce 100644 --- a/freqtrade/exchange/kucoin.py +++ b/freqtrade/exchange/kucoin.py @@ -32,7 +32,6 @@ class Kucoin(Exchange): Verify stop_loss against stoploss-order value (limit or price) Returns True if adjustment is necessary. """ - # TODO: since kucoin uses Limit orders, changes to models will be required. return order['info'].get('stop') is not None and stop_loss > float(order['stopPrice']) def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 70cbc32b7..e3214a61e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1170,8 +1170,8 @@ class FreqtradeBot(LoggingMixin): # if stoploss is on exchange and we are on dry_run mode, # we consider the sell price stop price - if self.config['dry_run'] and sell_type == 'stoploss' \ - and self.strategy.order_types['stoploss_on_exchange']: + if (self.config['dry_run'] and sell_type == 'stoploss' + and self.strategy.order_types['stoploss_on_exchange']): limit = trade.stop_loss # set custom_exit_price if available From 3942b30ebf5b33ed5854b25a5b9ae91650c8ff1d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Feb 2022 08:34:23 +0100 Subject: [PATCH 12/37] Add kraken TODO --- freqtrade/exchange/kraken.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 6a033f133..8cec2500e 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -86,6 +86,8 @@ class Kraken(Exchange): """ Creates a stoploss market order. Stoploss market orders is the only stoploss type supported by kraken. + TODO: investigate if this can be combined with generic implementation + (careful, prices are reversed) """ params = self._params.copy() From 2ec1a7b3707fa122699ff2ee43bf32d60e047b52 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 14 Jan 2022 19:39:09 +0100 Subject: [PATCH 13/37] Add huobi exchange class --- freqtrade/exchange/__init__.py | 1 + freqtrade/exchange/huobi.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 freqtrade/exchange/huobi.py diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 9dc2b8480..2b9ed47ea 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -18,6 +18,7 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, from freqtrade.exchange.ftx import Ftx from freqtrade.exchange.gateio import Gateio from freqtrade.exchange.hitbtc import Hitbtc +from freqtrade.exchange.huobi import Huobi from freqtrade.exchange.kraken import Kraken from freqtrade.exchange.kucoin import Kucoin from freqtrade.exchange.okx import Okx diff --git a/freqtrade/exchange/huobi.py b/freqtrade/exchange/huobi.py new file mode 100644 index 000000000..90865539e --- /dev/null +++ b/freqtrade/exchange/huobi.py @@ -0,0 +1,19 @@ +""" Huobi exchange subclass """ +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + + +logger = logging.getLogger(__name__) + + +class Huobi(Exchange): + """ + Huobi exchange class. Contains adjustments needed for Freqtrade to work + with this exchange. + """ + + _ft_has: Dict = { + "ohlcv_candle_limit": 2000, + } From ee7bc557277910a47984d23616adc38613b9e4d6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jan 2022 13:17:00 +0100 Subject: [PATCH 14/37] Add huobi to Exchange setup --- freqtrade/commands/build_config_commands.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/build_config_commands.py b/freqtrade/commands/build_config_commands.py index 4c722c810..ca55dbbc4 100644 --- a/freqtrade/commands/build_config_commands.py +++ b/freqtrade/commands/build_config_commands.py @@ -108,10 +108,11 @@ def ask_user_config() -> Dict[str, Any]: "binance", "binanceus", "bittrex", - "kraken", "ftx", - "kucoin", "gateio", + "huobi", + "kraken", + "kucoin", "okx", Separator(), "other", From 9504b3eb059dd8fcd4b7da63af453f03b6b21edc Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 16 Jan 2022 13:39:19 +0100 Subject: [PATCH 15/37] Improve huobi config generation --- freqtrade/templates/subtemplates/exchange_huobi.j2 | 12 ++++++++++++ tests/exchange/test_exchange.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 freqtrade/templates/subtemplates/exchange_huobi.j2 diff --git a/freqtrade/templates/subtemplates/exchange_huobi.j2 b/freqtrade/templates/subtemplates/exchange_huobi.j2 new file mode 100644 index 000000000..3cb521785 --- /dev/null +++ b/freqtrade/templates/subtemplates/exchange_huobi.j2 @@ -0,0 +1,12 @@ +"exchange": { + "name": "{{ exchange_name | lower }}", + "key": "{{ exchange_key }}", + "secret": "{{ exchange_secret }}", + "ccxt_config": {}, + "ccxt_async_config": {}, + "pair_whitelist": [ + ], + "pair_blacklist": [ + "HT/.*" + ] +} diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 33f34ba3c..527e8050b 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -166,7 +166,7 @@ def test_exchange_resolver(default_conf, mocker, caplog): mocker.patch('freqtrade.exchange.Exchange.validate_timeframes') mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency') - exchange = ExchangeResolver.load_exchange('huobi', default_conf) + exchange = ExchangeResolver.load_exchange('zaif', default_conf) assert isinstance(exchange, Exchange) assert log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog) caplog.clear() From 292c350885a9c4df760c5da42f037ff1a93602da Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 30 Jan 2022 14:15:07 +0100 Subject: [PATCH 16/37] Add stoploss support for huobi --- docs/exchanges.md | 7 ++- docs/stoploss.md | 2 +- freqtrade/exchange/huobi.py | 75 ++++++++++++++++++++++++ tests/exchange/test_huobi.py | 109 +++++++++++++++++++++++++++++++++++ 4 files changed, 191 insertions(+), 2 deletions(-) create mode 100644 tests/exchange/test_huobi.py diff --git a/docs/exchanges.md b/docs/exchanges.md index a758245d2..53af35736 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -57,7 +57,7 @@ This configuration enables kraken, as well as rate-limiting to avoid bans from t Binance supports [time_in_force](configuration.md#understand-order_time_in_force). !!! Tip "Stoploss on Exchange" - Binance supports `stoploss_on_exchange` and uses stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it. + Binance supports `stoploss_on_exchange` and uses `stop-loss-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange.. ### Binance Blacklist @@ -71,6 +71,11 @@ Binance has been split into 2, and users must use the correct ccxt exchange ID f * [binance.com](https://www.binance.com/) - International users. Use exchange id: `binance`. * [binance.us](https://www.binance.us/) - US based users. Use exchange id: `binanceus`. +## Huobi + +!!! Tip "Stoploss on Exchange" + Huobi supports `stoploss_on_exchange` and uses `stop-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange. + ## Kraken !!! Tip "Stoploss on Exchange" diff --git a/docs/stoploss.md b/docs/stoploss.md index 0158e0365..d0e106d8f 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -24,7 +24,7 @@ These modes can be configured with these values: ``` !!! Note - Stoploss on exchange is only supported for Binance (stop-loss-limit), Kraken (stop-loss-market, stop-loss-limit), FTX (stop limit and stop-market) and kucoin (stop-limit and stop-market) as of now. + Stoploss on exchange is only supported for Binance (stop-loss-limit), Huobi (stop-limit), Kraken (stop-loss-market, stop-loss-limit), FTX (stop limit and stop-market) and kucoin (stop-limit and stop-market) as of now. Do not set too low/tight stoploss value if using stop loss on exchange! If set to low/tight then you have greater risk of missing fill on the order and stoploss will not work. diff --git a/freqtrade/exchange/huobi.py b/freqtrade/exchange/huobi.py index 90865539e..609f2994b 100644 --- a/freqtrade/exchange/huobi.py +++ b/freqtrade/exchange/huobi.py @@ -2,7 +2,12 @@ import logging from typing import Dict +import ccxt + +from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, + OperationalException, TemporaryError) from freqtrade.exchange import Exchange +from freqtrade.exchange.common import retrier logger = logging.getLogger(__name__) @@ -15,5 +20,75 @@ class Huobi(Exchange): """ _ft_has: Dict = { + "stoploss_on_exchange": True, "ohlcv_candle_limit": 2000, } + + def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + """ + Verify stop_loss against stoploss-order value (limit or price) + Returns True if adjustment is necessary. + """ + return order['type'] == 'stop' and stop_loss > float(order['stopPrice']) + + @retrier(retries=0) + def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: + """ + creates a stoploss limit order. + this stoploss-limit is huobi-specific. + TODO: Compare this with other stoploss implementations - + """ + # 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 + + ordertype = "stop-limit" + + stop_price = self.price_to_precision(pair, stop_price) + + # Ensure rate is less than stop price + if stop_price <= rate: + raise OperationalException( + 'In stoploss limit order, stop price should be more than limit price') + + if self._config['dry_run']: + dry_order = self.create_dry_run_order( + pair, ordertype, "sell", amount, stop_price) + return dry_order + + try: + params = self._params.copy() + params.update({ + "stop-price": stop_price, + "operator": "lte", + }) + + amount = self.amount_to_precision(pair, amount) + + rate = self.price_to_precision(pair, rate) + + order = self._api.create_order(symbol=pair, type=ordertype, side='sell', + 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) + 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'Message: {e}') from e + except ccxt.InvalidOrder as e: + # Errors: + # `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'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 + except ccxt.BaseError as e: + raise OperationalException(e) from e diff --git a/tests/exchange/test_huobi.py b/tests/exchange/test_huobi.py new file mode 100644 index 000000000..8d2a35489 --- /dev/null +++ b/tests/exchange/test_huobi.py @@ -0,0 +1,109 @@ +from random import randint +from unittest.mock import MagicMock + +import ccxt +import pytest + +from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException +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), +]) +def test_stoploss_order_huobi(default_conf, mocker, limitratio, expected): + api_mock = MagicMock() + order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + order_type = 'stop-limit' + + api_mock.create_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + } + }) + default_conf['dry_run'] = False + 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, 'huobi') + + with pytest.raises(OperationalException): + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, + 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) + + 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]['amount'] == 1 + # Price should be 1% below stopprice + assert api_mock.create_order.call_args_list[0][1]['price'] == expected + assert api_mock.create_order.call_args_list[0][1]['params'] == {"stop-price": 220, + "operator": "lte", + } + + # 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, 'huobi') + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + 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={}) + + ccxt_exceptionhandlers(mocker, default_conf, api_mock, "huobi", + "stoploss", "create_order", retries=1, + pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + +def test_stoploss_order_dry_run_huobi(default_conf, mocker): + api_mock = MagicMock() + order_type = 'stop-limit' + default_conf['dry_run'] = True + 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, 'huobi') + + with pytest.raises(OperationalException): + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, + 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={}) + + assert 'id' in order + assert 'info' in order + assert 'type' in order + + assert order['type'] == order_type + assert order['price'] == 220 + assert order['amount'] == 1 + + +def test_stoploss_adjust_huobi(mocker, default_conf): + exchange = get_patched_exchange(mocker, default_conf, id='huobi') + order = { + 'type': 'stop', + 'price': 1500, + 'stopPrice': '1500', + } + assert exchange.stoploss_adjust(1501, order) + assert not exchange.stoploss_adjust(1499, order) + # Test with invalid order case + order['type'] = 'stop_loss' + assert not exchange.stoploss_adjust(1501, order) From 1b91be08fece53febdc24deac9a442105ca21c99 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Feb 2022 17:01:51 +0100 Subject: [PATCH 17/37] Add huobi to list of supported exchanges --- README.md | 3 ++- docs/exchanges.md | 12 ++++++------ docs/index.md | 3 ++- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 9b25775af..245a56133 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,8 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even - [X] [FTX](https://ftx.com) - [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [Kraken](https://kraken.com/) -- [X] [OKX](https://www.okx.com/) +- [X] [OKX](https://okx.com/) (Former OKEX) +- [X] [Huobi](http://huobi.com/) - [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ ### Community tested diff --git a/docs/exchanges.md b/docs/exchanges.md index 53af35736..8adf19081 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -71,11 +71,6 @@ Binance has been split into 2, and users must use the correct ccxt exchange ID f * [binance.com](https://www.binance.com/) - International users. Use exchange id: `binance`. * [binance.us](https://www.binance.us/) - US based users. Use exchange id: `binanceus`. -## Huobi - -!!! Tip "Stoploss on Exchange" - Huobi supports `stoploss_on_exchange` and uses `stop-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange. - ## Kraken !!! Tip "Stoploss on Exchange" @@ -191,7 +186,12 @@ Kucoin supports [time_in_force](configuration.md#understand-order_time_in_force) For Kucoin, please add `"KCS/"` to your blacklist to avoid issues. Accounts having KCS accounts use this to pay for fees - if your first trade happens to be on `KCS`, further trades will consume this position and make the initial KCS trade unsellable as the expected amount is not there anymore. -## OKX +## Huobi + +!!! Tip "Stoploss on Exchange" + Huobi supports `stoploss_on_exchange` and uses `stop-limit` orders. It provides great advantages, so we recommend to benefit from it by enabling stoploss on exchange. + +## OKX (former OKEX) OKX requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows: diff --git a/docs/index.md b/docs/index.md index 9fb302a91..134e00c4b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -47,7 +47,8 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual, - [X] [FTX](https://ftx.com) - [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [Kraken](https://kraken.com/) -- [X] [OKX](https://www.okx.com/) +- [X] [OKX](https://okx.com/) (Former OKEX) +- [X] [Huobi](http://huobi.com/) - [ ] [potentially many others through ccxt](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ ### Community tested From f3421dfa9fec2d00a81821de922fad5f553a1f7b Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Feb 2022 20:04:17 +0100 Subject: [PATCH 18/37] Use unified stopPrice argument --- freqtrade/exchange/huobi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/exchange/huobi.py b/freqtrade/exchange/huobi.py index 609f2994b..a56efc4a6 100644 --- a/freqtrade/exchange/huobi.py +++ b/freqtrade/exchange/huobi.py @@ -59,7 +59,7 @@ class Huobi(Exchange): try: params = self._params.copy() params.update({ - "stop-price": stop_price, + "stopPrice": stop_price, "operator": "lte", }) From a1f2f6ddebd869fbc70f31e42b5d74c50d9a6b66 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Feb 2022 15:50:45 +0100 Subject: [PATCH 19/37] Updates required for huobi datadownload --- freqtrade/exchange/huobi.py | 2 +- requirements.txt | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/huobi.py b/freqtrade/exchange/huobi.py index a56efc4a6..50629160b 100644 --- a/freqtrade/exchange/huobi.py +++ b/freqtrade/exchange/huobi.py @@ -21,7 +21,7 @@ class Huobi(Exchange): _ft_has: Dict = { "stoploss_on_exchange": True, - "ohlcv_candle_limit": 2000, + "ohlcv_candle_limit": 1000, } def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: diff --git a/requirements.txt b/requirements.txt index c50f14666..a8ff2f645 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.22.2 pandas==1.4.1 pandas-ta==0.3.14b -ccxt==1.73.70 +ccxt==1.74.17 # Pin cryptography for now due to rust build errors with piwheels cryptography==36.0.1 aiohttp==3.8.1 diff --git a/setup.py b/setup.py index b46396385..ec41228c1 100644 --- a/setup.py +++ b/setup.py @@ -42,7 +42,7 @@ setup( ], install_requires=[ # from requirements.txt - 'ccxt>=1.66.32', + 'ccxt>=1.74.17', 'SQLAlchemy', 'python-telegram-bot>=13.4', 'arrow>=0.17.0', From 14d49e85afda407ff632fb4517b9f89bf13f0fe5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Feb 2022 10:44:38 +0100 Subject: [PATCH 20/37] Update Huobi stoploss to shared method --- freqtrade/exchange/huobi.py | 73 ++++-------------------------------- tests/exchange/test_huobi.py | 2 +- 2 files changed, 9 insertions(+), 66 deletions(-) diff --git a/freqtrade/exchange/huobi.py b/freqtrade/exchange/huobi.py index 50629160b..71c69a9a2 100644 --- a/freqtrade/exchange/huobi.py +++ b/freqtrade/exchange/huobi.py @@ -2,12 +2,7 @@ import logging from typing import Dict -import ccxt - -from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, - OperationalException, TemporaryError) from freqtrade.exchange import Exchange -from freqtrade.exchange.common import retrier logger = logging.getLogger(__name__) @@ -21,6 +16,7 @@ class Huobi(Exchange): _ft_has: Dict = { "stoploss_on_exchange": True, + "stoploss_order_types": {"limit": "stop-limit"}, "ohlcv_candle_limit": 1000, } @@ -31,64 +27,11 @@ class Huobi(Exchange): """ return order['type'] == 'stop' and stop_loss > float(order['stopPrice']) - @retrier(retries=0) - def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: - """ - creates a stoploss limit order. - this stoploss-limit is huobi-specific. - TODO: Compare this with other stoploss implementations - - """ - # 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 + def _get_stop_params(self, ordertype: str, stop_price: float) -> Dict: - ordertype = "stop-limit" - - stop_price = self.price_to_precision(pair, stop_price) - - # Ensure rate is less than stop price - if stop_price <= rate: - raise OperationalException( - 'In stoploss limit order, stop price should be more than limit price') - - if self._config['dry_run']: - dry_order = self.create_dry_run_order( - pair, ordertype, "sell", amount, stop_price) - return dry_order - - try: - params = self._params.copy() - params.update({ - "stopPrice": stop_price, - "operator": "lte", - }) - - amount = self.amount_to_precision(pair, amount) - - rate = self.price_to_precision(pair, rate) - - order = self._api.create_order(symbol=pair, type=ordertype, side='sell', - 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) - 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'Message: {e}') from e - except ccxt.InvalidOrder as e: - # Errors: - # `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'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 - except ccxt.BaseError as e: - raise OperationalException(e) from e + params = self._params.copy() + params.update({ + "stopPrice": stop_price, + "operator": "lte", + }) + return params diff --git a/tests/exchange/test_huobi.py b/tests/exchange/test_huobi.py index 8d2a35489..b39b5ab30 100644 --- a/tests/exchange/test_huobi.py +++ b/tests/exchange/test_huobi.py @@ -48,7 +48,7 @@ def test_stoploss_order_huobi(default_conf, mocker, limitratio, expected): 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 - assert api_mock.create_order.call_args_list[0][1]['params'] == {"stop-price": 220, + assert api_mock.create_order.call_args_list[0][1]['params'] == {"stopPrice": 220, "operator": "lte", } From 41316abb55f1b42116a1c51b6d47ffaf4fcf6340 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Feb 2022 11:04:50 +0100 Subject: [PATCH 21/37] Sort supported exchanges alphabetically --- README.md | 4 ++-- docs/index.md | 4 ++-- freqtrade/exchange/exchange.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 245a56133..5c3ac1be5 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,13 @@ hesitate to read the source code and understand the mechanism of this bot. Please read the [exchange specific notes](docs/exchanges.md) to learn about eventual, special configurations needed for each exchange. -- [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#binance-blacklist)) +- [X] [Binance](https://www.binance.com/) - [X] [Bittrex](https://bittrex.com/) - [X] [FTX](https://ftx.com) - [X] [Gate.io](https://www.gate.io/ref/6266643) +- [X] [Huobi](http://huobi.com/) - [X] [Kraken](https://kraken.com/) - [X] [OKX](https://okx.com/) (Former OKEX) -- [X] [Huobi](http://huobi.com/) - [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ ### Community tested diff --git a/docs/index.md b/docs/index.md index 134e00c4b..32b19bd94 100644 --- a/docs/index.md +++ b/docs/index.md @@ -42,13 +42,13 @@ Freqtrade is a free and open source crypto trading bot written in Python. It is Please read the [exchange specific notes](exchanges.md) to learn about eventual, special configurations needed for each exchange. -- [X] [Binance](https://www.binance.com/) ([*Note for binance users](exchanges.md#binance-blacklist)) +- [X] [Binance](https://www.binance.com/) - [X] [Bittrex](https://bittrex.com/) - [X] [FTX](https://ftx.com) - [X] [Gate.io](https://www.gate.io/ref/6266643) +- [X] [Huobi](http://huobi.com/) - [X] [Kraken](https://kraken.com/) - [X] [OKX](https://okx.com/) (Former OKEX) -- [X] [Huobi](http://huobi.com/) - [ ] [potentially many others through ccxt](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ ### Community tested diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 760a1dd32..da89a7c8a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1664,7 +1664,7 @@ def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = Non def is_exchange_officially_supported(exchange_name: str) -> bool: - return exchange_name in ['bittrex', 'binance', 'kraken', 'ftx', 'gateio', 'okx'] + return exchange_name in ['binance', 'bittrex', 'ftx', 'gateio', 'huobi', 'kraken', 'okx'] def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]: From 0ebf40f39042bdc139bc4969f7d2cce3c0022aae Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Feb 2022 15:57:44 +0100 Subject: [PATCH 22/37] Don't call amount_to_precision twice on entry --- freqtrade/freqtradebot.py | 1 - tests/rpc/test_rpc.py | 4 ++-- tests/test_freqtradebot.py | 10 +++++----- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e3214a61e..91581e557 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -542,7 +542,6 @@ class FreqtradeBot(LoggingMixin): entry_tag=buy_tag): logger.info(f"User requested abortion of buying {pair}") return False - amount = self.exchange.amount_to_precision(pair, amount) order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy", amount=amount, rate=enter_limit_requested, time_in_force=time_in_force) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index dd6c969ed..7d0704d2f 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -79,7 +79,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'close_rate': None, 'current_rate': 1.099e-05, 'amount': 91.07468123, - 'amount_requested': 91.07468123, + 'amount_requested': 91.07468124, 'stake_amount': 0.001, 'trade_duration': None, 'trade_duration_s': None, @@ -154,7 +154,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'close_rate': None, 'current_rate': ANY, 'amount': 91.07468123, - 'amount_requested': 91.07468123, + 'amount_requested': 91.07468124, 'trade_duration': ANY, 'trade_duration_s': ANY, 'stake_amount': 0.001, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index d433998a1..7e56a96e6 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -727,7 +727,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_buy_order_usdt, call_args = buy_mm.call_args_list[0][1] assert call_args['pair'] == pair assert call_args['rate'] == bid - assert call_args['amount'] == round(stake_amount / bid, 8) + assert call_args['amount'] == stake_amount / bid buy_rate_mock.reset_mock() # Should create an open trade with an open order id @@ -748,7 +748,7 @@ def test_execute_entry(mocker, default_conf_usdt, fee, limit_buy_order_usdt, call_args = buy_mm.call_args_list[1][1] assert call_args['pair'] == pair assert call_args['rate'] == fix_price - assert call_args['amount'] == round(stake_amount / fix_price, 8) + assert call_args['amount'] == stake_amount / fix_price # In case of closed order limit_buy_order_usdt['status'] = 'closed' @@ -1266,7 +1266,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf_usdt, fee, cancel_order_mock.assert_called_once_with(100, 'ETH/USDT') stoploss_order_mock.assert_called_once_with( - amount=27.39726027, + amount=pytest.approx(27.39726027), pair='ETH/USDT', order_types=freqtrade.strategy.order_types, stop_price=4.4 * 0.95 @@ -1458,7 +1458,7 @@ def test_handle_stoploss_on_exchange_custom_stop( cancel_order_mock.assert_called_once_with(100, 'ETH/USDT') stoploss_order_mock.assert_called_once_with( - amount=31.57894736, + amount=pytest.approx(31.57894736), pair='ETH/USDT', order_types=freqtrade.strategy.order_types, stop_price=4.4 * 0.96 @@ -1583,7 +1583,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, assert trade.stop_loss == 4.4 * 0.99 cancel_order_mock.assert_called_once_with(100, 'NEO/BTC') stoploss_order_mock.assert_called_once_with( - amount=11.41438356, + amount=pytest.approx(11.41438356), pair='NEO/BTC', order_types=freqtrade.strategy.order_types, stop_price=4.4 * 0.99 From 1ac360674cf250bbcc9dbd3c5604a2f285024332 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Feb 2022 17:37:46 +0100 Subject: [PATCH 23/37] Update Readme quickstart #6472 --- README.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 5c3ac1be5..166e4833a 100644 --- a/README.md +++ b/README.md @@ -69,15 +69,9 @@ Please find the complete documentation on the [freqtrade website](https://www.fr ## Quick start -Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot. +Please refer to the [Docker Quickstart documentation](https://www.freqtrade.io/en/stable/docker_quickstart/) on how to get started quickly. -```bash -git clone -b develop https://github.com/freqtrade/freqtrade.git -cd freqtrade -./setup.sh --install -``` - -For any other type of installation please refer to [Installation doc](https://www.freqtrade.io/en/stable/installation/). +For further (native) installation methods, please refer to the [Installation documentation page](https://www.freqtrade.io/en/stable/installation/). ## Basic Usage From 590944a6004807479b4de3d83c599a7619d113dd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Feb 2022 03:01:17 +0000 Subject: [PATCH 24/37] Bump mkdocs-material from 8.2.1 to 8.2.3 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 8.2.1 to 8.2.3. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/8.2.1...8.2.3) --- updated-dependencies: - dependency-name: mkdocs-material dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- docs/requirements-docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 3e7fa2044..839485629 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,4 @@ mkdocs==1.2.3 -mkdocs-material==8.2.1 +mkdocs-material==8.2.3 mdx_truly_sane_lists==1.2 pymdown-extensions==9.2 From faf6a35ad7aec5643768fbf3c1115fc6cfdbde81 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Feb 2022 03:01:20 +0000 Subject: [PATCH 25/37] Bump types-requests from 2.27.10 to 2.27.11 Bumps [types-requests](https://github.com/python/typeshed) from 2.27.10 to 2.27.11. - [Release notes](https://github.com/python/typeshed/releases) - [Commits](https://github.com/python/typeshed/commits) --- updated-dependencies: - dependency-name: types-requests dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index c52032a60..9fc8a18ad 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -22,7 +22,7 @@ nbconvert==6.4.2 # mypy types types-cachetools==4.2.9 types-filelock==3.2.5 -types-requests==2.27.10 +types-requests==2.27.11 types-tabulate==0.8.5 # Extensions to datetime library From 42fbec4172f255d66afac957b8959b1018bef81b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Feb 2022 03:01:29 +0000 Subject: [PATCH 26/37] Bump ccxt from 1.74.17 to 1.74.43 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.74.17 to 1.74.43. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/exchanges.cfg) - [Commits](https://github.com/ccxt/ccxt/compare/1.74.17...1.74.43) --- updated-dependencies: - dependency-name: ccxt dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a8ff2f645..d3d96c39e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.22.2 pandas==1.4.1 pandas-ta==0.3.14b -ccxt==1.74.17 +ccxt==1.74.43 # Pin cryptography for now due to rust build errors with piwheels cryptography==36.0.1 aiohttp==3.8.1 From 207b211e5e28d957d15803dd0d61cb4d6c0b7ed8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 28 Feb 2022 03:01:38 +0000 Subject: [PATCH 27/37] Bump fastapi from 0.74.0 to 0.74.1 Bumps [fastapi](https://github.com/tiangolo/fastapi) from 0.74.0 to 0.74.1. - [Release notes](https://github.com/tiangolo/fastapi/releases) - [Commits](https://github.com/tiangolo/fastapi/compare/0.74.0...0.74.1) --- updated-dependencies: - dependency-name: fastapi dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index a8ff2f645..ad5c271ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,7 +31,7 @@ python-rapidjson==1.6 sdnotify==0.3.2 # API Server -fastapi==0.74.0 +fastapi==0.74.1 uvicorn==0.17.5 pyjwt==2.3.0 aiofiles==0.8.0 From 68bc2a610743374978a37f767dc3787a4f9b5584 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Feb 2022 11:56:22 +0100 Subject: [PATCH 28/37] Add huobi to ccxt compat tests --- freqtrade/exchange/huobi.py | 2 ++ tests/exchange/test_ccxt_compat.py | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/huobi.py b/freqtrade/exchange/huobi.py index 71c69a9a2..d07e13497 100644 --- a/freqtrade/exchange/huobi.py +++ b/freqtrade/exchange/huobi.py @@ -18,6 +18,8 @@ class Huobi(Exchange): "stoploss_on_exchange": True, "stoploss_order_types": {"limit": "stop-limit"}, "ohlcv_candle_limit": 1000, + "l2_limit_range": [5, 10, 20], + "l2_limit_range_required": False, } def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 09523bd59..877d53fe7 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -59,6 +59,12 @@ EXCHANGES = { 'hasQuoteVolume': True, 'timeframe': '5m', }, + 'huobi': { + 'pair': 'BTC/USDT', + 'stake_currency': 'USDT', + 'hasQuoteVolume': True, + 'timeframe': '5m', + }, 'bitvavo': { 'pair': 'BTC/EUR', 'stake_currency': 'EUR', @@ -140,7 +146,10 @@ class TestCCXTExchange(): else: next_limit = exchange.get_next_limit_in_list( val, l2_limit_range, l2_limit_range_required) - if next_limit is None or next_limit > 200: + if next_limit is None: + assert len(l2['asks']) > 100 + assert len(l2['asks']) > 100 + elif next_limit > 200: # Large orderbook sizes can be a problem for some exchanges (bitrex ...) assert len(l2['asks']) > 200 assert len(l2['asks']) > 200 From f26247e8e026901cc74c7602107fbb57d5115c23 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Mar 2022 19:08:04 +0100 Subject: [PATCH 29/37] Revert wrong version string --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 54cecbec2..2747efc96 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2022.1' +__version__ = 'develop' if __version__ == 'develop': From a2c9879375e744b1bd7fba216f61c302ca45294e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Mar 2022 19:30:16 +0100 Subject: [PATCH 30/37] Reset sell-reason if order is cancelled --- freqtrade/freqtradebot.py | 1 + tests/test_freqtradebot.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 99872ff0b..8e26c4c3a 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1108,6 +1108,7 @@ class FreqtradeBot(LoggingMixin): trade.close_date = None trade.is_open = True trade.open_order_id = None + trade.sell_reason = None else: # TODO: figure out how to handle partially complete sell orders reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index d7b47174b..997ec5159 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2549,9 +2549,12 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None: exchange='binance', open_rate=0.245441, open_order_id="123456", - open_date=arrow.utcnow().datetime, + open_date=arrow.utcnow().shift(days=-2).datetime, fee_open=fee.return_value, fee_close=fee.return_value, + close_rate=0.555, + close_date=arrow.utcnow().datetime, + sell_reason="sell_reason_whatever", ) order = {'remaining': 1, 'amount': 1, @@ -2560,6 +2563,8 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None: assert freqtrade.handle_cancel_exit(trade, order, reason) assert cancel_order_mock.call_count == 1 assert send_msg_mock.call_count == 1 + assert trade.close_rate == None + assert trade.sell_reason is None send_msg_mock.reset_mock() From 69cfb0b278d1d78b528216ecd1f4ab4bc995014e Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Mar 2022 19:32:25 +0100 Subject: [PATCH 31/37] Revert change to telegram - this should be handled at the source --- freqtrade/rpc/telegram.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a4310e3a0..69f7f2858 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -452,8 +452,7 @@ class Telegram(RPCHandler): "*Current Pair:* {pair}", "*Amount:* `{amount} ({stake_amount} {base_currency})`", "*Entry Tag:* `{buy_tag}`" if r['buy_tag'] else "", - "*Exit Reason:* `{sell_reason}`" - if (r['sell_reason'] and not r['is_open']) else "", + "*Exit Reason:* `{sell_reason}`" if r['sell_reason'] else "", ] if position_adjust: From 54165662cec1e7b53ded4f00769ae94b02ef3ee9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Mar 2022 19:41:26 +0100 Subject: [PATCH 32/37] Don't require unfilledtimeout, it's optional. --- freqtrade/constants.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 077be51f7..066a07c62 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -440,7 +440,6 @@ SCHEMA_TRADE_REQUIRED = [ 'dry_run_wallet', 'ask_strategy', 'bid_strategy', - 'unfilledtimeout', 'stoploss', 'minimal_roi', 'internals', @@ -456,7 +455,6 @@ SCHEMA_BACKTEST_REQUIRED = [ 'dry_run_wallet', 'dataformat_ohlcv', 'dataformat_trades', - 'unfilledtimeout', ] SCHEMA_MINIMAL_REQUIRED = [ From f74de1cca34ae22e7d173feeac8dd375b812ee72 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 1 Mar 2022 19:46:13 +0100 Subject: [PATCH 33/37] Improve Backtesting "wrong setup" message to include tradable_balance --- freqtrade/commands/optimize_commands.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/freqtrade/commands/optimize_commands.py b/freqtrade/commands/optimize_commands.py index f230b696c..1bfd384fc 100644 --- a/freqtrade/commands/optimize_commands.py +++ b/freqtrade/commands/optimize_commands.py @@ -25,12 +25,16 @@ def setup_optimize_configuration(args: Dict[str, Any], method: RunMode) -> Dict[ RunMode.HYPEROPT: 'hyperoptimization', } if method in no_unlimited_runmodes.keys(): + wallet_size = config['dry_run_wallet'] * config['tradable_balance_ratio'] + # tradable_balance_ratio if (config['stake_amount'] != constants.UNLIMITED_STAKE_AMOUNT - and config['stake_amount'] > config['dry_run_wallet']): - wallet = round_coin_value(config['dry_run_wallet'], config['stake_currency']) + and config['stake_amount'] > wallet_size): + wallet = round_coin_value(wallet_size, config['stake_currency']) stake = round_coin_value(config['stake_amount'], config['stake_currency']) - raise OperationalException(f"Starting balance ({wallet}) " - f"is smaller than stake_amount {stake}.") + raise OperationalException( + f"Starting balance ({wallet}) is smaller than stake_amount {stake}. " + f"Wallet is calculated as `dry_run_wallet * tradable_balance_ratio`." + ) return config From abc8854b5a1b5d3161241685b26509329db5c4be Mon Sep 17 00:00:00 2001 From: Sam Germain Date: Tue, 1 Mar 2022 17:36:11 -0600 Subject: [PATCH 34/37] setup.sh install gettext for mac --- setup.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.sh b/setup.sh index c642a654d..865a1cc89 100755 --- a/setup.sh +++ b/setup.sh @@ -132,6 +132,9 @@ function install_macos() { echo_block "Installing Brew" /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" fi + + brew install gettext + #Gets number after decimal in python version version=$(egrep -o 3.\[0-9\]+ <<< $PYTHON | sed 's/3.//g') From 71be547d82629d748436f34c19097094d3313e87 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Mar 2022 06:26:00 +0100 Subject: [PATCH 35/37] Bump ccxt to 1.74.63 closes #6484 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index c524eb49d..bcdcb02a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ numpy==1.22.2 pandas==1.4.1 pandas-ta==0.3.14b -ccxt==1.74.43 +ccxt==1.74.63 # Pin cryptography for now due to rust build errors with piwheels cryptography==36.0.1 aiohttp==3.8.1 From 17c9c3caf33d14abfce3a03ec19774a88193f01d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 26 Feb 2022 16:10:54 +0100 Subject: [PATCH 36/37] Enable orders via API --- freqtrade/persistence/models.py | 7 ++++--- freqtrade/rpc/api_server/api_schemas.py | 18 ++++++++++++++++++ freqtrade/rpc/api_server/api_v1.py | 3 ++- tests/rpc/test_rpc.py | 4 ++-- tests/rpc/test_rpc_apiserver.py | 3 +++ 5 files changed, 29 insertions(+), 6 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 559c7e94a..39f430124 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -120,7 +120,7 @@ class Order(_DECL_BASE): ft_pair: str = Column(String(25), nullable=False) ft_is_open = Column(Boolean, nullable=False, default=True, index=True) - order_id = Column(String(255), nullable=False, index=True) + order_id: str = Column(String(255), nullable=False, index=True) status = Column(String(255), nullable=True) symbol = Column(String(25), nullable=True) order_type: str = Column(String(50), nullable=True) @@ -193,6 +193,9 @@ class Order(_DECL_BASE): def to_json(self) -> Dict[str, Any]: return { + 'pair': self.ft_pair, + 'order_id': self.order_id, + 'status': self.status, 'amount': self.amount, 'average': round(self.average, 8) if self.average else 0, 'safe_price': self.safe_price, @@ -209,10 +212,8 @@ class Order(_DECL_BASE): 'order_filled_timestamp': int(self.order_filled_date.replace( tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None, 'order_type': self.order_type, - 'pair': self.ft_pair, 'price': self.price, 'remaining': self.remaining, - 'status': self.status, } def close_bt_order(self, close_date: datetime): diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index e22cf82b3..2914cd688 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -177,6 +177,22 @@ class ShowConfig(BaseModel): max_entry_position_adjustment: int +class OrderSchema(BaseModel): + pair: str + order_id: str + status: str + remaining: float + amount: float + safe_price: float + cost: float + filled: float + ft_order_side: str + order_type: str + is_open: bool + order_timestamp: Optional[int] + order_filled_timestamp: Optional[int] + + class TradeSchema(BaseModel): trade_id: int pair: str @@ -224,6 +240,8 @@ class TradeSchema(BaseModel): min_rate: Optional[float] max_rate: Optional[float] open_order_id: Optional[str] + filled_entry_orders: List[OrderSchema] + filled_exit_orders: List[OrderSchema] class OpenTradeSchema(TradeSchema): diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index f072e2b14..6379150ee 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -32,7 +32,8 @@ logger = logging.getLogger(__name__) # 1.11: forcebuy and forcesell accept ordertype # 1.12: add blacklist delete endpoint # 1.13: forcebuy supports stake_amount -API_VERSION = 1.13 +# 1.14: Add entry/exit orders to trade response +API_VERSION = 1.14 # Public API, requires no auth. router_public = APIRouter() diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 7d0704d2f..8b3865172 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -114,7 +114,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy', 'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY, 'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05, - 'is_open': False, 'pair': 'ETH/BTC', + 'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY, 'remaining': ANY, 'status': ANY}], 'filled_exit_orders': [] } @@ -189,7 +189,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy', 'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY, 'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05, - 'is_open': False, 'pair': 'ETH/BTC', + 'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY, 'remaining': ANY, 'status': ANY}], 'filled_exit_orders': [] } diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index de7dca47b..d78ab2b39 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -902,6 +902,9 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'buy_tag': None, 'timeframe': 5, 'exchange': 'binance', + 'filled_entry_orders': [], + 'filled_exit_orders': [], + } mocker.patch('freqtrade.exchange.Exchange.get_rate', From e9456cdf15d834461f259f4942a70930aff71717 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 27 Feb 2022 16:54:14 +0100 Subject: [PATCH 37/37] Update trade response to use a single Order object --- freqtrade/persistence/models.py | 12 ++---------- freqtrade/rpc/api_server/api_schemas.py | 3 +-- freqtrade/rpc/telegram.py | 8 +++++--- tests/rpc/test_rpc.py | 6 ++---- tests/rpc/test_rpc_apiserver.py | 4 ++-- tests/rpc/test_rpc_telegram.py | 2 +- tests/test_persistence.py | 6 ++---- 7 files changed, 15 insertions(+), 26 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 39f430124..af093a1eb 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -340,14 +340,7 @@ class LocalTrade(): def to_json(self) -> Dict[str, Any]: filled_orders = self.select_filled_orders() - filled_entries = [] - filled_exits = [] - if len(filled_orders) > 0: - for order in filled_orders: - if order.ft_order_side == 'buy': - filled_entries.append(order.to_json()) - if order.ft_order_side == 'sell': - filled_exits.append(order.to_json()) + orders = [order.to_json() for order in filled_orders] return { 'trade_id': self.id, @@ -412,8 +405,7 @@ class LocalTrade(): 'max_rate': self.max_rate, 'open_order_id': self.open_order_id, - 'filled_entry_orders': filled_entries, - 'filled_exit_orders': filled_exits, + 'orders': orders, } @staticmethod diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 2914cd688..32c7e9214 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -240,8 +240,7 @@ class TradeSchema(BaseModel): min_rate: Optional[float] max_rate: Optional[float] open_order_id: Optional[str] - filled_entry_orders: List[OrderSchema] - filled_exit_orders: List[OrderSchema] + orders: List[OrderSchema] class OpenTradeSchema(TradeSchema): diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 69f7f2858..5a20520dd 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -379,6 +379,8 @@ class Telegram(RPCHandler): first_avg = filled_orders[0]["safe_price"] for x, order in enumerate(filled_orders): + if order['ft_order_side'] != 'buy': + continue cur_entry_datetime = arrow.get(order["order_filled_date"]) cur_entry_amount = order["amount"] cur_entry_average = order["safe_price"] @@ -444,7 +446,7 @@ class Telegram(RPCHandler): messages = [] for r in results: r['open_date_hum'] = arrow.get(r['open_date']).humanize() - r['num_entries'] = len(r['filled_entry_orders']) + r['num_entries'] = len([o for o in r['orders'] if o['ft_order_side'] == 'buy']) r['sell_reason'] = r.get('sell_reason', "") lines = [ "*Trade ID:* `{trade_id}`" + @@ -488,8 +490,8 @@ class Telegram(RPCHandler): lines.append("*Open Order:* `{open_order}`") lines_detail = self._prepare_entry_details( - r['filled_entry_orders'], r['base_currency'], r['is_open']) - lines.extend((lines_detail if (len(r['filled_entry_orders']) > 1) else "")) + r['orders'], r['base_currency'], r['is_open']) + lines.extend(lines_detail if lines_detail else "") # Filter empty lines using list-comprehension messages.append("\n".join([line for line in lines if line]).format(**r)) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 8b3865172..6bfee8e86 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -109,14 +109,13 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, 'exchange': 'binance', - 'filled_entry_orders': [{ + 'orders': [{ 'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05, 'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy', 'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY, 'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05, 'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY, 'remaining': ANY, 'status': ANY}], - 'filled_exit_orders': [] } mocker.patch('freqtrade.exchange.Exchange.get_rate', @@ -184,14 +183,13 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, 'exchange': 'binance', - 'filled_entry_orders': [{ + 'orders': [{ 'amount': 91.07468123, 'average': 1.098e-05, 'safe_price': 1.098e-05, 'cost': 0.0009999999999054, 'filled': 91.07468123, 'ft_order_side': 'buy', 'order_date': ANY, 'order_timestamp': ANY, 'order_filled_date': ANY, 'order_filled_timestamp': ANY, 'order_type': 'limit', 'price': 1.098e-05, 'is_open': False, 'pair': 'ETH/BTC', 'order_id': ANY, 'remaining': ANY, 'status': ANY}], - 'filled_exit_orders': [] } diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index d78ab2b39..84a18440e 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -902,8 +902,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'buy_tag': None, 'timeframe': 5, 'exchange': 'binance', - 'filled_entry_orders': [], - 'filled_exit_orders': [], + 'orders': [ANY], } @@ -1092,6 +1091,7 @@ def test_api_forcebuy(botclient, mocker, fee): 'buy_tag': None, 'timeframe': 5, 'exchange': 'binance', + 'orders': [], } diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index ccf61f91b..f53f48cc2 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -203,7 +203,7 @@ def test_telegram_status(default_conf, update, mocker) -> None: 'stop_loss_ratio': -0.0001, 'open_order': '(limit buy rem=0.00000000)', 'is_open': True, - 'filled_entry_orders': [] + 'orders': [] }]), ) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 0f00bd4bb..32253d1cb 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -901,8 +901,7 @@ def test_to_json(default_conf, fee): 'buy_tag': None, 'timeframe': None, 'exchange': 'binance', - 'filled_entry_orders': [], - 'filled_exit_orders': [] + 'orders': [], } # Simulate dry_run entries @@ -970,8 +969,7 @@ def test_to_json(default_conf, fee): 'buy_tag': 'buys_signal_001', 'timeframe': None, 'exchange': 'binance', - 'filled_entry_orders': [], - 'filled_exit_orders': [] + 'orders': [], }