From 5938514e5d08edfe278760578ac75a4b57b2b4fc Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Sep 2021 07:03:26 +0200 Subject: [PATCH 01/49] Version bump to 2021.9 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 2747efc96..0b6152bbf 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = 'develop' +__version__ = '2021.9' if __version__ == 'develop': From 98ed7edb1131a6111a3d72145d0fc26c0d72b634 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Oct 2021 06:21:40 +0200 Subject: [PATCH 02/49] Version bump to 2021.10 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 0b6152bbf..df3c5d4f6 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2021.9' +__version__ = '2021.10' if __version__ == 'develop': From 3f10430eb5070e7de6a7b64f4e209d3237e52315 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 28 Sep 2021 07:03:26 +0200 Subject: [PATCH 03/49] Version bump to 2021.9 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 2747efc96..0b6152bbf 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = 'develop' +__version__ = '2021.9' if __version__ == 'develop': From a9cdb428d0bf328351c0d602a5e64a93b9057ee0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 28 Oct 2021 06:21:40 +0200 Subject: [PATCH 04/49] Version bump to 2021.10 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 0b6152bbf..df3c5d4f6 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2021.9' +__version__ = '2021.10' if __version__ == 'develop': From 7e1eedd7dfa7deb01ffef1518591c6c7b8df2f6d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 27 Nov 2021 09:55:00 +0100 Subject: [PATCH 05/49] Version bump to 2021.11 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index df3c5d4f6..ee09a45f5 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2021.10' +__version__ = '2021.11' if __version__ == 'develop': From 043218cc7ee5eb08ce2e7d1c73fd18ec2099ce04 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 29 Dec 2021 16:18:14 +0100 Subject: [PATCH 06/49] Version bump to 2021.12 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index ee09a45f5..a18beb0a0 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2021.11' +__version__ = '2021.12' if __version__ == 'develop': From 2d45163f8f89e5b63298d3881bc80cc24cc89b70 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 2 Feb 2022 19:46:48 +0100 Subject: [PATCH 07/49] Bump version to 2022.1 --- freqtrade/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index a18beb0a0..54cecbec2 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,5 +1,5 @@ """ Freqtrade bot """ -__version__ = '2021.12' +__version__ = '2022.1' if __version__ == 'develop': From cd54f1536e16846985a4a639a74978e0dac71144 Mon Sep 17 00:00:00 2001 From: Robert Davey Date: Mon, 14 Feb 2022 16:41:58 +0000 Subject: [PATCH 08/49] Update windows_installation.md Update links to include just the cpp build tools instead of the 4GB full Visual Studio link. --- docs/windows_installation.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/windows_installation.md b/docs/windows_installation.md index 9a068e152..c9964a94c 100644 --- a/docs/windows_installation.md +++ b/docs/windows_installation.md @@ -54,6 +54,8 @@ error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Unfortunately, many packages requiring compilation don't provide a pre-built wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use. -The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building C code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker compose](docker_quickstart.md) first. +You can download the Visual C++ build tools from [here](https://visualstudio.microsoft.com/visual-cpp-build-tools/). However, the easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building C code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or [docker compose](docker_quickstart.md) first. + + --- From df726a54f89af8f99cb5dd3969ba280187d22d58 Mon Sep 17 00:00:00 2001 From: Stefano Ariestasia Date: Fri, 25 Feb 2022 00:20:53 +0000 Subject: [PATCH 09/49] 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 3b1b66bee8a1bf3f58080a4ab74406e901834718 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Feb 2022 07:40:49 +0100 Subject: [PATCH 10/49] Prevent backtest starting when not in webserver mode #6455 --- freqtrade/rpc/api_server/api_backtest.py | 10 +++++----- freqtrade/rpc/api_server/deps.py | 7 +++++++ tests/rpc/test_rpc_apiserver.py | 5 +++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index 8b86b8005..757ed8aac 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -8,7 +8,7 @@ from freqtrade.configuration.config_validation import validate_config_consistenc from freqtrade.enums import BacktestState from freqtrade.exceptions import DependencyException from freqtrade.rpc.api_server.api_schemas import BacktestRequest, BacktestResponse -from freqtrade.rpc.api_server.deps import get_config +from freqtrade.rpc.api_server.deps import get_config, is_webserver_mode from freqtrade.rpc.api_server.webserver import ApiServer from freqtrade.rpc.rpc import RPCException @@ -22,7 +22,7 @@ router = APIRouter() @router.post('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) # flake8: noqa: C901 async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: BackgroundTasks, - config=Depends(get_config)): + config=Depends(get_config), ws_mode=Depends(is_webserver_mode)): """Start backtesting if not done so already""" if ApiServer._bgtask_running: raise RPCException('Bot Background task already running') @@ -121,7 +121,7 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac @router.get('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) -def api_get_backtest(): +def api_get_backtest(ws_mode=Depends(is_webserver_mode)): """ Get backtesting result. Returns Result after backtesting has been ran. @@ -157,7 +157,7 @@ def api_get_backtest(): @router.delete('/backtest', response_model=BacktestResponse, tags=['webserver', 'backtest']) -def api_delete_backtest(): +def api_delete_backtest(ws_mode=Depends(is_webserver_mode)): """Reset backtesting""" if ApiServer._bgtask_running: return { @@ -183,7 +183,7 @@ def api_delete_backtest(): @router.get('/backtest/abort', response_model=BacktestResponse, tags=['webserver', 'backtest']) -def api_backtest_abort(): +def api_backtest_abort(ws_mode=Depends(is_webserver_mode)): if not ApiServer._bgtask_running: return { "status": "not_running", diff --git a/freqtrade/rpc/api_server/deps.py b/freqtrade/rpc/api_server/deps.py index b428d9c6d..f5e61602e 100644 --- a/freqtrade/rpc/api_server/deps.py +++ b/freqtrade/rpc/api_server/deps.py @@ -2,6 +2,7 @@ from typing import Any, Dict, Iterator, Optional from fastapi import Depends +from freqtrade.enums import RunMode from freqtrade.persistence import Trade from freqtrade.rpc.rpc import RPC, RPCException @@ -38,3 +39,9 @@ def get_exchange(config=Depends(get_config)): ApiServer._exchange = ExchangeResolver.load_exchange( config['exchange']['name'], config) return ApiServer._exchange + + +def is_webserver_mode(config=Depends(get_config)): + if config['runmode'] != RunMode.WEBSERVER: + raise RPCException('Bot is not in the correct state') + return None diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 544321860..de7dca47b 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1350,6 +1350,11 @@ def test_api_backtesting(botclient, mocker, fee, caplog, tmpdir): ftbot, client = botclient mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) + rc = client_get(client, f"{BASE_URI}/backtest") + # Backtest prevented in default mode + assert_response(rc, 502) + + ftbot.config['runmode'] = RunMode.WEBSERVER # Backtesting not started yet rc = client_get(client, f"{BASE_URI}/backtest") assert_response(rc) From 4b7271df467e7ea4ddedd79543fc7f26f1fdea2d Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Feb 2022 10:53:12 +0100 Subject: [PATCH 11/49] Improve wording, add Picture detailing what must be installed. --- docs/assets/windows_install.png | Bin 0 -> 94230 bytes docs/windows_installation.md | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 docs/assets/windows_install.png diff --git a/docs/assets/windows_install.png b/docs/assets/windows_install.png new file mode 100644 index 0000000000000000000000000000000000000000..530c3047f5016a2213cbbe2fd60ce230b36d18fd GIT binary patch literal 94230 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%0A!WDg^0_m7ISjQC$&-z;wC`( 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 Date: Fri, 25 Feb 2022 15:07:35 +0100 Subject: [PATCH 12/49] Don't emergencysell partial sold exit closes #6457 --- freqtrade/freqtradebot.py | 15 +++++++++------ tests/test_freqtradebot.py | 23 ++++++++++++++++------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 99872ff0b..20fd833eb 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -979,10 +979,10 @@ class FreqtradeBot(LoggingMixin): or (order_obj and self.strategy.ft_check_timed_out( 'sell', trade, order_obj, datetime.now(timezone.utc)) ))): - self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) + canceled = self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT']) canceled_count = trade.get_exit_order_count() max_timeouts = self.config.get('unfilledtimeout', {}).get('exit_timeout_count', 0) - if max_timeouts > 0 and canceled_count >= max_timeouts: + if canceled and max_timeouts > 0 and canceled_count >= max_timeouts: logger.warning(f'Emergencyselling trade {trade}, as the sell order ' f'timed out {max_timeouts} times.') try: @@ -1079,11 +1079,12 @@ class FreqtradeBot(LoggingMixin): reason=reason) return was_trade_fully_canceled - def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> str: + def handle_cancel_exit(self, trade: Trade, order: Dict, reason: str) -> bool: """ Sell cancel - cancel order and update trade - :return: Reason for cancel + :return: True if exit order was cancelled, false otherwise """ + cancelled = False # if trade is not partially completed, just cancel the order if order['remaining'] == order['amount'] or order.get('filled') == 0.0: if not self.exchange.check_order_canceled_empty(order): @@ -1094,7 +1095,7 @@ class FreqtradeBot(LoggingMixin): trade.update_order(co) except InvalidOrderException: logger.exception(f"Could not cancel sell order {trade.open_order_id}") - return 'error cancelling order' + return False logger.info('Sell order %s for %s.', reason, trade) else: reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE'] @@ -1108,9 +1109,11 @@ class FreqtradeBot(LoggingMixin): trade.close_date = None trade.is_open = True trade.open_order_id = None + cancelled = True else: # TODO: figure out how to handle partially complete sell orders reason = constants.CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] + cancelled = False self.wallets.update() self._notify_exit_cancel( @@ -1118,7 +1121,7 @@ class FreqtradeBot(LoggingMixin): order_type=self.strategy.order_types['sell'], reason=reason ) - return reason + return cancelled def _safe_exit_amount(self, pair: str, amount: float) -> float: """ diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index d7b47174b..d433998a1 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -6,7 +6,7 @@ import time from copy import deepcopy from math import isclose from typing import List -from unittest.mock import ANY, MagicMock, PropertyMock +from unittest.mock import ANY, MagicMock, PropertyMock, patch import arrow import pytest @@ -2220,9 +2220,14 @@ def test_check_handle_timedout_sell_usercustom(default_conf_usdt, ticker_usdt, l et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit') caplog.clear() - # 2nd canceled trade ... open_trade.open_order_id = limit_sell_order_old['id'] + + # If cancelling fails - no emergency sell! + with patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit', return_value=False): + freqtrade.check_handle_timedout() + assert et_mock.call_count == 0 + freqtrade.check_handle_timedout() assert log_has_re('Emergencyselling trade.*', caplog) assert et_mock.call_count == 1 @@ -2564,13 +2569,17 @@ def test_handle_cancel_exit_limit(mocker, default_conf_usdt, fee) -> None: send_msg_mock.reset_mock() order['amount'] = 2 - assert freqtrade.handle_cancel_exit(trade, order, reason - ) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] + assert not freqtrade.handle_cancel_exit(trade, order, reason) # Assert cancel_order was not called (callcount remains unchanged) assert cancel_order_mock.call_count == 1 assert send_msg_mock.call_count == 1 - assert freqtrade.handle_cancel_exit(trade, order, reason - ) == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] + assert (send_msg_mock.call_args_list[0][0][0]['reason'] + == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN']) + + assert not freqtrade.handle_cancel_exit(trade, order, reason) + + send_msg_mock.call_args_list[0][0][0]['reason'] = CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] + # Message should not be iterated again assert trade.sell_order_status == CANCEL_REASON['PARTIALLY_FILLED_KEEP_OPEN'] assert send_msg_mock.call_count == 1 @@ -2589,7 +2598,7 @@ def test_handle_cancel_exit_cancel_exception(mocker, default_conf_usdt) -> None: order = {'remaining': 1, 'amount': 1, 'status': "open"} - assert freqtrade.handle_cancel_exit(trade, order, reason) == 'error cancelling order' + assert not freqtrade.handle_cancel_exit(trade, order, reason) def test_execute_trade_exit_up(default_conf_usdt, ticker_usdt, fee, ticker_usdt_sell_up, mocker From a0b42c7aa2ef018188e5a437a203065f9f25b900 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 25 Feb 2022 20:47:15 +0100 Subject: [PATCH 13/49] 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 14/49] 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 15/49] 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 16/49] 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 17/49] 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 18/49] 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 19/49] 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 20/49] 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 21/49] 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 22/49] 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 23/49] 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 24/49] 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 25/49] 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 26/49] 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 27/49] 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 28/49] 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 29/49] 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 30/49] 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 31/49] 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 32/49] 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 33/49] 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 34/49] 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 35/49] 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 36/49] 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 37/49] 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 38/49] 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 39/49] 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 40/49] 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 41/49] 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 42/49] 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 43/49] 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 44/49] 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 45/49] 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 46/49] 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 47/49] 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 48/49] 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': [], } From c0e12d632f269b3043ccdc08c4355733de4daa6e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 3 Mar 2022 19:19:10 +0100 Subject: [PATCH 49/49] Add FTX ref links --- README.md | 2 +- docs/index.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 166e4833a..efa334a27 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ Please read the [exchange specific notes](docs/exchanges.md) to learn about even - [X] [Binance](https://www.binance.com/) - [X] [Bittrex](https://bittrex.com/) -- [X] [FTX](https://ftx.com) +- [X] [FTX](https://ftx.com/#a=2258149) - [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [Huobi](http://huobi.com/) - [X] [Kraken](https://kraken.com/) diff --git a/docs/index.md b/docs/index.md index 32b19bd94..2aa80c240 100644 --- a/docs/index.md +++ b/docs/index.md @@ -44,7 +44,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual, - [X] [Binance](https://www.binance.com/) - [X] [Bittrex](https://bittrex.com/) -- [X] [FTX](https://ftx.com) +- [X] [FTX](https://ftx.com/#a=2258149) - [X] [Gate.io](https://www.gate.io/ref/6266643) - [X] [Huobi](http://huobi.com/) - [X] [Kraken](https://kraken.com/)