From c176e277f117badcbe495750707af79e07717b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Mon, 5 Apr 2021 19:31:34 +0200 Subject: [PATCH 01/73] Add a REST endpoint for getting a specific trade --- freqtrade/rpc/api_server/api_v1.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index b983402e9..6873c0c4c 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -88,6 +88,11 @@ def trades(limit: int = 0, rpc: RPC = Depends(get_rpc)): return rpc._rpc_trade_history(limit) +@router.get('/trade/{tradeid}', response_model=OpenTradeSchema, tags=['info', 'trading']) +def trade(tradeid: int = 0, rpc: RPC = Depends(get_rpc)): + return rpc._rpc_trade_status([tradeid])[0] + + @router.delete('/trades/{tradeid}', response_model=DeleteTrade, tags=['info', 'trading']) def trades_delete(tradeid: int, rpc: RPC = Depends(get_rpc)): return rpc._rpc_delete(tradeid) From ddba0d688e0b168054fda58b0267810ff13fad98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Mon, 5 Apr 2021 19:32:55 +0200 Subject: [PATCH 02/73] Add new trade endpoint to docs --- docs/rest-api.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/rest-api.md b/docs/rest-api.md index c41c3f24c..4e784b6af 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -125,6 +125,7 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `stopbuy` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `reload_config` | Reloads the configuration file. | `trades` | List last trades. +| `trade/` | Get specific trade. | `delete_trade ` | Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange. | `show_config` | Shows part of the current configuration with relevant settings to operation. | `logs` | Shows last log messages. @@ -275,6 +276,10 @@ trades :param limit: Limits trades to the X last trades. No limit to get all the trades. +trade + Return specific trade. + :param tradeid: Specify which trade to get. + version Return the version of the bot. From fc78246bbc2a7f4260e1bf2bdd3d898c52c98110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mads=20S=C3=B8rensen?= Date: Mon, 5 Apr 2021 19:34:01 +0200 Subject: [PATCH 03/73] Some changes to rest-api docs --- docs/rest-api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 4e784b6af..be3107fcb 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -182,7 +182,7 @@ count Return the amount of open trades. daily - Return the amount of open trades. + Return the profits for each day, and amount of trades. delete_lock Delete (disable) lock from the database. @@ -215,7 +215,7 @@ locks logs Show latest logs. - :param limit: Limits log messages to the last logs. No limit to get all the trades. + :param limit: Limits log messages to the last logs. No limit to get the entire log. pair_candles Return live dataframe for . From 906c4e64d335e6d401eac2c85d3fa0f0b2319f46 Mon Sep 17 00:00:00 2001 From: Ugur Cem Ozturk Date: Sun, 11 Apr 2021 15:38:08 +0300 Subject: [PATCH 04/73] chore(readme): Fix markdown of docker manual Link to docker-compose was pointing to the one from develop branch. It's changed as with the stable docker-compose. --- docs/docker_quickstart.md | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index ca0515281..b133e33f0 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -22,10 +22,10 @@ Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.co ### Docker quick start -Create a new directory and place the [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) in this directory. +Create a new directory and place the [docker-compose file](https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml) in this directory. -=== "PC/MAC/Linux" - ``` bash +#### PC/MAC/Linux + mkdir ft_userdata cd ft_userdata/ # Download the docker-compose file from the repository @@ -39,10 +39,10 @@ Create a new directory and place the [docker-compose file](https://github.com/fr # Create configuration - Requires answering interactive questions docker-compose run --rm freqtrade new-config --config user_data/config.json - ``` + + +#### RaspberryPi -=== "RaspberryPi" - ``` bash mkdir ft_userdata cd ft_userdata/ # Download the docker-compose file from the repository @@ -56,7 +56,6 @@ Create a new directory and place the [docker-compose file](https://github.com/fr # Create configuration - Requires answering interactive questions docker-compose run --rm freqtrade new-config --config user_data/config.json - ``` !!! Note "Change your docker Image" You have to change the docker image in the docker-compose file for your Raspberry build to work properly. @@ -68,12 +67,11 @@ Create a new directory and place the [docker-compose file](https://github.com/fr The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image. The last 2 steps in the snippet create the directory with `user_data`, as well as (interactively) the default configuration based on your selections. -!!! Question "How to edit the bot configuration?" - You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. +### Question: "How to edit the bot configuration?" +You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. +You can also change the both Strategy and commands by editing the command section of your `docker-compose.yml` file. - You can also change the both Strategy and commands by editing the command section of your `docker-compose.yml` file. - -#### Adding a custom strategy +##### Adding a custom strategy 1. The configuration is now available as `user_data/config.json` 2. Copy a custom strategy to the directory `user_data/strategies/` @@ -81,7 +79,7 @@ The last 2 steps in the snippet create the directory with `user_data`, as well a The `SampleStrategy` is run by default. -!!! Warning "`SampleStrategy` is just a demo!" +#### Warning "`SampleStrategy` is just a demo!" The `SampleStrategy` is there for your reference and give you ideas for your own strategy. Please always backtest your strategy and use dry-run for some time before risking real money! You will find more information about Strategy development in the [Strategy documentation](strategy-customization.md). @@ -92,7 +90,7 @@ Once this is done, you're ready to launch the bot in trading mode (Dry-run or Li docker-compose up -d ``` -!!! Warning "Default configuration" +#### Warning "Default configuration" While the configuration generated will be mostly functional, you will still need to verify that all options correspond to what you want (like Pricing, pairlist, ...) before starting the bot. #### Monitoring the bot @@ -122,7 +120,7 @@ docker-compose up -d This will first pull the latest image, and will then restart the container with the just pulled version. -!!! Warning "Check the Changelog" +#### Warning "Check the Changelog" You should always check the changelog for breaking changes / manual interventions required and make sure the bot starts correctly after the update. ### Editing the docker-compose file @@ -131,7 +129,7 @@ Advanced users may edit the docker-compose file further to include all possible All freqtrade arguments will be available by running `docker-compose run --rm freqtrade `. -!!! Note "`docker-compose run --rm`" +#### Note "`docker-compose run --rm`" Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). #### Example: Download data with docker-compose From 37c8fd6ad799ed0320147304304acdad3e18e0b4 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 11:55:03 +0200 Subject: [PATCH 05/73] Remove arrow from models.py --- freqtrade/persistence/models.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 8b4aa325a..49d3e2d62 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -6,7 +6,6 @@ from datetime import datetime, timezone from decimal import Decimal from typing import Any, Dict, List, Optional -import arrow from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, String, create_engine, desc, func, inspect) from sqlalchemy.exc import NoSuchModuleError @@ -160,8 +159,8 @@ class Order(_DECL_BASE): if self.status in ('closed', 'canceled', 'cancelled'): self.ft_is_open = False if order.get('filled', 0) > 0: - self.order_filled_date = arrow.utcnow().datetime - self.order_update_date = arrow.utcnow().datetime + self.order_filled_date = datetime.now(timezone.utc) + self.order_update_date = datetime.now(timezone.utc) @staticmethod def update_orders(orders: List['Order'], order: Dict[str, Any]): From 638cd4e8f13b963b03beec0b311f520cf35f0185 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 12:04:22 +0200 Subject: [PATCH 06/73] Upgrade cleanup action to latest version --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 102e6ed78..4169661c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -301,7 +301,7 @@ jobs: runs-on: ubuntu-20.04 steps: - name: Cleanup previous runs on this branch - uses: rokroskar/workflow-run-cleanup-action@v0.2.2 + uses: rokroskar/workflow-run-cleanup-action@v0.3.2 if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/stable' && github.repository == 'freqtrade/freqtrade'" env: GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" From e4bb6b158294a0b6b36ac5b9be41f93c9bbedafe Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 12:28:07 +0200 Subject: [PATCH 07/73] Add kucoin exchange subclass Kucoin has some specific orderbook restrictions closes #4723 --- freqtrade/exchange/__init__.py | 1 + freqtrade/exchange/bittrex.py | 4 ---- freqtrade/exchange/exchange.py | 14 +++++++++++--- freqtrade/exchange/kucoin.py | 24 ++++++++++++++++++++++++ tests/exchange/test_ccxt_compat.py | 13 ++++++++++--- tests/exchange/test_exchange.py | 3 +++ 6 files changed, 49 insertions(+), 10 deletions(-) create mode 100644 freqtrade/exchange/kucoin.py diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 8a5563623..889bb49c2 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -15,3 +15,4 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, validate_exchanges) from freqtrade.exchange.ftx import Ftx from freqtrade.exchange.kraken import Kraken +from freqtrade.exchange.kucoin import Kucoin diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index fd7d47668..69e2f2b8d 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -12,10 +12,6 @@ class Bittrex(Exchange): """ Bittrex exchange class. Contains adjustments needed for Freqtrade to work with this exchange. - - Please note that this exchange is not included in the list of exchanges - officially supported by the Freqtrade development team. So some features - may still not work as expected. """ _ft_has: Dict = { diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3224255d0..e52e0e0d0 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -63,6 +63,7 @@ class Exchange: "trades_pagination": "time", # Possible are "time" or "id" "trades_pagination_arg": "since", "l2_limit_range": None, + "l2_limit_range_required": True, # Allow Empty L2 limit (kucoin) } _ft_has: Dict = {} @@ -1154,14 +1155,20 @@ class Exchange: return self.fetch_order(order_id, pair) @staticmethod - def get_next_limit_in_list(limit: int, limit_range: Optional[List[int]]): + def get_next_limit_in_list(limit: int, limit_range: Optional[List[int]], + range_required: bool = True): """ Get next greater value in the list. Used by fetch_l2_order_book if the api only supports a limited range """ if not limit_range: return limit - return min([x for x in limit_range if limit <= x] + [max(limit_range)]) + + result = min([x for x in limit_range if limit <= x] + [max(limit_range)]) + if not range_required and limit > result: + # Range is not required - we can use None as parameter. + return None + return result @retrier def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict: @@ -1171,7 +1178,8 @@ class Exchange: Returns a dict in the format {'asks': [price, volume], 'bids': [price, volume]} """ - limit1 = self.get_next_limit_in_list(limit, self._ft_has['l2_limit_range']) + limit1 = self.get_next_limit_in_list(limit, self._ft_has['l2_limit_range'], + self._ft_has['l2_limit_range_required']) try: return self._api.fetch_l2_order_book(pair, limit1) diff --git a/freqtrade/exchange/kucoin.py b/freqtrade/exchange/kucoin.py new file mode 100644 index 000000000..22886a1d8 --- /dev/null +++ b/freqtrade/exchange/kucoin.py @@ -0,0 +1,24 @@ +""" Kucoin exchange subclass """ +import logging +from typing import Dict + +from freqtrade.exchange import Exchange + + +logger = logging.getLogger(__name__) + + +class Kucoin(Exchange): + """ + Kucoin exchange class. Contains adjustments needed for Freqtrade to work + with this exchange. + + Please note that this exchange is not included in the list of exchanges + officially supported by the Freqtrade development team. So some features + may still not work as expected. + """ + + _ft_has: Dict = { + "l2_limit_range": [20, 100], + "l2_limit_range_required": False, + } diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index 870e6cabd..dce10da84 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -36,7 +36,12 @@ EXCHANGES = { 'pair': 'BTC/USDT', 'hasQuoteVolume': True, 'timeframe': '5m', - } + }, + 'kucoin': { + 'pair': 'BTC/USDT', + 'hasQuoteVolume': True, + 'timeframe': '5m', + }, } @@ -100,14 +105,16 @@ class TestCCXTExchange(): assert 'asks' in l2 assert 'bids' in l2 l2_limit_range = exchange._ft_has['l2_limit_range'] + l2_limit_range_required = exchange._ft_has['l2_limit_range_required'] for val in [1, 2, 5, 25, 100]: l2 = exchange.fetch_l2_order_book(pair, val) if not l2_limit_range or val in l2_limit_range: assert len(l2['asks']) == val assert len(l2['bids']) == val else: - next_limit = exchange.get_next_limit_in_list(val, l2_limit_range) - if next_limit > 200: + 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: # Large orderbook sizes can be a problem for some exchanges (bitrex ...) assert len(l2['asks']) > 200 assert len(l2['asks']) > 200 diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 4ceba6eba..db67d038c 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1641,6 +1641,9 @@ def test_get_next_limit_in_list(): # Going over the limit ... assert Exchange.get_next_limit_in_list(1001, limit_range) == 1000 assert Exchange.get_next_limit_in_list(2000, limit_range) == 1000 + # Without required range + assert Exchange.get_next_limit_in_list(2000, limit_range, False) is None + assert Exchange.get_next_limit_in_list(15, limit_range, False) == 20 assert Exchange.get_next_limit_in_list(21, None) == 21 assert Exchange.get_next_limit_in_list(100, None) == 100 From 521e48c94a5e5b18ba409d965757c7b8f509f08f Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 13:55:08 +0200 Subject: [PATCH 08/73] Add doc section for Kucoin part of #4723 --- docs/exchanges.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/exchanges.md b/docs/exchanges.md index 1c5956088..662f2b908 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -100,6 +100,18 @@ To use subaccounts with FTX, you need to edit the configuration and add the foll } ``` +## Kucoin + +Kucoin requries a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows: + +```json +"exchange": { + "name": "kucoin", + "key": "your_exchange_key", + "secret": "your_exchange_secret", + "password": "your_exchange_api_key_password", +``` + ## All exchanges Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys. From 82d66410f74b47f25cb4af60b702cad6fdebcae8 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 19:20:57 +0200 Subject: [PATCH 09/73] Fix /performance output if multiple messages are necessary closes #4726 --- freqtrade/rpc/telegram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index a8c629149..09b7b235c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -702,7 +702,7 @@ class Telegram(RPCHandler): f"({trade['count']})\n") if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH: - self._send_msg(output) + self._send_msg(output, parse_mode=ParseMode.HTML) output = stat_line else: output += stat_line From c2f35ce416cf8d6e6c5806dd1a3e624a124d3843 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 20:09:22 +0200 Subject: [PATCH 10/73] /balance should use cached tickers when possible --- freqtrade/exchange/exchange.py | 18 ++++++++++++++++-- freqtrade/rpc/rpc.py | 2 +- tests/exchange/test_exchange.py | 8 ++++++++ tests/rpc/test_rpc.py | 2 ++ 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e52e0e0d0..3627a07e4 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -14,6 +14,7 @@ from typing import Any, Dict, List, Optional, Tuple import arrow import ccxt import ccxt.async_support as ccxt_async +from cachetools import TTLCache from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision) from pandas import DataFrame @@ -84,6 +85,9 @@ class Exchange: # Timestamp of last markets refresh self._last_markets_refresh: int = 0 + # Cache for 10 minutes ... + self._fetch_tickers_cache = TTLCache(maxsize=1, ttl=60 * 10) + # Holds candles self._klines: Dict[Tuple[str, str], DataFrame] = {} @@ -693,9 +697,19 @@ class Exchange: raise OperationalException(e) from e @retrier - def get_tickers(self) -> Dict: + def get_tickers(self, cached: bool = False) -> Dict: + """ + :param cached: Allow cached result + :return: fetch_tickers result + """ + if cached: + tickers = self._fetch_tickers_cache.get('fetch_tickers') + if tickers: + return tickers try: - return self._api.fetch_tickers() + tickers = self._api.fetch_tickers() + self._fetch_tickers_cache['fetch_tickers'] = tickers + return tickers except ccxt.NotSupported as e: raise OperationalException( f'Exchange {self._api.name} does not support fetching tickers in batch. ' diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 59758a573..88f8b36db 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -442,7 +442,7 @@ class RPC: output = [] total = 0.0 try: - tickers = self._freqtrade.exchange.get_tickers() + tickers = self._freqtrade.exchange.get_tickers(cached=True) except (ExchangeError): raise RPCException('Error getting current tickers.') diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index db67d038c..76095be2d 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1319,6 +1319,14 @@ def test_get_tickers(default_conf, mocker, exchange_name): assert tickers['ETH/BTC']['ask'] == 1 assert tickers['BCH/BTC']['bid'] == 0.6 assert tickers['BCH/BTC']['ask'] == 0.5 + assert api_mock.fetch_tickers.call_count == 1 + + api_mock.fetch_tickers.reset_mock() + + # Cached ticker should not call api again + tickers2 = exchange.get_tickers(cached=True) + assert tickers2 == tickers + assert api_mock.fetch_tickers.call_count == 0 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, "get_tickers", "fetch_tickers") diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index a97f6b65e..199845545 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -569,6 +569,8 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): result = rpc._rpc_balance(default_conf['stake_currency'], default_conf['fiat_display_currency']) assert prec_satoshi(result['total'], 12.309096315) assert prec_satoshi(result['value'], 184636.44472997) + assert tickers.call_count == 1 + assert tickers.call_args.kwargs['cached'] is True assert 'USD' == result['symbol'] assert result['currencies'] == [ {'currency': 'BTC', From c316531c491c995c62bda57bda177669b8d41690 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 21:54:06 +0200 Subject: [PATCH 11/73] make tests 3.7 compatible --- tests/exchange/test_exchange.py | 2 ++ tests/rpc/test_rpc.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 76095be2d..882cf6b5a 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1327,6 +1327,8 @@ def test_get_tickers(default_conf, mocker, exchange_name): tickers2 = exchange.get_tickers(cached=True) assert tickers2 == tickers assert api_mock.fetch_tickers.call_count == 0 + tickers2 = exchange.get_tickers(cached=False) + assert api_mock.fetch_tickers.call_count == 1 ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name, "get_tickers", "fetch_tickers") diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 199845545..a548505a7 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -570,7 +570,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): assert prec_satoshi(result['total'], 12.309096315) assert prec_satoshi(result['value'], 184636.44472997) assert tickers.call_count == 1 - assert tickers.call_args.kwargs['cached'] is True + assert tickers.call_args[1]['cached'] is True assert 'USD' == result['symbol'] assert result['currencies'] == [ {'currency': 'BTC', From ba38e398e42359293dd13f00b7b44615771f5fd1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 13 Apr 2021 22:17:42 +0200 Subject: [PATCH 12/73] Add type hint --- freqtrade/exchange/exchange.py | 2 +- tests/rpc/test_rpc.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3627a07e4..3958f6838 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -86,7 +86,7 @@ class Exchange: self._last_markets_refresh: int = 0 # Cache for 10 minutes ... - self._fetch_tickers_cache = TTLCache(maxsize=1, ttl=60 * 10) + self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=1, ttl=60 * 10) # Holds candles self._klines: Dict[Tuple[str, str], DataFrame] = {} diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index a548505a7..63e09cbe1 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -570,7 +570,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): assert prec_satoshi(result['total'], 12.309096315) assert prec_satoshi(result['value'], 184636.44472997) assert tickers.call_count == 1 - assert tickers.call_args[1]['cached'] is True + assert tickers.call_args_list[0][1]['cached'] is True assert 'USD' == result['symbol'] assert result['currencies'] == [ {'currency': 'BTC', From 862df2b431021d43f8ab5fcba198cff07250d336 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Apr 2021 19:43:32 +0200 Subject: [PATCH 13/73] Add blacklist recommendation for kucoin closes #4738 --- docs/exchanges.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/exchanges.md b/docs/exchanges.md index 662f2b908..8797ade8c 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -7,10 +7,10 @@ This page combines common gotchas and informations which are exchange-specific a !!! 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. -### Blacklists +### Binance Blacklist For Binance, please add `"BNB/"` to your blacklist to avoid issues. -Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB order unsellable as the expected amount is not there anymore. +Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB trade unsellable as the expected amount is not there anymore. ### Binance sites @@ -112,6 +112,11 @@ Kucoin requries a passphrase for each api key, you will therefore need to add th "password": "your_exchange_api_key_password", ``` +### Kucoin Blacklists + +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. + ## All exchanges Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys. From e820814809f6bef69f1501f16fb797d841034dec Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Apr 2021 20:32:34 +0200 Subject: [PATCH 14/73] Default-stoploss-hyperopt should use decimal space, nto real --- freqtrade/optimize/hyperopt_interface.py | 3 ++- freqtrade/optimize/space/__init__.py | 4 ++++ freqtrade/optimize/{ => space}/decimalspace.py | 0 freqtrade/strategy/hyper.py | 2 +- freqtrade/templates/sample_hyperopt_advanced.py | 4 ++-- tests/optimize/test_hyperopt.py | 2 +- 6 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 freqtrade/optimize/space/__init__.py rename freqtrade/optimize/{ => space}/decimalspace.py (100%) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 633c8bdd5..fa28463e9 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -12,6 +12,7 @@ from skopt.space import Categorical, Dimension, Integer, Real from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes from freqtrade.misc import round_dict +from freqtrade.optimize.space import SKDecimal from freqtrade.strategy import IStrategy @@ -167,7 +168,7 @@ class IHyperOpt(ABC): You may override it in your custom Hyperopt class. """ return [ - Real(-0.35, -0.02, name='stoploss'), + SKDecimal(-0.35, -0.02, decimals=3, name='stoploss'), ] def generate_trailing_params(self, params: Dict) -> Dict: diff --git a/freqtrade/optimize/space/__init__.py b/freqtrade/optimize/space/__init__.py new file mode 100644 index 000000000..bbdac4ab9 --- /dev/null +++ b/freqtrade/optimize/space/__init__.py @@ -0,0 +1,4 @@ +# flake8: noqa: F401 +from skopt.space import Categorical, Dimension, Integer, Real + +from .decimalspace import SKDecimal diff --git a/freqtrade/optimize/decimalspace.py b/freqtrade/optimize/space/decimalspace.py similarity index 100% rename from freqtrade/optimize/decimalspace.py rename to freqtrade/optimize/space/decimalspace.py diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py index 3fedda974..16b576a73 100644 --- a/freqtrade/strategy/hyper.py +++ b/freqtrade/strategy/hyper.py @@ -10,7 +10,7 @@ from typing import Any, Iterator, Optional, Sequence, Tuple, Union with suppress(ImportError): from skopt.space import Integer, Real, Categorical - from freqtrade.optimize.decimalspace import SKDecimal + from freqtrade.optimize.space import SKDecimal from freqtrade.exceptions import OperationalException diff --git a/freqtrade/templates/sample_hyperopt_advanced.py b/freqtrade/templates/sample_hyperopt_advanced.py index 7736570f7..32ba21716 100644 --- a/freqtrade/templates/sample_hyperopt_advanced.py +++ b/freqtrade/templates/sample_hyperopt_advanced.py @@ -7,7 +7,7 @@ from typing import Any, Callable, Dict, List import numpy as np # noqa import pandas as pd # noqa from pandas import DataFrame -from skopt.space import Categorical, Dimension, Integer, Real # noqa +from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal, Real # noqa from freqtrade.optimize.hyperopt_interface import IHyperOpt @@ -237,7 +237,7 @@ class AdvancedSampleHyperOpt(IHyperOpt): 'stoploss' optimization hyperspace. """ return [ - Real(-0.35, -0.02, name='stoploss'), + SKDecimal(-0.35, -0.02, decimals=3, name='stoploss'), ] @staticmethod diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index 129fe53d9..59bc4aefb 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -15,10 +15,10 @@ from filelock import Timeout from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt from freqtrade.data.history import load_data from freqtrade.exceptions import OperationalException -from freqtrade.optimize.decimalspace import SKDecimal from freqtrade.optimize.hyperopt import Hyperopt from freqtrade.optimize.hyperopt_auto import HyperOptAuto from freqtrade.optimize.hyperopt_tools import HyperoptTools +from freqtrade.optimize.space import SKDecimal from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver from freqtrade.state import RunMode from tests.conftest import (get_args, log_has, log_has_re, patch_exchange, From 52c482cecfd3133a1e8f741f9b708eec55bd41f7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Wed, 14 Apr 2021 20:34:34 +0200 Subject: [PATCH 15/73] Convert trailing and roi defaults to skdecimal --- freqtrade/optimize/hyperopt_interface.py | 15 +++++++++------ freqtrade/templates/sample_hyperopt_advanced.py | 10 +++++----- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index fa28463e9..1bb471b9c 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -7,7 +7,7 @@ import math from abc import ABC from typing import Any, Callable, Dict, List -from skopt.space import Categorical, Dimension, Integer, Real +from skopt.space import Categorical, Dimension, Integer from freqtrade.exceptions import OperationalException from freqtrade.exchange import timeframe_to_minutes @@ -155,9 +155,12 @@ class IHyperOpt(ABC): Integer(roi_limits['roi_t1_min'], roi_limits['roi_t1_max'], name='roi_t1'), Integer(roi_limits['roi_t2_min'], roi_limits['roi_t2_max'], name='roi_t2'), Integer(roi_limits['roi_t3_min'], roi_limits['roi_t3_max'], name='roi_t3'), - Real(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], name='roi_p1'), - Real(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], name='roi_p2'), - Real(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], name='roi_p3'), + SKDecimal(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], decimals=5, + name='roi_p1'), + SKDecimal(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], decimals=5, + name='roi_p2'), + SKDecimal(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], decimals=5, + name='roi_p3'), ] def stoploss_space(self) -> List[Dimension]: @@ -198,14 +201,14 @@ class IHyperOpt(ABC): # other 'trailing' hyperspace parameters. Categorical([True], name='trailing_stop'), - Real(0.01, 0.35, name='trailing_stop_positive'), + SKDecimal(0.01, 0.35, decimals=3, name='trailing_stop_positive'), # 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive', # so this intermediate parameter is used as the value of the difference between # them. The value of the 'trailing_stop_positive_offset' is constructed in the # generate_trailing_params() method. # This is similar to the hyperspace dimensions used for constructing the ROI tables. - Real(0.001, 0.1, name='trailing_stop_positive_offset_p1'), + SKDecimal(0.001, 0.1, decimals=3, name='trailing_stop_positive_offset_p1'), Categorical([True, False], name='trailing_only_offset_is_reached'), ] diff --git a/freqtrade/templates/sample_hyperopt_advanced.py b/freqtrade/templates/sample_hyperopt_advanced.py index 32ba21716..cc13b6ba3 100644 --- a/freqtrade/templates/sample_hyperopt_advanced.py +++ b/freqtrade/templates/sample_hyperopt_advanced.py @@ -223,9 +223,9 @@ class AdvancedSampleHyperOpt(IHyperOpt): Integer(10, 120, name='roi_t1'), Integer(10, 60, name='roi_t2'), Integer(10, 40, name='roi_t3'), - Real(0.01, 0.04, name='roi_p1'), - Real(0.01, 0.07, name='roi_p2'), - Real(0.01, 0.20, name='roi_p3'), + SKDecimal(0.01, 0.04, decimals=3, name='roi_p1'), + SKDecimal(0.01, 0.07, decimals=3, name='roi_p2'), + SKDecimal(0.01, 0.20, decimals=3, name='roi_p3'), ] @staticmethod @@ -256,14 +256,14 @@ class AdvancedSampleHyperOpt(IHyperOpt): # other 'trailing' hyperspace parameters. Categorical([True], name='trailing_stop'), - Real(0.01, 0.35, name='trailing_stop_positive'), + SKDecimal(0.01, 0.35, decimals=3, name='trailing_stop_positive'), # 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive', # so this intermediate parameter is used as the value of the difference between # them. The value of the 'trailing_stop_positive_offset' is constructed in the # generate_trailing_params() method. # This is similar to the hyperspace dimensions used for constructing the ROI tables. - Real(0.001, 0.1, name='trailing_stop_positive_offset_p1'), + SKDecimal(0.001, 0.1, decimals=3, name='trailing_stop_positive_offset_p1'), Categorical([True, False], name='trailing_only_offset_is_reached'), ] From fa343b0484a5850ee9181a7a825ef9f3653d5d61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Martin?= <33117460+theomart@users.noreply.github.com> Date: Thu, 15 Apr 2021 01:19:30 +0100 Subject: [PATCH 16/73] Fix get_min_pair_stake_amount formula --- freqtrade/exchange/exchange.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 3958f6838..ea6bcb29f 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -539,7 +539,9 @@ class Exchange: # reserve some percent defined in config (5% default) + stoploss amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent', DEFAULT_AMOUNT_RESERVE_PERCENT) - amount_reserve_percent += abs(stoploss) + amount_reserve_percent = ( + amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5 + ) # it should not be more than 50% amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1) From 885096f2b3830e14bf8b1678ad31e696775ddfbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Martin?= <33117460+theomart@users.noreply.github.com> Date: Thu, 15 Apr 2021 01:22:52 +0100 Subject: [PATCH 17/73] Update tests for get_min_pair_stake_amount --- tests/exchange/test_exchange.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 882cf6b5a..3bfff50e8 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -371,7 +371,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss) - assert isclose(result, 2 * 1.1) + assert isclose(result, 2 * (1+0.05) / (1-abs(stoploss))) # min amount is set markets["ETH/BTC"]["limits"] = { @@ -383,7 +383,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert isclose(result, 2 * 2 * 1.1) + assert isclose(result, 2 * 2 * (1+0.05) / (1-abs(stoploss))) # min amount and cost are set (cost is minimal) markets["ETH/BTC"]["limits"] = { @@ -395,7 +395,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert isclose(result, max(2, 2 * 2) * 1.1) + assert isclose(result, max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss))) # min amount and cost are set (amount is minial) markets["ETH/BTC"]["limits"] = { @@ -407,10 +407,10 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss) - assert isclose(result, max(8, 2 * 2) * 1.1) + assert isclose(result, max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss))) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4) - assert isclose(result, max(8, 2 * 2) * 1.45) + assert isclose(result, max(8, 2 * 2) * (1+0.05) / (1-abs(-0.4))) # Really big stoploss result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) @@ -432,7 +432,7 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) - assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) * 1.1, 8) + assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)), 8) def test_set_sandbox(default_conf, mocker): From ce23d9dfeef32c1a67f0cfbcc28fd39ff4305b11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20Martin?= <33117460+theomart@users.noreply.github.com> Date: Thu, 15 Apr 2021 01:38:08 +0100 Subject: [PATCH 18/73] Fix test min stake amount --- tests/exchange/test_exchange.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 3bfff50e8..4531bf816 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -410,7 +410,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: assert isclose(result, max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss))) result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4) - assert isclose(result, max(8, 2 * 2) * (1+0.05) / (1-abs(-0.4))) + assert isclose(result, max(8, 2 * 2) * 1.5) # Really big stoploss result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1) From c9c039d640b59e0794b7c54ff30a1f8412e6c86d Mon Sep 17 00:00:00 2001 From: JoeSchr Date: Thu, 15 Apr 2021 15:21:28 +0200 Subject: [PATCH 19/73] remove `copy()` from `custom_info` example `set_index` automatically copies if not stated otherwise with `inplace=True` > inplacebool, default False If True, modifies the DataFrame in place (do not create a new object). from: https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.set_index.html?highlight=set_index#pandas.DataFrame.set_index --- docs/strategy-advanced.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index 7fa824a5b..96c927965 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -57,7 +57,7 @@ class AwesomeStrategy(IStrategy): dataframe['atr'] = ta.ATR(dataframe) if self.dp.runmode.value in ('backtest', 'hyperopt'): # add indicator mapped to correct DatetimeIndex to custom_info - self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].copy().set_index('date') + self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].set_index('date') return dataframe ``` From 7142787256fd870193699929c377c0706a01599b Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Apr 2021 15:41:35 +0200 Subject: [PATCH 20/73] Roll back unintended changes that break rendering --- docs/docker_quickstart.md | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index b133e33f0..9096000c1 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -14,7 +14,7 @@ To simplify running freqtrade, please install [`docker-compose`](https://docs.do ## Freqtrade with docker-compose -Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) ready for usage. +Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker-compose file](https://github.com/freqtrade/freqtrade/blob/stable/docker-compose.yml) ready for usage. !!! Note - The following section assumes that `docker` and `docker-compose` are installed and available to the logged in user. @@ -24,8 +24,8 @@ Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.co Create a new directory and place the [docker-compose file](https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml) in this directory. -#### PC/MAC/Linux - +=== "PC/MAC/Linux" + ``` bash mkdir ft_userdata cd ft_userdata/ # Download the docker-compose file from the repository @@ -39,10 +39,10 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse # Create configuration - Requires answering interactive questions docker-compose run --rm freqtrade new-config --config user_data/config.json - - -#### RaspberryPi + ``` +=== "RaspberryPi" + ``` bash mkdir ft_userdata cd ft_userdata/ # Download the docker-compose file from the repository @@ -56,6 +56,7 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse # Create configuration - Requires answering interactive questions docker-compose run --rm freqtrade new-config --config user_data/config.json + ``` !!! Note "Change your docker Image" You have to change the docker image in the docker-compose file for your Raspberry build to work properly. @@ -67,11 +68,12 @@ Create a new directory and place the [docker-compose file](https://raw.githubuse The above snippet creates a new directory called `ft_userdata`, downloads the latest compose file and pulls the freqtrade image. The last 2 steps in the snippet create the directory with `user_data`, as well as (interactively) the default configuration based on your selections. -### Question: "How to edit the bot configuration?" -You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. -You can also change the both Strategy and commands by editing the command section of your `docker-compose.yml` file. +!!! Question "How to edit the bot configuration?" + You can edit the configuration at any time, which is available as `user_data/config.json` (within the directory `ft_userdata`) when using the above configuration. -##### Adding a custom strategy + You can also change the both Strategy and commands by editing the command section of your `docker-compose.yml` file. + +#### Adding a custom strategy 1. The configuration is now available as `user_data/config.json` 2. Copy a custom strategy to the directory `user_data/strategies/` @@ -79,7 +81,7 @@ You can also change the both Strategy and commands by editing the command sectio The `SampleStrategy` is run by default. -#### Warning "`SampleStrategy` is just a demo!" +!!! Warning "`SampleStrategy` is just a demo!" The `SampleStrategy` is there for your reference and give you ideas for your own strategy. Please always backtest your strategy and use dry-run for some time before risking real money! You will find more information about Strategy development in the [Strategy documentation](strategy-customization.md). @@ -90,7 +92,7 @@ Once this is done, you're ready to launch the bot in trading mode (Dry-run or Li docker-compose up -d ``` -#### Warning "Default configuration" +!!! Warning "Default configuration" While the configuration generated will be mostly functional, you will still need to verify that all options correspond to what you want (like Pricing, pairlist, ...) before starting the bot. #### Monitoring the bot @@ -120,7 +122,7 @@ docker-compose up -d This will first pull the latest image, and will then restart the container with the just pulled version. -#### Warning "Check the Changelog" +!!! Warning "Check the Changelog" You should always check the changelog for breaking changes / manual interventions required and make sure the bot starts correctly after the update. ### Editing the docker-compose file @@ -129,7 +131,7 @@ Advanced users may edit the docker-compose file further to include all possible All freqtrade arguments will be available by running `docker-compose run --rm freqtrade `. -#### Note "`docker-compose run --rm`" +!!! Note "`docker-compose run --rm`" Including `--rm` will remove the container after completion, and is highly recommended for all modes except trading mode (running with `freqtrade trade` command). #### Example: Download data with docker-compose From ce870bbcf7b7b4fe2dfd79884c2da33f225d4af3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 15 Apr 2021 21:38:20 +0200 Subject: [PATCH 21/73] Use 3 decimals for ROI space --- freqtrade/optimize/hyperopt_interface.py | 10 +++++----- freqtrade/optimize/space/decimalspace.py | 5 +++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py index 1bb471b9c..889854cad 100644 --- a/freqtrade/optimize/hyperopt_interface.py +++ b/freqtrade/optimize/hyperopt_interface.py @@ -140,7 +140,7 @@ class IHyperOpt(ABC): 'roi_p2': roi_limits['roi_p2_min'], 'roi_p3': roi_limits['roi_p3_min'], } - logger.info(f"Min roi table: {round_dict(self.generate_roi_table(p), 5)}") + logger.info(f"Min roi table: {round_dict(self.generate_roi_table(p), 3)}") p = { 'roi_t1': roi_limits['roi_t1_max'], 'roi_t2': roi_limits['roi_t2_max'], @@ -149,17 +149,17 @@ class IHyperOpt(ABC): 'roi_p2': roi_limits['roi_p2_max'], 'roi_p3': roi_limits['roi_p3_max'], } - logger.info(f"Max roi table: {round_dict(self.generate_roi_table(p), 5)}") + logger.info(f"Max roi table: {round_dict(self.generate_roi_table(p), 3)}") return [ Integer(roi_limits['roi_t1_min'], roi_limits['roi_t1_max'], name='roi_t1'), Integer(roi_limits['roi_t2_min'], roi_limits['roi_t2_max'], name='roi_t2'), Integer(roi_limits['roi_t3_min'], roi_limits['roi_t3_max'], name='roi_t3'), - SKDecimal(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], decimals=5, + SKDecimal(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], decimals=3, name='roi_p1'), - SKDecimal(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], decimals=5, + SKDecimal(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], decimals=3, name='roi_p2'), - SKDecimal(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], decimals=5, + SKDecimal(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], decimals=3, name='roi_p3'), ] diff --git a/freqtrade/optimize/space/decimalspace.py b/freqtrade/optimize/space/decimalspace.py index f5370b6d6..643999cc1 100644 --- a/freqtrade/optimize/space/decimalspace.py +++ b/freqtrade/optimize/space/decimalspace.py @@ -9,8 +9,9 @@ class SKDecimal(Integer): self.decimals = decimals _low = int(low * pow(10, self.decimals)) _high = int(high * pow(10, self.decimals)) - self.low_orig = low - self.high_orig = high + # trunc to precision to avoid points out of space + self.low_orig = round(_low * pow(0.1, self.decimals), self.decimals) + self.high_orig = round(_high * pow(0.1, self.decimals), self.decimals) super().__init__(_low, _high, prior, base, transform, name, dtype) From 5e51ba6258c77a33e7b06ed132159ade64029c7d Mon Sep 17 00:00:00 2001 From: grillzoo Date: Thu, 15 Apr 2021 21:38:00 +0100 Subject: [PATCH 22/73] fix flake8 --- freqtrade/exchange/exchange.py | 2 +- tests/exchange/test_exchange.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ea6bcb29f..ed7918b36 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -541,7 +541,7 @@ class Exchange: DEFAULT_AMOUNT_RESERVE_PERCENT) amount_reserve_percent = ( amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5 - ) + ) # it should not be more than 50% amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 4531bf816..27f4d0db9 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -432,7 +432,10 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None: PropertyMock(return_value=markets) ) result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss) - assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)), 8) + assert round(result, 8) == round( + max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)), + 8 + ) def test_set_sandbox(default_conf, mocker): From 01b303e0f95bf3c1dc4c7a375b82bcf2f29fc645 Mon Sep 17 00:00:00 2001 From: grillzoo Date: Thu, 15 Apr 2021 21:58:07 +0100 Subject: [PATCH 23/73] Aligning the doc --- docs/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/configuration.md b/docs/configuration.md index eb3351b8f..0ade558f1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -167,7 +167,7 @@ This exchange has also a limit on USD - where all orders must be > 10$ - which h To guarantee safe execution, freqtrade will not allow buying with a stake-amount of 10.1$, instead, it'll make sure that there's enough space to place a stoploss below the pair (+ an offset, defined by `amount_reserve_percent`, which defaults to 5%). -With a stoploss of 10% - we'd therefore end up with a value of ~13.8$ (`12 * (1 + 0.05 + 0.1)`). +With a reserve of 5%, the minimum stake amount would be ~12.6$ (`12 * (1 + 0.05)`). If we take in account a stoploss of 10% on top of that - we'd end up with a value of ~14$ (`12.6 / (1 - 0.1)`). To limit this calculation in case of large stoploss values, the calculated minimum stake-limit will never be more than 50% above the real limit. From 2011912a19620c337b37a777123e80f86f0c9ebb Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Apr 2021 07:46:00 +0200 Subject: [PATCH 24/73] Adapt documentation to use 3 decimals only --- docs/hyperopt.md | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 07cc963cf..0e6bded92 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -460,23 +460,26 @@ As stated in the comment, you can also use it as the value of the `minimal_roi` #### Default ROI Search Space -If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the timeframe used. By default the values vary in the following ranges (for some of the most used timeframes, values are rounded to 5 digits after the decimal point): +If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the timeframe used. By default the values vary in the following ranges (for some of the most used timeframes, values are rounded to 3 digits after the decimal point): -| # step | 1m | | 5m | | 1h | | 1d | | -| ------ | ------ | ----------------- | -------- | ----------- | ---------- | ----------------- | ------------ | ----------------- | -| 1 | 0 | 0.01161...0.11992 | 0 | 0.03...0.31 | 0 | 0.06883...0.71124 | 0 | 0.12178...1.25835 | -| 2 | 2...8 | 0.00774...0.04255 | 10...40 | 0.02...0.11 | 120...480 | 0.04589...0.25238 | 2880...11520 | 0.08118...0.44651 | -| 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 | -| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 | +| # step | 1m | | 5m | | 1h | | 1d | | +| ------ | ------ | ------------- | -------- | ----------- | ---------- | ------------- | ------------ | ------------- | +| 1 | 0 | 0.011...0.119 | 0 | 0.03...0.31 | 0 | 0.068...0.711 | 0 | 0.121...1.258 | +| 2 | 2...8 | 0.007...0.042 | 10...40 | 0.02...0.11 | 120...480 | 0.045...0.252 | 2880...11520 | 0.081...0.446 | +| 3 | 4...20 | 0.003...0.015 | 20...100 | 0.01...0.04 | 240...1200 | 0.022...0.091 | 5760...28800 | 0.040...0.162 | +| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 | These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used. If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default. -Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). +Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). A sample for these methods can be found in [sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). +!!! Note "Reduced search space" + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + ### Understand Hyperopt Stoploss results If you are optimizing stoploss values (i.e. if optimization search-space contains 'all', 'default' or 'stoploss'), your result will look as follows and include stoploss: @@ -516,6 +519,9 @@ If you have the `stoploss_space()` method in your custom hyperopt file, remove i Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). +!!! Note "Reduced search space" + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + ### Understand Hyperopt Trailing Stop results If you are optimizing trailing stop values (i.e. if optimization search-space contains 'all' or 'trailing'), your result will look as follows and include trailing stop parameters: @@ -551,6 +557,9 @@ If you are optimizing trailing stop values, Freqtrade creates the 'trailing' opt Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). +!!! Note "Reduced search space" + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + ### Reproducible results The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with an asterisk character (`*`) in the first column in the Hyperopt output. From 8ce5522a100f2058f7ba20a662fbfba3eb78cf71 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Apr 2021 08:00:04 +0200 Subject: [PATCH 25/73] Add additional documentation for SKDecimal space --- docs/advanced-hyperopt.md | 24 +++++++++++++++++++++++- docs/hyperopt.md | 1 + 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md index cc71f39a7..c86978b80 100644 --- a/docs/advanced-hyperopt.md +++ b/docs/advanced-hyperopt.md @@ -79,9 +79,31 @@ class MyAwesomeStrategy(IStrategy): class HyperOpt: # Define a custom stoploss space. def stoploss_space(self): - return [Real(-0.05, -0.01, name='stoploss')] + return [SKDecimal(-0.05, -0.01, decimals=3, name='stoploss')] ``` +## Space options + +For the additional spaces, scikit-optimize (in combination with Freqtrade) provides the following space types: + +* `Categorical` - Pick from a list of categories (e.g. `Categorical(['a', 'b', 'c'], name="cat")`) +* `Integer` - Pick from a range of whole numbers (e.g. `Integer(1, 10, name='rsi')`) +* `SKDecimal` - Pick from a range of decimal numbers with limited precision (e.g. `SKDecimal(0.1, 0.5, decimals=3, name='adx')`). *Available only with freqtrade*. +* `Real` - Pick from a range of decimal numbers with full precision (e.g. `Real(0.1, 0.5, name='adx')` + +You can import all of these from `freqtrade.optimize.space`, although `Categorical`, `Integer` and `Real` are only aliases for their corresponding scikit-optimize Spaces. `SKDecimal` is provided by freqtrade for faster optimizations. + +``` python +from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal, Real # noqa +``` + +!!! Hint "SKDecimal vs. Real" + We recommend to use `SKDecimal` instead of the `Real` space in almost all cases. While the Real space provides full accuracy (up to ~16 decimal places) - this precision is rarely needed, and leads to unnecessary long hyperopt times. + + Assuming the definition of a rather small space (`SKDecimal(0.10, 0.15, decimals=2, name='xxx')`) - SKDecimal will have 5 possibilities (`[0.10, 0.11, 0.12, 0.13, 0.14, 0.15]`). + + A corresponding real space `Real(0.10, 0.15 name='xxx')` on the other hand has an almost unlimited number of possibilities (`[0.10, 0.010000000001, 0.010000000002, ... 0.014999999999, 0.01500000000]`). + --- ## Legacy Hyperopt diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 0e6bded92..b073a73b6 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -294,6 +294,7 @@ Based on the results, hyperopt will tell you which parameter combination produce ## Parameter types There are four parameter types each suited for different purposes. + * `IntParameter` - defines an integral parameter with upper and lower boundaries of search space. * `DecimalParameter` - defines a floating point parameter with a limited number of decimals (default 3). Should be preferred instead of `RealParameter` in most cases. * `RealParameter` - defines a floating point parameter with upper and lower boundaries and no precision limit. Rarely used as it creates a space with a near infinite number of possibilities. From e6936ae1352246125a66685abf9ac35d05fbce3e Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Apr 2021 19:16:29 +0200 Subject: [PATCH 26/73] Improve wording in docs --- docs/hyperopt.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/hyperopt.md b/docs/hyperopt.md index b073a73b6..21cbadf7f 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -479,7 +479,7 @@ Override the `roi_space()` method if you need components of the ROI tables to va A sample for these methods can be found in [sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). !!! Note "Reduced search space" - To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. ### Understand Hyperopt Stoploss results @@ -521,7 +521,7 @@ If you have the `stoploss_space()` method in your custom hyperopt file, remove i Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). !!! Note "Reduced search space" - To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. ### Understand Hyperopt Trailing Stop results @@ -559,7 +559,7 @@ If you are optimizing trailing stop values, Freqtrade creates the 'trailing' opt Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py). !!! Note "Reduced search space" - To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is sufficient, every value more precise than this will usually result in overfitted results. + To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs. ### Reproducible results From aeb81f90ff0cf375def82f21194c922baa4c4b76 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Apr 2021 19:35:56 +0200 Subject: [PATCH 27/73] Implement errorhandling for /trade endpoint --- freqtrade/rpc/api_server/api_v1.py | 5 ++++- tests/rpc/test_rpc_apiserver.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index 6873c0c4c..02736aca6 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -90,7 +90,10 @@ def trades(limit: int = 0, rpc: RPC = Depends(get_rpc)): @router.get('/trade/{tradeid}', response_model=OpenTradeSchema, tags=['info', 'trading']) def trade(tradeid: int = 0, rpc: RPC = Depends(get_rpc)): - return rpc._rpc_trade_status([tradeid])[0] + try: + return rpc._rpc_trade_status([tradeid])[0] + except (RPCException, KeyError): + raise HTTPException(status_code=404, detail='Trade not found.') @router.delete('/trades/{tradeid}', response_model=DeleteTrade, tags=['info', 'trading']) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index a65b4ed6f..760d78b03 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -522,6 +522,26 @@ def test_api_trades(botclient, mocker, fee, markets): assert rc.json()['trades_count'] == 1 +def test_api_trade_single(botclient, mocker, fee, ticker, markets): + ftbot, client = botclient + patch_get_signal(ftbot, (True, False)) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + markets=PropertyMock(return_value=markets), + fetch_ticker=ticker, + ) + rc = client_get(client, f"{BASE_URI}/trade/3") + assert_response(rc, 404) + assert rc.json()['detail'] == 'Trade not found.' + + create_mock_trades(fee) + Trade.query.session.flush() + + rc = client_get(client, f"{BASE_URI}/trade/3") + assert_response(rc) + assert rc.json()['trade_id'] == 3 + + def test_api_delete_trade(botclient, mocker, fee, markets): ftbot, client = botclient patch_get_signal(ftbot, (True, False)) From 5c579613e1cf8505f659210ecae518168a0f026b Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 16 Apr 2021 19:42:13 +0200 Subject: [PATCH 28/73] add /trade endpoint to rest_client script --- docs/rest-api.md | 12 ++++++++---- scripts/rest_client.py | 12 ++++++++++-- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index be3107fcb..5c25e9eeb 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -235,6 +235,9 @@ pair_history performance Return the performance of the different coins. +ping + simple ping + plot_config Return plot configuration if the strategy defines one. @@ -271,15 +274,16 @@ strategy :param strategy: Strategy class name +trade + Return specific trade + + :param trade_id: Specify which trade to get. + trades Return trades history. :param limit: Limits trades to the X last trades. No limit to get all the trades. -trade - Return specific trade. - :param tradeid: Specify which trade to get. - version Return the version of the bot. diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 4d667879d..40b338ce8 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -127,7 +127,7 @@ class FtRestClient(): return self._delete("locks/{}".format(lock_id)) def daily(self, days=None): - """Return the amount of open trades. + """Return the profits for each day, and amount of trades. :return: json object """ @@ -195,7 +195,7 @@ class FtRestClient(): def logs(self, limit=None): """Show latest logs. - :param limit: Limits log messages to the last logs. No limit to get all the trades. + :param limit: Limits log messages to the last logs. No limit to get the entire log. :return: json object """ return self._get("logs", params={"limit": limit} if limit else 0) @@ -208,6 +208,14 @@ class FtRestClient(): """ return self._get("trades", params={"limit": limit} if limit else 0) + def trade(self, trade_id): + """Return specific trade + + :param trade_id: Specify which trade to get. + :return: json object + """ + return self._get("trade/{}".format(trade_id)) + def delete_trade(self, trade_id): """Delete trade from the database. Tries to close open orders. Requires manual handling of this asset on the exchange. From 1eb9ce4227a0424b5153e8f47d1522fe2ac1de29 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 10:47:32 +0200 Subject: [PATCH 29/73] Allow specifying pairs for optimize commands via `--pairs` --- freqtrade/commands/arguments.py | 2 +- freqtrade/commands/cli_options.py | 2 +- freqtrade/configuration/configuration.py | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 9468a7f7d..9cf9992ce 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -17,7 +17,7 @@ ARGS_STRATEGY = ["strategy", "strategy_path"] ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"] ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv", - "max_open_trades", "stake_amount", "fee"] + "max_open_trades", "stake_amount", "fee", "pairs"] ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", "enable_protections", "dry_run_wallet", diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 4fac8ac72..e49895de4 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -330,7 +330,7 @@ AVAILABLE_CLI_OPTIONS = { # Script options "pairs": Arg( '-p', '--pairs', - help='Show profits for only these pairs. Pairs are space-separated.', + help='Limit command to these pairs. Pairs are space-separated.', nargs='+', ), # Download data diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index 9acd532cc..cc11f97c2 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -445,6 +445,7 @@ class Configuration: """ if "pairs" in config: + config['exchange']['pair_whitelist'] = config['pairs'] return if "pairs_file" in self.args and self.args["pairs_file"]: From 6a9c47d15f7476f72dfb220e643a8801bcf5d058 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 10:48:24 +0200 Subject: [PATCH 30/73] Update docs with new options --- docs/backtesting.md | 6 +++++- docs/edge.md | 10 +++++++++- docs/hyperopt.md | 11 ++++++++--- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/docs/backtesting.md b/docs/backtesting.md index e16225f94..ee9926f32 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -15,7 +15,8 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH] [--data-format-ohlcv {json,jsongz,hdf5}] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] - [--eps] [--dmmp] [--enable-protections] + [-p PAIRS [PAIRS ...]] [--eps] [--dmmp] + [--enable-protections] [--dry-run-wallet DRY_RUN_WALLET] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--export EXPORT] [--export-filename PATH] @@ -37,6 +38,9 @@ optional arguments: setting. --fee FLOAT Specify fee ratio. Will be applied twice (on trade entry and exit). + -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] + Limit command to these pairs. Pairs are space- + separated. --eps, --enable-position-stacking Allow buying the same pair multiple times (position stacking). diff --git a/docs/edge.md b/docs/edge.md index 7f0a9cb2d..237ff36f6 100644 --- a/docs/edge.md +++ b/docs/edge.md @@ -215,8 +215,10 @@ Let's say the stake currency is **ETH** and there is $10$ **ETH** on the wallet. usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH] [-i TIMEFRAME] [--timerange TIMERANGE] + [--data-format-ohlcv {json,jsongz,hdf5}] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] - [--fee FLOAT] [--stoplosses STOPLOSS_RANGE] + [--fee FLOAT] [-p PAIRS [PAIRS ...]] + [--stoplosses STOPLOSS_RANGE] optional arguments: -h, --help show this help message and exit @@ -224,6 +226,9 @@ optional arguments: Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`). --timerange TIMERANGE Specify what timerange of data to use. + --data-format-ohlcv {json,jsongz,hdf5} + Storage format for downloaded candle (OHLCV) data. + (default: `None`). --max-open-trades INT Override the value of the `max_open_trades` configuration setting. @@ -232,6 +237,9 @@ optional arguments: setting. --fee FLOAT Specify fee ratio. Will be applied twice (on trade entry and exit). + -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] + Limit command to these pairs. Pairs are space- + separated. --stoplosses STOPLOSS_RANGE Defines a range of stoploss values against which edge will assess the strategy. The format is "min,max,step" diff --git a/docs/hyperopt.md b/docs/hyperopt.md index 21cbadf7f..51905e616 100644 --- a/docs/hyperopt.md +++ b/docs/hyperopt.md @@ -44,8 +44,9 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--data-format-ohlcv {json,jsongz,hdf5}] [--max-open-trades INT] [--stake-amount STAKE_AMOUNT] [--fee FLOAT] - [--hyperopt NAME] [--hyperopt-path PATH] [--eps] - [--dmmp] [--enable-protections] + [-p PAIRS [PAIRS ...]] [--hyperopt NAME] + [--hyperopt-path PATH] [--eps] [--dmmp] + [--enable-protections] [--dry-run-wallet DRY_RUN_WALLET] [-e INT] [--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]] [--print-all] [--no-color] [--print-json] [-j JOBS] @@ -69,6 +70,9 @@ optional arguments: setting. --fee FLOAT Specify fee ratio. Will be applied twice (on trade entry and exit). + -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] + Limit command to these pairs. Pairs are space- + separated. --hyperopt NAME Specify hyperopt class name which will be used by the bot. --hyperopt-path PATH Specify additional lookup path for Hyperopt and @@ -105,7 +109,8 @@ optional arguments: reproducible hyperopt results. --min-trades INT Set minimal desired number of trades for evaluations in the hyperopt optimization path (default: 1). - --hyperopt-loss NAME Specify the class name of the hyperopt loss function + --hyperopt-loss NAME, --hyperoptloss NAME + Specify the class name of the hyperopt loss function class (IHyperOptLoss). Different functions can generate completely different results, since the target for optimization is different. Built-in From c8d3d449a3b0fee283999d5782939dc0bbc7ff5d Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 10:51:02 +0200 Subject: [PATCH 31/73] Add quick test for pair_whitelist overwrite --- tests/test_configuration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_configuration.py b/tests/test_configuration.py index a512bf58a..b2c883108 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -1002,6 +1002,7 @@ def test_pairlist_resolving(): config = configuration.get_config() assert config['pairs'] == ['ETH/BTC', 'XRP/BTC'] + assert config['exchange']['pair_whitelist'] == ['ETH/BTC', 'XRP/BTC'] assert config['exchange']['name'] == 'binance' From fbb90755395e205f2235229dc2eb14c25ee88c99 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 10:53:03 +0200 Subject: [PATCH 32/73] Update util command structures too --- docs/data-download.md | 6 +++--- docs/plotting.md | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index 04f444a8b..7a78334d5 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -30,7 +30,7 @@ usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] optional arguments: -h, --help show this help message and exit -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] - Show profits for only these pairs. Pairs are space- + Limit command to these pairs. Pairs are space- separated. --pairs-file FILE File containing a list of pairs to download. --days INT Download data for given number of days. @@ -48,10 +48,10 @@ optional arguments: exchange/pairs/timeframes. --data-format-ohlcv {json,jsongz,hdf5} Storage format for downloaded candle (OHLCV) data. - (default: `json`). + (default: `None`). --data-format-trades {json,jsongz,hdf5} Storage format for downloaded trades data. (default: - `jsongz`). + `None`). Common arguments: -v, --verbose Verbose mode (-vv for more, -vvv to get all messages). diff --git a/docs/plotting.md b/docs/plotting.md index 63afa16b6..5d454c414 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -37,7 +37,7 @@ usage: freqtrade plot-dataframe [-h] [-v] [--logfile FILE] [-V] [-c PATH] optional arguments: -h, --help show this help message and exit -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] - Show profits for only these pairs. Pairs are space- + Limit command to these pairs. Pairs are space- separated. --indicators1 INDICATORS1 [INDICATORS1 ...] Set indicators from your strategy you want in the @@ -90,6 +90,7 @@ Strategy arguments: Specify strategy class name which will be used by the bot. --strategy-path PATH Specify additional strategy lookup path. + ``` Example: @@ -244,7 +245,7 @@ usage: freqtrade plot-profit [-h] [-v] [--logfile FILE] [-V] [-c PATH] optional arguments: -h, --help show this help message and exit -p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...] - Show profits for only these pairs. Pairs are space- + Limit command to these pairs. Pairs are space- separated. --timerange TIMERANGE Specify what timerange of data to use. @@ -286,6 +287,7 @@ Strategy arguments: Specify strategy class name which will be used by the bot. --strategy-path PATH Specify additional strategy lookup path. + ``` The `-p/--pairs` argument, can be used to limit the pairs that are considered for this calculation. From 44bfb53668d8a5fd301e0c2187623a6e958589a6 Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 19:29:34 +0200 Subject: [PATCH 33/73] Don't use current rate for closed trades --- freqtrade/rpc/api_server/api_schemas.py | 1 - freqtrade/rpc/rpc.py | 11 +++++++---- tests/rpc/test_rpc_apiserver.py | 1 - 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 41de0134c..12bee1cf2 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -189,7 +189,6 @@ class OpenTradeSchema(TradeSchema): stoploss_current_dist_ratio: Optional[float] stoploss_entry_dist: Optional[float] stoploss_entry_dist_ratio: Optional[float] - base_currency: str current_profit: float current_profit_abs: float current_profit_pct: float diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 88f8b36db..b86562e80 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -167,10 +167,13 @@ class RPC: if trade.open_order_id: order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair) # calculate profit and send message to user - try: - current_rate = self._freqtrade.get_sell_rate(trade.pair, False) - except (ExchangeError, PricingError): - current_rate = NAN + if trade.is_open: + try: + current_rate = self._freqtrade.get_sell_rate(trade.pair, False) + except (ExchangeError, PricingError): + current_rate = NAN + else: + current_rate = trade.close_rate current_profit = trade.calc_profit_ratio(current_rate) current_profit_abs = trade.calc_profit(current_rate) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 3d27922d8..d610906d5 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -773,7 +773,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets): assert rc.json()[0] == { 'amount': 123.0, 'amount_requested': 123.0, - 'base_currency': 'BTC', 'close_date': None, 'close_timestamp': None, 'close_profit': None, From 0737e3fa2230ba27dee588476fa589691123554e Mon Sep 17 00:00:00 2001 From: Matthias Date: Sat, 17 Apr 2021 19:48:29 +0200 Subject: [PATCH 34/73] Clarify refresh_period section for volumepairlist part of #4689 --- docs/includes/pairlists.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 8688494cc..85d157e75 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -60,6 +60,8 @@ When used in the chain of Pairlist Handlers in a non-leading position (after Sta When used on the leading position of the chain of Pairlist Handlers, it does not consider `pair_whitelist` configuration setting, but selects the top assets from all available markets (with matching stake-currency) on the exchange. The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes). +The pairlist cache (`refresh_period`) on `VolumePairList` is only applicable to generating pairlists. +Filtering instances (not the first position in the list) will not apply any cache and will always use up-to-date data. `VolumePairList` is based on the ticker data from exchange, as reported by the ccxt library: @@ -90,6 +92,7 @@ This filter allows freqtrade to ignore pairs until they have been listed for at #### PerformanceFilter Sorts pairs by past trade performance, as follows: + 1. Positive performance. 2. No closed trades yet. 3. Negative performance. From 296ea30cc341345bd87a46f99bbe99e511481ce2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 05:22:35 +0000 Subject: [PATCH 35/73] Bump pytest-asyncio from 0.14.0 to 0.15.0 Bumps [pytest-asyncio](https://github.com/pytest-dev/pytest-asyncio) from 0.14.0 to 0.15.0. - [Release notes](https://github.com/pytest-dev/pytest-asyncio/releases) - [Commits](https://github.com/pytest-dev/pytest-asyncio/compare/v0.14.0...v0.15.0) 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 cd93f2433..6ddbffb74 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -9,7 +9,7 @@ flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.1 mypy==0.812 pytest==6.2.3 -pytest-asyncio==0.14.0 +pytest-asyncio==0.15.0 pytest-cov==2.11.1 pytest-mock==3.5.1 pytest-random-order==1.0.4 From 8d2e6954a1fabb833b6e75a5609a4993aeac90bf Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 05:22:41 +0000 Subject: [PATCH 36/73] Bump flake8 from 3.9.0 to 3.9.1 Bumps [flake8](https://gitlab.com/pycqa/flake8) from 3.9.0 to 3.9.1. - [Release notes](https://gitlab.com/pycqa/flake8/tags) - [Commits](https://gitlab.com/pycqa/flake8/compare/3.9.0...3.9.1) 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 cd93f2433..35c57769a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,7 +4,7 @@ -r requirements-hyperopt.txt coveralls==3.0.1 -flake8==3.9.0 +flake8==3.9.1 flake8-type-annotations==0.1.0 flake8-tidy-imports==4.2.1 mypy==0.812 From 05246e6637637590909e9e34d0a3a813cc14aa5f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 05:22:52 +0000 Subject: [PATCH 37/73] Bump pandas from 1.2.3 to 1.2.4 Bumps [pandas](https://github.com/pandas-dev/pandas) from 1.2.3 to 1.2.4. - [Release notes](https://github.com/pandas-dev/pandas/releases) - [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md) - [Commits](https://github.com/pandas-dev/pandas/compare/v1.2.3...v1.2.4) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 129bb05b2..c6051463c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ numpy==1.20.2 -pandas==1.2.3 +pandas==1.2.4 ccxt==1.47.47 # Pin cryptography for now due to rust build errors with piwheels From 59d02f3f039f4e696ac78f7b2eeb3d7888020224 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 05:23:27 +0000 Subject: [PATCH 38/73] Bump sqlalchemy from 1.4.7 to 1.4.9 Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.4.7 to 1.4.9. - [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases) - [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES) - [Commits](https://github.com/sqlalchemy/sqlalchemy/commits) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 129bb05b2..5159d69e1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ ccxt==1.47.47 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 -SQLAlchemy==1.4.7 +SQLAlchemy==1.4.9 python-telegram-bot==13.4.1 arrow==1.0.3 cachetools==4.2.1 From b94de3030a11610d48aae70f9e9f182e098c0096 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 05:23:33 +0000 Subject: [PATCH 39/73] Bump mkdocs-material from 7.1.1 to 7.1.2 Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 7.1.1 to 7.1.2. - [Release notes](https://github.com/squidfunk/mkdocs-material/releases) - [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/docs/changelog.md) - [Commits](https://github.com/squidfunk/mkdocs-material/compare/7.1.1...7.1.2) 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 cfd63d1d0..4d7082a7f 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,3 +1,3 @@ -mkdocs-material==7.1.1 +mkdocs-material==7.1.2 mdx_truly_sane_lists==1.2 pymdown-extensions==8.1.1 From 9407dbcf87eea8a511642045b2910f54de448f49 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 07:46:39 +0200 Subject: [PATCH 40/73] Add freqtrade powered by ccxt --- README.md | 2 +- docs/assets/ccxt-logo.svg | 3 ++ docs/assets/freqtrade_poweredby.svg | 44 +++++++++++++++++++++++++++++ docs/index.md | 5 ++-- 4 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 docs/assets/ccxt-logo.svg create mode 100644 docs/assets/freqtrade_poweredby.svg diff --git a/README.md b/README.md index c3a665c47..916f9cf17 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Freqtrade +# ![freqtrade](docs/assets/freqtrade_poweredby.svg) [![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/) [![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop) diff --git a/docs/assets/ccxt-logo.svg b/docs/assets/ccxt-logo.svg new file mode 100644 index 000000000..e52682546 --- /dev/null +++ b/docs/assets/ccxt-logo.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/docs/assets/freqtrade_poweredby.svg b/docs/assets/freqtrade_poweredby.svg new file mode 100644 index 000000000..1041f87ab --- /dev/null +++ b/docs/assets/freqtrade_poweredby.svg @@ -0,0 +1,44 @@ + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + Freqtrade + + + + + poweredby + + diff --git a/docs/index.md b/docs/index.md index 61f2276c3..c2b6d5629 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,4 +1,5 @@ -# Freqtrade +![freqtrade](assets/freqtrade_poweredby.svg) + [![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/) [![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop) [![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability) @@ -39,7 +40,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual, - [X] [Bittrex](https://bittrex.com/) - [X] [FTX](https://ftx.com) - [X] [Kraken](https://kraken.com/) -- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ +- [ ] [potentially many others through ccxt](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ ### Community tested From 66b3ecfeed6023794d9710dde825f49d46ed0bd1 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 08:32:17 +0200 Subject: [PATCH 41/73] Remove faulty font-family in svg --- docs/assets/freqtrade_poweredby.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/assets/freqtrade_poweredby.svg b/docs/assets/freqtrade_poweredby.svg index 1041f87ab..71d165cbf 100644 --- a/docs/assets/freqtrade_poweredby.svg +++ b/docs/assets/freqtrade_poweredby.svg @@ -34,7 +34,7 @@ - Freqtrade + Freqtrade From 0ddc68b37d82bb7ee64ce0851cd050bce0b3539f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Apr 2021 09:41:49 +0000 Subject: [PATCH 42/73] Bump ccxt from 1.47.47 to 1.48.22 Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.47.47 to 1.48.22. - [Release notes](https://github.com/ccxt/ccxt/releases) - [Changelog](https://github.com/ccxt/ccxt/blob/master/doc/exchanges-by-country.rst) - [Commits](https://github.com/ccxt/ccxt/compare/1.47.47...1.48.22) Signed-off-by: dependabot[bot] --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d71cd1c05..a89eb2383 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.20.2 pandas==1.2.4 -ccxt==1.47.47 +ccxt==1.48.22 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 From a2acb54e7efc90fa2ce8af85284518cb840d26fa Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 15:15:40 +0200 Subject: [PATCH 43/73] Clarify comments in pairlist --- freqtrade/plugins/pairlist/IPairList.py | 2 +- freqtrade/plugins/pairlist/StaticPairList.py | 2 +- freqtrade/plugins/pairlist/VolumePairList.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/plugins/pairlist/IPairList.py b/freqtrade/plugins/pairlist/IPairList.py index 184feff9e..c4a9c3e40 100644 --- a/freqtrade/plugins/pairlist/IPairList.py +++ b/freqtrade/plugins/pairlist/IPairList.py @@ -85,7 +85,7 @@ class IPairList(LoggingMixin, ABC): position in the chain. :param cached_pairlist: Previously generated pairlist (cached) - :param tickers: Tickers (from exchange.get_tickers()). + :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: List of pairs """ raise OperationalException("This Pairlist Handler should not be used " diff --git a/freqtrade/plugins/pairlist/StaticPairList.py b/freqtrade/plugins/pairlist/StaticPairList.py index c5ced48c9..13d30fc47 100644 --- a/freqtrade/plugins/pairlist/StaticPairList.py +++ b/freqtrade/plugins/pairlist/StaticPairList.py @@ -46,7 +46,7 @@ class StaticPairList(IPairList): """ Generate the pairlist :param cached_pairlist: Previously generated pairlist (cached) - :param tickers: Tickers (from exchange.get_tickers()). + :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: List of pairs """ if self._allow_inactive: diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py index dd8fc64fd..e85fb1805 100644 --- a/freqtrade/plugins/pairlist/VolumePairList.py +++ b/freqtrade/plugins/pairlist/VolumePairList.py @@ -67,7 +67,7 @@ class VolumePairList(IPairList): """ Generate the pairlist :param cached_pairlist: Previously generated pairlist (cached) - :param tickers: Tickers (from exchange.get_tickers()). + :param tickers: Tickers (from exchange.get_tickers()). May be cached. :return: List of pairs """ # Generate dynamic whitelist From 75612496d7294ca6b0ccd89a62cb16562109d307 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 19:01:39 +0200 Subject: [PATCH 44/73] Improve poweredBy logo spacing --- docs/assets/freqtrade_poweredby.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/assets/freqtrade_poweredby.svg b/docs/assets/freqtrade_poweredby.svg index 71d165cbf..957ec6401 100644 --- a/docs/assets/freqtrade_poweredby.svg +++ b/docs/assets/freqtrade_poweredby.svg @@ -34,11 +34,11 @@ - Freqtrade + Freqtrade - poweredby + poweredby From c9e901cf325756707e51e4dbda2f060b2fed827b Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 11:31:37 +0200 Subject: [PATCH 45/73] Move wallet tasks to test_wallets --- tests/test_freqtradebot.py | 59 -------------------------------------- tests/test_wallets.py | 59 +++++++++++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 60 deletions(-) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index c91015766..0634df9e4 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -207,65 +207,6 @@ def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_b freqtrade.get_free_open_trades()) -def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5) - freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade) - - with pytest.raises(DependencyException, match=r'.*stake amount.*'): - freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) - - -@pytest.mark.parametrize("balance_ratio,result1", [ - (1, 0.005), - (0.99, 0.00495), - (0.50, 0.0025), - ]) -def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1, - limit_buy_order_open, fee, mocker) -> None: - patch_RPCManager(mocker) - patch_exchange(mocker) - mocker.patch.multiple( - 'freqtrade.exchange.Exchange', - fetch_ticker=ticker, - buy=MagicMock(return_value=limit_buy_order_open), - get_fee=fee - ) - - conf = deepcopy(default_conf) - conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT - conf['dry_run_wallet'] = 0.01 - conf['max_open_trades'] = 2 - conf['tradable_balance_ratio'] = balance_ratio - - freqtrade = FreqtradeBot(conf) - patch_get_signal(freqtrade) - - # no open trades, order amount should be 'balance / max_open_trades' - result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) - assert result == result1 - - # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' - freqtrade.execute_buy('ETH/BTC', result) - - result = freqtrade.wallets.get_trade_stake_amount('LTC/BTC', freqtrade.get_free_open_trades()) - assert result == result1 - - # create 2 trades, order amount should be None - freqtrade.execute_buy('LTC/BTC', result) - - result = freqtrade.wallets.get_trade_stake_amount('XRP/BTC', freqtrade.get_free_open_trades()) - assert result == 0 - - # set max_open_trades = None, so do not trade - conf['max_open_trades'] = 0 - freqtrade = FreqtradeBot(conf) - result = freqtrade.wallets.get_trade_stake_amount('NEO/BTC', freqtrade.get_free_open_trades()) - assert result == 0 - - def test_edge_called_in_process(mocker, edge_conf) -> None: patch_RPCManager(mocker) patch_edge(mocker) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index b7aead0c4..e6e41bab1 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -1,7 +1,12 @@ # pragma pylint: disable=missing-docstring +from copy import deepcopy from unittest.mock import MagicMock -from tests.conftest import get_patched_freqtradebot +import pytest + +from freqtrade.constants import UNLIMITED_STAKE_AMOUNT +from freqtrade.exceptions import DependencyException +from tests.conftest import get_patched_freqtradebot, patch_wallet def test_sync_wallet_at_boot(mocker, default_conf): @@ -106,3 +111,55 @@ def test_sync_wallet_missing_data(mocker, default_conf): assert freqtrade.wallets._wallets['GAS'].used is None assert freqtrade.wallets._wallets['GAS'].total == 0.260739 assert freqtrade.wallets.get_free('GAS') == 0.260739 + + +def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: + patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5) + freqtrade = get_patched_freqtradebot(mocker, default_conf) + + with pytest.raises(DependencyException, match=r'.*stake amount.*'): + freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) + + +@pytest.mark.parametrize("balance_ratio,result1", [ + (1, 0.005), + (0.99, 0.00495), + (0.50, 0.0025), +]) +def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1, + limit_buy_order_open, fee, mocker) -> None: + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + fetch_ticker=ticker, + buy=MagicMock(return_value=limit_buy_order_open), + get_fee=fee + ) + + conf = deepcopy(default_conf) + conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT + conf['dry_run_wallet'] = 0.01 + conf['max_open_trades'] = 2 + conf['tradable_balance_ratio'] = balance_ratio + + freqtrade = get_patched_freqtradebot(mocker, conf) + + # no open trades, order amount should be 'balance / max_open_trades' + result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) + assert result == result1 + + # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' + freqtrade.execute_buy('ETH/BTC', result) + + result = freqtrade.wallets.get_trade_stake_amount('LTC/BTC', freqtrade.get_free_open_trades()) + assert result == result1 + + # create 2 trades, order amount should be None + freqtrade.execute_buy('LTC/BTC', result) + + result = freqtrade.wallets.get_trade_stake_amount('XRP/BTC', freqtrade.get_free_open_trades()) + assert result == 0 + + # set max_open_trades = None, so do not trade + freqtrade.config['max_open_trades'] = 0 + result = freqtrade.wallets.get_trade_stake_amount('NEO/BTC', freqtrade.get_free_open_trades()) + assert result == 0 From bd7e535e42ccca777697f497d0e9264698c4421a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 11:58:47 +0200 Subject: [PATCH 46/73] Use "human" amounts in stake_amount tests --- tests/test_wallets.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/test_wallets.py b/tests/test_wallets.py index e6e41bab1..562957790 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -122,9 +122,9 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: @pytest.mark.parametrize("balance_ratio,result1", [ - (1, 0.005), - (0.99, 0.00495), - (0.50, 0.0025), + (1, 50), + (0.99, 49.5), + (0.50, 25), ]) def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1, limit_buy_order_open, fee, mocker) -> None: @@ -137,29 +137,29 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r conf = deepcopy(default_conf) conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT - conf['dry_run_wallet'] = 0.01 + conf['dry_run_wallet'] = 100 conf['max_open_trades'] = 2 conf['tradable_balance_ratio'] = balance_ratio freqtrade = get_patched_freqtradebot(mocker, conf) # no open trades, order amount should be 'balance / max_open_trades' - result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('ETH/USDT', freqtrade.get_free_open_trades()) assert result == result1 # create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)' - freqtrade.execute_buy('ETH/BTC', result) + freqtrade.execute_buy('ETH/USDT', result) - result = freqtrade.wallets.get_trade_stake_amount('LTC/BTC', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('LTC/USDDT', freqtrade.get_free_open_trades()) assert result == result1 # create 2 trades, order amount should be None freqtrade.execute_buy('LTC/BTC', result) - result = freqtrade.wallets.get_trade_stake_amount('XRP/BTC', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT', freqtrade.get_free_open_trades()) assert result == 0 # set max_open_trades = None, so do not trade freqtrade.config['max_open_trades'] = 0 - result = freqtrade.wallets.get_trade_stake_amount('NEO/BTC', freqtrade.get_free_open_trades()) + result = freqtrade.wallets.get_trade_stake_amount('NEO/USDT', freqtrade.get_free_open_trades()) assert result == 0 From 2254f65fa7f8c32d2d23d559fa57f297bf424dd0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 12:54:22 +0200 Subject: [PATCH 47/73] use binance intests instead of bittrex --- tests/commands/test_commands.py | 4 +- tests/config_test_comments.json | 2 +- tests/conftest.py | 8 ++-- tests/conftest_trades.py | 12 ++--- tests/plugins/test_protections.py | 2 +- tests/rpc/test_rpc.py | 4 +- tests/rpc/test_rpc_apiserver.py | 8 ++-- tests/rpc/test_rpc_manager.py | 2 +- tests/rpc/test_rpc_telegram.py | 22 ++++----- tests/rpc/test_rpc_webhook.py | 10 ++-- tests/strategy/test_interface.py | 8 ++-- tests/test_freqtradebot.py | 77 ++++++++++++++++++++----------- tests/test_persistence.py | 54 +++++++++++----------- 13 files changed, 117 insertions(+), 96 deletions(-) diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py index 232fc4e2c..d86bced5d 100644 --- a/tests/commands/test_commands.py +++ b/tests/commands/test_commands.py @@ -116,7 +116,7 @@ def test_list_timeframes(mocker, capsys): '1h': 'hour', '1d': 'day', } - patch_exchange(mocker, api_mock=api_mock) + patch_exchange(mocker, api_mock=api_mock, id='bittrex') args = [ "list-timeframes", ] @@ -201,7 +201,7 @@ def test_list_markets(mocker, markets, capsys): api_mock = MagicMock() api_mock.markets = markets - patch_exchange(mocker, api_mock=api_mock) + patch_exchange(mocker, api_mock=api_mock, id='bittrex') # Test with no --config args = [ diff --git a/tests/config_test_comments.json b/tests/config_test_comments.json index 4f201f86c..48a087dec 100644 --- a/tests/config_test_comments.json +++ b/tests/config_test_comments.json @@ -59,7 +59,7 @@ } }, "exchange": { - "name": "bittrex", + "name": "binance", "sandbox": false, "key": "your_exchange_key", "secret": "your_exchange_secret", diff --git a/tests/conftest.py b/tests/conftest.py index 4a2106a4d..cc4fe91f0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -79,7 +79,7 @@ def patched_configuration_load_config_file(mocker, config) -> None: ) -def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> None: +def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> None: mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) @@ -98,7 +98,7 @@ def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> No mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock()) -def get_patched_exchange(mocker, config, api_mock=None, id='bittrex', +def get_patched_exchange(mocker, config, api_mock=None, id='binance', mock_markets=True) -> Exchange: patch_exchange(mocker, api_mock, id, mock_markets) config['exchange']['name'] = id @@ -293,7 +293,7 @@ def get_default_conf(testdatadir): "order_book_max": 1 }, "exchange": { - "name": "bittrex", + "name": "binance", "enabled": True, "key": "key", "secret": "secret", @@ -1765,7 +1765,7 @@ def open_trade(): return Trade( pair='ETH/BTC', open_rate=0.00001099, - exchange='bittrex', + exchange='binance', open_order_id='123456789', amount=90.99181073, fee_open=0.0, diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index 34fc58aee..b92b51144 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -31,7 +31,7 @@ def mock_trade_1(fee): is_open=True, open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17), open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='dry_run_buy_12345', strategy='DefaultStrategy', timeframe=5, @@ -84,7 +84,7 @@ def mock_trade_2(fee): close_rate=0.128, close_profit=0.005, close_profit_abs=0.000584127, - exchange='bittrex', + exchange='binance', is_open=False, open_order_id='dry_run_sell_12345', strategy='DefaultStrategy', @@ -144,7 +144,7 @@ def mock_trade_3(fee): close_rate=0.06, close_profit=0.01, close_profit_abs=0.000155, - exchange='bittrex', + exchange='binance', is_open=False, strategy='DefaultStrategy', timeframe=5, @@ -187,7 +187,7 @@ def mock_trade_4(fee): open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=14), is_open=True, open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='prod_buy_12345', strategy='DefaultStrategy', timeframe=5, @@ -239,7 +239,7 @@ def mock_trade_5(fee): open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=12), is_open=True, open_rate=0.123, - exchange='bittrex', + exchange='binance', strategy='SampleStrategy', stoploss_order_id='prod_stoploss_3455', timeframe=5, @@ -293,7 +293,7 @@ def mock_trade_6(fee): fee_close=fee.return_value, is_open=True, open_rate=0.15, - exchange='bittrex', + exchange='binance', strategy='SampleStrategy', open_order_id="prod_sell_6", timeframe=5, diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py index 545387eaa..a39301145 100644 --- a/tests/plugins/test_protections.py +++ b/tests/plugins/test_protections.py @@ -27,7 +27,7 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool, open_rate=open_rate, is_open=is_open, amount=0.01 / open_rate, - exchange='bittrex', + exchange='binance', ) trade.recalc_open_trade_value() if not is_open: diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 63e09cbe1..6d31e7635 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -106,7 +106,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist': -0.00010475, 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, - 'exchange': 'bittrex', + 'exchange': 'binance', } mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', @@ -172,7 +172,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist': -0.00010475, 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, - 'exchange': 'bittrex', + 'exchange': 'binance', } diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index d610906d5..6505629eb 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -468,7 +468,7 @@ def test_api_show_config(botclient, mocker): rc = client_get(client, f"{BASE_URI}/show_config") assert_response(rc) assert 'dry_run' in rc.json() - assert rc.json()['exchange'] == 'bittrex' + assert rc.json()['exchange'] == 'binance' assert rc.json()['timeframe'] == '5m' assert rc.json()['timeframe_ms'] == 300000 assert rc.json()['timeframe_min'] == 5 @@ -825,7 +825,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'sell_order_status': None, 'strategy': 'DefaultStrategy', 'timeframe': 5, - 'exchange': 'bittrex', + 'exchange': 'binance', } mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', @@ -916,7 +916,7 @@ def test_api_forcebuy(botclient, mocker, fee): pair='ETH/ETH', amount=1, amount_requested=1, - exchange='bittrex', + exchange='binance', stake_amount=1, open_rate=0.245441, open_order_id="123456", @@ -979,7 +979,7 @@ def test_api_forcebuy(botclient, mocker, fee): 'sell_order_status': None, 'strategy': 'DefaultStrategy', 'timeframe': 5, - 'exchange': 'bittrex', + 'exchange': 'binance', } diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 3068e9764..6996c932b 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -140,7 +140,7 @@ def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None: rpc_manager.startup_messages(default_conf, freqtradebot.pairlists, freqtradebot.protections) assert telegram_mock.call_count == 3 - assert "*Exchange:* `bittrex`" in telegram_mock.call_args_list[1][0][0]['status'] + assert "*Exchange:* `binance`" in telegram_mock.call_args_list[1][0][0]['status'] telegram_mock.reset_mock() default_conf['dry_run'] = True diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 34bf057cb..ba32dc385 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -688,7 +688,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'profit', 'limit': 1.173e-05, @@ -749,7 +749,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'loss', 'limit': 1.043e-05, @@ -800,7 +800,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'loss', 'limit': 1.099e-05, @@ -1178,7 +1178,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None: telegram._show_config(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] - assert '*Exchange:* `bittrex`' in msg_mock.call_args_list[0][0][0] + assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0] assert '*Strategy:* `DefaultStrategy`' in msg_mock.call_args_list[0][0][0] assert '*Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] @@ -1187,7 +1187,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None: telegram._show_config(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0] - assert '*Exchange:* `bittrex`' in msg_mock.call_args_list[0][0][0] + assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0] assert '*Strategy:* `DefaultStrategy`' in msg_mock.call_args_list[0][0][0] assert '*Initial Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0] @@ -1197,7 +1197,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: msg = { 'type': RPCMessageType.BUY_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 1.099e-05, 'order_type': 'limit', @@ -1213,7 +1213,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: telegram.send_msg(msg) assert msg_mock.call_args[0][0] \ - == '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC (#1)\n' \ + == '\N{LARGE BLUE CIRCLE} *Binance:* Buying ETH/BTC (#1)\n' \ '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00001099`\n' \ '*Current Rate:* `0.00001099`\n' \ @@ -1242,11 +1242,11 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: telegram.send_msg({ 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'reason': CANCEL_REASON['TIMEOUT'] }) - assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Bittrex:* ' + assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Binance:* ' 'Cancelling open buy Order for ETH/BTC (#1). ' 'Reason: cancelled due to timeout.') @@ -1393,7 +1393,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: telegram.send_msg({ 'type': RPCMessageType.BUY_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 1.099e-05, 'order_type': 'limit', @@ -1405,7 +1405,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: 'amount': 1333.3333333333335, 'open_date': arrow.utcnow().shift(hours=-1) }) - assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC (#1)\n' + assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Binance:* Buying ETH/BTC (#1)\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00001099`\n' '*Current Rate:* `0.00001099`\n' diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 5361cd947..62818ecbb 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -59,7 +59,7 @@ def test_send_msg(default_conf, mocker): mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) msg = { 'type': RPCMessageType.BUY_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, 'stake_amount': 0.8, @@ -80,7 +80,7 @@ def test_send_msg(default_conf, mocker): mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) msg = { 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, 'stake_amount': 0.8, @@ -101,7 +101,7 @@ def test_send_msg(default_conf, mocker): mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) msg = { 'type': RPCMessageType.SELL_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': "profit", 'limit': 0.005, @@ -127,7 +127,7 @@ def test_send_msg(default_conf, mocker): mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) msg = { 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': "profit", 'limit': 0.005, @@ -184,7 +184,7 @@ def test_exception_send_msg(default_conf, mocker, caplog): webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) msg = { 'type': RPCMessageType.BUY_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, 'order_type': 'limit', diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 0ee80e0c5..78fa368e4 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -219,7 +219,7 @@ def test_min_roi_reached(default_conf, fee) -> None: open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, ) @@ -258,7 +258,7 @@ def test_min_roi_reached2(default_conf, fee) -> None: open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, ) @@ -293,7 +293,7 @@ def test_min_roi_reached3(default_conf, fee) -> None: open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, ) @@ -346,7 +346,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili open_date=arrow.utcnow().shift(hours=-1).datetime, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, ) trade.adjust_min_max_rates(trade.open_rate) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 0634df9e4..433cce170 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -362,7 +362,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> Non assert trade.stake_amount == 0.001 assert trade.is_open assert trade.open_date is not None - assert trade.exchange == 'bittrex' + assert trade.exchange == 'binance' # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) @@ -621,7 +621,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, limit_buy assert trade.stake_amount == default_conf['stake_amount'] assert trade.is_open assert trade.open_date is not None - assert trade.exchange == 'bittrex' + assert trade.exchange == 'binance' assert trade.open_rate == 0.00001098 assert trade.amount == 91.07468123 @@ -718,7 +718,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, is_open=True, amount=20, open_rate=0.01, - exchange='bittrex', + exchange='binance', )) Trade.query.session.add(Trade( pair='ETH/BTC', @@ -728,7 +728,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, is_open=True, amount=12, open_rate=0.001, - exchange='bittrex', + exchange='binance', )) assert pair not in freqtrade.active_pair_whitelist @@ -969,7 +969,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None return_value=limit_buy_order['amount']) stoploss = MagicMock(return_value={'id': 13434334}) - mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) + mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) freqtrade = FreqtradeBot(default_conf) freqtrade.strategy.order_types['stoploss_on_exchange'] = True @@ -1001,6 +1001,9 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', stoploss=stoploss ) freqtrade = FreqtradeBot(default_conf) @@ -1025,7 +1028,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = 100 hanging_stoploss_order = MagicMock(return_value={'status': 'open'}) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', hanging_stoploss_order) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', hanging_stoploss_order) assert freqtrade.handle_stoploss_on_exchange(trade) is False assert trade.stoploss_order_id == 100 @@ -1038,7 +1041,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.stoploss_order_id = 100 canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'}) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', canceled_stoploss_order) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', canceled_stoploss_order) stoploss.reset_mock() assert freqtrade.handle_stoploss_on_exchange(trade) is False @@ -1064,14 +1067,14 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, 'average': 2, 'amount': limit_buy_order['amount'], }) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hit) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hit) assert freqtrade.handle_stoploss_on_exchange(trade) is True assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog) assert trade.stoploss_order_id is None assert trade.is_open is False mocker.patch( - 'freqtrade.exchange.Exchange.stoploss', + 'freqtrade.exchange.Binance.stoploss', side_effect=ExchangeError() ) trade.is_open = True @@ -1083,9 +1086,9 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, # It should try to add stoploss order trade.stoploss_order_id = 100 stoploss.reset_mock() - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', side_effect=InvalidOrderException()) - mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) + mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) freqtrade.handle_stoploss_on_exchange(trade) assert stoploss.call_count == 1 @@ -1095,7 +1098,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, trade.is_open = False stoploss.reset_mock() mocker.patch('freqtrade.exchange.Exchange.fetch_order') - mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss) + mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss) assert freqtrade.handle_stoploss_on_exchange(trade) is False assert stoploss.call_count == 0 @@ -1115,6 +1118,9 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', fetch_stoploss_order=MagicMock(return_value={'status': 'canceled', 'id': 100}), stoploss=MagicMock(side_effect=ExchangeError()), ) @@ -1149,6 +1155,9 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, buy=MagicMock(return_value=limit_buy_order_open), sell=sell_mock, get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', fetch_order=MagicMock(return_value={'status': 'canceled'}), stoploss=MagicMock(side_effect=InvalidOrderException()), ) @@ -1194,6 +1203,9 @@ def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog, sell=sell_mock, get_fee=fee, fetch_order=MagicMock(return_value={'status': 'canceled'}), + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', stoploss=MagicMock(side_effect=InsufficientFundsError()), ) patch_get_signal(freqtrade) @@ -1231,6 +1243,9 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', stoploss=stoploss, stoploss_adjust=MagicMock(return_value=True), ) @@ -1271,7 +1286,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, } }) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hanging) # stoploss initially at 5% assert freqtrade.handle_trade(trade) is False @@ -1286,8 +1301,8 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, cancel_order_mock = MagicMock() stoploss_order_mock = MagicMock(return_value={'id': 13434334}) - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock) - mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock) + mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order', cancel_order_mock) + mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock) # stoploss should not be updated as the interval is 60 seconds assert freqtrade.handle_trade(trade) is False @@ -1334,6 +1349,9 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', stoploss=stoploss, stoploss_adjust=MagicMock(return_value=True), ) @@ -1369,9 +1387,9 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c 'stopPrice': '0.1' } } - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', + mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order', side_effect=InvalidOrderException()) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hanging) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog) @@ -1380,8 +1398,8 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c # Fail creating stoploss order caplog.clear() - cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_stoploss_order", MagicMock()) - mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=ExchangeError()) + cancel_mock = mocker.patch("freqtrade.exchange.Binance.cancel_stoploss_order", MagicMock()) + mocker.patch("freqtrade.exchange.Binance.stoploss", side_effect=ExchangeError()) freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging) assert cancel_mock.call_count == 1 assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog) @@ -1403,6 +1421,9 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, buy=MagicMock(return_value={'id': limit_buy_order['id']}), sell=MagicMock(return_value={'id': limit_sell_order['id']}), get_fee=fee, + ) + mocker.patch.multiple( + 'freqtrade.exchange.Binance', stoploss=stoploss, stoploss_adjust=MagicMock(return_value=True), ) @@ -1443,7 +1464,7 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, } }) - mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging) + mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hanging) assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False @@ -1457,8 +1478,8 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, cancel_order_mock = MagicMock() stoploss_order_mock = MagicMock(return_value={'id': 13434334}) - mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock) - mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock) + mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order', cancel_order_mock) + mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock) # stoploss should not be updated as the interval is 60 seconds assert freqtrade.handle_trade(trade) is False @@ -2603,7 +2624,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N assert { 'trade_id': 1, 'type': RPCMessageType.SELL_NOTIFICATION, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'profit', 'limit': 1.172e-05, @@ -2653,7 +2674,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'loss', 'limit': 1.044e-05, @@ -2710,7 +2731,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'loss', 'limit': 1.08801e-05, @@ -2916,7 +2937,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, - 'exchange': 'Bittrex', + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'profit', 'limit': 1.172e-05, @@ -3899,7 +3920,7 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order_open, assert trade.stake_amount == 0.001 assert trade.is_open assert trade.open_date is not None - assert trade.exchange == 'bittrex' + assert trade.exchange == 'binance' assert len(Trade.query.all()) == 1 @@ -4355,7 +4376,7 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog): is_open=True, amount=20, open_rate=0.01, - exchange='bittrex', + exchange='binance', ) Trade.query.session.add(trade) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 0a3d6858d..3b90f368f 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -64,7 +64,7 @@ def test_init_dryrun_db(default_conf, tmpdir): @pytest.mark.usefixtures("init_persistence") -def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog): +def test_update_with_binance(limit_buy_order, limit_sell_order, fee, caplog): """ On this test we will buy and sell a crypto currency. @@ -102,7 +102,7 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog): open_date=arrow.utcnow().datetime, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) assert trade.open_order_id is None assert trade.close_profit is None @@ -142,7 +142,7 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog): fee_open=fee.return_value, fee_close=fee.return_value, open_date=arrow.utcnow().datetime, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'something' @@ -177,7 +177,7 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee): amount=5, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'something' @@ -205,7 +205,7 @@ def test_trade_close(limit_buy_order, limit_sell_order, fee): fee_open=fee.return_value, fee_close=fee.return_value, open_date=arrow.Arrow(2020, 2, 1, 15, 5, 1).datetime, - exchange='bittrex', + exchange='binance', ) assert trade.close_profit is None assert trade.close_date is None @@ -233,7 +233,7 @@ def test_calc_close_trade_price_exception(limit_buy_order, fee): amount=5, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'something' @@ -250,7 +250,7 @@ def test_update_open_order(limit_buy_order): amount=5, fee_open=0.1, fee_close=0.1, - exchange='bittrex', + exchange='binance', ) assert trade.open_order_id is None @@ -274,7 +274,7 @@ def test_update_invalid_order(limit_buy_order): open_rate=0.001, fee_open=0.1, fee_close=0.1, - exchange='bittrex', + exchange='binance', ) limit_buy_order['type'] = 'invalid' with pytest.raises(ValueError, match=r'Unknown order type'): @@ -290,7 +290,7 @@ def test_calc_open_trade_value(limit_buy_order, fee): open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'open_trade' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -311,7 +311,7 @@ def test_calc_close_trade_price(limit_buy_order, limit_sell_order, fee): open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'close_trade' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -336,7 +336,7 @@ def test_calc_profit(limit_buy_order, limit_sell_order, fee): open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'something' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -370,7 +370,7 @@ def test_calc_profit_ratio(limit_buy_order, limit_sell_order, fee): open_rate=0.00001099, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', ) trade.open_order_id = 'something' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -400,7 +400,7 @@ def test_clean_dry_run_db(default_conf, fee): fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='dry_run_buy_12345' ) Trade.query.session.add(trade) @@ -412,7 +412,7 @@ def test_clean_dry_run_db(default_conf, fee): fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='dry_run_sell_12345' ) Trade.query.session.add(trade) @@ -425,7 +425,7 @@ def test_clean_dry_run_db(default_conf, fee): fee_open=fee.return_value, fee_close=fee.return_value, open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='prod_buy_12345' ) Trade.query.session.add(trade) @@ -463,7 +463,7 @@ def test_migrate_old(mocker, default_conf, fee): );""" insert_table_old = """INSERT INTO trades (exchange, pair, is_open, open_order_id, fee, open_rate, stake_amount, amount, open_date) - VALUES ('BITTREX', 'BTC_ETC', 1, '123123', {fee}, + VALUES ('binance', 'BTC_ETC', 1, '123123', {fee}, 0.00258580, {stake}, {amount}, '2017-11-28 12:44:24.000000') """.format(fee=fee.return_value, @@ -472,7 +472,7 @@ def test_migrate_old(mocker, default_conf, fee): ) insert_table_old2 = """INSERT INTO trades (exchange, pair, is_open, fee, open_rate, close_rate, stake_amount, amount, open_date) - VALUES ('BITTREX', 'BTC_ETC', 0, {fee}, + VALUES ('binance', 'BTC_ETC', 0, {fee}, 0.00258580, 0.00268580, {stake}, {amount}, '2017-11-28 12:44:24.000000') """.format(fee=fee.return_value, @@ -500,7 +500,7 @@ def test_migrate_old(mocker, default_conf, fee): assert trade.amount_requested == amount assert trade.stake_amount == default_conf.get("stake_amount") assert trade.pair == "ETC/BTC" - assert trade.exchange == "bittrex" + assert trade.exchange == "binance" assert trade.max_rate == 0.0 assert trade.stop_loss == 0.0 assert trade.initial_stop_loss == 0.0 @@ -694,7 +694,7 @@ def test_adjust_stop_loss(fee): amount=5, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, max_rate=1, ) @@ -746,7 +746,7 @@ def test_adjust_min_max_rates(fee): amount=5, fee_open=fee.return_value, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, ) @@ -790,7 +790,7 @@ def test_to_json(default_conf, fee): fee_close=fee.return_value, open_date=arrow.utcnow().shift(hours=-2).datetime, open_rate=0.123, - exchange='bittrex', + exchange='binance', open_order_id='dry_run_buy_12345' ) result = trade.to_json() @@ -841,7 +841,7 @@ def test_to_json(default_conf, fee): 'max_rate': None, 'strategy': None, 'timeframe': None, - 'exchange': 'bittrex', + 'exchange': 'binance', } # Simulate dry_run entries @@ -856,7 +856,7 @@ def test_to_json(default_conf, fee): close_date=arrow.utcnow().shift(hours=-1).datetime, open_rate=0.123, close_rate=0.125, - exchange='bittrex', + exchange='binance', ) result = trade.to_json() assert isinstance(result, dict) @@ -906,7 +906,7 @@ def test_to_json(default_conf, fee): 'sell_order_status': None, 'strategy': None, 'timeframe': None, - 'exchange': 'bittrex', + 'exchange': 'binance', } @@ -919,7 +919,7 @@ def test_stoploss_reinitialization(default_conf, fee): open_date=arrow.utcnow().shift(hours=-2).datetime, amount=10, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, max_rate=1, ) @@ -978,7 +978,7 @@ def test_update_fee(fee): open_date=arrow.utcnow().shift(hours=-2).datetime, amount=10, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, max_rate=1, ) @@ -1017,7 +1017,7 @@ def test_fee_updated(fee): open_date=arrow.utcnow().shift(hours=-2).datetime, amount=10, fee_close=fee.return_value, - exchange='bittrex', + exchange='binance', open_rate=1, max_rate=1, ) From 71b017e7c34c837b13e039740154cd0896d7bf79 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 19:53:16 +0200 Subject: [PATCH 48/73] Simplify webhook test --- tests/rpc/test_rpc_webhook.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 62818ecbb..bfb9cbb01 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -25,6 +25,11 @@ def get_webhook_dict() -> dict: "value2": "limit {limit:8f}", "value3": "{stake_amount:8f} {stake_currency}" }, + "webhookbuyfill": { + "value1": "Buy Order for {pair} filled", + "value2": "at {open_rate:8f}", + "value3": "{stake_amount:8f} {stake_currency}" + }, "webhooksell": { "value1": "Selling {pair}", "value2": "limit {limit:8f}", @@ -35,6 +40,11 @@ def get_webhook_dict() -> dict: "value2": "limit {limit:8f}", "value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})" }, + "webhooksellfill": { + "value1": "Sell Order for {pair} filled", + "value2": "at {close_rate:8f}", + "value3": "{stake_amount:8f} {stake_currency}" + }, "webhookstatus": { "value1": "Status: {status}", "value2": "", @@ -76,8 +86,8 @@ def test_send_msg(default_conf, mocker): assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookbuy"]["value3"].format(**msg)) # Test buy cancel - msg_mock = MagicMock() - mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) + msg_mock.reset_mock() + msg = { 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, 'exchange': 'Binance', @@ -97,8 +107,7 @@ def test_send_msg(default_conf, mocker): assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookbuycancel"]["value3"].format(**msg)) # Test sell - msg_mock = MagicMock() - mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) + msg_mock.reset_mock() msg = { 'type': RPCMessageType.SELL_NOTIFICATION, 'exchange': 'Binance', @@ -123,8 +132,7 @@ def test_send_msg(default_conf, mocker): assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhooksell"]["value3"].format(**msg)) # Test sell cancel - msg_mock = MagicMock() - mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) + msg_mock.reset_mock() msg = { 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, 'exchange': 'Binance', From fecd5c582b81e191b82a5d90834b0592eb1ffbe7 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 19:58:29 +0200 Subject: [PATCH 49/73] Add buy and sell fill notifications closes #3542 --- config_full.json.example | 2 ++ docs/webhook-config.md | 46 +++++++++++++++++++++++++++++++ freqtrade/freqtradebot.py | 28 ++++++++++++++++--- freqtrade/rpc/rpc.py | 2 ++ freqtrade/rpc/telegram.py | 8 ++++++ freqtrade/rpc/webhook.py | 4 +++ tests/rpc/test_rpc_webhook.py | 51 +++++++++++++++++++++++++++++++++-- 7 files changed, 136 insertions(+), 5 deletions(-) diff --git a/config_full.json.example b/config_full.json.example index 717797933..973afe2c8 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -163,7 +163,9 @@ "warning": "on", "startup": "on", "buy": "on", + "buy_fill": "on", "sell": "on", + "sell_fill": "on", "buy_cancel": "on", "sell_cancel": "on" } diff --git a/docs/webhook-config.md b/docs/webhook-config.md index 2e41ad2cc..8ce6edc18 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -19,6 +19,11 @@ Sample configuration (tested using IFTTT). "value1": "Cancelling Open Buy Order for {pair}", "value2": "limit {limit:8f}", "value3": "{stake_amount:8f} {stake_currency}" + }, + "webhookbuyfill": { + "value1": "Buy Order for {pair} filled", + "value2": "at {open_rate:8f}", + "value3": "" }, "webhooksell": { "value1": "Selling {pair}", @@ -30,6 +35,11 @@ Sample configuration (tested using IFTTT). "value2": "limit {limit:8f}", "value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})" }, + "webhooksellfill": { + "value1": "Sell Order for {pair} filled", + "value2": "at {close_rate:8f}.", + "value3": "" + }, "webhookstatus": { "value1": "Status: {status}", "value2": "", @@ -91,6 +101,21 @@ Possible parameters are: * `order_type` * `current_rate` +### Webhookbuyfill + +The fields in `webhook.webhookbuyfill` are filled when the bot filled a buy order. Parameters are filled using string.format. +Possible parameters are: + +* `trade_id` +* `exchange` +* `pair` +* `open_rate` +* `amount` +* `open_date` +* `stake_amount` +* `stake_currency` +* `fiat_currency` + ### Webhooksell The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format. @@ -103,6 +128,27 @@ Possible parameters are: * `limit` * `amount` * `open_rate` +* `profit_amount` +* `profit_ratio` +* `stake_currency` +* `fiat_currency` +* `sell_reason` +* `order_type` +* `open_date` +* `close_date` + +### Webhooksellfill + +The fields in `webhook.webhooksellfill` are filled when the bot fills a sell order (closes a Trae). Parameters are filled using string.format. +Possible parameters are: + +* `trade_id` +* `exchange` +* `pair` +* `gain` +* `close_rate` +* `amount` +* `open_rate` * `current_rate` * `profit_amount` * `profit_ratio` diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 1ebf28ebd..68f98ec21 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -675,6 +675,21 @@ class FreqtradeBot(LoggingMixin): # Send the message self.rpc.send_msg(msg) + def _notify_buy_fill(self, trade: Trade) -> None: + msg = { + 'trade_id': trade.id, + 'type': RPCMessageType.BUY_FILL_NOTIFICATION, + 'exchange': self.exchange.name.capitalize(), + 'pair': trade.pair, + 'open_rate': trade.open_rate, + 'stake_amount': trade.stake_amount, + 'stake_currency': self.config['stake_currency'], + 'fiat_currency': self.config.get('fiat_display_currency', None), + 'amount': trade.amount, + 'open_date': trade.open_date, + } + self.rpc.send_msg(msg) + # # SELL / exit positions / close trades logic and methods # @@ -1212,19 +1227,20 @@ class FreqtradeBot(LoggingMixin): return True - def _notify_sell(self, trade: Trade, order_type: str) -> None: + def _notify_sell(self, trade: Trade, order_type: str, fill: bool = False) -> None: """ Sends rpc notification when a sell occured. """ profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_trade = trade.calc_profit(rate=profit_rate) # Use cached rates here - it was updated seconds ago. - current_rate = self.get_sell_rate(trade.pair, False) + current_rate = self.get_sell_rate(trade.pair, False) if not fill else None profit_ratio = trade.calc_profit_ratio(profit_rate) gain = "profit" if profit_ratio > 0 else "loss" msg = { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': (RPCMessageType.SELL_FILL_NOTIFICATION if fill + else RPCMessageType.SELL_NOTIFICATION), 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, @@ -1233,6 +1249,7 @@ class FreqtradeBot(LoggingMixin): 'order_type': order_type, 'amount': trade.amount, 'open_rate': trade.open_rate, + 'close_rate': trade.close_rate, 'current_rate': current_rate, 'profit_amount': profit_trade, 'profit_ratio': profit_ratio, @@ -1344,9 +1361,14 @@ class FreqtradeBot(LoggingMixin): # Updating wallets when order is closed if not trade.is_open: + self._notify_sell(trade, '', True) self.protections.stop_per_pair(trade.pair) self.protections.global_stop() self.wallets.update() + elif trade.open_order_id is None: + # Buy fill + self._notify_buy_fill(trade) + return False def apply_fee_conditional(self, trade: Trade, trade_base_currency: str, diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index b86562e80..bf0b88f6c 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -35,8 +35,10 @@ class RPCMessageType(Enum): WARNING_NOTIFICATION = 'warning' STARTUP_NOTIFICATION = 'startup' BUY_NOTIFICATION = 'buy' + BUY_FILL_NOTIFICATION = 'buy_fill' BUY_CANCEL_NOTIFICATION = 'buy_cancel' SELL_NOTIFICATION = 'sell' + SELL_FILL_NOTIFICATION = 'sell_fill' SELL_CANCEL_NOTIFICATION = 'sell_cancel' def __repr__(self): diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 09b7b235c..4dceeb46c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -209,6 +209,10 @@ class Telegram(RPCHandler): "Cancelling open buy Order for {pair} (#{trade_id}). " "Reason: {reason}.".format(**msg)) + elif msg['type'] == RPCMessageType.BUY_FILL_NOTIFICATION: + message = ("\N{LARGE CIRCLE} *{exchange}:* " + "Buy order for {pair} (#{trade_id}) filled for {open_rate}.".format(**msg)) + elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: msg['amount'] = round(msg['amount'], 8) msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2) @@ -240,6 +244,10 @@ class Telegram(RPCHandler): message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order " "for {pair} (#{trade_id}). Reason: {reason}").format(**msg) + elif msg['type'] == RPCMessageType.SELL_FILL_NOTIFICATION: + message = ("\N{LARGE CIRCLE} *{exchange}:* " + "Sell order for {pair} (#{trade_id}) filled at {close_rate}.".format(**msg)) + elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: message = '*Status:* `{status}`'.format(**msg) diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index 5a30a9be8..c7e012af5 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -49,8 +49,12 @@ class Webhook(RPCHandler): valuedict = self._config['webhook'].get('webhookbuy', None) elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: valuedict = self._config['webhook'].get('webhookbuycancel', None) + elif msg['type'] == RPCMessageType.BUY_FILL_NOTIFICATION: + valuedict = self._config['webhook'].get('webhookbuyfill', None) elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: valuedict = self._config['webhook'].get('webhooksell', None) + elif msg['type'] == RPCMessageType.SELL_FILL_NOTIFICATION: + valuedict = self._config['webhook'].get('webhooksellfill', None) elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: valuedict = self._config['webhook'].get('webhooksellcancel', None) elif msg['type'] in (RPCMessageType.STATUS_NOTIFICATION, diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index bfb9cbb01..38d2fe539 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -43,7 +43,7 @@ def get_webhook_dict() -> dict: "webhooksellfill": { "value1": "Sell Order for {pair} filled", "value2": "at {close_rate:8f}", - "value3": "{stake_amount:8f} {stake_currency}" + "value3": "" }, "webhookstatus": { "value1": "Status: {status}", @@ -59,7 +59,7 @@ def test__init__(mocker, default_conf): assert webhook._config == default_conf -def test_send_msg(default_conf, mocker): +def test_send_msg_webhook(default_conf, mocker): default_conf["webhook"] = get_webhook_dict() msg_mock = MagicMock() mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) @@ -106,6 +106,27 @@ def test_send_msg(default_conf, mocker): default_conf["webhook"]["webhookbuycancel"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhookbuycancel"]["value3"].format(**msg)) + # Test buy fill + msg_mock.reset_mock() + + msg = { + 'type': RPCMessageType.BUY_FILL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'open_rate': 0.005, + 'stake_amount': 0.8, + 'stake_amount_fiat': 500, + 'stake_currency': 'BTC', + 'fiat_currency': 'EUR' + } + webhook.send_msg(msg=msg) + assert msg_mock.call_count == 1 + assert (msg_mock.call_args[0][0]["value1"] == + default_conf["webhook"]["webhookbuyfill"]["value1"].format(**msg)) + assert (msg_mock.call_args[0][0]["value2"] == + default_conf["webhook"]["webhookbuyfill"]["value2"].format(**msg)) + assert (msg_mock.call_args[0][0]["value3"] == + default_conf["webhook"]["webhookbuyfill"]["value3"].format(**msg)) # Test sell msg_mock.reset_mock() msg = { @@ -156,6 +177,32 @@ def test_send_msg(default_conf, mocker): default_conf["webhook"]["webhooksellcancel"]["value2"].format(**msg)) assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhooksellcancel"]["value3"].format(**msg)) + # Test Sell fill + msg_mock.reset_mock() + msg = { + 'type': RPCMessageType.SELL_FILL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'gain': "profit", + 'close_rate': 0.005, + 'amount': 0.8, + 'order_type': 'limit', + 'open_rate': 0.004, + 'current_rate': 0.005, + 'profit_amount': 0.001, + 'profit_ratio': 0.20, + 'stake_currency': 'BTC', + 'sell_reason': SellType.STOP_LOSS.value + } + webhook.send_msg(msg=msg) + assert msg_mock.call_count == 1 + assert (msg_mock.call_args[0][0]["value1"] == + default_conf["webhook"]["webhooksellfill"]["value1"].format(**msg)) + assert (msg_mock.call_args[0][0]["value2"] == + default_conf["webhook"]["webhooksellfill"]["value2"].format(**msg)) + assert (msg_mock.call_args[0][0]["value3"] == + default_conf["webhook"]["webhooksellfill"]["value3"].format(**msg)) + for msgtype in [RPCMessageType.STATUS_NOTIFICATION, RPCMessageType.WARNING_NOTIFICATION, RPCMessageType.STARTUP_NOTIFICATION]: From 8800a097700077955bc5149d567c1df92e1f9445 Mon Sep 17 00:00:00 2001 From: Matthias Date: Mon, 19 Apr 2021 21:32:04 +0200 Subject: [PATCH 50/73] Don't send double-notifications for stoploss fills --- freqtrade/freqtradebot.py | 3 ++- tests/rpc/test_rpc_telegram.py | 13 ++++++++----- tests/test_freqtradebot.py | 23 +++++++++++++++-------- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 68f98ec21..76212bf97 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1361,7 +1361,8 @@ class FreqtradeBot(LoggingMixin): # Updating wallets when order is closed if not trade.is_open: - self._notify_sell(trade, '', True) + if not stoploss_order: + self._notify_sell(trade, '', True) self.protections.stop_per_pair(trade.pair) self.protections.global_stop() self.wallets.update() diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index ba32dc385..a3c823aac 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -683,7 +683,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, context.args = ["1"] telegram._forcesell(update=update, context=context) - assert msg_mock.call_count == 3 + assert msg_mock.call_count == 4 last_msg = msg_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, @@ -703,6 +703,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, 'sell_reason': SellType.FORCE_SELL.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == last_msg @@ -743,7 +744,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, context.args = ["1"] telegram._forcesell(update=update, context=context) - assert msg_mock.call_count == 3 + assert msg_mock.call_count == 4 last_msg = msg_mock.call_args_list[-1][0][0] assert { @@ -764,6 +765,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, 'sell_reason': SellType.FORCE_SELL.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == last_msg @@ -794,9 +796,9 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None context.args = ["all"] telegram._forcesell(update=update, context=context) - # Called for each trade 3 times - assert msg_mock.call_count == 8 - msg = msg_mock.call_args_list[1][0][0] + # Called for each trade 4 times + assert msg_mock.call_count == 12 + msg = msg_mock.call_args_list[2][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, 'trade_id': 1, @@ -815,6 +817,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None 'sell_reason': SellType.FORCE_SELL.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == msg diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 433cce170..39c3f0561 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1710,6 +1710,7 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No open_rate=0.01, open_date=arrow.utcnow().datetime, amount=11, + exchange="binance", ) assert not freqtrade.update_trade_state(trade, None) assert log_has_re(r'Orderid for trade .* is empty.', caplog) @@ -2262,7 +2263,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, # check it does cancel sell orders over the time limit freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 1 + assert rpc_mock.call_count == 2 assert open_trade.is_open is True # Custom user sell-timeout is never called assert freqtrade.strategy.check_sell_timeout.call_count == 0 @@ -2319,7 +2320,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old # note this is for a partially-complete buy order freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 1 + assert rpc_mock.call_count == 2 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() assert len(trades) == 1 assert trades[0].amount == 23.0 @@ -2354,7 +2355,7 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap assert log_has_re(r"Applying fee on amount for Trade.*", caplog) assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 1 + assert rpc_mock.call_count == 2 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() assert len(trades) == 1 # Verify that trade has been updated @@ -2394,7 +2395,7 @@ def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade, assert log_has_re(r"Could not update trade amount: .*", caplog) assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 1 + assert rpc_mock.call_count == 2 trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all() assert len(trades) == 1 # Verify that trade has been updated @@ -2639,6 +2640,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N 'sell_reason': SellType.ROI.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == last_msg @@ -2689,6 +2691,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) 'sell_reason': SellType.STOP_LOSS.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == last_msg @@ -2746,7 +2749,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe 'sell_reason': SellType.STOP_LOSS.value, 'open_date': ANY, 'close_date': ANY, - + 'close_rate': ANY, } == last_msg @@ -2830,7 +2833,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke trade = Trade.query.first() assert trade assert cancel_order.call_count == 1 - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 3 def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, fee, @@ -2898,7 +2901,10 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f assert trade.stoploss_order_id is None assert trade.is_open is False assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 3 + assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.BUY_NOTIFICATION + assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.BUY_FILL_NOTIFICATION + assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL_NOTIFICATION def test_execute_sell_market_order(default_conf, ticker, fee, @@ -2932,7 +2938,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, assert not trade.is_open assert trade.close_profit == 0.0620716 - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 3 last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'type': RPCMessageType.SELL_NOTIFICATION, @@ -2952,6 +2958,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, 'sell_reason': SellType.ROI.value, 'open_date': ANY, 'close_date': ANY, + 'close_rate': ANY, } == last_msg From 0341ac5a55bc0a985776c38137489bc81960e80a Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 06:41:58 +0200 Subject: [PATCH 51/73] rename RPC message types --- freqtrade/freqtradebot.py | 16 ++--- freqtrade/rpc/rpc.py | 18 ++--- freqtrade/rpc/rpc_manager.py | 8 +-- freqtrade/rpc/telegram.py | 117 +++++++++++++++++---------------- freqtrade/rpc/webhook.py | 18 ++--- tests/rpc/test_rpc_manager.py | 6 +- tests/rpc/test_rpc_telegram.py | 28 ++++---- tests/rpc/test_rpc_webhook.py | 28 ++++---- tests/test_freqtradebot.py | 14 ++-- 9 files changed, 128 insertions(+), 125 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 76212bf97..c48ea851e 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -113,7 +113,7 @@ class FreqtradeBot(LoggingMixin): via RPC about changes in the bot status. """ self.rpc.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, + 'type': RPCMessageType.STATUS, 'status': msg }) @@ -205,7 +205,7 @@ class FreqtradeBot(LoggingMixin): if len(open_trades) != 0: msg = { - 'type': RPCMessageType.WARNING_NOTIFICATION, + 'type': RPCMessageType.WARNING, 'status': f"{len(open_trades)} open trades active.\n\n" f"Handle these trades manually on {self.exchange.name}, " f"or '/start' the bot again and use '/stopbuy' " @@ -634,7 +634,7 @@ class FreqtradeBot(LoggingMixin): """ msg = { 'trade_id': trade.id, - 'type': RPCMessageType.BUY_NOTIFICATION, + 'type': RPCMessageType.BUY, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, 'limit': trade.open_rate, @@ -658,7 +658,7 @@ class FreqtradeBot(LoggingMixin): msg = { 'trade_id': trade.id, - 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, + 'type': RPCMessageType.BUY_CANCEL, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, 'limit': trade.open_rate, @@ -678,7 +678,7 @@ class FreqtradeBot(LoggingMixin): def _notify_buy_fill(self, trade: Trade) -> None: msg = { 'trade_id': trade.id, - 'type': RPCMessageType.BUY_FILL_NOTIFICATION, + 'type': RPCMessageType.BUY_FILL, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, 'open_rate': trade.open_rate, @@ -1239,8 +1239,8 @@ class FreqtradeBot(LoggingMixin): gain = "profit" if profit_ratio > 0 else "loss" msg = { - 'type': (RPCMessageType.SELL_FILL_NOTIFICATION if fill - else RPCMessageType.SELL_NOTIFICATION), + 'type': (RPCMessageType.SELL_FILL if fill + else RPCMessageType.SELL), 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, @@ -1284,7 +1284,7 @@ class FreqtradeBot(LoggingMixin): gain = "profit" if profit_ratio > 0 else "loss" msg = { - 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'type': RPCMessageType.SELL_CANCEL, 'trade_id': trade.id, 'exchange': trade.exchange.capitalize(), 'pair': trade.pair, diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index bf0b88f6c..e5c0dffba 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -31,15 +31,15 @@ logger = logging.getLogger(__name__) class RPCMessageType(Enum): - STATUS_NOTIFICATION = 'status' - WARNING_NOTIFICATION = 'warning' - STARTUP_NOTIFICATION = 'startup' - BUY_NOTIFICATION = 'buy' - BUY_FILL_NOTIFICATION = 'buy_fill' - BUY_CANCEL_NOTIFICATION = 'buy_cancel' - SELL_NOTIFICATION = 'sell' - SELL_FILL_NOTIFICATION = 'sell_fill' - SELL_CANCEL_NOTIFICATION = 'sell_cancel' + STATUS = 'status' + WARNING = 'warning' + STARTUP = 'startup' + BUY = 'buy' + BUY_FILL = 'buy_fill' + BUY_CANCEL = 'buy_cancel' + SELL = 'sell' + SELL_FILL = 'sell_fill' + SELL_CANCEL = 'sell_cancel' def __repr__(self): return self.value diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 7977d68de..f819b55b4 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -67,7 +67,7 @@ class RPCManager: def startup_messages(self, config: Dict[str, Any], pairlist, protections) -> None: if config['dry_run']: self.send_msg({ - 'type': RPCMessageType.WARNING_NOTIFICATION, + 'type': RPCMessageType.WARNING, 'status': 'Dry run is enabled. All trades are simulated.' }) stake_currency = config['stake_currency'] @@ -79,7 +79,7 @@ class RPCManager: exchange_name = config['exchange']['name'] strategy_name = config.get('strategy', '') self.send_msg({ - 'type': RPCMessageType.STARTUP_NOTIFICATION, + 'type': RPCMessageType.STARTUP, 'status': f'*Exchange:* `{exchange_name}`\n' f'*Stake per trade:* `{stake_amount} {stake_currency}`\n' f'*Minimum ROI:* `{minimal_roi}`\n' @@ -88,13 +88,13 @@ class RPCManager: f'*Strategy:* `{strategy_name}`' }) self.send_msg({ - 'type': RPCMessageType.STARTUP_NOTIFICATION, + 'type': RPCMessageType.STARTUP, 'status': f'Searching for {stake_currency} pairs to buy and sell ' f'based on {pairlist.short_desc()}' }) if len(protections.name_list) > 0: prots = '\n'.join([p for prot in protections.short_desc() for k, p in prot.items()]) self.send_msg({ - 'type': RPCMessageType.STARTUP_NOTIFICATION, + 'type': RPCMessageType.STARTUP, 'status': f'Using Protections: \n{prots}' }) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 4dceeb46c..778baea3c 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -176,6 +176,53 @@ class Telegram(RPCHandler): """ self._updater.stop() + def _format_buy_msg(self, msg: Dict[str, Any]) -> str: + if self._rpc._fiat_converter: + msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount( + msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) + else: + msg['stake_amount_fiat'] = 0 + + message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}" + f" (#{msg['trade_id']})\n" + f"*Amount:* `{msg['amount']:.8f}`\n" + f"*Open Rate:* `{msg['limit']:.8f}`\n" + f"*Current Rate:* `{msg['current_rate']:.8f}`\n" + f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}") + + if msg.get('fiat_currency', None): + message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" + message += ")`" + return message + + def _format_sell_msg(self, msg: Dict[str, Any]) -> str: + msg['amount'] = round(msg['amount'], 8) + msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2) + msg['duration'] = msg['close_date'].replace( + microsecond=0) - msg['open_date'].replace(microsecond=0) + msg['duration_min'] = msg['duration'].total_seconds() / 60 + + msg['emoji'] = self._get_sell_emoji(msg) + + message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" + "*Amount:* `{amount:.8f}`\n" + "*Open Rate:* `{open_rate:.8f}`\n" + "*Current Rate:* `{current_rate:.8f}`\n" + "*Close Rate:* `{limit:.8f}`\n" + "*Sell Reason:* `{sell_reason}`\n" + "*Duration:* `{duration} ({duration_min:.1f} min)`\n" + "*Profit:* `{profit_percent:.2f}%`").format(**msg) + + # Check if all sell properties are available. + # This might not be the case if the message origin is triggered by /forcesell + if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency']) + and self._rpc._fiat_converter): + msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount( + msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) + message += (' `({gain}: {profit_amount:.8f} {stake_currency}' + ' / {profit_fiat:.3f} {fiat_currency})`').format(**msg) + return message + def send_msg(self, msg: Dict[str, Any]) -> None: """ Send a message to telegram channel """ @@ -186,75 +233,31 @@ class Telegram(RPCHandler): # Notification disabled return - if msg['type'] == RPCMessageType.BUY_NOTIFICATION: - if self._rpc._fiat_converter: - msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount( - msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) - else: - msg['stake_amount_fiat'] = 0 + if msg['type'] == RPCMessageType.BUY: + message = self._format_buy_msg(msg) - message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}" - f" (#{msg['trade_id']})\n" - f"*Amount:* `{msg['amount']:.8f}`\n" - f"*Open Rate:* `{msg['limit']:.8f}`\n" - f"*Current Rate:* `{msg['current_rate']:.8f}`\n" - f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}") - - if msg.get('fiat_currency', None): - message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" - message += ")`" - - elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: + elif msg['type'] in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL): + msg['message_side'] = 'buy' if msg['type'] == RPCMessageType.BUY_CANCEL else 'sell' message = ("\N{WARNING SIGN} *{exchange}:* " - "Cancelling open buy Order for {pair} (#{trade_id}). " + "Cancelling open {message_side} Order for {pair} (#{trade_id}). " "Reason: {reason}.".format(**msg)) - elif msg['type'] == RPCMessageType.BUY_FILL_NOTIFICATION: + elif msg['type'] == (RPCMessageType.BUY_FILL, RPCMessageType.SELL_FILL): + msg['message_side'] = 'Buy' if msg['type'] == RPCMessageType.BUY_FILL else 'Sell' + message = ("\N{LARGE CIRCLE} *{exchange}:* " "Buy order for {pair} (#{trade_id}) filled for {open_rate}.".format(**msg)) - elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: - msg['amount'] = round(msg['amount'], 8) - msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2) - msg['duration'] = msg['close_date'].replace( - microsecond=0) - msg['open_date'].replace(microsecond=0) - msg['duration_min'] = msg['duration'].total_seconds() / 60 + elif msg['type'] == RPCMessageType.SELL: + message = self._format_sell_msg(msg) - msg['emoji'] = self._get_sell_emoji(msg) - - message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n" - "*Amount:* `{amount:.8f}`\n" - "*Open Rate:* `{open_rate:.8f}`\n" - "*Current Rate:* `{current_rate:.8f}`\n" - "*Close Rate:* `{limit:.8f}`\n" - "*Sell Reason:* `{sell_reason}`\n" - "*Duration:* `{duration} ({duration_min:.1f} min)`\n" - "*Profit:* `{profit_percent:.2f}%`").format(**msg) - - # Check if all sell properties are available. - # This might not be the case if the message origin is triggered by /forcesell - if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency']) - and self._rpc._fiat_converter): - msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount( - msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) - message += (' `({gain}: {profit_amount:.8f} {stake_currency}' - ' / {profit_fiat:.3f} {fiat_currency})`').format(**msg) - - elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: - message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order " - "for {pair} (#{trade_id}). Reason: {reason}").format(**msg) - - elif msg['type'] == RPCMessageType.SELL_FILL_NOTIFICATION: - message = ("\N{LARGE CIRCLE} *{exchange}:* " - "Sell order for {pair} (#{trade_id}) filled at {close_rate}.".format(**msg)) - - elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: + elif msg['type'] == RPCMessageType.STATUS: message = '*Status:* `{status}`'.format(**msg) - elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION: + elif msg['type'] == RPCMessageType.WARNING: message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg) - elif msg['type'] == RPCMessageType.STARTUP_NOTIFICATION: + elif msg['type'] == RPCMessageType.STARTUP: message = '{status}'.format(**msg) else: diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py index c7e012af5..24e1348f1 100644 --- a/freqtrade/rpc/webhook.py +++ b/freqtrade/rpc/webhook.py @@ -45,21 +45,21 @@ class Webhook(RPCHandler): """ Send a message to telegram channel """ try: - if msg['type'] == RPCMessageType.BUY_NOTIFICATION: + if msg['type'] == RPCMessageType.BUY: valuedict = self._config['webhook'].get('webhookbuy', None) - elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION: + elif msg['type'] == RPCMessageType.BUY_CANCEL: valuedict = self._config['webhook'].get('webhookbuycancel', None) - elif msg['type'] == RPCMessageType.BUY_FILL_NOTIFICATION: + elif msg['type'] == RPCMessageType.BUY_FILL: valuedict = self._config['webhook'].get('webhookbuyfill', None) - elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: + elif msg['type'] == RPCMessageType.SELL: valuedict = self._config['webhook'].get('webhooksell', None) - elif msg['type'] == RPCMessageType.SELL_FILL_NOTIFICATION: + elif msg['type'] == RPCMessageType.SELL_FILL: valuedict = self._config['webhook'].get('webhooksellfill', None) - elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION: + elif msg['type'] == RPCMessageType.SELL_CANCEL: valuedict = self._config['webhook'].get('webhooksellcancel', None) - elif msg['type'] in (RPCMessageType.STATUS_NOTIFICATION, - RPCMessageType.STARTUP_NOTIFICATION, - RPCMessageType.WARNING_NOTIFICATION): + elif msg['type'] in (RPCMessageType.STATUS, + RPCMessageType.STARTUP, + RPCMessageType.WARNING): valuedict = self._config['webhook'].get('webhookstatus', None) else: raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py index 6996c932b..69a757fcf 100644 --- a/tests/rpc/test_rpc_manager.py +++ b/tests/rpc/test_rpc_manager.py @@ -71,7 +71,7 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None: freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) rpc_manager.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, + 'type': RPCMessageType.STATUS, 'status': 'test' }) @@ -86,7 +86,7 @@ def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None: freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) rpc_manager.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, + 'type': RPCMessageType.STATUS, 'status': 'test' }) @@ -124,7 +124,7 @@ def test_send_msg_webhook_CustomMessagetype(mocker, default_conf, caplog) -> Non rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules] - rpc_manager.send_msg({'type': RPCMessageType.STARTUP_NOTIFICATION, + rpc_manager.send_msg({'type': RPCMessageType.STARTUP, 'status': 'TestMessage'}) assert log_has( "Message type 'startup' not implemented by handler webhook.", diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index a3c823aac..accb94d34 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -686,7 +686,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, assert msg_mock.call_count == 4 last_msg = msg_mock.call_args_list[-1][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -748,7 +748,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, last_msg = msg_mock.call_args_list[-1][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -800,7 +800,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None assert msg_mock.call_count == 12 msg = msg_mock.call_args_list[2][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -1198,7 +1198,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None: def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: msg = { - 'type': RPCMessageType.BUY_NOTIFICATION, + 'type': RPCMessageType.BUY, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -1243,7 +1243,7 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, + 'type': RPCMessageType.BUY_CANCEL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -1261,7 +1261,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: old_convamount = telegram._rpc._fiat_converter.convert_amount telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram.send_msg({ - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', @@ -1291,7 +1291,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: msg_mock.reset_mock() telegram.send_msg({ - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', @@ -1328,7 +1328,7 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: old_convamount = telegram._rpc._fiat_converter.convert_amount telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812 telegram.send_msg({ - 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'type': RPCMessageType.SELL_CANCEL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', @@ -1340,7 +1340,7 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: msg_mock.reset_mock() telegram.send_msg({ - 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'type': RPCMessageType.SELL_CANCEL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', @@ -1357,7 +1357,7 @@ def test_send_msg_status_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.STATUS_NOTIFICATION, + 'type': RPCMessageType.STATUS, 'status': 'running' }) assert msg_mock.call_args[0][0] == '*Status:* `running`' @@ -1366,7 +1366,7 @@ def test_send_msg_status_notification(default_conf, mocker) -> None: def test_warning_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.WARNING_NOTIFICATION, + 'type': RPCMessageType.WARNING, 'status': 'message' }) assert msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Warning:* `message`' @@ -1375,7 +1375,7 @@ def test_warning_notification(default_conf, mocker) -> None: def test_startup_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.STARTUP_NOTIFICATION, + 'type': RPCMessageType.STARTUP, 'status': '*Custom:* `Hello World`' }) assert msg_mock.call_args[0][0] == '*Custom:* `Hello World`' @@ -1394,7 +1394,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.BUY_NOTIFICATION, + 'type': RPCMessageType.BUY, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -1420,7 +1420,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram.send_msg({ - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'KEY/ETH', diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py index 38d2fe539..0560f8d53 100644 --- a/tests/rpc/test_rpc_webhook.py +++ b/tests/rpc/test_rpc_webhook.py @@ -68,7 +68,7 @@ def test_send_msg_webhook(default_conf, mocker): msg_mock = MagicMock() mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) msg = { - 'type': RPCMessageType.BUY_NOTIFICATION, + 'type': RPCMessageType.BUY, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, @@ -89,7 +89,7 @@ def test_send_msg_webhook(default_conf, mocker): msg_mock.reset_mock() msg = { - 'type': RPCMessageType.BUY_CANCEL_NOTIFICATION, + 'type': RPCMessageType.BUY_CANCEL, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, @@ -110,8 +110,8 @@ def test_send_msg_webhook(default_conf, mocker): msg_mock.reset_mock() msg = { - 'type': RPCMessageType.BUY_FILL_NOTIFICATION, - 'exchange': 'Bittrex', + 'type': RPCMessageType.BUY_FILL, + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'open_rate': 0.005, 'stake_amount': 0.8, @@ -130,7 +130,7 @@ def test_send_msg_webhook(default_conf, mocker): # Test sell msg_mock.reset_mock() msg = { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': "profit", @@ -155,7 +155,7 @@ def test_send_msg_webhook(default_conf, mocker): # Test sell cancel msg_mock.reset_mock() msg = { - 'type': RPCMessageType.SELL_CANCEL_NOTIFICATION, + 'type': RPCMessageType.SELL_CANCEL, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': "profit", @@ -180,8 +180,8 @@ def test_send_msg_webhook(default_conf, mocker): # Test Sell fill msg_mock.reset_mock() msg = { - 'type': RPCMessageType.SELL_FILL_NOTIFICATION, - 'exchange': 'Bittrex', + 'type': RPCMessageType.SELL_FILL, + 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': "profit", 'close_rate': 0.005, @@ -203,9 +203,9 @@ def test_send_msg_webhook(default_conf, mocker): assert (msg_mock.call_args[0][0]["value3"] == default_conf["webhook"]["webhooksellfill"]["value3"].format(**msg)) - for msgtype in [RPCMessageType.STATUS_NOTIFICATION, - RPCMessageType.WARNING_NOTIFICATION, - RPCMessageType.STARTUP_NOTIFICATION]: + for msgtype in [RPCMessageType.STATUS, + RPCMessageType.WARNING, + RPCMessageType.STARTUP]: # Test notification msg = { 'type': msgtype, @@ -228,8 +228,8 @@ def test_exception_send_msg(default_conf, mocker, caplog): del default_conf["webhook"]["webhookbuy"] webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) - webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION}) - assert log_has(f"Message type '{RPCMessageType.BUY_NOTIFICATION}' not configured for webhooks", + webhook.send_msg({'type': RPCMessageType.BUY}) + assert log_has(f"Message type '{RPCMessageType.BUY}' not configured for webhooks", caplog) default_conf["webhook"] = get_webhook_dict() @@ -238,7 +238,7 @@ def test_exception_send_msg(default_conf, mocker, caplog): mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf) msg = { - 'type': RPCMessageType.BUY_NOTIFICATION, + 'type': RPCMessageType.BUY, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 0.005, diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 39c3f0561..25239d503 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2624,7 +2624,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N last_msg = rpc_mock.call_args_list[-1][0][0] assert { 'trade_id': 1, - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'exchange': 'Binance', 'pair': 'ETH/BTC', 'gain': 'profit', @@ -2674,7 +2674,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) assert rpc_mock.call_count == 2 last_msg = rpc_mock.call_args_list[-1][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -2732,7 +2732,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe last_msg = rpc_mock.call_args_list[-1][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -2902,9 +2902,9 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f assert trade.is_open is False assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value assert rpc_mock.call_count == 3 - assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.BUY_NOTIFICATION - assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.BUY_FILL_NOTIFICATION - assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL_NOTIFICATION + assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.BUY + assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.BUY_FILL + assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL def test_execute_sell_market_order(default_conf, ticker, fee, @@ -2941,7 +2941,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee, assert rpc_mock.call_count == 3 last_msg = rpc_mock.call_args_list[-1][0][0] assert { - 'type': RPCMessageType.SELL_NOTIFICATION, + 'type': RPCMessageType.SELL, 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', From d740aae8ca96a221a744b69fb16f708b8a632623 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 06:49:29 +0200 Subject: [PATCH 52/73] Default fill notifications to off --- freqtrade/constants.py | 14 ++++++++++++-- freqtrade/freqtradebot.py | 4 ++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 7b955c37d..aea6e1ff2 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -246,14 +246,24 @@ CONF_SCHEMA = { 'balance_dust_level': {'type': 'number', 'minimum': 0.0}, 'notification_settings': { 'type': 'object', + 'default': {}, 'properties': { 'status': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'warning': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'startup': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'buy': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, - 'sell': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, 'buy_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, - 'sell_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS} + 'buy_fill': {'type': 'string', + 'enum': TELEGRAM_SETTING_OPTIONS, + 'default': 'off' + }, + 'sell': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, + 'sell_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}, + 'sell_fill': { + 'type': 'string', + 'enum': TELEGRAM_SETTING_OPTIONS, + 'default': 'off' + }, } } }, diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index c48ea851e..ad55b38f8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1361,12 +1361,12 @@ class FreqtradeBot(LoggingMixin): # Updating wallets when order is closed if not trade.is_open: - if not stoploss_order: + if not stoploss_order and not trade.open_order_id: self._notify_sell(trade, '', True) self.protections.stop_per_pair(trade.pair) self.protections.global_stop() self.wallets.update() - elif trade.open_order_id is None: + elif not trade.open_order_id: # Buy fill self._notify_buy_fill(trade) From efbe0843be6a05d623a77c8e91adfce2158ca9f0 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 07:57:34 +0200 Subject: [PATCH 53/73] Add documentation for fill messages --- docs/telegram-usage.md | 9 ++++++++- tests/rpc/test_rpc_telegram.py | 8 ++++---- tests/test_freqtradebot.py | 2 +- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md index 377977892..824cb17c7 100644 --- a/docs/telegram-usage.md +++ b/docs/telegram-usage.md @@ -82,12 +82,19 @@ Example configuration showing the different settings: "buy": "silent", "sell": "on", "buy_cancel": "silent", - "sell_cancel": "on" + "sell_cancel": "on", + "buy_fill": "off", + "sell_fill": "off" }, "balance_dust_level": 0.01 }, ``` +`buy` notifications are sent when the order is placed, while `buy_fill` notifications are sent when the order is filled on the exchange. +`sell` notifications are sent when the order is placed, while `sell_fill` notifications are sent when the order is filled on the exchange. +`*_fill` notifications are off by default and must be explicitly enabled. + + `balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown. ## Create a custom keyboard (command shortcut buttons) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index accb94d34..718c1d3a0 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1335,8 +1335,8 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: 'reason': 'Cancelled on exchange' }) assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH (#1).' - ' Reason: Cancelled on exchange') + == ('\N{WARNING SIGN} *Binance:* Cancelling open sell Order for KEY/ETH (#1).' + ' Reason: Cancelled on exchange.') msg_mock.reset_mock() telegram.send_msg({ @@ -1347,8 +1347,8 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: 'reason': 'timeout' }) assert msg_mock.call_args[0][0] \ - == ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH (#1).' - ' Reason: timeout') + == ('\N{WARNING SIGN} *Binance:* Cancelling open sell Order for KEY/ETH (#1).' + ' Reason: timeout.') # Reset singleton function to avoid random breaks telegram._rpc._fiat_converter.convert_amount = old_convamount diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 25239d503..44791f928 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -2263,7 +2263,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, # check it does cancel sell orders over the time limit freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 - assert rpc_mock.call_count == 2 + assert rpc_mock.call_count == 1 assert open_trade.is_open is True # Custom user sell-timeout is never called assert freqtrade.strategy.check_sell_timeout.call_count == 0 From f821ef5aec8fb8321b75bfbe9ab33e0eed380ef5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 19:36:30 +0200 Subject: [PATCH 54/73] Final finetunings of rpc_fill messages --- freqtrade/rpc/telegram.py | 2 +- tests/conftest.py | 3 ++- tests/rpc/test_rpc_telegram.py | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 778baea3c..ffe7a7ceb 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -242,7 +242,7 @@ class Telegram(RPCHandler): "Cancelling open {message_side} Order for {pair} (#{trade_id}). " "Reason: {reason}.".format(**msg)) - elif msg['type'] == (RPCMessageType.BUY_FILL, RPCMessageType.SELL_FILL): + elif msg['type'] in (RPCMessageType.BUY_FILL, RPCMessageType.SELL_FILL): msg['message_side'] = 'Buy' if msg['type'] == RPCMessageType.BUY_FILL else 'Sell' message = ("\N{LARGE CIRCLE} *{exchange}:* " diff --git a/tests/conftest.py b/tests/conftest.py index cc4fe91f0..788586134 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -314,7 +314,8 @@ def get_default_conf(testdatadir): "telegram": { "enabled": True, "token": "token", - "chat_id": "0" + "chat_id": "0", + "notification_settings": {}, }, "datadir": str(testdatadir), "initial_state": "running", diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 718c1d3a0..d72ba36ad 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1254,6 +1254,25 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: 'Reason: cancelled due to timeout.') +def test_send_msg_buy_fill_notification(default_conf, mocker) -> None: + + default_conf['telegram']['notification_settings']['buy_fill'] = 'on' + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) + + telegram.send_msg({ + 'type': RPCMessageType.BUY_FILL, + 'trade_id': 1, + 'exchange': 'Binance', + 'pair': 'ETH/USDT', + 'open_rate': 200, + 'stake_amount': 100, + 'amount': 0.5, + 'open_date': arrow.utcnow().datetime + }) + assert (msg_mock.call_args[0][0] == '\N{LARGE CIRCLE} *Binance:* ' + 'Buy order for ETH/USDT (#1) filled for 200.') + + def test_send_msg_sell_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) From fd110c7d625fc8b5bc12445fe94998eb1f10eb31 Mon Sep 17 00:00:00 2001 From: Jose Hidalgo Date: Tue, 20 Apr 2021 11:50:53 -0600 Subject: [PATCH 55/73] The error that it prints says the contrary to what was evaluated. ex. Trading stopped due to Max Drawdown 0.79 < 0.2 within 48 candles --- freqtrade/plugins/protections/max_drawdown_protection.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py index d1c6b192d..67e204039 100644 --- a/freqtrade/plugins/protections/max_drawdown_protection.py +++ b/freqtrade/plugins/protections/max_drawdown_protection.py @@ -61,7 +61,7 @@ class MaxDrawdown(IProtection): if drawdown > self._max_allowed_drawdown: self.log_once( - f"Trading stopped due to Max Drawdown {drawdown:.2f} < {self._max_allowed_drawdown}" + f"Trading stopped due to Max Drawdown {drawdown:.2f} > {self._max_allowed_drawdown}" f" within {self.lookback_period_str}.", logger.info) until = self.calculate_lock_end(trades, self._stop_duration) From 5defd9a7f882d75a9bf2c457cdc3ddd94a8bb7e4 Mon Sep 17 00:00:00 2001 From: Bernd Zeimetz Date: Tue, 20 Apr 2021 19:52:57 +0200 Subject: [PATCH 56/73] setup.sh: Install libpython3-dev on Debian/Ubuntu Python.h is required to build c modules for Python. --- setup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.sh b/setup.sh index d0ca1f643..631c31df2 100755 --- a/setup.sh +++ b/setup.sh @@ -138,7 +138,7 @@ function install_macos() { # Install bot Debian_ubuntu function install_debian() { sudo apt-get update - sudo apt-get install -y build-essential autoconf libtool pkg-config make wget git + sudo apt-get install -y build-essential autoconf libtool pkg-config make wget git libpython3-dev install_talib } From cfa9315e2a3dd1706bca0f09a4e1e48315912ec3 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 20:29:53 +0200 Subject: [PATCH 57/73] Prevent out of candle ROI sells --- freqtrade/optimize/backtesting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ff1dd934c..a1d4a2578 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -239,7 +239,7 @@ class Backtesting: # Use the maximum between close_rate and low as we # cannot sell outside of a candle. # Applies when a new ROI setting comes in place and the whole candle is above that. - return max(close_rate, sell_row[LOW_IDX]) + return min(max(close_rate, sell_row[LOW_IDX]), sell_row[HIGH_IDX]) else: # This should not be reached... From bd92ce938c6b92c2092cd119b597fc29d26ec2ab Mon Sep 17 00:00:00 2001 From: Matthias Date: Sun, 18 Apr 2021 16:05:28 +0200 Subject: [PATCH 58/73] trade_history should paginate through results this avoids huge results --- freqtrade/rpc/api_server/api_schemas.py | 1 + freqtrade/rpc/api_server/api_v1.py | 4 ++-- freqtrade/rpc/rpc.py | 10 ++++++---- scripts/rest_client.py | 14 ++++++++++---- tests/rpc/test_rpc_apiserver.py | 2 +- 5 files changed, 20 insertions(+), 11 deletions(-) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 12bee1cf2..e582f6aa8 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -199,6 +199,7 @@ class OpenTradeSchema(TradeSchema): class TradeResponse(BaseModel): trades: List[TradeSchema] trades_count: int + total_trades: int class ForceBuyResponse(BaseModel): diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index ebfafc290..cb3a5a710 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -85,8 +85,8 @@ def status(rpc: RPC = Depends(get_rpc)): # Using the responsemodel here will cause a ~100% increase in response time (from 1s to 2s) # on big databases. Correct response model: response_model=TradeResponse, @router.get('/trades', tags=['info', 'trading']) -def trades(limit: int = 0, rpc: RPC = Depends(get_rpc)): - return rpc._rpc_trade_history(limit) +def trades(limit: int = 500, offset: int = 0, rpc: RPC = Depends(get_rpc)): + return rpc._rpc_trade_history(min(limit, 500), offset=offset, order_by_id=True) @router.get('/trade/{tradeid}', response_model=OpenTradeSchema, tags=['info', 'trading']) diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e5c0dffba..a7a4dcf5c 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -300,11 +300,12 @@ class RPC: 'data': data } - def _rpc_trade_history(self, limit: int) -> Dict: + def _rpc_trade_history(self, limit: int, offset: int = 0, order_by_id: bool = False) -> Dict: """ Returns the X last trades """ - if limit > 0: + order_by = Trade.id if order_by_id else Trade.close_date.desc() + if limit: trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by( - Trade.close_date.desc()).limit(limit) + order_by).limit(limit).offset(offset) else: trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by( Trade.close_date.desc()).all() @@ -313,7 +314,8 @@ class RPC: return { "trades": output, - "trades_count": len(output) + "trades_count": len(output), + "total_trades": Trade.get_trades([Trade.is_open.is_(False)]).count(), } def _rpc_stats(self) -> Dict[str, Any]: diff --git a/scripts/rest_client.py b/scripts/rest_client.py index 40b338ce8..900b784f2 100755 --- a/scripts/rest_client.py +++ b/scripts/rest_client.py @@ -200,13 +200,19 @@ class FtRestClient(): """ return self._get("logs", params={"limit": limit} if limit else 0) - def trades(self, limit=None): - """Return trades history. + def trades(self, limit=None, offset=None): + """Return trades history, sorted by id - :param limit: Limits trades to the X last trades. No limit to get all the trades. + :param limit: Limits trades to the X last trades. Max 500 trades. + :param offset: Offset by this amount of trades. :return: json object """ - return self._get("trades", params={"limit": limit} if limit else 0) + params = {} + if limit: + params['limit'] = limit + if offset: + params['offset'] = offset + return self._get("trades", params) def trade(self, trade_id): """Return specific trade diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 6505629eb..d87d4e59d 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -506,7 +506,7 @@ def test_api_trades(botclient, mocker, fee, markets): ) rc = client_get(client, f"{BASE_URI}/trades") assert_response(rc) - assert len(rc.json()) == 2 + assert len(rc.json()) == 3 assert rc.json()['trades_count'] == 0 create_mock_trades(fee) From 759bbd8e72fb72507669b1c67671eb3c67a95c64 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 21:23:37 +0200 Subject: [PATCH 59/73] Update documentation about pagination --- docs/rest-api.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 5c25e9eeb..514e0f719 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -124,7 +124,7 @@ python3 scripts/rest_client.py --config rest_config.json [optional par | `stop` | Stops the trader. | `stopbuy` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `reload_config` | Reloads the configuration file. -| `trades` | List last trades. +| `trades` | List last trades. Limited to 500 trades per call. | `trade/` | Get specific trade. | `delete_trade ` | Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange. | `show_config` | Shows part of the current configuration with relevant settings to operation. @@ -280,9 +280,10 @@ trade :param trade_id: Specify which trade to get. trades - Return trades history. + Return trades history, sorted by id - :param limit: Limits trades to the X last trades. No limit to get all the trades. + :param limit: Limits trades to the X last trades. Max 500 trades. + :param offset: Offset by this amount of trades. version Return the version of the bot. @@ -290,6 +291,7 @@ version whitelist Show the current whitelist. + ``` ### OpenAPI interface From 05ce3acc466dda62f7f65bb80d172d972d5923a5 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 21:28:13 +0200 Subject: [PATCH 60/73] Improve tests for api_trades --- docs/rest-api.md | 2 -- tests/rpc/test_rpc_apiserver.py | 3 +++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/rest-api.md b/docs/rest-api.md index 514e0f719..a0029a44c 100644 --- a/docs/rest-api.md +++ b/docs/rest-api.md @@ -290,8 +290,6 @@ version whitelist Show the current whitelist. - - ``` ### OpenAPI interface diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index d87d4e59d..69d312e65 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -508,6 +508,7 @@ def test_api_trades(botclient, mocker, fee, markets): assert_response(rc) assert len(rc.json()) == 3 assert rc.json()['trades_count'] == 0 + assert rc.json()['total_trades'] == 0 create_mock_trades(fee) Trade.query.session.flush() @@ -516,10 +517,12 @@ def test_api_trades(botclient, mocker, fee, markets): assert_response(rc) assert len(rc.json()['trades']) == 2 assert rc.json()['trades_count'] == 2 + assert rc.json()['total_trades'] == 2 rc = client_get(client, f"{BASE_URI}/trades?limit=1") assert_response(rc) assert len(rc.json()['trades']) == 1 assert rc.json()['trades_count'] == 1 + assert rc.json()['total_trades'] == 2 def test_api_trade_single(botclient, mocker, fee, ticker, markets): From 9f6f3e0862b27693706ecbb44be0856834d05717 Mon Sep 17 00:00:00 2001 From: Matthias Date: Tue, 20 Apr 2021 21:41:18 +0200 Subject: [PATCH 61/73] Address ZeroDivisionExceptiond closes #4764 closes #4617 --- freqtrade/persistence/models.py | 2 ++ tests/test_persistence.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 49d3e2d62..e7fd488c7 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -547,6 +547,8 @@ class LocalTrade(): rate=(rate or self.close_rate), fee=(fee or self.fee_close) ) + if self.open_trade_value == 0.0: + return 0.0 profit_ratio = (close_trade_value / self.open_trade_value) - 1 return float(f"{profit_ratio:.8f}") diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 3b90f368f..dad0e275e 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -388,6 +388,9 @@ def test_calc_profit_ratio(limit_buy_order, limit_sell_order, fee): # Test with a custom fee rate on the close trade assert trade.calc_profit_ratio(fee=0.003) == 0.06147824 + trade.open_trade_value = 0.0 + assert trade.calc_profit_ratio(fee=0.003) == 0.0 + @pytest.mark.usefixtures("init_persistence") def test_clean_dry_run_db(default_conf, fee): From 896ec58cadd3d94eb8d61077e3ec07be1c895b48 Mon Sep 17 00:00:00 2001 From: Jose Hidalgo Date: Wed, 21 Apr 2021 15:02:33 -0600 Subject: [PATCH 62/73] Add the reason why there is a global pairlock when lock is available --- freqtrade/freqtradebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index ad55b38f8..a64d3f4cc 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -378,7 +378,7 @@ class FreqtradeBot(LoggingMixin): if lock: self.log_once(f"Global pairlock active until " f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)}. " - "Not creating new trades.", logger.info) + f"Not creating new trades, reason: {lock.reason}.", logger.info) else: self.log_once("Global pairlock active. Not creating new trades.", logger.info) return trades_created From 515c73f3990ee976baea1c8dd959a090d64a2c86 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Apr 2021 06:51:26 +0200 Subject: [PATCH 63/73] Don't hard-limit trades endpoint for now --- freqtrade/rpc/api_server/api_v1.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py index cb3a5a710..e907b92f0 100644 --- a/freqtrade/rpc/api_server/api_v1.py +++ b/freqtrade/rpc/api_server/api_v1.py @@ -86,7 +86,7 @@ def status(rpc: RPC = Depends(get_rpc)): # on big databases. Correct response model: response_model=TradeResponse, @router.get('/trades', tags=['info', 'trading']) def trades(limit: int = 500, offset: int = 0, rpc: RPC = Depends(get_rpc)): - return rpc._rpc_trade_history(min(limit, 500), offset=offset, order_by_id=True) + return rpc._rpc_trade_history(limit, offset=offset, order_by_id=True) @router.get('/trade/{tradeid}', response_model=OpenTradeSchema, tags=['info', 'trading']) From 09efa7b06b7cd39726d07c9f1f231249651271c8 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Thu, 22 Apr 2021 10:07:13 +0300 Subject: [PATCH 64/73] Add --new-pairs-days parameter for download-data command. This parameter allows us to customize a number of days we would like to download for new pairs only. This allows us to achieve efficient data update, downloading all data for new pairs and only missing data for existing pairs. To do that use `freqtrade download-data --new-pairs-days=3650` (not specifying `--days` or `--timerange` causes freqtrade to download only missing data for existing pairs). --- docs/data-download.md | 6 ++++-- freqtrade/commands/arguments.py | 5 +++-- freqtrade/commands/cli_options.py | 7 +++++++ freqtrade/commands/data_commands.py | 9 +++++---- freqtrade/configuration/configuration.py | 7 +++++++ freqtrade/data/history/history_utils.py | 15 ++++++++++----- 6 files changed, 36 insertions(+), 13 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index 7a78334d5..7b09cf49c 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -11,8 +11,9 @@ Otherwise `--exchange` becomes mandatory. You can use a relative timerange (`--days 20`) or an absolute starting point (`--timerange 20200101-`). For incremental downloads, the relative approach should be used. !!! Tip "Tip: Updating existing data" - If you already have backtesting data available in your data-directory and would like to refresh this data up to today, use `--days xx` with a number slightly higher than the missing number of days. Freqtrade will keep the available data and only download the missing data. - Be careful though: If the number is too small (which would result in a few missing days), the whole dataset will be removed and only xx days will be downloaded. + If you already have backtesting data available in your data-directory and would like to refresh this data up to today, do not use `--days` or `--timerange` parameters. Freqtrade will keep the available data and only download the missing data. + If you are updating existing data after inserting new pairs that you have no data for, use `--new-pairs-days xx` parameter. Specified number of days will be downloaded for new pairs while old pairs will be updated with missing data only. + If you use `--days xx` parameter alone - data for specified number of days will be downloaded for _all_ pairs. Be careful, if specified number of days is smaller than gap between now and last downloaded candle - freqtrade will delete all existing data to avoid gaps in candle data. ### Usage @@ -34,6 +35,7 @@ optional arguments: separated. --pairs-file FILE File containing a list of pairs to download. --days INT Download data for given number of days. + --new-pairs-days INT Download data of new pairs for given number of days. Default: `30`. --timerange TIMERANGE Specify what timerange of data to use. --dl-trades Download trades instead of OHLCV data. The bot will diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py index 9cf9992ce..ffd317799 100644 --- a/freqtrade/commands/arguments.py +++ b/freqtrade/commands/arguments.py @@ -60,8 +60,9 @@ ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"] ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"] -ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "timerange", "download_trades", "exchange", - "timeframes", "erase", "dataformat_ohlcv", "dataformat_trades"] +ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "timerange", + "download_trades", "exchange", "timeframes", "erase", "dataformat_ohlcv", + "dataformat_trades"] ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", "db_url", "trade_source", "export", "exportfilename", diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index e49895de4..80c56ecfa 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -345,6 +345,13 @@ AVAILABLE_CLI_OPTIONS = { type=check_int_positive, metavar='INT', ), + "new_pairs_days": Arg( + '--new-pairs-days', + help='Download data of new pairs for given number of days. Default: `%(default)s`.', + type=check_int_positive, + metavar='INT', + default=30, + ), "download_trades": Arg( '--dl-trades', help='Download trades instead of OHLCV data. The bot will resample trades to the ' diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py index 1ce02eee5..58191ddb4 100644 --- a/freqtrade/commands/data_commands.py +++ b/freqtrade/commands/data_commands.py @@ -62,8 +62,8 @@ def start_download_data(args: Dict[str, Any]) -> None: if config.get('download_trades'): pairs_not_available = refresh_backtest_trades_data( exchange, pairs=expanded_pairs, datadir=config['datadir'], - timerange=timerange, erase=bool(config.get('erase')), - data_format=config['dataformat_trades']) + timerange=timerange, new_pairs_days=config['new_pairs_days'], + erase=bool(config.get('erase')), data_format=config['dataformat_trades']) # Convert downloaded trade data to different timeframes convert_trades_to_ohlcv( @@ -75,8 +75,9 @@ def start_download_data(args: Dict[str, Any]) -> None: else: pairs_not_available = refresh_backtest_ohlcv_data( exchange, pairs=expanded_pairs, timeframes=config['timeframes'], - datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')), - data_format=config['dataformat_ohlcv']) + datadir=config['datadir'], timerange=timerange, + new_pairs_days=config['new_pairs_days'], + erase=bool(config.get('erase')), data_format=config['dataformat_ohlcv']) except KeyboardInterrupt: sys.exit("SIGINT received, aborting ...") diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index cc11f97c2..86f337c1b 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -108,6 +108,8 @@ class Configuration: self._process_plot_options(config) + self._process_data_options(config) + # Check if the exchange set by the user is supported check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True)) @@ -399,6 +401,11 @@ class Configuration: self._args_to_config(config, argname='dataformat_trades', logstring='Using "{}" to store trades data.') + def _process_data_options(self, config: Dict[str, Any]) -> None: + + self._args_to_config(config, argname='new_pairs_days', + logstring='Detected --new-pairs-days: {}') + def _process_runmode(self, config: Dict[str, Any]) -> None: self._args_to_config(config, argname='dry_run', diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py index 3b8b5a2f0..58965abe0 100644 --- a/freqtrade/data/history/history_utils.py +++ b/freqtrade/data/history/history_utils.py @@ -155,6 +155,7 @@ def _load_cached_data_for_updating(pair: str, timeframe: str, timerange: Optiona def _download_pair_history(datadir: Path, exchange: Exchange, pair: str, *, + new_pairs_days: int = 30, timeframe: str = '5m', timerange: Optional[TimeRange] = None, data_handler: IDataHandler = None) -> bool: @@ -193,7 +194,7 @@ def _download_pair_history(datadir: Path, timeframe=timeframe, since_ms=since_ms if since_ms else int(arrow.utcnow().shift( - days=-30).float_timestamp) * 1000 + days=-new_pairs_days).float_timestamp) * 1000 ) # TODO: Maybe move parsing to exchange class (?) new_dataframe = ohlcv_to_dataframe(new_data, timeframe, pair, @@ -223,7 +224,8 @@ def _download_pair_history(datadir: Path, def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes: List[str], datadir: Path, timerange: Optional[TimeRange] = None, - erase: bool = False, data_format: str = None) -> List[str]: + new_pairs_days: int = 30, erase: bool = False, + data_format: str = None) -> List[str]: """ Refresh stored ohlcv data for backtesting and hyperopt operations. Used by freqtrade download-data subcommand. @@ -246,12 +248,14 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes logger.info(f'Downloading pair {pair}, interval {timeframe}.') _download_pair_history(datadir=datadir, exchange=exchange, pair=pair, timeframe=str(timeframe), + new_pairs_days=new_pairs_days, timerange=timerange, data_handler=data_handler) return pairs_not_available def _download_trades_history(exchange: Exchange, pair: str, *, + new_pairs_days: int = 30, timerange: Optional[TimeRange] = None, data_handler: IDataHandler ) -> bool: @@ -263,7 +267,7 @@ def _download_trades_history(exchange: Exchange, since = timerange.startts * 1000 if \ (timerange and timerange.starttype == 'date') else int(arrow.utcnow().shift( - days=-30).float_timestamp) * 1000 + days=-new_pairs_days).float_timestamp) * 1000 trades = data_handler.trades_load(pair) @@ -311,8 +315,8 @@ def _download_trades_history(exchange: Exchange, def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: Path, - timerange: TimeRange, erase: bool = False, - data_format: str = 'jsongz') -> List[str]: + timerange: TimeRange, new_pairs_days: int = 30, + erase: bool = False, data_format: str = 'jsongz') -> List[str]: """ Refresh stored trades data for backtesting and hyperopt operations. Used by freqtrade download-data subcommand. @@ -333,6 +337,7 @@ def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: logger.info(f'Downloading trades for pair {pair}.') _download_trades_history(exchange=exchange, pair=pair, + new_pairs_days=new_pairs_days, timerange=timerange, data_handler=data_handler) return pairs_not_available From f744df2374b8a7edb450277f640f815a0eca647f Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Apr 2021 10:01:41 +0200 Subject: [PATCH 65/73] Fix bad fill message --- 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 ffe7a7ceb..cb3dbe6c8 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -246,7 +246,8 @@ class Telegram(RPCHandler): msg['message_side'] = 'Buy' if msg['type'] == RPCMessageType.BUY_FILL else 'Sell' message = ("\N{LARGE CIRCLE} *{exchange}:* " - "Buy order for {pair} (#{trade_id}) filled for {open_rate}.".format(**msg)) + "{message_side} order for {pair} (#{trade_id}) filled " + "for {open_rate}.".format(**msg)) elif msg['type'] == RPCMessageType.SELL: message = self._format_sell_msg(msg) From 3144185409ddeb36529b693a7bcb19df400aac20 Mon Sep 17 00:00:00 2001 From: Rokas Kupstys Date: Thu, 22 Apr 2021 11:18:28 +0300 Subject: [PATCH 66/73] Allow specifying "new_pairs_days" in config. --- freqtrade/commands/cli_options.py | 1 - freqtrade/constants.py | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py index 80c56ecfa..b583b47ba 100644 --- a/freqtrade/commands/cli_options.py +++ b/freqtrade/commands/cli_options.py @@ -350,7 +350,6 @@ AVAILABLE_CLI_OPTIONS = { help='Download data of new pairs for given number of days. Default: `%(default)s`.', type=check_int_positive, metavar='INT', - default=30, ), "download_trades": Arg( '--dl-trades', diff --git a/freqtrade/constants.py b/freqtrade/constants.py index aea6e1ff2..77bd2a029 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -11,6 +11,7 @@ DEFAULT_EXCHANGE = 'bittrex' PROCESS_THROTTLE_SECS = 5 # sec HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec +NEW_PAIRS_DAYS = 30 DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite' UNLIMITED_STAKE_AMOUNT = 'unlimited' @@ -96,6 +97,7 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'max_open_trades': {'type': ['integer', 'number'], 'minimum': -1}, + 'new_pairs_days': {'type': 'integer', 'default': NEW_PAIRS_DAYS}, 'timeframe': {'type': 'string'}, 'stake_currency': {'type': 'string'}, 'stake_amount': { From 7e2e196643cfa5bf6cf5aa76c55fab33139f65f9 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Apr 2021 17:13:22 +0200 Subject: [PATCH 67/73] improve sell_message by using sell rate --- freqtrade/rpc/telegram.py | 11 ++++++----- tests/rpc/test_rpc_telegram.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index cb3dbe6c8..3eeedcd12 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -242,13 +242,14 @@ class Telegram(RPCHandler): "Cancelling open {message_side} Order for {pair} (#{trade_id}). " "Reason: {reason}.".format(**msg)) - elif msg['type'] in (RPCMessageType.BUY_FILL, RPCMessageType.SELL_FILL): - msg['message_side'] = 'Buy' if msg['type'] == RPCMessageType.BUY_FILL else 'Sell' - + elif msg['type'] == RPCMessageType.BUY_FILL: message = ("\N{LARGE CIRCLE} *{exchange}:* " - "{message_side} order for {pair} (#{trade_id}) filled " + "Buy order for {pair} (#{trade_id}) filled " "for {open_rate}.".format(**msg)) - + elif msg['type'] == RPCMessageType.SELL_FILL: + message = ("\N{LARGE CIRCLE} *{exchange}:* " + "Sell order for {pair} (#{trade_id}) filled " + "for {close_rate}.".format(**msg)) elif msg['type'] == RPCMessageType.SELL: message = self._format_sell_msg(msg) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index d72ba36ad..6a36c12a7 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1372,6 +1372,35 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None: telegram._rpc._fiat_converter.convert_amount = old_convamount +def test_send_msg_sell_fill_notification(default_conf, mocker) -> None: + + default_conf['telegram']['notification_settings']['sell_fill'] = 'on' + telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) + + telegram.send_msg({ + 'type': RPCMessageType.SELL_FILL, + 'trade_id': 1, + 'exchange': 'Binance', + 'pair': 'ETH/USDT', + 'gain': 'loss', + 'limit': 3.201e-05, + 'amount': 0.1, + 'order_type': 'market', + 'open_rate': 500, + 'close_rate': 550, + 'current_rate': 3.201e-05, + 'profit_amount': -0.05746268, + 'profit_ratio': -0.57405275, + 'stake_currency': 'ETH', + 'fiat_currency': 'USD', + 'sell_reason': SellType.STOP_LOSS.value, + 'open_date': arrow.utcnow().shift(hours=-1), + 'close_date': arrow.utcnow(), + }) + assert msg_mock.call_args[0][0] \ + == ('\N{LARGE CIRCLE} *Binance:* Sell order for ETH/USDT (#1) filled for 550.') + + def test_send_msg_status_notification(default_conf, mocker) -> None: telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) From 0d2457cd478978dfbad3d2de526be03170176c35 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Apr 2021 19:28:39 +0200 Subject: [PATCH 68/73] Add lock_reason to per-pair lock --- freqtrade/freqtradebot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index a64d3f4cc..c3a4bc0e0 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -456,7 +456,8 @@ class FreqtradeBot(LoggingMixin): lock = PairLocks.get_pair_longest_lock(pair, nowtime) if lock: self.log_once(f"Pair {pair} is still locked until " - f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)}.", + f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)} " + f"due to {lock.reason}.", logger.info) else: self.log_once(f"Pair {pair} is still locked.", logger.info) From ccaf5764da5f9d63c77bb901a0b89541cb3ea661 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Apr 2021 19:41:01 +0200 Subject: [PATCH 69/73] Small adjustments --- docs/data-download.md | 8 +++++--- freqtrade/constants.py | 3 +-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/data-download.md b/docs/data-download.md index 7b09cf49c..01561c89b 100644 --- a/docs/data-download.md +++ b/docs/data-download.md @@ -21,8 +21,9 @@ You can use a relative timerange (`--days 20`) or an absolute starting point (`- usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] [--userdir PATH] [-p PAIRS [PAIRS ...]] [--pairs-file FILE] - [--days INT] [--timerange TIMERANGE] - [--dl-trades] [--exchange EXCHANGE] + [--days INT] [--new-pairs-days INT] + [--timerange TIMERANGE] [--dl-trades] + [--exchange EXCHANGE] [-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]] [--erase] [--data-format-ohlcv {json,jsongz,hdf5}] @@ -35,7 +36,8 @@ optional arguments: separated. --pairs-file FILE File containing a list of pairs to download. --days INT Download data for given number of days. - --new-pairs-days INT Download data of new pairs for given number of days. Default: `30`. + --new-pairs-days INT Download data of new pairs for given number of days. + Default: `None`. --timerange TIMERANGE Specify what timerange of data to use. --dl-trades Download trades instead of OHLCV data. The bot will diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 77bd2a029..bfd1e72f1 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -11,7 +11,6 @@ DEFAULT_EXCHANGE = 'bittrex' PROCESS_THROTTLE_SECS = 5 # sec HYPEROPT_EPOCH = 100 # epochs RETRY_TIMEOUT = 30 # sec -NEW_PAIRS_DAYS = 30 DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite' UNLIMITED_STAKE_AMOUNT = 'unlimited' @@ -97,7 +96,7 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'max_open_trades': {'type': ['integer', 'number'], 'minimum': -1}, - 'new_pairs_days': {'type': 'integer', 'default': NEW_PAIRS_DAYS}, + 'new_pairs_days': {'type': 'integer', 'default': 30}, 'timeframe': {'type': 'string'}, 'stake_currency': {'type': 'string'}, 'stake_amount': { From 406c1267a2cc2f396656be2ea0d96293c8335d88 Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 22 Apr 2021 20:01:08 +0200 Subject: [PATCH 70/73] Remove superfluss space --- freqtrade/data/converter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py index c9d4ef19f..af6c6a2ef 100644 --- a/freqtrade/data/converter.py +++ b/freqtrade/data/converter.py @@ -113,7 +113,7 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str) pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0 if len_before != len_after: message = (f"Missing data fillup for {pair}: before: {len_before} - after: {len_after}" - f" - {round(pct_missing * 100, 2)} %") + f" - {round(pct_missing * 100, 2)}%") if pct_missing > 0.01: logger.info(message) else: From 4005708f85b8b1885143168e5f9cf0323d34ef09 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Apr 2021 06:50:39 +0200 Subject: [PATCH 71/73] Handle edge with volumepairlist and empty pair_whitelist closes #4779 --- freqtrade/edge/edge_positioning.py | 9 +++++++-- tests/edge/test_edge.py | 23 ++++++++++++++++++++--- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py index d1f76c21f..334aabfab 100644 --- a/freqtrade/edge/edge_positioning.py +++ b/freqtrade/edge/edge_positioning.py @@ -81,10 +81,15 @@ class Edge: if config.get('fee'): self.fee = config['fee'] else: - self.fee = self.exchange.get_fee(symbol=expand_pairlist( - self.config['exchange']['pair_whitelist'], list(self.exchange.markets))[0]) + try: + self.fee = self.exchange.get_fee(symbol=expand_pairlist( + self.config['exchange']['pair_whitelist'], list(self.exchange.markets))[0]) + except IndexError: + self.fee = None def calculate(self, pairs: List[str]) -> bool: + if self.fee is None and pairs: + self.fee = self.exchange.get_fee(pairs[0]) heartbeat = self.edge_config.get('process_throttle_secs') diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index 5142dd985..25e0da5e2 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -330,11 +330,11 @@ def test_edge_process_no_data(mocker, edge_conf, caplog): def test_edge_process_no_trades(mocker, edge_conf, caplog): freqtrade = get_patched_freqtradebot(mocker, edge_conf) - mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) - mocker.patch('freqtrade.edge.edge_positioning.refresh_data', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0.001) + mocker.patch('freqtrade.edge.edge_positioning.refresh_data', ) mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data) # Return empty - mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', MagicMock(return_value=[])) + mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', return_value=[]) edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) assert not edge.calculate(edge_conf['exchange']['pair_whitelist']) @@ -342,6 +342,23 @@ def test_edge_process_no_trades(mocker, edge_conf, caplog): assert log_has("No trades found.", caplog) +def test_edge_process_no_pairs(mocker, edge_conf, caplog): + edge_conf['exchange']['pair_whitelist'] = [] + freqtrade = get_patched_freqtradebot(mocker, edge_conf) + fee_mock = mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0.001) + mocker.patch('freqtrade.edge.edge_positioning.refresh_data') + mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data) + # Return empty + mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', return_value=[]) + edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy) + assert fee_mock.call_count == 0 + assert edge.fee is None + + assert not edge.calculate(['XRP/USDT']) + assert fee_mock.call_count == 1 + assert edge.fee == 0.001 + + def test_edge_init_error(mocker, edge_conf,): edge_conf['stake_amount'] = 0.5 mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001)) From df16fbd742bd6b0f11a9391c57f8a828f9bc5d68 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Apr 2021 19:22:41 +0200 Subject: [PATCH 72/73] Add "dataload complete" message to backtest + hyperopt --- freqtrade/optimize/backtesting.py | 1 + freqtrade/optimize/hyperopt.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index a1d4a2578..4731e6a38 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -478,6 +478,7 @@ class Backtesting: data: Dict[str, Any] = {} data, timerange = self.load_bt_data() + logger.info("Dataload complete. Calculating indicators") for strat in self.strategylist: min_date, max_date = self.backtest_one_strategy(strat, data, timerange) diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index d6003cf86..d1dabff36 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -379,7 +379,7 @@ class Hyperopt: logger.info(f"Using optimizer random state: {self.random_state}") self.hyperopt_table_header = -1 data, timerange = self.backtesting.load_bt_data() - + logger.info("Dataload complete. Calculating indicators") preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data) # Trim startup period from analyzed dataframe From 191a31db30403fcef3dad510167c45c41599c833 Mon Sep 17 00:00:00 2001 From: Matthias Date: Fri, 23 Apr 2021 19:36:26 +0200 Subject: [PATCH 73/73] NameErrors should not stop loading a different strategy --- freqtrade/resolvers/iresolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py index 37cfd70e6..b51795e9e 100644 --- a/freqtrade/resolvers/iresolver.py +++ b/freqtrade/resolvers/iresolver.py @@ -61,7 +61,7 @@ class IResolver: module = importlib.util.module_from_spec(spec) try: spec.loader.exec_module(module) # type: ignore # importlib does not use typehints - except (ModuleNotFoundError, SyntaxError, ImportError) as err: + except (ModuleNotFoundError, SyntaxError, ImportError, NameError) as err: # Catch errors in case a specific module is not installed logger.warning(f"Could not import {module_path} due to '{err}'") if enum_failed: