diff --git a/build_helpers/publish_docker_arm64.sh b/build_helpers/publish_docker_arm64.sh index 756d5e41d..08793d339 100755 --- a/build_helpers/publish_docker_arm64.sh +++ b/build_helpers/publish_docker_arm64.sh @@ -37,7 +37,7 @@ fi # Tag image for upload and next build step docker tag freqtrade:$TAG_ARM ${CACHE_IMAGE}:$TAG_ARM -docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot . +docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot . docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM @@ -63,13 +63,13 @@ echo "create manifests" docker manifest create --amend ${IMAGE_NAME}:${TAG} ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG} docker manifest push -p ${IMAGE_NAME}:${TAG} -docker manifest create --amend ${IMAGE_NAME}:${TAG_PLOT} ${CACHE_IMAGE}:${TAG_PLOT_ARM} ${CACHE_IMAGE}:${TAG_PLOT} +docker manifest create ${IMAGE_NAME}:${TAG_PLOT} ${CACHE_IMAGE}:${TAG_PLOT_ARM} ${CACHE_IMAGE}:${TAG_PLOT} docker manifest push -p ${IMAGE_NAME}:${TAG_PLOT} -Tag as latest for develop builds +# Tag as latest for develop builds if [ "${TAG}" = "develop" ]; then - docker tag ${IMAGE_NAME}:develop ${IMAGE_NAME}:latest - docker push ${IMAGE_NAME}:latest + docker manifest create ${IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG} + docker manifest push -p ${IMAGE_NAME}:latest fi docker images diff --git a/build_helpers/publish_docker_multi.sh b/build_helpers/publish_docker_multi.sh index 4961cb9a7..4010eed45 100755 --- a/build_helpers/publish_docker_multi.sh +++ b/build_helpers/publish_docker_multi.sh @@ -48,7 +48,7 @@ fi # Tag image for upload and next build step docker tag freqtrade:$TAG ${CACHE_IMAGE}:$TAG -docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot . +docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG} -t freqtrade:${TAG_PLOT} -f docker/Dockerfile.plot . docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT diff --git a/docker/Dockerfile.plot b/docker/Dockerfile.plot index d2fc3618a..e7f6bbb16 100644 --- a/docker/Dockerfile.plot +++ b/docker/Dockerfile.plot @@ -1,5 +1,6 @@ -ARG sourceimage=develop -FROM freqtradeorg/freqtrade:${sourceimage} +ARG sourceimage=freqtradeorg/freqtrade +ARG sourcetag=develop +FROM ${sourceimage}:${sourcetag} # Install dependencies COPY requirements-plot.txt /freqtrade/ diff --git a/docs/configuration.md b/docs/configuration.md index 5d4b1f2c3..fd4806fe6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -11,6 +11,37 @@ Per default, the bot loads the configuration from the `config.json` file, locate You can specify a different configuration file used by the bot with the `-c/--config` command-line option. +If you used the [Quick start](installation.md/#quick-start) method for installing +the bot, the installation script should have already created the default configuration file (`config.json`) for you. + +If the default configuration file is not created we recommend to use `freqtrade new-config --config config.json` to generate a basic configuration file. + +The Freqtrade configuration file is to be written in JSON format. + +Additionally to the standard JSON syntax, you may use one-line `// ...` and multi-line `/* ... */` comments in your configuration files and trailing commas in the lists of parameters. + +Do not worry if you are not familiar with JSON format -- simply open the configuration file with an editor of your choice, make some changes to the parameters you need, save your changes and, finally, restart the bot or, if it was previously stopped, run it again with the changes you made to the configuration. The bot validates the syntax of the configuration file at startup and will warn you if you made any errors editing it, pointing out problematic lines. + +### Environment variables + +Set options in the Freqtrade configuration via environment variables. +This takes priority over the corresponding value in configuration or strategy. + +Environment variables must be prefixed with `FREQTRADE__` to be loaded to the freqtrade configuration. + +`__` serves as level separator, so the format used should correspond to `FREQTRADE__{section}__{key}`. +As such - an environment variable defined as `export FREQTRADE__STAKE_AMOUNT=200` would result in `{stake_amount: 200}`. + +A more complex example might be `export FREQTRADE__EXCHANGE__KEY=` to keep your exchange key secret. This will move the value to the `exchange.key` section of the configuration. +Using this scheme, all configuration settings will also be available as environment variables. + +Please note that Environment variables will overwrite corresponding settings in your configuration, but command line Arguments will always win. + +!!! Note + Environment variables detected are logged at startup - so if you can't find why a value is not what you think it should be based on the configuration, make sure it's not loaded from an environment variable. + +### Multiple configuration files + Multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream. !!! Tip "Use multiple configuration files to keep secrets secret" @@ -22,17 +53,6 @@ Multiple configuration files can be specified and used by the bot or the bot can The 2nd file should only specify what you intend to override. If a key is in more than one of the configurations, then the "last specified configuration" wins (in the above example, `config-private.json`). -If you used the [Quick start](installation.md/#quick-start) method for installing -the bot, the installation script should have already created the default configuration file (`config.json`) for you. - -If the default configuration file is not created we recommend you to use `freqtrade new-config --config config.json` to generate a basic configuration file. - -The Freqtrade configuration file is to be written in JSON format. - -Additionally to the standard JSON syntax, you may use one-line `// ...` and multi-line `/* ... */` comments in your configuration files and trailing commas in the lists of parameters. - -Do not worry if you are not familiar with JSON format -- simply open the configuration file with an editor of your choice, make some changes to the parameters you need, save your changes and, finally, restart the bot or, if it was previously stopped, run it again with the changes you made to the configuration. The bot validates the syntax of the configuration file at startup and will warn you if you made any errors editing it, pointing out problematic lines. - ## Configuration parameters The table below will list all configuration parameters available. @@ -41,6 +61,7 @@ Freqtrade can also load many options via command line (CLI) arguments (check out The prevalence for all Options is as follows: - CLI arguments override any other option +- [Environment Variables](#environment-variables) - Configuration files are used in sequence (the last file wins) and override Strategy configurations. - Strategy configurations are only used if they are not set via configuration or command-line arguments. These options are marked with [Strategy Override](#parameters-in-the-strategy) in the below table. @@ -526,9 +547,10 @@ Once you will be happy with your bot performance running in the Dry-run mode, yo ## Switch to production mode -In production mode, the bot will engage your money. Be careful, since a wrong -strategy can lose all your money. Be aware of what you are doing when -you run it in production mode. +In production mode, the bot will engage your money. Be careful, since a wrong strategy can lose all your money. +Be aware of what you are doing when you run it in production mode. + +When switching to Production mode, please make sure to use a different / fresh database to avoid dry-run trades messing with your exchange money and eventually tainting your statistics. ### Setup your exchange account diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md index cb66fc7e2..1fa229225 100644 --- a/docs/docker_quickstart.md +++ b/docs/docker_quickstart.md @@ -24,82 +24,21 @@ 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" - ``` bash - mkdir ft_userdata - cd ft_userdata/ - # Download the docker-compose file from the repository - curl https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml -o docker-compose.yml +``` bash +mkdir ft_userdata +cd ft_userdata/ +# Download the docker-compose file from the repository +curl https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml -o docker-compose.yml - # Pull the freqtrade image - docker-compose pull +# Pull the freqtrade image +docker-compose pull - # Create user directory structure - docker-compose run --rm freqtrade create-userdir --userdir user_data +# Create user directory structure +docker-compose run --rm freqtrade create-userdir --userdir user_data - # Create configuration - Requires answering interactive questions - docker-compose run --rm freqtrade new-config --config user_data/config.json - ``` - -=== "RaspberryPi" - ``` bash - mkdir ft_userdata - cd ft_userdata/ - # Download the docker-compose file from the repository - curl https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml -o docker-compose.yml - - # Edit the compose file to use an image named `*_pi` (stable_pi or develop_pi) - - # Pull the freqtrade image - docker-compose pull - - # Create user directory structure - docker-compose run --rm freqtrade create-userdir --userdir user_data - - # 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. - ``` yml - image: freqtradeorg/freqtrade:stable_pi - # image: freqtradeorg/freqtrade:develop_pi - ``` - -=== "ARM 64 Systenms (Mac M1, Raspberry Pi 4, Jetson Nano)" - In case of a Mac M1, make sure that your docker installation is running in native mode - Arm64 images are not yet provided via Docker Hub and need to be build locally first. - Depending on the device, this may take a few minutes (Apple M1) or multiple hours (Raspberry Pi) - - ``` bash - # Clone Freqtrade repository - git clone https://github.com/freqtrade/freqtrade.git - cd freqtrade - # Optionally switch to the stable version - git checkout stable - - # Modify your docker-compose file to enable building and change the image name - # (see the Note Box below for necessary changes) - - # Build image - docker-compose build - - # Create user directory structure - docker-compose run --rm freqtrade create-userdir --userdir user_data - - # 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 arm64 build to work properly. - ``` yml - image: freqtradeorg/freqtrade:custom_arm64 - build: - context: . - dockerfile: "Dockerfile" - ``` +# Create configuration - Requires answering interactive questions +docker-compose run --rm freqtrade new-config --config user_data/config.json +``` 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. @@ -117,7 +56,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!" +!!! Danger "`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). @@ -167,6 +106,10 @@ 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 `. +!!! Warning "`docker-compose` for trade commands" + Trade commands (`freqtrade trade <...>`) should not be ran via `docker-compose run` - but should use `docker-compose up -d` instead. + This makes sure that the container is properly started (including port forwardings) and will make sure that the container will restart after a system reboot. + !!! 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). diff --git a/docs/exchanges.md b/docs/exchanges.md index e54f97714..29b9bb533 100644 --- a/docs/exchanges.md +++ b/docs/exchanges.md @@ -77,8 +77,9 @@ You can get a list of restricted markets by using the following snippet: ``` python import ccxt ct = ccxt.bittrex() -_ = ct.load_markets() -res = [ f"{x['MarketCurrency']}/{x['BaseCurrency']}" for x in ct.publicGetMarkets()['result'] if x['IsRestricted']] +lm = ct.load_markets() + +res = [p for p, x in lm.items() if 'US' in x['info']['prohibitedIn']] print(res) ``` diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md index 8727cc3fc..995e49a2d 100644 --- a/docs/includes/pairlists.md +++ b/docs/includes/pairlists.md @@ -221,10 +221,10 @@ If `DOGE/BTC` maximum bid is 0.00000026 and minimum ask is 0.00000027, the ratio #### RangeStabilityFilter -Removes pairs where the difference between lowest low and highest high over `lookback_days` days is below `min_rate_of_change`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. +Removes pairs where the difference between lowest low and highest high over `lookback_days` days is below `min_rate_of_change` or above `max_rate_of_change`. Since this is a filter that requires additional data, the results are cached for `refresh_period`. In the below example: -If the trading range over the last 10 days is <1%, remove the pair from the whitelist. +If the trading range over the last 10 days is <1% or >99%, remove the pair from the whitelist. ```json "pairlists": [ @@ -232,6 +232,7 @@ If the trading range over the last 10 days is <1%, remove the pair from the whit "method": "RangeStabilityFilter", "lookback_days": 10, "min_rate_of_change": 0.01, + "max_rate_of_change": 0.99, "refresh_period": 1440 } ] @@ -239,6 +240,7 @@ If the trading range over the last 10 days is <1%, remove the pair from the whit !!! Tip This Filter can be used to automatically remove stable coin pairs, which have a very low trading range, and are therefore extremely difficult to trade with profit. + Additionally, it can also be used to automatically remove pairs with extreme high/low variance over a given amount of time. #### VolatilityFilter diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 6a904c951..047821f2d 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,4 +1,4 @@ mkdocs==1.2.2 -mkdocs-material==7.2.1 +mkdocs-material==7.2.2 mdx_truly_sane_lists==1.2 pymdown-extensions==8.2 diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index 477396931..caa3f53a6 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -110,7 +110,7 @@ DELETE FROM trades WHERE id = 31; Freqtrade supports PostgreSQL by using SQLAlchemy, which supports multiple different database systems. Installation: -`pip install psycopg2` +`pip install psycopg2-binary` Usage: `... --db-url postgresql+psycopg2://:@localhost:5432/` diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md index cd0f60ce6..0704473fb 100644 --- a/docs/strategy-advanced.md +++ b/docs/strategy-advanced.md @@ -114,6 +114,36 @@ class AwesomeStrategy(IStrategy): See [Dataframe access](#dataframe-access) for more information about dataframe use in strategy callbacks. +## Buy Tag + +When your strategy has multiple buy signals, you can name the signal that triggered. +Then you can access you buy signal on `custom_sell` + +```python +def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + dataframe.loc[ + ( + (dataframe['rsi'] < 35) & + (dataframe['volume'] > 0) + ), + ['buy', 'buy_tag']] = (1, 'buy_signal_rsi') + + return dataframe + +def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, + current_profit: float, **kwargs): + dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) + last_candle = dataframe.iloc[-1].squeeze() + if trade.buy_tag == 'buy_signal_rsi' and last_candle['rsi'] > 80: + return 'sell_signal_rsi' + return None + +``` + +!!! Note + `buy_tag` is limited to 100 characters, remaining data will be truncated. + + ## Custom stoploss The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss. diff --git a/docs/utils.md b/docs/utils.md index 789462de4..6395fb6f9 100644 --- a/docs/utils.md +++ b/docs/utils.md @@ -627,7 +627,7 @@ FreqUI will also show the backtesting results. ``` usage: freqtrade webserver [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] - [--userdir PATH] [-s NAME] [--strategy-path PATH] + [--userdir PATH] optional arguments: -h, --help show this help message and exit @@ -648,12 +648,6 @@ Common arguments: --userdir PATH, --user-data-dir PATH Path to userdata directory. -Strategy arguments: - -s NAME, --strategy NAME - Specify strategy class name which will be used by the - bot. - --strategy-path PATH Specify additional strategy lookup path. - ``` ## List Hyperopt results diff --git a/docs/webhook-config.md b/docs/webhook-config.md index 8ce6edc18..288afc384 100644 --- a/docs/webhook-config.md +++ b/docs/webhook-config.md @@ -83,6 +83,7 @@ Possible parameters are: * `fiat_currency` * `order_type` * `current_rate` +* `buy_tag` ### Webhookbuycancel @@ -100,6 +101,7 @@ Possible parameters are: * `fiat_currency` * `order_type` * `current_rate` +* `buy_tag` ### Webhookbuyfill @@ -115,6 +117,7 @@ Possible parameters are: * `stake_amount` * `stake_currency` * `fiat_currency` +* `buy_tag` ### Webhooksell diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py index bd30adcae..4dd5b7203 100644 --- a/freqtrade/configuration/configuration.py +++ b/freqtrade/configuration/configuration.py @@ -11,6 +11,7 @@ from freqtrade import constants from freqtrade.configuration.check_exchange import check_exchange from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir +from freqtrade.configuration.environment_vars import enironment_vars_to_dict from freqtrade.configuration.load_config import load_config_file, load_file from freqtrade.enums import NON_UTIL_MODES, TRADING_MODES, RunMode from freqtrade.exceptions import OperationalException @@ -71,6 +72,11 @@ class Configuration: # Merge config options, overwriting old values config = deep_merge_dicts(load_config_file(path), config) + + # Load environment variables + env_data = enironment_vars_to_dict() + config = deep_merge_dicts(env_data, config) + config['config_files'] = files # Normalize config if 'internals' not in config: diff --git a/freqtrade/configuration/environment_vars.py b/freqtrade/configuration/environment_vars.py new file mode 100644 index 000000000..4c190ed04 --- /dev/null +++ b/freqtrade/configuration/environment_vars.py @@ -0,0 +1,54 @@ +import logging +import os +from typing import Any, Dict + +from freqtrade.constants import ENV_VAR_PREFIX +from freqtrade.misc import deep_merge_dicts + + +logger = logging.getLogger(__name__) + + +def get_var_typed(val): + try: + return int(val) + except ValueError: + try: + return float(val) + except ValueError: + if val.lower() in ('t', 'true'): + return True + elif val.lower() in ('f', 'false'): + return False + # keep as string + return val + + +def flat_vars_to_nested_dict(env_dict: Dict[str, Any], prefix: str) -> Dict[str, Any]: + """ + Environment variables must be prefixed with FREQTRADE. + FREQTRADE__{section}__{key} + :param env_dict: Dictionary to validate - usually os.environ + :param prefix: Prefix to consider (usually FREQTRADE__) + :return: Nested dict based on available and relevant variables. + """ + relevant_vars: Dict[str, Any] = {} + + for env_var, val in sorted(env_dict.items()): + if env_var.startswith(prefix): + logger.info(f"Loading variable '{env_var}'") + key = env_var.replace(prefix, '') + for k in reversed(key.split('__')): + val = {k.lower(): get_var_typed(val) if type(val) != dict else val} + relevant_vars = deep_merge_dicts(val, relevant_vars) + + return relevant_vars + + +def enironment_vars_to_dict() -> Dict[str, Any]: + """ + Read environment variables and return a nested dict for relevant variables + Relevant variables must follow the FREQTRADE__{section}__{key} pattern + :return: Nested dict based on available and relevant variables. + """ + return flat_vars_to_nested_dict(os.environ.copy(), ENV_VAR_PREFIX) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 2f93ace1c..b48644c58 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -47,6 +47,7 @@ USERPATH_STRATEGIES = 'strategies' USERPATH_NOTEBOOKS = 'notebooks' TELEGRAM_SETTING_OPTIONS = ['on', 'off', 'silent'] +ENV_VAR_PREFIX = 'FREQTRADE__' # Define decimals per coin for outputs diff --git a/freqtrade/data/btanalysis.py b/freqtrade/data/btanalysis.py index e7af5eab8..d62712cbb 100644 --- a/freqtrade/data/btanalysis.py +++ b/freqtrade/data/btanalysis.py @@ -30,7 +30,7 @@ BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date', 'fee_open', 'fee_close', 'trade_duration', 'profit_ratio', 'profit_abs', 'sell_reason', 'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs', - 'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', ] + 'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'buy_tag'] def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str: diff --git a/freqtrade/data/dataprovider.py b/freqtrade/data/dataprovider.py index 391ed4587..cdee0f078 100644 --- a/freqtrade/data/dataprovider.py +++ b/freqtrade/data/dataprovider.py @@ -10,11 +10,12 @@ from typing import Any, Dict, List, Optional, Tuple from pandas import DataFrame +from freqtrade.configuration import TimeRange from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe from freqtrade.data.history import load_pair_history from freqtrade.enums import RunMode from freqtrade.exceptions import ExchangeError, OperationalException -from freqtrade.exchange import Exchange +from freqtrade.exchange import Exchange, timeframe_to_seconds logger = logging.getLogger(__name__) @@ -31,6 +32,7 @@ class DataProvider: self._pairlists = pairlists self.__cached_pairs: Dict[PairWithTimeframe, Tuple[DataFrame, datetime]] = {} self.__slice_index: Optional[int] = None + self.__cached_pairs_backtesting: Dict[PairWithTimeframe, DataFrame] = {} def _set_dataframe_max_index(self, limit_index: int): """ @@ -62,11 +64,22 @@ class DataProvider: :param pair: pair to get the data for :param timeframe: timeframe to get data for """ - return load_pair_history(pair=pair, - timeframe=timeframe or self._config['timeframe'], - datadir=self._config['datadir'], - data_format=self._config.get('dataformat_ohlcv', 'json') - ) + saved_pair = (pair, str(timeframe)) + if saved_pair not in self.__cached_pairs_backtesting: + timerange = TimeRange.parse_timerange(None if self._config.get( + 'timerange') is None else str(self._config.get('timerange'))) + # Move informative start time respecting startup_candle_count + timerange.subtract_start( + timeframe_to_seconds(str(timeframe)) * self._config.get('startup_candle_count', 0) + ) + self.__cached_pairs_backtesting[saved_pair] = load_pair_history( + pair=pair, + timeframe=timeframe or self._config['timeframe'], + datadir=self._config['datadir'], + timerange=timerange, + data_format=self._config.get('dataformat_ohlcv', 'json') + ) + return self.__cached_pairs_backtesting[saved_pair].copy() def get_pair_dataframe(self, pair: str, timeframe: str = None) -> DataFrame: """ diff --git a/freqtrade/data/history/jsondatahandler.py b/freqtrade/data/history/jsondatahandler.py index 990e75bd9..24d6e814b 100644 --- a/freqtrade/data/history/jsondatahandler.py +++ b/freqtrade/data/history/jsondatahandler.py @@ -62,7 +62,7 @@ class JsonDataHandler(IDataHandler): filename = self._pair_data_filename(self._datadir, pair, timeframe) _data = data.copy() # Convert date to int - _data['date'] = _data['date'].astype(np.int64) // 1000 // 1000 + _data['date'] = _data['date'].view(np.int64) // 1000 // 1000 # Reset index, select only appropriate columns and save as json _data.reset_index(drop=True).loc[:, self._columns].to_json( diff --git a/freqtrade/enums/__init__.py b/freqtrade/enums/__init__.py index 179d2d5e9..6099f7003 100644 --- a/freqtrade/enums/__init__.py +++ b/freqtrade/enums/__init__.py @@ -4,5 +4,5 @@ from freqtrade.enums.interestmode import InterestMode from freqtrade.enums.rpcmessagetype import RPCMessageType from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode from freqtrade.enums.selltype import SellType -from freqtrade.enums.signaltype import SignalType +from freqtrade.enums.signaltype import SignalTagType, SignalType from freqtrade.enums.state import State diff --git a/freqtrade/enums/signaltype.py b/freqtrade/enums/signaltype.py index d636f378a..d2995d57a 100644 --- a/freqtrade/enums/signaltype.py +++ b/freqtrade/enums/signaltype.py @@ -7,3 +7,10 @@ class SignalType(Enum): """ BUY = "buy" SELL = "sell" + + +class SignalTagType(Enum): + """ + Enum for signal columns + """ + BUY_TAG = "buy_tag" diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index ed2ab0a61..c6f60e08a 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -387,7 +387,7 @@ class Exchange: # its contents depend on the exchange. # It can also be a string or similar ... so we need to verify that first. elif (isinstance(self.markets[pair].get('info', None), dict) - and self.markets[pair].get('info', {}).get('IsRestricted', False)): + and self.markets[pair].get('info', {}).get('prohibitedIn', False)): # Warn users about restricted pairs in whitelist. # We cannot determine reliably if Users are affected. logger.warning(f"Pair {pair} is restricted for some users on this exchange." @@ -689,7 +689,16 @@ class Exchange: # Order handling def create_order(self, pair: str, ordertype: str, side: str, amount: float, - rate: float, params: Dict = {}) -> Dict: + rate: float, time_in_force: str = 'gtc') -> Dict: + + if self._config['dry_run']: + dry_order = self.create_dry_run_order(pair, ordertype, side, amount, rate) + return dry_order + + params = self._params.copy() + if time_in_force != 'gtc' and ordertype != 'market': + params.update({'timeInForce': time_in_force}) + try: # Set the precision for amount and price(rate) as accepted by the exchange amount = self.amount_to_precision(pair, amount) @@ -720,32 +729,6 @@ class Exchange: except ccxt.BaseError as e: raise OperationalException(e) from e - def buy(self, pair: str, ordertype: str, amount: float, - rate: float, time_in_force: str) -> Dict: - - if self._config['dry_run']: - dry_order = self.create_dry_run_order(pair, ordertype, "buy", amount, rate) - return dry_order - - params = self._params.copy() - if time_in_force != 'gtc' and ordertype != 'market': - params.update({'timeInForce': time_in_force}) - - return self.create_order(pair, ordertype, 'buy', amount, rate, params) - - def sell(self, pair: str, ordertype: str, amount: float, - rate: float, time_in_force: str = 'gtc') -> Dict: - - if self._config['dry_run']: - dry_order = self.create_dry_run_order(pair, ordertype, "sell", amount, rate) - return dry_order - - params = self._params.copy() - if time_in_force != 'gtc' and ordertype != 'market': - params.update({'timeInForce': time_in_force}) - - return self.create_order(pair, ordertype, 'sell', amount, rate, params) - def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: """ Verify stop_loss against stoploss-order value (limit or price) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 5d12c4cd4..179c99d2c 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -420,20 +420,24 @@ class FreqtradeBot(LoggingMixin): return False # running get_signal on historical data fetched - (buy, sell) = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df) + (buy, sell, buy_tag) = self.strategy.get_signal( + pair, + self.strategy.timeframe, + analyzed_df + ) if buy and not sell: stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge) bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {}) if ((bid_check_dom.get('enabled', False)) and - (bid_check_dom.get('bids_to_ask_delta', 0) > 0)): + (bid_check_dom.get('bids_to_ask_delta', 0) > 0)): if self._check_depth_of_market_buy(pair, bid_check_dom): - return self.execute_buy(pair, stake_amount) + return self.execute_buy(pair, stake_amount, buy_tag=buy_tag) else: return False - return self.execute_buy(pair, stake_amount) + return self.execute_buy(pair, stake_amount, buy_tag=buy_tag) else: return False @@ -462,7 +466,7 @@ class FreqtradeBot(LoggingMixin): return False def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None, - forcebuy: bool = False) -> bool: + forcebuy: bool = False, buy_tag: Optional[str] = None) -> bool: """ Executes a limit buy for the given pair :param pair: pair for which we want to create a LIMIT_BUY @@ -510,9 +514,9 @@ class FreqtradeBot(LoggingMixin): logger.info(f"User requested abortion of buying {pair}") return False amount = self.exchange.amount_to_precision(pair, amount) - order = self.exchange.buy(pair=pair, ordertype=order_type, - amount=amount, rate=buy_limit_requested, - time_in_force=time_in_force) + order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy", + amount=amount, rate=buy_limit_requested, + time_in_force=time_in_force) order_obj = Order.parse_from_ccxt_object(order, pair, 'buy') order_id = order['id'] order_status = order.get('status', None) @@ -565,6 +569,7 @@ class FreqtradeBot(LoggingMixin): exchange=self.exchange.id, open_order_id=order_id, strategy=self.strategy.get_strategy_name(), + buy_tag=buy_tag, timeframe=timeframe_to_minutes(self.config['timeframe']) ) trade.orders.append(order_obj) @@ -590,6 +595,7 @@ class FreqtradeBot(LoggingMixin): msg = { 'trade_id': trade.id, 'type': RPCMessageType.BUY, + 'buy_tag': trade.buy_tag, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, 'limit': trade.open_rate, @@ -614,6 +620,7 @@ class FreqtradeBot(LoggingMixin): msg = { 'trade_id': trade.id, 'type': RPCMessageType.BUY_CANCEL, + 'buy_tag': trade.buy_tag, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, 'limit': trade.open_rate, @@ -634,6 +641,7 @@ class FreqtradeBot(LoggingMixin): msg = { 'trade_id': trade.id, 'type': RPCMessageType.BUY_FILL, + 'buy_tag': trade.buy_tag, 'exchange': self.exchange.name.capitalize(), 'pair': trade.pair, 'open_rate': trade.open_rate, @@ -692,7 +700,11 @@ class FreqtradeBot(LoggingMixin): analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, self.strategy.timeframe) - (buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.timeframe, analyzed_df) + (buy, sell, _) = self.strategy.get_signal( + trade.pair, + self.strategy.timeframe, + analyzed_df + ) logger.debug('checking sell') sell_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell") @@ -1094,11 +1106,11 @@ class FreqtradeBot(LoggingMixin): try: # Execute sell and update trade record - order = self.exchange.sell(pair=trade.pair, - ordertype=order_type, - amount=amount, rate=limit, - time_in_force=time_in_force - ) + order = self.exchange.create_order(pair=trade.pair, + ordertype=order_type, side="sell", + amount=amount, rate=limit, + time_in_force=time_in_force + ) except InsufficientFundsError as e: logger.warning(f"Unable to place order {e}.") # Try to figure out what went wrong diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index ebd4135d9..45e60e013 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -15,7 +15,7 @@ from freqtrade.configuration import TimeRange, remove_credentials, validate_conf from freqtrade.constants import DATETIME_PRINT_FORMAT from freqtrade.data import history from freqtrade.data.btanalysis import trade_list_to_dataframe -from freqtrade.data.converter import trim_dataframes +from freqtrade.data.converter import trim_dataframe, trim_dataframes from freqtrade.data.dataprovider import DataProvider from freqtrade.enums import BacktestState, SellType from freqtrade.exceptions import DependencyException, OperationalException @@ -43,6 +43,7 @@ CLOSE_IDX = 3 SELL_IDX = 4 LOW_IDX = 5 HIGH_IDX = 6 +BUY_TAG_IDX = 7 class Backtesting: @@ -116,8 +117,13 @@ class Backtesting: self.wallets = Wallets(self.config, self.exchange, log=False) + self.timerange = TimeRange.parse_timerange( + None if self.config.get('timerange') is None else str(self.config.get('timerange'))) + # Get maximum required startup period self.required_startup = max([strat.startup_candle_count for strat in self.strategylist]) + # Add maximum startup candle count to configuration for informative pairs support + self.config['startup_candle_count'] = self.required_startup self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe) self.progress = BTProgress() @@ -154,14 +160,11 @@ class Backtesting: """ self.progress.init_step(BacktestState.DATALOAD, 1) - timerange = TimeRange.parse_timerange(None if self.config.get( - 'timerange') is None else str(self.config.get('timerange'))) - data = history.load_data( datadir=self.config['datadir'], pairs=self.pairlists.whitelist, timeframe=self.timeframe, - timerange=timerange, + timerange=self.timerange, startup_candles=self.required_startup, fail_without_data=True, data_format=self.config.get('dataformat_ohlcv', 'json'), @@ -174,11 +177,11 @@ class Backtesting: f'({(max_date - min_date).days} days).') # Adjust startts forward if not enough data is available - timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe), - self.required_startup, min_date) + self.timerange.adjust_start_if_necessary(timeframe_to_seconds(self.timeframe), + self.required_startup, min_date) self.progress.set_new_value(1) - return data, timerange + return data, self.timerange def prepare_backtest(self, enable_protections): """ @@ -217,23 +220,34 @@ class Backtesting: for pair, pair_data in processed.items(): self.check_abort() self.progress.increment() + has_buy_tag = 'buy_tag' in pair_data + headers = headers + ['buy_tag'] if has_buy_tag else headers if not pair_data.empty: pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist + if has_buy_tag: + pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist df_analyzed = self.strategy.advise_sell( - self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() - + self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair}).copy() + # Trim startup period from analyzed dataframe + df_analyzed = trim_dataframe(df_analyzed, self.timerange, + startup_candles=self.required_startup) # To avoid using data from future, we use buy/sell signals shifted # from the previous candle df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1) df_analyzed.loc[:, 'sell'] = df_analyzed.loc[:, 'sell'].shift(1) + if has_buy_tag: + df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1) df_analyzed.drop(df_analyzed.head(1).index, inplace=True) + # Update dataprovider cache + self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed) + # Convert from Pandas to list for performance reasons # (Looping Pandas is slow.) - data[pair] = df_analyzed.values.tolist() + data[pair] = df_analyzed[headers].values.tolist() return data def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple, @@ -262,7 +276,7 @@ class Backtesting: # Worst case: price reaches stop_positive_offset and dives down. stop_rate = (sell_row[OPEN_IDX] * (1 + abs(self.strategy.trailing_stop_positive_offset) - - abs(self.strategy.trailing_stop_positive))) + abs(self.strategy.trailing_stop_positive))) else: # Worst case: price ticks tiny bit above open and dives down. stop_rate = sell_row[OPEN_IDX] * (1 - abs(trade.stop_loss_pct)) @@ -358,6 +372,7 @@ class Backtesting: if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount): # Enter trade + has_buy_tag = len(row) >= BUY_TAG_IDX + 1 trade = LocalTrade( pair=pair, open_rate=row[OPEN_IDX], @@ -367,6 +382,7 @@ class Backtesting: fee_open=self.fee, fee_close=self.fee, is_open=True, + buy_tag=row[BUY_TAG_IDX] if has_buy_tag else None, exchange='backtesting', ) return trade @@ -423,10 +439,6 @@ class Backtesting: trades: List[LocalTrade] = [] self.prepare_backtest(enable_protections) - # Update dataprovider cache - for pair, dataframe in processed.items(): - self.dataprovider._set_cached_df(pair, self.timeframe, dataframe) - # Use dict of lists with data for performance # (looping lists is a lot faster than pandas DataFrames) data: Dict = self._get_ohlcv_as_lists(processed) @@ -537,14 +549,15 @@ class Backtesting: preprocessed = self.strategy.ohlcvdata_to_dataframe(data) # Trim startup period from analyzed dataframe - preprocessed = trim_dataframes(preprocessed, timerange, self.required_startup) + preprocessed_tmp = trim_dataframes(preprocessed, timerange, self.required_startup) - if not preprocessed: + if not preprocessed_tmp: raise OperationalException( "No data left after adjusting for startup candles.") - min_date, max_date = history.get_timerange(preprocessed) - + # Use preprocessed_tmp for date generation (the trimmed dataframe). + # Backtesting will re-trim the dataframes after buy/sell signal generation. + min_date, max_date = history.get_timerange(preprocessed_tmp) logger.info(f'Backtesting with data from {min_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} ' f'({(max_date - min_date).days} days).') diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 80ae8886e..a69e5a5a2 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -102,16 +102,18 @@ class Hyperopt: self.num_epochs_saved = 0 self.current_best_epoch: Optional[Dict[str, Any]] = None - # Populate functions here (hasattr is slow so should not be run during "regular" operations) - if hasattr(self.custom_hyperopt, 'populate_indicators'): - self.backtesting.strategy.advise_indicators = ( # type: ignore - self.custom_hyperopt.populate_indicators) # type: ignore - if hasattr(self.custom_hyperopt, 'populate_buy_trend'): - self.backtesting.strategy.advise_buy = ( # type: ignore - self.custom_hyperopt.populate_buy_trend) # type: ignore - if hasattr(self.custom_hyperopt, 'populate_sell_trend'): - self.backtesting.strategy.advise_sell = ( # type: ignore - self.custom_hyperopt.populate_sell_trend) # type: ignore + if not self.auto_hyperopt: + # Populate "fallback" functions here + # (hasattr is slow so should not be run during "regular" operations) + if hasattr(self.custom_hyperopt, 'populate_indicators'): + self.backtesting.strategy.advise_indicators = ( # type: ignore + self.custom_hyperopt.populate_indicators) # type: ignore + if hasattr(self.custom_hyperopt, 'populate_buy_trend'): + self.backtesting.strategy.advise_buy = ( # type: ignore + self.custom_hyperopt.populate_buy_trend) # type: ignore + if hasattr(self.custom_hyperopt, 'populate_sell_trend'): + self.backtesting.strategy.advise_sell = ( # type: ignore + self.custom_hyperopt.populate_sell_trend) # type: ignore # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set if self.config.get('use_max_market_positions', True): @@ -264,17 +266,14 @@ class Hyperopt: def generate_optimizer(self, raw_params: List[Any], iteration=None) -> Dict: """ - Used Optimize function. Called once per epoch to optimize whatever is configured. + Used Optimize function. + Called once per epoch to optimize whatever is configured. Keep this function as optimized as possible! """ backtest_start_time = datetime.now(timezone.utc) params_dict = self._get_params_dict(self.dimensions, raw_params) # Apply parameters - if HyperoptTools.has_space(self.config, 'roi'): - self.backtesting.strategy.minimal_roi = ( # type: ignore - self.custom_hyperopt.generate_roi_table(params_dict)) - if HyperoptTools.has_space(self.config, 'buy'): self.backtesting.strategy.advise_buy = ( # type: ignore self.custom_hyperopt.buy_strategy_generator(params_dict)) @@ -283,6 +282,10 @@ class Hyperopt: self.backtesting.strategy.advise_sell = ( # type: ignore self.custom_hyperopt.sell_strategy_generator(params_dict)) + if HyperoptTools.has_space(self.config, 'roi'): + self.backtesting.strategy.minimal_roi = ( # type: ignore + self.custom_hyperopt.generate_roi_table(params_dict)) + if HyperoptTools.has_space(self.config, 'stoploss'): self.backtesting.strategy.stoploss = params_dict['stoploss'] @@ -378,16 +381,15 @@ class Hyperopt: preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data) - # Trim startup period from analyzed dataframe + # Trim startup period from analyzed dataframe to get correct dates for output. processed = trim_dataframes(preprocessed, timerange, self.backtesting.required_startup) - self.min_date, self.max_date = get_timerange(processed) logger.info(f'Hyperopting with data from {self.min_date.strftime(DATETIME_PRINT_FORMAT)} ' f'up to {self.max_date.strftime(DATETIME_PRINT_FORMAT)} ' f'({(self.max_date - self.min_date).days} days)..') - - dump(processed, self.data_pickle_file) + # Store non-trimmed data - will be trimmed after signal generation. + dump(preprocessed, self.data_pickle_file) def start(self) -> None: self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None)) diff --git a/freqtrade/persistence/migrations.py b/freqtrade/persistence/migrations.py index 8077a3a49..22d0b56b7 100644 --- a/freqtrade/persistence/migrations.py +++ b/freqtrade/persistence/migrations.py @@ -54,6 +54,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col # sqlite does not support literals for booleans is_short = get_column_def(cols, 'is_short', '0') interest_mode = get_column_def(cols, 'interest_mode', 'null') + buy_tag = get_column_def(cols, 'buy_tag', 'null') # If ticker-interval existed use that, else null. if has_column(cols, 'ticker_interval'): timeframe = get_column_def(cols, 'timeframe', 'ticker_interval') @@ -72,7 +73,8 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col # Schema migration necessary with engine.begin() as connection: connection.execute(text(f"alter table trades rename to {table_back_name}")) - # drop indexes on backup table + with engine.begin() as connection: + # drop indexes on backup table in new session for index in inspector.get_indexes(table_back_name): connection.execute(text(f"drop index {index['name']}")) # let SQLAlchemy create the schema as required @@ -83,23 +85,16 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col connection.execute(text(f"""insert into trades (id, exchange, pair, is_open, fee_open, fee_open_cost, fee_open_currency, - fee_close, fee_close_cost, fee_open_currency, open_rate, + fee_close, fee_close_cost, fee_close_currency, open_rate, open_rate_requested, close_rate, close_rate_requested, close_profit, stake_amount, amount, amount_requested, open_date, close_date, open_order_id, stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct, stoploss_order_id, stoploss_last_update, - max_rate, min_rate, sell_reason, sell_order_status, strategy, + max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag, timeframe, open_trade_value, close_profit_abs, leverage, interest_rate, isolated_liq, is_short, interest_mode ) - select id, lower(exchange), - case - when instr(pair, '_') != 0 then - substr(pair, instr(pair, '_') + 1) || '/' || - substr(pair, 1, instr(pair, '_') - 1) - else pair - end - pair, + select id, lower(exchange), pair, is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost, {fee_open_currency} fee_open_currency, {fee_close} fee_close, {fee_close_cost} fee_close_cost, {fee_close_currency} fee_close_currency, @@ -112,7 +107,7 @@ def migrate_trades_table(decl_base, inspector, engine, table_back_name: str, col {stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update, {max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason, {sell_order_status} sell_order_status, - {strategy} strategy, {timeframe} timeframe, + {strategy} strategy, {buy_tag} buy_tag, {timeframe} timeframe, {open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs, {leverage} leverage, {interest_rate} interest_rate, {isolated_liq} isolated_liq, {is_short} is_short, @@ -143,7 +138,9 @@ def migrate_orders_table(decl_base, inspector, engine, table_back_name: str, col with engine.begin() as connection: connection.execute(text(f"alter table orders rename to {table_back_name}")) - # drop indexes on backup table + + with engine.begin() as connection: + # drop indexes on backup table in new session for index in inspector.get_indexes(table_back_name): connection.execute(text(f"drop index {index['name']}")) @@ -174,7 +171,7 @@ def check_migrate(engine, decl_base, previous_tables) -> None: table_back_name = get_backup_name(tabs, 'trades_bak') # Check for latest column - if not has_column(cols, 'open_trade_value'): + if not has_column(cols, 'buy_tag'): logger.info(f'Running database migration for trades - backup: {table_back_name}') migrate_trades_table(decl_base, inspector, engine, table_back_name, cols) # Reread columns - the above recreated the table! diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 2cc7f51be..d09c5ed68 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -261,6 +261,7 @@ class LocalTrade(): sell_reason: str = '' sell_order_status: str = '' strategy: str = '' + buy_tag: Optional[str] = None timeframe: Optional[int] = None # Margin trading properties @@ -378,6 +379,7 @@ class LocalTrade(): 'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None, 'stake_amount': round(self.stake_amount, 8), 'strategy': self.strategy, + 'buy_tag': self.buy_tag, 'timeframe': self.timeframe, 'fee_open': self.fee_open, @@ -817,7 +819,7 @@ class LocalTrade(): # skip case if trailing-stop changed the stoploss already. if (trade.stop_loss == trade.initial_stop_loss - and trade.initial_stop_loss_pct != desired_stoploss): + and trade.initial_stop_loss_pct != desired_stoploss): # Stoploss value got changed logger.info(f"Stoploss for {trade} needs adjustment...") @@ -884,6 +886,7 @@ class Trade(_DECL_BASE, LocalTrade): sell_reason = Column(String(100), nullable=True) # TODO-mg: Change to close_reason sell_order_status = Column(String(100), nullable=True) # TODO-mg: Change to close_order_status strategy = Column(String(100), nullable=True) + buy_tag = Column(String(100), nullable=True) timeframe = Column(Integer, nullable=True) # Margin trading properties diff --git a/freqtrade/plugins/pairlist/rangestabilityfilter.py b/freqtrade/plugins/pairlist/rangestabilityfilter.py index a6d1820de..ef7f2cbcb 100644 --- a/freqtrade/plugins/pairlist/rangestabilityfilter.py +++ b/freqtrade/plugins/pairlist/rangestabilityfilter.py @@ -26,6 +26,7 @@ class RangeStabilityFilter(IPairList): self._days = pairlistconfig.get('lookback_days', 10) self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01) + self._max_rate_of_change = pairlistconfig.get('max_rate_of_change', None) self._refresh_period = pairlistconfig.get('refresh_period', 1440) self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period) @@ -50,8 +51,12 @@ class RangeStabilityFilter(IPairList): """ Short whitelist method description - used for startup-messages """ + max_rate_desc = "" + if self._max_rate_of_change: + max_rate_desc = (f" and above {self._max_rate_of_change}") return (f"{self.name} - Filtering pairs with rate of change below " - f"{self._min_rate_of_change} over the last {plural(self._days, 'day')}.") + f"{self._min_rate_of_change}{max_rate_desc} over the " + f"last {plural(self._days, 'day')}.") def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]: """ @@ -104,6 +109,16 @@ class RangeStabilityFilter(IPairList): f"which is below the threshold of {self._min_rate_of_change}.", logger.info) result = False + if self._max_rate_of_change: + if pct_change <= self._max_rate_of_change: + result = True + else: + self.log_once( + f"Removed {pair} from whitelist, because rate of change " + f"over {self._days} {plural(self._days, 'day')} is {pct_change:.3f}, " + f"which is above the threshold of {self._max_rate_of_change}.", + logger.info) + result = False self._pair_cache[pair] = result return result diff --git a/freqtrade/rpc/api_server/api_backtest.py b/freqtrade/rpc/api_server/api_backtest.py index 76b4a8169..2fa66645b 100644 --- a/freqtrade/rpc/api_server/api_backtest.py +++ b/freqtrade/rpc/api_server/api_backtest.py @@ -47,15 +47,15 @@ async def api_start_backtest(bt_settings: BacktestRequest, background_tasks: Bac not ApiServer._bt or lastconfig.get('timeframe') != strat.timeframe or lastconfig.get('dry_run_wallet') != btconfig.get('dry_run_wallet', 0) + or lastconfig.get('timerange') != btconfig['timerange'] ): from freqtrade.optimize.backtesting import Backtesting ApiServer._bt = Backtesting(btconfig) - # Only reload data if timeframe or timerange changed. + # Only reload data if timeframe changed. if ( not ApiServer._bt_data or not ApiServer._bt_timerange - or lastconfig.get('timerange') != btconfig['timerange'] or lastconfig.get('stake_amount') != btconfig.get('stake_amount') or lastconfig.get('enable_protections') != btconfig.get('enable_protections') or lastconfig.get('protections') != btconfig.get('protections', []) diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index c6b6a6d28..318762136 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -151,6 +151,7 @@ class TradeSchema(BaseModel): amount_requested: float stake_amount: float strategy: str + buy_tag: Optional[str] timeframe: int fee_open: Optional[float] fee_open_cost: Optional[float] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 263a3fc6d..a1f6a7e33 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -208,15 +208,25 @@ class Telegram(RPCHandler): 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'])}") - + content = [] + content.append( + f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}" + f" (#{msg['trade_id']})\n" + ) + if msg.get('buy_tag', None): + content.append(f"*Buy Tag:* `{msg['buy_tag']}`\n") + content.append(f"*Amount:* `{msg['amount']:.8f}`\n") + content.append(f"*Open Rate:* `{msg['limit']:.8f}`\n") + content.append(f"*Current Rate:* `{msg['current_rate']:.8f}`\n") + content.append( + 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'])}" + content.append( + f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" + ) + + message = ''.join(content) message += ")`" return message @@ -354,6 +364,7 @@ class Telegram(RPCHandler): "*Trade ID:* `{trade_id}` `(since {open_date_hum})`", "*Current Pair:* {pair}", "*Amount:* `{amount} ({stake_amount} {base_currency})`", + "*Buy Tag:* `{buy_tag}`" if r['buy_tag'] else "", "*Open Rate:* `{open_rate:.8f}`", "*Close Rate:* `{close_rate}`" if r['close_rate'] else "", "*Current Rate:* `{current_rate:.8f}`", diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index f9772e1df..6f3e047eb 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -13,7 +13,7 @@ from pandas import DataFrame from freqtrade.constants import ListPairsWithTimeframes from freqtrade.data.dataprovider import DataProvider -from freqtrade.enums import SellType, SignalType +from freqtrade.enums import SellType, SignalTagType, SignalType from freqtrade.exceptions import OperationalException, StrategyError from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds from freqtrade.exchange.exchange import timeframe_to_next_date @@ -422,6 +422,7 @@ class IStrategy(ABC, HyperStrategyMixin): logger.debug("Skipping TA Analysis for already analyzed candle") dataframe['buy'] = 0 dataframe['sell'] = 0 + dataframe['buy_tag'] = None # Other Defs in strategy that want to be called every loop here # twitter_sell = self.watch_twitter_feed(dataframe, metadata) @@ -482,8 +483,6 @@ class IStrategy(ABC, HyperStrategyMixin): message = "No dataframe returned (return statement missing?)." elif 'buy' not in dataframe: message = "Buy column not set." - elif 'sell' not in dataframe: - message = "Sell column not set." elif df_len != len(dataframe): message = message_template.format("length") elif df_close != dataframe["close"].iloc[-1]: @@ -496,7 +495,12 @@ class IStrategy(ABC, HyperStrategyMixin): else: raise StrategyError(message) - def get_signal(self, pair: str, timeframe: str, dataframe: DataFrame) -> Tuple[bool, bool]: + def get_signal( + self, + pair: str, + timeframe: str, + dataframe: DataFrame + ) -> Tuple[bool, bool, Optional[str]]: """ Calculates current signal based based on the buy / sell columns of the dataframe. Used by Bot to get the signal to buy or sell @@ -507,7 +511,7 @@ class IStrategy(ABC, HyperStrategyMixin): """ if not isinstance(dataframe, DataFrame) or dataframe.empty: logger.warning(f'Empty candle (OHLCV) data for pair {pair}') - return False, False + return False, False, None latest_date = dataframe['date'].max() latest = dataframe.loc[dataframe['date'] == latest_date].iloc[-1] @@ -522,9 +526,16 @@ class IStrategy(ABC, HyperStrategyMixin): 'Outdated history for pair %s. Last tick is %s minutes old', pair, int((arrow.utcnow() - latest_date).total_seconds() // 60) ) - return False, False + return False, False, None + + buy = latest[SignalType.BUY.value] == 1 + + sell = False + if SignalType.SELL.value in latest: + sell = latest[SignalType.SELL.value] == 1 + + buy_tag = latest.get(SignalTagType.BUY_TAG.value, None) - (buy, sell) = latest[SignalType.BUY.value] == 1, latest[SignalType.SELL.value] == 1 logger.debug('trigger: %s (pair=%s) buy=%s sell=%s', latest['date'], pair, str(buy), str(sell)) timeframe_seconds = timeframe_to_seconds(timeframe) @@ -532,8 +543,8 @@ class IStrategy(ABC, HyperStrategyMixin): current_time=datetime.now(timezone.utc), timeframe_seconds=timeframe_seconds, buy=buy): - return False, sell - return buy, sell + return False, sell, buy_tag + return buy, sell, buy_tag def ignore_expired_candle(self, latest_date: datetime, current_time: datetime, timeframe_seconds: int, buy: bool): diff --git a/requirements-dev.txt b/requirements-dev.txt index 537936a86..c1f7d6486 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -13,7 +13,7 @@ pytest-asyncio==0.15.1 pytest-cov==2.12.1 pytest-mock==3.6.1 pytest-random-order==1.0.4 -isort==5.9.2 +isort==5.9.3 # Convert jupyter notebooks to markdown documents nbconvert==6.1.0 @@ -21,5 +21,5 @@ nbconvert==6.1.0 # mypy types types-cachetools==0.1.9 types-filelock==0.1.4 -types-requests==2.25.0 +types-requests==2.25.1 types-tabulate==0.1.1 diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 83e23e3ec..d7f22634b 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -2,7 +2,7 @@ -r requirements.txt # Required for hyperopt -scipy==1.7.0 +scipy==1.7.1 scikit-learn==0.24.2 scikit-optimize==0.8.1 filelock==3.0.12 diff --git a/requirements.txt b/requirements.txt index 3be66bff7..6c2ef56c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ numpy==1.21.1 pandas==1.3.1 -ccxt==1.53.72 +ccxt==1.54.24 # Pin cryptography for now due to rust build errors with piwheels cryptography==3.4.7 aiohttp==3.7.4.post0 @@ -31,7 +31,7 @@ python-rapidjson==1.4 sdnotify==0.3.2 # API Server -fastapi==0.67.0 +fastapi==0.68.0 uvicorn==0.14.0 pyjwt==2.1.0 aiofiles==0.7.0 diff --git a/tests/conftest.py b/tests/conftest.py index 2b0aee336..859c34aae 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -182,7 +182,7 @@ def get_patched_worker(mocker, config) -> Worker: return Worker(args=None, config=config) -def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: +def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False, None)) -> None: """ :param mocker: mocker to patch IStrategy class :param value: which value IStrategy.get_signal() must return diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py index 0655b3a0f..7bdc940df 100644 --- a/tests/edge/test_edge.py +++ b/tests/edge/test_edge.py @@ -29,7 +29,6 @@ from tests.optimize import (BTContainer, BTrade, _build_backtest_dataframe, tests_start_time = arrow.get(2018, 10, 3) timeframe_in_minute = 60 -_ohlc = {'date': 0, 'buy': 1, 'open': 2, 'high': 3, 'low': 4, 'close': 5, 'sell': 6, 'volume': 7} # Helpers for this test file diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 02adf01c4..a3ebbe8bd 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -673,7 +673,7 @@ def test_validate_pairs_restricted(default_conf, mocker, caplog): api_mock = MagicMock() type(api_mock).load_markets = MagicMock(return_value={ 'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'}, - 'XRP/BTC': {'quote': 'BTC', 'info': {'IsRestricted': True}}, + 'XRP/BTC': {'quote': 'BTC', 'info': {'prohibitedIn': ['US']}}, 'NEO/BTC': {'quote': 'BTC', 'info': 'TestString'}, # info can also be a string ... }) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) @@ -1056,8 +1056,8 @@ def test_buy_dry_run(default_conf, mocker): default_conf['dry_run'] = True exchange = get_patched_exchange(mocker, default_conf) - order = exchange.buy(pair='ETH/BTC', ordertype='limit', - amount=1, rate=200, time_in_force='gtc') + order = exchange.create_order(pair='ETH/BTC', ordertype='limit', side="buy", + amount=1, rate=200, time_in_force='gtc') assert 'id' in order assert 'dry_run_buy_' in order['id'] @@ -1080,8 +1080,8 @@ def test_buy_prod(default_conf, mocker, exchange_name): mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - order = exchange.buy(pair='ETH/BTC', ordertype=order_type, - amount=1, rate=200, time_in_force=time_in_force) + order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="buy", + amount=1, rate=200, time_in_force=time_in_force) assert 'id' in order assert 'info' in order @@ -1094,9 +1094,10 @@ def test_buy_prod(default_conf, mocker, exchange_name): api_mock.create_order.reset_mock() order_type = 'limit' - order = exchange.buy( + order = exchange.create_order( pair='ETH/BTC', ordertype=order_type, + side="buy", amount=1, rate=200, time_in_force=time_in_force) @@ -1110,32 +1111,32 @@ def test_buy_prod(default_conf, mocker, exchange_name): with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("Not enough funds")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.buy(pair='ETH/BTC', ordertype=order_type, - amount=1, rate=200, time_in_force=time_in_force) + exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="buy", + amount=1, rate=200, time_in_force=time_in_force) with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.buy(pair='ETH/BTC', ordertype='limit', - amount=1, rate=200, time_in_force=time_in_force) + exchange.create_order(pair='ETH/BTC', ordertype='limit', side="buy", + amount=1, rate=200, time_in_force=time_in_force) with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.buy(pair='ETH/BTC', ordertype='market', - amount=1, rate=200, time_in_force=time_in_force) + exchange.create_order(pair='ETH/BTC', ordertype='market', side="buy", + amount=1, rate=200, time_in_force=time_in_force) with pytest.raises(TemporaryError): api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("Network disconnect")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.buy(pair='ETH/BTC', ordertype=order_type, - amount=1, rate=200, time_in_force=time_in_force) + exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="buy", + amount=1, rate=200, time_in_force=time_in_force) with pytest.raises(OperationalException): api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("Unknown error")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.buy(pair='ETH/BTC', ordertype=order_type, - amount=1, rate=200, time_in_force=time_in_force) + exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="buy", + amount=1, rate=200, time_in_force=time_in_force) @pytest.mark.parametrize("exchange_name", EXCHANGES) @@ -1157,8 +1158,8 @@ def test_buy_considers_time_in_force(default_conf, mocker, exchange_name): order_type = 'limit' time_in_force = 'ioc' - order = exchange.buy(pair='ETH/BTC', ordertype=order_type, - amount=1, rate=200, time_in_force=time_in_force) + order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="buy", + amount=1, rate=200, time_in_force=time_in_force) assert 'id' in order assert 'info' in order @@ -1174,8 +1175,8 @@ def test_buy_considers_time_in_force(default_conf, mocker, exchange_name): order_type = 'market' time_in_force = 'ioc' - order = exchange.buy(pair='ETH/BTC', ordertype=order_type, - amount=1, rate=200, time_in_force=time_in_force) + order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="buy", + amount=1, rate=200, time_in_force=time_in_force) assert 'id' in order assert 'info' in order @@ -1193,7 +1194,8 @@ def test_sell_dry_run(default_conf, mocker): default_conf['dry_run'] = True exchange = get_patched_exchange(mocker, default_conf) - order = exchange.sell(pair='ETH/BTC', ordertype='limit', amount=1, rate=200) + order = exchange.create_order(pair='ETH/BTC', ordertype='limit', + side="sell", amount=1, rate=200) assert 'id' in order assert 'dry_run_sell_' in order['id'] @@ -1216,7 +1218,8 @@ def test_sell_prod(default_conf, mocker, exchange_name): mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) + order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, + side="sell", amount=1, rate=200) assert 'id' in order assert 'info' in order @@ -1229,7 +1232,8 @@ def test_sell_prod(default_conf, mocker, exchange_name): api_mock.create_order.reset_mock() order_type = 'limit' - order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) + order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, + side="sell", amount=1, rate=200) assert api_mock.create_order.call_args[0][0] == 'ETH/BTC' assert api_mock.create_order.call_args[0][1] == order_type assert api_mock.create_order.call_args[0][2] == 'sell' @@ -1240,28 +1244,28 @@ def test_sell_prod(default_conf, mocker, exchange_name): with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) + exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="sell", amount=1, rate=200) with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.sell(pair='ETH/BTC', ordertype='limit', amount=1, rate=200) + exchange.create_order(pair='ETH/BTC', ordertype='limit', side="sell", amount=1, rate=200) # Market orders don't require price, so the behaviour is slightly different with pytest.raises(DependencyException): api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.sell(pair='ETH/BTC', ordertype='market', amount=1, rate=200) + exchange.create_order(pair='ETH/BTC', ordertype='market', side="sell", amount=1, rate=200) with pytest.raises(TemporaryError): api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No Connection")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) + exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="sell", amount=1, rate=200) with pytest.raises(OperationalException): api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef")) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) - exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) + exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="sell", amount=1, rate=200) @pytest.mark.parametrize("exchange_name", EXCHANGES) @@ -1283,8 +1287,8 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): order_type = 'limit' time_in_force = 'ioc' - order = exchange.sell(pair='ETH/BTC', ordertype=order_type, - amount=1, rate=200, time_in_force=time_in_force) + order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="sell", + amount=1, rate=200, time_in_force=time_in_force) assert 'id' in order assert 'info' in order @@ -1299,8 +1303,8 @@ def test_sell_considers_time_in_force(default_conf, mocker, exchange_name): order_type = 'market' time_in_force = 'ioc' - order = exchange.sell(pair='ETH/BTC', ordertype=order_type, - amount=1, rate=200, time_in_force=time_in_force) + order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="sell", + amount=1, rate=200, time_in_force=time_in_force) assert 'id' in order assert 'info' in order @@ -2186,7 +2190,7 @@ def test_cancel_order_dry_run(default_conf, mocker, exchange_name): assert exchange.cancel_order(order_id='123', pair='TKN/BTC') == {} assert exchange.cancel_stoploss_order(order_id='123', pair='TKN/BTC') == {} - order = exchange.buy('ETH/BTC', 'limit', 5, 0.55, 'gtc') + order = exchange.create_order('ETH/BTC', 'limit', "buy", 5, 0.55, 'gtc') cancel_order = exchange.cancel_order(order_id=order['id'], pair='ETH/BTC') assert order['id'] == cancel_order['id'] diff --git a/tests/exchange/test_kraken.py b/tests/exchange/test_kraken.py index ed22cde92..eb79dfc10 100644 --- a/tests/exchange/test_kraken.py +++ b/tests/exchange/test_kraken.py @@ -31,8 +31,8 @@ def test_buy_kraken_trading_agreement(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") - order = exchange.buy(pair='ETH/BTC', ordertype=order_type, - amount=1, rate=200, time_in_force=time_in_force) + order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="buy", + amount=1, rate=200, time_in_force=time_in_force) assert 'id' in order assert 'info' in order @@ -63,7 +63,8 @@ def test_sell_kraken_trading_agreement(default_conf, mocker): mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken") - order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200) + order = exchange.create_order(pair='ETH/BTC', ordertype=order_type, + side="sell", amount=1, rate=200) assert 'id' in order assert 'info' in order diff --git a/tests/optimize/__init__.py b/tests/optimize/__init__.py index ca91019e6..f29d8d585 100644 --- a/tests/optimize/__init__.py +++ b/tests/optimize/__init__.py @@ -18,6 +18,7 @@ class BTrade(NamedTuple): sell_reason: SellType open_tick: int close_tick: int + buy_tag: Optional[str] = None class BTContainer(NamedTuple): @@ -44,6 +45,7 @@ def _get_frame_time_from_offset(offset): def _build_backtest_dataframe(data): columns = ['date', 'open', 'high', 'low', 'close', 'volume', 'buy', 'sell'] + columns = columns + ['buy_tag'] if len(data[0]) == 9 else columns frame = DataFrame.from_records(data, columns=columns) frame['date'] = frame['date'].apply(_get_frame_time_from_offset) diff --git a/tests/optimize/test_backtest_detail.py b/tests/optimize/test_backtest_detail.py index 0bf197739..e5c037f3e 100644 --- a/tests/optimize/test_backtest_detail.py +++ b/tests/optimize/test_backtest_detail.py @@ -516,6 +516,26 @@ tc32 = BTContainer(data=[ trades=[BTrade(sell_reason=SellType.TRAILING_STOP_LOSS, open_tick=1, close_tick=1)] ) +# Test 33: trailing_stop should be triggered immediately on trade open candle. +# stop-loss: 1%, ROI: 10% (should not apply) +tc33 = BTContainer(data=[ + # D O H L C V B S BT + [0, 5000, 5050, 4950, 5000, 6172, 1, 0, 'buy_signal_01'], + [1, 5000, 5500, 5000, 4900, 6172, 0, 0, None], # enter trade (signal on last candle) and stop + [2, 4900, 5250, 4500, 5100, 6172, 0, 0, None], + [3, 5100, 5100, 4650, 4750, 6172, 0, 0, None], + [4, 4750, 4950, 4350, 4750, 6172, 0, 0, None]], + stop_loss=-0.01, roi={"0": 0.10}, profit_perc=-0.01, trailing_stop=True, + trailing_only_offset_is_reached=True, trailing_stop_positive_offset=0.02, + trailing_stop_positive=0.01, use_custom_stoploss=True, + trades=[BTrade( + sell_reason=SellType.TRAILING_STOP_LOSS, + open_tick=1, + close_tick=1, + buy_tag='buy_signal_01' + )] +) + TESTS = [ tc0, tc1, @@ -550,6 +570,7 @@ TESTS = [ tc30, tc31, tc32, + tc33, ] @@ -575,6 +596,7 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: frame = _build_backtest_dataframe(data.data) backtesting = Backtesting(default_conf) backtesting._set_strategy(backtesting.strategylist[0]) + backtesting.required_startup = 0 backtesting.strategy.advise_buy = lambda a, m: frame backtesting.strategy.advise_sell = lambda a, m: frame backtesting.strategy.use_custom_stoploss = data.use_custom_stoploss @@ -598,5 +620,6 @@ def test_backtest_results(default_conf, fee, mocker, caplog, data) -> None: for c, trade in enumerate(data.trades): res = results.iloc[c] assert res.sell_reason == trade.sell_reason.value + assert res.buy_tag == trade.buy_tag assert res.open_date == _get_frame_time_from_offset(trade.open_tick) assert res.close_date == _get_frame_time_from_offset(trade.close_tick) diff --git a/tests/optimize/test_backtesting.py b/tests/optimize/test_backtesting.py index 77b9e23fa..deaaf9f2f 100644 --- a/tests/optimize/test_backtesting.py +++ b/tests/optimize/test_backtesting.py @@ -496,6 +496,7 @@ def test_backtest__enter_trade(default_conf, fee, mocker) -> None: 0, # Sell 0.00099, # Low 0.0012, # High + '', # Buy Signal Name ] trade = backtesting._enter_trade(pair, row=row) assert isinstance(trade, LocalTrade) @@ -583,6 +584,7 @@ def test_backtest_one(default_conf, fee, mocker, testdatadir) -> None: 'min_rate': [0.1038, 0.10302485], 'max_rate': [0.10501, 0.1038888], 'is_open': [False, False], + 'buy_tag': [None, None], }) pd.testing.assert_frame_equal(results, expected) data_pair = processed[pair] @@ -727,6 +729,7 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): pair='UNITTEST/BTC', datadir=testdatadir) default_conf['timeframe'] = '1m' backtesting = Backtesting(default_conf) + backtesting.required_startup = 0 backtesting._set_strategy(backtesting.strategylist[0]) backtesting.strategy.advise_buy = _trend_alternate # Override backtesting.strategy.advise_sell = _trend_alternate # Override @@ -736,6 +739,9 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker, testdatadir): # 100 buys signals results = result['results'] assert len(results) == 100 + # Cached data should be 200 (no change since required_startup is 0) + assert len(backtesting.dataprovider.get_analyzed_dataframe('UNITTEST/BTC', '1m')[0]) == 200 + # One trade was force-closed at the end assert len(results.loc[results['is_open']]) == 0 @@ -791,6 +797,10 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir) # make sure we don't have trades with more than configured max_open_trades assert len(evaluate_result_multi(results['results'], '5m', 3)) == 0 + # Cached data correctly removed amounts + removed_candles = len(data[pair]) - 1 - backtesting.strategy.startup_candle_count + assert len(backtesting.dataprovider.get_analyzed_dataframe(pair, '5m')[0]) == removed_candles + backtest_conf = { 'processed': processed, 'start_date': min_date, @@ -857,7 +867,7 @@ def test_backtest_start_multi_strat(default_conf, mocker, caplog, testdatadir): 'locks': [], 'rejected_signals': 20, 'final_balance': 1000, - }) + }) mocker.patch('freqtrade.plugins.pairlistmanager.PairListManager.whitelist', PropertyMock(return_value=['UNITTEST/BTC'])) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) diff --git a/tests/plugins/test_pairlist.py b/tests/plugins/test_pairlist.py index b15126a33..5f0701a22 100644 --- a/tests/plugins/test_pairlist.py +++ b/tests/plugins/test_pairlist.py @@ -427,6 +427,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf): {"method": "RangeStabilityFilter", "lookback_days": 10, "min_rate_of_change": 0.01, "refresh_period": 1440}], "BTC", ['ETH/BTC', 'TKN/BTC', 'HOT/BTC']), + ([{"method": "StaticPairList"}, + {"method": "RangeStabilityFilter", "lookback_days": 10, + "max_rate_of_change": 0.01, "refresh_period": 1440}], + "BTC", []), # All removed because of max_rate_of_change being 0.017 ([{"method": "StaticPairList"}, {"method": "VolatilityFilter", "lookback_days": 3, "min_volatility": 0.002, "max_volatility": 0.004, "refresh_period": 1440}], @@ -874,15 +878,16 @@ def test_rangestabilityfilter_checks(mocker, default_conf, markets, tickers): get_patched_freqtradebot(mocker, default_conf) -@pytest.mark.parametrize('min_rate_of_change,expected_length', [ - (0.01, 5), - (0.05, 0), # Setting rate_of_change to 5% removes all pairs from the whitelist. +@pytest.mark.parametrize('min_rate_of_change,max_rate_of_change,expected_length', [ + (0.01, 0.99, 5), + (0.05, 0.0, 0), # Setting min rate_of_change to 5% removes all pairs from the whitelist. ]) def test_rangestabilityfilter_caching(mocker, markets, default_conf, tickers, ohlcv_history, - min_rate_of_change, expected_length): + min_rate_of_change, max_rate_of_change, expected_length): default_conf['pairlists'] = [{'method': 'VolumePairList', 'number_assets': 10}, {'method': 'RangeStabilityFilter', 'lookback_days': 2, - 'min_rate_of_change': min_rate_of_change}] + 'min_rate_of_change': min_rate_of_change, + "max_rate_of_change": max_rate_of_change}] mocker.patch.multiple('freqtrade.exchange.Exchange', markets=PropertyMock(return_value=markets), @@ -984,11 +989,18 @@ def test_spreadfilter_invalid_data(mocker, default_conf, markets, tickers, caplo None, "PriceFilter requires max_value to be >= 0" ), # OperationalException expected - ({"method": "RangeStabilityFilter", "lookback_days": 10, "min_rate_of_change": 0.01}, + ({"method": "RangeStabilityFilter", "lookback_days": 10, + "min_rate_of_change": 0.01}, "[{'RangeStabilityFilter': 'RangeStabilityFilter - Filtering pairs with rate of change below " "0.01 over the last days.'}]", None ), + ({"method": "RangeStabilityFilter", "lookback_days": 10, + "min_rate_of_change": 0.01, "max_rate_of_change": 0.99}, + "[{'RangeStabilityFilter': 'RangeStabilityFilter - Filtering pairs with rate of change below " + "0.01 and above 0.99 over the last days.'}]", + None + ), ]) def test_pricefilter_desc(mocker, whitelist_conf, markets, pairlistconfig, desc_expected, exception_expected): diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index 323a647c1..db7ad484c 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -35,7 +35,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) rpc = RPC(freqtradebot) freqtradebot.state = State.RUNNING @@ -69,6 +69,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'min_rate': ANY, 'max_rate': ANY, 'strategy': ANY, + 'buy_tag': ANY, 'timeframe': 5, 'open_order_id': ANY, 'close_date': None, @@ -139,6 +140,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'min_rate': ANY, 'max_rate': ANY, 'strategy': ANY, + 'buy_tag': ANY, 'timeframe': ANY, 'open_order_id': ANY, 'close_date': None, @@ -198,7 +200,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None: ) del default_conf['fiat_display_currency'] freqtradebot = get_patched_freqtradebot(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) rpc = RPC(freqtradebot) freqtradebot.state = State.RUNNING @@ -245,7 +247,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) stake_currency = default_conf['stake_currency'] fiat_display_currency = default_conf['fiat_display_currency'] @@ -377,7 +379,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) stake_currency = default_conf['stake_currency'] fiat_display_currency = default_conf['fiat_display_currency'] @@ -465,7 +467,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) stake_currency = default_conf['stake_currency'] fiat_display_currency = default_conf['fiat_display_currency'] @@ -532,7 +534,7 @@ def test_rpc_balance_handle_error(default_conf, mocker): ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) rpc = RPC(freqtradebot) rpc._fiat_converter = CryptoToFiatConverter() with pytest.raises(RPCException, match="Error getting current tickers."): @@ -573,7 +575,7 @@ def test_rpc_balance_handle(default_conf, mocker, tickers): ) default_conf['dry_run'] = False freqtradebot = get_patched_freqtradebot(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) rpc = RPC(freqtradebot) rpc._fiat_converter = CryptoToFiatConverter() @@ -618,7 +620,7 @@ def test_rpc_start(mocker, default_conf) -> None: ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) rpc = RPC(freqtradebot) freqtradebot.state = State.STOPPED @@ -639,7 +641,7 @@ def test_rpc_stop(mocker, default_conf) -> None: ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) rpc = RPC(freqtradebot) freqtradebot.state = State.RUNNING @@ -661,7 +663,7 @@ def test_rpc_stopbuy(mocker, default_conf) -> None: ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) rpc = RPC(freqtradebot) freqtradebot.state = State.RUNNING @@ -693,7 +695,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None: mocker.patch('freqtrade.wallets.Wallets.get_free', return_value=1000) freqtradebot = get_patched_freqtradebot(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) rpc = RPC(freqtradebot) freqtradebot.state = State.STOPPED @@ -811,7 +813,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) rpc = RPC(freqtradebot) # Create some test data @@ -844,7 +846,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee) -> None: ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) rpc = RPC(freqtradebot) counts = rpc._rpc_count() @@ -865,11 +867,11 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) -> get_balances=MagicMock(return_value=ticker), fetch_ticker=ticker, get_fee=fee, - buy=buy_mm + create_order=buy_mm ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) rpc = RPC(freqtradebot) pair = 'ETH/BTC' trade = rpc._rpc_forcebuy(pair, None) @@ -895,7 +897,7 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) -> # Test not buying freqtradebot = get_patched_freqtradebot(mocker, default_conf) freqtradebot.config['stake_amount'] = 0 - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) rpc = RPC(freqtradebot) pair = 'TKN/BTC' trade = rpc._rpc_forcebuy(pair, None) @@ -908,7 +910,7 @@ def test_rpcforcebuy_stopped(mocker, default_conf) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) freqtradebot = get_patched_freqtradebot(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) rpc = RPC(freqtradebot) pair = 'ETH/BTC' with pytest.raises(RPCException, match=r'trader is not running'): @@ -919,7 +921,7 @@ def test_rpcforcebuy_disabled(mocker, default_conf) -> None: mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) freqtradebot = get_patched_freqtradebot(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) rpc = RPC(freqtradebot) pair = 'ETH/BTC' with pytest.raises(RPCException, match=r'Forcebuy not enabled.'): diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index 921d8160d..68f23e0fd 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -442,7 +442,7 @@ def test_api_balance(botclient, mocker, rpc_balance): def test_api_count(botclient, mocker, ticker, fee, markets): ftbot, client = botclient - patch_get_signal(ftbot, (True, False)) + patch_get_signal(ftbot, (True, False, None)) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), @@ -504,7 +504,7 @@ def test_api_locks(botclient): def test_api_show_config(botclient, mocker): ftbot, client = botclient - patch_get_signal(ftbot, (True, False)) + patch_get_signal(ftbot, (True, False, None)) rc = client_get(client, f"{BASE_URI}/show_config") assert_response(rc) @@ -522,7 +522,7 @@ def test_api_show_config(botclient, mocker): def test_api_daily(botclient, mocker, ticker, fee, markets): ftbot, client = botclient - patch_get_signal(ftbot, (True, False)) + patch_get_signal(ftbot, (True, False, None)) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), @@ -540,7 +540,7 @@ def test_api_daily(botclient, mocker, ticker, fee, markets): def test_api_trades(botclient, mocker, fee, markets): ftbot, client = botclient - patch_get_signal(ftbot, (True, False)) + patch_get_signal(ftbot, (True, False, None)) mocker.patch.multiple( 'freqtrade.exchange.Exchange', markets=PropertyMock(return_value=markets) @@ -568,7 +568,7 @@ def test_api_trades(botclient, mocker, fee, markets): def test_api_trade_single(botclient, mocker, fee, ticker, markets): ftbot, client = botclient - patch_get_signal(ftbot, (True, False)) + patch_get_signal(ftbot, (True, False, None)) mocker.patch.multiple( 'freqtrade.exchange.Exchange', markets=PropertyMock(return_value=markets), @@ -588,7 +588,7 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets): def test_api_delete_trade(botclient, mocker, fee, markets): ftbot, client = botclient - patch_get_signal(ftbot, (True, False)) + patch_get_signal(ftbot, (True, False, None)) stoploss_mock = MagicMock() cancel_mock = MagicMock() mocker.patch.multiple( @@ -656,13 +656,13 @@ def test_api_logs(botclient): # Help debugging random test failure print(f"rc={rc.json()}") print(f"rc1={rc1.json()}") - assert rc1.json()['log_count'] == 5 + assert rc1.json()['log_count'] > 2 assert len(rc1.json()['logs']) == rc1.json()['log_count'] def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): ftbot, client = botclient - patch_get_signal(ftbot, (True, False)) + patch_get_signal(ftbot, (True, False, None)) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), @@ -678,7 +678,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): @pytest.mark.usefixtures("init_persistence") def test_api_profit(botclient, mocker, ticker, fee, markets): ftbot, client = botclient - patch_get_signal(ftbot, (True, False)) + patch_get_signal(ftbot, (True, False, None)) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), @@ -729,7 +729,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets): @pytest.mark.usefixtures("init_persistence") def test_api_stats(botclient, mocker, ticker, fee, markets,): ftbot, client = botclient - patch_get_signal(ftbot, (True, False)) + patch_get_signal(ftbot, (True, False, None)) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), @@ -757,7 +757,7 @@ def test_api_stats(botclient, mocker, ticker, fee, markets,): def test_api_performance(botclient, fee): ftbot, client = botclient - patch_get_signal(ftbot, (True, False)) + patch_get_signal(ftbot, (True, False, None)) trade = Trade( pair='LTC/ETH', @@ -803,7 +803,7 @@ def test_api_performance(botclient, fee): def test_api_status(botclient, mocker, ticker, fee, markets): ftbot, client = botclient - patch_get_signal(ftbot, (True, False)) + patch_get_signal(ftbot, (True, False, None)) mocker.patch.multiple( 'freqtrade.exchange.Exchange', get_balances=MagicMock(return_value=ticker), @@ -875,6 +875,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'sell_reason': None, 'sell_order_status': None, 'strategy': 'DefaultStrategy', + 'buy_tag': None, 'timeframe': 5, 'exchange': 'binance', } @@ -1029,6 +1030,7 @@ def test_api_forcebuy(botclient, mocker, fee): 'sell_reason': None, 'sell_order_status': None, 'strategy': 'DefaultStrategy', + 'buy_tag': None, 'timeframe': 5, 'exchange': 'binance', } @@ -1044,7 +1046,7 @@ def test_api_forcesell(botclient, mocker, ticker, fee, markets): markets=PropertyMock(return_value=markets), _is_dry_limit_order_filled=MagicMock(return_value=False), ) - patch_get_signal(ftbot, (True, False)) + patch_get_signal(ftbot, (True, False, None)) rc = client_post(client, f"{BASE_URI}/forcesell", data='{"tradeid": "1"}') @@ -1185,8 +1187,10 @@ def test_api_plot_config(botclient): assert_response(rc) assert rc.json() == {} - ftbot.strategy.plot_config = {'main_plot': {'sma': {}}, - 'subplots': {'RSI': {'rsi': {'color': 'red'}}}} + ftbot.strategy.plot_config = { + 'main_plot': {'sma': {}}, + 'subplots': {'RSI': {'rsi': {'color': 'red'}}} + } rc = client_get(client, f"{BASE_URI}/plot_config") assert_response(rc) assert rc.json() == ftbot.strategy.plot_config diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index cccc78117..b678c3363 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -119,7 +119,7 @@ def test_authorized_only(default_conf, mocker, caplog, update) -> None: rpc = RPC(bot) dummy = DummyCls(rpc, default_conf) - patch_get_signal(bot, (True, False)) + patch_get_signal(bot, (True, False, None)) dummy.dummy_handler(update=update, context=MagicMock()) assert dummy.state['called'] is True assert log_has('Executing handler: dummy_handler for chat_id: 0', caplog) @@ -139,7 +139,7 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: rpc = RPC(bot) dummy = DummyCls(rpc, default_conf) - patch_get_signal(bot, (True, False)) + patch_get_signal(bot, (True, False, None)) dummy.dummy_handler(update=update, context=MagicMock()) assert dummy.state['called'] is False assert not log_has('Executing handler: dummy_handler for chat_id: 3735928559', caplog) @@ -155,7 +155,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog, update) -> None bot = FreqtradeBot(default_conf) rpc = RPC(bot) dummy = DummyCls(rpc, default_conf) - patch_get_signal(bot, (True, False)) + patch_get_signal(bot, (True, False, None)) dummy.dummy_exception(update=update, context=MagicMock()) assert dummy.state['called'] is False @@ -185,6 +185,7 @@ def test_telegram_status(default_conf, update, mocker) -> None: 'current_rate': 1.098e-05, 'amount': 90.99181074, 'stake_amount': 90.99181074, + 'buy_tag': None, 'close_profit_pct': None, 'profit': -0.0059, 'profit_pct': -0.59, @@ -228,7 +229,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) freqtradebot.state = State.STOPPED # Status is also enabled when stopped @@ -285,7 +286,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) freqtradebot.state = State.STOPPED # Status table is also enabled when stopped @@ -329,7 +330,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) # Create some test data freqtradebot.enter_positions() @@ -400,7 +401,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: ) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) # Try invalid data msg_mock.reset_mock() @@ -432,7 +433,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, ) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) telegram._profit(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -487,7 +488,7 @@ def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee, get_fee=fee, ) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) telegram._stats(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -513,7 +514,7 @@ def test_telegram_balance_handle(default_conf, update, mocker, rpc_balance, tick side_effect=lambda a, b: f"{a}/{b}") telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] @@ -536,7 +537,7 @@ def test_balance_handle_empty_response(default_conf, update, mocker) -> None: mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={}) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) freqtradebot.config['dry_run'] = False telegram._balance(update=update, context=MagicMock()) @@ -549,7 +550,7 @@ def test_balance_handle_empty_response_dry(default_conf, update, mocker) -> None mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={}) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) telegram._balance(update=update, context=MagicMock()) result = msg_mock.call_args_list[0][0][0] @@ -578,7 +579,7 @@ def test_balance_handle_too_large_response(default_conf, update, mocker) -> None }) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) telegram._balance(update=update, context=MagicMock()) assert msg_mock.call_count > 1 @@ -677,7 +678,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee, freqtradebot = FreqtradeBot(default_conf) rpc = RPC(freqtradebot) telegram = Telegram(rpc, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) # Create some test data freqtradebot.enter_positions() @@ -736,7 +737,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee, freqtradebot = FreqtradeBot(default_conf) rpc = RPC(freqtradebot) telegram = Telegram(rpc, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) # Create some test data freqtradebot.enter_positions() @@ -797,7 +798,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None freqtradebot = FreqtradeBot(default_conf) rpc = RPC(freqtradebot) telegram = Telegram(rpc, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) # Create some test data freqtradebot.enter_positions() @@ -838,7 +839,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: return_value=15000.0) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) # Trader is not running freqtradebot.state = State.STOPPED @@ -876,7 +877,7 @@ def test_forcebuy_handle(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.RPC._rpc_forcebuy', fbuy_mock) telegram, freqtradebot, _ = get_telegram_testobject(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) # /forcebuy ETH/BTC context = MagicMock() @@ -905,7 +906,7 @@ def test_forcebuy_handle_exception(default_conf, update, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) update.message.text = '/forcebuy ETH/Nonepair' telegram._forcebuy(update=update, context=MagicMock()) @@ -922,7 +923,7 @@ def test_forcebuy_no_pair(default_conf, update, mocker) -> None: telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) context = MagicMock() context.args = [] @@ -950,7 +951,7 @@ def test_performance_handle(default_conf, update, ticker, fee, get_fee=fee, ) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) # Create some test data freqtradebot.enter_positions() @@ -978,7 +979,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None: get_fee=fee, ) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) freqtradebot.state = State.STOPPED telegram._count(update=update, context=MagicMock()) @@ -1007,7 +1008,7 @@ def test_telegram_lock_handle(default_conf, update, ticker, fee, mocker) -> None get_fee=fee, ) telegram, freqtradebot, msg_mock = get_telegram_testobject(mocker, default_conf) - patch_get_signal(freqtradebot, (True, False)) + patch_get_signal(freqtradebot, (True, False, None)) telegram._locks(update=update, context=MagicMock()) assert msg_mock.call_count == 1 assert 'No active locks.' in msg_mock.call_args_list[0][0][0] @@ -1253,6 +1254,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None: msg = { 'type': RPCMessageType.BUY, 'trade_id': 1, + 'buy_tag': 'buy_signal_01', 'exchange': 'Binance', 'pair': 'ETH/BTC', 'limit': 1.099e-05, @@ -1270,6 +1272,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} *Binance:* Buying ETH/BTC (#1)\n' \ + '*Buy Tag:* `buy_signal_01`\n' \ '*Amount:* `1333.33333333`\n' \ '*Open Rate:* `0.00001099`\n' \ '*Current Rate:* `0.00001099`\n' \ @@ -1297,6 +1300,7 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None: telegram.send_msg({ 'type': RPCMessageType.BUY_CANCEL, + 'buy_tag': 'buy_signal_01', 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -1314,6 +1318,7 @@ def test_send_msg_buy_fill_notification(default_conf, mocker) -> None: telegram.send_msg({ 'type': RPCMessageType.BUY_FILL, + 'buy_tag': 'buy_signal_01', 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/USDT', @@ -1498,6 +1503,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: telegram.send_msg({ 'type': RPCMessageType.BUY, + 'buy_tag': 'buy_signal_01', 'trade_id': 1, 'exchange': 'Binance', 'pair': 'ETH/BTC', @@ -1512,6 +1518,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: 'open_date': arrow.utcnow().shift(hours=-1) }) assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Binance:* Buying ETH/BTC (#1)\n' + '*Buy Tag:* `buy_signal_01`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00001099`\n' '*Current Rate:* `0.00001099`\n' diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py index 62a638ed3..d8c87506c 100644 --- a/tests/strategy/test_interface.py +++ b/tests/strategy/test_interface.py @@ -38,15 +38,20 @@ def test_returns_latest_signal(mocker, default_conf, ohlcv_history): mocked_history['buy'] = 0 mocked_history.loc[1, 'sell'] = 1 - assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, True) + assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, True, None) mocked_history.loc[1, 'sell'] = 0 mocked_history.loc[1, 'buy'] = 1 - assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False) + assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False, None) mocked_history.loc[1, 'sell'] = 0 mocked_history.loc[1, 'buy'] = 0 - assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, False) + assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (False, False, None) + mocked_history.loc[1, 'sell'] = 0 + mocked_history.loc[1, 'buy'] = 1 + mocked_history.loc[1, 'buy_tag'] = 'buy_signal_01' + + assert _STRATEGY.get_signal('ETH/BTC', '5m', mocked_history) == (True, False, 'buy_signal_01') def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history): @@ -63,15 +68,21 @@ def test_analyze_pair_empty(default_conf, mocker, caplog, ohlcv_history): def test_get_signal_empty(default_conf, mocker, caplog): - assert (False, False) == _STRATEGY.get_signal('foo', default_conf['timeframe'], DataFrame()) + assert (False, False, None) == _STRATEGY.get_signal( + 'foo', default_conf['timeframe'], DataFrame() + ) assert log_has('Empty candle (OHLCV) data for pair foo', caplog) caplog.clear() - assert (False, False) == _STRATEGY.get_signal('bar', default_conf['timeframe'], None) + assert (False, False, None) == _STRATEGY.get_signal('bar', default_conf['timeframe'], None) assert log_has('Empty candle (OHLCV) data for pair bar', caplog) caplog.clear() - assert (False, False) == _STRATEGY.get_signal('baz', default_conf['timeframe'], DataFrame([])) + assert (False, False, None) == _STRATEGY.get_signal( + 'baz', + default_conf['timeframe'], + DataFrame([]) + ) assert log_has('Empty candle (OHLCV) data for pair baz', caplog) @@ -107,10 +118,35 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog, ohlcv_history): caplog.set_level(logging.INFO) mocker.patch.object(_STRATEGY, 'assert_df') - assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['timeframe'], mocked_history) + assert (False, False, None) == _STRATEGY.get_signal( + 'xyz', + default_conf['timeframe'], + mocked_history + ) assert log_has('Outdated history for pair xyz. Last tick is 16 minutes old', caplog) +def test_get_signal_no_sell_column(default_conf, mocker, caplog, ohlcv_history): + # default_conf defines a 5m interval. we check interval * 2 + 5m + # this is necessary as the last candle is removed (partial candles) by default + ohlcv_history.loc[1, 'date'] = arrow.utcnow() + # Take a copy to correctly modify the call + mocked_history = ohlcv_history.copy() + # Intentionally don't set sell column + # mocked_history['sell'] = 0 + mocked_history['buy'] = 0 + mocked_history.loc[1, 'buy'] = 1 + + caplog.set_level(logging.INFO) + mocker.patch.object(_STRATEGY, 'assert_df') + + assert (True, False, None) == _STRATEGY.get_signal( + 'xyz', + default_conf['timeframe'], + mocked_history + ) + + def test_ignore_expired_candle(default_conf): default_conf.update({'strategy': 'DefaultStrategy'}) strategy = StrategyResolver.load_strategy(default_conf) @@ -182,10 +218,6 @@ def test_assert_df(ohlcv_history, caplog): match="Buy column not set"): _STRATEGY.assert_df(ohlcv_history.drop('buy', axis=1), len(ohlcv_history), ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date']) - with pytest.raises(StrategyError, - match="Sell column not set"): - _STRATEGY.assert_df(ohlcv_history.drop('sell', axis=1), len(ohlcv_history), - ohlcv_history.loc[df_len, 'close'], ohlcv_history.loc[0, 'date']) _STRATEGY.disable_dataframe_checks = True caplog.clear() diff --git a/tests/test_configuration.py b/tests/test_configuration.py index 34db892b2..7012333e9 100644 --- a/tests/test_configuration.py +++ b/tests/test_configuration.py @@ -18,8 +18,9 @@ from freqtrade.configuration.deprecated_settings import (check_conflicting_setti process_deprecated_setting, process_removed_setting, process_temporary_deprecated_settings) +from freqtrade.configuration.environment_vars import flat_vars_to_nested_dict from freqtrade.configuration.load_config import load_config_file, load_file, log_config_error_range -from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL +from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL, ENV_VAR_PREFIX from freqtrade.enums import RunMode from freqtrade.exceptions import OperationalException from freqtrade.loggers import _set_loggers, setup_logging, setup_logging_pre @@ -1349,3 +1350,35 @@ def test_process_deprecated_ticker_interval(mocker, default_conf, caplog): with pytest.raises(OperationalException, match=r"Both 'timeframe' and 'ticker_interval' detected."): process_temporary_deprecated_settings(config) + + +def test_flat_vars_to_nested_dict(caplog): + + test_args = { + 'FREQTRADE__EXCHANGE__SOME_SETTING': 'true', + 'FREQTRADE__EXCHANGE__SOME_FALSE_SETTING': 'false', + 'FREQTRADE__EXCHANGE__CONFIG__whatever': 'sometime', + 'FREQTRADE__ASK_STRATEGY__PRICE_SIDE': 'bid', + 'FREQTRADE__ASK_STRATEGY__cccc': '500', + 'FREQTRADE__STAKE_AMOUNT': '200.05', + 'NOT_RELEVANT': '200.0', # Will be ignored + } + expected = { + 'stake_amount': 200.05, + 'ask_strategy': { + 'price_side': 'bid', + 'cccc': 500, + }, + 'exchange': { + 'config': { + 'whatever': 'sometime', + }, + 'some_setting': True, + 'some_false_setting': False, + } + } + res = flat_vars_to_nested_dict(test_args, ENV_VAR_PREFIX) + assert res == expected + + assert log_has("Loading variable 'FREQTRADE__EXCHANGE__SOME_SETTING'", caplog) + assert not log_has("Loading variable 'NOT_RELEVANT'", caplog) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 8742228ac..7c37bb269 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -169,7 +169,7 @@ def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_b mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(return_value=limit_buy_order_open), get_fee=fee ) default_conf['dry_run_wallet'] = wallet @@ -384,7 +384,7 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order_open, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=buy_mock, + create_order=buy_mock, get_fee=fee, ) default_conf['stake_amount'] = 0.0005 @@ -404,7 +404,7 @@ def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_ord mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=buy_mock, + create_order=buy_mock, get_fee=fee, ) @@ -425,7 +425,7 @@ def test_create_trade_zero_stake_amount(default_conf, ticker, limit_buy_order_op mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=buy_mock, + create_order=buy_mock, get_fee=fee, ) @@ -444,7 +444,7 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order_open, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) default_conf['max_open_trades'] = 0 @@ -464,7 +464,7 @@ def test_enter_positions_no_pairs_left(default_conf, ticker, limit_buy_order_ope mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) @@ -487,7 +487,7 @@ def test_enter_positions_no_pairs_in_whitelist(default_conf, ticker, limit_buy_o mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + create_order=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) default_conf['exchange']['pair_whitelist'] = [] @@ -507,7 +507,7 @@ def test_enter_positions_global_pairlock(default_conf, ticker, limit_buy_order, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + create_order=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -536,7 +536,7 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None: ) default_conf['stake_amount'] = 10 freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade, value=(False, False)) + patch_get_signal(freqtrade, value=(False, False, None)) Trade.query = MagicMock() Trade.query.filter = MagicMock() @@ -556,7 +556,7 @@ def test_create_trades_multiple_trades(default_conf, ticker, fee, mocker, limit_ mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -577,7 +577,7 @@ def test_create_trades_preopen(default_conf, ticker, fee, mocker, limit_buy_orde mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -606,7 +606,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, limit_buy mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(return_value=limit_buy_order_open), fetch_order=MagicMock(return_value=limit_buy_order), get_fee=fee, ) @@ -641,7 +641,7 @@ def test_process_exchange_failures(default_conf, ticker, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(side_effect=TemporaryError) + create_order=MagicMock(side_effect=TemporaryError) ) sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None) @@ -658,7 +658,7 @@ def test_process_operational_exception(default_conf, ticker, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(side_effect=OperationalException) + create_order=MagicMock(side_effect=OperationalException) ) worker = Worker(args=None, config=default_conf) patch_get_signal(worker.freqtrade) @@ -676,7 +676,7 @@ def test_process_trade_handling(default_conf, ticker, limit_buy_order_open, fee, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(return_value=limit_buy_order_open), fetch_order=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) @@ -703,7 +703,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + create_order=MagicMock(return_value={'id': limit_buy_order['id']}), fetch_order=MagicMock(return_value=limit_buy_order), get_fee=fee, ) @@ -753,11 +753,14 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None: mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(side_effect=TemporaryError), + create_order=MagicMock(side_effect=TemporaryError), refresh_latest_ohlcv=refresh_mock, ) inf_pairs = MagicMock(return_value=[("BTC/ETH", '1m'), ("ETH/USDT", "1h")]) - mocker.patch('freqtrade.strategy.interface.IStrategy.get_signal', return_value=(False, False)) + mocker.patch( + 'freqtrade.strategy.interface.IStrategy.get_signal', + return_value=(False, False, '') + ) mocker.patch('time.sleep', return_value=None) freqtrade = FreqtradeBot(default_conf) @@ -790,7 +793,7 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order 'ask': 0.00001173, 'last': 0.00001172 }), - buy=buy_mm, + create_order=buy_mm, get_min_pair_stake_amount=MagicMock(return_value=1), get_fee=fee, ) @@ -839,7 +842,8 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order limit_buy_order['cost'] = 100 limit_buy_order['id'] = '444' - mocker.patch('freqtrade.exchange.Exchange.buy', MagicMock(return_value=limit_buy_order)) + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=limit_buy_order)) assert freqtrade.execute_buy(pair, stake_amount) trade = Trade.query.all()[2] assert trade @@ -855,7 +859,8 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order limit_buy_order['price'] = 0.5 limit_buy_order['cost'] = 40.495905365 limit_buy_order['id'] = '555' - mocker.patch('freqtrade.exchange.Exchange.buy', MagicMock(return_value=limit_buy_order)) + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=limit_buy_order)) assert freqtrade.execute_buy(pair, stake_amount) trade = Trade.query.all()[3] assert trade @@ -889,7 +894,8 @@ def test_execute_buy(mocker, default_conf, fee, limit_buy_order, limit_buy_order limit_buy_order['price'] = 0.5 limit_buy_order['cost'] = 0.0 limit_buy_order['id'] = '66' - mocker.patch('freqtrade.exchange.Exchange.buy', MagicMock(return_value=limit_buy_order)) + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=limit_buy_order)) assert not freqtrade.execute_buy(pair, stake_amount) # Fail to get price... @@ -908,7 +914,7 @@ def test_execute_buy_confirm_error(mocker, default_conf, fee, limit_buy_order) - 'ask': 0.00001173, 'last': 0.00001172 }), - buy=MagicMock(return_value=limit_buy_order), + create_order=MagicMock(return_value=limit_buy_order), get_rate=MagicMock(return_value=0.11), get_min_pair_stake_amount=MagicMock(return_value=1), get_fee=fee, @@ -970,8 +976,10 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog, 'ask': 0.00001173, 'last': 0.00001172 }), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), - sell=MagicMock(return_value={'id': limit_sell_order['id']}), + create_order=MagicMock(side_effect=[ + {'id': limit_buy_order['id']}, + {'id': limit_sell_order['id']}, + ]), get_fee=fee, ) mocker.patch.multiple( @@ -1087,8 +1095,10 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog, 'ask': 0.00001173, 'last': 0.00001172 }), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), - sell=MagicMock(return_value={'id': limit_sell_order['id']}), + create_order=MagicMock(side_effect=[ + {'id': limit_buy_order['id']}, + {'id': limit_sell_order['id']}, + ]), get_fee=fee, ) mocker.patch.multiple( @@ -1116,7 +1126,10 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, limit_buy_order_open, limit_sell_order): rpc_mock = patch_RPCManager(mocker) patch_exchange(mocker) - sell_mock = MagicMock(return_value={'id': limit_sell_order['id']}) + create_order_mock = MagicMock(side_effect=[ + limit_buy_order_open, + {'id': limit_sell_order['id']} + ]) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=MagicMock(return_value={ @@ -1124,8 +1137,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, 'ask': 0.00001173, 'last': 0.00001172 }), - buy=MagicMock(return_value=limit_buy_order_open), - sell=sell_mock, + create_order=create_order_mock, get_fee=fee, ) mocker.patch.multiple( @@ -1147,10 +1159,10 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee, assert log_has("Selling the trade forcefully", caplog) # Should call a market sell - assert sell_mock.call_count == 1 - assert sell_mock.call_args[1]['ordertype'] == 'market' - assert sell_mock.call_args[1]['pair'] == trade.pair - assert sell_mock.call_args[1]['amount'] == trade.amount + assert create_order_mock.call_count == 2 + assert create_order_mock.call_args[1]['ordertype'] == 'market' + assert create_order_mock.call_args[1]['pair'] == trade.pair + assert create_order_mock.call_args[1]['amount'] == trade.amount # Rpc is sending first buy, then sell assert rpc_mock.call_count == 2 @@ -1171,8 +1183,10 @@ def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog, 'ask': 0.00001173, 'last': 0.00001172 }), - buy=MagicMock(return_value=limit_buy_order_open), - sell=sell_mock, + create_order=MagicMock(side_effect=[ + limit_buy_order_open, + sell_mock, + ]), get_fee=fee, fetch_order=MagicMock(return_value={'status': 'canceled'}), ) @@ -1212,8 +1226,10 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, 'ask': 0.00001173, 'last': 0.00001172 }), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), - sell=MagicMock(return_value={'id': limit_sell_order['id']}), + create_order=MagicMock(side_effect=[ + {'id': limit_buy_order['id']}, + {'id': limit_sell_order['id']}, + ]), get_fee=fee, ) mocker.patch.multiple( @@ -1318,8 +1334,10 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c 'ask': 0.00001173, 'last': 0.00001172 }), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), - sell=MagicMock(return_value={'id': limit_sell_order['id']}), + create_order=MagicMock(side_effect=[ + {'id': limit_buy_order['id']}, + {'id': limit_sell_order['id']}, + ]), get_fee=fee, ) mocker.patch.multiple( @@ -1391,8 +1409,10 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee, 'ask': 0.00001173, 'last': 0.00001172 }), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), - sell=MagicMock(return_value={'id': limit_sell_order['id']}), + create_order=MagicMock(side_effect=[ + {'id': limit_buy_order['id']}, + {'id': limit_sell_order['id']}, + ]), get_fee=fee, ) mocker.patch.multiple( @@ -1502,8 +1522,10 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog, 'ask': 0.00001173, 'last': 0.00001172 }), - buy=MagicMock(return_value={'id': limit_buy_order['id']}), - sell=MagicMock(return_value={'id': limit_sell_order['id']}), + create_order=MagicMock(side_effect=[ + {'id': limit_buy_order['id']}, + {'id': limit_sell_order['id']}, + ]), get_fee=fee, stoploss=stoploss, ) @@ -1840,8 +1862,10 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order_open, limi 'ask': 0.00001173, 'last': 0.00001172 }), - buy=MagicMock(return_value=limit_buy_order), - sell=MagicMock(return_value=limit_sell_order_open), + create_order=MagicMock(side_effect=[ + limit_buy_order, + limit_sell_order_open, + ]), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -1857,7 +1881,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order_open, limi assert trade.is_open is True freqtrade.wallets.update() - patch_get_signal(freqtrade, value=(False, True)) + patch_get_signal(freqtrade, value=(False, True, None)) assert freqtrade.handle_trade(trade) is True assert trade.open_order_id == limit_sell_order['id'] @@ -1877,12 +1901,15 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(side_effect=[ + limit_buy_order_open, + {'id': 1234553382}, + ]), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) - patch_get_signal(freqtrade, value=(True, True)) + patch_get_signal(freqtrade, value=(True, True, None)) freqtrade.strategy.min_roi_reached = MagicMock(return_value=False) freqtrade.enter_positions() @@ -1893,7 +1920,7 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, assert nb_trades == 0 # Buy is triggering, so buying ... - patch_get_signal(freqtrade, value=(True, False)) + patch_get_signal(freqtrade, value=(True, False, None)) freqtrade.enter_positions() trades = Trade.query.all() nb_trades = len(trades) @@ -1901,7 +1928,7 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, assert trades[0].is_open is True # Buy and Sell are not triggering, so doing nothing ... - patch_get_signal(freqtrade, value=(False, False)) + patch_get_signal(freqtrade, value=(False, False, None)) assert freqtrade.handle_trade(trades[0]) is False trades = Trade.query.all() nb_trades = len(trades) @@ -1909,7 +1936,7 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, assert trades[0].is_open is True # Buy and Sell are triggering, so doing nothing ... - patch_get_signal(freqtrade, value=(True, True)) + patch_get_signal(freqtrade, value=(True, True, None)) assert freqtrade.handle_trade(trades[0]) is False trades = Trade.query.all() nb_trades = len(trades) @@ -1917,7 +1944,7 @@ def test_handle_overlapping_signals(default_conf, ticker, limit_buy_order_open, assert trades[0].is_open is True # Sell is triggering, guess what : we are Selling! - patch_get_signal(freqtrade, value=(False, True)) + patch_get_signal(freqtrade, value=(False, True, None)) trades = Trade.query.all() assert freqtrade.handle_trade(trades[0]) is True @@ -1930,12 +1957,15 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order_open, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(side_effect=[ + limit_buy_order_open, + {'id': 1234553382}, + ]), get_fee=fee, ) freqtrade = get_patched_freqtradebot(mocker, default_conf) - patch_get_signal(freqtrade, value=(True, False)) + patch_get_signal(freqtrade, value=(True, False, None)) freqtrade.strategy.min_roi_reached = MagicMock(return_value=True) freqtrade.enter_positions() @@ -1948,21 +1978,24 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order_open, # we might just want to check if we are in a sell condition without # executing # if ROI is reached we must sell - patch_get_signal(freqtrade, value=(False, True)) + patch_get_signal(freqtrade, value=(False, True, None)) assert freqtrade.handle_trade(trade) assert log_has("ETH/BTC - Required profit reached. sell_type=SellType.ROI", caplog) -def test_handle_trade_use_sell_signal( - default_conf, ticker, limit_buy_order_open, fee, mocker, caplog) -> None: +def test_handle_trade_use_sell_signal(default_conf, ticker, limit_buy_order_open, + limit_sell_order_open, fee, mocker, caplog) -> None: # use_sell_signal is True buy default caplog.set_level(logging.DEBUG) patch_RPCManager(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(side_effect=[ + limit_buy_order_open, + limit_sell_order_open, + ]), get_fee=fee, ) @@ -1974,10 +2007,10 @@ def test_handle_trade_use_sell_signal( trade = Trade.query.first() trade.is_open = True - patch_get_signal(freqtrade, value=(False, False)) + patch_get_signal(freqtrade, value=(False, False, None)) assert not freqtrade.handle_trade(trade) - patch_get_signal(freqtrade, value=(False, True)) + patch_get_signal(freqtrade, value=(False, True, None)) assert freqtrade.handle_trade(trade) assert log_has("ETH/BTC - Sell signal received. sell_type=SellType.SELL_SIGNAL", caplog) @@ -1990,7 +2023,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_buy_order_open mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -2747,13 +2780,16 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', side_effect=InvalidOrderException()) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=300)) - sellmock = MagicMock(return_value={'id': '12345555'}) + create_order_mock = MagicMock(side_effect=[ + {'id': '12345554'}, + {'id': '12345555'}, + ]) patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, - sell=sellmock + create_order=create_order_mock, ) freqtrade.strategy.order_types['stoploss_on_exchange'] = True @@ -2768,7 +2804,7 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, c freqtrade.execute_sell(trade=trade, limit=1234, sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS)) - assert sellmock.call_count == 1 + assert create_order_mock.call_count == 2 assert log_has('Could not cancel stoploss order abcd', caplog) @@ -2964,7 +3000,10 @@ def test_execute_sell_insufficient_funds_error(default_conf, ticker, fee, 'freqtrade.exchange.Exchange', fetch_ticker=ticker, get_fee=fee, - sell=MagicMock(side_effect=InsufficientFundsError()) + create_order=MagicMock(side_effect=[ + {'id': 1234553382}, + InsufficientFundsError(), + ]), ) patch_get_signal(freqtrade) @@ -2997,7 +3036,10 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy 'ask': 0.00001173, 'last': 0.00001172 }), - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(side_effect=[ + limit_buy_order_open, + {'id': 1234553382}, + ]), get_fee=fee, ) default_conf.update({ @@ -3014,7 +3056,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, limit_buy trade = Trade.query.first() trade.update(limit_buy_order) freqtrade.wallets.update() - patch_get_signal(freqtrade, value=(False, True)) + patch_get_signal(freqtrade, value=(False, True, None)) assert freqtrade.handle_trade(trade) is False freqtrade.strategy.sell_profit_offset = 0.0 @@ -3034,7 +3076,10 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, limit_bu 'ask': 0.00002173, 'last': 0.00002172 }), - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(side_effect=[ + limit_buy_order_open, + {'id': 1234553382}, + ]), get_fee=fee, ) default_conf.update({ @@ -3049,7 +3094,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, limit_bu trade = Trade.query.first() trade.update(limit_buy_order) freqtrade.wallets.update() - patch_get_signal(freqtrade, value=(False, True)) + patch_get_signal(freqtrade, value=(False, True, None)) assert freqtrade.handle_trade(trade) is True assert trade.sell_reason == SellType.SELL_SIGNAL.value @@ -3065,7 +3110,10 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_o 'ask': 0.00000173, 'last': 0.00000172 }), - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(side_effect=[ + limit_buy_order_open, + {'id': 1234553382}, + ]), get_fee=fee, ) default_conf.update({ @@ -3080,7 +3128,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, limit_buy_o trade = Trade.query.first() trade.update(limit_buy_order) - patch_get_signal(freqtrade, value=(False, True)) + patch_get_signal(freqtrade, value=(False, True, None)) assert freqtrade.handle_trade(trade) is False @@ -3095,7 +3143,10 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, limit_buy_ 'ask': 0.0000173, 'last': 0.0000172 }), - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(side_effect=[ + limit_buy_order_open, + {'id': 1234553382}, + ]), get_fee=fee, ) default_conf.update({ @@ -3112,7 +3163,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, limit_buy_ trade = Trade.query.first() trade.update(limit_buy_order) freqtrade.wallets.update() - patch_get_signal(freqtrade, value=(False, True)) + patch_get_signal(freqtrade, value=(False, True, None)) assert freqtrade.handle_trade(trade) is True assert trade.sell_reason == SellType.SELL_SIGNAL.value @@ -3128,7 +3179,10 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_ 'ask': 0.00002173, 'last': 0.00002172 }), - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(side_effect=[ + limit_buy_order_open, + {'id': 1234553382}, + ]), get_fee=fee, ) @@ -3141,7 +3195,7 @@ def test_sell_not_enough_balance(default_conf, limit_buy_order, limit_buy_order_ trade = Trade.query.first() amnt = trade.amount trade.update(limit_buy_order) - patch_get_signal(freqtrade, value=(False, True)) + patch_get_signal(freqtrade, value=(False, True, None)) mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(return_value=trade.amount * 0.985)) assert freqtrade.handle_trade(trade) is True @@ -3246,7 +3300,10 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_buy_order 'ask': 0.0000173, 'last': 0.0000172 }), - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(side_effect=[ + limit_buy_order_open, + {'id': 1234553382}, + ]), get_fee=fee, ) default_conf['ignore_roi_if_buy_signal'] = True @@ -3260,11 +3317,11 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_buy_order trade = Trade.query.first() trade.update(limit_buy_order) freqtrade.wallets.update() - patch_get_signal(freqtrade, value=(True, True)) + patch_get_signal(freqtrade, value=(True, True, None)) assert freqtrade.handle_trade(trade) is False # Test if buy-signal is absent (should sell due to roi = true) - patch_get_signal(freqtrade, value=(False, True)) + patch_get_signal(freqtrade, value=(False, True, None)) assert freqtrade.handle_trade(trade) is True assert trade.sell_reason == SellType.ROI.value @@ -3280,7 +3337,10 @@ def test_trailing_stop_loss(default_conf, limit_buy_order_open, limit_buy_order, 'ask': 0.00001099, 'last': 0.00001099 }), - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(side_effect=[ + limit_buy_order_open, + {'id': 1234553382}, + ]), get_fee=fee, ) default_conf['trailing_stop'] = True @@ -3332,7 +3392,10 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, limit_buy_or 'ask': buy_price - 0.000001, 'last': buy_price - 0.000001 }), - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(side_effect=[ + limit_buy_order_open, + {'id': 1234553382}, + ]), get_fee=fee, ) default_conf['trailing_stop'] = True @@ -3389,7 +3452,10 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, limit_buy_orde 'ask': buy_price - 0.000001, 'last': buy_price - 0.000001 }), - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(side_effect=[ + limit_buy_order_open, + {'id': 1234553382}, + ]), get_fee=fee, ) patch_whitelist(mocker, default_conf) @@ -3449,7 +3515,7 @@ def test_tsl_only_offset_reached(default_conf, limit_buy_order, limit_buy_order_ 'ask': buy_price, 'last': buy_price }), - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) patch_whitelist(mocker, default_conf) @@ -3509,7 +3575,11 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_b 'ask': 0.00000173, 'last': 0.00000172 }), - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(side_effect=[ + limit_buy_order_open, + {'id': 1234553382}, + {'id': 1234553383} + ]), get_fee=fee, _is_dry_limit_order_filled=MagicMock(return_value=False), ) @@ -3525,11 +3595,11 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, limit_b trade = Trade.query.first() trade.update(limit_buy_order) # Sell due to min_roi_reached - patch_get_signal(freqtrade, value=(True, True)) + patch_get_signal(freqtrade, value=(True, True, None)) assert freqtrade.handle_trade(trade) is True # Test if buy-signal is absent - patch_get_signal(freqtrade, value=(False, True)) + patch_get_signal(freqtrade, value=(False, True, None)) assert freqtrade.handle_trade(trade) is True assert trade.sell_reason == SellType.SELL_SIGNAL.value @@ -3917,7 +3987,7 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order_open, mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) @@ -3954,7 +4024,7 @@ def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_o mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value={'id': limit_buy_order['id']}), + create_order=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, ) # Save state of current whitelist @@ -4051,8 +4121,10 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order_open, limit_buy_o 'ask': 0.00001173, 'last': 0.00001172 }), - buy=MagicMock(return_value=limit_buy_order_open), - sell=MagicMock(return_value=limit_sell_order_open), + create_order=MagicMock(side_effect=[ + limit_buy_order_open, + limit_sell_order_open, + ]), get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) @@ -4068,7 +4140,7 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order_open, limit_buy_o freqtrade.wallets.update() assert trade.is_open is True - patch_get_signal(freqtrade, value=(False, True)) + patch_get_signal(freqtrade, value=(False, True, None)) assert freqtrade.handle_trade(trade) is True assert trade.close_rate_requested == order_book_l2.return_value['asks'][0][0] @@ -4117,7 +4189,7 @@ def test_sync_wallet_dry_run(mocker, default_conf, ticker, fee, limit_buy_order_ mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(return_value=limit_buy_order_open), get_fee=fee, ) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 1e2719ac2..16469f6fc 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -69,7 +69,7 @@ def test_init_dryrun_db(default_conf, tmpdir): def test_enter_exit_side(fee): trade = Trade( id=2, - pair='ETH/BTC', + pair='ADA/USDT', stake_amount=0.001, open_rate=0.01, amount=5, @@ -94,10 +94,10 @@ def test_enter_exit_side(fee): def test__set_stop_loss_isolated_liq(fee): trade = Trade( id=2, - pair='ETH/BTC', - stake_amount=0.001, - open_rate=0.01, - amount=5, + pair='ADA/USDT', + stake_amount=60.0, + open_rate=2.0, + amount=30.0, is_open=True, open_date=arrow.utcnow().datetime, fee_open=fee.return_value, @@ -227,7 +227,7 @@ def test_interest(market_buy_order_usdt, fee): """ trade = Trade( - pair='ETH/BTC', + pair='ADA/USDT', stake_amount=20.0, amount=30.0, open_rate=2.0, @@ -404,7 +404,7 @@ def test_borrowed(limit_buy_order_usdt, limit_sell_order_usdt, fee, caplog): trade = Trade( id=2, - pair='ETH/BTC', + pair='ADA/USDT', stake_amount=60.0, open_rate=2.0, amount=30.0, @@ -498,7 +498,7 @@ def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, ca trade = Trade( id=2, - pair='ETH/BTC', + pair='ADA/USDT', stake_amount=60.0, open_rate=2.0, amount=30.0, @@ -519,7 +519,7 @@ def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, ca assert trade.close_profit is None assert trade.close_date is None assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=2, " - r'pair=ETH/BTC, amount=30.00000000, ' + r'pair=ADA/USDT, amount=30.00000000, ' r"is_short=False, leverage=1.0, open_rate=2.00000000, open_since=.*\).", caplog) @@ -531,14 +531,14 @@ def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, ca assert trade.close_profit == round(0.0945137157107232, 8) assert trade.close_date is not None assert log_has_re(r"LIMIT_SELL has been fulfilled for Trade\(id=2, " - r"pair=ETH/BTC, amount=30.00000000, " + r"pair=ADA/USDT, amount=30.00000000, " r"is_short=False, leverage=1.0, open_rate=2.00000000, open_since=.*\).", caplog) caplog.clear() trade = Trade( id=226531, - pair='ETH/BTC', + pair='ADA/USDT', stake_amount=20.0, open_rate=2.0, amount=30.0, @@ -561,7 +561,7 @@ def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, ca assert trade.close_date is None assert log_has_re(r"LIMIT_SELL has been fulfilled for Trade\(id=226531, " - r"pair=ETH/BTC, amount=30.00000000, " + r"pair=ADA/USDT, amount=30.00000000, " r"is_short=True, leverage=3.0, open_rate=2.20000000, open_since=.*\).", caplog) caplog.clear() @@ -573,7 +573,7 @@ def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, ca assert trade.close_profit == round(0.2589996297562085, 8) assert trade.close_date is not None assert log_has_re(r"LIMIT_BUY has been fulfilled for Trade\(id=226531, " - r"pair=ETH/BTC, amount=30.00000000, " + r"pair=ADA/USDT, amount=30.00000000, " r"is_short=True, leverage=3.0, open_rate=2.20000000, open_since=.*\).", caplog) caplog.clear() @@ -583,7 +583,7 @@ def test_update_limit_order(limit_buy_order_usdt, limit_sell_order_usdt, fee, ca def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, caplog): trade = Trade( id=1, - pair='ETH/BTC', + pair='ADA/USDT', stake_amount=60.0, open_rate=2.0, amount=30.0, @@ -601,7 +601,7 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, assert trade.close_profit is None assert trade.close_date is None assert log_has_re(r"MARKET_BUY has been fulfilled for Trade\(id=1, " - r"pair=ETH/BTC, amount=30.00000000, is_short=False, leverage=1.0, " + r"pair=ADA/USDT, amount=30.00000000, is_short=False, leverage=1.0, " r"open_rate=2.00000000, open_since=.*\).", caplog) @@ -614,7 +614,7 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, assert trade.close_profit == round(0.0945137157107232, 8) assert trade.close_date is not None assert log_has_re(r"MARKET_SELL has been fulfilled for Trade\(id=1, " - r"pair=ETH/BTC, amount=30.00000000, is_short=False, leverage=1.0, " + r"pair=ADA/USDT, amount=30.00000000, is_short=False, leverage=1.0, " r"open_rate=2.00000000, open_since=.*\).", caplog) @@ -622,7 +622,7 @@ def test_update_market_order(market_buy_order_usdt, market_sell_order_usdt, fee, @pytest.mark.usefixtures("init_persistence") def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, fee): trade = Trade( - pair='ETH/BTC', + pair='ADA/USDT', stake_amount=60.0, open_rate=2.0, amount=30.0, @@ -685,7 +685,7 @@ def test_calc_open_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt @pytest.mark.usefixtures("init_persistence") def test_trade_close(limit_buy_order_usdt, limit_sell_order_usdt, fee): trade = Trade( - pair='ETH/BTC', + pair='ADA/USDT', stake_amount=60.0, open_rate=2.0, amount=30.0, @@ -710,14 +710,14 @@ def test_trade_close(limit_buy_order_usdt, limit_sell_order_usdt, fee): # Close should NOT update close_date if the trade has been closed already assert trade.is_open is False trade.close_date = new_date - trade.close(0.02) + trade.close(2.2) assert trade.close_date == new_date @pytest.mark.usefixtures("init_persistence") def test_calc_close_trade_price_exception(limit_buy_order_usdt, fee): trade = Trade( - pair='ETH/BTC', + pair='ADA/USDT', stake_amount=60.0, open_rate=2.0, amount=30.0, @@ -734,7 +734,7 @@ def test_calc_close_trade_price_exception(limit_buy_order_usdt, fee): @pytest.mark.usefixtures("init_persistence") def test_update_open_order(limit_buy_order_usdt): trade = Trade( - pair='ETH/BTC', + pair='ADA/USDT', stake_amount=60.0, open_rate=2.0, amount=30.0, @@ -758,7 +758,7 @@ def test_update_open_order(limit_buy_order_usdt): @pytest.mark.usefixtures("init_persistence") def test_update_invalid_order(limit_buy_order_usdt): trade = Trade( - pair='ETH/BTC', + pair='ADA/USDT', stake_amount=60.0, amount=30.0, open_rate=2.0, @@ -788,7 +788,7 @@ def test_calc_open_trade_value(limit_buy_order_usdt, fee): # 1x, 3x: 30 * 2 + 30 * 2 * 0.003 = 60.18 quote # -1x,-3x: 30 * 2 - 30 * 2 * 0.003 = 59.82 quote trade = Trade( - pair='ETH/BTC', + pair='ADA/USDT', stake_amount=60.0, amount=30.0, open_rate=2.0, @@ -823,7 +823,7 @@ def test_calc_open_trade_value(limit_buy_order_usdt, fee): @pytest.mark.usefixtures("init_persistence") def test_calc_close_trade_price(limit_buy_order_usdt, limit_sell_order_usdt, fee): trade = Trade( - pair='ETH/BTC', + pair='ADA/USDT', stake_amount=60.0, amount=30.0, open_rate=2.0, @@ -1007,7 +1007,7 @@ def test_calc_profit(limit_buy_order_usdt, limit_sell_order_usdt, fee): 2.2 quote: (65.802 / 60.15) - 1 = 0.09396508728179565 """ trade = Trade( - pair='ETH/BTC', + pair='ADA/USDT', stake_amount=60.0, amount=30.0, open_rate=2.0, @@ -1109,7 +1109,7 @@ def test_calc_profit(limit_buy_order_usdt, limit_sell_order_usdt, fee): @pytest.mark.usefixtures("init_persistence") def test_calc_profit_ratio(limit_buy_order_usdt, limit_sell_order_usdt, fee): trade = Trade( - pair='ETH/BTC', + pair='ADA/USDT', stake_amount=60.0, amount=30.0, open_rate=2.0, @@ -1214,7 +1214,7 @@ def test_clean_dry_run_db(default_conf, fee): # Simulate dry_run entries trade = Trade( - pair='ETH/BTC', + pair='ADA/USDT', stake_amount=0.001, amount=123.0, fee_open=fee.return_value, @@ -1259,96 +1259,6 @@ def test_clean_dry_run_db(default_conf, fee): assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 1 -def test_migrate_old(mocker, default_conf, fee): - """ - Test Database migration(starting with old pairformat) - """ - amount = 103.223 - create_table_old = """CREATE TABLE IF NOT EXISTS "trades" ( - id INTEGER NOT NULL, - exchange VARCHAR NOT NULL, - pair VARCHAR NOT NULL, - is_open BOOLEAN NOT NULL, - fee FLOAT NOT NULL, - open_rate FLOAT, - close_rate FLOAT, - close_profit FLOAT, - stake_amount FLOAT NOT NULL, - amount FLOAT, - open_date DATETIME NOT NULL, - close_date DATETIME, - open_order_id VARCHAR, - PRIMARY KEY (id), - CHECK (is_open IN (0, 1)) - );""" - insert_table_old = """INSERT INTO trades (exchange, pair, is_open, open_order_id, fee, - open_rate, stake_amount, amount, open_date) - VALUES ('binance', 'BTC_ETC', 1, '123123', {fee}, - 0.00258580, {stake}, {amount}, - '2017-11-28 12:44:24.000000') - """.format(fee=fee.return_value, - stake=default_conf.get("stake_amount"), - amount=amount - ) - insert_table_old2 = """INSERT INTO trades (exchange, pair, is_open, fee, - open_rate, close_rate, stake_amount, amount, open_date) - VALUES ('binance', 'BTC_ETC', 0, {fee}, - 0.00258580, 0.00268580, {stake}, {amount}, - '2017-11-28 12:44:24.000000') - """.format(fee=fee.return_value, - stake=default_conf.get("stake_amount"), - amount=amount - ) - engine = create_engine('sqlite://') - mocker.patch('freqtrade.persistence.models.create_engine', lambda *args, **kwargs: engine) - - # Create table using the old format - with engine.begin() as connection: - connection.execute(text(create_table_old)) - connection.execute(text(insert_table_old)) - connection.execute(text(insert_table_old2)) - # Run init to test migration - init_db(default_conf['db_url'], default_conf['dry_run']) - - assert len(Trade.query.filter(Trade.id == 1).all()) == 1 - trade = Trade.query.filter(Trade.id == 1).first() - assert trade.fee_open == fee.return_value - assert trade.fee_close == fee.return_value - assert trade.open_rate_requested is None - assert trade.close_rate_requested is None - assert trade.is_open == 1 - assert trade.amount == amount - assert trade.amount_requested == amount - assert trade.stake_amount == default_conf.get("stake_amount") - assert trade.pair == "ETC/BTC" - assert trade.exchange == "binance" - assert trade.max_rate == 0.0 - assert trade.stop_loss == 0.0 - assert trade.initial_stop_loss == 0.0 - assert trade.open_trade_value == trade._calc_open_trade_value() - assert trade.close_profit_abs is None - assert trade.fee_open_cost is None - assert trade.fee_open_currency is None - assert trade.fee_close_cost is None - assert trade.fee_close_currency is None - assert trade.timeframe is None - - trade = Trade.query.filter(Trade.id == 2).first() - assert trade.close_rate is not None - assert trade.is_open == 0 - assert trade.open_rate_requested is None - assert trade.close_rate_requested is None - assert trade.close_rate is not None - assert pytest.approx(trade.close_profit_abs) == trade.calc_profit() - assert trade.sell_order_status is None - - # Should've created one order - assert len(Order.query.all()) == 1 - order = Order.query.first() - assert order.order_id == '123123' - assert order.ft_order_side == 'buy' - - def test_migrate_new(mocker, default_conf, fee, caplog): """ Test Database migration (starting with new pairformat) @@ -1571,9 +1481,9 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog): def test_adjust_stop_loss(fee): trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=5, + pair='ADA/USDT', + stake_amount=30.0, + amount=30, fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', @@ -1623,7 +1533,7 @@ def test_adjust_stop_loss(fee): def test_adjust_stop_loss_short(fee): trade = Trade( - pair='ETH/BTC', + pair='ADA/USDT', stake_amount=0.001, amount=5, fee_open=fee.return_value, @@ -1677,9 +1587,9 @@ def test_adjust_stop_loss_short(fee): def test_adjust_min_max_rates(fee): trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, - amount=5, + pair='ADA/USDT', + stake_amount=30.0, + amount=30.0, fee_open=fee.return_value, fee_close=fee.return_value, exchange='binance', @@ -1735,7 +1645,7 @@ def test_to_json(default_conf, fee): # Simulate dry_run entries trade = Trade( - pair='ETH/BTC', + pair='ADA/USDT', stake_amount=0.001, amount=123.0, amount_requested=123.0, @@ -1744,13 +1654,14 @@ def test_to_json(default_conf, fee): open_date=arrow.utcnow().shift(hours=-2).datetime, open_rate=0.123, exchange='binance', + buy_tag=None, open_order_id='dry_run_buy_12345' ) result = trade.to_json() assert isinstance(result, dict) assert result == {'trade_id': None, - 'pair': 'ETH/BTC', + 'pair': 'ADA/USDT', 'is_open': None, 'open_date': trade.open_date.strftime("%Y-%m-%d %H:%M:%S"), 'open_timestamp': int(trade.open_date.timestamp() * 1000), @@ -1793,6 +1704,7 @@ def test_to_json(default_conf, fee): 'min_rate': None, 'max_rate': None, 'strategy': None, + 'buy_tag': None, 'timeframe': None, 'exchange': 'binance', 'leverage': None, @@ -1813,6 +1725,7 @@ def test_to_json(default_conf, fee): close_date=arrow.utcnow().shift(hours=-1).datetime, open_rate=0.123, close_rate=0.125, + buy_tag='buys_signal_001', exchange='binance', ) result = trade.to_json() @@ -1862,6 +1775,7 @@ def test_to_json(default_conf, fee): 'sell_reason': None, 'sell_order_status': None, 'strategy': None, + 'buy_tag': 'buys_signal_001', 'timeframe': None, 'exchange': 'binance', 'leverage': None, @@ -1874,11 +1788,11 @@ def test_to_json(default_conf, fee): def test_stoploss_reinitialization(default_conf, fee): init_db(default_conf['db_url']) trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, + pair='ADA/USDT', + stake_amount=30.0, fee_open=fee.return_value, open_date=arrow.utcnow().shift(hours=-2).datetime, - amount=10, + amount=30.0, fee_close=fee.return_value, exchange='binance', open_rate=1, @@ -1934,7 +1848,7 @@ def test_stoploss_reinitialization(default_conf, fee): def test_stoploss_reinitialization_short(default_conf, fee): init_db(default_conf['db_url']) trade = Trade( - pair='ETH/BTC', + pair='ADA/USDT', stake_amount=0.001, fee_open=fee.return_value, open_date=arrow.utcnow().shift(hours=-2).datetime, @@ -1993,11 +1907,11 @@ def test_stoploss_reinitialization_short(default_conf, fee): def test_update_fee(fee): trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, + pair='ADA/USDT', + stake_amount=30.0, fee_open=fee.return_value, open_date=arrow.utcnow().shift(hours=-2).datetime, - amount=10, + amount=30.0, fee_close=fee.return_value, exchange='binance', open_rate=1, @@ -2032,11 +1946,11 @@ def test_update_fee(fee): def test_fee_updated(fee): trade = Trade( - pair='ETH/BTC', - stake_amount=0.001, + pair='ADA/USDT', + stake_amount=30.0, fee_open=fee.return_value, open_date=arrow.utcnow().shift(hours=-2).datetime, - amount=10, + amount=30.0, fee_close=fee.return_value, exchange='binance', open_rate=1, @@ -2164,16 +2078,16 @@ def test_get_best_pair_lev(fee): @pytest.mark.usefixtures("init_persistence") def test_update_order_from_ccxt(caplog): # Most basic order return (only has orderid) - o = Order.parse_from_ccxt_object({'id': '1234'}, 'ETH/BTC', 'buy') + o = Order.parse_from_ccxt_object({'id': '1234'}, 'ADA/USDT', 'buy') assert isinstance(o, Order) - assert o.ft_pair == 'ETH/BTC' + assert o.ft_pair == 'ADA/USDT' assert o.ft_order_side == 'buy' assert o.order_id == '1234' assert o.ft_is_open ccxt_order = { 'id': '1234', 'side': 'buy', - 'symbol': 'ETH/BTC', + 'symbol': 'ADA/USDT', 'type': 'limit', 'price': 1234.5, 'amount': 20.0, @@ -2182,9 +2096,9 @@ def test_update_order_from_ccxt(caplog): 'status': 'open', 'timestamp': 1599394315123 } - o = Order.parse_from_ccxt_object(ccxt_order, 'ETH/BTC', 'buy') + o = Order.parse_from_ccxt_object(ccxt_order, 'ADA/USDT', 'buy') assert isinstance(o, Order) - assert o.ft_pair == 'ETH/BTC' + assert o.ft_pair == 'ADA/USDT' assert o.ft_order_side == 'buy' assert o.order_id == '1234' assert o.order_type == 'limit' diff --git a/tests/test_wallets.py b/tests/test_wallets.py index 64db3b9cd..9f58cb71d 100644 --- a/tests/test_wallets.py +++ b/tests/test_wallets.py @@ -125,7 +125,7 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None: (1, None, 50, 66.66666), (0.99, None, 49.5, 66.0), (0.50, None, 25, 33.3333), - # Tests with capital ignore balance_ratio + # Tests with capital ignore balance_ratio (1, 100, 50, 0.0), (0.99, 200, 50, 66.66666), (0.99, 150, 50, 50), @@ -138,7 +138,7 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_r mocker.patch.multiple( 'freqtrade.exchange.Exchange', fetch_ticker=ticker, - buy=MagicMock(return_value=limit_buy_order_open), + create_order=MagicMock(return_value=limit_buy_order_open), get_fee=fee )