diff --git a/Dockerfile b/Dockerfile index afafd93c1..309763d2a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.6.5-slim-stretch +FROM python:3.6.6-slim-stretch # Install TA-lib RUN apt-get update && apt-get -y install curl build-essential && apt-get clean @@ -15,7 +15,8 @@ WORKDIR /freqtrade # Install dependencies COPY requirements.txt /freqtrade/ -RUN pip install -r requirements.txt +RUN pip install numpy \ + && pip install -r requirements.txt # Install and execute COPY . /freqtrade/ diff --git a/README.md b/README.md index 929d40292..da691230f 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,12 @@ [![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop) [![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability) - -Simple High frequency trading bot for crypto currencies designed to -support multi exchanges and be controlled via Telegram. +Simple High frequency trading bot for crypto currencies designed to support multi exchanges and be controlled via Telegram. ![freqtrade](https://raw.githubusercontent.com/freqtrade/freqtrade/develop/docs/assets/freqtrade-screenshot.png) ## Disclaimer + This software is for educational purposes only. Do not risk money which you are afraid to lose. USE THE SOFTWARE AT YOUR OWN RISK. THE AUTHORS AND ALL AFFILIATES ASSUME NO RESPONSIBILITY FOR YOUR TRADING RESULTS. @@ -23,18 +22,18 @@ We strongly recommend you to have coding and Python knowledge. Do not hesitate to read the source code and understand the mechanism of this bot. ## Exchange marketplaces supported + - [X] [Bittrex](https://bittrex.com/) - [X] [Binance](https://www.binance.com/) - [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ ## Features -- [x] **Based on Python 3.6+**: For botting on any operating system - -Windows, macOS and Linux + +- [x] **Based on Python 3.6+**: For botting on any operating system - Windows, macOS and Linux - [x] **Persistence**: Persistence is achieved through sqlite - [x] **Dry-run**: Run the bot without playing money. - [x] **Backtesting**: Run a simulation of your buy/sell strategy. -- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell -strategy parameters with real exchange data. +- [x] **Strategy Optimization by machine learning**: Use machine learning to optimize your buy/sell strategy parameters with real exchange data. - [x] **Whitelist crypto-currencies**: Select which crypto-currency you want to trade. - [x] **Blacklist crypto-currencies**: Select which crypto-currency you want to avoid. - [x] **Manageable via Telegram**: Manage the bot with Telegram @@ -43,38 +42,45 @@ strategy parameters with real exchange data. - [x] **Performance status report**: Provide a performance status of your current trades. ## Table of Contents + - [Quick start](#quick-start) - [Documentations](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md) - - [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md) - - [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) - - [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md) - - [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) - - [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md) + - [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md) + - [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) + - [Strategy Optimization](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md) + - [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) + - [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md) + - [Sandbox Testing](https://github.com/freqtrade/freqtrade/blob/develop/docs/sandbox-testing.md) - [Basic Usage](#basic-usage) - [Bot commands](#bot-commands) - [Telegram RPC commands](#telegram-rpc-commands) - [Support](#support) - - [Help](#help--slack) - - [Bugs](#bugs--issues) - - [Feature Requests](#feature-requests) - - [Pull Requests](#pull-requests) + - [Help](#help--slack) + - [Bugs](#bugs--issues) + - [Feature Requests](#feature-requests) + - [Pull Requests](#pull-requests) - [Requirements](#requirements) - - [Min hardware required](#min-hardware-required) - - [Software requirements](#software-requirements) + - [Min hardware required](#min-hardware-required) + - [Software requirements](#software-requirements) + ## Quick start + Freqtrade provides a Linux/macOS script to install all dependencies and help you to configure the bot. + ```bash git clone git@github.com:freqtrade/freqtrade.git -git checkout develop cd freqtrade +git checkout develop ./setup.sh --install ``` + _Windows installation is explained in [Installation doc](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md)_ - ## Documentation + We invite you to read the bot documentation to ensure you understand how the bot is working. + - [Index](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md) - [Installation](https://github.com/freqtrade/freqtrade/blob/develop/docs/installation.md) - [Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md) @@ -86,7 +92,6 @@ We invite you to read the bot documentation to ensure you understand how the bot - [Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) - [Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md) - ## Basic Usage ### Bot commands @@ -125,17 +130,15 @@ optional arguments: ``` ### Telegram RPC commands -Telegram is not mandatory. However, this is a great way to control your -bot. More details on our -[documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md) + +Telegram is not mandatory. However, this is a great way to control your bot. More details on our [documentation](https://github.com/freqtrade/freqtrade/blob/develop/docs/index.md) - `/start`: Starts the trader - `/stop`: Stops the trader - `/status [table]`: Lists all open trades - `/count`: Displays number of open trades - `/profit`: Lists cumulative profit from all finished trades -- `/forcesell |all`: Instantly sells the given trade -(Ignoring `minimum_roi`). +- `/forcesell |all`: Instantly sells the given trade (Ignoring `minimum_roi`). - `/performance`: Show performance of each finished trade grouped by pair - `/balance`: Show account balance per currency - `/daily `: Shows profit or loss per day, over the last n days @@ -144,20 +147,23 @@ bot. More details on our ## Development branches -The project is currently setup in two main branches: -- `develop` - This branch has often new features, but might also cause -breaking changes. -- `master` - This branch contains the latest stable release. The bot -'should' be stable on this branch, and is generally well tested. +The project is currently setup in two main branches: + +- `develop` - This branch has often new features, but might also cause breaking changes. +- `master` - This branch contains the latest stable release. The bot 'should' be stable on this branch, and is generally well tested. ## Support + ### Help / Slack + For any questions not covered by the documentation or for further information about the bot, we encourage you to join our slack channel. + - [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE). ### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) + If you discover a bug in the bot, please [search our issue tracker](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) first. If it hasn't been reported, please @@ -166,6 +172,7 @@ ensure you follow the template guide so that our team can assist you as quickly as possible. ### [Feature Requests](https://github.com/freqtrade/freqtrade/labels/enhancement) + Have you a great idea to improve the bot you want to share? Please, first search if this feature was not [already discussed](https://github.com/freqtrade/freqtrade/labels/enhancement). If it hasn't been requested, please @@ -174,6 +181,7 @@ and ensure you follow the template guide so that it does not get lost in the bug reports. ### [Pull Requests](https://github.com/freqtrade/freqtrade/pulls) + Feel like our bot is missing a feature? We welcome your pull requests! Please read our [Contributing document](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md) @@ -181,16 +189,18 @@ to understand the requirements before sending your pull-requests. **Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. -**Important:** Always create your PR against the `develop` branch, not -`master`. +**Important:** Always create your PR against the `develop` branch, not `master`. ## Requirements ### Min hardware required + To run this bot we recommend you a cloud instance with a minimum of: -* Minimal (advised) system requirements: 2GB RAM, 1GB disk space, 2vCPU + +- Minimal (advised) system requirements: 2GB RAM, 1GB disk space, 2vCPU ### Software requirements + - [Python 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/) - [pip](https://pip.pypa.io/en/stable/installing/) - [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) diff --git a/config.json.example b/config.json.example index e8473e919..8bd3942e6 100644 --- a/config.json.example +++ b/config.json.example @@ -17,6 +17,7 @@ "name": "bittrex", "key": "your_exchange_key", "secret": "your_exchange_secret", + "ccxt_rate_limit": true, "pair_whitelist": [ "ETH/BTC", "LTC/BTC", diff --git a/config_full.json.example b/config_full.json.example index 4003b1c5c..cc3b3d630 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -7,6 +7,7 @@ "ticker_interval": "5m", "trailing_stop": false, "trailing_stop_positive": 0.005, + "trailing_stop_positive_offset": 0.0051, "minimal_roi": { "40": 0.0, "30": 0.01, @@ -25,6 +26,7 @@ "name": "bittrex", "key": "your_exchange_key", "secret": "your_exchange_secret", + "ccxt_rate_limit": true, "pair_whitelist": [ "ETH/BTC", "LTC/BTC", diff --git a/docs/backtesting.md b/docs/backtesting.md index 172969ae2..766875970 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -29,25 +29,25 @@ The backtesting is very easy with freqtrade. #### With 5 min tickers (Per default) ```bash -python3 ./freqtrade/main.py backtesting --realistic-simulation +python3 ./freqtrade/main.py backtesting ``` #### With 1 min tickers ```bash -python3 ./freqtrade/main.py backtesting --realistic-simulation --ticker-interval 1m +python3 ./freqtrade/main.py backtesting --ticker-interval 1m ``` #### Update cached pairs with the latest data ```bash -python3 ./freqtrade/main.py backtesting --realistic-simulation --refresh-pairs-cached +python3 ./freqtrade/main.py backtesting --refresh-pairs-cached ``` #### With live data (do not alter your testdata files) ```bash -python3 ./freqtrade/main.py backtesting --realistic-simulation --live +python3 ./freqtrade/main.py backtesting --live ``` #### Using a different on-disk ticker-data source @@ -83,7 +83,7 @@ with filename.open() as file: data = json.load(file) columns = ["pair", "profit", "opents", "closets", "index", "duration", - "open_rate", "close_rate", "open_at_end"] + "open_rate", "close_rate", "open_at_end", "sell_reason"] df = pd.DataFrame(data, columns=columns) df['opents'] = pd.to_datetime(df['opents'], @@ -98,6 +98,8 @@ df['closets'] = pd.to_datetime(df['closets'], ) ``` +If you have some ideas for interesting / helpful backtest data analysis, feel free to submit a PR so the community can benefit from it. + #### Exporting trades to file specifying a custom filename ```bash diff --git a/docs/bot-optimization.md b/docs/bot-optimization.md index d7b628fd4..0214ce5c5 100644 --- a/docs/bot-optimization.md +++ b/docs/bot-optimization.md @@ -39,7 +39,6 @@ A strategy file contains all the information needed to build a good strategy: - Sell strategy rules - Minimal ROI recommended - Stoploss recommended -- Hyperopt parameter The bot also include a sample strategy called `TestStrategy` you can update: `user_data/strategies/test_strategy.py`. You can test it with the parameter: `--strategy TestStrategy` @@ -61,22 +60,22 @@ file as reference.** ### Buy strategy -Edit the method `populate_buy_trend()` into your strategy file to -update your buy strategy. +Edit the method `populate_buy_trend()` into your strategy file to update your buy strategy. Sample from `user_data/strategies/test_strategy.py`: ```python -def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: +def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the buy signal for the given dataframe - :param dataframe: DataFrame + :param dataframe: DataFrame populated with indicators + :param metadata: Additional information, like the currently traded pair :return: DataFrame with buy column """ dataframe.loc[ ( (dataframe['adx'] > 30) & - (dataframe['tema'] <= dataframe['blower']) & + (dataframe['tema'] <= dataframe['bb_middleband']) & (dataframe['tema'] > dataframe['tema'].shift(1)) ), 'buy'] = 1 @@ -87,38 +86,47 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: ### Sell strategy Edit the method `populate_sell_trend()` into your strategy file to update your sell strategy. +Please note that the sell-signal is only used if `use_sell_signal` is set to true in the configuration. Sample from `user_data/strategies/test_strategy.py`: ```python -def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: +def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the sell signal for the given dataframe - :param dataframe: DataFrame + :param dataframe: DataFrame populated with indicators + :param metadata: Additional information, like the currently traded pair :return: DataFrame with buy column """ dataframe.loc[ ( (dataframe['adx'] > 70) & - (dataframe['tema'] > dataframe['blower']) & + (dataframe['tema'] > dataframe['bb_middleband']) & (dataframe['tema'] < dataframe['tema'].shift(1)) ), 'sell'] = 1 return dataframe ``` -## Add more Indicator +## Add more Indicators -As you have seen, buy and sell strategies need indicators. You can add -more indicators by extending the list contained in -the method `populate_indicators()` from your strategy file. +As you have seen, buy and sell strategies need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file. + +You should only add the indicators used in either `populate_buy_trend()`, `populate_sell_trend()`, or to populate another indicator, otherwise performance may suffer. Sample: ```python -def populate_indicators(dataframe: DataFrame) -> DataFrame: +def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Adds several different TA indicators to the given DataFrame + + Performance Note: For the best performance be frugal on the number of indicators + you are using. Let uncomment only the indicator you are using in your strategies + or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() + :param metadata: Additional information, like the currently traded pair + :return: a Dataframe with all mandatory indicators for the strategies """ dataframe['sar'] = ta.SAR(dataframe) dataframe['adx'] = ta.ADX(dataframe) @@ -149,6 +157,11 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame: return dataframe ``` +### Metadata dict + +The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `populate_indicators`) contains additional information. +Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`. + ### Want more indicator examples Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py). diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 25fc78f0a..4e479adac 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -117,18 +117,21 @@ python3 ./freqtrade/main.py -c config.json --db-url sqlite:///tradesv3.dry_run.s Backtesting also uses the config specified via `-c/--config`. ``` -usage: main.py backtesting [-h] [-i TICKER_INTERVAL] [--realistic-simulation] - [--timerange TIMERANGE] [-l] [-r] [--export EXPORT] - [--export-filename EXPORTFILENAME] - +usage: main.py backtesting [-h] [-i TICKER_INTERVAL] [--eps] [--dmmp] + [--timerange TIMERANGE] [-l] [-r] + [--export EXPORT] [--export-filename PATH] optional arguments: -h, --help show this help message and exit -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL specify ticker interval (1m, 5m, 30m, 1h, 1d) - --realistic-simulation - uses max_open_trades from config to simulate real - world limitations + --eps, --enable-position-stacking + Allow buying the same pair multiple times (position + stacking) + --dmmp, --disable-max-market-positions + Disable applying `max_open_trades` during backtest + (same as setting `max_open_trades` to a very high + number) --timerange TIMERANGE specify what timerange of data to use. -l, --live using live data @@ -138,11 +141,13 @@ optional arguments: run your backtesting with up-to-date data. --export EXPORT export backtest results, argument are: trades Example --export=trades - --export-filename EXPORTFILENAME + --export-filename PATH Save backtest results to this filename requires --export to be set as well Example --export- - filename=backtest_today.json (default: backtest- - result.json + filename=user_data/backtest_data/backtest_today.json + (default: user_data/backtest_data/backtest- + result.json) + ``` ### How to use --refresh-pairs-cached parameter? @@ -164,22 +169,28 @@ To optimize your strategy, you can use hyperopt parameter hyperoptimization to find optimal parameter values for your stategy. ``` -usage: main.py hyperopt [-h] [-i TICKER_INTERVAL] [--realistic-simulation] - [--timerange TIMERANGE] [-e INT] - [-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]] +usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--eps] [--dmmp] + [--timerange TIMERANGE] [-e INT] + [-s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...]] optional arguments: -h, --help show this help message and exit -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL specify ticker interval (1m, 5m, 30m, 1h, 1d) - --realistic-simulation - uses max_open_trades from config to simulate real - world limitations - --timerange TIMERANGE specify what timerange of data to use. + --eps, --enable-position-stacking + Allow buying the same pair multiple times (position + stacking) + --dmmp, --disable-max-market-positions + Disable applying `max_open_trades` during backtest + (same as setting `max_open_trades` to a very high + number) + --timerange TIMERANGE + specify what timerange of data to use. -e INT, --epochs INT specify number of epochs (default: 100) -s {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...], --spaces {all,buy,roi,stoploss} [{all,buy,roi,stoploss} ...] Specify which parameters to hyperopt. Space separate list. Default: all + ``` ## A parameter missing in the configuration? diff --git a/docs/configuration.md b/docs/configuration.md index 57c7a0bec..ff5ce118c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -27,6 +27,7 @@ The table below will list all configuration parameters. | `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file. | `trailing_stoploss` | false | No | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file). | `trailing_stoploss_positve` | 0 | No | Changes stop-loss once profit has been reached. +| `trailing_stoploss_positve_offset` | 0 | No | Offset on when to apply `trailing_stoploss_positive`. Percentage value which should be positive. | `unfilledtimeout.buy` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. | `unfilledtimeout.sell` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. | `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below. @@ -41,6 +42,11 @@ The table below will list all configuration parameters. | `telegram.enabled` | true | Yes | Enable or not the usage of Telegram. | `telegram.token` | token | No | Your Telegram bot token. Only required if `telegram.enabled` is `true`. | `telegram.chat_id` | chat_id | No | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. +| `webhook.enabled` | false | No | Enable useage of Webhook notifications +| `webhook.url` | false | No | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. +| `webhook.webhookbuy` | false | No | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. +| `webhook.webhooksell` | false | No | Payload to send on sell. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. +| `webhook.webhookstatus` | false | No | Payload to send on status calls. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. | `db_url` | `sqlite:///tradesv3.sqlite` | No | Declares database URL to use. NOTE: This defaults to `sqlite://` if `dry_run` is `True`. | `initial_state` | running | No | Defines the initial application state. More information below. | `strategy` | DefaultStrategy | No | Defines Strategy class to use. diff --git a/docs/index.md b/docs/index.md index fd6bf4378..730f1095e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -27,8 +27,10 @@ Pull-request. Do not hesitate to reach us on - [Test your strategy with Backtesting](https://github.com/freqtrade/freqtrade/blob/develop/docs/backtesting.md) - [Find optimal parameters with Hyperopt](https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md) - [Control the bot with telegram](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md) +- [Receive notifications via webhook](https://github.com/freqtrade/freqtrade/blob/develop/docs/webhook-config.md) - [Contribute to the project](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md) - [How to contribute](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md) - [Run tests & Check PEP8 compliance](https://github.com/freqtrade/freqtrade/blob/develop/CONTRIBUTING.md) - [FAQ](https://github.com/freqtrade/freqtrade/blob/develop/docs/faq.md) - [SQL cheatsheet](https://github.com/freqtrade/freqtrade/blob/develop/docs/sql_cheatsheet.md) +- [Sandbox Testing](https://github.com/freqtrade/freqtrade/blob/develop/docs/sandbox-testing.md)) diff --git a/docs/installation.md b/docs/installation.md index e5724a7dc..7a7719fc0 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -56,23 +56,29 @@ Reset parameter will hard reset your branch (only if you are on `master` or `dev Config parameter is a `config.json` configurator. This script will ask you questions to setup your bot and create your `config.json`. - ## Manual installation - Linux/MacOS + The following steps are made for Linux/MacOS environment -**1. Clone the repo** +### 1. Clone the repo + ```bash git clone git@github.com:freqtrade/freqtrade.git git checkout develop cd freqtrade ``` -**2. Create the config file** + +### 2. Create the config file + Switch `"dry_run": true,` + ```bash cp config.json.example config.json vi config.json ``` -**3. Build your docker image and run it** + +### 3. Build your docker image and run it + ```bash docker build -t freqtrade . docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade diff --git a/docs/plotting.md b/docs/plotting.md index 242d80005..54a4bb4b8 100644 --- a/docs/plotting.md +++ b/docs/plotting.md @@ -24,7 +24,7 @@ script/plot_dataframe.py [-h] [-p pair] [--live] Example ``` -python scripts/plot_dataframe.py -p BTC_ETH +python scripts/plot_dataframe.py -p BTC/ETH ``` The `-p` pair argument, can be used to specify what @@ -34,18 +34,18 @@ pair you would like to plot. To plot the current live price use the `--live` flag: ``` -python scripts/plot_dataframe.py -p BTC_ETH --live +python scripts/plot_dataframe.py -p BTC/ETH --live ``` To plot a timerange (to zoom in): ``` -python scripts/plot_dataframe.py -p BTC_ETH --timerange=100-200 +python scripts/plot_dataframe.py -p BTC/ETH --timerange=100-200 ``` Timerange doesn't work with live data. To plot trades stored in a database use `--db-url` argument: ``` -python scripts/plot_dataframe.py --db-url tradesv3.dry_run.sqlite -p BTC_ETH +python scripts/plot_dataframe.py --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH ``` To plot a test strategy the strategy should have first be backtested. diff --git a/docs/sandbox-testing.md b/docs/sandbox-testing.md new file mode 100644 index 000000000..572fbccef --- /dev/null +++ b/docs/sandbox-testing.md @@ -0,0 +1,151 @@ +# Sandbox API testing +Where an exchange provides a sandbox for risk-free integration, or end-to-end, testing CCXT provides access to these. + +This document is a *light overview of configuring Freqtrade and GDAX sandbox. +This can be useful to developers and trader alike as Freqtrade is quite customisable. + +When testing your API connectivity, make sure to use the following URLs. +***Website** +https://public.sandbox.gdax.com +***REST API** +https://api-public.sandbox.gdax.com + +--- +# Configure a Sandbox account on Gdax +Aim of this document section +- An sanbox account +- create 2FA (needed to create an API) +- Add test 50BTC to account +- Create : +- - API-KEY +- - API-Secret +- - API Password + +## Acccount + +This link will redirect to the sandbox main page to login / create account dialogues: +https://public.sandbox.pro.coinbase.com/orders/ + +After registration and Email confimation you wil be redirected into your sanbox account. It is easy to verify you're in sandbox by checking the URL bar. +> https://public.sandbox.pro.coinbase.com/ + +## Enable 2Fa (a prerequisite to creating sandbox API Keys) +From within sand box site select your profile, top right. +>Or as a direct link: https://public.sandbox.pro.coinbase.com/profile + +From the menu panel to the left of the screen select +> Security: "*View or Update*" + +In the new site select "enable authenticator" as typical google Authenticator. +- open Google Authenticator on your phone +- scan barcode +- enter your generated 2fa + +## Enable API Access +From within sandbox select profile>api>create api-keys +>or as a direct link: https://public.sandbox.pro.coinbase.com/profile/api + +Click on "create one" and ensure **view** and **trade** are "checked" and sumbit your 2Fa +- **Copy and paste the Passphase** into a notepade this will be needed later +- **Copy and paste the API Secret** popup into a notepad this will needed later +- **Copy and paste the API Key** into a notepad this will needed later + +## Add 50 BTC test funds +To add funds, use the web interface deposit and withdraw buttons. + + +To begin select 'Wallets' from the top menu. +> Or as a direct link: https://public.sandbox.pro.coinbase.com/wallets + +- Deposits (bottom left of screen) +- - Deposit Funds Bitcoin +- - - Coinbase BTC Wallet +- - - - Max (50 BTC) +- - - - - Deposit + +*This process may be repeated for other currencies, ETH as example* +--- +# Configure Freqtrade to use Gax Sandbox + +The aim of this document section + - Enable sandbox URLs in Freqtrade + - Configure API + - - secret + - - key + - - passphrase + +## Sandbox URLs +Freqtrade makes use of CCXT which in turn provides a list of URLs to Freqtrade. +These include `['test']` and `['api']`. +- `[Test]` if available will point to an Exchanges sandbox. +- `[Api]` normally used, and resolves to live API target on the exchange + +To make use of sandbox / test add "sandbox": true, to your config.json +``` + "exchange": { + "name": "gdax", + "sandbox": true, + "key": "5wowfxemogxeowo;heiohgmd", + "secret": "/ZMH1P62rCVmwefewrgcewX8nh4gob+lywxfwfxwwfxwfNsH1ySgvWCUR/w==", + "password": "1bkjfkhfhfu6sr", + "pair_whitelist": [ + "BTC/USD" +``` +Also insert your +- api-key (noted earlier) +- api-secret (noted earlier) +- password (the passphrase - noted earlier) + +--- +## You should now be ready to test your sandbox! +Ensure Freqtrade logs show the sandbox URL, and trades made are shown in sandbox. +** Typically the BTC/USD has the most activity in sandbox to test against. + +## GDAX - Old Candles problem +It is my experience that GDAX sandbox candles may be 20+- minutes out of date. This can cause trades to fail as one of Freqtrades safety checks + +To disable this check, edit: +>strategy/interface.py +Look for the following section: +``` + # Check if dataframe is out of date + signal_date = arrow.get(latest['date']) + interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval] + if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5))): + logger.warning( + 'Outdated history for pair %s. Last tick is %s minutes old', + pair, + (arrow.utcnow() - signal_date).seconds // 60 + ) + return False, False +``` + +You could Hash out the entire check as follows: +``` + # # Check if dataframe is out of date + # signal_date = arrow.get(latest['date']) + # interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval] + # if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5))): + # logger.warning( + # 'Outdated history for pair %s. Last tick is %s minutes old', + # pair, + # (arrow.utcnow() - signal_date).seconds // 60 + # ) + # return False, False + ``` + + Or inrease the timeout to offer a level of protection/alignment of this test to freqtrade in live. + + As example, to allow an additional 30 minutes. "(interval_minutes * 2 + 5 + 30)" + ``` + # Check if dataframe is out of date + signal_date = arrow.get(latest['date']) + interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval] + if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5 + 30))): + logger.warning( + 'Outdated history for pair %s. Last tick is %s minutes old', + pair, + (arrow.utcnow() - signal_date).seconds // 60 + ) + return False, False +``` \ No newline at end of file diff --git a/docs/sql_cheatsheet.md b/docs/sql_cheatsheet.md index ba26f1707..ff0b92347 100644 --- a/docs/sql_cheatsheet.md +++ b/docs/sql_cheatsheet.md @@ -59,7 +59,7 @@ SELECT * FROM trades; ```sql UPDATE trades -SET is_open=0, close_date=, close_rate=, close_profit=close_rate/open_rate +SET is_open=0, close_date=, close_rate=, close_profit=close_rate/open_rate-1 WHERE id=; ``` diff --git a/docs/stoploss.md b/docs/stoploss.md index db4433a02..9740672cd 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -35,14 +35,17 @@ basically what this means is that your stop loss will be adjusted to be always b ### Custom positive loss -Due to demand, it is possible to have a default stop loss, when you are in the red with your buy, but once your buy turns positive, -the system will utilize a new stop loss, which can be a different value. For example your default stop loss is 5%, but once you are in the -black, it will be changed to be only a 1% stop loss +Due to demand, it is possible to have a default stop loss, when you are in the red with your buy, but once your profit surpasses a certain percentage, +the system will utilize a new stop loss, which can be a different value. For example your default stop loss is 5%, but once you have 1.1% profit, +it will be changed to be only a 1% stop loss, which trails the green candles until it goes below them. -This can be configured in the main configuration file and requires `"trailing_stop": true` to be set to true. +Both values can be configured in the main configuration file and requires `"trailing_stop": true` to be set to true. ``` json "trailing_stop_positive": 0.01, + "trailing_stop_positive_offset": 0.011, ``` -The 0.01 would translate to a 1% stop loss, once you hit profit. +The 0.01 would translate to a 1% stop loss, once you hit 1.1% profit. + +You should also make sure to have this value higher than your minimal ROI, otherwise minimal ROI will apply first and sell your trade. diff --git a/docs/webhook-config.md b/docs/webhook-config.md new file mode 100644 index 000000000..71524187a --- /dev/null +++ b/docs/webhook-config.md @@ -0,0 +1,74 @@ +# Webhook usage + +This page explains how to configure your bot to talk to webhooks. + +## Configuration + +Enable webhooks by adding a webhook-section to your configuration file, and setting `webhook.enabled` to `true`. + +Sample configuration (tested using IFTTT). + +```json + "webhook": { + "enabled": true, + "url": "https://maker.ifttt.com/trigger//with/key//", + "webhookbuy": { + "value1": "Buying {pair}", + "value2": "limit {limit:8f}", + "value3": "{stake_amount:8f} {stake_currency}" + }, + "webhooksell": { + "value1": "Selling {pair}", + "value2": "limit {limit:8f}", + "value3": "profit: {profit_amount:8f} {stake_currency}" + }, + "webhookstatus": { + "value1": "Status: {status}", + "value2": "", + "value3": "" + } + }, +``` + +The url in `webhook.url` should point to the correct url for your webhook. If you're using [IFTTT](https://ifttt.com) (as shown in the sample above) please insert our event and key to the url. + +Different payloads can be configured for different events. Not all fields are necessary, but you should configure at least one of the dicts, otherwise the webhook will never be called. + +### Webhookbuy + +The fields in `webhook.webhookbuy` are filled when the bot executes a buy. Parameters are filled using string.format. +Possible parameters are: + +* exchange +* pair +* market_url +* limit +* stake_amount +* stake_amount_fiat +* stake_currency +* fiat_currency + +### Webhooksell + +The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format. +Possible parameters are: + +* exchange +* pair +* gain +* market_url +* limit +* amount +* open_rate +* current_rate +* profit_amount +* profit_percent +* profit_fiat +* stake_currency +* fiat_currency + +### Webhookstatus + +The fields in `webhook.webhookstatus` are used for regular status messages (Started / Stopped / ...). Parameters are filled using string.format. + +The only possible value here is `{status}`. diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py deleted file mode 100644 index 6d6a85596..000000000 --- a/freqtrade/analyze.py +++ /dev/null @@ -1,270 +0,0 @@ -""" -Functions to analyze ticker data with indicators and produce buy and sell signals -""" -import logging -from datetime import datetime, timedelta -from enum import Enum -from typing import Dict, List, Tuple - -import arrow -from pandas import DataFrame, to_datetime - -from freqtrade import constants -from freqtrade.exchange import Exchange -from freqtrade.persistence import Trade -from freqtrade.strategy.resolver import IStrategy, StrategyResolver - -logger = logging.getLogger(__name__) - - -class SignalType(Enum): - """ - Enum to distinguish between buy and sell signals - """ - BUY = "buy" - SELL = "sell" - - -class Analyze(object): - """ - Analyze class contains everything the bot need to determine if the situation is good for - buying or selling. - """ - def __init__(self, config: dict) -> None: - """ - Init Analyze - :param config: Bot configuration (use the one from Configuration()) - """ - self.config = config - self.strategy: IStrategy = StrategyResolver(self.config).strategy - - @staticmethod - def parse_ticker_dataframe(ticker: list) -> DataFrame: - """ - Analyses the trend for the given ticker history - :param ticker: See exchange.get_ticker_history - :return: DataFrame - """ - cols = ['date', 'open', 'high', 'low', 'close', 'volume'] - frame = DataFrame(ticker, columns=cols) - - frame['date'] = to_datetime(frame['date'], - unit='ms', - utc=True, - infer_datetime_format=True) - - # group by index and aggregate results to eliminate duplicate ticks - frame = frame.groupby(by='date', as_index=False, sort=True).agg({ - 'open': 'first', - 'high': 'max', - 'low': 'min', - 'close': 'last', - 'volume': 'max', - }) - frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle - return frame - - def populate_indicators(self, dataframe: DataFrame) -> DataFrame: - """ - Adds several different TA indicators to the given DataFrame - - Performance Note: For the best performance be frugal on the number of indicators - you are using. Let uncomment only the indicator you are using in your strategies - or your hyperopt configuration, otherwise you will waste your memory and CPU usage. - """ - return self.strategy.populate_indicators(dataframe=dataframe) - - def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: - """ - Based on TA indicators, populates the buy signal for the given dataframe - :param dataframe: DataFrame - :return: DataFrame with buy column - """ - return self.strategy.populate_buy_trend(dataframe=dataframe) - - def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: - """ - Based on TA indicators, populates the sell signal for the given dataframe - :param dataframe: DataFrame - :return: DataFrame with buy column - """ - return self.strategy.populate_sell_trend(dataframe=dataframe) - - def get_ticker_interval(self) -> str: - """ - Return ticker interval to use - :return: Ticker interval value to use - """ - return self.strategy.ticker_interval - - def get_stoploss(self) -> float: - """ - Return stoploss to use - :return: Strategy stoploss value to use - """ - return self.strategy.stoploss - - def analyze_ticker(self, ticker_history: List[Dict]) -> DataFrame: - """ - Parses the given ticker history and returns a populated DataFrame - add several TA indicators and buy signal to it - :return DataFrame with ticker data and indicator data - """ - dataframe = self.parse_ticker_dataframe(ticker_history) - dataframe = self.populate_indicators(dataframe) - dataframe = self.populate_buy_trend(dataframe) - dataframe = self.populate_sell_trend(dataframe) - return dataframe - - def get_signal(self, exchange: Exchange, pair: str, interval: str) -> Tuple[bool, bool]: - """ - Calculates current signal based several technical analysis indicators - :param pair: pair in format ANT/BTC - :param interval: Interval to use (in min) - :return: (Buy, Sell) A bool-tuple indicating buy/sell signal - """ - ticker_hist = exchange.get_ticker_history(pair, interval) - if not ticker_hist: - logger.warning('Empty ticker history for pair %s', pair) - return False, False - - try: - dataframe = self.analyze_ticker(ticker_hist) - except ValueError as error: - logger.warning( - 'Unable to analyze ticker for pair %s: %s', - pair, - str(error) - ) - return False, False - except Exception as error: - logger.exception( - 'Unexpected error when analyzing ticker for pair %s: %s', - pair, - str(error) - ) - return False, False - - if dataframe.empty: - logger.warning('Empty dataframe for pair %s', pair) - return False, False - - latest = dataframe.iloc[-1] - - # Check if dataframe is out of date - signal_date = arrow.get(latest['date']) - interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval] - if signal_date < (arrow.utcnow() - timedelta(minutes=(interval_minutes + 5))): - logger.warning( - 'Outdated history for pair %s. Last tick is %s minutes old', - pair, - (arrow.utcnow() - signal_date).seconds // 60 - ) - return False, False - - (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) - ) - return buy, sell - - def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, sell: bool) -> bool: - """ - This function evaluate if on the condition required to trigger a sell has been reached - if the threshold is reached and updates the trade record. - :return: True if trade should be sold, False otherwise - """ - current_profit = trade.calc_profit_percent(rate) - if self.stop_loss_reached(current_rate=rate, trade=trade, current_time=date): - return True - - experimental = self.config.get('experimental', {}) - - if buy and experimental.get('ignore_roi_if_buy_signal', False): - logger.debug('Buy signal still active - not selling.') - return False - - # Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee) - if self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date): - logger.debug('Required profit reached. Selling..') - return True - - if experimental.get('sell_profit_only', False): - logger.debug('Checking if trade is profitable..') - if trade.calc_profit(rate=rate) <= 0: - return False - if sell and not buy and experimental.get('use_sell_signal', False): - logger.debug('Sell signal received. Selling..') - return True - - return False - - def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime) -> bool: - """ - Based on current profit of the trade and configured (trailing) stoploss, - decides to sell or not - """ - - current_profit = trade.calc_profit_percent(current_rate) - trailing_stop = self.config.get('trailing_stop', False) - - trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True) - - # evaluate if the stoploss was hit - if self.strategy.stoploss is not None and trade.stop_loss >= current_rate: - - if trailing_stop: - logger.debug( - f"HIT STOP: current price at {current_rate:.6f}, " - f"stop loss is {trade.stop_loss:.6f}, " - f"initial stop loss was at {trade.initial_stop_loss:.6f}, " - f"trade opened at {trade.open_rate:.6f}") - logger.debug(f"trailing stop saved {trade.stop_loss - trade.initial_stop_loss:.6f}") - - logger.debug('Stop loss hit.') - return True - - # update the stop loss afterwards, after all by definition it's supposed to be hanging - if trailing_stop: - - # check if we have a special stop loss for positive condition - # and if profit is positive - stop_loss_value = self.strategy.stoploss - if 'trailing_stop_positive' in self.config and current_profit > 0: - - # Ignore mypy error check in configuration that this is a float - stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore - logger.debug(f"using positive stop loss mode: {stop_loss_value} " - f"since we have profit {current_profit}") - - trade.adjust_stop_loss(current_rate, stop_loss_value) - - return False - - def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool: - """ - Based an earlier trade and current price and ROI configuration, decides whether bot should - sell - :return True if bot should sell at current rate - """ - - # Check if time matches and current rate is above threshold - time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60 - for duration, threshold in self.strategy.minimal_roi.items(): - if time_diff <= duration: - return False - if current_profit > threshold: - return True - - return False - - def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]: - """ - Creates a dataframe and populates indicators for given ticker data - """ - return {pair: self.populate_indicators(self.parse_ticker_dataframe(pair_data)) - for pair, pair_data in tickerdata.items()} diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 731c5d88c..022a2c739 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -3,7 +3,6 @@ This module contains the argument manager class """ import argparse -import logging import os import re from typing import List, NamedTuple, Optional @@ -64,11 +63,10 @@ class Arguments(object): """ self.parser.add_argument( '-v', '--verbose', - help='be verbose', - action='store_const', + help='verbose mode (-vv for more, -vvv to get all messages)', + action='count', dest='loglevel', - const=logging.DEBUG, - default=logging.INFO, + default=0, ) self.parser.add_argument( '--version', @@ -178,11 +176,22 @@ class Arguments(object): type=str, ) parser.add_argument( - '--realistic-simulation', - help='uses max_open_trades from config to simulate real world limitations', + '--eps', '--enable-position-stacking', + help='Allow buying the same pair multiple times (position stacking)', action='store_true', - dest='realistic_simulation', + dest='position_stacking', + default=False ) + + parser.add_argument( + '--dmmp', '--disable-max-market-positions', + help='Disable applying `max_open_trades` during backtest ' + '(same as setting `max_open_trades` to a very high number)', + action='store_false', + dest='use_max_market_positions', + default=True + ) + parser.add_argument( '--timerange', help='specify what timerange of data to use.', diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index 582b2889c..dcc6e4332 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -12,10 +12,22 @@ from jsonschema import Draft4Validator, validate from jsonschema.exceptions import ValidationError, best_match from freqtrade import OperationalException, constants - logger = logging.getLogger(__name__) +def set_loggers(log_level: int = 0) -> None: + """ + Set the logger level for Third party libs + :return: None + """ + + logging.getLogger('requests').setLevel(logging.INFO if log_level <= 1 else logging.DEBUG) + logging.getLogger("urllib3").setLevel(logging.INFO if log_level <= 1 else logging.DEBUG) + logging.getLogger('ccxt.base.exchange').setLevel( + logging.INFO if log_level <= 2 else logging.DEBUG) + logging.getLogger('telegram').setLevel(logging.INFO) + + class Configuration(object): """ Class to read and init the bot configuration @@ -79,12 +91,15 @@ class Configuration(object): # Log level if 'loglevel' in self.args and self.args.loglevel: - config.update({'loglevel': self.args.loglevel}) - logging.basicConfig( - level=config['loglevel'], - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - ) - logger.info('Log level set to %s', logging.getLevelName(config['loglevel'])) + config.update({'verbosity': self.args.loglevel}) + else: + config.update({'verbosity': 0}) + logging.basicConfig( + level=logging.INFO if config['verbosity'] < 1 else logging.DEBUG, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + ) + set_loggers(config['verbosity']) + logger.info('Verbosity set to %s', config['verbosity']) # Add dynamic_whitelist if found if 'dynamic_whitelist' in self.args and self.args.dynamic_whitelist: @@ -142,11 +157,18 @@ class Configuration(object): config.update({'live': True}) logger.info('Parameter -l/--live detected ...') - # If --realistic-simulation is used we add it to the configuration - if 'realistic_simulation' in self.args and self.args.realistic_simulation: - config.update({'realistic_simulation': True}) - logger.info('Parameter --realistic-simulation detected ...') - logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) + # If --enable-position-stacking is used we add it to the configuration + if 'position_stacking' in self.args and self.args.position_stacking: + config.update({'position_stacking': True}) + logger.info('Parameter --enable-position-stacking detected ...') + + # If --disable-max-market-positions is used we add it to the configuration + if 'use_max_market_positions' in self.args and not self.args.use_max_market_positions: + config.update({'use_max_market_positions': False}) + logger.info('Parameter --disable-max-market-positions detected ...') + logger.info('max_open_trades set to unlimited ...') + else: + logger.info('Using max_open_trades: %s ...', config.get('max_open_trades')) # If --timerange is used we add it to the configuration if 'timerange' in self.args and self.args.timerange: @@ -182,7 +204,7 @@ class Configuration(object): Extract information for sys.argv and load Hyperopt configuration :return: configuration as dictionary """ - # If --realistic-simulation is used we add it to the configuration + # If --epochs is used we add it to the configuration if 'epochs' in self.args and self.args.epochs: config.update({'epochs': self.args.epochs}) logger.info('Parameter --epochs detected ...') diff --git a/freqtrade/constants.py b/freqtrade/constants.py index ec7765455..87e354455 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -63,6 +63,7 @@ CONF_SCHEMA = { 'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True}, 'trailing_stop': {'type': 'boolean'}, 'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1}, + 'trailing_stop_positive_offset': {'type': 'number', 'minimum': 0, 'maximum': 1}, 'unfilledtimeout': { 'type': 'object', 'properties': { @@ -100,6 +101,15 @@ CONF_SCHEMA = { }, 'required': ['enabled', 'token', 'chat_id'] }, + 'webhook': { + 'type': 'object', + 'properties': { + 'enabled': {'type': 'boolean'}, + 'webhookbuy': {'type': 'object'}, + 'webhooksell': {'type': 'object'}, + 'webhookstatus': {'type': 'object'}, + }, + }, 'db_url': {'type': 'string'}, 'initial_state': {'type': 'string', 'enum': ['running', 'stopped']}, 'internals': { @@ -115,8 +125,11 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'name': {'type': 'string'}, + 'sandbox': {'type': 'boolean'}, 'key': {'type': 'string'}, 'secret': {'type': 'string'}, + 'password': {'type': 'string'}, + 'uid': {'type': 'string'}, 'pair_whitelist': { 'type': 'array', 'items': { @@ -144,7 +157,6 @@ CONF_SCHEMA = { 'max_open_trades', 'stake_currency', 'stake_amount', - 'fiat_display_currency', 'dry_run', 'bid_strategy', 'telegram' diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index acfefdad4..810957902 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -4,6 +4,7 @@ import logging from random import randint from typing import List, Dict, Any, Optional from datetime import datetime +from math import floor, ceil import ccxt import arrow @@ -70,6 +71,10 @@ class Exchange(object): # Check if all pairs are available self.validate_pairs(config['exchange']['pair_whitelist']) + if config.get('ticker_interval'): + # Check if timeframe is available + self.validate_timeframes(config['ticker_interval']) + def _init_ccxt(self, exchange_config: dict) -> ccxt.Exchange: """ Initialize ccxt with given config and return valid @@ -86,11 +91,13 @@ class Exchange(object): 'secret': exchange_config.get('secret'), 'password': exchange_config.get('password'), 'uid': exchange_config.get('uid', ''), - 'enableRateLimit': True, + 'enableRateLimit': exchange_config.get('ccxt_rate_limit', True), }) except (KeyError, AttributeError): raise OperationalException(f'Exchange {name} is not supported') + self.set_sandbox(api, exchange_config, name) + return api @property @@ -103,6 +110,16 @@ class Exchange(object): """exchange ccxt id""" return self._api.id + def set_sandbox(self, api, exchange_config: dict, name: str): + if exchange_config.get('sandbox'): + if api.urls.get('test'): + api.urls['api'] = api.urls['test'] + logger.info("Enabled Sandbox API on %s", name) + else: + logger.warning(self, "No Sandbox URL in CCXT, exiting. " + "Please check your config.json") + raise OperationalException(f'Exchange {name} does not provide a sandbox api') + def validate_pairs(self, pairs: List[str]) -> None: """ Checks if all given pairs are tradable on the current exchange. @@ -128,6 +145,15 @@ class Exchange(object): raise OperationalException( f'Pair {pair} is not available at {self.name}') + def validate_timeframes(self, timeframe: List[str]) -> None: + """ + Checks if ticker interval from config is a supported timeframe on the exchange + """ + timeframes = self._api.timeframes + if timeframe not in timeframes: + raise OperationalException( + f'Invalid ticker {timeframe}, this Exchange supports {timeframes}') + def exchange_has(self, endpoint: str) -> bool: """ Checks if exchange implements a specific API endpoint. @@ -137,6 +163,28 @@ class Exchange(object): """ return endpoint in self._api.has and self._api.has[endpoint] + def symbol_amount_prec(self, pair, amount: float): + ''' + Returns the amount to buy or sell to a precision the Exchange accepts + Rounded down + ''' + if self._api.markets[pair]['precision']['amount']: + symbol_prec = self._api.markets[pair]['precision']['amount'] + big_amount = amount * pow(10, symbol_prec) + amount = floor(big_amount) / pow(10, symbol_prec) + return amount + + def symbol_price_prec(self, pair, price: float): + ''' + Returns the price buying or selling with to the precision the Exchange accepts + Rounds up + ''' + if self._api.markets[pair]['precision']['price']: + symbol_prec = self._api.markets[pair]['precision']['price'] + big_price = price * pow(10, symbol_prec) + price = ceil(big_price) / pow(10, symbol_prec) + return price + def buy(self, pair: str, rate: float, amount: float) -> Dict: if self._conf['dry_run']: order_id = f'dry_run_buy_{randint(0, 10**6)}' @@ -154,6 +202,10 @@ class Exchange(object): return {'id': order_id} try: + # Set the precision for amount and price(rate) as accepted by the exchange + amount = self.symbol_amount_prec(pair, amount) + rate = self.symbol_price_prec(pair, rate) + return self._api.create_limit_buy_order(pair, amount, rate) except ccxt.InsufficientFunds as e: raise DependencyException( @@ -187,6 +239,10 @@ class Exchange(object): return {'id': order_id} try: + # Set the precision for amount and price(rate) as accepted by the exchange + amount = self.symbol_amount_prec(pair, amount) + rate = self.symbol_price_prec(pair, rate) + return self._api.create_limit_sell_order(pair, amount, rate) except ccxt.InsufficientFunds as e: raise DependencyException( diff --git a/freqtrade/exchange/exchange_helpers.py b/freqtrade/exchange/exchange_helpers.py new file mode 100644 index 000000000..254c16309 --- /dev/null +++ b/freqtrade/exchange/exchange_helpers.py @@ -0,0 +1,33 @@ +""" +Functions to analyze ticker data with indicators and produce buy and sell signals +""" +import logging +from pandas import DataFrame, to_datetime + +logger = logging.getLogger(__name__) + + +def parse_ticker_dataframe(ticker: list) -> DataFrame: + """ + Analyses the trend for the given ticker history + :param ticker: See exchange.get_ticker_history + :return: DataFrame + """ + cols = ['date', 'open', 'high', 'low', 'close', 'volume'] + frame = DataFrame(ticker, columns=cols) + + frame['date'] = to_datetime(frame['date'], + unit='ms', + utc=True, + infer_datetime_format=True) + + # group by index and aggregate results to eliminate duplicate ticks + frame = frame.groupby(by='date', as_index=False, sort=True).agg({ + 'open': 'first', + 'high': 'max', + 'low': 'min', + 'close': 'last', + 'volume': 'max', + }) + frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle + return frame diff --git a/freqtrade/fiat_convert.py b/freqtrade/fiat_convert.py index 2e1a7cac8..6812bf77f 100644 --- a/freqtrade/fiat_convert.py +++ b/freqtrade/fiat_convert.py @@ -7,7 +7,6 @@ import logging import time from typing import Dict, List -from requests.exceptions import RequestException from coinmarketcap import Market from freqtrade.constants import SUPPORTED_FIAT @@ -90,10 +89,10 @@ class CryptoToFiatConverter(object): coinlistings = self._coinmarketcap.listings() self._cryptomap = dict(map(lambda coin: (coin["symbol"], str(coin["id"])), coinlistings["data"])) - except (ValueError, RequestException) as exception: + except (BaseException) as exception: logger.error( "Could not load FIAT Cryptocurrency map for the following problem: %s", - exception + type(exception).__name__ ) def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 9def7078c..46fbb3a38 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -15,12 +15,12 @@ from cachetools import TTLCache, cached from freqtrade import (DependencyException, OperationalException, TemporaryError, __version__, constants, persistence) -from freqtrade.analyze import Analyze from freqtrade.exchange import Exchange -from freqtrade.fiat_convert import CryptoToFiatConverter from freqtrade.persistence import Trade -from freqtrade.rpc.rpc_manager import RPCManager +from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.state import State +from freqtrade.strategy.interface import SellType +from freqtrade.strategy.resolver import IStrategy, StrategyResolver logger = logging.getLogger(__name__) @@ -48,12 +48,10 @@ class FreqtradeBot(object): # Init objects self.config = config - self.analyze = Analyze(self.config) - self.fiat_converter = CryptoToFiatConverter() + self.strategy: IStrategy = StrategyResolver(self.config).strategy self.rpc: RPCManager = RPCManager(self) self.persistence = None self.exchange = Exchange(self.config) - self._init_modules() def _init_modules(self) -> None: @@ -91,7 +89,10 @@ class FreqtradeBot(object): # Log state transition state = self.state if state != old_state: - self.rpc.send_msg(f'*Status:* `{state.name.lower()}`') + self.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': f'{state.name.lower()}' + }) logger.info('Changing state to: %s', state.name) if state == State.STOPPED: @@ -167,9 +168,10 @@ class FreqtradeBot(object): except OperationalException: tb = traceback.format_exc() hint = 'Issue `/start` if you think it is safe to restart.' - self.rpc.send_msg( - f'*Status:* OperationalException:\n```\n{tb}```{hint}' - ) + self.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': f'OperationalException:\n```\n{tb}```{hint}' + }) logger.exception('OperationalException. Stopping trader ...') self.state = State.STOPPED return state_changed @@ -243,6 +245,11 @@ class FreqtradeBot(object): return ticker['ask'] + balance * (ticker['last'] - ticker['ask']) def _get_trade_stake_amount(self) -> Optional[float]: + """ + Check if stake amount can be fulfilled with the available balance + for the stake currency + :return: float: Stake Amount + """ stake_amount = self.config['stake_amount'] avaliable_amount = self.exchange.get_balance(self.config['stake_currency']) @@ -288,8 +295,8 @@ class FreqtradeBot(object): return None amount_reserve_percent = 1 - 0.05 # reserve 5% + stoploss - if self.analyze.get_stoploss() is not None: - amount_reserve_percent += self.analyze.get_stoploss() + if self.strategy.stoploss is not None: + amount_reserve_percent += self.strategy.stoploss # it should not be more than 50% amount_reserve_percent = max(amount_reserve_percent, 0.5) return min(min_stake_amounts)/amount_reserve_percent @@ -300,14 +307,11 @@ class FreqtradeBot(object): if one pair triggers the buy_signal a new trade record gets created :return: True if a trade object has been created and persisted, False otherwise """ - interval = self.analyze.get_ticker_interval() + interval = self.strategy.ticker_interval stake_amount = self._get_trade_stake_amount() if not stake_amount: return False - stake_currency = self.config['stake_currency'] - fiat_currency = self.config['fiat_display_currency'] - exc_name = self.exchange.name logger.info( 'Checking buy signals to create a new trade with stake_amount: %f ...', @@ -326,14 +330,23 @@ class FreqtradeBot(object): # Pick pair based on buy signals for _pair in whitelist: - (buy, sell) = self.analyze.get_signal(self.exchange, _pair, interval) + thistory = self.exchange.get_ticker_history(_pair, interval) + (buy, sell) = self.strategy.get_signal(_pair, interval, thistory) + if buy and not sell: - pair = _pair - break - else: - return False + return self.execute_buy(_pair, stake_amount) + return False + + def execute_buy(self, pair: str, stake_amount: float) -> bool: + """ + Executes a limit buy for the given pair + :param pair: pair for which we want to create a LIMIT_BUY + :return: None + """ pair_s = pair.replace('_', '/') pair_url = self.exchange.get_pair_detail_url(pair) + stake_currency = self.config['stake_currency'] + fiat_currency = self.config.get('fiat_display_currency', None) # Calculate amount buy_limit = self.get_target_bid(self.exchange.get_ticker(pair)) @@ -350,18 +363,16 @@ class FreqtradeBot(object): order_id = self.exchange.buy(pair, buy_limit, amount)['id'] - stake_amount_fiat = self.fiat_converter.convert_amount( - stake_amount, - stake_currency, - fiat_currency - ) - - # Create trade entity and return - self.rpc.send_msg( - f"""*{exc_name}:* Buying [{pair_s}]({pair_url}) \ -with limit `{buy_limit:.8f} ({stake_amount:.6f} \ -{stake_currency}, {stake_amount_fiat:.3f} {fiat_currency})`""" - ) + self.rpc.send_msg({ + 'type': RPCMessageType.BUY_NOTIFICATION, + 'exchange': self.exchange.name.capitalize(), + 'pair': pair_s, + 'market_url': pair_url, + 'limit': buy_limit, + 'stake_amount': stake_amount, + 'stake_currency': stake_currency, + 'fiat_currency': fiat_currency + }) # Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker') trade = Trade( @@ -374,7 +385,9 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ open_rate_requested=buy_limit, open_date=datetime.utcnow(), exchange=self.exchange.id, - open_order_id=order_id + open_order_id=order_id, + strategy=self.strategy.get_strategy_name(), + ticker_interval=constants.TICKER_INTERVAL_MINUTES[self.config['ticker_interval']] ) Trade.session.add(trade) Trade.session.flush() @@ -484,11 +497,13 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ (buy, sell) = (False, False) experimental = self.config.get('experimental', {}) if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'): - (buy, sell) = self.analyze.get_signal(self.exchange, - trade.pair, self.analyze.get_ticker_interval()) + ticker = self.exchange.get_ticker_history(trade.pair, self.strategy.ticker_interval) + (buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.ticker_interval, + ticker) - if self.analyze.should_sell(trade, current_rate, datetime.utcnow(), buy, sell): - self.execute_sell(trade, current_rate) + should_sell = self.strategy.should_sell(trade, current_rate, datetime.utcnow(), buy, sell) + if should_sell.sell_flag: + self.execute_sell(trade, current_rate, should_sell.sell_type) return True logger.info('Found no sell signals for whitelisted currencies. Trying again..') return False @@ -546,7 +561,10 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ Trade.session.delete(trade) Trade.session.flush() logger.info('Buy order timeout for %s.', trade) - self.rpc.send_msg(f'*Timeout:* Unfilled buy order for {pair_s} cancelled') + self.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': f'Unfilled buy order for {pair_s} cancelled due to timeout' + }) return True # if trade is partially complete, edit the stake details for the trade @@ -555,7 +573,10 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ trade.stake_amount = trade.amount * trade.open_rate trade.open_order_id = None logger.info('Partial buy order timeout for %s.', trade) - self.rpc.send_msg(f'*Timeout:* Remaining buy order for {pair_s} cancelled') + self.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': f'Remaining buy order for {pair_s} cancelled due to timeout' + }) return False # FIX: 20180110, should cancel_order() be cond. or unconditionally called? @@ -573,61 +594,59 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ trade.close_date = None trade.is_open = True trade.open_order_id = None - self.rpc.send_msg(f'*Timeout:* Unfilled sell order for {pair_s} cancelled') + self.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': f'Unfilled sell order for {pair_s} cancelled due to timeout' + }) logger.info('Sell order timeout for %s.', trade) return True # TODO: figure out how to handle partially complete sell orders return False - def execute_sell(self, trade: Trade, limit: float) -> None: + def execute_sell(self, trade: Trade, limit: float, sell_reason: SellType) -> None: """ Executes a limit sell for the given trade and limit :param trade: Trade instance :param limit: limit rate for the sell order + :param sellreason: Reason the sell was triggered :return: None """ - exc = trade.exchange - pair = trade.pair # Execute sell and update trade record order_id = self.exchange.sell(str(trade.pair), limit, trade.amount)['id'] trade.open_order_id = order_id trade.close_rate_requested = limit + trade.sell_reason = sell_reason.value - fmt_exp_profit = round(trade.calc_profit_percent(rate=limit) * 100, 2) profit_trade = trade.calc_profit(rate=limit) current_rate = self.exchange.get_ticker(trade.pair)['bid'] - profit = trade.calc_profit_percent(limit) + profit_percent = trade.calc_profit_percent(limit) pair_url = self.exchange.get_pair_detail_url(trade.pair) - gain = "profit" if fmt_exp_profit > 0 else "loss" + gain = "profit" if profit_percent > 0 else "loss" - message = f"*{exc}:* Selling\n" \ - f"*Current Pair:* [{pair}]({pair_url})\n" \ - f"*Limit:* `{limit}`\n" \ - f"*Amount:* `{round(trade.amount, 8)}`\n" \ - f"*Open Rate:* `{trade.open_rate:.8f}`\n" \ - f"*Current Rate:* `{current_rate:.8f}`\n" \ - f"*Profit:* `{round(profit * 100, 2):.2f}%`" \ - "" + msg = { + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': trade.exchange.capitalize(), + 'pair': trade.pair, + 'gain': gain, + 'market_url': pair_url, + 'limit': limit, + 'amount': trade.amount, + 'open_rate': trade.open_rate, + 'current_rate': current_rate, + 'profit_amount': profit_trade, + 'profit_percent': profit_percent, + } # For regular case, when the configuration exists if 'stake_currency' in self.config and 'fiat_display_currency' in self.config: - stake = self.config['stake_currency'] - fiat = self.config['fiat_display_currency'] - fiat_converter = CryptoToFiatConverter() - profit_fiat = fiat_converter.convert_amount( - profit_trade, - stake, - fiat - ) - message += f'` ({gain}: {fmt_exp_profit:.2f}%, {profit_trade:.8f} {stake}`' \ - f'` / {profit_fiat:.3f} {fiat})`'\ - '' - # Because telegram._forcesell does not have the configuration - # Ignore the FIAT value and does not show the stake_currency as well - else: - gain = "profit" if fmt_exp_profit > 0 else "loss" - message += f'` ({gain}: {fmt_exp_profit:.2f}%, {profit_trade:.8f})`' + stake_currency = self.config['stake_currency'] + fiat_currency = self.config['fiat_display_currency'] + msg.update({ + 'stake_currency': stake_currency, + 'fiat_currency': fiat_currency, + }) + # Send the message - self.rpc.send_msg(message) + self.rpc.send_msg(msg) Trade.session.flush() diff --git a/freqtrade/main.py b/freqtrade/main.py index 79080ce37..3ed478ec3 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -10,9 +10,10 @@ from typing import List from freqtrade import OperationalException from freqtrade.arguments import Arguments -from freqtrade.configuration import Configuration +from freqtrade.configuration import Configuration, set_loggers from freqtrade.freqtradebot import FreqtradeBot from freqtrade.state import State +from freqtrade.rpc import RPCMessageType logger = logging.getLogger('freqtrade') @@ -59,7 +60,10 @@ def main(sysargv: List[str]) -> None: logger.exception('Fatal exception!') finally: if freqtrade: - freqtrade.rpc.send_msg('*Status:* `Process died ...`') + freqtrade.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': 'process died' + }) freqtrade.cleanup() sys.exit(return_code) @@ -73,21 +77,13 @@ def reconfigure(freqtrade: FreqtradeBot, args: Namespace) -> FreqtradeBot: # Create new instance freqtrade = FreqtradeBot(Configuration(args).get_config()) - freqtrade.rpc.send_msg( - '*Status:* `Config reloaded {freqtrade.state.name.lower()}...`') + freqtrade.rpc.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': 'config reloaded' + }) return freqtrade -def set_loggers() -> None: - """ - Set the logger level for Third party libs - :return: None - """ - logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO) - logging.getLogger('ccxt.base.exchange').setLevel(logging.INFO) - logging.getLogger('telegram').setLevel(logging.INFO) - - if __name__ == '__main__': set_loggers() main(sys.argv[1:]) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 16c21258f..593af619c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -6,7 +6,7 @@ This module contains the backtesting logic import logging import operator from argparse import Namespace -from datetime import datetime +from datetime import datetime, timedelta from typing import Any, Dict, List, NamedTuple, Optional, Tuple import arrow @@ -15,12 +15,13 @@ from tabulate import tabulate import freqtrade.optimize as optimize from freqtrade import DependencyException, constants -from freqtrade.analyze import Analyze from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration from freqtrade.exchange import Exchange from freqtrade.misc import file_dump_json from freqtrade.persistence import Trade +from freqtrade.strategy.interface import SellType +from freqtrade.strategy.resolver import IStrategy, StrategyResolver logger = logging.getLogger(__name__) @@ -40,6 +41,7 @@ class BacktestResult(NamedTuple): open_at_end: bool open_rate: float close_rate: float + sell_reason: SellType class Backtesting(object): @@ -52,11 +54,11 @@ class Backtesting(object): """ def __init__(self, config: Dict[str, Any]) -> None: self.config = config - self.analyze = Analyze(self.config) - self.ticker_interval = self.analyze.strategy.ticker_interval - self.tickerdata_to_dataframe = self.analyze.tickerdata_to_dataframe - self.populate_buy_trend = self.analyze.populate_buy_trend - self.populate_sell_trend = self.analyze.populate_sell_trend + self.strategy: IStrategy = StrategyResolver(self.config).strategy + self.ticker_interval = self.strategy.ticker_interval + self.tickerdata_to_dataframe = self.strategy.tickerdata_to_dataframe + self.advise_buy = self.strategy.advise_buy + self.advise_sell = self.strategy.advise_sell # Reset keys for backtesting self.config['exchange']['key'] = '' @@ -75,7 +77,7 @@ class Backtesting(object): :return: tuple containing min_date, max_date """ timeframe = [ - (arrow.get(min(frame.date)), arrow.get(max(frame.date))) + (arrow.get(frame['date'].min()), arrow.get(frame['date'].max())) for frame in data.values() ] return min(timeframe, key=operator.itemgetter(0))[0], \ @@ -88,9 +90,9 @@ class Backtesting(object): """ stake_currency = str(self.config.get('stake_currency')) - floatfmt = ('s', 'd', '.2f', '.8f', '.1f') + floatfmt = ('s', 'd', '.2f', '.2f', '.8f', 'd', '.1f', '.1f') tabular_data = [] - headers = ['pair', 'buy count', 'avg profit %', + headers = ['pair', 'buy count', 'avg profit %', 'cum profit %', 'total profit ' + stake_currency, 'avg duration', 'profit', 'loss'] for pair in data: result = results[results.pair == pair] @@ -98,8 +100,10 @@ class Backtesting(object): pair, len(result.index), result.profit_percent.mean() * 100.0, + result.profit_percent.sum() * 100.0, result.profit_abs.sum(), - result.trade_duration.mean(), + str(timedelta( + minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00', len(result[result.profit_abs > 0]), len(result[result.profit_abs < 0]) ]) @@ -109,18 +113,30 @@ class Backtesting(object): 'TOTAL', len(results.index), results.profit_percent.mean() * 100.0, + results.profit_percent.sum() * 100.0, results.profit_abs.sum(), - results.trade_duration.mean(), + str(timedelta( + minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00', len(results[results.profit_abs > 0]), len(results[results.profit_abs < 0]) ]) return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe") + def _generate_text_table_sell_reason(self, data: Dict[str, Dict], results: DataFrame) -> str: + """ + Generate small table outlining Backtest results + """ + tabular_data = [] + headers = ['Sell Reason', 'Count'] + for reason, count in results['sell_reason'].value_counts().iteritems(): + tabular_data.append([reason.value, count]) + return tabulate(tabular_data, headers=headers, tablefmt="pipe") + def _store_backtest_result(self, recordfilename: Optional[str], results: DataFrame) -> None: records = [(t.pair, t.profit_percent, t.open_time.timestamp(), t.close_time.timestamp(), t.open_index - 1, t.trade_duration, - t.open_rate, t.close_rate, t.open_at_end) + t.open_rate, t.close_rate, t.open_at_end, t.sell_reason.value) for index, t in results.iterrows()] if records: @@ -134,7 +150,7 @@ class Backtesting(object): stake_amount = args['stake_amount'] max_open_trades = args.get('max_open_trades', 0) trade = Trade( - open_rate=buy_row.close, + open_rate=buy_row.open, open_date=buy_row.date, stake_amount=stake_amount, amount=stake_amount / buy_row.open, @@ -149,35 +165,40 @@ class Backtesting(object): trade_count_lock[sell_row.date] = trade_count_lock.get(sell_row.date, 0) + 1 buy_signal = sell_row.buy - if self.analyze.should_sell(trade, sell_row.close, sell_row.date, buy_signal, - sell_row.sell): + sell = self.strategy.should_sell(trade, sell_row.open, sell_row.date, buy_signal, + sell_row.sell) + if sell.sell_flag: return BacktestResult(pair=pair, - profit_percent=trade.calc_profit_percent(rate=sell_row.close), - profit_abs=trade.calc_profit(rate=sell_row.close), + profit_percent=trade.calc_profit_percent(rate=sell_row.open), + profit_abs=trade.calc_profit(rate=sell_row.open), open_time=buy_row.date, close_time=sell_row.date, - trade_duration=(sell_row.date - buy_row.date).seconds // 60, + trade_duration=int(( + sell_row.date - buy_row.date).total_seconds() // 60), open_index=buy_row.Index, close_index=sell_row.Index, open_at_end=False, - open_rate=buy_row.close, - close_rate=sell_row.close + open_rate=buy_row.open, + close_rate=sell_row.open, + sell_reason=sell.sell_type ) if partial_ticker: # no sell condition found - trade stil open at end of backtest period sell_row = partial_ticker[-1] btr = BacktestResult(pair=pair, - profit_percent=trade.calc_profit_percent(rate=sell_row.close), - profit_abs=trade.calc_profit(rate=sell_row.close), + profit_percent=trade.calc_profit_percent(rate=sell_row.open), + profit_abs=trade.calc_profit(rate=sell_row.open), open_time=buy_row.date, close_time=sell_row.date, - trade_duration=(sell_row.date - buy_row.date).seconds // 60, + trade_duration=int(( + sell_row.date - buy_row.date).total_seconds() // 60), open_index=buy_row.Index, close_index=sell_row.Index, open_at_end=True, - open_rate=buy_row.close, - close_rate=sell_row.close + open_rate=buy_row.open, + close_rate=sell_row.open, + sell_reason=SellType.FORCE_SELL ) logger.debug('Force_selling still open trade %s with %s perc - %s', btr.pair, btr.profit_percent, btr.profit_abs) @@ -196,20 +217,20 @@ class Backtesting(object): stake_amount: btc amount to use for each trade processed: a processed dictionary with format {pair, data} max_open_trades: maximum number of concurrent trades (default: 0, disabled) - realistic: do we try to simulate realistic trades? (default: True) + position_stacking: do we allow position stacking? (default: False) :return: DataFrame """ headers = ['date', 'buy', 'open', 'close', 'sell'] processed = args['processed'] max_open_trades = args.get('max_open_trades', 0) - realistic = args.get('realistic', False) + position_stacking = args.get('position_stacking', False) trades = [] trade_count_lock: Dict = {} for pair, pair_data in processed.items(): pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run - ticker_data = self.populate_sell_trend( - self.populate_buy_trend(pair_data))[headers].copy() + ticker_data = self.advise_sell( + self.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() # to avoid using data from future, we buy/sell with signal from previous candle ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1) @@ -226,7 +247,7 @@ class Backtesting(object): if row.buy == 0 or row.sell == 1: continue # skip rows where no buy signal or that would immediately sell off - if realistic: + if not position_stacking: if lock_pair_until is not None and row.date <= lock_pair_until: continue if max_open_trades > 0: @@ -280,11 +301,11 @@ class Backtesting(object): if not data: logger.critical("No data found. Terminating.") return - # Ignore max_open_trades in backtesting, except realistic flag was passed - if self.config.get('realistic_simulation', False): + # Use max_open_trades in backtesting, except --disable-max-market-positions is set + if self.config.get('use_max_market_positions', True): max_open_trades = self.config['max_open_trades'] else: - logger.info('Ignoring max_open_trades (realistic_simulation not set) ...') + logger.info('Ignoring max_open_trades (--disable-max-market-positions was used) ...') max_open_trades = 0 preprocessed = self.tickerdata_to_dataframe(data) @@ -304,7 +325,7 @@ class Backtesting(object): 'stake_amount': self.config.get('stake_amount'), 'processed': preprocessed, 'max_open_trades': max_open_trades, - 'realistic': self.config.get('realistic_simulation', False), + 'position_stacking': self.config.get('position_stacking', False), } ) @@ -312,21 +333,31 @@ class Backtesting(object): self._store_backtest_result(self.config.get('exportfilename'), results) logger.info( - '\n======================================== ' - 'BACKTESTING REPORT' - ' =========================================\n' + '\n' + '=' * 49 + + ' BACKTESTING REPORT ' + + '=' * 50 + '\n' '%s', self._generate_text_table( data, results ) ) + # logger.info( + # results[['sell_reason']].groupby('sell_reason').count() + # ) logger.info( - '\n====================================== ' - 'LEFT OPEN TRADES REPORT' - ' ======================================\n' - '%s', + '\n' + + ' SELL READON STATS '.center(119, '=') + + '\n%s \n', + self._generate_text_table_sell_reason(data, results) + + ) + + logger.info( + '\n' + + ' LEFT OPEN TRADES REPORT '.center(119, '=') + + '\n%s', self._generate_text_table( data, results.loc[results.open_at_end] diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 72bf34eb3..086cad5aa 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -75,7 +75,7 @@ class Hyperopt(Backtesting): return arg_dict @staticmethod - def populate_indicators(dataframe: DataFrame) -> DataFrame: + def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['adx'] = ta.ADX(dataframe) macd = ta.MACD(dataframe) dataframe['macd'] = macd['macd'] @@ -228,7 +228,7 @@ class Hyperopt(Backtesting): """ Define the buy strategy parameters to be used by hyperopt """ - def populate_buy_trend(dataframe: DataFrame) -> DataFrame: + def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: """ Buy strategy Hyperopt will build and use """ @@ -267,20 +267,20 @@ class Hyperopt(Backtesting): params = self.get_args(_params) if self.has_space('roi'): - self.analyze.strategy.minimal_roi = self.generate_roi_table(params) + self.strategy.minimal_roi = self.generate_roi_table(params) if self.has_space('buy'): - self.populate_buy_trend = self.buy_strategy_generator(params) + self.advise_buy = self.buy_strategy_generator(params) if self.has_space('stoploss'): - self.analyze.strategy.stoploss = params['stoploss'] + self.strategy.stoploss = params['stoploss'] processed = load(TICKERDATA_PICKLE) results = self.backtest( { 'stake_amount': self.config['stake_amount'], 'processed': processed, - 'realistic': self.config.get('realistic_simulation', False), + 'position_stacking': self.config.get('position_stacking', True), } ) result_explanation = self.format_results(results) @@ -351,7 +351,7 @@ class Hyperopt(Backtesting): ) if self.has_space('buy'): - self.analyze.populate_indicators = Hyperopt.populate_indicators # type: ignore + self.strategy.advise_indicators = Hyperopt.populate_indicators # type: ignore dump(self.tickerdata_to_dataframe(data), TICKERDATA_PICKLE) self.exchange = None # type: ignore self.load_previous_results() @@ -360,7 +360,7 @@ class Hyperopt(Backtesting): logger.info(f'Found {cpus} CPU cores. Let\'s make them scream!') opt = self.get_optimizer(cpus) - EVALS = max(self.total_tries//cpus, 1) + EVALS = max(self.total_tries // cpus, 1) try: with Parallel(n_jobs=cpus) as parallel: for i in range(EVALS): diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 0e0b22e82..8fb01d074 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -83,11 +83,16 @@ def check_migrate(engine) -> None: # Check for latest column if not has_column(cols, 'max_rate'): + fee_open = get_column_def(cols, 'fee_open', 'fee') + fee_close = get_column_def(cols, 'fee_close', 'fee') open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null') close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null') stop_loss = get_column_def(cols, 'stop_loss', '0.0') initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0') max_rate = get_column_def(cols, 'max_rate', '0.0') + sell_reason = get_column_def(cols, 'sell_reason', 'null') + strategy = get_column_def(cols, 'strategy', 'null') + ticker_interval = get_column_def(cols, 'ticker_interval', 'null') # Schema migration necessary engine.execute(f"alter table trades rename to {table_back_name}") @@ -99,7 +104,8 @@ def check_migrate(engine) -> None: (id, exchange, pair, is_open, fee_open, fee_close, open_rate, open_rate_requested, close_rate, close_rate_requested, close_profit, stake_amount, amount, open_date, close_date, open_order_id, - stop_loss, initial_stop_loss, max_rate + stop_loss, initial_stop_loss, max_rate, sell_reason, strategy, + ticker_interval ) select id, lower(exchange), case @@ -109,12 +115,13 @@ def check_migrate(engine) -> None: else pair end pair, - is_open, fee fee_open, fee fee_close, + is_open, {fee_open} fee_open, {fee_close} fee_close, open_rate, {open_rate_requested} open_rate_requested, close_rate, {close_rate_requested} close_rate_requested, close_profit, stake_amount, amount, open_date, close_date, open_order_id, {stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss, - {max_rate} max_rate + {max_rate} max_rate, {sell_reason} sell_reason, {strategy} strategy, + {ticker_interval} ticker_interval from {table_back_name} """) @@ -170,6 +177,9 @@ class Trade(_DECL_BASE): initial_stop_loss = Column(Float, nullable=True, default=0.0) # absolute value of the highest reached price max_rate = Column(Float, nullable=True, default=0.0) + sell_reason = Column(String, nullable=True) + strategy = Column(String, nullable=True) + ticker_interval = Column(Integer, nullable=True) def __repr__(self): open_since = arrow.get(self.open_date).humanize() if self.is_open else 'closed' diff --git a/freqtrade/rpc/__init__.py b/freqtrade/rpc/__init__.py index e69de29bb..31c854f82 100644 --- a/freqtrade/rpc/__init__.py +++ b/freqtrade/rpc/__init__.py @@ -0,0 +1,2 @@ +from .rpc import RPC, RPCMessageType, RPCException # noqa +from .rpc_manager import RPCManager # noqa diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 11658c6fb..f58fbae9a 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -3,22 +3,34 @@ This module contains class to define a RPC communications """ import logging from abc import abstractmethod -from datetime import date, datetime, timedelta +from datetime import timedelta, datetime, date from decimal import Decimal -from typing import Any, Dict, List, Tuple +from enum import Enum +from typing import Dict, Any, List, Optional import arrow import sqlalchemy as sql from numpy import mean, nan_to_num from pandas import DataFrame +from freqtrade.fiat_convert import CryptoToFiatConverter from freqtrade.misc import shorten_date from freqtrade.persistence import Trade from freqtrade.state import State +from freqtrade.strategy.interface import SellType logger = logging.getLogger(__name__) +class RPCMessageType(Enum): + STATUS_NOTIFICATION = 'status' + BUY_NOTIFICATION = 'buy' + SELL_NOTIFICATION = 'sell' + + def __repr__(self): + return self.value + + class RPCException(Exception): """ Should be raised with a rpc-formatted message in an _rpc_* method @@ -26,13 +38,21 @@ class RPCException(Exception): raise RPCException('*Status:* `no active trade`') """ - pass + def __init__(self, message: str) -> None: + super().__init__(self) + self.message = message + + def __str__(self): + return self.message class RPC(object): """ RPC class can be used to have extra feature, like bot data, and access to DB data """ + # Bind _fiat_converter if needed in each RPC handler + _fiat_converter: Optional[CryptoToFiatConverter] = None + def __init__(self, freqtrade) -> None: """ Initializes all enabled rpc modules @@ -41,20 +61,20 @@ class RPC(object): """ self._freqtrade = freqtrade + @property + def name(self) -> str: + """ Returns the lowercase name of the implementation """ + return self.__class__.__name__.lower() + @abstractmethod def cleanup(self) -> None: """ Cleanup pending module resources """ - @property @abstractmethod - def name(self) -> str: - """ Returns the lowercase name of this module """ - - @abstractmethod - def send_msg(self, msg: str) -> None: + def send_msg(self, msg: Dict[str, str]) -> None: """ Sends a message to all registered rpc modules """ - def _rpc_trade_status(self) -> List[str]: + def _rpc_trade_status(self) -> List[Dict[str, Any]]: """ Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is a remotely exposed function @@ -62,11 +82,11 @@ class RPC(object): # Fetch open trade trades = Trade.query.filter(Trade.is_open.is_(True)).all() if self._freqtrade.state != State.RUNNING: - raise RPCException('*Status:* `trader is not running`') + raise RPCException('trader is not running') elif not trades: - raise RPCException('*Status:* `no active trade`') + raise RPCException('no active trade') else: - result = [] + results = [] for trade in trades: order = None if trade.open_order_id: @@ -76,39 +96,29 @@ class RPC(object): current_profit = trade.calc_profit_percent(current_rate) fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%' if trade.close_profit else None) - market_url = self._freqtrade.exchange.get_pair_detail_url(trade.pair) - trade_date = arrow.get(trade.open_date).humanize() - open_rate = trade.open_rate - close_rate = trade.close_rate - amount = round(trade.amount, 8) - current_profit = round(current_profit * 100, 2) - open_order = '' - if order: - order_type = order['type'] - order_side = order['side'] - order_rem = order['remaining'] - open_order = f'({order_type} {order_side} rem={order_rem:.8f})' - - message = f"*Trade ID:* `{trade.id}`\n" \ - f"*Current Pair:* [{trade.pair}]({market_url})\n" \ - f"*Open Since:* `{trade_date}`\n" \ - f"*Amount:* `{amount}`\n" \ - f"*Open Rate:* `{open_rate:.8f}`\n" \ - f"*Close Rate:* `{close_rate}`\n" \ - f"*Current Rate:* `{current_rate:.8f}`\n" \ - f"*Close Profit:* `{fmt_close_profit}`\n" \ - f"*Current Profit:* `{current_profit:.2f}%`\n" \ - f"*Open Order:* `{open_order}`"\ - - result.append(message) - return result + results.append(dict( + trade_id=trade.id, + pair=trade.pair, + market_url=self._freqtrade.exchange.get_pair_detail_url(trade.pair), + date=arrow.get(trade.open_date), + open_rate=trade.open_rate, + close_rate=trade.close_rate, + current_rate=current_rate, + amount=round(trade.amount, 8), + close_profit=fmt_close_profit, + current_profit=round(current_profit * 100, 2), + open_order='({} {} rem={:.8f})'.format( + order['type'], order['side'], order['remaining'] + ) if order else None, + )) + return results def _rpc_status_table(self) -> DataFrame: trades = Trade.query.filter(Trade.is_open.is_(True)).all() if self._freqtrade.state != State.RUNNING: - raise RPCException('*Status:* `trader is not running`') + raise RPCException('trader is not running') elif not trades: - raise RPCException('*Status:* `no active order`') + raise RPCException('no active order') else: trades_list = [] for trade in trades: @@ -134,9 +144,8 @@ class RPC(object): profit_days: Dict[date, Dict] = {} if not (isinstance(timescale, int) and timescale > 0): - raise RPCException('*Daily [n]:* `must be an integer greater than 0`') + raise RPCException('timescale must be an integer greater than 0') - fiat = self._freqtrade.fiat_converter for day in range(0, timescale): profitday = today - timedelta(days=day) trades = Trade.query \ @@ -159,11 +168,11 @@ class RPC(object): symbol=stake_currency ), '{value:.3f} {symbol}'.format( - value=fiat.convert_amount( + value=self._fiat_converter.convert_amount( value['amount'], stake_currency, fiat_display_currency - ), + ) if self._fiat_converter else 0, symbol=fiat_display_currency ), '{value} trade{s}'.format( @@ -214,34 +223,33 @@ class RPC(object): .order_by(sql.text('profit_sum DESC')).first() if not best_pair: - raise RPCException('*Status:* `no closed trade`') + raise RPCException('no closed trade') bp_pair, bp_rate = best_pair - # FIX: we want to keep fiatconverter in a state/environment, - # doing this will utilize its caching functionallity, instead we reinitialize it here - fiat = self._freqtrade.fiat_converter # Prepare data to display - profit_closed_coin = round(sum(profit_closed_coin), 8) + profit_closed_coin_sum = round(sum(profit_closed_coin), 8) profit_closed_percent = round(nan_to_num(mean(profit_closed_percent)) * 100, 2) - profit_closed_fiat = fiat.convert_amount( - profit_closed_coin, + profit_closed_fiat = self._fiat_converter.convert_amount( + profit_closed_coin_sum, stake_currency, fiat_display_currency - ) - profit_all_coin = round(sum(profit_all_coin), 8) + ) if self._fiat_converter else 0 + + profit_all_coin_sum = round(sum(profit_all_coin), 8) profit_all_percent = round(nan_to_num(mean(profit_all_percent)) * 100, 2) - profit_all_fiat = fiat.convert_amount( - profit_all_coin, + profit_all_fiat = self._fiat_converter.convert_amount( + profit_all_coin_sum, stake_currency, fiat_display_currency - ) + ) if self._fiat_converter else 0 + num = float(len(durations) or 1) return { - 'profit_closed_coin': profit_closed_coin, + 'profit_closed_coin': profit_closed_coin_sum, 'profit_closed_percent': profit_closed_percent, 'profit_closed_fiat': profit_closed_fiat, - 'profit_all_coin': profit_all_coin, + 'profit_all_coin': profit_all_coin_sum, 'profit_all_percent': profit_all_percent, 'profit_all_fiat': profit_all_fiat, 'trade_count': len(trades), @@ -252,7 +260,7 @@ class RPC(object): 'best_rate': round(bp_rate * 100, 2), } - def _rpc_balance(self, fiat_display_currency: str) -> Tuple[List[Dict], float, str, float]: + def _rpc_balance(self, fiat_display_currency: str) -> Dict: """ Returns current account balance per crypto """ output = [] total = 0.0 @@ -269,45 +277,47 @@ class RPC(object): rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid'] est_btc: float = rate * balance['total'] total = total + est_btc - output.append( - { - 'currency': coin, - 'available': balance['free'], - 'balance': balance['total'], - 'pending': balance['used'], - 'est_btc': est_btc - } - ) + output.append({ + 'currency': coin, + 'available': balance['free'], + 'balance': balance['total'], + 'pending': balance['used'], + 'est_btc': est_btc, + }) if total == 0.0: - raise RPCException('`All balances are zero.`') + raise RPCException('all balances are zero') - fiat = self._freqtrade.fiat_converter symbol = fiat_display_currency - value = fiat.convert_amount(total, 'BTC', symbol) - return output, total, symbol, value + value = self._fiat_converter.convert_amount(total, 'BTC', + symbol) if self._fiat_converter else 0 + return { + 'currencies': output, + 'total': total, + 'symbol': symbol, + 'value': value, + } - def _rpc_start(self) -> str: + def _rpc_start(self) -> Dict[str, str]: """ Handler for start """ if self._freqtrade.state == State.RUNNING: - return '*Status:* `already running`' + return {'status': 'already running'} self._freqtrade.state = State.RUNNING - return '`Starting trader ...`' + return {'status': 'starting trader ...'} - def _rpc_stop(self) -> str: + def _rpc_stop(self) -> Dict[str, str]: """ Handler for stop """ if self._freqtrade.state == State.RUNNING: self._freqtrade.state = State.STOPPED - return '`Stopping trader ...`' + return {'status': 'stopping trader ...'} - return '*Status:* `already stopped`' + return {'status': 'already stopped'} - def _rpc_reload_conf(self) -> str: + def _rpc_reload_conf(self) -> Dict[str, str]: """ Handler for reload_conf. """ self._freqtrade.state = State.RELOAD_CONF - return '*Status:* `Reloading config ...`' + return {'status': 'reloading config ...'} - # FIX: no test for this!!!! def _rpc_forcesell(self, trade_id) -> None: """ Handler for forcesell . @@ -337,11 +347,11 @@ class RPC(object): # Get current rate and execute sell current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid'] - self._freqtrade.execute_sell(trade, current_rate) + self._freqtrade.execute_sell(trade, current_rate, SellType.FORCE_SELL) # ---- EOF def _exec_forcesell ---- if self._freqtrade.state != State.RUNNING: - raise RPCException('`trader is not running`') + raise RPCException('trader is not running') if trade_id == 'all': # Execute sell for all open orders @@ -358,7 +368,7 @@ class RPC(object): ).first() if not trade: logger.warning('forcesell: Invalid argument received') - raise RPCException('Invalid argument.') + raise RPCException('invalid argument') _exec_forcesell(trade) Trade.session.flush() @@ -369,7 +379,7 @@ class RPC(object): Shows a performance statistic from finished trades """ if self._freqtrade.state != State.RUNNING: - raise RPCException('`trader is not running`') + raise RPCException('trader is not running') pair_rates = Trade.session.query(Trade.pair, sql.func.sum(Trade.close_profit).label('profit_sum'), @@ -386,6 +396,6 @@ class RPC(object): def _rpc_count(self) -> List[Trade]: """ Returns the number of trades running """ if self._freqtrade.state != State.RUNNING: - raise RPCException('`trader is not running`') + raise RPCException('trader is not running') return Trade.query.filter(Trade.is_open.is_(True)).all() diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py index 252bbcdd8..022578378 100644 --- a/freqtrade/rpc/rpc_manager.py +++ b/freqtrade/rpc/rpc_manager.py @@ -2,9 +2,9 @@ This module contains class to manage RPC communications (Telegram, Slack, ...) """ import logging -from typing import List +from typing import List, Dict, Any -from freqtrade.rpc.rpc import RPC +from freqtrade.rpc import RPC logger = logging.getLogger(__name__) @@ -23,6 +23,12 @@ class RPCManager(object): from freqtrade.rpc.telegram import Telegram self.registered_modules.append(Telegram(freqtrade)) + # Enable Webhook + if freqtrade.config.get('webhook', {}).get('enabled', False): + logger.info('Enabling rpc.webhook ...') + from freqtrade.rpc.webhook import Webhook + self.registered_modules.append(Webhook(freqtrade)) + def cleanup(self) -> None: """ Stops all enabled rpc modules """ logger.info('Cleaning up rpc modules ...') @@ -32,11 +38,14 @@ class RPCManager(object): mod.cleanup() del mod - def send_msg(self, msg: str) -> None: + def send_msg(self, msg: Dict[str, Any]) -> None: """ - Send given markdown message to all registered rpc modules - :param msg: message - :return: None + Send given message to all registered rpc modules. + A message consists of one or more key value pairs of strings. + e.g.: + { + 'status': 'stopping bot' + } """ logger.info('Sending rpc message: %s', msg) for mod in self.registered_modules: diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 13a2b1913..3b5ce3f74 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -4,7 +4,7 @@ This module manage Telegram communication """ import logging -from typing import Any, Callable +from typing import Any, Callable, Dict from tabulate import tabulate from telegram import Bot, ParseMode, ReplyKeyboardMarkup, Update @@ -12,7 +12,8 @@ from telegram.error import NetworkError, TelegramError from telegram.ext import CommandHandler, Updater from freqtrade.__init__ import __version__ -from freqtrade.rpc.rpc import RPC, RPCException +from freqtrade.fiat_convert import CryptoToFiatConverter +from freqtrade.rpc import RPC, RPCException, RPCMessageType logger = logging.getLogger(__name__) @@ -55,10 +56,6 @@ def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Call class Telegram(RPC): """ This class handles all telegram communication """ - @property - def name(self) -> str: - return "telegram" - def __init__(self, freqtrade) -> None: """ Init the Telegram call, and init the super class RPC @@ -70,6 +67,8 @@ class Telegram(RPC): self._updater: Updater = None self._config = freqtrade.config self._init() + if self._config.get('fiat_display_currency', None): + self._fiat_converter = CryptoToFiatConverter() def _init(self) -> None: """ @@ -114,9 +113,51 @@ class Telegram(RPC): """ self._updater.stop() - def send_msg(self, msg: str) -> None: + def send_msg(self, msg: Dict[str, Any]) -> None: """ Send a message to telegram channel """ - self._send_msg(msg) + + if msg['type'] == RPCMessageType.BUY_NOTIFICATION: + if self._fiat_converter: + msg['stake_amount_fiat'] = self._fiat_converter.convert_amount( + msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) + else: + msg['stake_amount_fiat'] = 0 + + message = "*{exchange}:* Buying [{pair}]({market_url})\n" \ + "with limit `{limit:.8f}\n" \ + "({stake_amount:.6f} {stake_currency}".format(**msg) + + if msg.get('fiat_currency', None): + message += ",{stake_amount_fiat:.3f} {fiat_currency}".format(**msg) + message += ")`" + + elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: + msg['amount'] = round(msg['amount'], 8) + msg['profit_percent'] = round(msg['profit_percent'] * 100, 2) + + message = "*{exchange}:* Selling [{pair}]({market_url})\n" \ + "*Limit:* `{limit:.8f}`\n" \ + "*Amount:* `{amount:.8f}`\n" \ + "*Open Rate:* `{open_rate:.8f}`\n" \ + "*Current Rate:* `{current_rate:.8f}`\n" \ + "*Profit:* `{profit_percent:.2f}%`".format(**msg) + + # Check if all sell properties are available. + # This might not be the case if the message origin is triggered by /forcesell + if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency']) + and self._fiat_converter): + msg['profit_fiat'] = self._fiat_converter.convert_amount( + msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) + message += '` ({gain}: {profit_amount:.8f} {stake_currency}`' \ + '` / {profit_fiat:.3f} {fiat_currency})`'.format(**msg) + + elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: + message = '*Status:* `{status}`'.format(**msg) + + else: + raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) + + self._send_msg(message) @authorized_only def _status(self, bot: Bot, update: Update) -> None: @@ -136,8 +177,26 @@ class Telegram(RPC): return try: - for trade_msg in self._rpc_trade_status(): - self._send_msg(trade_msg, bot=bot) + results = self._rpc_trade_status() + # pre format data + for result in results: + result['date'] = result['date'].humanize() + + messages = [ + "*Trade ID:* `{trade_id}`\n" + "*Current Pair:* [{pair}]({market_url})\n" + "*Open Since:* `{date}`\n" + "*Amount:* `{amount}`\n" + "*Open Rate:* `{open_rate:.8f}`\n" + "*Close Rate:* `{close_rate}`\n" + "*Current Rate:* `{current_rate:.8f}`\n" + "*Close Profit:* `{close_profit}`\n" + "*Current Profit:* `{current_profit:.2f}%`\n" + "*Open Order:* `{open_order}`".format(**result) + for result in results + ] + for msg in messages: + self._send_msg(msg, bot=bot) except RPCException as e: self._send_msg(str(e), bot=bot) @@ -167,7 +226,7 @@ class Telegram(RPC): :return: None """ stake_cur = self._config['stake_currency'] - fiat_disp_cur = self._config['fiat_display_currency'] + fiat_disp_cur = self._config.get('fiat_display_currency', '') try: timescale = int(update.message.text.replace('/daily', '').strip()) except (TypeError, ValueError): @@ -200,7 +259,7 @@ class Telegram(RPC): :return: None """ stake_cur = self._config['stake_currency'] - fiat_disp_cur = self._config['fiat_display_currency'] + fiat_disp_cur = self._config.get('fiat_display_currency', '') try: stats = self._rpc_trade_statistics( @@ -239,10 +298,9 @@ class Telegram(RPC): def _balance(self, bot: Bot, update: Update) -> None: """ Handler for /balance """ try: - currencys, total, symbol, value = \ - self._rpc_balance(self._config['fiat_display_currency']) + result = self._rpc_balance(self._config.get('fiat_display_currency', '')) output = '' - for currency in currencys: + for currency in result['currencies']: output += "*{currency}:*\n" \ "\t`Available: {available: .8f}`\n" \ "\t`Balance: {balance: .8f}`\n" \ @@ -250,8 +308,8 @@ class Telegram(RPC): "\t`Est. BTC: {est_btc: .8f}`\n".format(**currency) output += "\n*Estimated Value*:\n" \ - "\t`BTC: {0: .8f}`\n" \ - "\t`{1}: {2: .2f}`\n".format(total, symbol, value) + "\t`BTC: {total: .8f}`\n" \ + "\t`{symbol}: {value: .2f}`\n".format(**result) self._send_msg(output, bot=bot) except RPCException as e: self._send_msg(str(e), bot=bot) @@ -266,7 +324,7 @@ class Telegram(RPC): :return: None """ msg = self._rpc_start() - self._send_msg(msg, bot=bot) + self._send_msg('Status: `{status}`'.format(**msg), bot=bot) @authorized_only def _stop(self, bot: Bot, update: Update) -> None: @@ -278,7 +336,7 @@ class Telegram(RPC): :return: None """ msg = self._rpc_stop() - self._send_msg(msg, bot=bot) + self._send_msg('Status: `{status}`'.format(**msg), bot=bot) @authorized_only def _reload_conf(self, bot: Bot, update: Update) -> None: @@ -290,7 +348,7 @@ class Telegram(RPC): :return: None """ msg = self._rpc_reload_conf() - self._send_msg(msg, bot=bot) + self._send_msg('Status: `{status}`'.format(**msg), bot=bot) @authorized_only def _forcesell(self, bot: Bot, update: Update) -> None: diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py new file mode 100644 index 000000000..bfc82b8d6 --- /dev/null +++ b/freqtrade/rpc/webhook.py @@ -0,0 +1,66 @@ +""" +This module manages webhook communication +""" +import logging +from typing import Any, Dict + +from requests import post, RequestException + +from freqtrade.rpc import RPC, RPCMessageType + + +logger = logging.getLogger(__name__) + +logger.debug('Included module rpc.webhook ...') + + +class Webhook(RPC): + """ This class handles all webhook communication """ + + def __init__(self, freqtrade) -> None: + """ + Init the Webhook class, and init the super class RPC + :param freqtrade: Instance of a freqtrade bot + :return: None + """ + super().__init__(freqtrade) + + self._config = freqtrade.config + self._url = self._config['webhook']['url'] + + def cleanup(self) -> None: + """ + Cleanup pending module resources. + This will do nothing for webhooks, they will simply not be called anymore + """ + pass + + def send_msg(self, msg: Dict[str, Any]) -> None: + """ Send a message to telegram channel """ + try: + + if msg['type'] == RPCMessageType.BUY_NOTIFICATION: + valuedict = self._config['webhook'].get('webhookbuy', None) + elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: + valuedict = self._config['webhook'].get('webhooksell', None) + elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: + valuedict = self._config['webhook'].get('webhookstatus', None) + else: + raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) + if not valuedict: + logger.info("Message type %s not configured for webhooks", msg['type']) + return + + payload = {key: value.format(**msg) for (key, value) in valuedict.items()} + self._send_msg(payload) + except KeyError as exc: + logger.exception("Problem calling Webhook. Please check your webhook configuration. " + "Exception: %s", exc) + + def _send_msg(self, payload: dict) -> None: + """do the actual call to the webhook""" + + try: + post(self._url, data=payload) + except RequestException as exc: + logger.warning("Could not call webhook url. Exception: %s", exc) diff --git a/freqtrade/strategy/__init__.py b/freqtrade/strategy/__init__.py index e1dc7bb3f..283426dfa 100644 --- a/freqtrade/strategy/__init__.py +++ b/freqtrade/strategy/__init__.py @@ -7,7 +7,7 @@ from freqtrade.strategy.interface import IStrategy logger = logging.getLogger(__name__) -def import_strategy(strategy: IStrategy) -> IStrategy: +def import_strategy(strategy: IStrategy, config: dict) -> IStrategy: """ Imports given Strategy instance to global scope of freqtrade.strategy and returns an instance of it @@ -29,4 +29,4 @@ def import_strategy(strategy: IStrategy) -> IStrategy: # Modify global scope to declare class globals()[name] = clazz - return clazz() + return clazz(config) diff --git a/freqtrade/strategy/default_strategy.py b/freqtrade/strategy/default_strategy.py index 22689f17a..f1646779b 100644 --- a/freqtrade/strategy/default_strategy.py +++ b/freqtrade/strategy/default_strategy.py @@ -28,13 +28,16 @@ class DefaultStrategy(IStrategy): # Optimal ticker interval for the strategy ticker_interval = '5m' - def populate_indicators(self, dataframe: DataFrame) -> DataFrame: + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Adds several different TA indicators to the given DataFrame Performance Note: For the best performance be frugal on the number of indicators you are using. Let uncomment only the indicator you are using in your strategies or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() + :param metadata: Additional information, like the currently traded pair + :return: a Dataframe with all mandatory indicators for the strategies """ # Momentum Indicator @@ -196,10 +199,11 @@ class DefaultStrategy(IStrategy): return dataframe - def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the buy signal for the given dataframe :param dataframe: DataFrame + :param metadata: Additional information, like the currently traded pair :return: DataFrame with buy column """ dataframe.loc[ @@ -217,10 +221,11 @@ class DefaultStrategy(IStrategy): return dataframe - def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the sell signal for the given dataframe :param dataframe: DataFrame + :param metadata: Additional information, like the currently traded pair :return: DataFrame with buy column """ dataframe.loc[ diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index f73617f46..dfd624393 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -2,11 +2,50 @@ IStrategy interface This module defines the interface to apply for strategies """ +import logging from abc import ABC, abstractmethod -from typing import Dict +from datetime import datetime +from enum import Enum +from typing import Dict, List, NamedTuple, Tuple +import warnings +import arrow from pandas import DataFrame +from freqtrade import constants +from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe +from freqtrade.persistence import Trade + +logger = logging.getLogger(__name__) + + +class SignalType(Enum): + """ + Enum to distinguish between buy and sell signals + """ + BUY = "buy" + SELL = "sell" + + +class SellType(Enum): + """ + Enum to distinguish between sell reasons + """ + ROI = "roi" + STOP_LOSS = "stop_loss" + TRAILING_STOP_LOSS = "trailing_stop_loss" + SELL_SIGNAL = "sell_signal" + FORCE_SELL = "force_sell" + NONE = "" + + +class SellCheckTuple(NamedTuple): + """ + NamedTuple for Sell type + reason + """ + sell_flag: bool + sell_type: SellType + class IStrategy(ABC): """ @@ -19,30 +58,267 @@ class IStrategy(ABC): ticker_interval -> str: value of the ticker interval to use for the strategy """ + _populate_fun_len: int = 0 + _buy_fun_len: int = 0 + _sell_fun_len: int = 0 + # associated minimal roi minimal_roi: Dict + + # associated stoploss stoploss: float + + # associated ticker interval ticker_interval: str + def __init__(self, config: dict) -> None: + self.config = config + @abstractmethod - def populate_indicators(self, dataframe: DataFrame) -> DataFrame: + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Populate indicators that will be used in the Buy and Sell strategy :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() + :param metadata: Additional information, like the currently traded pair :return: a Dataframe with all mandatory indicators for the strategies """ @abstractmethod - def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the buy signal for the given dataframe :param dataframe: DataFrame + :param metadata: Additional information, like the currently traded pair :return: DataFrame with buy column """ @abstractmethod - def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the sell signal for the given dataframe :param dataframe: DataFrame + :param metadata: Additional information, like the currently traded pair :return: DataFrame with sell column """ + + def get_strategy_name(self) -> str: + """ + Returns strategy class name + """ + return self.__class__.__name__ + + def analyze_ticker(self, ticker_history: List[Dict], metadata: dict) -> DataFrame: + """ + Parses the given ticker history and returns a populated DataFrame + add several TA indicators and buy signal to it + :return DataFrame with ticker data and indicator data + """ + dataframe = parse_ticker_dataframe(ticker_history) + dataframe = self.advise_indicators(dataframe, metadata) + dataframe = self.advise_buy(dataframe, metadata) + dataframe = self.advise_sell(dataframe, metadata) + return dataframe + + def get_signal(self, pair: str, interval: str, ticker_hist: List[Dict]) -> Tuple[bool, bool]: + """ + Calculates current signal based several technical analysis indicators + :param pair: pair in format ANT/BTC + :param interval: Interval to use (in min) + :return: (Buy, Sell) A bool-tuple indicating buy/sell signal + """ + if not ticker_hist: + logger.warning('Empty ticker history for pair %s', pair) + return False, False + + try: + dataframe = self.analyze_ticker(ticker_hist, {'pair': pair}) + except ValueError as error: + logger.warning( + 'Unable to analyze ticker for pair %s: %s', + pair, + str(error) + ) + return False, False + except Exception as error: + logger.exception( + 'Unexpected error when analyzing ticker for pair %s: %s', + pair, + str(error) + ) + return False, False + + if dataframe.empty: + logger.warning('Empty dataframe for pair %s', pair) + return False, False + + latest = dataframe.iloc[-1] + + # Check if dataframe is out of date + signal_date = arrow.get(latest['date']) + interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval] + if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5))): + logger.warning( + 'Outdated history for pair %s. Last tick is %s minutes old', + pair, + (arrow.utcnow() - signal_date).seconds // 60 + ) + return False, False + + (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) + ) + return buy, sell + + def should_sell(self, trade: Trade, rate: float, date: datetime, buy: bool, + sell: bool) -> SellCheckTuple: + """ + This function evaluate if on the condition required to trigger a sell has been reached + if the threshold is reached and updates the trade record. + :return: True if trade should be sold, False otherwise + """ + current_profit = trade.calc_profit_percent(rate) + stoplossflag = self.stop_loss_reached(current_rate=rate, trade=trade, current_time=date, + current_profit=current_profit) + if stoplossflag.sell_flag: + return stoplossflag + + experimental = self.config.get('experimental', {}) + + if buy and experimental.get('ignore_roi_if_buy_signal', False): + logger.debug('Buy signal still active - not selling.') + return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) + + # Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee) + if self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date): + logger.debug('Required profit reached. Selling..') + return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) + + if experimental.get('sell_profit_only', False): + logger.debug('Checking if trade is profitable..') + if trade.calc_profit(rate=rate) <= 0: + return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) + if sell and not buy and experimental.get('use_sell_signal', False): + logger.debug('Sell signal received. Selling..') + return SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL) + + return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) + + def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime, + current_profit: float) -> SellCheckTuple: + """ + Based on current profit of the trade and configured (trailing) stoploss, + decides to sell or not + :param current_profit: current profit in percent + """ + + trailing_stop = self.config.get('trailing_stop', False) + + trade.adjust_stop_loss(trade.open_rate, self.stoploss, initial=True) + + # evaluate if the stoploss was hit + if self.stoploss is not None and trade.stop_loss >= current_rate: + selltype = SellType.STOP_LOSS + if trailing_stop: + selltype = SellType.TRAILING_STOP_LOSS + logger.debug( + f"HIT STOP: current price at {current_rate:.6f}, " + f"stop loss is {trade.stop_loss:.6f}, " + f"initial stop loss was at {trade.initial_stop_loss:.6f}, " + f"trade opened at {trade.open_rate:.6f}") + logger.debug(f"trailing stop saved {trade.stop_loss - trade.initial_stop_loss:.6f}") + + logger.debug('Stop loss hit.') + return SellCheckTuple(sell_flag=True, sell_type=selltype) + + # update the stop loss afterwards, after all by definition it's supposed to be hanging + if trailing_stop: + + # check if we have a special stop loss for positive condition + # and if profit is positive + stop_loss_value = self.stoploss + sl_offset = self.config.get('trailing_stop_positive_offset', 0.0) + + if 'trailing_stop_positive' in self.config and current_profit > sl_offset: + + # Ignore mypy error check in configuration that this is a float + stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore + logger.debug(f"using positive stop loss mode: {stop_loss_value} " + f"with offset {sl_offset:.4g} " + f"since we have profit {current_profit:.4f}%") + + trade.adjust_stop_loss(current_rate, stop_loss_value) + + return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) + + def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool: + """ + Based an earlier trade and current price and ROI configuration, decides whether bot should + sell + :return True if bot should sell at current rate + """ + + # Check if time matches and current rate is above threshold + time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60 + for duration, threshold in self.minimal_roi.items(): + if time_diff <= duration: + return False + if current_profit > threshold: + return True + + return False + + def tickerdata_to_dataframe(self, tickerdata: Dict[str, List]) -> Dict[str, DataFrame]: + """ + Creates a dataframe and populates indicators for given ticker data + """ + return {pair: self.advise_indicators(parse_ticker_dataframe(pair_data), {'pair': pair}) + for pair, pair_data in tickerdata.items()} + + def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Populate indicators that will be used in the Buy and Sell strategy + This method should not be overridden. + :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() + :param metadata: Additional information, like the currently traded pair + :return: a Dataframe with all mandatory indicators for the strategies + """ + if self._populate_fun_len == 2: + warnings.warn("deprecated - check out the Sample strategy to see " + "the current function headers!", DeprecationWarning) + return self.populate_indicators(dataframe) # type: ignore + else: + return self.populate_indicators(dataframe, metadata) + + def advise_buy(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + This method should not be overridden. + :param dataframe: DataFrame + :param pair: Additional information, like the currently traded pair + :return: DataFrame with buy column + """ + if self._buy_fun_len == 2: + warnings.warn("deprecated - check out the Sample strategy to see " + "the current function headers!", DeprecationWarning) + return self.populate_buy_trend(dataframe) # type: ignore + else: + return self.populate_buy_trend(dataframe, metadata) + + def advise_sell(self, dataframe: DataFrame, metadata: dict) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + This method should not be overridden. + :param dataframe: DataFrame + :param pair: Additional information, like the currently traded pair + :return: DataFrame with sell column + """ + if self._sell_fun_len == 2: + warnings.warn("deprecated - check out the Sample strategy to see " + "the current function headers!", DeprecationWarning) + return self.populate_sell_trend(dataframe) # type: ignore + else: + return self.populate_sell_trend(dataframe, metadata) diff --git a/freqtrade/strategy/resolver.py b/freqtrade/strategy/resolver.py index ea4f5c5e3..7aeec300e 100644 --- a/freqtrade/strategy/resolver.py +++ b/freqtrade/strategy/resolver.py @@ -37,6 +37,7 @@ class StrategyResolver(object): # Verify the strategy is in the configuration, otherwise fallback to the default strategy strategy_name = config.get('strategy') or constants.DEFAULT_STRATEGY self.strategy: IStrategy = self._load_strategy(strategy_name, + config=config, extra_dir=config.get('strategy_path')) # Set attributes @@ -44,12 +45,16 @@ class StrategyResolver(object): if 'minimal_roi' in config: self.strategy.minimal_roi = config['minimal_roi'] logger.info("Override strategy \'minimal_roi\' with value in config file.") + else: + config['minimal_roi'] = self.strategy.minimal_roi if 'stoploss' in config: self.strategy.stoploss = config['stoploss'] logger.info( "Override strategy \'stoploss\' with value in config file: %s.", config['stoploss'] ) + else: + config['stoploss'] = self.strategy.stoploss if 'ticker_interval' in config: self.strategy.ticker_interval = config['ticker_interval'] @@ -57,6 +62,8 @@ class StrategyResolver(object): "Override strategy \'ticker_interval\' with value in config file: %s.", config['ticker_interval'] ) + else: + config['ticker_interval'] = self.strategy.ticker_interval # Sort and apply type conversions self.strategy.minimal_roi = OrderedDict(sorted( @@ -65,10 +72,11 @@ class StrategyResolver(object): self.strategy.stoploss = float(self.strategy.stoploss) def _load_strategy( - self, strategy_name: str, extra_dir: Optional[str] = None) -> IStrategy: + self, strategy_name: str, config: dict, extra_dir: Optional[str] = None) -> IStrategy: """ Search and loads the specified strategy. :param strategy_name: name of the module to import + :param config: configuration for the strategy :param extra_dir: additional directory to search for the given strategy :return: Strategy instance or None """ @@ -100,10 +108,17 @@ class StrategyResolver(object): for path in abs_paths: try: - strategy = self._search_strategy(path, strategy_name) + strategy = self._search_strategy(path, strategy_name=strategy_name, config=config) if strategy: logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path) - return import_strategy(strategy) + strategy._populate_fun_len = len( + inspect.getfullargspec(strategy.populate_indicators).args) + strategy._buy_fun_len = len( + inspect.getfullargspec(strategy.populate_buy_trend).args) + strategy._sell_fun_len = len( + inspect.getfullargspec(strategy.populate_sell_trend).args) + + return import_strategy(strategy, config=config) except FileNotFoundError: logger.warning('Path "%s" does not exist', path) @@ -133,7 +148,7 @@ class StrategyResolver(object): return next(valid_strategies_gen, None) @staticmethod - def _search_strategy(directory: str, strategy_name: str) -> Optional[IStrategy]: + def _search_strategy(directory: str, strategy_name: str, config: dict) -> Optional[IStrategy]: """ Search for the strategy_name in the given directory :param directory: relative or absolute directory path @@ -149,5 +164,5 @@ class StrategyResolver(object): os.path.abspath(os.path.join(directory, entry)), strategy_name ) if strategy: - return strategy() + return strategy(config) return None diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 9c86d1ece..d18016e16 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -8,11 +8,9 @@ from unittest.mock import MagicMock import arrow import pytest -from jsonschema import validate from telegram import Chat, Message, Update -from freqtrade import constants -from freqtrade.analyze import Analyze +from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe from freqtrade.exchange import Exchange from freqtrade.freqtradebot import FreqtradeBot @@ -20,7 +18,7 @@ logging.getLogger('').setLevel(logging.INFO) def log_has(line, logs): - # caplog mocker returns log as a tuple: ('freqtrade.analyze', logging.WARNING, 'foobar') + # caplog mocker returns log as a tuple: ('freqtrade.something', logging.WARNING, 'foobar') # and we want to match line against foobar in the tuple return reduce(lambda a, b: a or b, filter(lambda x: x[2] == line, logs), @@ -29,6 +27,7 @@ def log_has(line, logs): def patch_exchange(mocker, api_mock=None) -> None: mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) if api_mock: mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) else: @@ -51,13 +50,11 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: """ # mocker.patch('freqtrade.fiat_convert.Market', {'price_usd': 12345.0}) patch_coinmarketcap(mocker, {'price_usd': 12345.0}) - mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) patch_exchange(mocker, None) mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock()) - mocker.patch('freqtrade.freqtradebot.Analyze.get_signal', MagicMock()) return FreqtradeBot(config) @@ -128,7 +125,6 @@ def default_conf(): "db_url": "sqlite://", "loglevel": logging.DEBUG, } - validate(configuration, constants.CONF_SCHEMA) return configuration @@ -616,7 +612,7 @@ def tickers(): @pytest.fixture def result(): with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file: - return Analyze.parse_ticker_dataframe(json.load(data_file)) + return parse_ticker_dataframe(json.load(data_file)) # FIX: # Create an fixture/function diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 3ddec0ded..d327b97c7 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1,7 +1,6 @@ # pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement # pragma pylint: disable=protected-access import logging -from copy import deepcopy from datetime import datetime from random import randint from unittest.mock import MagicMock, PropertyMock @@ -15,8 +14,6 @@ from freqtrade.tests.conftest import get_patched_exchange, log_has def ccxt_exceptionhandlers(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs): - """Function to test ccxt exception handling """ - with pytest.raises(TemporaryError): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError) exchange = get_patched_exchange(mocker, default_conf, api_mock) @@ -52,6 +49,93 @@ def test_init_exception(default_conf, mocker): Exchange(default_conf) +def test_symbol_amount_prec(default_conf, mocker): + ''' + Test rounds down to 4 Decimal places + ''' + api_mock = MagicMock() + api_mock.load_markets = MagicMock(return_value={ + 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' + }) + mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='binance')) + + markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': 4}}}) + type(api_mock).markets = markets + + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) + exchange = Exchange(default_conf) + + amount = 2.34559 + pair = 'ETH/BTC' + amount = exchange.symbol_amount_prec(pair, amount) + assert amount == 2.3455 + + +def test_symbol_price_prec(default_conf, mocker): + ''' + Test rounds up to 4 decimal places + ''' + api_mock = MagicMock() + api_mock.load_markets = MagicMock(return_value={ + 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' + }) + mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='binance')) + + markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': 4}}}) + type(api_mock).markets = markets + + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) + exchange = Exchange(default_conf) + + price = 2.34559 + pair = 'ETH/BTC' + price = exchange.symbol_price_prec(pair, price) + assert price == 2.3456 + + +def test_set_sandbox(default_conf, mocker): + """ + Test working scenario + """ + api_mock = MagicMock() + api_mock.load_markets = MagicMock(return_value={ + 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' + }) + url_mock = PropertyMock(return_value={'test': "api-public.sandbox.gdax.com", + 'api': 'https://api.gdax.com'}) + type(api_mock).urls = url_mock + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) + + exchange = Exchange(default_conf) + liveurl = exchange._api.urls['api'] + default_conf['exchange']['sandbox'] = True + exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname') + assert exchange._api.urls['api'] != liveurl + + +def test_set_sandbox_exception(default_conf, mocker): + """ + Test Fail scenario + """ + api_mock = MagicMock() + api_mock.load_markets = MagicMock(return_value={ + 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' + }) + url_mock = PropertyMock(return_value={'api': 'https://api.gdax.com'}) + type(api_mock).urls = url_mock + + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) + + with pytest.raises(OperationalException, match=r'does not provide a sandbox api'): + exchange = Exchange(default_conf) + default_conf['exchange']['sandbox'] = True + exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname') + + def test_validate_pairs(default_conf, mocker): api_mock = MagicMock() api_mock.load_markets = MagicMock(return_value={ @@ -61,6 +145,7 @@ def test_validate_pairs(default_conf, mocker): type(api_mock).id = id_mock mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) Exchange(default_conf) @@ -68,6 +153,7 @@ def test_validate_pairs_not_available(default_conf, mocker): api_mock = MagicMock() api_mock.load_markets = MagicMock(return_value={}) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) with pytest.raises(OperationalException, match=r'not available'): Exchange(default_conf) @@ -78,12 +164,11 @@ def test_validate_pairs_not_compatible(default_conf, mocker): api_mock.load_markets = MagicMock(return_value={ 'ETH/BTC': '', 'TKN/BTC': '', 'TRST/BTC': '', 'SWT/BTC': '', 'BCC/BTC': '' }) - conf = deepcopy(default_conf) - conf['stake_currency'] = 'ETH' + default_conf['stake_currency'] = 'ETH' mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) - + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) with pytest.raises(OperationalException, match=r'not compatible'): - Exchange(conf) + Exchange(default_conf) def test_validate_pairs_exception(default_conf, mocker, caplog): @@ -93,6 +178,7 @@ def test_validate_pairs_exception(default_conf, mocker, caplog): api_mock.load_markets = MagicMock(return_value={}) mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available at Binance'): Exchange(default_conf) @@ -107,20 +193,69 @@ def test_validate_pairs_exception(default_conf, mocker, caplog): def test_validate_pairs_stake_exception(default_conf, mocker, caplog): caplog.set_level(logging.INFO) - conf = deepcopy(default_conf) - conf['stake_currency'] = 'ETH' + default_conf['stake_currency'] = 'ETH' api_mock = MagicMock() api_mock.name = MagicMock(return_value='binance') mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) with pytest.raises( OperationalException, match=r'Pair ETH/BTC not compatible with stake_currency: ETH' ): - Exchange(conf) + Exchange(default_conf) -def test_exchangehas(default_conf, mocker): +def test_validate_timeframes(default_conf, mocker): + default_conf["ticker_interval"] = "5m" + api_mock = MagicMock() + id_mock = PropertyMock(return_value='test_exchange') + type(api_mock).id = id_mock + timeframes = PropertyMock(return_value={'1m': '1m', + '5m': '5m', + '15m': '15m', + '1h': '1h'}) + type(api_mock).timeframes = timeframes + + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + Exchange(default_conf) + + +def test_validate_timeframes_failed(default_conf, mocker): + default_conf["ticker_interval"] = "3m" + api_mock = MagicMock() + id_mock = PropertyMock(return_value='test_exchange') + type(api_mock).id = id_mock + timeframes = PropertyMock(return_value={'1m': '1m', + '5m': '5m', + '15m': '15m', + '1h': '1h'}) + type(api_mock).timeframes = timeframes + + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + with pytest.raises(OperationalException, match=r'Invalid ticker 3m, this Exchange supports.*'): + Exchange(default_conf) + + +def test_validate_timeframes_not_in_config(default_conf, mocker): + del default_conf["ticker_interval"] + api_mock = MagicMock() + id_mock = PropertyMock(return_value='test_exchange') + type(api_mock).id = id_mock + timeframes = PropertyMock(return_value={'1m': '1m', + '5m': '5m', + '15m': '15m', + '1h': '1h'}) + type(api_mock).timeframes = timeframes + + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) + Exchange(default_conf) + + +def test_exchange_has(default_conf, mocker): exchange = get_patched_exchange(mocker, default_conf) assert not exchange.exchange_has('ASDFASDF') api_mock = MagicMock() diff --git a/freqtrade/tests/exchange/test_exchange_helpers.py b/freqtrade/tests/exchange/test_exchange_helpers.py new file mode 100644 index 000000000..82525e805 --- /dev/null +++ b/freqtrade/tests/exchange/test_exchange_helpers.py @@ -0,0 +1,21 @@ +# pragma pylint: disable=missing-docstring, C0103 + +from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe + + +def test_dataframe_correct_length(result): + dataframe = parse_ticker_dataframe(result) + assert len(result.index) - 1 == len(dataframe.index) # last partial candle removed + + +def test_dataframe_correct_columns(result): + assert result.columns.tolist() == \ + ['date', 'open', 'high', 'low', 'close', 'volume'] + + +def test_parse_ticker_dataframe(ticker_history): + columns = ['date', 'open', 'high', 'low', 'close', 'volume'] + + # Test file with BV data + dataframe = parse_ticker_dataframe(ticker_history) + assert dataframe.columns.tolist() == columns diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index cb225e465..5d121d27c 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -3,7 +3,6 @@ import json import math import random -from copy import deepcopy from typing import List from unittest.mock import MagicMock @@ -13,11 +12,12 @@ import pytest from arrow import Arrow from freqtrade import DependencyException, constants, optimize -from freqtrade.analyze import Analyze from freqtrade.arguments import Arguments, TimeRange from freqtrade.optimize.backtesting import (Backtesting, setup_configuration, start) from freqtrade.tests.conftest import log_has, patch_exchange +from freqtrade.strategy.interface import SellType +from freqtrade.strategy.default_strategy import DefaultStrategy def get_args(args) -> List[str]: @@ -96,7 +96,7 @@ def simple_backtest(config, contour, num_results, mocker) -> None: 'stake_amount': config['stake_amount'], 'processed': processed, 'max_open_trades': 1, - 'realistic': True + 'position_stacking': False } ) # results :: @@ -127,7 +127,7 @@ def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None): 'stake_amount': conf['stake_amount'], 'processed': backtesting.tickerdata_to_dataframe(data), 'max_open_trades': 10, - 'realistic': True, + 'position_stacking': False, 'record': record } @@ -145,7 +145,7 @@ def _trend(signals, buy_value, sell_value): return signals -def _trend_alternate(dataframe=None): +def _trend_alternate(dataframe=None, metadata=None): signals = dataframe low = signals['low'] n = len(low) @@ -163,9 +163,6 @@ def _trend_alternate(dataframe=None): # Unit tests def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: - """ - Test setup_configuration() function - """ mocker.patch('freqtrade.configuration.open', mocker.mock_open( read_data=json.dumps(default_conf) )) @@ -193,8 +190,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert 'live' not in config assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples) - assert 'realistic_simulation' not in config - assert not log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples) + assert 'position_stacking' not in config + assert not log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples) assert 'refresh_pairs' not in config assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) @@ -204,9 +201,6 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None: - """ - Test setup_configuration() function - """ mocker.patch('freqtrade.configuration.open', mocker.mock_open( read_data=json.dumps(default_conf) )) @@ -218,7 +212,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non 'backtesting', '--ticker-interval', '1m', '--live', - '--realistic-simulation', + '--enable-position-stacking', + '--disable-max-market-positions', '--refresh-pairs-cached', '--timerange', ':100', '--export', '/bar/foo', @@ -246,9 +241,12 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non assert 'live' in config assert log_has('Parameter -l/--live detected ...', caplog.record_tuples) - assert 'realistic_simulation' in config - assert log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples) - assert log_has('Using max_open_trades: 1 ...', caplog.record_tuples) + assert 'position_stacking' in config + assert log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples) + + assert 'use_max_market_positions' in config + assert log_has('Parameter --disable-max-market-positions detected ...', caplog.record_tuples) + assert log_has('max_open_trades set to unlimited ...', caplog.record_tuples) assert 'refresh_pairs' in config assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) @@ -271,15 +269,10 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog) -> None: - """ - Test setup_configuration() function - """ - - conf = deepcopy(default_conf) - conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT + default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(conf) + read_data=json.dumps(default_conf) )) args = [ @@ -293,9 +286,6 @@ def test_setup_configuration_unlimited_stake_amount(mocker, default_conf, caplog def test_start(mocker, fee, default_conf, caplog) -> None: - """ - Test start() function - """ start_mock = MagicMock() mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) @@ -318,26 +308,19 @@ def test_start(mocker, fee, default_conf, caplog) -> None: def test_backtesting_init(mocker, default_conf) -> None: - """ - Test Backtesting._init() method - """ patch_exchange(mocker) get_fee = mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.5)) backtesting = Backtesting(default_conf) assert backtesting.config == default_conf - assert isinstance(backtesting.analyze, Analyze) assert backtesting.ticker_interval == '5m' assert callable(backtesting.tickerdata_to_dataframe) - assert callable(backtesting.populate_buy_trend) - assert callable(backtesting.populate_sell_trend) + assert callable(backtesting.advise_buy) + assert callable(backtesting.advise_sell) get_fee.assert_called() assert backtesting.fee == 0.5 def test_tickerdata_to_dataframe(default_conf, mocker) -> None: - """ - Test Backtesting.tickerdata_to_dataframe() method - """ patch_exchange(mocker) timerange = TimeRange(None, 'line', 0, -100) tick = optimize.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange) @@ -347,16 +330,13 @@ def test_tickerdata_to_dataframe(default_conf, mocker) -> None: data = backtesting.tickerdata_to_dataframe(tickerlist) assert len(data['UNITTEST/BTC']) == 99 - # Load Analyze to compare the result between Backtesting function and Analyze are the same - analyze = Analyze(default_conf) - data2 = analyze.tickerdata_to_dataframe(tickerlist) + # Load strategy to compare the result between Backtesting function and strategy are the same + strategy = DefaultStrategy(default_conf) + data2 = strategy.tickerdata_to_dataframe(tickerlist) assert data['UNITTEST/BTC'].equals(data2['UNITTEST/BTC']) def test_get_timeframe(default_conf, mocker) -> None: - """ - Test Backtesting.get_timeframe() method - """ patch_exchange(mocker) backtesting = Backtesting(default_conf) @@ -373,9 +353,6 @@ def test_get_timeframe(default_conf, mocker) -> None: def test_generate_text_table(default_conf, mocker): - """ - Test Backtesting.generate_text_table() method - """ patch_exchange(mocker) backtesting = Backtesting(default_conf) @@ -391,27 +368,48 @@ def test_generate_text_table(default_conf, mocker): ) result_str = ( - '| pair | buy count | avg profit % | ' - 'total profit BTC | avg duration | profit | loss |\n' - '|:--------|------------:|---------------:|' - '-------------------:|---------------:|---------:|-------:|\n' - '| ETH/BTC | 2 | 15.00 | ' - '0.60000000 | 20.0 | 2 | 0 |\n' - '| TOTAL | 2 | 15.00 | ' - '0.60000000 | 20.0 | 2 | 0 |' + '| pair | buy count | avg profit % | cum profit % | ' + 'total profit BTC | avg duration | profit | loss |\n' + '|:--------|------------:|---------------:|---------------:|' + '-------------------:|:---------------|---------:|-------:|\n' + '| ETH/BTC | 2 | 15.00 | 30.00 | ' + '0.60000000 | 0:20:00 | 2 | 0 |\n' + '| TOTAL | 2 | 15.00 | 30.00 | ' + '0.60000000 | 0:20:00 | 2 | 0 |' ) assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str -def test_backtesting_start(default_conf, mocker, caplog) -> None: - """ - Test Backtesting.start() method - """ +def test_generate_text_table_sell_reason(default_conf, mocker): + patch_exchange(mocker) + backtesting = Backtesting(default_conf) + results = pd.DataFrame( + { + 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], + 'profit_percent': [0.1, 0.2, 0.3], + 'profit_abs': [0.2, 0.4, 0.5], + 'trade_duration': [10, 30, 10], + 'profit': [2, 0, 0], + 'loss': [0, 0, 1], + 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] + } + ) + + result_str = ( + '| Sell Reason | Count |\n' + '|:--------------|--------:|\n' + '| roi | 2 |\n' + '| stop_loss | 1 |' + ) + assert backtesting._generate_text_table_sell_reason( + data={'ETH/BTC': {}}, results=results) == result_str + + +def test_backtesting_start(default_conf, mocker, caplog) -> None: def get_timeframe(input1, input2): return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) - mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock()) mocker.patch('freqtrade.optimize.load_data', mocked_load_data) mocker.patch('freqtrade.exchange.Exchange.get_ticker_history') patch_exchange(mocker) @@ -422,15 +420,14 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None: get_timeframe=get_timeframe, ) - conf = deepcopy(default_conf) - conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] - conf['ticker_interval'] = 1 - conf['live'] = False - conf['datadir'] = None - conf['export'] = None - conf['timerange'] = '-100' + default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] + default_conf['ticker_interval'] = 1 + default_conf['live'] = False + default_conf['datadir'] = None + default_conf['export'] = None + default_conf['timerange'] = '-100' - backtesting = Backtesting(conf) + backtesting = Backtesting(default_conf) backtesting.start() # check the logs, that will contain the backtest result exists = [ @@ -445,14 +442,9 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None: def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None: - """ - Test Backtesting.start() method if no data is found - """ - def get_timeframe(input1, input2): return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59) - mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock()) mocker.patch('freqtrade.optimize.load_data', MagicMock(return_value={})) mocker.patch('freqtrade.exchange.Exchange.get_ticker_history') patch_exchange(mocker) @@ -463,15 +455,14 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None: get_timeframe=get_timeframe, ) - conf = deepcopy(default_conf) - conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] - conf['ticker_interval'] = "1m" - conf['live'] = False - conf['datadir'] = None - conf['export'] = None - conf['timerange'] = '20180101-20180102' + default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] + default_conf['ticker_interval'] = "1m" + default_conf['live'] = False + default_conf['datadir'] = None + default_conf['export'] = None + default_conf['timerange'] = '20180101-20180102' - backtesting = Backtesting(conf) + backtesting = Backtesting(default_conf) backtesting.start() # check the logs, that will contain the backtest result @@ -479,31 +470,53 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None: def test_backtest(default_conf, fee, mocker) -> None: - """ - Test Backtesting.backtest() method - """ mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) backtesting = Backtesting(default_conf) - + pair = 'UNITTEST/BTC' data = optimize.load_data(None, ticker_interval='5m', pairs=['UNITTEST/BTC']) data = trim_dictlist(data, -200) + data_processed = backtesting.tickerdata_to_dataframe(data) results = backtesting.backtest( { 'stake_amount': default_conf['stake_amount'], - 'processed': backtesting.tickerdata_to_dataframe(data), + 'processed': data_processed, 'max_open_trades': 10, - 'realistic': True + 'position_stacking': False } ) assert not results.empty assert len(results) == 2 + expected = pd.DataFrame( + {'pair': [pair, pair], + 'profit_percent': [0.00029975, 0.00056708], + 'profit_abs': [1.49e-06, 7.6e-07], + 'open_time': [Arrow(2018, 1, 29, 18, 40, 0).datetime, + Arrow(2018, 1, 30, 3, 30, 0).datetime], + 'close_time': [Arrow(2018, 1, 29, 22, 40, 0).datetime, + Arrow(2018, 1, 30, 4, 20, 0).datetime], + 'open_index': [77, 183], + 'close_index': [125, 193], + 'trade_duration': [240, 50], + 'open_at_end': [False, False], + 'open_rate': [0.104445, 0.10302485], + 'close_rate': [0.105, 0.10359999], + 'sell_reason': [SellType.ROI, SellType.ROI] + }) + pd.testing.assert_frame_equal(results, expected) + data_pair = data_processed[pair] + for _, t in results.iterrows(): + ln = data_pair.loc[data_pair["date"] == t["open_time"]] + # Check open trade rate alignes to open rate + assert ln is not None + assert round(ln.iloc[0]["open"], 6) == round(t["open_rate"], 6) + # check close trade rate alignes to close rate + ln = data_pair.loc[data_pair["date"] == t["close_time"]] + assert round(ln.iloc[0]["open"], 6) == round(t["close_rate"], 6) + def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None: - """ - Test Backtesting.backtest() method with 1 min ticker - """ mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) backtesting = Backtesting(default_conf) @@ -516,7 +529,7 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None: 'stake_amount': default_conf['stake_amount'], 'processed': backtesting.tickerdata_to_dataframe(data), 'max_open_trades': 1, - 'realistic': True + 'position_stacking': False } ) assert not results.empty @@ -524,9 +537,6 @@ def test_backtest_1min_ticker_interval(default_conf, fee, mocker) -> None: def test_processed(default_conf, mocker) -> None: - """ - Test Backtesting.backtest() method with offline data - """ patch_exchange(mocker) backtesting = Backtesting(default_conf) @@ -552,42 +562,42 @@ def test_backtest_ticks(default_conf, fee, mocker): mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) patch_exchange(mocker) ticks = [1, 5] - fun = Backtesting(default_conf).populate_buy_trend + fun = Backtesting(default_conf).advise_buy for _ in ticks: backtest_conf = _make_backtest_conf(mocker, conf=default_conf) backtesting = Backtesting(default_conf) - backtesting.populate_buy_trend = fun # Override - backtesting.populate_sell_trend = fun # Override + backtesting.advise_buy = fun # Override + backtesting.advise_sell = fun # Override results = backtesting.backtest(backtest_conf) assert not results.empty def test_backtest_clash_buy_sell(mocker, default_conf): # Override the default buy trend function in our default_strategy - def fun(dataframe=None): + def fun(dataframe=None, pair=None): buy_value = 1 sell_value = 1 return _trend(dataframe, buy_value, sell_value) backtest_conf = _make_backtest_conf(mocker, conf=default_conf) backtesting = Backtesting(default_conf) - backtesting.populate_buy_trend = fun # Override - backtesting.populate_sell_trend = fun # Override + backtesting.advise_buy = fun # Override + backtesting.advise_sell = fun # Override results = backtesting.backtest(backtest_conf) assert results.empty def test_backtest_only_sell(mocker, default_conf): # Override the default buy trend function in our default_strategy - def fun(dataframe=None): + def fun(dataframe=None, pair=None): buy_value = 0 sell_value = 1 return _trend(dataframe, buy_value, sell_value) backtest_conf = _make_backtest_conf(mocker, conf=default_conf) backtesting = Backtesting(default_conf) - backtesting.populate_buy_trend = fun # Override - backtesting.populate_sell_trend = fun # Override + backtesting.advise_buy = fun # Override + backtesting.advise_sell = fun # Override results = backtesting.backtest(backtest_conf) assert results.empty @@ -596,8 +606,8 @@ def test_backtest_alternate_buy_sell(default_conf, fee, mocker): mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) backtest_conf = _make_backtest_conf(mocker, conf=default_conf, pair='UNITTEST/BTC') backtesting = Backtesting(default_conf) - backtesting.populate_buy_trend = _trend_alternate # Override - backtesting.populate_sell_trend = _trend_alternate # Override + backtesting.advise_buy = _trend_alternate # Override + backtesting.advise_sell = _trend_alternate # Override results = backtesting.backtest(backtest_conf) backtesting._store_backtest_result("test_.json", results) assert len(results) == 4 @@ -633,7 +643,9 @@ def test_backtest_record(default_conf, fee, mocker): "open_index": [1, 119, 153, 185], "close_index": [118, 151, 184, 199], "trade_duration": [123, 34, 31, 14], - "open_at_end": [False, False, False, True] + "open_at_end": [False, False, False, True], + "sell_reason": [SellType.ROI, SellType.STOP_LOSS, + SellType.ROI, SellType.FORCE_SELL] }) backtesting._store_backtest_result("backtest-result.json", results) assert len(results) == 4 @@ -646,7 +658,7 @@ def test_backtest_record(default_conf, fee, mocker): # Below follows just a typecheck of the schema/type of trade-records oix = None for (pair, profit, date_buy, date_sell, buy_index, dur, - openr, closer, open_at_end) in records: + openr, closer, open_at_end, sell_reason) in records: assert pair == 'UNITTEST/BTC' assert isinstance(profit, float) # FIX: buy/sell should be converted to ints @@ -655,6 +667,7 @@ def test_backtest_record(default_conf, fee, mocker): assert isinstance(openr, float) assert isinstance(closer, float) assert isinstance(open_at_end, bool) + assert isinstance(sell_reason, str) isinstance(buy_index, pd._libs.tslib.Timestamp) if oix: assert buy_index > oix @@ -663,15 +676,14 @@ def test_backtest_record(default_conf, fee, mocker): def test_backtest_start_live(default_conf, mocker, caplog): - conf = deepcopy(default_conf) - conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] + default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', new=lambda s, n, i: _load_pair_as_ticks(n, i)) patch_exchange(mocker) mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock()) mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock()) mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(conf) + read_data=json.dumps(default_conf) )) args = MagicMock() @@ -691,7 +703,8 @@ def test_backtest_start_live(default_conf, mocker, caplog): '--ticker-interval', '1m', '--live', '--timerange', '-100', - '--realistic-simulation' + '--enable-position-stacking', + '--disable-max-market-positions' ] args = get_args(args) start(args) @@ -700,14 +713,14 @@ def test_backtest_start_live(default_conf, mocker, caplog): 'Parameter -i/--ticker-interval detected ...', 'Using ticker_interval: 1m ...', 'Parameter -l/--live detected ...', - 'Using max_open_trades: 1 ...', + 'Ignoring max_open_trades (--disable-max-market-positions was used) ...', 'Parameter --timerange detected: -100 ...', 'Using data folder: freqtrade/tests/testdata ...', 'Using stake_currency: BTC ...', 'Using stake_amount: 0.001 ...', 'Downloading data for all pairs in whitelist ...', 'Measuring data from 2017-11-14T19:31:00+00:00 up to 2017-11-14T22:58:00+00:00 (0 days)..', - 'Parameter --realistic-simulation detected ...' + 'Parameter --enable-position-stacking detected ...' ] for line in exists: diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 72a102c22..65a3c2fdb 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -1,6 +1,5 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 import os -from copy import deepcopy from unittest.mock import MagicMock import pandas as pd @@ -12,29 +11,22 @@ from freqtrade.strategy.resolver import StrategyResolver from freqtrade.tests.conftest import log_has, patch_exchange from freqtrade.tests.optimize.test_backtesting import get_args -# Avoid to reinit the same object again and again -_HYPEROPT_INITIALIZED = False -_HYPEROPT = None - @pytest.fixture(scope='function') -def init_hyperopt(default_conf, mocker): - global _HYPEROPT_INITIALIZED, _HYPEROPT - if not _HYPEROPT_INITIALIZED: - patch_exchange(mocker) - _HYPEROPT = Hyperopt(default_conf) - _HYPEROPT_INITIALIZED = True +def hyperopt(default_conf, mocker): + patch_exchange(mocker) + return Hyperopt(default_conf) # Functions for recurrent object patching -def create_trials(mocker) -> None: +def create_trials(mocker, hyperopt) -> None: """ When creating trials, mock the hyperopt Trials so that *by default* - we don't create any pickle'd files in the filesystem - we might have a pickle'd file so make sure that we return false when looking for it """ - _HYPEROPT.trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle') + hyperopt.trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle') mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=False) mocker.patch('freqtrade.optimize.hyperopt.os.path.getsize', return_value=1) @@ -45,9 +37,6 @@ def create_trials(mocker) -> None: def test_start(mocker, default_conf, caplog) -> None: - """ - Test start() function - """ start_mock = MagicMock() mocker.patch( 'freqtrade.configuration.Configuration._load_config_file', @@ -76,11 +65,7 @@ def test_start(mocker, default_conf, caplog) -> None: assert start_mock.call_count == 1 -def test_loss_calculation_prefer_correct_trade_count(init_hyperopt) -> None: - """ - Test Hyperopt.calculate_loss() - """ - hyperopt = _HYPEROPT +def test_loss_calculation_prefer_correct_trade_count(hyperopt) -> None: StrategyResolver({'strategy': 'DefaultStrategy'}) correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20) @@ -90,20 +75,13 @@ def test_loss_calculation_prefer_correct_trade_count(init_hyperopt) -> None: assert under > correct -def test_loss_calculation_prefer_shorter_trades(init_hyperopt) -> None: - """ - Test Hyperopt.calculate_loss() - """ - hyperopt = _HYPEROPT - +def test_loss_calculation_prefer_shorter_trades(hyperopt) -> None: shorter = hyperopt.calculate_loss(1, 100, 20) longer = hyperopt.calculate_loss(1, 100, 30) assert shorter < longer -def test_loss_calculation_has_limited_profit(init_hyperopt) -> None: - hyperopt = _HYPEROPT - +def test_loss_calculation_has_limited_profit(hyperopt) -> None: correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20) over = hyperopt.calculate_loss(hyperopt.expected_max_profit * 2, hyperopt.target_trades, 20) under = hyperopt.calculate_loss(hyperopt.expected_max_profit / 2, hyperopt.target_trades, 20) @@ -111,8 +89,7 @@ def test_loss_calculation_has_limited_profit(init_hyperopt) -> None: assert under > correct -def test_log_results_if_loss_improves(init_hyperopt, capsys) -> None: - hyperopt = _HYPEROPT +def test_log_results_if_loss_improves(hyperopt, capsys) -> None: hyperopt.current_best_loss = 2 hyperopt.log_results( { @@ -123,11 +100,10 @@ def test_log_results_if_loss_improves(init_hyperopt, capsys) -> None: } ) out, err = capsys.readouterr() - assert ' 1/2: foo. Loss 1.00000'in out + assert ' 1/2: foo. Loss 1.00000' in out -def test_no_log_if_loss_does_not_improve(init_hyperopt, caplog) -> None: - hyperopt = _HYPEROPT +def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None: hyperopt.current_best_loss = 2 hyperopt.log_results( { @@ -137,13 +113,10 @@ def test_no_log_if_loss_does_not_improve(init_hyperopt, caplog) -> None: assert caplog.record_tuples == [] -def test_save_trials_saves_trials(mocker, init_hyperopt, caplog) -> None: - trials = create_trials(mocker) +def test_save_trials_saves_trials(mocker, hyperopt, caplog) -> None: + trials = create_trials(mocker, hyperopt) mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None) - - hyperopt = _HYPEROPT - _HYPEROPT.trials = trials - + hyperopt.trials = trials hyperopt.save_trials() trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle') @@ -154,11 +127,9 @@ def test_save_trials_saves_trials(mocker, init_hyperopt, caplog) -> None: mock_dump.assert_called_once() -def test_read_trials_returns_trials_file(mocker, init_hyperopt, caplog) -> None: - trials = create_trials(mocker) +def test_read_trials_returns_trials_file(mocker, hyperopt, caplog) -> None: + trials = create_trials(mocker, hyperopt) mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=trials) - - hyperopt = _HYPEROPT hyperopt_trial = hyperopt.read_trials() trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle') assert log_has( @@ -169,7 +140,7 @@ def test_read_trials_returns_trials_file(mocker, init_hyperopt, caplog) -> None: mock_load.assert_called_once() -def test_roi_table_generation(init_hyperopt) -> None: +def test_roi_table_generation(hyperopt) -> None: params = { 'roi_t1': 5, 'roi_t2': 10, @@ -179,11 +150,10 @@ def test_roi_table_generation(init_hyperopt) -> None: 'roi_p3': 3, } - hyperopt = _HYPEROPT assert hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0} -def test_start_calls_optimizer(mocker, init_hyperopt, default_conf, caplog) -> None: +def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.multiprocessing.cpu_count', MagicMock(return_value=1)) @@ -193,13 +163,12 @@ def test_start_calls_optimizer(mocker, init_hyperopt, default_conf, caplog) -> N ) patch_exchange(mocker) - conf = deepcopy(default_conf) - conf.update({'config': 'config.json.example'}) - conf.update({'epochs': 1}) - conf.update({'timerange': None}) - conf.update({'spaces': 'all'}) + default_conf.update({'config': 'config.json.example'}) + default_conf.update({'epochs': 1}) + default_conf.update({'timerange': None}) + default_conf.update({'spaces': 'all'}) - hyperopt = Hyperopt(conf) + hyperopt = Hyperopt(default_conf) hyperopt.tickerdata_to_dataframe = MagicMock() hyperopt.start() @@ -209,11 +178,7 @@ def test_start_calls_optimizer(mocker, init_hyperopt, default_conf, caplog) -> N assert dumper.called -def test_format_results(init_hyperopt): - """ - Test Hyperopt.format_results() - """ - +def test_format_results(hyperopt): # Test with BTC as stake_currency trades = [ ('ETH/BTC', 2, 2, 123), @@ -223,7 +188,7 @@ def test_format_results(init_hyperopt): labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration'] df = pd.DataFrame.from_records(trades, columns=labels) - result = _HYPEROPT.format_results(df) + result = hyperopt.format_results(df) assert result.find(' 66.67%') assert result.find('Total profit 1.00000000 BTC') assert result.find('2.0000Σ %') @@ -235,31 +200,25 @@ def test_format_results(init_hyperopt): ('XPR/EUR', -1, -2, -246) ] df = pd.DataFrame.from_records(trades, columns=labels) - result = _HYPEROPT.format_results(df) + result = hyperopt.format_results(df) assert result.find('Total profit 1.00000000 EUR') -def test_has_space(init_hyperopt): - """ - Test Hyperopt.has_space() method - """ - _HYPEROPT.config.update({'spaces': ['buy', 'roi']}) - assert _HYPEROPT.has_space('roi') - assert _HYPEROPT.has_space('buy') - assert not _HYPEROPT.has_space('stoploss') +def test_has_space(hyperopt): + hyperopt.config.update({'spaces': ['buy', 'roi']}) + assert hyperopt.has_space('roi') + assert hyperopt.has_space('buy') + assert not hyperopt.has_space('stoploss') - _HYPEROPT.config.update({'spaces': ['all']}) - assert _HYPEROPT.has_space('buy') + hyperopt.config.update({'spaces': ['all']}) + assert hyperopt.has_space('buy') -def test_populate_indicators(init_hyperopt) -> None: - """ - Test Hyperopt.populate_indicators() - """ +def test_populate_indicators(hyperopt) -> None: tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') tickerlist = {'UNITTEST/BTC': tick} - dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist) - dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC']) + dataframes = hyperopt.tickerdata_to_dataframe(tickerlist) + dataframe = hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'}) # Check if some indicators are generated. We will not test all of them assert 'adx' in dataframe @@ -267,16 +226,13 @@ def test_populate_indicators(init_hyperopt) -> None: assert 'rsi' in dataframe -def test_buy_strategy_generator(init_hyperopt) -> None: - """ - Test Hyperopt.buy_strategy_generator() - """ +def test_buy_strategy_generator(hyperopt) -> None: tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') tickerlist = {'UNITTEST/BTC': tick} - dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist) - dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC']) + dataframes = hyperopt.tickerdata_to_dataframe(tickerlist) + dataframe = hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'}) - populate_buy_trend = _HYPEROPT.buy_strategy_generator( + populate_buy_trend = hyperopt.buy_strategy_generator( { 'adx-value': 20, 'fastd-value': 20, @@ -289,20 +245,16 @@ def test_buy_strategy_generator(init_hyperopt) -> None: 'trigger': 'bb_lower' } ) - result = populate_buy_trend(dataframe) + result = populate_buy_trend(dataframe, {'pair': 'UNITTEST/BTC'}) # Check if some indicators are generated. We will not test all of them assert 'buy' in result assert 1 in result['buy'] -def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None: - """ - Test Hyperopt.generate_optimizer() function - """ - conf = deepcopy(default_conf) - conf.update({'config': 'config.json.example'}) - conf.update({'timerange': None}) - conf.update({'spaces': 'all'}) +def test_generate_optimizer(mocker, default_conf) -> None: + default_conf.update({'config': 'config.json.example'}) + default_conf.update({'timerange': None}) + default_conf.update({'spaces': 'all'}) trades = [ ('POWR/BTC', 0.023117, 0.000233, 100) @@ -335,7 +287,6 @@ def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None: 'roi_p3': 0.1, 'stoploss': -0.4, } - response_expected = { 'loss': 1.9840569076926293, 'result': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC ' @@ -343,6 +294,6 @@ def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None: 'params': optimizer_param } - hyperopt = Hyperopt(conf) + hyperopt = Hyperopt(default_conf) generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values())) assert generate_optimizer_value == response_expected diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py index 4ab32343a..eef79bef3 100644 --- a/freqtrade/tests/optimize/test_optimize.py +++ b/freqtrade/tests/optimize/test_optimize.py @@ -53,9 +53,6 @@ def _clean_test_file(file: str) -> None: def test_load_data_30min_ticker(ticker_history, mocker, caplog, default_conf) -> None: - """ - Test load_data() with 30 min ticker - """ mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-30m.json') _backup_file(file, copy_file=True) @@ -66,9 +63,6 @@ def test_load_data_30min_ticker(ticker_history, mocker, caplog, default_conf) -> def test_load_data_5min_ticker(ticker_history, mocker, caplog, default_conf) -> None: - """ - Test load_data() with 5 min ticker - """ mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-5m.json') @@ -80,11 +74,7 @@ def test_load_data_5min_ticker(ticker_history, mocker, caplog, default_conf) -> def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None: - """ - Test load_data() with 1 min ticker - """ mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) - file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json') _backup_file(file, copy_file=True) optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC']) @@ -421,10 +411,6 @@ def test_trim_tickerlist() -> None: def test_file_dump_json() -> None: - """ - Test file_dump_json() - :return: None - """ file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'test_{id}.json'.format(id=str(uuid.uuid4()))) data = {'bar': 'foo'} diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 58514d1c0..70b7dcfd9 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -1,20 +1,18 @@ +# pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=invalid-sequence-index, invalid-name, too-many-arguments -""" -Unit test file for rpc/rpc.py -""" - from datetime import datetime -from unittest.mock import MagicMock +from unittest.mock import MagicMock, ANY import pytest +from freqtrade.fiat_convert import CryptoToFiatConverter from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade -from freqtrade.rpc.rpc import RPC, RPCException +from freqtrade.rpc import RPC, RPCException from freqtrade.state import State -from freqtrade.tests.test_freqtradebot import (patch_coinmarketcap, - patch_get_signal) +from freqtrade.tests.test_freqtradebot import patch_get_signal +from freqtrade.tests.conftest import patch_coinmarketcap # Functions for recurrent object patching @@ -27,10 +25,6 @@ def prec_satoshi(a, b) -> float: # Unit tests def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: - """ - Test rpc_trade_status() method - """ - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -42,6 +36,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: ) freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) freqtradebot.state = State.STOPPED @@ -53,31 +48,24 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: rpc._rpc_trade_status() freqtradebot.create_trade() - trades = rpc._rpc_trade_status() - trade = trades[0] + results = rpc._rpc_trade_status() - result_message = [ - '*Trade ID:* `1`\n' - '*Current Pair:* ' - '[ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n' - '*Open Since:* `just now`\n' - '*Amount:* `90.99181074`\n' - '*Open Rate:* `0.00001099`\n' - '*Close Rate:* `None`\n' - '*Current Rate:* `0.00001098`\n' - '*Close Profit:* `None`\n' - '*Current Profit:* `-0.59%`\n' - '*Open Order:* `(limit buy rem=0.00000000)`' - ] - assert trades == result_message - assert trade.find('[ETH/BTC]') >= 0 + assert { + 'trade_id': 1, + 'pair': 'ETH/BTC', + 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', + 'date': ANY, + 'open_rate': 1.099e-05, + 'close_rate': None, + 'current_rate': 1.098e-05, + 'amount': 90.99181074, + 'close_profit': None, + 'current_profit': -0.59, + 'open_order': '(limit buy rem=0.00000000)' + } == results[0] def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: - """ - Test rpc_status_table() method - """ - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -89,14 +77,15 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: ) freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) freqtradebot.state = State.STOPPED - with pytest.raises(RPCException, match=r'.*\*Status:\* `trader is not running``*'): + with pytest.raises(RPCException, match=r'.*trader is not running*'): rpc._rpc_status_table() freqtradebot.state = State.RUNNING - with pytest.raises(RPCException, match=r'.*\*Status:\* `no active order`*'): + with pytest.raises(RPCException, match=r'.*no active order*'): rpc._rpc_status_table() freqtradebot.create_trade() @@ -108,10 +97,6 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: def test_rpc_daily_profit(default_conf, update, ticker, fee, limit_buy_order, limit_sell_order, markets, mocker) -> None: - """ - Test rpc_daily_profit() method - """ - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -123,11 +108,12 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, ) freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) stake_currency = default_conf['stake_currency'] fiat_display_currency = default_conf['fiat_display_currency'] rpc = RPC(freqtradebot) - + rpc._fiat_converter = CryptoToFiatConverter() # Create some test data freqtradebot.create_trade() trade = Trade.query.first() @@ -160,15 +146,12 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee, def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, limit_buy_order, limit_sell_order, markets, mocker) -> None: - """ - Test rpc_trade_statistics() method - """ - patch_get_signal(mocker, (True, False)) mocker.patch.multiple( 'freqtrade.fiat_convert.Market', ticker=MagicMock(return_value={'price_usd': 15000.0}), ) - mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) + patch_coinmarketcap(mocker) + mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -179,10 +162,12 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, ) freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) stake_currency = default_conf['stake_currency'] fiat_display_currency = default_conf['fiat_display_currency'] rpc = RPC(freqtradebot) + rpc._fiat_converter = CryptoToFiatConverter() with pytest.raises(RPCException, match=r'.*no closed trade*'): rpc._rpc_trade_statistics(stake_currency, fiat_display_currency) @@ -237,10 +222,6 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, # trade.open_rate (it is set to None) def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets, ticker_sell_up, limit_buy_order, limit_sell_order): - """ - Test rpc_trade_statistics() method - """ - patch_get_signal(mocker, (True, False)) mocker.patch.multiple( 'freqtrade.fiat_convert.Market', ticker=MagicMock(return_value={'price_usd': 15000.0}), @@ -256,6 +237,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets, ) freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) stake_currency = default_conf['stake_currency'] fiat_display_currency = default_conf['fiat_display_currency'] @@ -296,9 +278,6 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets, def test_rpc_balance_handle(default_conf, mocker): - """ - Test rpc_balance() method - """ mock_balance = { 'BTC': { 'free': 10.0, @@ -312,12 +291,12 @@ def test_rpc_balance_handle(default_conf, mocker): } } - patch_get_signal(mocker, (True, False)) mocker.patch.multiple( 'freqtrade.fiat_convert.Market', ticker=MagicMock(return_value={'price_usd': 15000.0}), ) - mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) + patch_coinmarketcap(mocker) + mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -326,25 +305,24 @@ def test_rpc_balance_handle(default_conf, mocker): ) freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) + rpc._fiat_converter = CryptoToFiatConverter() - output, total, symbol, value = rpc._rpc_balance(default_conf['fiat_display_currency']) - assert prec_satoshi(total, 12) - assert prec_satoshi(value, 180000) - assert 'USD' in symbol - assert len(output) == 1 - assert 'BTC' in output[0]['currency'] - assert prec_satoshi(output[0]['available'], 10) - assert prec_satoshi(output[0]['balance'], 12) - assert prec_satoshi(output[0]['pending'], 2) - assert prec_satoshi(output[0]['est_btc'], 12) + result = rpc._rpc_balance(default_conf['fiat_display_currency']) + assert prec_satoshi(result['total'], 12) + assert prec_satoshi(result['value'], 180000) + assert 'USD' == result['symbol'] + assert result['currencies'] == [{ + 'currency': 'BTC', + 'available': 10.0, + 'balance': 12.0, + 'pending': 2.0, + 'est_btc': 12.0, + }] def test_rpc_start(mocker, default_conf) -> None: - """ - Test rpc_start() method - """ - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -354,23 +332,20 @@ def test_rpc_start(mocker, default_conf) -> None: ) freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) freqtradebot.state = State.STOPPED result = rpc._rpc_start() - assert '`Starting trader ...`' in result + assert {'status': 'starting trader ...'} == result assert freqtradebot.state == State.RUNNING result = rpc._rpc_start() - assert '*Status:* `already running`' in result + assert {'status': 'already running'} == result assert freqtradebot.state == State.RUNNING def test_rpc_stop(mocker, default_conf) -> None: - """ - Test rpc_stop() method - """ - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -380,23 +355,21 @@ def test_rpc_stop(mocker, default_conf) -> None: ) freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) freqtradebot.state = State.RUNNING result = rpc._rpc_stop() - assert '`Stopping trader ...`' in result + assert {'status': 'stopping trader ...'} == result assert freqtradebot.state == State.STOPPED result = rpc._rpc_stop() - assert '*Status:* `already stopped`' in result + + assert {'status': 'already stopped'} == result assert freqtradebot.state == State.STOPPED def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: - """ - Test rpc_forcesell() method - """ - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) @@ -418,14 +391,15 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: ) freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) freqtradebot.state = State.STOPPED - with pytest.raises(RPCException, match=r'.*`trader is not running`*'): + with pytest.raises(RPCException, match=r'.*trader is not running*'): rpc._rpc_forcesell(None) freqtradebot.state = State.RUNNING - with pytest.raises(RPCException, match=r'.*Invalid argument.*'): + with pytest.raises(RPCException, match=r'.*invalid argument*'): rpc._rpc_forcesell(None) rpc._rpc_forcesell('all') @@ -436,10 +410,10 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: rpc._rpc_forcesell('1') freqtradebot.state = State.STOPPED - with pytest.raises(RPCException, match=r'.*`trader is not running`*'): + with pytest.raises(RPCException, match=r'.*trader is not running*'): rpc._rpc_forcesell(None) - with pytest.raises(RPCException, match=r'.*`trader is not running`*'): + with pytest.raises(RPCException, match=r'.*trader is not running*'): rpc._rpc_forcesell('all') freqtradebot.state = State.RUNNING @@ -497,10 +471,6 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: def test_performance_handle(default_conf, ticker, limit_buy_order, fee, limit_sell_order, markets, mocker) -> None: - """ - Test rpc_performance() method - """ - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -513,6 +483,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, ) freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) # Create some test data @@ -536,10 +507,6 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee, def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None: - """ - Test rpc_count() method - """ - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) mocker.patch.multiple( @@ -552,6 +519,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None: ) freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) rpc = RPC(freqtradebot) trades = rpc._rpc_count() diff --git a/freqtrade/tests/rpc/test_rpc_manager.py b/freqtrade/tests/rpc/test_rpc_manager.py index 5aea98d48..90c693830 100644 --- a/freqtrade/tests/rpc/test_rpc_manager.py +++ b/freqtrade/tests/rpc/test_rpc_manager.py @@ -1,50 +1,31 @@ -""" -Unit test file for rpc/rpc_manager.py -""" +# pragma pylint: disable=missing-docstring, C0103 import logging -from copy import deepcopy from unittest.mock import MagicMock -from freqtrade.rpc.rpc_manager import RPCManager -from freqtrade.tests.conftest import get_patched_freqtradebot, log_has - - -def test_rpc_manager_object() -> None: - """ Test the Arguments object has the mandatory methods """ - assert hasattr(RPCManager, 'send_msg') - assert hasattr(RPCManager, 'cleanup') +from freqtrade.rpc import RPCMessageType, RPCManager +from freqtrade.tests.conftest import log_has, get_patched_freqtradebot def test__init__(mocker, default_conf) -> None: - """ Test __init__() method """ - conf = deepcopy(default_conf) - conf['telegram']['enabled'] = False + default_conf['telegram']['enabled'] = False - rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf)) + rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) assert rpc_manager.registered_modules == [] def test_init_telegram_disabled(mocker, default_conf, caplog) -> None: - """ Test _init() method with Telegram disabled """ caplog.set_level(logging.DEBUG) - - conf = deepcopy(default_conf) - conf['telegram']['enabled'] = False - - rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf)) + default_conf['telegram']['enabled'] = False + rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples) assert rpc_manager.registered_modules == [] def test_init_telegram_enabled(mocker, default_conf, caplog) -> None: - """ - Test _init() method with Telegram enabled - """ caplog.set_level(logging.DEBUG) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) - rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) assert log_has('Enabling rpc.telegram ...', caplog.record_tuples) @@ -54,16 +35,11 @@ def test_init_telegram_enabled(mocker, default_conf, caplog) -> None: def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None: - """ - Test cleanup() method with Telegram disabled - """ caplog.set_level(logging.DEBUG) telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock()) + default_conf['telegram']['enabled'] = False - conf = deepcopy(default_conf) - conf['telegram']['enabled'] = False - - freqtradebot = get_patched_freqtradebot(mocker, conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) rpc_manager.cleanup() @@ -72,9 +48,6 @@ def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None: def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None: - """ - Test cleanup() method with Telegram enabled - """ caplog.set_level(logging.DEBUG) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.cleanup', MagicMock()) @@ -92,32 +65,51 @@ def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None: def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None: - """ - Test send_msg() method with Telegram disabled - """ telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) + default_conf['telegram']['enabled'] = False - conf = deepcopy(default_conf) - conf['telegram']['enabled'] = False - - freqtradebot = get_patched_freqtradebot(mocker, conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) - rpc_manager.send_msg('test') + rpc_manager.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': 'test' + }) - assert log_has('Sending rpc message: test', caplog.record_tuples) + assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog.record_tuples) assert telegram_mock.call_count == 0 def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None: - """ - Test send_msg() method with Telegram disabled - """ telegram_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(freqtradebot) - rpc_manager.send_msg('test') + rpc_manager.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': 'test' + }) - assert log_has('Sending rpc message: test', caplog.record_tuples) + assert log_has("Sending rpc message: {'type': status, 'status': 'test'}", caplog.record_tuples) assert telegram_mock.call_count == 1 + + +def test_init_webhook_disabled(mocker, default_conf, caplog) -> None: + caplog.set_level(logging.DEBUG) + default_conf['telegram']['enabled'] = False + default_conf['webhook'] = {'enabled': False} + rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) + + assert not log_has('Enabling rpc.webhook ...', caplog.record_tuples) + assert rpc_manager.registered_modules == [] + + +def test_init_webhook_enabled(mocker, default_conf, caplog) -> None: + caplog.set_level(logging.DEBUG) + default_conf['telegram']['enabled'] = False + default_conf['webhook'] = {'enabled': True, 'url': "https://DEADBEEF.com"} + rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf)) + + assert log_has('Enabling rpc.webhook ...', caplog.record_tuples) + assert len(rpc_manager.registered_modules) == 1 + assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules] diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 2710328bd..4b2fe4cf5 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -1,28 +1,27 @@ +# pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=protected-access, unused-argument, invalid-name # pragma pylint: disable=too-many-lines, too-many-arguments -""" -Unit test file for rpc/telegram.py -""" - import re -from copy import deepcopy from datetime import datetime from random import randint -from unittest.mock import MagicMock +from unittest.mock import MagicMock, ANY +import arrow +import pytest from telegram import Chat, Message, Update from telegram.error import NetworkError from freqtrade import __version__ from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade +from freqtrade.rpc import RPCMessageType from freqtrade.rpc.telegram import Telegram, authorized_only from freqtrade.state import State from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has, patch_exchange) -from freqtrade.tests.test_freqtradebot import (patch_coinmarketcap, - patch_get_signal) +from freqtrade.tests.test_freqtradebot import patch_get_signal +from freqtrade.tests.conftest import patch_coinmarketcap class DummyCls(Telegram): @@ -52,9 +51,6 @@ class DummyCls(Telegram): def test__init__(default_conf, mocker) -> None: - """ - Test __init__() method - """ mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) @@ -64,7 +60,6 @@ def test__init__(default_conf, mocker) -> None: def test_init(default_conf, mocker, caplog) -> None: - """ Test _init() method """ start_polling = MagicMock() mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling)) @@ -83,9 +78,6 @@ def test_init(default_conf, mocker, caplog) -> None: def test_cleanup(default_conf, mocker) -> None: - """ - Test cleanup() method - """ updater_mock = MagicMock() updater_mock.stop = MagicMock() mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock) @@ -96,10 +88,6 @@ def test_cleanup(default_conf, mocker) -> None: def test_authorized_only(default_conf, mocker, caplog) -> None: - """ - Test authorized_only() method when we are authorized - """ - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker) patch_exchange(mocker, None) @@ -107,9 +95,10 @@ def test_authorized_only(default_conf, mocker, caplog) -> None: update = Update(randint(1, 100)) update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat) - conf = deepcopy(default_conf) - conf['telegram']['enabled'] = False - dummy = DummyCls(FreqtradeBot(conf)) + default_conf['telegram']['enabled'] = False + bot = FreqtradeBot(default_conf) + patch_get_signal(bot, (True, False)) + dummy = DummyCls(bot) dummy.dummy_handler(bot=MagicMock(), update=update) assert dummy.state['called'] is True assert log_has( @@ -127,19 +116,16 @@ def test_authorized_only(default_conf, mocker, caplog) -> None: def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: - """ - Test authorized_only() method when we are unauthorized - """ - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker) patch_exchange(mocker, None) chat = Chat(0xdeadbeef, 0) update = Update(randint(1, 100)) update.message = Message(randint(1, 100), 0, datetime.utcnow(), chat) - conf = deepcopy(default_conf) - conf['telegram']['enabled'] = False - dummy = DummyCls(FreqtradeBot(conf)) + default_conf['telegram']['enabled'] = False + bot = FreqtradeBot(default_conf) + patch_get_signal(bot, (True, False)) + dummy = DummyCls(bot) dummy.dummy_handler(bot=MagicMock(), update=update) assert dummy.state['called'] is False assert not log_has( @@ -157,19 +143,18 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None: def test_authorized_only_exception(default_conf, mocker, caplog) -> None: - """ - Test authorized_only() method when an exception is thrown - """ - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker) patch_exchange(mocker) update = Update(randint(1, 100)) update.message = Message(randint(1, 100), 0, datetime.utcnow(), Chat(0, 0)) - conf = deepcopy(default_conf) - conf['telegram']['enabled'] = False - dummy = DummyCls(FreqtradeBot(conf)) + default_conf['telegram']['enabled'] = False + + bot = FreqtradeBot(default_conf) + patch_get_signal(bot, (True, False)) + dummy = DummyCls(bot) + dummy.dummy_exception(bot=MagicMock(), update=update) assert dummy.state['called'] is False assert not log_has( @@ -187,16 +172,12 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None: def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: - """ - Test _status() method - """ update.message.chat.id = 123 - conf = deepcopy(default_conf) - conf['telegram']['enabled'] = False - conf['telegram']['chat_id'] = 123 + default_conf['telegram']['enabled'] = False + default_conf['telegram']['chat_id'] = 123 - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker) + mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -210,13 +191,26 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', _init=MagicMock(), - _rpc_trade_status=MagicMock(return_value=[1, 2, 3]), + _rpc_trade_status=MagicMock(return_value=[{ + 'trade_id': 1, + 'pair': 'ETH/BTC', + 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', + 'date': arrow.utcnow(), + 'open_rate': 1.099e-05, + 'close_rate': None, + 'current_rate': 1.098e-05, + 'amount': 90.99181074, + 'close_profit': None, + 'current_profit': -0.59, + 'open_order': '(limit buy rem=0.00000000)' + }]), _status_table=status_table, _send_msg=msg_mock ) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - freqtradebot = FreqtradeBot(conf) + freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) # Create some test data @@ -224,7 +218,7 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: freqtradebot.create_trade() telegram._status(bot=MagicMock(), update=update) - assert msg_mock.call_count == 3 + assert msg_mock.call_count == 1 update.message.text = MagicMock() update.message.text.replace = MagicMock(return_value='table 2 3') @@ -233,10 +227,6 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> None: - """ - Test _status() method - """ - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -256,6 +246,8 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) + telegram = Telegram(freqtradebot) freqtradebot.state = State.STOPPED @@ -280,10 +272,6 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) -> None: - """ - Test _status_table() method - """ - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -301,9 +289,10 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) ) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) - conf = deepcopy(default_conf) - conf['stake_amount'] = 15.0 - freqtradebot = FreqtradeBot(conf) + default_conf['stake_amount'] = 15.0 + freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) + telegram = Telegram(freqtradebot) freqtradebot.state = State.STOPPED @@ -334,13 +323,9 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, limit_sell_order, markets, mocker) -> None: - """ - Test _daily() method - """ - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) mocker.patch( - 'freqtrade.fiat_convert.CryptoToFiatConverter._find_price', + 'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0 ) mocker.patch.multiple( @@ -359,6 +344,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) # Create some test data @@ -408,10 +394,6 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee, def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: - """ - Test _daily() method - """ - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -427,6 +409,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) # Try invalid data @@ -447,12 +430,8 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None: def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, limit_buy_order, limit_sell_order, markets, mocker) -> None: - """ - Test _profit() method - """ - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) - mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) + mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -469,6 +448,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) telegram._profit(bot=MagicMock(), update=update) @@ -508,10 +488,6 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee, def test_telegram_balance_handle(default_conf, update, mocker) -> None: - """ - Test _balance() method - """ - mock_balance = { 'BTC': { 'total': 12.0, @@ -536,9 +512,6 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None: } def mock_ticker(symbol, refresh): - """ - Mock Bittrex.get_ticker() response - """ if symbol == 'BTC/USDT': return { 'bid': 10000.00, @@ -552,7 +525,6 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None: 'last': 0.1, } - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value=mock_balance) mocker.patch('freqtrade.exchange.Exchange.get_ticker', side_effect=mock_ticker) @@ -565,6 +537,8 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None: ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) + patch_get_signal(freqtradebot, (True, False)) + telegram = Telegram(freqtradebot) telegram._balance(bot=MagicMock(), update=update) @@ -578,11 +552,7 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None: assert 'BTC: 14.00000000' in result -def test_zero_balance_handle(default_conf, update, mocker) -> None: - """ - Test _balance() method when the Exchange platform returns nothing - """ - patch_get_signal(mocker, (True, False)) +def test_balance_handle_empty_response(default_conf, update, mocker) -> None: mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={}) msg_mock = MagicMock() @@ -593,18 +563,17 @@ def test_zero_balance_handle(default_conf, update, mocker) -> None: ) freqtradebot = get_patched_freqtradebot(mocker, default_conf) + patch_get_signal(freqtradebot, (True, False)) + telegram = Telegram(freqtradebot) telegram._balance(bot=MagicMock(), update=update) result = msg_mock.call_args_list[0][0][0] assert msg_mock.call_count == 1 - assert '`All balances are zero.`' in result + assert 'all balances are zero' in result def test_start_handle(default_conf, update, mocker) -> None: - """ - Test _start() method - """ msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -623,9 +592,6 @@ def test_start_handle(default_conf, update, mocker) -> None: def test_start_handle_already_running(default_conf, update, mocker) -> None: - """ - Test _start() method - """ msg_mock = MagicMock() mocker.patch.multiple( 'freqtrade.rpc.telegram.Telegram', @@ -645,9 +611,6 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None: def test_stop_handle(default_conf, update, mocker) -> None: - """ - Test _stop() method - """ patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( @@ -664,13 +627,10 @@ def test_stop_handle(default_conf, update, mocker) -> None: telegram._stop(bot=MagicMock(), update=update) assert freqtradebot.state == State.STOPPED assert msg_mock.call_count == 1 - assert 'Stopping trader' in msg_mock.call_args_list[0][0][0] + assert 'stopping trader' in msg_mock.call_args_list[0][0][0] def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: - """ - Test _stop() method - """ patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( @@ -691,7 +651,6 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: def test_reload_conf_handle(default_conf, update, mocker) -> None: - """ Test _reload_conf() method """ patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( @@ -708,17 +667,13 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None: telegram._reload_conf(bot=MagicMock(), update=update) assert freqtradebot.state == State.RELOAD_CONF assert msg_mock.call_count == 1 - assert 'Reloading config' in msg_mock.call_args_list[0][0][0] + assert 'reloading config' in msg_mock.call_args_list[0][0][0] def test_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, markets, mocker) -> None: - """ - Test _forcesell() method - """ - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) - mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) + mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) mocker.patch.multiple( @@ -730,6 +685,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee, ) freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) # Create some test data @@ -745,20 +701,26 @@ def test_forcesell_handle(default_conf, update, ticker, fee, telegram._forcesell(bot=MagicMock(), update=update) assert rpc_mock.call_count == 2 - assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0] - assert 'Amount' in rpc_mock.call_args_list[-1][0][0] - assert '0.00001172' in rpc_mock.call_args_list[-1][0][0] - assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0] - assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0] + last_msg = rpc_mock.call_args_list[-1][0][0] + assert { + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'gain': 'profit', + 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', + 'limit': 1.172e-05, + 'amount': 90.99181073703367, + 'open_rate': 1.099e-05, + 'current_rate': 1.172e-05, + 'profit_amount': 6.126e-05, + 'profit_percent': 0.06110514, + 'stake_currency': 'BTC', + 'fiat_currency': 'USD', + } == last_msg def test_forcesell_down_handle(default_conf, update, ticker, fee, ticker_sell_down, markets, mocker) -> None: - """ - Test _forcesell() method - """ - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) @@ -772,6 +734,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, ) freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) # Create some test data @@ -791,19 +754,26 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, telegram._forcesell(bot=MagicMock(), update=update) assert rpc_mock.call_count == 2 - assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0] - assert 'Amount' in rpc_mock.call_args_list[-1][0][0] - assert '0.00001044' in rpc_mock.call_args_list[-1][0][0] - assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0] - assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0] + + last_msg = rpc_mock.call_args_list[-1][0][0] + assert { + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'gain': 'loss', + 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', + 'limit': 1.044e-05, + 'amount': 90.99181073703367, + 'open_rate': 1.099e-05, + 'current_rate': 1.044e-05, + 'profit_amount': -5.492e-05, + 'profit_percent': -0.05478343, + 'stake_currency': 'BTC', + 'fiat_currency': 'USD', + } == last_msg def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None: - """ - Test _forcesell() method - """ - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) @@ -818,6 +788,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker ) freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) # Create some test data @@ -829,17 +800,25 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker telegram._forcesell(bot=MagicMock(), update=update) assert rpc_mock.call_count == 4 - for args in rpc_mock.call_args_list: - assert '0.00001098' in args[0][0] - assert 'loss: -0.59%, -0.00000591 BTC' in args[0][0] - assert '-0.089 USD' in args[0][0] + msg = rpc_mock.call_args_list[0][0][0] + assert { + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'gain': 'loss', + 'market_url': ANY, + 'limit': 1.098e-05, + 'amount': 90.99181073703367, + 'open_rate': 1.099e-05, + 'current_rate': 1.098e-05, + 'profit_amount': -5.91e-06, + 'profit_percent': -0.00589292, + 'stake_currency': 'BTC', + 'fiat_currency': 'USD', + } == msg def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: - """ - Test _forcesell() method - """ - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) msg_mock = MagicMock() @@ -851,6 +830,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) # Trader is not running @@ -866,7 +846,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: update.message.text = '/forcesell' telegram._forcesell(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 - assert 'Invalid argument' in msg_mock.call_args_list[0][0][0] + assert 'invalid argument' in msg_mock.call_args_list[0][0][0] # Invalid argument msg_mock.reset_mock() @@ -874,15 +854,11 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: update.message.text = '/forcesell 123456' telegram._forcesell(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 - assert 'Invalid argument.' in msg_mock.call_args_list[0][0][0] + assert 'invalid argument' in msg_mock.call_args_list[0][0][0] def test_performance_handle(default_conf, update, ticker, fee, limit_buy_order, limit_sell_order, markets, mocker) -> None: - """ - Test _performance() method - """ - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( @@ -899,6 +875,7 @@ def test_performance_handle(default_conf, update, ticker, fee, ) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) # Create some test data @@ -921,10 +898,6 @@ def test_performance_handle(default_conf, update, ticker, fee, def test_performance_handle_invalid(default_conf, update, mocker) -> None: - """ - Test _performance() method - """ - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( @@ -934,6 +907,7 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None: ) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) # Trader is not running @@ -944,10 +918,6 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None: def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> None: - """ - Test _count() method - """ - patch_get_signal(mocker, (True, False)) patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( @@ -964,6 +934,7 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non ) mocker.patch('freqtrade.exchange.Exchange.get_fee', fee) freqtradebot = FreqtradeBot(default_conf) + patch_get_signal(freqtradebot, (True, False)) telegram = Telegram(freqtradebot) freqtradebot.state = State.STOPPED @@ -988,9 +959,6 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non def test_help_handle(default_conf, update, mocker) -> None: - """ - Test _help() method - """ patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( @@ -1008,9 +976,6 @@ def test_help_handle(default_conf, update, mocker) -> None: def test_version_handle(default_conf, update, mocker) -> None: - """ - Test _version() method - """ patch_coinmarketcap(mocker) msg_mock = MagicMock() mocker.patch.multiple( @@ -1026,15 +991,192 @@ def test_version_handle(default_conf, update, mocker) -> None: assert '*Version:* `{}`'.format(__version__) in msg_mock.call_args_list[0][0][0] -def test_send_msg(default_conf, mocker) -> None: - """ - Test send_msg() method - """ +def test_send_msg_buy_notification(default_conf, mocker) -> None: + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(freqtradebot) + telegram.send_msg({ + 'type': RPCMessageType.BUY_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', + 'limit': 1.099e-05, + 'stake_amount': 0.001, + 'stake_amount_fiat': 0.0, + 'stake_currency': 'BTC', + 'fiat_currency': 'USD' + }) + assert msg_mock.call_args[0][0] \ + == '*Bittrex:* Buying [ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n' \ + 'with limit `0.00001099\n' \ + '(0.001000 BTC,0.000 USD)`' + + +def test_send_msg_sell_notification(default_conf, mocker) -> None: + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(freqtradebot) + old_convamount = telegram._fiat_converter.convert_amount + telegram._fiat_converter.convert_amount = lambda a, b, c: -24.812 + telegram.send_msg({ + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': 'Binance', + 'pair': 'KEY/ETH', + 'gain': 'loss', + 'market_url': 'https://www.binance.com/tradeDetail.html?symbol=KEY_ETH', + 'limit': 3.201e-05, + 'amount': 1333.3333333333335, + 'open_rate': 7.5e-05, + 'current_rate': 3.201e-05, + 'profit_amount': -0.05746268, + 'profit_percent': -0.57405275, + 'stake_currency': 'ETH', + 'fiat_currency': 'USD' + }) + assert msg_mock.call_args[0][0] \ + == '*Binance:* Selling [KEY/ETH]' \ + '(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n' \ + '*Limit:* `0.00003201`\n' \ + '*Amount:* `1333.33333333`\n' \ + '*Open Rate:* `0.00007500`\n' \ + '*Current Rate:* `0.00003201`\n' \ + '*Profit:* `-57.41%`` (loss: -0.05746268 ETH`` / -24.812 USD)`' + + msg_mock.reset_mock() + telegram.send_msg({ + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': 'Binance', + 'pair': 'KEY/ETH', + 'gain': 'loss', + 'market_url': 'https://www.binance.com/tradeDetail.html?symbol=KEY_ETH', + 'limit': 3.201e-05, + 'amount': 1333.3333333333335, + 'open_rate': 7.5e-05, + 'current_rate': 3.201e-05, + 'profit_amount': -0.05746268, + 'profit_percent': -0.57405275, + 'stake_currency': 'ETH', + }) + assert msg_mock.call_args[0][0] \ + == '*Binance:* Selling [KEY/ETH]' \ + '(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n' \ + '*Limit:* `0.00003201`\n' \ + '*Amount:* `1333.33333333`\n' \ + '*Open Rate:* `0.00007500`\n' \ + '*Current Rate:* `0.00003201`\n' \ + '*Profit:* `-57.41%`' + # Reset singleton function to avoid random breaks + telegram._fiat_converter.convert_amount = old_convamount + + +def test_send_msg_status_notification(default_conf, mocker) -> None: + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(freqtradebot) + telegram.send_msg({ + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': 'running' + }) + assert msg_mock.call_args[0][0] == '*Status:* `running`' + + +def test_send_msg_unknown_type(default_conf, mocker) -> None: + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(freqtradebot) + with pytest.raises(NotImplementedError, match=r'Unknown message type: None'): + telegram.send_msg({ + 'type': None, + }) + + +def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None: + del default_conf['fiat_display_currency'] + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(freqtradebot) + telegram.send_msg({ + 'type': RPCMessageType.BUY_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', + 'limit': 1.099e-05, + 'stake_amount': 0.001, + 'stake_amount_fiat': 0.0, + 'stake_currency': 'BTC', + 'fiat_currency': None + }) + assert msg_mock.call_args[0][0] \ + == '*Bittrex:* Buying [ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n' \ + 'with limit `0.00001099\n' \ + '(0.001000 BTC)`' + + +def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: + del default_conf['fiat_display_currency'] + msg_mock = MagicMock() + mocker.patch.multiple( + 'freqtrade.rpc.telegram.Telegram', + _init=MagicMock(), + _send_msg=msg_mock + ) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) + telegram = Telegram(freqtradebot) + telegram.send_msg({ + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': 'Binance', + 'pair': 'KEY/ETH', + 'gain': 'loss', + 'market_url': 'https://www.binance.com/tradeDetail.html?symbol=KEY_ETH', + 'limit': 3.201e-05, + 'amount': 1333.3333333333335, + 'open_rate': 7.5e-05, + 'current_rate': 3.201e-05, + 'profit_amount': -0.05746268, + 'profit_percent': -0.57405275, + 'stake_currency': 'ETH', + 'fiat_currency': 'USD' + }) + assert msg_mock.call_args[0][0] \ + == '*Binance:* Selling [KEY/ETH]' \ + '(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n' \ + '*Limit:* `0.00003201`\n' \ + '*Amount:* `1333.33333333`\n' \ + '*Open Rate:* `0.00007500`\n' \ + '*Current Rate:* `0.00003201`\n' \ + '*Profit:* `-57.41%`' + + +def test__send_msg(default_conf, mocker) -> None: patch_coinmarketcap(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) - conf = deepcopy(default_conf) bot = MagicMock() - freqtradebot = get_patched_freqtradebot(mocker, conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) telegram = Telegram(freqtradebot) telegram._config['telegram']['enabled'] = True @@ -1042,16 +1184,12 @@ def test_send_msg(default_conf, mocker) -> None: assert len(bot.method_calls) == 1 -def test_send_msg_network_error(default_conf, mocker, caplog) -> None: - """ - Test send_msg() method - """ +def test__send_msg_network_error(default_conf, mocker, caplog) -> None: patch_coinmarketcap(mocker) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) - conf = deepcopy(default_conf) bot = MagicMock() bot.send_message = MagicMock(side_effect=NetworkError('Oh snap')) - freqtradebot = get_patched_freqtradebot(mocker, conf) + freqtradebot = get_patched_freqtradebot(mocker, default_conf) telegram = Telegram(freqtradebot) telegram._config['telegram']['enabled'] = True diff --git a/freqtrade/tests/rpc/test_rpc_webhook.py b/freqtrade/tests/rpc/test_rpc_webhook.py new file mode 100644 index 000000000..bfe0416b0 --- /dev/null +++ b/freqtrade/tests/rpc/test_rpc_webhook.py @@ -0,0 +1,166 @@ +# pragma pylint: disable=missing-docstring, C0103, protected-access + +from unittest.mock import MagicMock + +import pytest +from requests import RequestException + +from freqtrade.rpc import RPCMessageType +from freqtrade.rpc.webhook import Webhook +from freqtrade.tests.conftest import get_patched_freqtradebot, log_has + + +def get_webhook_dict() -> dict: + return { + "enabled": True, + "url": "https://maker.ifttt.com/trigger/freqtrade_test/with/key/c764udvJ5jfSlswVRukZZ2/", + "webhookbuy": { + "value1": "Buying {pair}", + "value2": "limit {limit:8f}", + "value3": "{stake_amount:8f} {stake_currency}" + }, + "webhooksell": { + "value1": "Selling {pair}", + "value2": "limit {limit:8f}", + "value3": "profit: {profit_amount:8f} {stake_currency}" + }, + "webhookstatus": { + "value1": "Status: {status}", + "value2": "", + "value3": "" + } + } + + +def test__init__(mocker, default_conf): + default_conf['webhook'] = {'enabled': True, 'url': "https://DEADBEEF.com"} + webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) + assert webhook._config == default_conf + + +def test_send_msg(default_conf, mocker): + default_conf["webhook"] = get_webhook_dict() + msg_mock = MagicMock() + mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) + webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) + msg = { + 'type': RPCMessageType.BUY_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'market_url': "http://mockedurl/ETH_BTC", + 'limit': 0.005, + 'stake_amount': 0.8, + 'stake_amount_fiat': 500, + 'stake_currency': 'BTC', + 'fiat_currency': 'EUR' + } + msg_mock = MagicMock() + mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) + webhook.send_msg(msg=msg) + assert msg_mock.call_count == 1 + assert (msg_mock.call_args[0][0]["value1"] == + default_conf["webhook"]["webhookbuy"]["value1"].format(**msg)) + assert (msg_mock.call_args[0][0]["value2"] == + default_conf["webhook"]["webhookbuy"]["value2"].format(**msg)) + assert (msg_mock.call_args[0][0]["value3"] == + default_conf["webhook"]["webhookbuy"]["value3"].format(**msg)) + # Test sell + msg_mock = MagicMock() + mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) + msg = { + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'gain': "profit", + 'market_url': "http://mockedurl/ETH_BTC", + 'limit': 0.005, + 'amount': 0.8, + 'open_rate': 0.004, + 'current_rate': 0.005, + 'profit_amount': 0.001, + 'profit_percent': 0.20, + 'stake_currency': 'BTC', + } + webhook.send_msg(msg=msg) + assert msg_mock.call_count == 1 + assert (msg_mock.call_args[0][0]["value1"] == + default_conf["webhook"]["webhooksell"]["value1"].format(**msg)) + assert (msg_mock.call_args[0][0]["value2"] == + default_conf["webhook"]["webhooksell"]["value2"].format(**msg)) + assert (msg_mock.call_args[0][0]["value3"] == + default_conf["webhook"]["webhooksell"]["value3"].format(**msg)) + + # Test notification + msg = { + 'type': RPCMessageType.STATUS_NOTIFICATION, + 'status': 'Unfilled sell order for BTC cancelled due to timeout' + } + msg_mock = MagicMock() + mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) + webhook.send_msg(msg) + assert msg_mock.call_count == 1 + assert (msg_mock.call_args[0][0]["value1"] == + default_conf["webhook"]["webhookstatus"]["value1"].format(**msg)) + assert (msg_mock.call_args[0][0]["value2"] == + default_conf["webhook"]["webhookstatus"]["value2"].format(**msg)) + assert (msg_mock.call_args[0][0]["value3"] == + default_conf["webhook"]["webhookstatus"]["value3"].format(**msg)) + + +def test_exception_send_msg(default_conf, mocker, caplog): + default_conf["webhook"] = get_webhook_dict() + default_conf["webhook"]["webhookbuy"] = None + + webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) + webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION}) + assert log_has(f"Message type {RPCMessageType.BUY_NOTIFICATION} not configured for webhooks", + caplog.record_tuples) + + default_conf["webhook"] = get_webhook_dict() + default_conf["webhook"]["webhookbuy"]["value1"] = "{DEADBEEF:8f}" + msg_mock = MagicMock() + mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) + webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) + msg = { + 'type': RPCMessageType.BUY_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'market_url': "http://mockedurl/ETH_BTC", + 'limit': 0.005, + 'stake_amount': 0.8, + 'stake_amount_fiat': 500, + 'stake_currency': 'BTC', + 'fiat_currency': 'EUR' + } + webhook.send_msg(msg) + assert log_has("Problem calling Webhook. Please check your webhook configuration. " + "Exception: 'DEADBEEF'", caplog.record_tuples) + + msg_mock = MagicMock() + mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) + msg = { + 'type': 'DEADBEEF', + 'status': 'whatever' + } + with pytest.raises(NotImplementedError): + webhook.send_msg(msg) + + +def test__send_msg(default_conf, mocker, caplog): + default_conf["webhook"] = get_webhook_dict() + webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) + msg = {'value1': 'DEADBEEF', + 'value2': 'ALIVEBEEF', + 'value3': 'FREQTRADE'} + post = MagicMock() + mocker.patch("freqtrade.rpc.webhook.post", post) + webhook._send_msg(msg) + + assert post.call_count == 1 + assert post.call_args[1] == {'data': msg} + assert post.call_args[0] == (default_conf['webhook']['url'], ) + + post = MagicMock(side_effect=RequestException) + mocker.patch("freqtrade.rpc.webhook.post", post) + webhook._send_msg(msg) + assert log_has('Could not call webhook url. Exception: ', caplog.record_tuples) diff --git a/freqtrade/tests/strategy/legacy_strategy.py b/freqtrade/tests/strategy/legacy_strategy.py new file mode 100644 index 000000000..2cd13b791 --- /dev/null +++ b/freqtrade/tests/strategy/legacy_strategy.py @@ -0,0 +1,235 @@ + +# --- Do not remove these libs --- +from freqtrade.strategy.interface import IStrategy +from pandas import DataFrame +# -------------------------------- + +# Add your lib to import here +import talib.abstract as ta +import freqtrade.vendor.qtpylib.indicators as qtpylib +import numpy # noqa + + +# This class is a sample. Feel free to customize it. +class TestStrategyLegacy(IStrategy): + """ + This is a test strategy using the legacy function headers, which will be + removed in a future update. + Please do not use this as a template, but refer to user_data/strategy/TestStrategy.py + for a uptodate version of this template. + + """ + + # Minimal ROI designed for the strategy. + # This attribute will be overridden if the config file contains "minimal_roi" + minimal_roi = { + "40": 0.0, + "30": 0.01, + "20": 0.02, + "0": 0.04 + } + + # Optimal stoploss designed for the strategy + # This attribute will be overridden if the config file contains "stoploss" + stoploss = -0.10 + + # Optimal ticker interval for the strategy + ticker_interval = '5m' + + def populate_indicators(self, dataframe: DataFrame) -> DataFrame: + """ + Adds several different TA indicators to the given DataFrame + + Performance Note: For the best performance be frugal on the number of indicators + you are using. Let uncomment only the indicator you are using in your strategies + or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + """ + + # Momentum Indicator + # ------------------------------------ + + # ADX + dataframe['adx'] = ta.ADX(dataframe) + + """ + # Awesome oscillator + dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) + + # Commodity Channel Index: values Oversold:<-100, Overbought:>100 + dataframe['cci'] = ta.CCI(dataframe) + + # MACD + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + # MFI + dataframe['mfi'] = ta.MFI(dataframe) + + # Minus Directional Indicator / Movement + dataframe['minus_dm'] = ta.MINUS_DM(dataframe) + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # Plus Directional Indicator / Movement + dataframe['plus_dm'] = ta.PLUS_DM(dataframe) + dataframe['plus_di'] = ta.PLUS_DI(dataframe) + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # ROC + dataframe['roc'] = ta.ROC(dataframe) + + # RSI + dataframe['rsi'] = ta.RSI(dataframe) + + # Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy) + rsi = 0.1 * (dataframe['rsi'] - 50) + dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1) + + # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) + dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) + + # Stoch + stoch = ta.STOCH(dataframe) + dataframe['slowd'] = stoch['slowd'] + dataframe['slowk'] = stoch['slowk'] + + # Stoch fast + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + dataframe['fastk'] = stoch_fast['fastk'] + + # Stoch RSI + stoch_rsi = ta.STOCHRSI(dataframe) + dataframe['fastd_rsi'] = stoch_rsi['fastd'] + dataframe['fastk_rsi'] = stoch_rsi['fastk'] + """ + + # Overlap Studies + # ------------------------------------ + + # Bollinger bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + + """ + # EMA - Exponential Moving Average + dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) + dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) + dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) + dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) + + # SAR Parabol + dataframe['sar'] = ta.SAR(dataframe) + + # SMA - Simple Moving Average + dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) + """ + + # TEMA - Triple Exponential Moving Average + dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) + + # Cycle Indicator + # ------------------------------------ + # Hilbert Transform Indicator - SineWave + hilbert = ta.HT_SINE(dataframe) + dataframe['htsine'] = hilbert['sine'] + dataframe['htleadsine'] = hilbert['leadsine'] + + # Pattern Recognition - Bullish candlestick patterns + # ------------------------------------ + """ + # Hammer: values [0, 100] + dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) + # Inverted Hammer: values [0, 100] + dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) + # Dragonfly Doji: values [0, 100] + dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) + # Piercing Line: values [0, 100] + dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] + # Morningstar: values [0, 100] + dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] + # Three White Soldiers: values [0, 100] + dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] + """ + + # Pattern Recognition - Bearish candlestick patterns + # ------------------------------------ + """ + # Hanging Man: values [0, 100] + dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) + # Shooting Star: values [0, 100] + dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) + # Gravestone Doji: values [0, 100] + dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) + # Dark Cloud Cover: values [0, 100] + dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) + # Evening Doji Star: values [0, 100] + dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) + # Evening Star: values [0, 100] + dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) + """ + + # Pattern Recognition - Bullish/Bearish candlestick patterns + # ------------------------------------ + """ + # Three Line Strike: values [0, -100, 100] + dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) + # Spinning Top: values [0, -100, 100] + dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] + # Engulfing: values [0, -100, 100] + dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] + # Harami: values [0, -100, 100] + dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] + # Three Outside Up/Down: values [0, -100, 100] + dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] + # Three Inside Up/Down: values [0, -100, 100] + dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] + """ + + # Chart type + # ------------------------------------ + """ + # Heikinashi stategy + heikinashi = qtpylib.heikinashi(dataframe) + dataframe['ha_open'] = heikinashi['open'] + dataframe['ha_close'] = heikinashi['close'] + dataframe['ha_high'] = heikinashi['high'] + dataframe['ha_low'] = heikinashi['low'] + """ + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + (dataframe['adx'] > 30) & + (dataframe['tema'] <= dataframe['bb_middleband']) & + (dataframe['tema'] > dataframe['tema'].shift(1)) + ), + 'buy'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + (dataframe['adx'] > 70) & + (dataframe['tema'] > dataframe['bb_middleband']) & + (dataframe['tema'] < dataframe['tema'].shift(1)) + ), + 'sell'] = 1 + return dataframe diff --git a/freqtrade/tests/strategy/test_default_strategy.py b/freqtrade/tests/strategy/test_default_strategy.py index 900fc2234..6acfc439f 100644 --- a/freqtrade/tests/strategy/test_default_strategy.py +++ b/freqtrade/tests/strategy/test_default_strategy.py @@ -3,14 +3,14 @@ import json import pytest from pandas import DataFrame -from freqtrade.analyze import Analyze +from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe from freqtrade.strategy.default_strategy import DefaultStrategy @pytest.fixture def result(): with open('freqtrade/tests/testdata/ETH_BTC-1m.json') as data_file: - return Analyze.parse_ticker_dataframe(json.load(data_file)) + return parse_ticker_dataframe(json.load(data_file)) def test_default_strategy_structure(): @@ -23,12 +23,13 @@ def test_default_strategy_structure(): def test_default_strategy(result): - strategy = DefaultStrategy() + strategy = DefaultStrategy({}) + metadata = {'pair': 'ETH/BTC'} assert type(strategy.minimal_roi) is dict assert type(strategy.stoploss) is float assert type(strategy.ticker_interval) is str - indicators = strategy.populate_indicators(result) + indicators = strategy.populate_indicators(result, metadata) assert type(indicators) is DataFrame - assert type(strategy.populate_buy_trend(indicators)) is DataFrame - assert type(strategy.populate_sell_trend(indicators)) is DataFrame + assert type(strategy.populate_buy_trend(indicators, metadata)) is DataFrame + assert type(strategy.populate_sell_trend(indicators, metadata)) is DataFrame diff --git a/freqtrade/tests/strategy/test_interface.py b/freqtrade/tests/strategy/test_interface.py new file mode 100644 index 000000000..2c056870f --- /dev/null +++ b/freqtrade/tests/strategy/test_interface.py @@ -0,0 +1,107 @@ +# pragma pylint: disable=missing-docstring, C0103 + +import logging +from unittest.mock import MagicMock + +import arrow +from pandas import DataFrame + +from freqtrade.arguments import TimeRange +from freqtrade.optimize.__init__ import load_tickerdata_file +from freqtrade.tests.conftest import get_patched_exchange, log_has +from freqtrade.strategy.default_strategy import DefaultStrategy + +# Avoid to reinit the same object again and again +_STRATEGY = DefaultStrategy(config={}) + + +def test_returns_latest_buy_signal(mocker, default_conf): + mocker.patch.object( + _STRATEGY, 'analyze_ticker', + return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}]) + ) + assert _STRATEGY.get_signal('ETH/BTC', '5m', MagicMock()) == (True, False) + + mocker.patch.object( + _STRATEGY, 'analyze_ticker', + return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}]) + ) + assert _STRATEGY.get_signal('ETH/BTC', '5m', MagicMock()) == (False, True) + + +def test_returns_latest_sell_signal(mocker, default_conf): + mocker.patch.object( + _STRATEGY, 'analyze_ticker', + return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}]) + ) + + assert _STRATEGY.get_signal('ETH/BTC', '5m', MagicMock()) == (False, True) + + mocker.patch.object( + _STRATEGY, 'analyze_ticker', + return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}]) + ) + assert _STRATEGY.get_signal('ETH/BTC', '5m', MagicMock()) == (True, False) + + +def test_get_signal_empty(default_conf, mocker, caplog): + assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'], + None) + assert log_has('Empty ticker history for pair foo', caplog.record_tuples) + + +def test_get_signal_exception_valueerror(default_conf, mocker, caplog): + caplog.set_level(logging.INFO) + mocker.patch.object( + _STRATEGY, 'analyze_ticker', + side_effect=ValueError('xyz') + ) + assert (False, False) == _STRATEGY.get_signal('foo', default_conf['ticker_interval'], 1) + assert log_has('Unable to analyze ticker for pair foo: xyz', caplog.record_tuples) + + +def test_get_signal_empty_dataframe(default_conf, mocker, caplog): + caplog.set_level(logging.INFO) + mocker.patch.object( + _STRATEGY, 'analyze_ticker', + return_value=DataFrame([]) + ) + assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], 1) + assert log_has('Empty dataframe for pair xyz', caplog.record_tuples) + + +def test_get_signal_old_dataframe(default_conf, mocker, caplog): + caplog.set_level(logging.INFO) + # default_conf defines a 5m interval. we check interval * 2 + 5m + # this is necessary as the last candle is removed (partial candles) by default + oldtime = arrow.utcnow().shift(minutes=-16) + ticks = DataFrame([{'buy': 1, 'date': oldtime}]) + mocker.patch.object( + _STRATEGY, 'analyze_ticker', + return_value=DataFrame(ticks) + ) + assert (False, False) == _STRATEGY.get_signal('xyz', default_conf['ticker_interval'], 1) + assert log_has( + 'Outdated history for pair xyz. Last tick is 16 minutes old', + caplog.record_tuples + ) + + +def test_get_signal_handles_exceptions(mocker, default_conf): + mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=MagicMock()) + exchange = get_patched_exchange(mocker, default_conf) + mocker.patch.object( + _STRATEGY, 'analyze_ticker', + side_effect=Exception('invalid ticker history ') + ) + assert _STRATEGY.get_signal(exchange, 'ETH/BTC', '5m') == (False, False) + + +def test_tickerdata_to_dataframe(default_conf) -> None: + strategy = DefaultStrategy(default_conf) + + timerange = TimeRange(None, 'line', 0, -100) + tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange) + tickerlist = {'UNITTEST/BTC': tick} + data = strategy.tickerdata_to_dataframe(tickerlist) + assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 72125a244..8135995a7 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -1,9 +1,11 @@ # pragma pylint: disable=missing-docstring, protected-access, C0103 import logging -import os from base64 import urlsafe_b64encode +from os import path +import warnings import pytest +from pandas import DataFrame from freqtrade.strategy import import_strategy from freqtrade.strategy.default_strategy import DefaultStrategy @@ -13,14 +15,15 @@ from freqtrade.strategy.resolver import StrategyResolver def test_import_strategy(caplog): caplog.set_level(logging.DEBUG) + default_config = {} - strategy = DefaultStrategy() + strategy = DefaultStrategy(default_config) strategy.some_method = lambda *args, **kwargs: 42 assert strategy.__module__ == 'freqtrade.strategy.default_strategy' assert strategy.some_method() == 42 - imported_strategy = import_strategy(strategy) + imported_strategy = import_strategy(strategy, default_config) assert dir(strategy) == dir(imported_strategy) @@ -36,19 +39,29 @@ def test_import_strategy(caplog): def test_search_strategy(): - default_location = os.path.join(os.path.dirname( - os.path.realpath(__file__)), '..', '..', 'strategy' + default_config = {} + default_location = path.join(path.dirname( + path.realpath(__file__)), '..', '..', 'strategy' ) assert isinstance( - StrategyResolver._search_strategy(default_location, 'DefaultStrategy'), IStrategy + StrategyResolver._search_strategy( + default_location, + config=default_config, + strategy_name='DefaultStrategy' + ), + IStrategy ) - assert StrategyResolver._search_strategy(default_location, 'NotFoundStrategy') is None + assert StrategyResolver._search_strategy( + default_location, + config=default_config, + strategy_name='NotFoundStrategy' + ) is None def test_load_strategy(result): resolver = StrategyResolver({'strategy': 'TestStrategy'}) - assert hasattr(resolver.strategy, 'populate_indicators') - assert 'adx' in resolver.strategy.populate_indicators(result) + metadata = {'pair': 'ETH/BTC'} + assert 'adx' in resolver.strategy.advise_indicators(result, metadata=metadata) def test_load_strategy_byte64(result): @@ -61,8 +74,8 @@ def test_load_strategy_byte64(result): def test_load_strategy_invalid_directory(result, caplog): resolver = StrategyResolver() - extra_dir = os.path.join('some', 'path') - resolver._load_strategy('TestStrategy', extra_dir) + extra_dir = path.join('some', 'path') + resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir) assert ( 'freqtrade.strategy.resolver', @@ -70,8 +83,7 @@ def test_load_strategy_invalid_directory(result, caplog): 'Path "{}" does not exist'.format(extra_dir), ) in caplog.record_tuples - assert hasattr(resolver.strategy, 'populate_indicators') - assert 'adx' in resolver.strategy.populate_indicators(result) + assert 'adx' in resolver.strategy.advise_indicators(result, {'pair': 'ETH/BTC'}) def test_load_not_found_strategy(): @@ -79,27 +91,30 @@ def test_load_not_found_strategy(): with pytest.raises(ImportError, match=r'Impossible to load Strategy \'NotFoundStrategy\'.' r' This class does not exist or contains Python code errors'): - strategy._load_strategy('NotFoundStrategy') + strategy._load_strategy(strategy_name='NotFoundStrategy', config={}) def test_strategy(result): - resolver = StrategyResolver({'strategy': 'DefaultStrategy'}) + config = {'strategy': 'DefaultStrategy'} - assert hasattr(resolver.strategy, 'minimal_roi') + resolver = StrategyResolver(config) + metadata = {'pair': 'ETH/BTC'} assert resolver.strategy.minimal_roi[0] == 0.04 + assert config["minimal_roi"]['0'] == 0.04 - assert hasattr(resolver.strategy, 'stoploss') assert resolver.strategy.stoploss == -0.10 + assert config['stoploss'] == -0.10 - assert hasattr(resolver.strategy, 'populate_indicators') - assert 'adx' in resolver.strategy.populate_indicators(result) + assert resolver.strategy.ticker_interval == '5m' + assert config['ticker_interval'] == '5m' - assert hasattr(resolver.strategy, 'populate_buy_trend') - dataframe = resolver.strategy.populate_buy_trend(resolver.strategy.populate_indicators(result)) + df_indicators = resolver.strategy.advise_indicators(result, metadata=metadata) + assert 'adx' in df_indicators + + dataframe = resolver.strategy.advise_buy(df_indicators, metadata=metadata) assert 'buy' in dataframe.columns - assert hasattr(resolver.strategy, 'populate_sell_trend') - dataframe = resolver.strategy.populate_sell_trend(resolver.strategy.populate_indicators(result)) + dataframe = resolver.strategy.advise_sell(df_indicators, metadata=metadata) assert 'sell' in dataframe.columns @@ -113,7 +128,6 @@ def test_strategy_override_minimal_roi(caplog): } resolver = StrategyResolver(config) - assert hasattr(resolver.strategy, 'minimal_roi') assert resolver.strategy.minimal_roi[0] == 0.5 assert ('freqtrade.strategy.resolver', logging.INFO, @@ -129,7 +143,6 @@ def test_strategy_override_stoploss(caplog): } resolver = StrategyResolver(config) - assert hasattr(resolver.strategy, 'stoploss') assert resolver.strategy.stoploss == -0.5 assert ('freqtrade.strategy.resolver', logging.INFO, @@ -146,9 +159,64 @@ def test_strategy_override_ticker_interval(caplog): } resolver = StrategyResolver(config) - assert hasattr(resolver.strategy, 'ticker_interval') assert resolver.strategy.ticker_interval == 60 assert ('freqtrade.strategy.resolver', logging.INFO, 'Override strategy \'ticker_interval\' with value in config file: 60.' ) in caplog.record_tuples + + +def test_deprecate_populate_indicators(result): + default_location = path.join(path.dirname(path.realpath(__file__))) + resolver = StrategyResolver({'strategy': 'TestStrategyLegacy', + 'strategy_path': default_location}) + with warnings.catch_warnings(record=True) as w: + # Cause all warnings to always be triggered. + warnings.simplefilter("always") + indicators = resolver.strategy.advise_indicators(result, 'ETH/BTC') + assert len(w) == 1 + assert issubclass(w[-1].category, DeprecationWarning) + assert "deprecated - check out the Sample strategy to see the current function headers!" \ + in str(w[-1].message) + + with warnings.catch_warnings(record=True) as w: + # Cause all warnings to always be triggered. + warnings.simplefilter("always") + resolver.strategy.advise_buy(indicators, 'ETH/BTC') + assert len(w) == 1 + assert issubclass(w[-1].category, DeprecationWarning) + assert "deprecated - check out the Sample strategy to see the current function headers!" \ + in str(w[-1].message) + + with warnings.catch_warnings(record=True) as w: + # Cause all warnings to always be triggered. + warnings.simplefilter("always") + resolver.strategy.advise_sell(indicators, 'ETH_BTC') + assert len(w) == 1 + assert issubclass(w[-1].category, DeprecationWarning) + assert "deprecated - check out the Sample strategy to see the current function headers!" \ + in str(w[-1].message) + + +def test_call_deprecated_function(result, monkeypatch): + default_location = path.join(path.dirname(path.realpath(__file__))) + resolver = StrategyResolver({'strategy': 'TestStrategyLegacy', + 'strategy_path': default_location}) + metadata = {'pair': 'ETH/BTC'} + + # Make sure we are using a legacy function + assert resolver.strategy._populate_fun_len == 2 + assert resolver.strategy._buy_fun_len == 2 + assert resolver.strategy._sell_fun_len == 2 + + indicator_df = resolver.strategy.advise_indicators(result, metadata=metadata) + assert type(indicator_df) is DataFrame + assert 'adx' in indicator_df.columns + + buydf = resolver.strategy.advise_buy(result, metadata=metadata) + assert type(buydf) is DataFrame + assert 'buy' in buydf.columns + + selldf = resolver.strategy.advise_sell(result, metadata=metadata) + assert type(selldf) is DataFrame + assert 'sell' in selldf diff --git a/freqtrade/tests/test_acl_pair.py b/freqtrade/tests/test_acl_pair.py index 094c166b8..535684b22 100644 --- a/freqtrade/tests/test_acl_pair.py +++ b/freqtrade/tests/test_acl_pair.py @@ -11,7 +11,6 @@ import freqtrade.tests.conftest as tt # test tools def whitelist_conf(): config = tt.default_conf() - config['stake_currency'] = 'BTC' config['exchange']['pair_whitelist'] = [ 'ETH/BTC', @@ -20,7 +19,6 @@ def whitelist_conf(): 'SWT/BTC', 'BCC/BTC' ] - config['exchange']['pair_blacklist'] = [ 'BLK/BTC' ] diff --git a/freqtrade/tests/test_analyze.py b/freqtrade/tests/test_analyze.py deleted file mode 100644 index e6108e8f8..000000000 --- a/freqtrade/tests/test_analyze.py +++ /dev/null @@ -1,198 +0,0 @@ -# pragma pylint: disable=missing-docstring, C0103 - -""" -Unit test file for analyse.py -""" - -import datetime -import logging -from unittest.mock import MagicMock - -import arrow -from pandas import DataFrame - -from freqtrade.analyze import Analyze, SignalType -from freqtrade.arguments import TimeRange -from freqtrade.optimize.__init__ import load_tickerdata_file -from freqtrade.tests.conftest import get_patched_exchange, log_has - -# Avoid to reinit the same object again and again -_ANALYZE = Analyze({'strategy': 'DefaultStrategy'}) - - -def test_signaltype_object() -> None: - """ - Test the SignalType object has the mandatory Constants - :return: None - """ - assert hasattr(SignalType, 'BUY') - assert hasattr(SignalType, 'SELL') - - -def test_analyze_object() -> None: - """ - Test the Analyze object has the mandatory methods - :return: None - """ - assert hasattr(Analyze, 'parse_ticker_dataframe') - assert hasattr(Analyze, 'populate_indicators') - assert hasattr(Analyze, 'populate_buy_trend') - assert hasattr(Analyze, 'populate_sell_trend') - assert hasattr(Analyze, 'analyze_ticker') - assert hasattr(Analyze, 'get_signal') - assert hasattr(Analyze, 'should_sell') - assert hasattr(Analyze, 'min_roi_reached') - assert hasattr(Analyze, 'stop_loss_reached') - - -def test_dataframe_correct_length(result): - dataframe = Analyze.parse_ticker_dataframe(result) - assert len(result.index) - 1 == len(dataframe.index) # last partial candle removed - - -def test_dataframe_correct_columns(result): - assert result.columns.tolist() == \ - ['date', 'open', 'high', 'low', 'close', 'volume'] - - -def test_populates_buy_trend(result): - # Load the default strategy for the unit test, because this logic is done in main.py - dataframe = _ANALYZE.populate_buy_trend(_ANALYZE.populate_indicators(result)) - assert 'buy' in dataframe.columns - - -def test_populates_sell_trend(result): - # Load the default strategy for the unit test, because this logic is done in main.py - dataframe = _ANALYZE.populate_sell_trend(_ANALYZE.populate_indicators(result)) - assert 'sell' in dataframe.columns - - -def test_returns_latest_buy_signal(mocker, default_conf): - mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=MagicMock()) - exchange = get_patched_exchange(mocker, default_conf) - mocker.patch.multiple( - 'freqtrade.analyze.Analyze', - analyze_ticker=MagicMock( - return_value=DataFrame([{'buy': 1, 'sell': 0, 'date': arrow.utcnow()}]) - ) - ) - assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (True, False) - - mocker.patch.multiple( - 'freqtrade.analyze.Analyze', - analyze_ticker=MagicMock( - return_value=DataFrame([{'buy': 0, 'sell': 1, 'date': arrow.utcnow()}]) - ) - ) - assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (False, True) - - -def test_returns_latest_sell_signal(mocker, default_conf): - mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=MagicMock()) - exchange = get_patched_exchange(mocker, default_conf) - mocker.patch.multiple( - 'freqtrade.analyze.Analyze', - analyze_ticker=MagicMock( - return_value=DataFrame([{'sell': 1, 'buy': 0, 'date': arrow.utcnow()}]) - ) - ) - - assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (False, True) - - mocker.patch.multiple( - 'freqtrade.analyze.Analyze', - analyze_ticker=MagicMock( - return_value=DataFrame([{'sell': 0, 'buy': 1, 'date': arrow.utcnow()}]) - ) - ) - assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (True, False) - - -def test_get_signal_empty(default_conf, mocker, caplog): - caplog.set_level(logging.INFO) - mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=None) - exchange = get_patched_exchange(mocker, default_conf) - assert (False, False) == _ANALYZE.get_signal(exchange, 'foo', default_conf['ticker_interval']) - assert log_has('Empty ticker history for pair foo', caplog.record_tuples) - - -def test_get_signal_exception_valueerror(default_conf, mocker, caplog): - caplog.set_level(logging.INFO) - mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=1) - exchange = get_patched_exchange(mocker, default_conf) - mocker.patch.multiple( - 'freqtrade.analyze.Analyze', - analyze_ticker=MagicMock( - side_effect=ValueError('xyz') - ) - ) - assert (False, False) == _ANALYZE.get_signal(exchange, 'foo', default_conf['ticker_interval']) - assert log_has('Unable to analyze ticker for pair foo: xyz', caplog.record_tuples) - - -def test_get_signal_empty_dataframe(default_conf, mocker, caplog): - caplog.set_level(logging.INFO) - mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=1) - exchange = get_patched_exchange(mocker, default_conf) - mocker.patch.multiple( - 'freqtrade.analyze.Analyze', - analyze_ticker=MagicMock( - return_value=DataFrame([]) - ) - ) - assert (False, False) == _ANALYZE.get_signal(exchange, 'xyz', default_conf['ticker_interval']) - assert log_has('Empty dataframe for pair xyz', caplog.record_tuples) - - -def test_get_signal_old_dataframe(default_conf, mocker, caplog): - caplog.set_level(logging.INFO) - mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=1) - exchange = get_patched_exchange(mocker, default_conf) - # FIX: The get_signal function has hardcoded 10, which we must inturn hardcode - oldtime = arrow.utcnow() - datetime.timedelta(minutes=11) - ticks = DataFrame([{'buy': 1, 'date': oldtime}]) - mocker.patch.multiple( - 'freqtrade.analyze.Analyze', - analyze_ticker=MagicMock( - return_value=DataFrame(ticks) - ) - ) - assert (False, False) == _ANALYZE.get_signal(exchange, 'xyz', default_conf['ticker_interval']) - assert log_has( - 'Outdated history for pair xyz. Last tick is 11 minutes old', - caplog.record_tuples - ) - - -def test_get_signal_handles_exceptions(mocker, default_conf): - mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=MagicMock()) - exchange = get_patched_exchange(mocker, default_conf) - mocker.patch.multiple( - 'freqtrade.analyze.Analyze', - analyze_ticker=MagicMock( - side_effect=Exception('invalid ticker history ') - ) - ) - - assert _ANALYZE.get_signal(exchange, 'ETH/BTC', '5m') == (False, False) - - -def test_parse_ticker_dataframe(ticker_history): - columns = ['date', 'open', 'high', 'low', 'close', 'volume'] - - # Test file with BV data - dataframe = Analyze.parse_ticker_dataframe(ticker_history) - assert dataframe.columns.tolist() == columns - - -def test_tickerdata_to_dataframe(default_conf) -> None: - """ - Test Analyze.tickerdata_to_dataframe() method - """ - analyze = Analyze(default_conf) - - timerange = TimeRange(None, 'line', 0, -100) - tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange) - tickerlist = {'UNITTEST/BTC': tick} - data = analyze.tickerdata_to_dataframe(tickerlist) - assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 8a41e3379..79bd0254b 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -1,41 +1,24 @@ # pragma pylint: disable=missing-docstring, C0103 -""" -Unit test file for arguments.py -""" - import argparse -import logging import pytest from freqtrade.arguments import Arguments, TimeRange -def test_arguments_object() -> None: - """ - Test the Arguments object has the mandatory methods - :return: None - """ - assert hasattr(Arguments, 'get_parsed_arg') - assert hasattr(Arguments, 'parse_args') - assert hasattr(Arguments, 'parse_timerange') - assert hasattr(Arguments, 'scripts_options') - - # Parse common command-line-arguments. Used for all tools def test_parse_args_none() -> None: arguments = Arguments([], '') assert isinstance(arguments, Arguments) assert isinstance(arguments.parser, argparse.ArgumentParser) - assert isinstance(arguments.parser, argparse.ArgumentParser) def test_parse_args_defaults() -> None: args = Arguments([], '').get_parsed_arg() assert args.config == 'config.json' assert args.dynamic_whitelist is None - assert args.loglevel == logging.INFO + assert args.loglevel == 0 def test_parse_args_config() -> None: @@ -53,10 +36,10 @@ def test_parse_args_db_url() -> None: def test_parse_args_verbose() -> None: args = Arguments(['-v'], '').get_parsed_arg() - assert args.loglevel == logging.DEBUG + assert args.loglevel == 1 args = Arguments(['--verbose'], '').get_parsed_arg() - assert args.loglevel == logging.DEBUG + assert args.loglevel == 1 def test_scripts_options() -> None: @@ -153,7 +136,7 @@ def test_parse_args_backtesting_custom() -> None: call_args = Arguments(args, '').get_parsed_arg() assert call_args.config == 'test_conf.json' assert call_args.live is True - assert call_args.loglevel == logging.INFO + assert call_args.loglevel == 0 assert call_args.subparser == 'backtesting' assert call_args.func is not None assert call_args.ticker_interval == '1m' @@ -170,7 +153,7 @@ def test_parse_args_hyperopt_custom() -> None: call_args = Arguments(args, '').get_parsed_arg() assert call_args.config == 'test_conf.json' assert call_args.epochs == 20 - assert call_args.loglevel == logging.INFO + assert call_args.loglevel == 0 assert call_args.subparser == 'hyperopt' assert call_args.spaces == ['buy'] assert call_args.func is not None diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index e64e1b486..e48553bdf 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -1,76 +1,46 @@ -# pragma pylint: disable=protected-access, invalid-name +# pragma pylint: disable=missing-docstring, protected-access, invalid-name -""" -Unit test file for configuration.py -""" import json from argparse import Namespace -from copy import deepcopy +import logging from unittest.mock import MagicMock import pytest -from jsonschema import ValidationError +from jsonschema import validate, ValidationError +from freqtrade import constants from freqtrade import OperationalException from freqtrade.arguments import Arguments -from freqtrade.configuration import Configuration +from freqtrade.configuration import Configuration, set_loggers from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL from freqtrade.tests.conftest import log_has -def test_configuration_object() -> None: - """ - Test the Constants object has the mandatory Constants - """ - assert hasattr(Configuration, 'load_config') - assert hasattr(Configuration, '_load_config_file') - assert hasattr(Configuration, '_validate_config') - assert hasattr(Configuration, '_load_common_config') - assert hasattr(Configuration, '_load_backtesting_config') - assert hasattr(Configuration, '_load_hyperopt_config') - assert hasattr(Configuration, 'get_config') - - def test_load_config_invalid_pair(default_conf) -> None: - """ - Test the configuration validator with an invalid PAIR format - """ - conf = deepcopy(default_conf) - conf['exchange']['pair_whitelist'].append('ETH-BTC') + default_conf['exchange']['pair_whitelist'].append('ETH-BTC') with pytest.raises(ValidationError, match=r'.*does not match.*'): configuration = Configuration(Namespace()) - configuration._validate_config(conf) + configuration._validate_config(default_conf) def test_load_config_missing_attributes(default_conf) -> None: - """ - Test the configuration validator with a missing attribute - """ - conf = deepcopy(default_conf) - conf.pop('exchange') + default_conf.pop('exchange') with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'): configuration = Configuration(Namespace()) - configuration._validate_config(conf) + configuration._validate_config(default_conf) def test_load_config_incorrect_stake_amount(default_conf) -> None: - """ - Test the configuration validator with a missing attribute - """ - conf = deepcopy(default_conf) - conf['stake_amount'] = 'fake' + default_conf['stake_amount'] = 'fake' with pytest.raises(ValidationError, match=r'.*\'fake\' does not match \'unlimited\'.*'): configuration = Configuration(Namespace()) - configuration._validate_config(conf) + configuration._validate_config(default_conf) def test_load_config_file(default_conf, mocker, caplog) -> None: - """ - Test Configuration._load_config_file() method - """ file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open( read_data=json.dumps(default_conf) )) @@ -84,13 +54,9 @@ def test_load_config_file(default_conf, mocker, caplog) -> None: def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None: - """ - Test Configuration._load_config_file() method - """ - conf = deepcopy(default_conf) - conf['max_open_trades'] = 0 + default_conf['max_open_trades'] = 0 file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(conf) + read_data=json.dumps(default_conf) )) Configuration(Namespace())._load_config_file('somefile') @@ -99,9 +65,6 @@ def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None: def test_load_config_file_exception(mocker) -> None: - """ - Test Configuration._load_config_file() method - """ mocker.patch( 'freqtrade.configuration.open', MagicMock(side_effect=FileNotFoundError('File not found')) @@ -113,9 +76,6 @@ def test_load_config_file_exception(mocker) -> None: def test_load_config(default_conf, mocker) -> None: - """ - Test Configuration.load_config() without any cli params - """ mocker.patch('freqtrade.configuration.open', mocker.mock_open( read_data=json.dumps(default_conf) )) @@ -130,13 +90,9 @@ def test_load_config(default_conf, mocker) -> None: def test_load_config_with_params(default_conf, mocker) -> None: - """ - Test Configuration.load_config() with cli params used - """ mocker.patch('freqtrade.configuration.open', mocker.mock_open( read_data=json.dumps(default_conf) )) - arglist = [ '--dynamic-whitelist', '10', '--strategy', 'TestStrategy', @@ -144,7 +100,6 @@ def test_load_config_with_params(default_conf, mocker) -> None: '--db-url', 'sqlite:///someurl', ] args = Arguments(arglist, '').get_parsed_arg() - configuration = Configuration(args) validated_conf = configuration.load_config() @@ -161,10 +116,10 @@ def test_load_config_with_params(default_conf, mocker) -> None: )) arglist = [ - '--dynamic-whitelist', '10', - '--strategy', 'TestStrategy', - '--strategy-path', '/some/path' - ] + '--dynamic-whitelist', '10', + '--strategy', 'TestStrategy', + '--strategy-path', '/some/path' + ] args = Arguments(arglist, '').get_parsed_arg() configuration = Configuration(args) @@ -192,16 +147,12 @@ def test_load_config_with_params(default_conf, mocker) -> None: def test_load_custom_strategy(default_conf, mocker) -> None: - """ - Test Configuration.load_config() without any cli params - """ - custom_conf = deepcopy(default_conf) - custom_conf.update({ + default_conf.update({ 'strategy': 'CustomStrategy', 'strategy_path': '/tmp/strategies', }) mocker.patch('freqtrade.configuration.open', mocker.mock_open( - read_data=json.dumps(custom_conf) + read_data=json.dumps(default_conf) )) args = Arguments([], '').get_parsed_arg() @@ -213,13 +164,9 @@ def test_load_custom_strategy(default_conf, mocker) -> None: def test_show_info(default_conf, mocker, caplog) -> None: - """ - Test Configuration.show_info() - """ mocker.patch('freqtrade.configuration.open', mocker.mock_open( read_data=json.dumps(default_conf) )) - arglist = [ '--dynamic-whitelist', '10', '--strategy', 'TestStrategy', @@ -236,19 +183,14 @@ def test_show_info(default_conf, mocker, caplog) -> None: '(not applicable with Backtesting and Hyperopt)', caplog.record_tuples ) - assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog.record_tuples) assert log_has('Dry run is enabled', caplog.record_tuples) def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None: - """ - Test setup_configuration() function - """ mocker.patch('freqtrade.configuration.open', mocker.mock_open( read_data=json.dumps(default_conf) )) - arglist = [ '--config', 'config.json', '--strategy', 'DefaultStrategy', @@ -275,8 +217,8 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> assert 'live' not in config assert not log_has('Parameter -l/--live detected ...', caplog.record_tuples) - assert 'realistic_simulation' not in config - assert not log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples) + assert 'position_stacking' not in config + assert not log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples) assert 'refresh_pairs' not in config assert not log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) @@ -286,9 +228,6 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> None: - """ - Test setup_configuration() function - """ mocker.patch('freqtrade.configuration.open', mocker.mock_open( read_data=json.dumps(default_conf) )) @@ -300,7 +239,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non 'backtesting', '--ticker-interval', '1m', '--live', - '--realistic-simulation', + '--enable-position-stacking', + '--disable-max-market-positions', '--refresh-pairs-cached', '--timerange', ':100', '--export', '/bar/foo' @@ -330,9 +270,12 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non assert 'live' in config assert log_has('Parameter -l/--live detected ...', caplog.record_tuples) - assert 'realistic_simulation'in config - assert log_has('Parameter --realistic-simulation detected ...', caplog.record_tuples) - assert log_has('Using max_open_trades: 1 ...', caplog.record_tuples) + assert 'position_stacking'in config + assert log_has('Parameter --enable-position-stacking detected ...', caplog.record_tuples) + + assert 'use_max_market_positions' in config + assert log_has('Parameter --disable-max-market-positions detected ...', caplog.record_tuples) + assert log_has('max_open_trades set to unlimited ...', caplog.record_tuples) assert 'refresh_pairs'in config assert log_has('Parameter -r/--refresh-pairs-cached detected ...', caplog.record_tuples) @@ -350,19 +293,14 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: - """ - Test setup_configuration() function - """ mocker.patch('freqtrade.configuration.open', mocker.mock_open( read_data=json.dumps(default_conf) )) - arglist = [ 'hyperopt', '--epochs', '10', '--spaces', 'all', ] - args = Arguments(arglist, '').get_parsed_arg() configuration = Configuration(args) @@ -379,26 +317,79 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: def test_check_exchange(default_conf) -> None: - """ - Test the configuration validator with a missing attribute - """ - conf = deepcopy(default_conf) configuration = Configuration(Namespace()) # Test a valid exchange - conf.get('exchange').update({'name': 'BITTREX'}) - assert configuration.check_exchange(conf) + default_conf.get('exchange').update({'name': 'BITTREX'}) + assert configuration.check_exchange(default_conf) # Test a valid exchange - conf.get('exchange').update({'name': 'binance'}) - assert configuration.check_exchange(conf) + default_conf.get('exchange').update({'name': 'binance'}) + assert configuration.check_exchange(default_conf) # Test a invalid exchange - conf.get('exchange').update({'name': 'unknown_exchange'}) - configuration.config = conf + default_conf.get('exchange').update({'name': 'unknown_exchange'}) + configuration.config = default_conf with pytest.raises( OperationalException, match=r'.*Exchange "unknown_exchange" not supported.*' ): - configuration.check_exchange(conf) + configuration.check_exchange(default_conf) + + +def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: + mocker.patch('freqtrade.configuration.open', mocker.mock_open( + read_data=json.dumps(default_conf))) + # Prevent setting loggers + mocker.patch('freqtrade.configuration.set_loggers', MagicMock) + arglist = ['-vvv'] + args = Arguments(arglist, '').get_parsed_arg() + + configuration = Configuration(args) + validated_conf = configuration.load_config() + + assert validated_conf.get('verbosity') == 3 + assert log_has('Verbosity set to 3', caplog.record_tuples) + + +def test_set_loggers() -> None: + # Reset Logging to Debug, otherwise this fails randomly as it's set globally + logging.getLogger('requests').setLevel(logging.DEBUG) + logging.getLogger("urllib3").setLevel(logging.DEBUG) + logging.getLogger('ccxt.base.exchange').setLevel(logging.DEBUG) + logging.getLogger('telegram').setLevel(logging.DEBUG) + + previous_value1 = logging.getLogger('requests').level + previous_value2 = logging.getLogger('ccxt.base.exchange').level + previous_value3 = logging.getLogger('telegram').level + + set_loggers() + + value1 = logging.getLogger('requests').level + assert previous_value1 is not value1 + assert value1 is logging.INFO + + value2 = logging.getLogger('ccxt.base.exchange').level + assert previous_value2 is not value2 + assert value2 is logging.INFO + + value3 = logging.getLogger('telegram').level + assert previous_value3 is not value3 + assert value3 is logging.INFO + + set_loggers(log_level=2) + + assert logging.getLogger('requests').level is logging.DEBUG + assert logging.getLogger('ccxt.base.exchange').level is logging.INFO + assert logging.getLogger('telegram').level is logging.INFO + + set_loggers(log_level=3) + + assert logging.getLogger('requests').level is logging.DEBUG + assert logging.getLogger('ccxt.base.exchange').level is logging.DEBUG + assert logging.getLogger('telegram').level is logging.INFO + + +def test_validate_default_conf(default_conf) -> None: + validate(default_conf, constants.CONF_SCHEMA) diff --git a/freqtrade/tests/test_constants.py b/freqtrade/tests/test_constants.py deleted file mode 100644 index 541c6e533..000000000 --- a/freqtrade/tests/test_constants.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -Unit test file for constants.py -""" - -from freqtrade import constants - - -def test_constant_object() -> None: - """ - Test the Constants object has the mandatory Constants - """ - assert hasattr(constants, 'CONF_SCHEMA') - assert hasattr(constants, 'DYNAMIC_WHITELIST') - assert hasattr(constants, 'PROCESS_THROTTLE_SECS') - assert hasattr(constants, 'TICKER_INTERVAL') - assert hasattr(constants, 'HYPEROPT_EPOCH') - assert hasattr(constants, 'RETRY_TIMEOUT') - assert hasattr(constants, 'DEFAULT_STRATEGY') - - -def test_conf_schema() -> None: - """ - Test the CONF_SCHEMA is from the right type - """ - assert isinstance(constants.CONF_SCHEMA, dict) diff --git a/freqtrade/tests/test_dataframe.py b/freqtrade/tests/test_dataframe.py index fd461a503..ce144e118 100644 --- a/freqtrade/tests/test_dataframe.py +++ b/freqtrade/tests/test_dataframe.py @@ -2,33 +2,31 @@ import pandas -from freqtrade.analyze import Analyze from freqtrade.optimize import load_data from freqtrade.strategy.resolver import StrategyResolver _pairs = ['ETH/BTC'] -def load_dataframe_pair(pairs): +def load_dataframe_pair(pairs, strategy): ld = load_data(None, ticker_interval='5m', pairs=pairs) assert isinstance(ld, dict) assert isinstance(pairs[0], str) dataframe = ld[pairs[0]] - analyze = Analyze({'strategy': 'DefaultStrategy'}) - dataframe = analyze.analyze_ticker(dataframe) + dataframe = strategy.analyze_ticker(dataframe, pairs[0]) return dataframe def test_dataframe_load(): - StrategyResolver({'strategy': 'DefaultStrategy'}) - dataframe = load_dataframe_pair(_pairs) + strategy = StrategyResolver({'strategy': 'DefaultStrategy'}).strategy + dataframe = load_dataframe_pair(_pairs, strategy) assert isinstance(dataframe, pandas.core.frame.DataFrame) def test_dataframe_columns_exists(): - StrategyResolver({'strategy': 'DefaultStrategy'}) - dataframe = load_dataframe_pair(_pairs) + strategy = StrategyResolver({'strategy': 'DefaultStrategy'}).strategy + dataframe = load_dataframe_pair(_pairs, strategy) assert 'high' in dataframe.columns assert 'low' in dataframe.columns assert 'close' in dataframe.columns diff --git a/freqtrade/tests/test_fiat_convert.py b/freqtrade/tests/test_fiat_convert.py index 5af85d268..8fd3b66b4 100644 --- a/freqtrade/tests/test_fiat_convert.py +++ b/freqtrade/tests/test_fiat_convert.py @@ -183,6 +183,24 @@ def test_fiat_convert_without_network(mocker): CryptoToFiatConverter._coinmarketcap = cmc_temp +def test_fiat_invalid_response(mocker, caplog): + # Because CryptoToFiatConverter is a Singleton we reset the listings + listmock = MagicMock(return_value="{'novalidjson':DEADBEEFf}") + mocker.patch.multiple( + 'freqtrade.fiat_convert.Market', + listings=listmock, + ) + # with pytest.raises(RequestEsxception): + fiat_convert = CryptoToFiatConverter() + fiat_convert._cryptomap = {} + fiat_convert._load_cryptomap() + + length_cryptomap = len(fiat_convert._cryptomap) + assert length_cryptomap == 0 + assert log_has('Could not load FIAT Cryptocurrency map for the following problem: TypeError', + caplog.record_tuples) + + def test_convert_amount(mocker): patch_coinmarketcap(mocker) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 17bd6aa7c..69f349107 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1,9 +1,6 @@ +# pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments -""" -Unit test file for freqtradebot.py -""" - import logging import re import time @@ -18,9 +15,10 @@ from freqtrade import (DependencyException, OperationalException, TemporaryError, constants) from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade +from freqtrade.rpc import RPCMessageType from freqtrade.state import State -from freqtrade.tests.conftest import (log_has, patch_coinmarketcap, - patch_exchange) +from freqtrade.strategy.interface import SellType, SellCheckTuple +from freqtrade.tests.conftest import log_has, patch_exchange # Functions for recurrent object patching @@ -31,26 +29,21 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: :param config: Config to pass to the bot :return: None """ - mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) patch_exchange(mocker) - patch_coinmarketcap(mocker) return FreqtradeBot(config) -def patch_get_signal(mocker, value=(True, False)) -> None: +def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: """ - - :param mocker: mocker to patch Analyze class - :param value: which value Analyze.get_signal() must return + :param mocker: mocker to patch IStrategy class + :param value: which value IStrategy.get_signal() must return :return: None """ - mocker.patch( - 'freqtrade.freqtradebot.Analyze.get_signal', - side_effect=lambda e, s, t: value - ) + freqtrade.strategy.get_signal = lambda e, s, t: value + freqtrade.exchange.get_ticker_history = lambda p, i: None def patch_RPCManager(mocker) -> MagicMock: @@ -65,40 +58,17 @@ def patch_RPCManager(mocker) -> MagicMock: # Unit tests -def test_freqtradebot_object() -> None: - """ - Test the FreqtradeBot object has the mandatory public methods - """ - assert hasattr(FreqtradeBot, 'worker') - assert hasattr(FreqtradeBot, 'cleanup') - assert hasattr(FreqtradeBot, 'create_trade') - assert hasattr(FreqtradeBot, 'get_target_bid') - assert hasattr(FreqtradeBot, 'process_maybe_execute_buy') - assert hasattr(FreqtradeBot, 'process_maybe_execute_sell') - assert hasattr(FreqtradeBot, 'handle_trade') - assert hasattr(FreqtradeBot, 'check_handle_timedout') - assert hasattr(FreqtradeBot, 'handle_timedout_limit_buy') - assert hasattr(FreqtradeBot, 'handle_timedout_limit_sell') - assert hasattr(FreqtradeBot, 'execute_sell') - def test_freqtradebot(mocker, default_conf) -> None: - """ - Test __init__, _init_modules, update_state, and get_state methods - """ freqtrade = get_patched_freqtradebot(mocker, default_conf) assert freqtrade.state is State.RUNNING - conf = deepcopy(default_conf) - conf.pop('initial_state') - freqtrade = FreqtradeBot(conf) + default_conf.pop('initial_state') + freqtrade = FreqtradeBot(default_conf) assert freqtrade.state is State.STOPPED def test_cleanup(mocker, default_conf, caplog) -> None: - """ - Test clean() method - """ mock_cleanup = MagicMock() mocker.patch('freqtrade.persistence.cleanup', mock_cleanup) @@ -109,9 +79,6 @@ def test_cleanup(mocker, default_conf, caplog) -> None: def test_worker_running(mocker, default_conf, caplog) -> None: - """ - Test worker() method. Test when we start the bot - """ mock_throttle = MagicMock() mocker.patch('freqtrade.freqtradebot.FreqtradeBot._throttle', mock_throttle) @@ -124,9 +91,6 @@ def test_worker_running(mocker, default_conf, caplog) -> None: def test_worker_stopped(mocker, default_conf, caplog) -> None: - """ - Test worker() method. Test when we stop the bot - """ mock_throttle = MagicMock() mocker.patch('freqtrade.freqtradebot.FreqtradeBot._throttle', mock_throttle) mock_sleep = mocker.patch('time.sleep', return_value=None) @@ -141,53 +105,38 @@ def test_worker_stopped(mocker, default_conf, caplog) -> None: def test_throttle(mocker, default_conf, caplog) -> None: - """ - Test _throttle() method - """ - def func(): - """ - Test function to throttle - """ + def throttled_func(): return 42 caplog.set_level(logging.DEBUG) freqtrade = get_patched_freqtradebot(mocker, default_conf) start = time.time() - result = freqtrade._throttle(func, min_secs=0.1) + result = freqtrade._throttle(throttled_func, min_secs=0.1) end = time.time() assert result == 42 assert end - start > 0.1 - assert log_has('Throttling func for 0.10 seconds', caplog.record_tuples) + assert log_has('Throttling throttled_func for 0.10 seconds', caplog.record_tuples) - result = freqtrade._throttle(func, min_secs=-1) + result = freqtrade._throttle(throttled_func, min_secs=-1) assert result == 42 def test_throttle_with_assets(mocker, default_conf) -> None: - """ - Test _throttle() method when the function passed can have parameters - """ - def func(nb_assets=-1): - """ - Test function to throttle - """ + def throttled_func(nb_assets=-1): return nb_assets freqtrade = get_patched_freqtradebot(mocker, default_conf) - result = freqtrade._throttle(func, min_secs=0.1, nb_assets=666) + result = freqtrade._throttle(throttled_func, min_secs=0.1, nb_assets=666) assert result == 666 - result = freqtrade._throttle(func, min_secs=0.1) + result = freqtrade._throttle(throttled_func, min_secs=0.1) assert result == -1 def test_gen_pair_whitelist(mocker, default_conf, tickers) -> None: - """ - Test _gen_pair_whitelist() method - """ freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) @@ -212,17 +161,10 @@ def test_gen_pair_whitelist(mocker, default_conf, tickers) -> None: @pytest.mark.skip(reason="Test not implemented") def test_refresh_whitelist() -> None: - """ - Test _refresh_whitelist() method - """ pass def test_get_trade_stake_amount(default_conf, ticker, limit_buy_order, fee, mocker) -> None: - """ - Test get_trade_stake_amount() method - """ - patch_RPCManager(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -233,7 +175,7 @@ def test_get_trade_stake_amount(default_conf, ticker, limit_buy_order, fee, mock freqtrade = FreqtradeBot(default_conf) result = freqtrade._get_trade_stake_amount() - assert(result == default_conf['stake_amount']) + assert result == default_conf['stake_amount'] def test_get_trade_stake_amount_no_stake_amount(default_conf, @@ -241,17 +183,12 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, limit_buy_order, fee, mocker) -> None: - """ - Test get_trade_stake_amount() method - """ patch_RPCManager(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5) ) - - # test defined stake amount freqtrade = FreqtradeBot(default_conf) with pytest.raises(DependencyException, match=r'.*stake amount.*'): @@ -264,12 +201,7 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, fee, markets, mocker) -> None: - """ - Test get_trade_stake_amount() method - """ - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -285,6 +217,7 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, conf['max_open_trades'] = 2 freqtrade = FreqtradeBot(conf) + patch_get_signal(freqtrade) # no open trades, order amount should be 'balance / max_open_trades' result = freqtrade._get_trade_stake_amount() @@ -310,15 +243,10 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, def test_get_min_pair_stake_amount(mocker, default_conf) -> None: - """ - Test get_trade_stake_amount() method - """ - patch_RPCManager(mocker) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) - mocker.patch('freqtrade.freqtradebot.Analyze.get_stoploss', MagicMock(return_value=-0.05)) freqtrade = FreqtradeBot(default_conf) - + freqtrade.strategy.stoploss = -0.05 # no pair found mocker.patch( 'freqtrade.exchange.Exchange.get_markets', @@ -450,12 +378,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: def test_create_trade(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: - """ - Test create_trade() method - """ - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -468,6 +391,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, markets, mocke # Save state of current whitelist whitelist = deepcopy(default_conf['exchange']['pair_whitelist']) freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) freqtrade.create_trade() trade = Trade.query.first() @@ -488,12 +412,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, markets, mocke def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: - """ - Test create_trade() method - """ - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -504,6 +423,7 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, get_markets=markets ) freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) with pytest.raises(DependencyException, match=r'.*stake amount.*'): freqtrade.create_trade() @@ -511,12 +431,7 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: - """ - Test create_trade() method - """ - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -526,24 +441,18 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, get_fee=fee, get_markets=markets ) - - conf = deepcopy(default_conf) - conf['stake_amount'] = 0.0005 - freqtrade = FreqtradeBot(conf) + default_conf['stake_amount'] = 0.0005 + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) freqtrade.create_trade() rate, amount = buy_mock.call_args[0][1], buy_mock.call_args[0][2] - assert rate * amount >= conf['stake_amount'] + assert rate * amount >= default_conf['stake_amount'] def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: - """ - Test create_trade() method - """ - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -554,9 +463,9 @@ def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_ord get_markets=markets ) - conf = deepcopy(default_conf) - conf['stake_amount'] = 0.000000005 - freqtrade = FreqtradeBot(conf) + default_conf['stake_amount'] = 0.000000005 + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) result = freqtrade.create_trade() assert result is False @@ -564,12 +473,7 @@ def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_ord def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: - """ - Test create_trade() method - """ - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -579,23 +483,18 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order, get_fee=fee, get_markets=markets ) - conf = deepcopy(default_conf) - conf['max_open_trades'] = 0 - conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT + default_conf['max_open_trades'] = 0 + default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT - freqtrade = FreqtradeBot(conf) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) assert freqtrade.create_trade() is False assert freqtrade._get_trade_stake_amount() is None def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: - """ - Test create_trade() method - """ - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -605,10 +504,10 @@ def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, marke get_markets=markets ) - conf = deepcopy(default_conf) - conf['exchange']['pair_whitelist'] = ["ETH/BTC"] - conf['exchange']['pair_blacklist'] = ["ETH/BTC"] - freqtrade = FreqtradeBot(conf) + default_conf['exchange']['pair_whitelist'] = ["ETH/BTC"] + default_conf['exchange']['pair_blacklist'] = ["ETH/BTC"] + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) freqtrade.create_trade() @@ -618,12 +517,7 @@ def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, marke def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: - """ - Test create_trade() method - """ - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -632,11 +526,10 @@ def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, get_fee=fee, get_markets=markets ) - - conf = deepcopy(default_conf) - conf['exchange']['pair_whitelist'] = ["ETH/BTC"] - conf['exchange']['pair_blacklist'] = ["ETH/BTC"] - freqtrade = FreqtradeBot(conf) + default_conf['exchange']['pair_whitelist'] = ["ETH/BTC"] + default_conf['exchange']['pair_blacklist'] = ["ETH/BTC"] + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) freqtrade.create_trade() @@ -645,15 +538,9 @@ def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, def test_create_trade_no_signal(default_conf, fee, mocker) -> None: - """ - Test create_trade() method - """ - conf = deepcopy(default_conf) - conf['dry_run'] = True + default_conf['dry_run'] = True - patch_get_signal(mocker, value=(False, False)) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -661,10 +548,9 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None: get_balance=MagicMock(return_value=20), get_fee=fee, ) - - conf = deepcopy(default_conf) - conf['stake_amount'] = 10 - freqtrade = FreqtradeBot(conf) + default_conf['stake_amount'] = 10 + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade, value=(False, False)) Trade.query = MagicMock() Trade.query.filter = MagicMock() @@ -673,12 +559,7 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None: def test_process_trade_creation(default_conf, ticker, limit_buy_order, markets, fee, mocker, caplog) -> None: - """ - Test the trade creation in _process() method - """ - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker, value={'price_usd': 12345.0}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -689,6 +570,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) trades = Trade.query.filter(Trade.is_open.is_(True)).all() assert not trades @@ -714,12 +596,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> None: - """ - Test _process() method when a RequestException happens - """ - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker, value={'price_usd': 12345.0}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -730,18 +607,15 @@ def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> Non sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None) freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + result = freqtrade._process() assert result is False assert sleep_mock.has_calls() def test_process_operational_exception(default_conf, ticker, markets, mocker) -> None: - """ - Test _process() method when an OperationalException happens - """ - patch_get_signal(mocker) msg_mock = patch_RPCManager(mocker) - patch_coinmarketcap(mocker, value={'price_usd': 12345.0}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -750,22 +624,19 @@ def test_process_operational_exception(default_conf, ticker, markets, mocker) -> buy=MagicMock(side_effect=OperationalException) ) freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + assert freqtrade.state == State.RUNNING result = freqtrade._process() assert result is False assert freqtrade.state == State.STOPPED - assert 'OperationalException' in msg_mock.call_args_list[-1][0][0] + assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]['status'] def test_process_trade_handling( default_conf, ticker, limit_buy_order, markets, fee, mocker) -> None: - """ - Test _process() - """ - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker, value={'price_usd': 12345.0}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -776,6 +647,7 @@ def test_process_trade_handling( get_fee=fee, ) freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) trades = Trade.query.filter(Trade.is_open.is_(True)).all() assert not trades @@ -789,9 +661,6 @@ def test_process_trade_handling( def test_balance_fully_ask_side(mocker, default_conf) -> None: - """ - Test get_target_bid() method - """ default_conf['bid_strategy']['ask_last_balance'] = 0.0 freqtrade = get_patched_freqtradebot(mocker, default_conf) @@ -799,9 +668,6 @@ def test_balance_fully_ask_side(mocker, default_conf) -> None: def test_balance_fully_last_side(mocker, default_conf) -> None: - """ - Test get_target_bid() method - """ default_conf['bid_strategy']['ask_last_balance'] = 1.0 freqtrade = get_patched_freqtradebot(mocker, default_conf) @@ -809,9 +675,6 @@ def test_balance_fully_last_side(mocker, default_conf) -> None: def test_balance_bigger_last_ask(mocker, default_conf) -> None: - """ - Test get_target_bid() method - """ default_conf['bid_strategy']['ask_last_balance'] = 1.0 freqtrade = get_patched_freqtradebot(mocker, default_conf) @@ -819,9 +682,6 @@ def test_balance_bigger_last_ask(mocker, default_conf) -> None: def test_process_maybe_execute_buy(mocker, default_conf) -> None: - """ - Test process_maybe_execute_buy() method - """ freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trade', MagicMock(return_value=True)) @@ -832,9 +692,6 @@ def test_process_maybe_execute_buy(mocker, default_conf) -> None: def test_process_maybe_execute_buy_exception(mocker, default_conf, caplog) -> None: - """ - Test exception on process_maybe_execute_buy() method - """ freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch( @@ -846,9 +703,6 @@ def test_process_maybe_execute_buy_exception(mocker, default_conf, caplog) -> No def test_process_maybe_execute_sell(mocker, default_conf, limit_buy_order, caplog) -> None: - """ - Test process_maybe_execute_sell() method - """ freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) @@ -882,9 +736,6 @@ def test_process_maybe_execute_sell(mocker, default_conf, limit_buy_order, caplo def test_process_maybe_execute_sell_exception(mocker, default_conf, limit_buy_order, caplog) -> None: - """ - Test the exceptions in process_maybe_execute_sell() - """ freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) @@ -911,10 +762,6 @@ def test_process_maybe_execute_sell_exception(mocker, default_conf, def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, markets, mocker) -> None: - """ - Test check_handle() method - """ - patch_get_signal(mocker) patch_RPCManager(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -929,9 +776,8 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, get_fee=fee, get_markets=markets ) - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) - freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) freqtrade.create_trade() @@ -942,7 +788,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, trade.update(limit_buy_order) assert trade.is_open is True - patch_get_signal(mocker, value=(False, True)) + patch_get_signal(freqtrade, value=(False, True)) assert freqtrade.handle_trade(trade) is True assert trade.open_order_id == limit_sell_order['id'] @@ -957,16 +803,9 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: - """ - Test check_handle() method - """ - conf = deepcopy(default_conf) - conf.update({'experimental': {'use_sell_signal': True}}) + default_conf.update({'experimental': {'use_sell_signal': True}}) - patch_get_signal(mocker, value=(True, True)) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -976,7 +815,9 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, get_markets=markets ) - freqtrade = FreqtradeBot(conf) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade, value=(True, True)) + freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False freqtrade.create_trade() @@ -986,7 +827,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, assert nb_trades == 0 # Buy is triggering, so buying ... - patch_get_signal(mocker, value=(True, False)) + patch_get_signal(freqtrade, value=(True, False)) freqtrade.create_trade() trades = Trade.query.all() nb_trades = len(trades) @@ -994,7 +835,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, assert trades[0].is_open is True # Buy and Sell are not triggering, so doing nothing ... - patch_get_signal(mocker, value=(False, False)) + patch_get_signal(freqtrade, value=(False, False)) assert freqtrade.handle_trade(trades[0]) is False trades = Trade.query.all() nb_trades = len(trades) @@ -1002,7 +843,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, assert trades[0].is_open is True # Buy and Sell are triggering, so doing nothing ... - patch_get_signal(mocker, value=(True, True)) + patch_get_signal(freqtrade, value=(True, True)) assert freqtrade.handle_trade(trades[0]) is False trades = Trade.query.all() nb_trades = len(trades) @@ -1010,23 +851,17 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, assert trades[0].is_open is True # Sell is triggering, guess what : we are Selling! - patch_get_signal(mocker, value=(False, True)) + patch_get_signal(freqtrade, value=(False, True)) trades = Trade.query.all() assert freqtrade.handle_trade(trades[0]) is True def test_handle_trade_roi(default_conf, ticker, limit_buy_order, fee, mocker, markets, caplog) -> None: - """ - Test check_handle() method - """ caplog.set_level(logging.DEBUG) - conf = deepcopy(default_conf) - conf.update({'experimental': {'use_sell_signal': True}}) + default_conf.update({'experimental': {'use_sell_signal': True}}) - patch_get_signal(mocker, value=(True, False)) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1036,8 +871,10 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, get_markets=markets ) - mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=True) - freqtrade = FreqtradeBot(conf) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade, value=(True, False)) + freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: True + freqtrade.create_trade() trade = Trade.query.first() @@ -1048,23 +885,16 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, # 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(mocker, value=(False, True)) + patch_get_signal(freqtrade, value=(False, True)) assert freqtrade.handle_trade(trade) assert log_has('Required profit reached. Selling..', caplog.record_tuples) def test_handle_trade_experimental( default_conf, ticker, limit_buy_order, fee, mocker, markets, caplog) -> None: - """ - Test check_handle() method - """ caplog.set_level(logging.DEBUG) - conf = deepcopy(default_conf) - conf.update({'experimental': {'use_sell_signal': True}}) - - patch_get_signal(mocker) + default_conf.update({'experimental': {'use_sell_signal': True}}) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1073,30 +903,26 @@ def test_handle_trade_experimental( get_fee=fee, get_markets=markets ) - mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False) - freqtrade = FreqtradeBot(conf) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False freqtrade.create_trade() trade = Trade.query.first() trade.is_open = True - patch_get_signal(mocker, value=(False, False)) + patch_get_signal(freqtrade, value=(False, False)) assert not freqtrade.handle_trade(trade) - patch_get_signal(mocker, value=(False, True)) + patch_get_signal(freqtrade, value=(False, True)) assert freqtrade.handle_trade(trade) assert log_has('Sell signal received. Selling..', caplog.record_tuples) def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, fee, markets, mocker) -> None: - """ - Test check_handle() method - """ - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1106,6 +932,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, get_markets=markets ) freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) # Create trade and sell it freqtrade.create_trade() @@ -1122,12 +949,8 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, fee, mocker) -> None: - """ - Test check_handle_timedout() method - """ rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock() - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1163,11 +986,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, fe def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker) -> None: - """ - Test check_handle_timedout() method - """ rpc_mock = patch_RPCManager(mocker) - patch_coinmarketcap(mocker) cancel_order_mock = MagicMock() mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -1203,11 +1022,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial, mocker) -> None: - """ - Test check_handle_timedout() method - """ rpc_mock = patch_RPCManager(mocker) - patch_coinmarketcap(mocker) cancel_order_mock = MagicMock() mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -1245,12 +1060,8 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) -> None: - """ - Test check_handle_timedout() method when get_order throw an exception - """ patch_RPCManager(mocker) cancel_order_mock = MagicMock() - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', @@ -1291,11 +1102,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) - def test_handle_timedout_limit_buy(mocker, default_conf) -> None: - """ - Test handle_timedout_limit_buy() method - """ patch_RPCManager(mocker) - patch_coinmarketcap(mocker) cancel_order_mock = MagicMock() mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -1317,12 +1124,8 @@ def test_handle_timedout_limit_buy(mocker, default_conf) -> None: def test_handle_timedout_limit_sell(mocker, default_conf) -> None: - """ - Test handle_timedout_limit_sell() method - """ patch_RPCManager(mocker) cancel_order_mock = MagicMock() - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1343,12 +1146,7 @@ def test_handle_timedout_limit_sell(mocker, default_conf) -> None: def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, mocker) -> None: - """ - Test execute_sell() method with a ticker going UP - """ - patch_get_signal(mocker) rpc_mock = patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1356,8 +1154,8 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc get_fee=fee, get_markets=markets ) - mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) # Create some test data freqtrade.create_trade() @@ -1372,26 +1170,29 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc get_ticker=ticker_sell_up ) - freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid']) + freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) assert rpc_mock.call_count == 2 - assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0] - assert 'Amount' in rpc_mock.call_args_list[-1][0][0] - assert 'Profit' in rpc_mock.call_args_list[-1][0][0] - assert '0.00001172' in rpc_mock.call_args_list[-1][0][0] - assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0] - assert '0.919 USD' in rpc_mock.call_args_list[-1][0][0] + last_msg = rpc_mock.call_args_list[-1][0][0] + assert { + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'gain': 'profit', + 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', + 'limit': 1.172e-05, + 'amount': 90.99181073703367, + 'open_rate': 1.099e-05, + 'current_rate': 1.172e-05, + 'profit_amount': 6.126e-05, + 'profit_percent': 0.06110514, + 'stake_currency': 'BTC', + 'fiat_currency': 'USD', + } == last_msg def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, mocker) -> None: - """ - Test execute_sell() method with a ticker going DOWN - """ - patch_get_signal(mocker) rpc_mock = patch_RPCManager(mocker) - patch_coinmarketcap(mocker) - mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1400,6 +1201,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, get_markets=markets ) freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) # Create some test data freqtrade.create_trade() @@ -1414,25 +1216,31 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, get_ticker=ticker_sell_down ) - freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid']) + freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], + sell_reason=SellType.STOP_LOSS) assert rpc_mock.call_count == 2 - assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0] - assert 'Amount' in rpc_mock.call_args_list[-1][0][0] - assert '0.00001044' in rpc_mock.call_args_list[-1][0][0] - assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0] - assert '-0.824 USD' in rpc_mock.call_args_list[-1][0][0] + last_msg = rpc_mock.call_args_list[-1][0][0] + assert { + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'gain': 'loss', + 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', + 'limit': 1.044e-05, + 'amount': 90.99181073703367, + 'open_rate': 1.099e-05, + 'current_rate': 1.044e-05, + 'profit_amount': -5.492e-05, + 'profit_percent': -0.05478343, + 'stake_currency': 'BTC', + 'fiat_currency': 'USD', + } == last_msg def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, mocker) -> None: - """ - Test execute_sell() method with a ticker going DOWN and with a bot config empty - """ - patch_get_signal(mocker) rpc_mock = patch_RPCManager(mocker) - patch_coinmarketcap(mocker, value={'price_usd': 12345.0}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1441,6 +1249,7 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, get_markets=markets ) freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) # Create some test data freqtrade.create_trade() @@ -1456,25 +1265,28 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, ) freqtrade.config = {} - freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid']) + freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) assert rpc_mock.call_count == 2 - assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0] - assert 'Amount' in rpc_mock.call_args_list[-1][0][0] - assert '0.00001172' in rpc_mock.call_args_list[-1][0][0] - assert '(profit: 6.11%, 0.00006126)' in rpc_mock.call_args_list[-1][0][0] - assert 'USD' not in rpc_mock.call_args_list[-1][0][0] + last_msg = rpc_mock.call_args_list[-1][0][0] + assert { + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'gain': 'profit', + 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', + 'limit': 1.172e-05, + 'amount': 90.99181073703367, + 'open_rate': 1.099e-05, + 'current_rate': 1.172e-05, + 'profit_amount': 6.126e-05, + 'profit_percent': 0.06110514, + } == last_msg def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, mocker) -> None: - """ - Test execute_sell() method with a ticker going DOWN and with a bot config empty - """ - patch_get_signal(mocker) rpc_mock = patch_RPCManager(mocker) - patch_coinmarketcap(mocker, value={'price_usd': 12345.0}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1483,6 +1295,7 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, get_markets=markets ) freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) # Create some test data freqtrade.create_trade() @@ -1498,24 +1311,29 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, ) freqtrade.config = {} - freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid']) + freqtrade.execute_sell(trade=trade, limit=ticker_sell_down()['bid'], + sell_reason=SellType.STOP_LOSS) assert rpc_mock.call_count == 2 - assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0] - assert '0.00001044' in rpc_mock.call_args_list[-1][0][0] - assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0] + last_msg = rpc_mock.call_args_list[-1][0][0] + assert { + 'type': RPCMessageType.SELL_NOTIFICATION, + 'exchange': 'Bittrex', + 'pair': 'ETH/BTC', + 'gain': 'loss', + 'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH', + 'limit': 1.044e-05, + 'amount': 90.99181073703367, + 'open_rate': 1.099e-05, + 'current_rate': 1.044e-05, + 'profit_amount': -5.492e-05, + 'profit_percent': -0.05478343, + } == last_msg def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, fee, markets, mocker) -> None: - """ - Test sell_profit_only feature when enabled - """ - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1528,29 +1346,26 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, get_fee=fee, get_markets=markets ) - conf = deepcopy(default_conf) - conf['experimental'] = { + default_conf['experimental'] = { 'use_sell_signal': True, 'sell_profit_only': True, } - freqtrade = FreqtradeBot(conf) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False + freqtrade.create_trade() trade = Trade.query.first() trade.update(limit_buy_order) - patch_get_signal(mocker, value=(False, True)) + patch_get_signal(freqtrade, value=(False, True)) assert freqtrade.handle_trade(trade) is True + assert trade.sell_reason == SellType.SELL_SIGNAL.value def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, fee, markets, mocker) -> None: - """ - Test sell_profit_only feature when disabled - """ - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1563,28 +1378,24 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, get_fee=fee, get_markets=markets ) - conf = deepcopy(default_conf) - conf['experimental'] = { + default_conf['experimental'] = { 'use_sell_signal': True, 'sell_profit_only': False, } - freqtrade = FreqtradeBot(conf) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False freqtrade.create_trade() trade = Trade.query.first() trade.update(limit_buy_order) - patch_get_signal(mocker, value=(False, True)) + patch_get_signal(freqtrade, value=(False, True)) assert freqtrade.handle_trade(trade) is True + assert trade.sell_reason == SellType.SELL_SIGNAL.value def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, markets, mocker) -> None: - """ - Test sell_profit_only feature when enabled and we have a loss - """ - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.Analyze.stop_loss_reached', return_value=False) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1597,28 +1408,25 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, market get_fee=fee, get_markets=markets ) - conf = deepcopy(default_conf) - conf['experimental'] = { + default_conf['experimental'] = { 'use_sell_signal': True, 'sell_profit_only': True, } - freqtrade = FreqtradeBot(conf) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + freqtrade.strategy.stop_loss_reached = \ + lambda current_rate, trade, current_time, current_profit: SellCheckTuple( + sell_flag=False, sell_type=SellType.NONE) freqtrade.create_trade() trade = Trade.query.first() trade.update(limit_buy_order) - patch_get_signal(mocker, value=(False, True)) + patch_get_signal(freqtrade, value=(False, True)) assert freqtrade.handle_trade(trade) is False def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, markets, mocker) -> None: - """ - Test sell_profit_only feature when enabled and we have a loss - """ - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1631,30 +1439,26 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, marke get_fee=fee, get_markets=markets ) - - conf = deepcopy(default_conf) - conf['experimental'] = { + default_conf['experimental'] = { 'use_sell_signal': True, 'sell_profit_only': False, } - freqtrade = FreqtradeBot(conf) + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False + freqtrade.create_trade() trade = Trade.query.first() trade.update(limit_buy_order) - patch_get_signal(mocker, value=(False, True)) + patch_get_signal(freqtrade, value=(False, True)) assert freqtrade.handle_trade(trade) is True + assert trade.sell_reason == SellType.SELL_SIGNAL.value def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, mocker) -> None: - """ - Test sell_profit_only feature when enabled and we have a loss - """ - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=True) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1667,33 +1471,28 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, m get_fee=fee, get_markets=markets ) - - conf = deepcopy(default_conf) - conf['experimental'] = { + default_conf['experimental'] = { 'ignore_roi_if_buy_signal': True } + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: True - freqtrade = FreqtradeBot(conf) freqtrade.create_trade() trade = Trade.query.first() trade.update(limit_buy_order) - patch_get_signal(mocker, value=(True, True)) + patch_get_signal(freqtrade, value=(True, True)) assert freqtrade.handle_trade(trade) is False # Test if buy-signal is absent (should sell due to roi = true) - patch_get_signal(mocker, value=(False, True)) + patch_get_signal(freqtrade, value=(False, True)) assert freqtrade.handle_trade(trade) is True + assert trade.sell_reason == SellType.ROI.value -def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker) -> None: - """ - Test sell_profit_only feature when enabled and we have a loss - """ - patch_get_signal(mocker) +def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog, mocker) -> None: patch_RPCManager(mocker) - patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1704,12 +1503,13 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker) }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, + get_markets=markets, ) + default_conf['trailing_stop'] = True + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False - conf = deepcopy(default_conf) - conf['trailing_stop'] = True - print(limit_buy_order) - freqtrade = FreqtradeBot(conf) freqtrade.create_trade() trade = Trade.query.first() @@ -1720,17 +1520,13 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker) assert log_has( f'HIT STOP: current price at 0.000001, stop loss is {trade.stop_loss:.6f}, ' f'initial stop loss was at 0.000010, trade opened at 0.000011', caplog.record_tuples) + assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value -def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, caplog, mocker) -> None: - """ - Test sell_profit_only feature when enabled and we have a loss - """ +def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, markets, + caplog, mocker) -> None: buy_price = limit_buy_order['price'] - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1741,12 +1537,13 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, caplog, }), buy=MagicMock(return_value={'id': limit_buy_order['id']}), get_fee=fee, + get_markets=markets, ) - - conf = deepcopy(default_conf) - conf['trailing_stop'] = True - conf['trailing_stop_positive'] = 0.01 - freqtrade = FreqtradeBot(conf) + default_conf['trailing_stop'] = True + default_conf['trailing_stop_positive'] = 0.01 + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False freqtrade.create_trade() trade = Trade.query.first() @@ -1764,7 +1561,8 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, caplog, })) # stop-loss not reached, adjusted stoploss assert freqtrade.handle_trade(trade) is False - assert log_has(f'using positive stop loss mode: 0.01 since we have profit 0.26662643', + assert log_has(f'using positive stop loss mode: 0.01 with offset 0 ' + f'since we have profit 0.2666%', caplog.record_tuples) assert log_has(f'adjusted stop loss', caplog.record_tuples) assert trade.stop_loss == 0.0000138501 @@ -1783,15 +1581,70 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, caplog, f'initial stop loss was at 0.000010, trade opened at 0.000011', caplog.record_tuples) +def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, + caplog, mocker, markets) -> None: + buy_price = limit_buy_order['price'] + patch_RPCManager(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + validate_pairs=MagicMock(), + get_ticker=MagicMock(return_value={ + 'bid': buy_price - 0.000001, + 'ask': buy_price - 0.000001, + 'last': buy_price - 0.000001 + }), + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + get_fee=fee, + get_markets=markets, + ) + + default_conf['trailing_stop'] = True + default_conf['trailing_stop_positive'] = 0.01 + default_conf['trailing_stop_positive_offset'] = 0.011 + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False + freqtrade.create_trade() + + trade = Trade.query.first() + trade.update(limit_buy_order) + caplog.set_level(logging.DEBUG) + # stop-loss not reached + assert freqtrade.handle_trade(trade) is False + + # Raise ticker above buy price + mocker.patch('freqtrade.exchange.Exchange.get_ticker', + MagicMock(return_value={ + 'bid': buy_price + 0.000003, + 'ask': buy_price + 0.000003, + 'last': buy_price + 0.000003 + })) + # stop-loss not reached, adjusted stoploss + assert freqtrade.handle_trade(trade) is False + assert log_has(f'using positive stop loss mode: 0.01 with offset 0.011 ' + f'since we have profit 0.2666%', + caplog.record_tuples) + assert log_has(f'adjusted stop loss', caplog.record_tuples) + assert trade.stop_loss == 0.0000138501 + + mocker.patch('freqtrade.exchange.Exchange.get_ticker', + MagicMock(return_value={ + 'bid': buy_price + 0.000002, + 'ask': buy_price + 0.000002, + 'last': buy_price + 0.000002 + })) + # Lower price again (but still positive) + assert freqtrade.handle_trade(trade) is True + assert log_has( + f'HIT STOP: current price at {buy_price + 0.000002:.6f}, ' + f'stop loss is {trade.stop_loss:.6f}, ' + f'initial stop loss was at 0.000010, trade opened at 0.000011', caplog.record_tuples) + assert trade.sell_reason == SellType.TRAILING_STOP_LOSS.value + + def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, mocker) -> None: - """ - Test sell_profit_only feature when enabled and we have a loss - """ - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) - mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=True) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1804,36 +1657,30 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, get_fee=fee, get_markets=markets ) - - conf = deepcopy(default_conf) - conf['experimental'] = { + default_conf['experimental'] = { 'ignore_roi_if_buy_signal': False } + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: True - freqtrade = FreqtradeBot(conf) freqtrade.create_trade() trade = Trade.query.first() trade.update(limit_buy_order) # Sell due to min_roi_reached - patch_get_signal(mocker, value=(True, True)) + patch_get_signal(freqtrade, value=(True, True)) assert freqtrade.handle_trade(trade) is True # Test if buy-signal is absent - patch_get_signal(mocker, value=(False, True)) + patch_get_signal(freqtrade, value=(False, True)) assert freqtrade.handle_trade(trade) is True + assert trade.sell_reason == SellType.STOP_LOSS.value def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, caplog, mocker): - """ - Test get_real_amount - fee in quote currency - """ - mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) - - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) amount = sum(x['amount'] for x in trades_for_order) trade = Trade( @@ -1844,6 +1691,8 @@ def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, ca open_order_id="123456" ) freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + # Amount is reduced by "fee" assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001) assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' @@ -1852,15 +1701,9 @@ def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, ca def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker): - """ - Test get_real_amount - fee in quote currency - """ - mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) amount = buy_order_fee['amount'] trade = Trade( @@ -1871,6 +1714,8 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker): open_order_id="123456" ) freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + # Amount is reduced by "fee" assert freqtrade.get_real_amount(trade, buy_order_fee) == amount assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' @@ -1879,14 +1724,9 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker): def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, mocker): - """ - Test get_real_amount - fees in Stake currency - """ trades_for_order[0]['fee']['currency'] = 'ETH' - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) amount = sum(x['amount'] for x in trades_for_order) @@ -1898,21 +1738,17 @@ def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, mo open_order_id="123456" ) freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + # Amount does not change assert freqtrade.get_real_amount(trade, buy_order_fee) == amount def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, mocker): - """ - Test get_real_amount - Fees in BNB - """ - trades_for_order[0]['fee']['currency'] = 'BNB' trades_for_order[0]['fee']['cost'] = 0.00094518 - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) amount = sum(x['amount'] for x in trades_for_order) @@ -1924,18 +1760,14 @@ def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, mock open_order_id="123456" ) freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + # Amount does not change assert freqtrade.get_real_amount(trade, buy_order_fee) == amount def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, caplog, mocker): - """ - Test get_real_amount with split trades (multiple trades for this order) - """ - - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order2) amount = float(sum(x['amount'] for x in trades_for_order2)) @@ -1947,6 +1779,8 @@ def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, c open_order_id="123456" ) freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + # Amount is reduced by "fee" assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001) assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' @@ -1955,15 +1789,10 @@ def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, c def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee, caplog, mocker): - """ - Test get_real_amount with split trades (multiple trades for this order) - """ limit_buy_order = deepcopy(buy_order_fee) limit_buy_order['fee'] = {'cost': 0.004, 'currency': 'LTC'} - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[trades_for_order]) @@ -1976,6 +1805,8 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee open_order_id="123456" ) freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + # Amount is reduced by "fee" assert freqtrade.get_real_amount(trade, limit_buy_order) == amount - 0.004 assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, ' @@ -1984,15 +1815,10 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order_fee, mocker): - """ - Test get_real_amount with split trades (multiple trades for this order) - """ limit_buy_order = deepcopy(buy_order_fee) limit_buy_order['fee'] = {'cost': 0.004} - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) amount = float(sum(x['amount'] for x in trades_for_order)) @@ -2004,20 +1830,17 @@ def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order open_order_id="123456" ) freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + # Amount does not change assert freqtrade.get_real_amount(trade, limit_buy_order) == amount def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee, mocker): - """ - Test get_real_amount - fees in Stake currency - """ # Remove "Currency" from fee dict trades_for_order[0]['fee'] = {'cost': 0.008} - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) amount = sum(x['amount'] for x in trades_for_order) @@ -2029,17 +1852,13 @@ def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee, open_order_id="123456" ) freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) # Amount does not change assert freqtrade.get_real_amount(trade, buy_order_fee) == amount def test_get_real_amount_open_trade(default_conf, mocker): - """ - Test get_real_amount condition trade.fee_open == 0 or order['status'] == 'open' - """ - patch_get_signal(mocker) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) amount = 12345 trade = Trade( @@ -2055,4 +1874,5 @@ def test_get_real_amount_open_trade(default_conf, mocker): 'status': 'open', } freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) assert freqtrade.get_real_amount(trade, order) == amount diff --git a/freqtrade/tests/test_indicator_helpers.py b/freqtrade/tests/test_indicator_helpers.py index f3d34ec0b..99d6cd79c 100644 --- a/freqtrade/tests/test_indicator_helpers.py +++ b/freqtrade/tests/test_indicator_helpers.py @@ -1,3 +1,5 @@ +# pragma pylint: disable=missing-docstring + import pandas as pd from freqtrade.indicator_helpers import went_down, went_up diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 20a02eedc..7aae98ebe 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -1,8 +1,5 @@ -""" -Unit test file for main.py -""" +# pragma pylint: disable=missing-docstring -import logging from copy import deepcopy from unittest.mock import MagicMock @@ -11,7 +8,7 @@ import pytest from freqtrade import OperationalException from freqtrade.arguments import Arguments from freqtrade.freqtradebot import FreqtradeBot -from freqtrade.main import main, reconfigure, set_loggers +from freqtrade.main import main, reconfigure from freqtrade.state import State from freqtrade.tests.conftest import log_has, patch_exchange @@ -27,49 +24,24 @@ def test_parse_args_backtesting(mocker) -> None: call_args = backtesting_mock.call_args[0][0] assert call_args.config == 'config.json' assert call_args.live is False - assert call_args.loglevel == 20 + assert call_args.loglevel == 0 assert call_args.subparser == 'backtesting' assert call_args.func is not None assert call_args.ticker_interval is None def test_main_start_hyperopt(mocker) -> None: - """ - Test that main() can start hyperopt - """ hyperopt_mock = mocker.patch('freqtrade.optimize.hyperopt.start', MagicMock()) main(['hyperopt']) assert hyperopt_mock.call_count == 1 call_args = hyperopt_mock.call_args[0][0] assert call_args.config == 'config.json' - assert call_args.loglevel == 20 + assert call_args.loglevel == 0 assert call_args.subparser == 'hyperopt' assert call_args.func is not None -def test_set_loggers() -> None: - """ - Test set_loggers() update the logger level for third-party libraries - """ - previous_value1 = logging.getLogger('requests.packages.urllib3').level - previous_value2 = logging.getLogger('telegram').level - - set_loggers() - - value1 = logging.getLogger('requests.packages.urllib3').level - assert previous_value1 is not value1 - assert value1 is logging.INFO - - value2 = logging.getLogger('telegram').level - assert previous_value2 is not value2 - assert value2 is logging.INFO - - def test_main_fatal_exception(mocker, default_conf, caplog) -> None: - """ - Test main() function - In this test we are skipping the while True loop by throwing an exception. - """ patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', @@ -81,7 +53,6 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None: 'freqtrade.configuration.Configuration._load_config_file', lambda *args, **kwargs: default_conf ) - mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) args = ['-c', 'config.json.example'] @@ -94,10 +65,6 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None: def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None: - """ - Test main() function - In this test we are skipping the while True loop by throwing an exception. - """ patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', @@ -109,7 +76,6 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None: 'freqtrade.configuration.Configuration._load_config_file', lambda *args, **kwargs: default_conf ) - mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) args = ['-c', 'config.json.example'] @@ -122,10 +88,6 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None: def test_main_operational_exception(mocker, default_conf, caplog) -> None: - """ - Test main() function - In this test we are skipping the while True loop by throwing an exception. - """ patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', @@ -137,7 +99,6 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None: 'freqtrade.configuration.Configuration._load_config_file', lambda *args, **kwargs: default_conf ) - mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) args = ['-c', 'config.json.example'] @@ -150,10 +111,6 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None: def test_main_reload_conf(mocker, default_conf, caplog) -> None: - """ - Test main() function - In this test we are skipping the while True loop by throwing an exception. - """ patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', @@ -165,7 +122,6 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None: 'freqtrade.configuration.Configuration._load_config_file', lambda *args, **kwargs: default_conf ) - mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) # Raise exception as side effect to avoid endless loop @@ -181,7 +137,6 @@ def test_main_reload_conf(mocker, default_conf, caplog) -> None: def test_reconfigure(mocker, default_conf) -> None: - """ Test recreate() function """ patch_exchange(mocker) mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', @@ -193,7 +148,6 @@ def test_reconfigure(mocker, default_conf) -> None: 'freqtrade.configuration.Configuration._load_config_file', lambda *args, **kwargs: default_conf ) - mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) freqtrade = FreqtradeBot(default_conf) diff --git a/freqtrade/tests/test_misc.py b/freqtrade/tests/test_misc.py index e2ba40dee..26e0c5ee6 100644 --- a/freqtrade/tests/test_misc.py +++ b/freqtrade/tests/test_misc.py @@ -1,34 +1,23 @@ # pragma pylint: disable=missing-docstring,C0103 -""" -Unit test file for misc.py -""" - import datetime from unittest.mock import MagicMock -from freqtrade.analyze import Analyze +from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe from freqtrade.misc import (common_datearray, datesarray_to_datetimearray, file_dump_json, format_ms_time, shorten_date) from freqtrade.optimize.__init__ import load_tickerdata_file +from freqtrade.strategy.default_strategy import DefaultStrategy def test_shorten_date() -> None: - """ - Test shorten_date() function - :return: None - """ str_data = '1 day, 2 hours, 3 minutes, 4 seconds ago' str_shorten_data = '1 d, 2 h, 3 min, 4 sec ago' assert shorten_date(str_data) == str_shorten_data def test_datesarray_to_datetimearray(ticker_history): - """ - Test datesarray_to_datetimearray() function - :return: None - """ - dataframes = Analyze.parse_ticker_dataframe(ticker_history) + dataframes = parse_ticker_dataframe(ticker_history) dates = datesarray_to_datetimearray(dataframes['date']) assert isinstance(dates[0], datetime.datetime) @@ -43,14 +32,10 @@ def test_datesarray_to_datetimearray(ticker_history): def test_common_datearray(default_conf) -> None: - """ - Test common_datearray() - :return: None - """ - analyze = Analyze(default_conf) + strategy = DefaultStrategy(default_conf) tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') tickerlist = {'UNITTEST/BTC': tick} - dataframes = analyze.tickerdata_to_dataframe(tickerlist) + dataframes = strategy.tickerdata_to_dataframe(tickerlist) dates = common_datearray(dataframes) @@ -60,10 +45,6 @@ def test_common_datearray(default_conf) -> None: def test_file_dump_json(mocker) -> None: - """ - Test file_dump_json() - :return: None - """ file_open = mocker.patch('freqtrade.misc.open', MagicMock()) json_dump = mocker.patch('json.dump', MagicMock()) file_dump_json('somefile', [1, 2, 3]) @@ -77,10 +58,6 @@ def test_file_dump_json(mocker) -> None: def test_format_ms_time() -> None: - """ - test format_ms_time() - :return: None - """ # Date 2018-04-10 18:02:01 date_in_epoch_ms = 1523383321000 date = format_ms_time(date_in_epoch_ms) diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index b24f2dd6c..26932136a 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -1,5 +1,4 @@ # pragma pylint: disable=missing-docstring, C0103 -from copy import deepcopy from unittest.mock import MagicMock import pytest @@ -23,46 +22,40 @@ def test_init_create_session(default_conf): def test_init_custom_db_url(default_conf, mocker): - conf = deepcopy(default_conf) - # Update path to a value other than default, but still in-memory - conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'}) + default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'}) create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) - init(conf) + init(default_conf) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite' def test_init_invalid_db_url(default_conf): - conf = deepcopy(default_conf) - # Update path to a value other than default, but still in-memory - conf.update({'db_url': 'unknown:///some.url'}) + default_conf.update({'db_url': 'unknown:///some.url'}) with pytest.raises(OperationalException, match=r'.*no valid database URL*'): - init(conf) + init(default_conf) def test_init_prod_db(default_conf, mocker): - conf = deepcopy(default_conf) - conf.update({'dry_run': False}) - conf.update({'db_url': constants.DEFAULT_DB_PROD_URL}) + default_conf.update({'dry_run': False}) + default_conf.update({'db_url': constants.DEFAULT_DB_PROD_URL}) create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) - init(conf) + init(default_conf) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite' def test_init_dryrun_db(default_conf, mocker): - conf = deepcopy(default_conf) - conf.update({'dry_run': True}) - conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL}) + default_conf.update({'dry_run': True}) + default_conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL}) create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) - init(conf) + init(default_conf) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite://' @@ -465,10 +458,72 @@ def test_migrate_new(mocker, default_conf, fee, caplog): assert trade.max_rate == 0.0 assert trade.stop_loss == 0.0 assert trade.initial_stop_loss == 0.0 + assert trade.sell_reason is None + assert trade.strategy is None + assert trade.ticker_interval is None assert log_has("trying trades_bak1", caplog.record_tuples) assert log_has("trying trades_bak2", caplog.record_tuples) +def test_migrate_mid_state(mocker, default_conf, fee, caplog): + """ + Test Database migration (starting with new 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_open FLOAT NOT NULL, + fee_close 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, fee_open, fee_close, + open_rate, stake_amount, amount, open_date) + VALUES ('binance', 'ETC/BTC', 1, {fee}, {fee}, + 0.00258580, {stake}, {amount}, + '2019-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.create_engine', lambda *args, **kwargs: engine) + + # Create table using the old format + engine.execute(create_table_old) + engine.execute(insert_table_old) + + # Run init to test migration + init(default_conf) + + 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.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 log_has("trying trades_bak0", caplog.record_tuples) + + def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee): trade = Trade( pair='ETH/BTC', diff --git a/freqtrade/tests/test_state.py b/freqtrade/tests/test_state.py deleted file mode 100644 index 51fa06cc2..000000000 --- a/freqtrade/tests/test_state.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -Unit test file for constants.py -""" - -from freqtrade.state import State - - -def test_state_object() -> None: - """ - Test the State object has the mandatory states - :return: None - """ - assert hasattr(State, 'RUNNING') - assert hasattr(State, 'STOPPED') diff --git a/requirements.txt b/requirements.txt index f87241e32..77501b7a9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,18 +1,18 @@ -ccxt==1.15.13 -SQLAlchemy==1.2.9 +ccxt==1.17.56 +SQLAlchemy==1.2.10 python-telegram-bot==10.1.0 arrow==0.12.1 cachetools==2.1.0 requests==2.19.1 urllib3==1.22 wrapt==1.10.11 -pandas==0.23.1 -scikit-learn==0.19.1 +pandas==0.23.3 +scikit-learn==0.19.2 scipy==1.1.0 jsonschema==2.6.0 -numpy==1.14.5 +numpy==1.15.0 TA-Lib==0.4.17 -pytest==3.6.3 +pytest==3.7.0 pytest-mock==1.10.0 pytest-cov==2.5.1 tabulate==0.8.2 @@ -22,4 +22,4 @@ coinmarketcap==5.0.3 scikit-optimize==0.5.2 # Required for plotting data -#plotly==2.7.0 +#plotly==3.0.0 diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 1cc6b818a..fbb385a3c 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -24,29 +24,33 @@ Example of usage: > python3 scripts/plot_dataframe.py --pair BTC/EUR -d user_data/data/ --indicators1 sma,ema3 --indicators2 fastk,fastd """ +import json import logging import sys -import json -from pathlib import Path from argparse import Namespace +from pathlib import Path from typing import Dict, List, Any import pandas as pd import plotly.graph_objs as go +import pytz + from plotly import tools from plotly.offline import plot import freqtrade.optimize as optimize from freqtrade import persistence -from freqtrade.analyze import Analyze from freqtrade.arguments import Arguments, TimeRange from freqtrade.exchange import Exchange from freqtrade.optimize.backtesting import setup_configuration from freqtrade.persistence import Trade +from freqtrade.strategy.resolver import StrategyResolver logger = logging.getLogger(__name__) _CONF: Dict[str, Any] = {} +timeZone = pytz.UTC + def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFrame: trades: pd.DataFrame = pd.DataFrame() @@ -54,14 +58,18 @@ def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFram persistence.init(_CONF) columns = ["pair", "profit", "opents", "closets", "open_rate", "close_rate", "duration"] + for x in Trade.query.all(): + print("date: {}".format(x.open_date)) + trades = pd.DataFrame([(t.pair, t.calc_profit(), - t.open_date, t.close_date, + t.open_date.replace(tzinfo=timeZone), + t.close_date.replace(tzinfo=timeZone) if t.close_date else None, t.open_rate, t.close_rate, - t.close_date.timestamp() - t.open_date.timestamp()) + t.close_date.timestamp() - t.open_date.timestamp() if t.close_date else None) for t in Trade.query.filter(Trade.pair.is_(pair)).all()], columns=columns) - if args.exportfilename: + elif args.exportfilename: file = Path(args.exportfilename) # must align with columns in backtest.py columns = ["pair", "profit", "opents", "closets", "index", "duration", @@ -97,6 +105,7 @@ def plot_analyzed_dataframe(args: Namespace) -> None: # Load the configuration _CONF.update(setup_configuration(args)) + print(_CONF) # Set the pair to audit pair = args.pair @@ -113,7 +122,7 @@ def plot_analyzed_dataframe(args: Namespace) -> None: # Load the strategy try: - analyze = Analyze(_CONF) + strategy = StrategyResolver(_CONF).strategy exchange = Exchange(_CONF) except AttributeError: logger.critical( @@ -123,7 +132,7 @@ def plot_analyzed_dataframe(args: Namespace) -> None: exit() # Set the ticker to use - tick_interval = analyze.get_ticker_interval() + tick_interval = strategy.ticker_interval # Load pair tickers tickers = {} @@ -136,27 +145,28 @@ def plot_analyzed_dataframe(args: Namespace) -> None: pairs=[pair], ticker_interval=tick_interval, refresh_pairs=_CONF.get('refresh_pairs', False), - timerange=timerange + timerange=timerange, + exchange=Exchange(_CONF) ) # No ticker found, or impossible to download if tickers == {}: exit() - if args.db_url and args.exportfilename: - logger.critical("Can only specify --db-url or --export-filename") # Get trades already made from the DB trades = load_trades(args, pair, timerange) - dataframes = analyze.tickerdata_to_dataframe(tickers) + dataframes = strategy.tickerdata_to_dataframe(tickers) + dataframe = dataframes[pair] - dataframe = analyze.populate_buy_trend(dataframe) - dataframe = analyze.populate_sell_trend(dataframe) + dataframe = strategy.advise_buy(dataframe, {'pair': pair}) + dataframe = strategy.advise_sell(dataframe, {'pair': pair}) if len(dataframe.index) > args.plot_limit: logger.warning('Ticker contained more than %s candles as defined ' 'with --plot-limit, clipping.', args.plot_limit) dataframe = dataframe.tail(args.plot_limit) + trades = trades.loc[trades['opents'] >= dataframe.iloc[0]['date']] fig = generate_graph( pair=pair, @@ -261,7 +271,7 @@ def generate_graph(pair, trades: pd.DataFrame, data: pd.DataFrame, args) -> tool x=data.date, y=data.bb_lowerband, name='BB lower', - line={'color': "transparent"}, + line={'color': 'rgba(255,255,255,0)'}, ) bb_upper = go.Scatter( x=data.date, @@ -269,7 +279,7 @@ def generate_graph(pair, trades: pd.DataFrame, data: pd.DataFrame, args) -> tool name='BB upper', fill="tonexty", fillcolor="rgba(0,176,246,0.2)", - line={'color': "transparent"}, + line={'color': 'rgba(255,255,255,0)'}, ) fig.append_trace(bb_lower, 1, 1) fig.append_trace(bb_upper, 1, 1) diff --git a/scripts/plot_profit.py b/scripts/plot_profit.py index 012446065..9c3468c74 100755 --- a/scripts/plot_profit.py +++ b/scripts/plot_profit.py @@ -26,9 +26,8 @@ import plotly.graph_objs as go from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration -from freqtrade.analyze import Analyze from freqtrade import constants - +from freqtrade.strategy.resolver import StrategyResolver import freqtrade.optimize as optimize import freqtrade.misc as misc @@ -87,7 +86,8 @@ def plot_profit(args: Namespace) -> None: # Init strategy try: - analyze = Analyze({'strategy': config.get('strategy')}) + strategy = StrategyResolver({'strategy': config.get('strategy')}).strategy + except AttributeError: logger.critical( 'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"', @@ -113,7 +113,7 @@ def plot_profit(args: Namespace) -> None: else: filter_pairs = config['exchange']['pair_whitelist'] - tick_interval = analyze.strategy.ticker_interval + tick_interval = strategy.ticker_interval pairs = config['exchange']['pair_whitelist'] if filter_pairs: @@ -127,7 +127,7 @@ def plot_profit(args: Namespace) -> None: refresh_pairs=False, timerange=timerange ) - dataframes = analyze.tickerdata_to_dataframe(tickers) + dataframes = strategy.tickerdata_to_dataframe(tickers) # NOTE: the dataframes are of unequal length, # 'dates' is an merged date array of them all. diff --git a/setup.py b/setup.py index cd0574fa2..8853ef7f8 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ setup(name='freqtrade', license='GPLv3', packages=['freqtrade'], scripts=['bin/freqtrade'], - setup_requires=['pytest-runner'], + setup_requires=['pytest-runner', 'numpy'], tests_require=['pytest', 'pytest-mock', 'pytest-cov'], install_requires=[ 'ccxt', diff --git a/user_data/strategies/test_strategy.py b/user_data/strategies/test_strategy.py index 34f496e38..80c238d92 100644 --- a/user_data/strategies/test_strategy.py +++ b/user_data/strategies/test_strategy.py @@ -12,11 +12,13 @@ import numpy # noqa # This class is a sample. Feel free to customize it. class TestStrategy(IStrategy): + __test__ = False # pytest expects to find tests here because of the name """ This is a test strategy to inspire you. More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md You can: + :return: a Dataframe with all mandatory indicators for the strategies - Rename the class name (Do not forget to update class_name) - Add any methods you want to build your strategy - Add any lib you need to build your strategy @@ -43,13 +45,16 @@ class TestStrategy(IStrategy): # Optimal ticker interval for the strategy ticker_interval = '5m' - def populate_indicators(self, dataframe: DataFrame) -> DataFrame: + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Adds several different TA indicators to the given DataFrame Performance Note: For the best performance be frugal on the number of indicators you are using. Let uncomment only the indicator you are using in your strategies or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() + :param metadata: Additional information, like the currently traded pair + :return: a Dataframe with all mandatory indicators for the strategies """ # Momentum Indicator @@ -210,10 +215,11 @@ class TestStrategy(IStrategy): return dataframe - def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the buy signal for the given dataframe - :param dataframe: DataFrame + :param dataframe: DataFrame populated with indicators + :param metadata: Additional information, like the currently traded pair :return: DataFrame with buy column """ dataframe.loc[ @@ -226,10 +232,11 @@ class TestStrategy(IStrategy): return dataframe - def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the sell signal for the given dataframe - :param dataframe: DataFrame + :param dataframe: DataFrame populated with indicators + :param metadata: Additional information, like the currently traded pair :return: DataFrame with buy column """ dataframe.loc[