Merge branch 'develop' into ujson-loader
This commit is contained in:
		| @@ -5,6 +5,7 @@ RUN apt-get update && apt-get -y install curl build-essential && apt-get clean | ||||
| RUN curl -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz | \ | ||||
|   tar xzvf - && \ | ||||
|   cd ta-lib && \ | ||||
|   sed -i "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h && \ | ||||
|   ./configure && make && make install && \ | ||||
|   cd .. && rm -rf ta-lib | ||||
| ENV LD_LIBRARY_PATH /usr/local/lib | ||||
| @@ -15,7 +16,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/ | ||||
|   | ||||
							
								
								
									
										11
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								README.md
									
									
									
									
									
								
							| @@ -24,7 +24,7 @@ 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/) | ||||
| - [X] [Binance](https://www.binance.com/) ([*Note for binance users](#a-note-on-binance)) | ||||
| - [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ | ||||
|  | ||||
| ## Features | ||||
| @@ -50,6 +50,7 @@ hesitate to read the source code and understand the mechanism of this bot. | ||||
|   - [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) | ||||
| @@ -61,6 +62,7 @@ hesitate to read the source code and understand the mechanism of this bot. | ||||
| - [Requirements](#requirements) | ||||
|   - [Min hardware required](#min-hardware-required) | ||||
|   - [Software requirements](#software-requirements) | ||||
|   | ||||
|  | ||||
| ## Quick start | ||||
|  | ||||
| @@ -150,6 +152,13 @@ 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. | ||||
| - `feat/*` - These are feature branches, which are beeing worked on heavily. Please don't use these unless you want to test a specific feature. | ||||
|  | ||||
|  | ||||
| ## A note on Binance | ||||
|  | ||||
| For Binance, please add `"BNB/<STAKE>"` to your blacklist to avoid issues. | ||||
| Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB order unsellable as the expected amount is not there anymore. | ||||
|  | ||||
| ## Support | ||||
|  | ||||
|   | ||||
| @@ -17,6 +17,7 @@ | ||||
|         "name": "bittrex", | ||||
|         "key": "your_exchange_key", | ||||
|         "secret": "your_exchange_secret", | ||||
|         "ccxt_rate_limit": true, | ||||
|         "pair_whitelist": [ | ||||
|             "ETH/BTC", | ||||
|             "LTC/BTC", | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -151,7 +151,7 @@ cp freqtrade/tests/testdata/pairs.json user_data/data/binance | ||||
| Then run: | ||||
|  | ||||
| ```bash | ||||
| python scripts/download_backtest_data --exchange binance | ||||
| python scripts/download_backtest_data.py --exchange binance | ||||
| ``` | ||||
|  | ||||
| This will download ticker data for all the currency pairs you defined in `pairs.json`. | ||||
| @@ -238,6 +238,31 @@ On the other hand, if you set a too high `minimal_roi` like `"0":  0.55` | ||||
| profit. Hence, keep in mind that your performance is a mix of your  | ||||
| strategies, your configuration, and the crypto-currency you have set up. | ||||
|  | ||||
| ## Backtesting multiple strategies | ||||
|  | ||||
| To backtest multiple strategies, a list of Strategies can be provided. | ||||
|  | ||||
| This is limited to 1 ticker-interval per run, however, data is only loaded once from disk so if you have multiple  | ||||
| strategies you'd like to compare, this should give a nice runtime boost. | ||||
|  | ||||
| All listed Strategies need to be in the same folder. | ||||
|  | ||||
| ``` bash | ||||
| freqtrade backtesting --timerange 20180401-20180410 --ticker-interval 5m --strategy-list Strategy001 Strategy002 --export trades  | ||||
| ``` | ||||
|  | ||||
| This will save the results to `user_data/backtest_data/backtest-result-<strategy>.json`, injecting the strategy-name into the target filename. | ||||
| There will be an additional table comparing win/losses of the different strategies (identical to the "Total" row in the first table). | ||||
| Detailed output for all strategies one after the other will be available, so make sure to scroll up. | ||||
|  | ||||
| ``` | ||||
| =================================================== Strategy Summary ==================================================== | ||||
| | Strategy   |   buy count |   avg profit % |   cum profit % |   total profit ETH | avg duration    |   profit |   loss | | ||||
| |:-----------|------------:|---------------:|---------------:|-------------------:|:----------------|---------:|-------:| | ||||
| | Strategy1  |          19 |          -0.76 |         -14.39 |        -0.01440287 | 15:48:00        |       15 |      4 | | ||||
| | Strategy2  |           6 |          -2.73 |         -16.40 |        -0.01641299 | 1 day, 14:12:00 |        3 |      3 | | ||||
| ``` | ||||
|  | ||||
| ## Next step | ||||
|  | ||||
| Great, your strategy is profitable. What if the bot can give your the | ||||
|   | ||||
| @@ -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). | ||||
|   | ||||
| @@ -1,13 +1,15 @@ | ||||
| # Bot usage | ||||
| This page explains the difference parameters of the bot and how to run  | ||||
| it. | ||||
|  | ||||
| This page explains the difference parameters of the bot and how to run it. | ||||
|  | ||||
| ## Table of Contents | ||||
|  | ||||
| - [Bot commands](#bot-commands) | ||||
| - [Backtesting commands](#backtesting-commands) | ||||
| - [Hyperopt commands](#hyperopt-commands) | ||||
|  | ||||
| ## Bot commands | ||||
|  | ||||
| ``` | ||||
| usage: freqtrade [-h] [-v] [--version] [-c PATH] [-d PATH] [-s NAME] | ||||
|                  [--strategy-path PATH] [--dynamic-whitelist [INT]] | ||||
| @@ -41,6 +43,7 @@ optional arguments: | ||||
| ``` | ||||
|  | ||||
| ### How to use a different config file? | ||||
|  | ||||
| The bot allows you to select which config file you want to use. Per  | ||||
| default, the bot will load the file `./config.json` | ||||
|  | ||||
| @@ -49,6 +52,7 @@ python3 ./freqtrade/main.py -c path/far/far/away/config.json | ||||
| ``` | ||||
|  | ||||
| ### How to use --strategy? | ||||
|  | ||||
| This parameter will allow you to load your custom strategy class. | ||||
| Per default without `--strategy` or `-s` the bot will load the | ||||
| `DefaultStrategy` included with the bot (`freqtrade/strategy/default_strategy.py`). | ||||
| @@ -60,6 +64,7 @@ To load a strategy, simply pass the class name (e.g.: `CustomStrategy`) in this | ||||
| **Example:**   | ||||
| In `user_data/strategies` you have a file `my_awesome_strategy.py` which has | ||||
| a strategy class called `AwesomeStrategy` to load it: | ||||
|  | ||||
| ```bash | ||||
| python3 ./freqtrade/main.py --strategy AwesomeStrategy | ||||
| ``` | ||||
| @@ -70,6 +75,7 @@ message the reason (File not found, or errors in your code). | ||||
| Learn more about strategy file in [optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md). | ||||
|  | ||||
| ### How to use --strategy-path? | ||||
|  | ||||
| This parameter allows you to add an additional strategy lookup path, which gets | ||||
| checked before the default locations (The passed path must be a folder!): | ||||
| ```bash | ||||
| @@ -77,21 +83,25 @@ python3 ./freqtrade/main.py --strategy AwesomeStrategy --strategy-path /some/fol | ||||
| ``` | ||||
|  | ||||
| #### How to install a strategy? | ||||
|  | ||||
| This is very simple. Copy paste your strategy file into the folder  | ||||
| `user_data/strategies` or use `--strategy-path`. And voila, the bot is ready to use it. | ||||
|  | ||||
| ### How to use --dynamic-whitelist? | ||||
|  | ||||
| Per default `--dynamic-whitelist` will retrieve the 20 currencies based  | ||||
| on BaseVolume. This value can be changed when you run the script. | ||||
|  | ||||
| **By Default**   | ||||
| Get the 20 currencies based on BaseVolume.   | ||||
|  | ||||
| ```bash | ||||
| python3 ./freqtrade/main.py --dynamic-whitelist | ||||
| ``` | ||||
|  | ||||
| **Customize the number of currencies to retrieve**   | ||||
| Get the 30 currencies based on BaseVolume.   | ||||
|  | ||||
| ```bash | ||||
| python3 ./freqtrade/main.py --dynamic-whitelist 30 | ||||
| ``` | ||||
| @@ -102,6 +112,7 @@ negative value (e.g -2), `--dynamic-whitelist` will use the default | ||||
| value (20). | ||||
|  | ||||
| ### How to use --db-url? | ||||
|  | ||||
| When you run the bot in Dry-run mode, per default no transactions are  | ||||
| stored in a database. If you want to store your bot actions in a DB  | ||||
| using `--db-url`. This can also be used to specify a custom database | ||||
| @@ -111,14 +122,14 @@ in production mode. Example command: | ||||
| python3 ./freqtrade/main.py -c config.json --db-url sqlite:///tradesv3.dry_run.sqlite | ||||
| ``` | ||||
|  | ||||
|  | ||||
| ## Backtesting commands | ||||
|  | ||||
| Backtesting also uses the config specified via `-c/--config`. | ||||
|  | ||||
| ``` | ||||
| usage: main.py backtesting [-h] [-i TICKER_INTERVAL] [--eps] [--dmmp] | ||||
| usage: freqtrade backtesting [-h] [-i TICKER_INTERVAL] [--eps] [--dmmp] | ||||
|                              [--timerange TIMERANGE] [-l] [-r] | ||||
|                              [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] | ||||
|                              [--export EXPORT] [--export-filename PATH] | ||||
|  | ||||
| optional arguments: | ||||
| @@ -139,6 +150,13 @@ optional arguments: | ||||
|                         refresh the pairs files in tests/testdata with the | ||||
|                         latest data from the exchange. Use it if you want to | ||||
|                         run your backtesting with up-to-date data. | ||||
|   --strategy-list STRATEGY_LIST [STRATEGY_LIST ...] | ||||
|                         Provide a commaseparated list of strategies to | ||||
|                         backtest Please note that ticker-interval needs to be | ||||
|                         set either in config or via command line. When using | ||||
|                         this together with --export trades, the strategy-name | ||||
|                         is injected into the filename (so backtest-data.json | ||||
|                         becomes backtest-data-DefaultStrategy.json | ||||
|   --export EXPORT       export backtest results, argument are: trades Example | ||||
|                         --export=trades | ||||
|   --export-filename PATH | ||||
| @@ -151,6 +169,7 @@ optional arguments: | ||||
| ``` | ||||
|  | ||||
| ### How to use --refresh-pairs-cached parameter? | ||||
|  | ||||
| The first time your run Backtesting, it will take the pairs you have  | ||||
| set in your config file and download data from Bittrex.  | ||||
|  | ||||
| @@ -162,7 +181,6 @@ to come back to the previous version.** | ||||
| To test your strategy with latest data, we recommend continuing using  | ||||
| the parameter `-l` or `--live`. | ||||
|  | ||||
|  | ||||
| ## Hyperopt commands | ||||
|  | ||||
| To optimize your strategy, you can use hyperopt parameter hyperoptimization | ||||
| @@ -194,10 +212,11 @@ optional arguments: | ||||
| ``` | ||||
|  | ||||
| ## A parameter missing in the configuration? | ||||
|  | ||||
| All parameters for `main.py`, `backtesting`, `hyperopt` are referenced | ||||
| in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L84) | ||||
|  | ||||
| ## Next step | ||||
| The optimal strategy of the bot will change with time depending of the | ||||
| market trends. The next step is to  | ||||
|  | ||||
| The optimal strategy of the bot will change with time depending of the market trends. The next step is to  | ||||
| [optimize your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.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. | ||||
| @@ -196,6 +197,33 @@ you run it in production mode. | ||||
| ``` | ||||
| If you have not your Bittrex API key yet, [see our tutorial](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md). | ||||
|  | ||||
|  | ||||
| ### Embedding Strategies | ||||
|  | ||||
| FreqTrade provides you with with an easy way to embed the strategy into your configuration file.  | ||||
| This is done by utilizing BASE64 encoding and providing this string at the strategy configuration field, | ||||
| in your chosen config file. | ||||
|  | ||||
| ##### Encoding a string as BASE64 | ||||
|  | ||||
| This is a quick example, how to generate the BASE64 string in python | ||||
|  | ||||
| ```python | ||||
| from base64 import urlsafe_b64encode | ||||
|  | ||||
| with open(file, 'r') as f: | ||||
|     content = f.read() | ||||
| content = urlsafe_b64encode(content.encode('utf-8')) | ||||
| ``` | ||||
|  | ||||
| The variable 'content', will contain the strategy file in a BASE64 encoded form. Which can now be set in your configurations file as following | ||||
|  | ||||
| ```json | ||||
| "strategy": "NameOfStrategy:BASE64String" | ||||
| ``` | ||||
|  | ||||
| Please ensure that 'NameOfStrategy' is identical to the strategy name! | ||||
|  | ||||
| ## Next step | ||||
|  | ||||
| Now you have configured your config.json, the next step is to [start your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md). | ||||
|   | ||||
| @@ -33,3 +33,4 @@ Pull-request. Do not hesitate to reach us on | ||||
| 	- [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)) | ||||
|   | ||||
| @@ -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 | ||||
| @@ -261,6 +267,7 @@ Official webpage: https://mrjbq7.github.io/ta-lib/install.html | ||||
| wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz | ||||
| tar xvzf ta-lib-0.4.0-src.tar.gz | ||||
| cd ta-lib | ||||
| sed -i "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h | ||||
| ./configure --prefix=/usr | ||||
| make | ||||
| make install | ||||
|   | ||||
							
								
								
									
										151
									
								
								docs/sandbox-testing.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								docs/sandbox-testing.md
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| ``` | ||||
| @@ -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. | ||||
|   | ||||
| @@ -142,6 +142,16 @@ class Arguments(object): | ||||
|             action='store_true', | ||||
|             dest='refresh_pairs', | ||||
|         ) | ||||
|         parser.add_argument( | ||||
|             '--strategy-list', | ||||
|             help='Provide a commaseparated list of strategies to backtest ' | ||||
|                  'Please note that ticker-interval needs to be set either in config ' | ||||
|                  'or via command line. When using this together with --export trades, ' | ||||
|                  'the strategy-name is injected into the filename ' | ||||
|                  '(so backtest-data.json becomes backtest-data-DefaultStrategy.json', | ||||
|             nargs='+', | ||||
|             dest='strategy_list', | ||||
|         ) | ||||
|         parser.add_argument( | ||||
|             '--export', | ||||
|             help='export backtest results, argument are: trades\ | ||||
|   | ||||
| @@ -187,6 +187,14 @@ class Configuration(object): | ||||
|             config.update({'refresh_pairs': True}) | ||||
|             logger.info('Parameter -r/--refresh-pairs-cached detected ...') | ||||
|  | ||||
|         if 'strategy_list' in self.args and self.args.strategy_list: | ||||
|             config.update({'strategy_list': self.args.strategy_list}) | ||||
|             logger.info('Using strategy list of %s Strategies', len(self.args.strategy_list)) | ||||
|  | ||||
|         if 'ticker_interval' in self.args and self.args.ticker_interval: | ||||
|             config.update({'ticker_interval': self.args.ticker_interval}) | ||||
|             logger.info('Overriding ticker interval with Command line argument') | ||||
|  | ||||
|         # If --export is used we add it to the configuration | ||||
|         if 'export' in self.args and self.args.export: | ||||
|             config.update({'export': self.args.export}) | ||||
|   | ||||
| @@ -36,7 +36,7 @@ SUPPORTED_FIAT = [ | ||||
|     "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", | ||||
|     "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", | ||||
|     "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD", | ||||
|     "BTC", "ETH", "XRP", "LTC", "BCH", "USDT" | ||||
|     "BTC", "XBT", "ETH", "XRP", "LTC", "BCH", "USDT" | ||||
|     ] | ||||
|  | ||||
| # Required json-schema for user specified config | ||||
| @@ -45,7 +45,7 @@ CONF_SCHEMA = { | ||||
|     'properties': { | ||||
|         'max_open_trades': {'type': 'integer', 'minimum': 0}, | ||||
|         'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())}, | ||||
|         'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT', 'EUR', 'USD']}, | ||||
|         'stake_currency': {'type': 'string', 'enum': ['BTC', 'XBT', 'ETH', 'USDT', 'EUR', 'USD']}, | ||||
|         'stake_amount': { | ||||
|             "type": ["number", "string"], | ||||
|             "minimum": 0.0005, | ||||
| @@ -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': { | ||||
| @@ -124,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': { | ||||
|   | ||||
| @@ -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 | ||||
| @@ -90,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 | ||||
| @@ -107,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. | ||||
| @@ -150,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)}' | ||||
| @@ -167,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( | ||||
| @@ -200,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( | ||||
| @@ -287,7 +330,7 @@ class Exchange(object): | ||||
|             return self._cached_ticker[pair] | ||||
|  | ||||
|     @retrier | ||||
|     def get_ticker_history(self, pair: str, tick_interval: str, | ||||
|     def get_candle_history(self, pair: str, tick_interval: str, | ||||
|                            since_ms: Optional[int] = None) -> List[Dict]: | ||||
|         try: | ||||
|             # last item should be in the time interval [now - tick_interval, now] | ||||
|   | ||||
| @@ -10,7 +10,7 @@ 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 | ||||
|     :param ticker: See exchange.get_candle_history | ||||
|     :return: DataFrame | ||||
|     """ | ||||
|     cols = ['date', 'open', 'high', 'low', 'close', 'volume'] | ||||
|   | ||||
| @@ -330,7 +330,7 @@ class FreqtradeBot(object): | ||||
|  | ||||
|         # Pick pair based on buy signals | ||||
|         for _pair in whitelist: | ||||
|             thistory = self.exchange.get_ticker_history(_pair, interval) | ||||
|             thistory = self.exchange.get_candle_history(_pair, interval) | ||||
|             (buy, sell) = self.strategy.get_signal(_pair, interval, thistory) | ||||
|  | ||||
|             if buy and not sell: | ||||
| @@ -497,7 +497,7 @@ class FreqtradeBot(object): | ||||
|         (buy, sell) = (False, False) | ||||
|         experimental = self.config.get('experimental', {}) | ||||
|         if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'): | ||||
|             ticker = self.exchange.get_ticker_history(trade.pair, self.strategy.ticker_interval) | ||||
|             ticker = self.exchange.get_candle_history(trade.pair, self.strategy.ticker_interval) | ||||
|             (buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.ticker_interval, | ||||
|                                                    ticker) | ||||
|  | ||||
|   | ||||
| @@ -77,11 +77,11 @@ def load_tickerdata_file( | ||||
|     if os.path.isfile(gzipfile): | ||||
|         logger.debug('Loading ticker data from file %s', gzipfile) | ||||
|         with gzip.open(gzipfile) as tickerdata: | ||||
|             pairdata = json_load(tickerdata) | ||||
|             pairdata = json.load(tickerdata) | ||||
|     elif os.path.isfile(file): | ||||
|         logger.debug('Loading ticker data from file %s', file) | ||||
|         with open(file) as tickerdata: | ||||
|             pairdata = json_load(tickerdata) | ||||
|             pairdata = json.load(tickerdata) | ||||
|     else: | ||||
|         return None | ||||
|  | ||||
| @@ -177,7 +177,7 @@ def load_cached_data_for_updating(filename: str, | ||||
|     # read the cached file | ||||
|     if os.path.isfile(filename): | ||||
|         with open(filename, "rt") as file: | ||||
|             data = json_load(file) | ||||
|             data = json.load(file) | ||||
|             # remove the last item, because we are not sure if it is correct | ||||
|             # it could be fetched when the candle was incompleted | ||||
|             if data: | ||||
| @@ -233,7 +233,7 @@ def download_backtesting_testdata(datadir: str, | ||||
|     logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None') | ||||
|     logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None') | ||||
|  | ||||
|     new_data = exchange.get_ticker_history(pair=pair, tick_interval=tick_interval, | ||||
|     new_data = exchange.get_candle_history(pair=pair, tick_interval=tick_interval, | ||||
|                                            since_ms=since_ms) | ||||
|     data.extend(new_data) | ||||
|  | ||||
|   | ||||
| @@ -6,7 +6,9 @@ This module contains the backtesting logic | ||||
| import logging | ||||
| import operator | ||||
| from argparse import Namespace | ||||
| from copy import deepcopy | ||||
| from datetime import datetime, timedelta | ||||
| from pathlib import Path | ||||
| from typing import Any, Dict, List, NamedTuple, Optional, Tuple | ||||
|  | ||||
| import arrow | ||||
| @@ -52,13 +54,9 @@ class Backtesting(object): | ||||
|     backtesting = Backtesting(config) | ||||
|     backtesting.start() | ||||
|     """ | ||||
|  | ||||
|     def __init__(self, config: Dict[str, Any]) -> None: | ||||
|         self.config = config | ||||
|         self.strategy: IStrategy = StrategyResolver(self.config).strategy | ||||
|         self.ticker_interval = self.strategy.ticker_interval | ||||
|         self.tickerdata_to_dataframe = self.strategy.tickerdata_to_dataframe | ||||
|         self.populate_buy_trend = self.strategy.populate_buy_trend | ||||
|         self.populate_sell_trend = self.strategy.populate_sell_trend | ||||
|  | ||||
|         # Reset keys for backtesting | ||||
|         self.config['exchange']['key'] = '' | ||||
| @@ -66,9 +64,36 @@ class Backtesting(object): | ||||
|         self.config['exchange']['password'] = '' | ||||
|         self.config['exchange']['uid'] = '' | ||||
|         self.config['dry_run'] = True | ||||
|         self.strategylist: List[IStrategy] = [] | ||||
|         if self.config.get('strategy_list', None): | ||||
|             # Force one interval | ||||
|             self.ticker_interval = str(self.config.get('ticker_interval')) | ||||
|             for strat in list(self.config['strategy_list']): | ||||
|                 stratconf = deepcopy(self.config) | ||||
|                 stratconf['strategy'] = strat | ||||
|                 self.strategylist.append(StrategyResolver(stratconf).strategy) | ||||
|  | ||||
|         else: | ||||
|             # only one strategy | ||||
|             strat = StrategyResolver(self.config).strategy | ||||
|  | ||||
|             self.strategylist.append(StrategyResolver(self.config).strategy) | ||||
|         # Load one strategy | ||||
|         self._set_strategy(self.strategylist[0]) | ||||
|  | ||||
|         self.exchange = Exchange(self.config) | ||||
|         self.fee = self.exchange.get_fee() | ||||
|  | ||||
|     def _set_strategy(self, strategy): | ||||
|         """ | ||||
|         Load strategy into backtesting | ||||
|         """ | ||||
|         self.strategy = strategy | ||||
|         self.ticker_interval = self.config.get('ticker_interval') | ||||
|         self.tickerdata_to_dataframe = strategy.tickerdata_to_dataframe | ||||
|         self.advise_buy = strategy.advise_buy | ||||
|         self.advise_sell = strategy.advise_sell | ||||
|  | ||||
|     @staticmethod | ||||
|     def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: | ||||
|         """ | ||||
| @@ -132,7 +157,32 @@ class Backtesting(object): | ||||
|             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: | ||||
|     def _generate_text_table_strategy(self, all_results: dict) -> str: | ||||
|         """ | ||||
|         Generate summary table per strategy | ||||
|         """ | ||||
|         stake_currency = str(self.config.get('stake_currency')) | ||||
|  | ||||
|         floatfmt = ('s', 'd', '.2f', '.2f', '.8f', 'd', '.1f', '.1f') | ||||
|         tabular_data = [] | ||||
|         headers = ['Strategy', 'buy count', 'avg profit %', 'cum profit %', | ||||
|                    'total profit ' + stake_currency, 'avg duration', 'profit', 'loss'] | ||||
|         for strategy, results in all_results.items(): | ||||
|             tabular_data.append([ | ||||
|                 strategy, | ||||
|                 len(results.index), | ||||
|                 results.profit_percent.mean() * 100.0, | ||||
|                 results.profit_percent.sum() * 100.0, | ||||
|                 results.profit_abs.sum(), | ||||
|                 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 _store_backtest_result(self, recordfilename: str, results: DataFrame, | ||||
|                                strategyname: Optional[str] = None) -> None: | ||||
|  | ||||
|         records = [(t.pair, t.profit_percent, t.open_time.timestamp(), | ||||
|                     t.close_time.timestamp(), t.open_index - 1, t.trade_duration, | ||||
| @@ -140,6 +190,11 @@ class Backtesting(object): | ||||
|                    for index, t in results.iterrows()] | ||||
|  | ||||
|         if records: | ||||
|             if strategyname: | ||||
|                 # Inject strategyname to filename | ||||
|                 recname = Path(recordfilename) | ||||
|                 recordfilename = str(Path.joinpath( | ||||
|                     recname.parent, f'{recname.stem}-{strategyname}').with_suffix(recname.suffix)) | ||||
|             logger.info('Dumping backtest results to %s', recordfilename) | ||||
|             file_dump_json(recordfilename, records) | ||||
|  | ||||
| @@ -229,8 +284,8 @@ class Backtesting(object): | ||||
|         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) | ||||
| @@ -283,7 +338,7 @@ class Backtesting(object): | ||||
|         if self.config.get('live'): | ||||
|             logger.info('Downloading data for all pairs in whitelist ...') | ||||
|             for pair in pairs: | ||||
|                 data[pair] = self.exchange.get_ticker_history(pair, self.ticker_interval) | ||||
|                 data[pair] = self.exchange.get_candle_history(pair, self.ticker_interval) | ||||
|         else: | ||||
|             logger.info('Using local backtesting data (using whitelist in given config) ...') | ||||
|  | ||||
| @@ -307,62 +362,55 @@ class Backtesting(object): | ||||
|         else: | ||||
|             logger.info('Ignoring max_open_trades (--disable-max-market-positions was used) ...') | ||||
|             max_open_trades = 0 | ||||
|         all_results = {} | ||||
|  | ||||
|         preprocessed = self.tickerdata_to_dataframe(data) | ||||
|         for strat in self.strategylist: | ||||
|             logger.info("Running backtesting for Strategy %s", strat.get_strategy_name()) | ||||
|             self._set_strategy(strat) | ||||
|  | ||||
|         # Print timeframe | ||||
|         min_date, max_date = self.get_timeframe(preprocessed) | ||||
|         logger.info( | ||||
|             'Measuring data from %s up to %s (%s days)..', | ||||
|             min_date.isoformat(), | ||||
|             max_date.isoformat(), | ||||
|             (max_date - min_date).days | ||||
|         ) | ||||
|             # need to reprocess data every time to populate signals | ||||
|             preprocessed = self.tickerdata_to_dataframe(data) | ||||
|  | ||||
|         # Execute backtest and print results | ||||
|         results = self.backtest( | ||||
|             { | ||||
|                 'stake_amount': self.config.get('stake_amount'), | ||||
|                 'processed': preprocessed, | ||||
|                 'max_open_trades': max_open_trades, | ||||
|                 'position_stacking': self.config.get('position_stacking', False), | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|         if self.config.get('export', False): | ||||
|             self._store_backtest_result(self.config.get('exportfilename'), results) | ||||
|  | ||||
|         logger.info( | ||||
|             '\n' + '=' * 49 + | ||||
|             ' BACKTESTING REPORT ' + | ||||
|             '=' * 50 + '\n' | ||||
|             '%s', | ||||
|             self._generate_text_table( | ||||
|                 data, | ||||
|                 results | ||||
|             # Print timeframe | ||||
|             min_date, max_date = self.get_timeframe(preprocessed) | ||||
|             logger.info( | ||||
|                 'Measuring data from %s up to %s (%s days)..', | ||||
|                 min_date.isoformat(), | ||||
|                 max_date.isoformat(), | ||||
|                 (max_date - min_date).days | ||||
|             ) | ||||
|         ) | ||||
|         # logger.info( | ||||
|         #     results[['sell_reason']].groupby('sell_reason').count() | ||||
|         # ) | ||||
|  | ||||
|         logger.info( | ||||
|             '\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] | ||||
|             # Execute backtest and print results | ||||
|             all_results[self.strategy.get_strategy_name()] = self.backtest( | ||||
|                 { | ||||
|                     'stake_amount': self.config.get('stake_amount'), | ||||
|                     'processed': preprocessed, | ||||
|                     'max_open_trades': max_open_trades, | ||||
|                     'position_stacking': self.config.get('position_stacking', False), | ||||
|                 } | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|         for strategy, results in all_results.items(): | ||||
|  | ||||
|             if self.config.get('export', False): | ||||
|                 self._store_backtest_result(self.config['exportfilename'], results, | ||||
|                                             strategy if len(self.strategylist) > 1 else None) | ||||
|  | ||||
|             print(f"Result for strategy {strategy}") | ||||
|             print(' BACKTESTING REPORT '.center(119, '=')) | ||||
|             print(self._generate_text_table(data, results)) | ||||
|  | ||||
|             print(' SELL REASON STATS '.center(119, '=')) | ||||
|             print(self._generate_text_table_sell_reason(data, results)) | ||||
|  | ||||
|             print(' LEFT OPEN TRADES REPORT '.center(119, '=')) | ||||
|             print(self._generate_text_table(data, results.loc[results.open_at_end])) | ||||
|             print() | ||||
|         if len(all_results) > 1: | ||||
|             # Print Strategy summary table | ||||
|             print(' Strategy Summary '.center(119, '=')) | ||||
|             print(self._generate_text_table_strategy(all_results)) | ||||
|             print('\nFor more details, please look at the detail tables above') | ||||
|  | ||||
|  | ||||
| def setup_configuration(args: Namespace) -> Dict[str, Any]: | ||||
|   | ||||
| @@ -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 | ||||
|             """ | ||||
| @@ -270,7 +270,7 @@ class Hyperopt(Backtesting): | ||||
|             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.strategy.stoploss = params['stoploss'] | ||||
| @@ -351,7 +351,7 @@ class Hyperopt(Backtesting): | ||||
|         ) | ||||
|  | ||||
|         if self.has_space('buy'): | ||||
|             self.strategy.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): | ||||
|   | ||||
| @@ -82,7 +82,7 @@ def check_migrate(engine) -> None: | ||||
|         logger.info(f'trying {table_back_name}') | ||||
|  | ||||
|     # Check for latest column | ||||
|     if not has_column(cols, 'max_rate'): | ||||
|     if not has_column(cols, 'ticker_interval'): | ||||
|         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') | ||||
| @@ -157,8 +157,8 @@ class Trade(_DECL_BASE): | ||||
|  | ||||
|     id = Column(Integer, primary_key=True) | ||||
|     exchange = Column(String, nullable=False) | ||||
|     pair = Column(String, nullable=False) | ||||
|     is_open = Column(Boolean, nullable=False, default=True) | ||||
|     pair = Column(String, nullable=False, index=True) | ||||
|     is_open = Column(Boolean, nullable=False, default=True, index=True) | ||||
|     fee_open = Column(Float, nullable=False, default=0.0) | ||||
|     fee_close = Column(Float, nullable=False, default=0.0) | ||||
|     open_rate = Column(Float) | ||||
|   | ||||
| @@ -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[ | ||||
|   | ||||
| @@ -7,6 +7,7 @@ from abc import ABC, abstractmethod | ||||
| from datetime import datetime | ||||
| from enum import Enum | ||||
| from typing import Dict, List, NamedTuple, Tuple | ||||
| import warnings | ||||
|  | ||||
| import arrow | ||||
| from pandas import DataFrame | ||||
| @@ -57,34 +58,45 @@ 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 | ||||
|         """ | ||||
|  | ||||
| @@ -94,16 +106,16 @@ class IStrategy(ABC): | ||||
|         """ | ||||
|         return self.__class__.__name__ | ||||
|  | ||||
|     def analyze_ticker(self, ticker_history: List[Dict]) -> DataFrame: | ||||
|     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.populate_indicators(dataframe) | ||||
|         dataframe = self.populate_buy_trend(dataframe) | ||||
|         dataframe = self.populate_sell_trend(dataframe) | ||||
|         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]: | ||||
| @@ -118,7 +130,7 @@ class IStrategy(ABC): | ||||
|             return False, False | ||||
|  | ||||
|         try: | ||||
|             dataframe = self.analyze_ticker(ticker_hist) | ||||
|             dataframe = self.analyze_ticker(ticker_hist, {'pair': pair}) | ||||
|         except ValueError as error: | ||||
|             logger.warning( | ||||
|                 'Unable to analyze ticker for pair %s: %s', | ||||
| @@ -200,6 +212,7 @@ class IStrategy(ABC): | ||||
|         """ | ||||
|         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) | ||||
| @@ -227,12 +240,15 @@ class IStrategy(ABC): | ||||
|             # check if we have a special stop loss for positive condition | ||||
|             # and if profit is positive | ||||
|             stop_loss_value = self.stoploss | ||||
|             if 'trailing_stop_positive' in self.config and current_profit > 0: | ||||
|             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"since we have profit {current_profit}") | ||||
|                              f"with offset {sl_offset:.4g} " | ||||
|                              f"since we have profit {current_profit:.4f}%") | ||||
|  | ||||
|             trade.adjust_stop_loss(current_rate, stop_loss_value) | ||||
|  | ||||
| @@ -259,5 +275,50 @@ class IStrategy(ABC): | ||||
|         """ | ||||
|         Creates a dataframe and populates indicators for given ticker data | ||||
|         """ | ||||
|         return {pair: self.populate_indicators(parse_ticker_dataframe(pair_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) | ||||
|   | ||||
| @@ -7,7 +7,10 @@ import importlib.util | ||||
| import inspect | ||||
| import logging | ||||
| import os | ||||
| import tempfile | ||||
| from base64 import urlsafe_b64decode | ||||
| from collections import OrderedDict | ||||
| from pathlib import Path | ||||
| from typing import Dict, Optional, Type | ||||
|  | ||||
| from freqtrade import constants | ||||
| @@ -87,11 +90,34 @@ class StrategyResolver(object): | ||||
|             # Add extra strategy directory on top of search paths | ||||
|             abs_paths.insert(0, extra_dir) | ||||
|  | ||||
|         if ":" in strategy_name: | ||||
|             logger.info("loading base64 endocded strategy") | ||||
|             strat = strategy_name.split(":") | ||||
|  | ||||
|             if len(strat) == 2: | ||||
|                 temp = Path(tempfile.mkdtemp("freq", "strategy")) | ||||
|                 name = strat[0] + ".py" | ||||
|  | ||||
|                 temp.joinpath(name).write_text(urlsafe_b64decode(strat[1]).decode('utf-8')) | ||||
|                 temp.joinpath("__init__.py").touch() | ||||
|  | ||||
|                 strategy_name = os.path.splitext(name)[0] | ||||
|  | ||||
|                 # register temp path with the bot | ||||
|                 abs_paths.insert(0, str(temp.resolve())) | ||||
|  | ||||
|         for path in abs_paths: | ||||
|             try: | ||||
|                 strategy = self._search_strategy(path, strategy_name=strategy_name, config=config) | ||||
|                 if strategy: | ||||
|                     logger.info('Using resolved strategy %s from \'%s\'', strategy_name, path) | ||||
|                     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) | ||||
|   | ||||
| @@ -8,10 +8,8 @@ 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.exchange.exchange_helpers import parse_ticker_dataframe | ||||
| from freqtrade.exchange import Exchange | ||||
| from freqtrade.freqtradebot import FreqtradeBot | ||||
| @@ -127,7 +125,6 @@ def default_conf(): | ||||
|         "db_url": "sqlite://", | ||||
|         "loglevel": logging.DEBUG, | ||||
|     } | ||||
|     validate(configuration, constants.CONF_SCHEMA) | ||||
|     return configuration | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -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={ | ||||
| @@ -80,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): | ||||
| @@ -110,8 +193,7 @@ 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) | ||||
| @@ -121,7 +203,7 @@ def test_validate_pairs_stake_exception(default_conf, mocker, caplog): | ||||
|         OperationalException, | ||||
|         match=r'Pair ETH/BTC not compatible with stake_currency: ETH' | ||||
|     ): | ||||
|         Exchange(conf) | ||||
|         Exchange(default_conf) | ||||
|  | ||||
|  | ||||
| def test_validate_timeframes(default_conf, mocker): | ||||
| @@ -173,7 +255,7 @@ def test_validate_timeframes_not_in_config(default_conf, mocker): | ||||
|     Exchange(default_conf) | ||||
|  | ||||
|  | ||||
| def test_exchangehas(default_conf, mocker): | ||||
| def test_exchange_has(default_conf, mocker): | ||||
|     exchange = get_patched_exchange(mocker, default_conf) | ||||
|     assert not exchange.exchange_has('ASDFASDF') | ||||
|     api_mock = MagicMock() | ||||
| @@ -442,7 +524,7 @@ def make_fetch_ohlcv_mock(data): | ||||
|     return fetch_ohlcv_mock | ||||
|  | ||||
|  | ||||
| def test_get_ticker_history(default_conf, mocker): | ||||
| def test_get_candle_history(default_conf, mocker): | ||||
|     api_mock = MagicMock() | ||||
|     tick = [ | ||||
|         [ | ||||
| @@ -459,7 +541,7 @@ def test_get_ticker_history(default_conf, mocker): | ||||
|     exchange = get_patched_exchange(mocker, default_conf, api_mock) | ||||
|  | ||||
|     # retrieve original ticker | ||||
|     ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) | ||||
|     ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval']) | ||||
|     assert ticks[0][0] == 1511686200000 | ||||
|     assert ticks[0][1] == 1 | ||||
|     assert ticks[0][2] == 2 | ||||
| @@ -481,7 +563,7 @@ def test_get_ticker_history(default_conf, mocker): | ||||
|     api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(new_tick)) | ||||
|     exchange = get_patched_exchange(mocker, default_conf, api_mock) | ||||
|  | ||||
|     ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) | ||||
|     ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval']) | ||||
|     assert ticks[0][0] == 1511686210000 | ||||
|     assert ticks[0][1] == 6 | ||||
|     assert ticks[0][2] == 7 | ||||
| @@ -490,16 +572,16 @@ def test_get_ticker_history(default_conf, mocker): | ||||
|     assert ticks[0][5] == 10 | ||||
|  | ||||
|     ccxt_exceptionhandlers(mocker, default_conf, api_mock, | ||||
|                            "get_ticker_history", "fetch_ohlcv", | ||||
|                            "get_candle_history", "fetch_ohlcv", | ||||
|                            pair='ABCD/BTC', tick_interval=default_conf['ticker_interval']) | ||||
|  | ||||
|     with pytest.raises(OperationalException, match=r'Exchange .* does not support.*'): | ||||
|         api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported) | ||||
|         exchange = get_patched_exchange(mocker, default_conf, api_mock) | ||||
|         exchange.get_ticker_history(pair='ABCD/BTC', tick_interval=default_conf['ticker_interval']) | ||||
|         exchange.get_candle_history(pair='ABCD/BTC', tick_interval=default_conf['ticker_interval']) | ||||
|  | ||||
|  | ||||
| def test_get_ticker_history_sort(default_conf, mocker): | ||||
| def test_get_candle_history_sort(default_conf, mocker): | ||||
|     api_mock = MagicMock() | ||||
|  | ||||
|     # GDAX use-case (real data from GDAX) | ||||
| @@ -522,7 +604,7 @@ def test_get_ticker_history_sort(default_conf, mocker): | ||||
|     exchange = get_patched_exchange(mocker, default_conf, api_mock) | ||||
|  | ||||
|     # Test the ticker history sort | ||||
|     ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) | ||||
|     ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval']) | ||||
|     assert ticks[0][0] == 1527830400000 | ||||
|     assert ticks[0][1] == 0.07649 | ||||
|     assert ticks[0][2] == 0.07651 | ||||
| @@ -555,7 +637,7 @@ def test_get_ticker_history_sort(default_conf, mocker): | ||||
|     api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick)) | ||||
|     exchange = get_patched_exchange(mocker, default_conf, api_mock) | ||||
|     # Test the ticker history sort | ||||
|     ticks = exchange.get_ticker_history('ETH/BTC', default_conf['ticker_interval']) | ||||
|     ticks = exchange.get_candle_history('ETH/BTC', default_conf['ticker_interval']) | ||||
|     assert ticks[0][0] == 1527827700000 | ||||
|     assert ticks[0][1] == 0.07659999 | ||||
|     assert ticks[0][2] == 0.0766 | ||||
|   | ||||
| @@ -1,9 +1,5 @@ | ||||
| # pragma pylint: disable=missing-docstring, C0103 | ||||
|  | ||||
| """ | ||||
| Unit test file for exchange_helpers.py | ||||
| """ | ||||
|  | ||||
| from freqtrade.exchange.exchange_helpers import parse_ticker_dataframe | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -3,7 +3,6 @@ | ||||
| import json | ||||
| import math | ||||
| import random | ||||
| from copy import deepcopy | ||||
| from typing import List | ||||
| from unittest.mock import MagicMock | ||||
|  | ||||
| @@ -111,7 +110,7 @@ def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=Fals | ||||
|     return pairdata | ||||
|  | ||||
|  | ||||
| # use for mock freqtrade.exchange.get_ticker_history' | ||||
| # use for mock freqtrade.exchange.get_candle_history' | ||||
| def _load_pair_as_ticks(pair, tickfreq): | ||||
|     ticks = optimize.load_data(None, ticker_interval=tickfreq, pairs=[pair]) | ||||
|     ticks = trim_dictlist(ticks, -201) | ||||
| @@ -146,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) | ||||
| @@ -164,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) | ||||
|     )) | ||||
| @@ -205,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) | ||||
|     )) | ||||
| @@ -276,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 = [ | ||||
| @@ -298,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) | ||||
| @@ -323,25 +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 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) | ||||
| @@ -358,9 +337,6 @@ def test_tickerdata_to_dataframe(default_conf, mocker) -> None: | ||||
|  | ||||
|  | ||||
| def test_get_timeframe(default_conf, mocker) -> None: | ||||
|     """ | ||||
|     Test Backtesting.get_timeframe() method | ||||
|     """ | ||||
|     patch_exchange(mocker) | ||||
|     backtesting = Backtesting(default_conf) | ||||
|  | ||||
| @@ -377,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) | ||||
|  | ||||
| @@ -408,9 +381,6 @@ def test_generate_text_table(default_conf, mocker): | ||||
|  | ||||
|  | ||||
| def test_generate_text_table_sell_reason(default_conf, mocker): | ||||
|     """ | ||||
|     Test Backtesting.generate_text_table_sell_reason() method | ||||
|     """ | ||||
|     patch_exchange(mocker) | ||||
|     backtesting = Backtesting(default_conf) | ||||
|  | ||||
| @@ -436,16 +406,56 @@ def test_generate_text_table_sell_reason(default_conf, mocker): | ||||
|         data={'ETH/BTC': {}}, results=results) == result_str | ||||
|  | ||||
|  | ||||
| def test_backtesting_start(default_conf, mocker, caplog) -> None: | ||||
| def test_generate_text_table_strategyn(default_conf, mocker): | ||||
|     """ | ||||
|     Test Backtesting.start() method | ||||
|     Test Backtesting.generate_text_table_sell_reason() method | ||||
|     """ | ||||
|     patch_exchange(mocker) | ||||
|     backtesting = Backtesting(default_conf) | ||||
|     results = {} | ||||
|     results['ETH/BTC'] = 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] | ||||
|         } | ||||
|     ) | ||||
|     results['LTC/BTC'] = pd.DataFrame( | ||||
|         { | ||||
|             'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'], | ||||
|             'profit_percent': [0.4, 0.2, 0.3], | ||||
|             'profit_abs': [0.4, 0.4, 0.5], | ||||
|             'trade_duration': [15, 30, 15], | ||||
|             'profit': [4, 1, 0], | ||||
|             'loss': [0, 0, 1], | ||||
|             'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] | ||||
|         } | ||||
|     ) | ||||
|  | ||||
|     result_str = ( | ||||
|         '| Strategy   |   buy count |   avg profit % |   cum profit % ' | ||||
|         '|   total profit BTC | avg duration   |   profit |   loss |\n' | ||||
|         '|:-----------|------------:|---------------:|---------------:' | ||||
|         '|-------------------:|:---------------|---------:|-------:|\n' | ||||
|         '| ETH/BTC    |           3 |          20.00 |          60.00 ' | ||||
|         '|         1.10000000 | 0:17:00        |        3 |      0 |\n' | ||||
|         '| LTC/BTC    |           3 |          30.00 |          90.00 ' | ||||
|         '|         1.30000000 | 0:20:00        |        3 |      0 |' | ||||
|     ) | ||||
|     print(backtesting._generate_text_table_strategy(all_results=results)) | ||||
|     assert backtesting._generate_text_table_strategy(all_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.optimize.load_data', mocked_load_data) | ||||
|     mocker.patch('freqtrade.exchange.Exchange.get_ticker_history') | ||||
|     mocker.patch('freqtrade.exchange.Exchange.get_candle_history') | ||||
|     patch_exchange(mocker) | ||||
|     mocker.patch.multiple( | ||||
|         'freqtrade.optimize.backtesting.Backtesting', | ||||
| @@ -454,15 +464,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 = [ | ||||
| @@ -477,15 +486,11 @@ 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.optimize.load_data', MagicMock(return_value={})) | ||||
|     mocker.patch('freqtrade.exchange.Exchange.get_ticker_history') | ||||
|     mocker.patch('freqtrade.exchange.Exchange.get_candle_history') | ||||
|     patch_exchange(mocker) | ||||
|     mocker.patch.multiple( | ||||
|         'freqtrade.optimize.backtesting.Backtesting', | ||||
| @@ -494,15 +499,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 | ||||
|  | ||||
| @@ -510,9 +514,6 @@ 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) | ||||
| @@ -560,9 +561,6 @@ def test_backtest(default_conf, fee, mocker) -> None: | ||||
|  | ||||
|  | ||||
| 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) | ||||
| @@ -583,9 +581,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) | ||||
|  | ||||
| @@ -611,42 +606,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 | ||||
|  | ||||
| @@ -655,8 +650,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 | ||||
| @@ -703,6 +698,18 @@ def test_backtest_record(default_conf, fee, mocker): | ||||
|     records = records[0] | ||||
|     # Ensure records are of correct type | ||||
|     assert len(records) == 4 | ||||
|  | ||||
|     # reset test to test with strategy name | ||||
|     names = [] | ||||
|     records = [] | ||||
|     backtesting._store_backtest_result("backtest-result.json", results, "DefStrat") | ||||
|     assert len(results) == 4 | ||||
|     # Assert file_dump_json was only called once | ||||
|     assert names == ['backtest-result-DefStrat.json'] | ||||
|     records = records[0] | ||||
|     # Ensure records are of correct type | ||||
|     assert len(records) == 4 | ||||
|  | ||||
|     # ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117) | ||||
|     # Below follows just a typecheck of the schema/type of trade-records | ||||
|     oix = None | ||||
| @@ -725,26 +732,16 @@ 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'] | ||||
|     mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', | ||||
|     default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] | ||||
|     mocker.patch('freqtrade.exchange.Exchange.get_candle_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() | ||||
|     args.ticker_interval = 1 | ||||
|     args.level = 10 | ||||
|     args.live = True | ||||
|     args.datadir = None | ||||
|     args.export = None | ||||
|     args.strategy = 'DefaultStrategy' | ||||
|     args.timerange = '-100'  # needed due to MagicMock malleability | ||||
|  | ||||
|     args = [ | ||||
|         '--config', 'config.json', | ||||
|         '--strategy', 'DefaultStrategy', | ||||
| @@ -775,3 +772,60 @@ def test_backtest_start_live(default_conf, mocker, caplog): | ||||
|  | ||||
|     for line in exists: | ||||
|         assert log_has(line, caplog.record_tuples) | ||||
|  | ||||
|  | ||||
| def test_backtest_start_multi_strat(default_conf, mocker, caplog): | ||||
|     default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC'] | ||||
|     mocker.patch('freqtrade.exchange.Exchange.get_candle_history', | ||||
|                  new=lambda s, n, i: _load_pair_as_ticks(n, i)) | ||||
|     patch_exchange(mocker) | ||||
|     backtestmock = MagicMock() | ||||
|     mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock) | ||||
|     gen_table_mock = MagicMock() | ||||
|     mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', gen_table_mock) | ||||
|     gen_strattable_mock = MagicMock() | ||||
|     mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table_strategy', | ||||
|                  gen_strattable_mock) | ||||
|     mocker.patch('freqtrade.configuration.open', mocker.mock_open( | ||||
|         read_data=json.dumps(default_conf) | ||||
|     )) | ||||
|  | ||||
|     args = [ | ||||
|         '--config', 'config.json', | ||||
|         '--datadir', 'freqtrade/tests/testdata', | ||||
|         'backtesting', | ||||
|         '--ticker-interval', '1m', | ||||
|         '--live', | ||||
|         '--timerange', '-100', | ||||
|         '--enable-position-stacking', | ||||
|         '--disable-max-market-positions', | ||||
|         '--strategy-list', | ||||
|         'DefaultStrategy', | ||||
|         'TestStrategy', | ||||
|     ] | ||||
|     args = get_args(args) | ||||
|     start(args) | ||||
|     # 2 backtests, 4 tables | ||||
|     assert backtestmock.call_count == 2 | ||||
|     assert gen_table_mock.call_count == 4 | ||||
|     assert gen_strattable_mock.call_count == 1 | ||||
|  | ||||
|     # check the logs, that will contain the backtest result | ||||
|     exists = [ | ||||
|         'Parameter -i/--ticker-interval detected ...', | ||||
|         'Using ticker_interval: 1m ...', | ||||
|         'Parameter -l/--live detected ...', | ||||
|         '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 --enable-position-stacking detected ...', | ||||
|         'Running backtesting for Strategy DefaultStrategy', | ||||
|         'Running backtesting for Strategy TestStrategy', | ||||
|     ] | ||||
|  | ||||
|     for line in exists: | ||||
|         assert log_has(line, caplog.record_tuples) | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -53,10 +53,7 @@ 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) | ||||
|     mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history) | ||||
|     file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-30m.json') | ||||
|     _backup_file(file, copy_file=True) | ||||
|     optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='30m') | ||||
| @@ -66,10 +63,7 @@ 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) | ||||
|     mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history) | ||||
|  | ||||
|     file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-5m.json') | ||||
|     _backup_file(file, copy_file=True) | ||||
| @@ -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) | ||||
|  | ||||
|     mocker.patch('freqtrade.exchange.Exchange.get_candle_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']) | ||||
| @@ -97,7 +87,7 @@ def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog, default_co | ||||
|     """ | ||||
|     Test load_data() with 1 min ticker | ||||
|     """ | ||||
|     mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) | ||||
|     mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history) | ||||
|     exchange = get_patched_exchange(mocker, default_conf) | ||||
|     file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json') | ||||
|  | ||||
| @@ -128,7 +118,7 @@ def test_testdata_path() -> None: | ||||
|  | ||||
|  | ||||
| def test_download_pairs(ticker_history, mocker, default_conf) -> None: | ||||
|     mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) | ||||
|     mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history) | ||||
|     exchange = get_patched_exchange(mocker, default_conf) | ||||
|     file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json') | ||||
|     file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json') | ||||
| @@ -271,7 +261,7 @@ def test_load_cached_data_for_updating(mocker) -> None: | ||||
|  | ||||
|  | ||||
| def test_download_pairs_exception(ticker_history, mocker, caplog, default_conf) -> None: | ||||
|     mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) | ||||
|     mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history) | ||||
|     mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata', | ||||
|                  side_effect=BaseException('File Error')) | ||||
|     exchange = get_patched_exchange(mocker, default_conf) | ||||
| @@ -289,7 +279,7 @@ def test_download_pairs_exception(ticker_history, mocker, caplog, default_conf) | ||||
|  | ||||
|  | ||||
| def test_download_backtesting_testdata(ticker_history, mocker, default_conf) -> None: | ||||
|     mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=ticker_history) | ||||
|     mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history) | ||||
|     exchange = get_patched_exchange(mocker, default_conf) | ||||
|  | ||||
|     # Download a 1 min ticker file | ||||
| @@ -314,7 +304,7 @@ def test_download_backtesting_testdata2(mocker, default_conf) -> None: | ||||
|         [1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199] | ||||
|     ] | ||||
|     json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None) | ||||
|     mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=tick) | ||||
|     mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=tick) | ||||
|     exchange = get_patched_exchange(mocker, default_conf) | ||||
|     download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='1m') | ||||
|     download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='3m') | ||||
| @@ -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'} | ||||
|   | ||||
| @@ -1,9 +1,6 @@ | ||||
| # 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, ANY | ||||
|  | ||||
| @@ -14,8 +11,8 @@ from freqtrade.freqtradebot import FreqtradeBot | ||||
| from freqtrade.persistence import Trade | ||||
| 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 | ||||
| @@ -28,9 +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_coinmarketcap(mocker) | ||||
|     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) | ||||
|     mocker.patch.multiple( | ||||
| @@ -72,9 +66,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None: | ||||
|  | ||||
|  | ||||
| def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None: | ||||
|     """ | ||||
|     Test rpc_status_table() method | ||||
|     """ | ||||
|     patch_coinmarketcap(mocker) | ||||
|     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) | ||||
|     mocker.patch.multiple( | ||||
| @@ -106,9 +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_coinmarketcap(mocker, value={'price_usd': 15000.0}) | ||||
|     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) | ||||
|     mocker.patch.multiple( | ||||
| @@ -158,13 +146,11 @@ 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 | ||||
|     """ | ||||
|     mocker.patch.multiple( | ||||
|         'freqtrade.fiat_convert.Market', | ||||
|         ticker=MagicMock(return_value={'price_usd': 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( | ||||
| @@ -236,9 +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 | ||||
|     """ | ||||
|     mocker.patch.multiple( | ||||
|         'freqtrade.fiat_convert.Market', | ||||
|         ticker=MagicMock(return_value={'price_usd': 15000.0}), | ||||
| @@ -295,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, | ||||
| @@ -315,6 +295,7 @@ def test_rpc_balance_handle(default_conf, mocker): | ||||
|         'freqtrade.fiat_convert.Market', | ||||
|         ticker=MagicMock(return_value={'price_usd': 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( | ||||
| @@ -342,9 +323,6 @@ def test_rpc_balance_handle(default_conf, mocker): | ||||
|  | ||||
|  | ||||
| def test_rpc_start(mocker, default_conf) -> None: | ||||
|     """ | ||||
|     Test rpc_start() method | ||||
|     """ | ||||
|     patch_coinmarketcap(mocker) | ||||
|     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) | ||||
|     mocker.patch.multiple( | ||||
| @@ -368,9 +346,6 @@ def test_rpc_start(mocker, default_conf) -> None: | ||||
|  | ||||
|  | ||||
| def test_rpc_stop(mocker, default_conf) -> None: | ||||
|     """ | ||||
|     Test rpc_stop() method | ||||
|     """ | ||||
|     patch_coinmarketcap(mocker) | ||||
|     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) | ||||
|     mocker.patch.multiple( | ||||
| @@ -395,9 +370,6 @@ def test_rpc_stop(mocker, default_conf) -> None: | ||||
|  | ||||
|  | ||||
| def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None: | ||||
|     """ | ||||
|     Test rpc_forcesell() method | ||||
|     """ | ||||
|     patch_coinmarketcap(mocker) | ||||
|     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) | ||||
|  | ||||
| @@ -499,9 +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_coinmarketcap(mocker) | ||||
|     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) | ||||
|     mocker.patch.multiple( | ||||
| @@ -538,9 +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_coinmarketcap(mocker) | ||||
|     mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock()) | ||||
|     mocker.patch.multiple( | ||||
|   | ||||
| @@ -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 import RPCMessageType, RPCManager | ||||
| from freqtrade.tests.conftest import log_has, get_patched_freqtradebot | ||||
|  | ||||
|  | ||||
| def test_rpc_manager_object() -> None: | ||||
|     """ Test the Arguments object has the mandatory methods """ | ||||
|     assert hasattr(RPCManager, 'send_msg') | ||||
|     assert hasattr(RPCManager, 'cleanup') | ||||
|  | ||||
|  | ||||
| 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,15 +65,10 @@ 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({ | ||||
|         'type': RPCMessageType.STATUS_NOTIFICATION, | ||||
| @@ -112,9 +80,6 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None: | ||||
|  | ||||
|  | ||||
| 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()) | ||||
|  | ||||
| @@ -130,30 +95,21 @@ def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None: | ||||
|  | ||||
|  | ||||
| def test_init_webhook_disabled(mocker, default_conf, caplog) -> None: | ||||
|     """ Test _init() method with Webhook disabled """ | ||||
|     caplog.set_level(logging.DEBUG) | ||||
|  | ||||
|     conf = deepcopy(default_conf) | ||||
|     conf['telegram']['enabled'] = False | ||||
|     conf['webhook'] = {'enabled': False} | ||||
|  | ||||
|     rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf)) | ||||
|     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: | ||||
|     """ | ||||
|     Test _init() method with Webhook enabled | ||||
|     """ | ||||
|     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) | ||||
|     len_modules = len(rpc_manager.registered_modules) | ||||
|     assert len_modules == 1 | ||||
|     assert len(rpc_manager.registered_modules) == 1 | ||||
|     assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules] | ||||
|   | ||||
| @@ -1,12 +1,8 @@ | ||||
| # 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, ANY | ||||
| @@ -24,8 +20,8 @@ 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): | ||||
| @@ -55,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()) | ||||
|  | ||||
| @@ -67,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)) | ||||
|  | ||||
| @@ -86,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) | ||||
| @@ -99,9 +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_coinmarketcap(mocker) | ||||
|     patch_exchange(mocker, None) | ||||
|  | ||||
| @@ -109,9 +95,8 @@ 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 | ||||
|     bot = 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) | ||||
| @@ -131,18 +116,14 @@ 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_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 | ||||
|     bot = 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) | ||||
| @@ -162,19 +143,15 @@ 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_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 | ||||
|     default_conf['telegram']['enabled'] = False | ||||
|  | ||||
|     bot = FreqtradeBot(conf) | ||||
|     bot = FreqtradeBot(default_conf) | ||||
|     patch_get_signal(bot, (True, False)) | ||||
|     dummy = DummyCls(bot) | ||||
|  | ||||
| @@ -195,13 +172,9 @@ 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_coinmarketcap(mocker) | ||||
|  | ||||
| @@ -236,7 +209,7 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None: | ||||
|     ) | ||||
|     mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) | ||||
|  | ||||
|     freqtradebot = FreqtradeBot(conf) | ||||
|     freqtradebot = FreqtradeBot(default_conf) | ||||
|     patch_get_signal(freqtradebot, (True, False)) | ||||
|     telegram = Telegram(freqtradebot) | ||||
|  | ||||
| @@ -254,9 +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_coinmarketcap(mocker) | ||||
|     mocker.patch.multiple( | ||||
|         'freqtrade.exchange.Exchange', | ||||
| @@ -302,9 +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_coinmarketcap(mocker) | ||||
|     mocker.patch.multiple( | ||||
|         'freqtrade.exchange.Exchange', | ||||
| @@ -322,9 +289,8 @@ 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) | ||||
| @@ -357,9 +323,6 @@ 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_coinmarketcap(mocker, value={'price_usd': 15000.0}) | ||||
|     mocker.patch( | ||||
|         'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', | ||||
| @@ -431,9 +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_coinmarketcap(mocker, value={'price_usd': 15000.0}) | ||||
|     mocker.patch.multiple( | ||||
|         'freqtrade.exchange.Exchange', | ||||
| @@ -470,9 +430,6 @@ 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_coinmarketcap(mocker, value={'price_usd': 15000.0}) | ||||
|     mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0) | ||||
|     mocker.patch.multiple( | ||||
| @@ -531,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, | ||||
| @@ -559,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, | ||||
| @@ -602,10 +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 | ||||
|     """ | ||||
| def test_balance_handle_empty_response(default_conf, update, mocker) -> None: | ||||
|     mocker.patch('freqtrade.exchange.Exchange.get_balances', return_value={}) | ||||
|  | ||||
|     msg_mock = MagicMock() | ||||
| @@ -627,9 +574,6 @@ def test_zero_balance_handle(default_conf, update, mocker) -> None: | ||||
|  | ||||
|  | ||||
| def test_start_handle(default_conf, update, mocker) -> None: | ||||
|     """ | ||||
|     Test _start() method | ||||
|     """ | ||||
|     msg_mock = MagicMock() | ||||
|     mocker.patch.multiple( | ||||
|         'freqtrade.rpc.telegram.Telegram', | ||||
| @@ -648,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', | ||||
| @@ -670,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( | ||||
| @@ -693,9 +631,6 @@ def test_stop_handle(default_conf, update, mocker) -> None: | ||||
|  | ||||
|  | ||||
| def test_stop_handle_already_stopped(default_conf, update, mocker) -> None: | ||||
|     """ | ||||
|     Test _stop() method | ||||
|     """ | ||||
|     patch_coinmarketcap(mocker) | ||||
|     msg_mock = MagicMock() | ||||
|     mocker.patch.multiple( | ||||
| @@ -716,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( | ||||
| @@ -738,9 +672,6 @@ def test_reload_conf_handle(default_conf, update, mocker) -> None: | ||||
|  | ||||
| def test_forcesell_handle(default_conf, update, ticker, fee, | ||||
|                           ticker_sell_up, markets, mocker) -> None: | ||||
|     """ | ||||
|     Test _forcesell() method | ||||
|     """ | ||||
|     patch_coinmarketcap(mocker, value={'price_usd': 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()) | ||||
| @@ -790,9 +721,6 @@ def test_forcesell_handle(default_conf, update, ticker, fee, | ||||
|  | ||||
| def test_forcesell_down_handle(default_conf, update, ticker, fee, | ||||
|                                ticker_sell_down, markets, mocker) -> None: | ||||
|     """ | ||||
|     Test _forcesell() method | ||||
|     """ | ||||
|     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()) | ||||
| @@ -846,9 +774,6 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, | ||||
|  | ||||
|  | ||||
| def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None: | ||||
|     """ | ||||
|     Test _forcesell() method | ||||
|     """ | ||||
|     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()) | ||||
| @@ -894,9 +819,6 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker | ||||
|  | ||||
|  | ||||
| def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: | ||||
|     """ | ||||
|     Test _forcesell() method | ||||
|     """ | ||||
|     patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) | ||||
|     mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) | ||||
|     msg_mock = MagicMock() | ||||
| @@ -937,9 +859,6 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None: | ||||
|  | ||||
| def test_performance_handle(default_conf, update, ticker, fee, | ||||
|                             limit_buy_order, limit_sell_order, markets, mocker) -> None: | ||||
|     """ | ||||
|     Test _performance() method | ||||
|     """ | ||||
|     patch_coinmarketcap(mocker) | ||||
|     msg_mock = MagicMock() | ||||
|     mocker.patch.multiple( | ||||
| @@ -979,9 +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_coinmarketcap(mocker) | ||||
|     msg_mock = MagicMock() | ||||
|     mocker.patch.multiple( | ||||
| @@ -1002,9 +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_coinmarketcap(mocker) | ||||
|     msg_mock = MagicMock() | ||||
|     mocker.patch.multiple( | ||||
| @@ -1046,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( | ||||
| @@ -1066,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( | ||||
| @@ -1266,14 +1173,10 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: | ||||
|  | ||||
|  | ||||
| def test__send_msg(default_conf, mocker) -> None: | ||||
|     """ | ||||
|     Test send_msg() method | ||||
|     """ | ||||
|     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 | ||||
| @@ -1282,15 +1185,11 @@ def test__send_msg(default_conf, mocker) -> None: | ||||
|  | ||||
|  | ||||
| def test__send_msg_network_error(default_conf, mocker, caplog) -> None: | ||||
|     """ | ||||
|     Test send_msg() method | ||||
|     """ | ||||
|     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 | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| # 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 | ||||
| @@ -32,23 +33,12 @@ def get_webhook_dict() -> dict: | ||||
|  | ||||
|  | ||||
| def test__init__(mocker, default_conf): | ||||
|     """ | ||||
|     Test __init__() method | ||||
|     """ | ||||
|     default_conf['webhook'] = {'enabled': True, 'url': "https://DEADBEEF.com"} | ||||
|     webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) | ||||
|     assert webhook._config == default_conf | ||||
|  | ||||
|  | ||||
| def test_cleanup(default_conf, mocker) -> None: | ||||
|     """ | ||||
|     Test cleanup() method - not needed for webhook | ||||
|     """ | ||||
|     pass | ||||
|  | ||||
|  | ||||
| def test_send_msg(default_conf, mocker): | ||||
|     """ Test send_msg for Webhook rpc class""" | ||||
|     default_conf["webhook"] = get_webhook_dict() | ||||
|     msg_mock = MagicMock() | ||||
|     mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock) | ||||
| @@ -118,7 +108,6 @@ def test_send_msg(default_conf, mocker): | ||||
|  | ||||
|  | ||||
| def test_exception_send_msg(default_conf, mocker, caplog): | ||||
|     """Test misconfigured notification""" | ||||
|     default_conf["webhook"] = get_webhook_dict() | ||||
|     default_conf["webhook"]["webhookbuy"] = None | ||||
|  | ||||
| @@ -158,8 +147,6 @@ def test_exception_send_msg(default_conf, mocker, caplog): | ||||
|  | ||||
|  | ||||
| def test__send_msg(default_conf, mocker, caplog): | ||||
|     """Test internal method - calling the actual api""" | ||||
|  | ||||
|     default_conf["webhook"] = get_webhook_dict() | ||||
|     webhook = Webhook(get_patched_freqtradebot(mocker, default_conf)) | ||||
|     msg = {'value1': 'DEADBEEF', | ||||
|   | ||||
							
								
								
									
										235
									
								
								freqtrade/tests/strategy/legacy_strategy.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										235
									
								
								freqtrade/tests/strategy/legacy_strategy.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
| @@ -25,10 +25,11 @@ def test_default_strategy_structure(): | ||||
| def test_default_strategy(result): | ||||
|     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 | ||||
|   | ||||
| @@ -1,9 +1,5 @@ | ||||
| # pragma pylint: disable=missing-docstring, C0103 | ||||
|  | ||||
| """ | ||||
| Unit test file for analyse.py | ||||
| """ | ||||
|  | ||||
| import logging | ||||
| from unittest.mock import MagicMock | ||||
|  | ||||
| @@ -92,7 +88,7 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog): | ||||
|  | ||||
|  | ||||
| def test_get_signal_handles_exceptions(mocker, default_conf): | ||||
|     mocker.patch('freqtrade.exchange.Exchange.get_ticker_history', return_value=MagicMock()) | ||||
|     mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=MagicMock()) | ||||
|     exchange = get_patched_exchange(mocker, default_conf) | ||||
|     mocker.patch.object( | ||||
|         _STRATEGY, 'analyze_ticker', | ||||
| @@ -102,9 +98,6 @@ def test_get_signal_handles_exceptions(mocker, default_conf): | ||||
|  | ||||
|  | ||||
| def test_tickerdata_to_dataframe(default_conf) -> None: | ||||
|     """ | ||||
|     Test Analyze.tickerdata_to_dataframe() method | ||||
|     """ | ||||
|     strategy = DefaultStrategy(default_conf) | ||||
|  | ||||
|     timerange = TimeRange(None, 'line', 0, -100) | ||||
|   | ||||
| @@ -1,8 +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 | ||||
| @@ -37,8 +40,8 @@ def test_import_strategy(caplog): | ||||
|  | ||||
| def test_search_strategy(): | ||||
|     default_config = {} | ||||
|     default_location = os.path.join(os.path.dirname( | ||||
|         os.path.realpath(__file__)), '..', '..', 'strategy' | ||||
|     default_location = path.join(path.dirname( | ||||
|         path.realpath(__file__)), '..', '..', 'strategy' | ||||
|     ) | ||||
|     assert isinstance( | ||||
|         StrategyResolver._search_strategy( | ||||
| @@ -57,13 +60,20 @@ def test_search_strategy(): | ||||
|  | ||||
| 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): | ||||
|     with open("freqtrade/tests/strategy/test_strategy.py", "r") as file: | ||||
|         encoded_string = urlsafe_b64encode(file.read().encode("utf-8")).decode("utf-8") | ||||
|     resolver = StrategyResolver({'strategy': 'TestStrategy:{}'.format(encoded_string)}) | ||||
|     assert 'adx' in resolver.strategy.advise_indicators(result, 'ETH/BTC') | ||||
|  | ||||
|  | ||||
| def test_load_strategy_invalid_directory(result, caplog): | ||||
|     resolver = StrategyResolver() | ||||
|     extra_dir = os.path.join('some', 'path') | ||||
|     extra_dir = path.join('some', 'path') | ||||
|     resolver._load_strategy('TestStrategy', config={}, extra_dir=extra_dir) | ||||
|  | ||||
|     assert ( | ||||
| @@ -72,8 +82,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(): | ||||
| @@ -88,28 +97,23 @@ def test_strategy(result): | ||||
|     config = {'strategy': 'DefaultStrategy'} | ||||
|  | ||||
|     resolver = StrategyResolver(config) | ||||
|  | ||||
|     assert hasattr(resolver.strategy, 'minimal_roi') | ||||
|     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, 'ticker_interval') | ||||
|     assert resolver.strategy.ticker_interval == '5m' | ||||
|     assert config['ticker_interval'] == '5m' | ||||
|  | ||||
|     assert hasattr(resolver.strategy, 'populate_indicators') | ||||
|     assert 'adx' in resolver.strategy.populate_indicators(result) | ||||
|     df_indicators = resolver.strategy.advise_indicators(result, metadata=metadata) | ||||
|     assert 'adx' in df_indicators | ||||
|  | ||||
|     assert hasattr(resolver.strategy, 'populate_buy_trend') | ||||
|     dataframe = resolver.strategy.populate_buy_trend(resolver.strategy.populate_indicators(result)) | ||||
|     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 | ||||
|  | ||||
|  | ||||
| @@ -123,7 +127,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, | ||||
| @@ -139,7 +142,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, | ||||
| @@ -156,9 +158,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 | ||||
|   | ||||
| @@ -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' | ||||
|     ] | ||||
|   | ||||
| @@ -1,9 +1,5 @@ | ||||
| # pragma pylint: disable=missing-docstring, C0103 | ||||
|  | ||||
| """ | ||||
| Unit test file for arguments.py | ||||
| """ | ||||
|  | ||||
| import argparse | ||||
|  | ||||
| import pytest | ||||
| @@ -11,23 +7,11 @@ 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: | ||||
| @@ -148,7 +132,11 @@ def test_parse_args_backtesting_custom() -> None: | ||||
|         'backtesting', | ||||
|         '--live', | ||||
|         '--ticker-interval', '1m', | ||||
|         '--refresh-pairs-cached'] | ||||
|         '--refresh-pairs-cached', | ||||
|         '--strategy-list', | ||||
|         'DefaultStrategy', | ||||
|         'TestStrategy' | ||||
|         ] | ||||
|     call_args = Arguments(args, '').get_parsed_arg() | ||||
|     assert call_args.config == 'test_conf.json' | ||||
|     assert call_args.live is True | ||||
| @@ -157,6 +145,8 @@ def test_parse_args_backtesting_custom() -> None: | ||||
|     assert call_args.func is not None | ||||
|     assert call_args.ticker_interval == '1m' | ||||
|     assert call_args.refresh_pairs is True | ||||
|     assert type(call_args.strategy_list) is list | ||||
|     assert len(call_args.strategy_list) == 2 | ||||
|  | ||||
|  | ||||
| def test_parse_args_hyperopt_custom() -> None: | ||||
|   | ||||
| @@ -1,17 +1,14 @@ | ||||
| # 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, set_loggers | ||||
| @@ -19,59 +16,31 @@ 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) | ||||
|     )) | ||||
| @@ -85,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') | ||||
| @@ -100,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')) | ||||
| @@ -114,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) | ||||
|     )) | ||||
| @@ -131,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', | ||||
| @@ -145,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() | ||||
|  | ||||
| @@ -162,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) | ||||
| @@ -193,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() | ||||
| @@ -214,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', | ||||
| @@ -237,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', | ||||
| @@ -287,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) | ||||
|     )) | ||||
| @@ -354,7 +292,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: | ||||
| def test_setup_configuration_with_stratlist(mocker, default_conf, caplog) -> None: | ||||
|     """ | ||||
|     Test setup_configuration() function | ||||
|     """ | ||||
| @@ -362,12 +300,62 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: | ||||
|         read_data=json.dumps(default_conf) | ||||
|     )) | ||||
|  | ||||
|     arglist = [ | ||||
|         '--config', 'config.json', | ||||
|         'backtesting', | ||||
|         '--ticker-interval', '1m', | ||||
|         '--export', '/bar/foo', | ||||
|         '--strategy-list', | ||||
|         'DefaultStrategy', | ||||
|         'TestStrategy' | ||||
|     ] | ||||
|  | ||||
|     args = Arguments(arglist, '').get_parsed_arg() | ||||
|  | ||||
|     configuration = Configuration(args) | ||||
|     config = configuration.get_config() | ||||
|     assert 'max_open_trades' in config | ||||
|     assert 'stake_currency' in config | ||||
|     assert 'stake_amount' in config | ||||
|     assert 'exchange' in config | ||||
|     assert 'pair_whitelist' in config['exchange'] | ||||
|     assert 'datadir' in config | ||||
|     assert log_has( | ||||
|         'Using data folder: {} ...'.format(config['datadir']), | ||||
|         caplog.record_tuples | ||||
|     ) | ||||
|     assert 'ticker_interval' in config | ||||
|     assert log_has('Parameter -i/--ticker-interval detected ...', caplog.record_tuples) | ||||
|     assert log_has( | ||||
|         'Using ticker_interval: 1m ...', | ||||
|         caplog.record_tuples | ||||
|     ) | ||||
|  | ||||
|     assert 'strategy_list' in config | ||||
|     assert log_has('Using strategy list of 2 Strategies', caplog.record_tuples) | ||||
|  | ||||
|     assert 'position_stacking' not in config | ||||
|  | ||||
|     assert 'use_max_market_positions' not in config | ||||
|  | ||||
|     assert 'timerange' not in config | ||||
|  | ||||
|     assert 'export' in config | ||||
|     assert log_has( | ||||
|         'Parameter --export detected: {} ...'.format(config['export']), | ||||
|         caplog.record_tuples | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None: | ||||
|     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) | ||||
| @@ -384,36 +372,28 @@ 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: | ||||
|     """ | ||||
|     Test Configuration.load_config() with cli params used | ||||
|     """ | ||||
|  | ||||
|     mocker.patch('freqtrade.configuration.open', mocker.mock_open( | ||||
|         read_data=json.dumps(default_conf))) | ||||
|     # Prevent setting loggers | ||||
| @@ -429,9 +409,6 @@ def test_cli_verbose_with_params(default_conf, mocker, caplog) -> None: | ||||
|  | ||||
|  | ||||
| def test_set_loggers() -> None: | ||||
|     """ | ||||
|     Test set_loggers() update the logger level for third-party libraries | ||||
|     """ | ||||
|     # 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) | ||||
| @@ -467,3 +444,7 @@ def test_set_loggers() -> None: | ||||
|     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) | ||||
|   | ||||
| @@ -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) | ||||
| @@ -14,7 +14,7 @@ def load_dataframe_pair(pairs, strategy): | ||||
|     assert isinstance(pairs[0], str) | ||||
|     dataframe = ld[pairs[0]] | ||||
|  | ||||
|     dataframe = strategy.analyze_ticker(dataframe) | ||||
|     dataframe = strategy.analyze_ticker(dataframe, {'pair': pairs[0]}) | ||||
|     return dataframe | ||||
|  | ||||
|  | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,3 +1,5 @@ | ||||
| # pragma pylint: disable=missing-docstring | ||||
|  | ||||
| import pandas as pd | ||||
|  | ||||
| from freqtrade.indicator_helpers import went_down, went_up | ||||
|   | ||||
| @@ -1,6 +1,4 @@ | ||||
| """ | ||||
| Unit test file for main.py | ||||
| """ | ||||
| # pragma pylint: disable=missing-docstring | ||||
|  | ||||
| from copy import deepcopy | ||||
| from unittest.mock import MagicMock | ||||
| @@ -33,9 +31,6 @@ def test_parse_args_backtesting(mocker) -> 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 | ||||
| @@ -47,10 +42,6 @@ def test_main_start_hyperopt(mocker) -> None: | ||||
|  | ||||
|  | ||||
| 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', | ||||
| @@ -74,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', | ||||
| @@ -101,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', | ||||
| @@ -128,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', | ||||
| @@ -158,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', | ||||
|   | ||||
| @@ -1,9 +1,5 @@ | ||||
| # pragma pylint: disable=missing-docstring,C0103 | ||||
|  | ||||
| """ | ||||
| Unit test file for misc.py | ||||
| """ | ||||
|  | ||||
| import datetime | ||||
| from unittest.mock import MagicMock | ||||
|  | ||||
| @@ -15,20 +11,12 @@ 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 = parse_ticker_dataframe(ticker_history) | ||||
|     dates = datesarray_to_datetimearray(dataframes['date']) | ||||
|  | ||||
| @@ -44,10 +32,6 @@ def test_datesarray_to_datetimearray(ticker_history): | ||||
|  | ||||
|  | ||||
| def test_common_datearray(default_conf) -> None: | ||||
|     """ | ||||
|     Test common_datearray() | ||||
|     :return: None | ||||
|     """ | ||||
|     strategy = DefaultStrategy(default_conf) | ||||
|     tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') | ||||
|     tickerlist = {'UNITTEST/BTC': tick} | ||||
| @@ -61,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]) | ||||
| @@ -78,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) | ||||
|   | ||||
| @@ -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://' | ||||
|  | ||||
| @@ -411,6 +404,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog): | ||||
|     Test Database migration (starting with new pairformat) | ||||
|     """ | ||||
|     amount = 103.223 | ||||
|     # Always create all columns apart from the last! | ||||
|     create_table_old = """CREATE TABLE IF NOT EXISTS "trades" ( | ||||
|                                 id INTEGER NOT NULL, | ||||
|                                 exchange VARCHAR NOT NULL, | ||||
| @@ -425,14 +419,21 @@ def test_migrate_new(mocker, default_conf, fee, caplog): | ||||
|                                 open_date DATETIME NOT NULL, | ||||
|                                 close_date DATETIME, | ||||
|                                 open_order_id VARCHAR, | ||||
|                                 stop_loss FLOAT, | ||||
|                                 initial_stop_loss FLOAT, | ||||
|                                 max_rate FLOAT, | ||||
|                                 sell_reason VARCHAR, | ||||
|                                 strategy VARCHAR, | ||||
|                                 PRIMARY KEY (id), | ||||
|                                 CHECK (is_open IN (0, 1)) | ||||
|                                 );""" | ||||
|     insert_table_old = """INSERT INTO trades (exchange, pair, is_open, fee, | ||||
|                           open_rate, stake_amount, amount, open_date) | ||||
|                           open_rate, stake_amount, amount, open_date, | ||||
|                           stop_loss, initial_stop_loss, max_rate) | ||||
|                           VALUES ('binance', 'ETC/BTC', 1, {fee}, | ||||
|                           0.00258580, {stake}, {amount}, | ||||
|                           '2019-11-28 12:44:24.000000') | ||||
|                           '2019-11-28 12:44:24.000000', | ||||
|                           0.0, 0.0, 0.0) | ||||
|                           """.format(fee=fee.return_value, | ||||
|                                      stake=default_conf.get("stake_amount"), | ||||
|                                      amount=amount | ||||
|   | ||||
| @@ -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') | ||||
							
								
								
									
										16
									
								
								freqtrade/tests/test_talib.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								freqtrade/tests/test_talib.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | ||||
|  | ||||
|  | ||||
| import talib.abstract as ta | ||||
| import pandas as pd | ||||
|  | ||||
|  | ||||
| def test_talib_bollingerbands_near_zero_values(): | ||||
|     inputs = pd.DataFrame([ | ||||
|         {'close': 0.00000010}, | ||||
|         {'close': 0.00000011}, | ||||
|         {'close': 0.00000012}, | ||||
|         {'close': 0.00000013}, | ||||
|         {'close': 0.00000014} | ||||
|     ]) | ||||
|     bollinger = ta.BBANDS(inputs, matype=0, timeperiod=2) | ||||
|     assert (bollinger['upperband'][3] != bollinger['middleband'][3]) | ||||
| @@ -1,6 +1,6 @@ | ||||
| if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then | ||||
|   tar zxvf ta-lib-0.4.0-src.tar.gz | ||||
|   cd ta-lib && ./configure && make && sudo make install && cd .. | ||||
|   cd ta-lib && sed -i "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h && ./configure && make && sudo make install && cd .. | ||||
| else | ||||
|   echo "TA-lib already installed, skipping download and build." | ||||
|   cd ta-lib && sudo make install && cd .. | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| ccxt==1.17.29 | ||||
| ccxt==1.17.106 | ||||
| SQLAlchemy==1.2.10 | ||||
| python-telegram-bot==10.1.0 | ||||
| arrow==0.12.1 | ||||
| @@ -6,13 +6,13 @@ cachetools==2.1.0 | ||||
| requests==2.19.1 | ||||
| urllib3==1.22 | ||||
| wrapt==1.10.11 | ||||
| pandas==0.23.3 | ||||
| pandas==0.23.4 | ||||
| scikit-learn==0.19.2 | ||||
| scipy==1.1.0 | ||||
| jsonschema==2.6.0 | ||||
| numpy==1.15.0 | ||||
| TA-Lib==0.4.17 | ||||
| pytest==3.6.3 | ||||
| pytest==3.7.1 | ||||
| pytest-mock==1.10.0 | ||||
| pytest-cov==2.5.1 | ||||
| tabulate==0.8.2 | ||||
|   | ||||
							
								
								
									
										93
									
								
								scripts/get_market_pairs.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								scripts/get_market_pairs.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | ||||
| import os | ||||
| import sys | ||||
|  | ||||
| root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) | ||||
| sys.path.append(root + '/python') | ||||
|  | ||||
| import ccxt  # noqa: E402 | ||||
|  | ||||
|  | ||||
| def style(s, style): | ||||
|     return style + s + '\033[0m' | ||||
|  | ||||
|  | ||||
| def green(s): | ||||
|     return style(s, '\033[92m') | ||||
|  | ||||
|  | ||||
| def blue(s): | ||||
|     return style(s, '\033[94m') | ||||
|  | ||||
|  | ||||
| def yellow(s): | ||||
|     return style(s, '\033[93m') | ||||
|  | ||||
|  | ||||
| def red(s): | ||||
|     return style(s, '\033[91m') | ||||
|  | ||||
|  | ||||
| def pink(s): | ||||
|     return style(s, '\033[95m') | ||||
|  | ||||
|  | ||||
| def bold(s): | ||||
|     return style(s, '\033[1m') | ||||
|  | ||||
|  | ||||
| def underline(s): | ||||
|     return style(s, '\033[4m') | ||||
|  | ||||
|  | ||||
| def dump(*args): | ||||
|     print(' '.join([str(arg) for arg in args])) | ||||
|  | ||||
|  | ||||
| def print_supported_exchanges(): | ||||
|     dump('Supported exchanges:', green(', '.join(ccxt.exchanges))) | ||||
|  | ||||
|  | ||||
| try: | ||||
|  | ||||
|     id = sys.argv[1]  # get exchange id from command line arguments | ||||
|  | ||||
|  | ||||
|     # check if the exchange is supported by ccxt | ||||
|     exchange_found = id in ccxt.exchanges | ||||
|  | ||||
|     if exchange_found: | ||||
|         dump('Instantiating', green(id), 'exchange') | ||||
|  | ||||
|         # instantiate the exchange by id | ||||
|         exchange = getattr(ccxt, id)({ | ||||
|             # 'proxy':'https://cors-anywhere.herokuapp.com/', | ||||
|         }) | ||||
|  | ||||
|         # load all markets from the exchange | ||||
|         markets = exchange.load_markets() | ||||
|  | ||||
|         # output a list of all market symbols | ||||
|         dump(green(id), 'has', len(exchange.symbols), 'symbols:', exchange.symbols) | ||||
|  | ||||
|         tuples = list(ccxt.Exchange.keysort(markets).items()) | ||||
|  | ||||
|         # debug | ||||
|         for (k, v) in tuples: | ||||
|             print(v) | ||||
|  | ||||
|         # output a table of all markets | ||||
|         dump(pink('{:<15} {:<15} {:<15} {:<15}'.format('id', 'symbol', 'base', 'quote'))) | ||||
|  | ||||
|         for (k, v) in tuples: | ||||
|             dump('{:<15} {:<15} {:<15} {:<15}'.format(v['id'], v['symbol'], v['base'], v['quote'])) | ||||
|  | ||||
|     else: | ||||
|  | ||||
|         dump('Exchange ' + red(id) + ' not found') | ||||
|         print_supported_exchanges() | ||||
|  | ||||
| except Exception as e: | ||||
|     dump('[' + type(e).__name__ + ']', str(e)) | ||||
|     dump("Usage: python " + sys.argv[0], green('id')) | ||||
|     print_supported_exchanges() | ||||
|  | ||||
| @@ -138,7 +138,7 @@ def plot_analyzed_dataframe(args: Namespace) -> None: | ||||
|     tickers = {} | ||||
|     if args.live: | ||||
|         logger.info('Downloading pair.') | ||||
|         tickers[pair] = exchange.get_ticker_history(pair, tick_interval) | ||||
|         tickers[pair] = exchange.get_candle_history(pair, tick_interval) | ||||
|     else: | ||||
|         tickers = optimize.load_data( | ||||
|             datadir=_CONF.get("datadir"), | ||||
| @@ -159,8 +159,8 @@ def plot_analyzed_dataframe(args: Namespace) -> None: | ||||
|     dataframes = strategy.tickerdata_to_dataframe(tickers) | ||||
|  | ||||
|     dataframe = dataframes[pair] | ||||
|     dataframe = strategy.populate_buy_trend(dataframe) | ||||
|     dataframe = strategy.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 ' | ||||
|   | ||||
							
								
								
									
										2
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								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', | ||||
|   | ||||
| @@ -18,6 +18,7 @@ class TestStrategy(IStrategy): | ||||
|     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 | ||||
| @@ -44,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 | ||||
| @@ -211,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[ | ||||
| @@ -227,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[ | ||||
|   | ||||
		Reference in New Issue
	
	Block a user