diff --git a/Dockerfile b/Dockerfile index d2c2b1b22..e959b9296 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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/ diff --git a/README.md b/README.md index b5715f743..02b870209 100644 --- a/README.md +++ b/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/"` 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 diff --git a/config.json.example b/config.json.example index e8473e919..8bd3942e6 100644 --- a/config.json.example +++ b/config.json.example @@ -17,6 +17,7 @@ "name": "bittrex", "key": "your_exchange_key", "secret": "your_exchange_secret", + "ccxt_rate_limit": true, "pair_whitelist": [ "ETH/BTC", "LTC/BTC", diff --git a/config_full.json.example b/config_full.json.example index 4003b1c5c..cc3b3d630 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -7,6 +7,7 @@ "ticker_interval": "5m", "trailing_stop": false, "trailing_stop_positive": 0.005, + "trailing_stop_positive_offset": 0.0051, "minimal_roi": { "40": 0.0, "30": 0.01, @@ -25,6 +26,7 @@ "name": "bittrex", "key": "your_exchange_key", "secret": "your_exchange_secret", + "ccxt_rate_limit": true, "pair_whitelist": [ "ETH/BTC", "LTC/BTC", diff --git a/docs/backtesting.md b/docs/backtesting.md index 766875970..cc8ecd6c7 100644 --- a/docs/backtesting.md +++ b/docs/backtesting.md @@ -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-.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 diff --git a/docs/bot-optimization.md b/docs/bot-optimization.md index d7b628fd4..0214ce5c5 100644 --- a/docs/bot-optimization.md +++ b/docs/bot-optimization.md @@ -39,7 +39,6 @@ A strategy file contains all the information needed to build a good strategy: - Sell strategy rules - Minimal ROI recommended - Stoploss recommended -- Hyperopt parameter The bot also include a sample strategy called `TestStrategy` you can update: `user_data/strategies/test_strategy.py`. You can test it with the parameter: `--strategy TestStrategy` @@ -61,22 +60,22 @@ file as reference.** ### Buy strategy -Edit the method `populate_buy_trend()` into your strategy file to -update your buy strategy. +Edit the method `populate_buy_trend()` into your strategy file to update your buy strategy. Sample from `user_data/strategies/test_strategy.py`: ```python -def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: +def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the buy signal for the given dataframe - :param dataframe: DataFrame + :param dataframe: DataFrame populated with indicators + :param metadata: Additional information, like the currently traded pair :return: DataFrame with buy column """ dataframe.loc[ ( (dataframe['adx'] > 30) & - (dataframe['tema'] <= dataframe['blower']) & + (dataframe['tema'] <= dataframe['bb_middleband']) & (dataframe['tema'] > dataframe['tema'].shift(1)) ), 'buy'] = 1 @@ -87,38 +86,47 @@ def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: ### Sell strategy Edit the method `populate_sell_trend()` into your strategy file to update your sell strategy. +Please note that the sell-signal is only used if `use_sell_signal` is set to true in the configuration. Sample from `user_data/strategies/test_strategy.py`: ```python -def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: +def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the sell signal for the given dataframe - :param dataframe: DataFrame + :param dataframe: DataFrame populated with indicators + :param metadata: Additional information, like the currently traded pair :return: DataFrame with buy column """ dataframe.loc[ ( (dataframe['adx'] > 70) & - (dataframe['tema'] > dataframe['blower']) & + (dataframe['tema'] > dataframe['bb_middleband']) & (dataframe['tema'] < dataframe['tema'].shift(1)) ), 'sell'] = 1 return dataframe ``` -## Add more Indicator +## Add more Indicators -As you have seen, buy and sell strategies need indicators. You can add -more indicators by extending the list contained in -the method `populate_indicators()` from your strategy file. +As you have seen, buy and sell strategies need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file. + +You should only add the indicators used in either `populate_buy_trend()`, `populate_sell_trend()`, or to populate another indicator, otherwise performance may suffer. Sample: ```python -def populate_indicators(dataframe: DataFrame) -> DataFrame: +def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Adds several different TA indicators to the given DataFrame + + Performance Note: For the best performance be frugal on the number of indicators + you are using. Let uncomment only the indicator you are using in your strategies + or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() + :param metadata: Additional information, like the currently traded pair + :return: a Dataframe with all mandatory indicators for the strategies """ dataframe['sar'] = ta.SAR(dataframe) dataframe['adx'] = ta.ADX(dataframe) @@ -149,6 +157,11 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame: return dataframe ``` +### Metadata dict + +The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `populate_indicators`) contains additional information. +Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`. + ### Want more indicator examples Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py). diff --git a/docs/bot-usage.md b/docs/bot-usage.md index 4e479adac..83a8ee833 100644 --- a/docs/bot-usage.md +++ b/docs/bot-usage.md @@ -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). diff --git a/docs/configuration.md b/docs/configuration.md index ddfa9834e..ff5ce118c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -27,6 +27,7 @@ The table below will list all configuration parameters. | `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file. | `trailing_stoploss` | false | No | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file). | `trailing_stoploss_positve` | 0 | No | Changes stop-loss once profit has been reached. +| `trailing_stoploss_positve_offset` | 0 | No | Offset on when to apply `trailing_stoploss_positive`. Percentage value which should be positive. | `unfilledtimeout.buy` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. | `unfilledtimeout.sell` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. | `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below. @@ -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). diff --git a/docs/index.md b/docs/index.md index f76bb125d..730f1095e 100644 --- a/docs/index.md +++ b/docs/index.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)) diff --git a/docs/installation.md b/docs/installation.md index e5724a7dc..4de05c121 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -56,23 +56,29 @@ Reset parameter will hard reset your branch (only if you are on `master` or `dev Config parameter is a `config.json` configurator. This script will ask you questions to setup your bot and create your `config.json`. - ## Manual installation - Linux/MacOS + The following steps are made for Linux/MacOS environment -**1. Clone the repo** +### 1. Clone the repo + ```bash git clone git@github.com:freqtrade/freqtrade.git git checkout develop cd freqtrade ``` -**2. Create the config file** + +### 2. Create the config file + Switch `"dry_run": true,` + ```bash cp config.json.example config.json vi config.json ``` -**3. Build your docker image and run it** + +### 3. Build your docker image and run it + ```bash docker build -t freqtrade . docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade @@ -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 diff --git a/docs/sandbox-testing.md b/docs/sandbox-testing.md new file mode 100644 index 000000000..572fbccef --- /dev/null +++ b/docs/sandbox-testing.md @@ -0,0 +1,151 @@ +# Sandbox API testing +Where an exchange provides a sandbox for risk-free integration, or end-to-end, testing CCXT provides access to these. + +This document is a *light overview of configuring Freqtrade and GDAX sandbox. +This can be useful to developers and trader alike as Freqtrade is quite customisable. + +When testing your API connectivity, make sure to use the following URLs. +***Website** +https://public.sandbox.gdax.com +***REST API** +https://api-public.sandbox.gdax.com + +--- +# Configure a Sandbox account on Gdax +Aim of this document section +- An sanbox account +- create 2FA (needed to create an API) +- Add test 50BTC to account +- Create : +- - API-KEY +- - API-Secret +- - API Password + +## Acccount + +This link will redirect to the sandbox main page to login / create account dialogues: +https://public.sandbox.pro.coinbase.com/orders/ + +After registration and Email confimation you wil be redirected into your sanbox account. It is easy to verify you're in sandbox by checking the URL bar. +> https://public.sandbox.pro.coinbase.com/ + +## Enable 2Fa (a prerequisite to creating sandbox API Keys) +From within sand box site select your profile, top right. +>Or as a direct link: https://public.sandbox.pro.coinbase.com/profile + +From the menu panel to the left of the screen select +> Security: "*View or Update*" + +In the new site select "enable authenticator" as typical google Authenticator. +- open Google Authenticator on your phone +- scan barcode +- enter your generated 2fa + +## Enable API Access +From within sandbox select profile>api>create api-keys +>or as a direct link: https://public.sandbox.pro.coinbase.com/profile/api + +Click on "create one" and ensure **view** and **trade** are "checked" and sumbit your 2Fa +- **Copy and paste the Passphase** into a notepade this will be needed later +- **Copy and paste the API Secret** popup into a notepad this will needed later +- **Copy and paste the API Key** into a notepad this will needed later + +## Add 50 BTC test funds +To add funds, use the web interface deposit and withdraw buttons. + + +To begin select 'Wallets' from the top menu. +> Or as a direct link: https://public.sandbox.pro.coinbase.com/wallets + +- Deposits (bottom left of screen) +- - Deposit Funds Bitcoin +- - - Coinbase BTC Wallet +- - - - Max (50 BTC) +- - - - - Deposit + +*This process may be repeated for other currencies, ETH as example* +--- +# Configure Freqtrade to use Gax Sandbox + +The aim of this document section + - Enable sandbox URLs in Freqtrade + - Configure API + - - secret + - - key + - - passphrase + +## Sandbox URLs +Freqtrade makes use of CCXT which in turn provides a list of URLs to Freqtrade. +These include `['test']` and `['api']`. +- `[Test]` if available will point to an Exchanges sandbox. +- `[Api]` normally used, and resolves to live API target on the exchange + +To make use of sandbox / test add "sandbox": true, to your config.json +``` + "exchange": { + "name": "gdax", + "sandbox": true, + "key": "5wowfxemogxeowo;heiohgmd", + "secret": "/ZMH1P62rCVmwefewrgcewX8nh4gob+lywxfwfxwwfxwfNsH1ySgvWCUR/w==", + "password": "1bkjfkhfhfu6sr", + "pair_whitelist": [ + "BTC/USD" +``` +Also insert your +- api-key (noted earlier) +- api-secret (noted earlier) +- password (the passphrase - noted earlier) + +--- +## You should now be ready to test your sandbox! +Ensure Freqtrade logs show the sandbox URL, and trades made are shown in sandbox. +** Typically the BTC/USD has the most activity in sandbox to test against. + +## GDAX - Old Candles problem +It is my experience that GDAX sandbox candles may be 20+- minutes out of date. This can cause trades to fail as one of Freqtrades safety checks + +To disable this check, edit: +>strategy/interface.py +Look for the following section: +``` + # Check if dataframe is out of date + signal_date = arrow.get(latest['date']) + interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval] + if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5))): + logger.warning( + 'Outdated history for pair %s. Last tick is %s minutes old', + pair, + (arrow.utcnow() - signal_date).seconds // 60 + ) + return False, False +``` + +You could Hash out the entire check as follows: +``` + # # Check if dataframe is out of date + # signal_date = arrow.get(latest['date']) + # interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval] + # if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5))): + # logger.warning( + # 'Outdated history for pair %s. Last tick is %s minutes old', + # pair, + # (arrow.utcnow() - signal_date).seconds // 60 + # ) + # return False, False + ``` + + Or inrease the timeout to offer a level of protection/alignment of this test to freqtrade in live. + + As example, to allow an additional 30 minutes. "(interval_minutes * 2 + 5 + 30)" + ``` + # Check if dataframe is out of date + signal_date = arrow.get(latest['date']) + interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval] + if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5 + 30))): + logger.warning( + 'Outdated history for pair %s. Last tick is %s minutes old', + pair, + (arrow.utcnow() - signal_date).seconds // 60 + ) + return False, False +``` \ No newline at end of file diff --git a/docs/stoploss.md b/docs/stoploss.md index db4433a02..9740672cd 100644 --- a/docs/stoploss.md +++ b/docs/stoploss.md @@ -35,14 +35,17 @@ basically what this means is that your stop loss will be adjusted to be always b ### Custom positive loss -Due to demand, it is possible to have a default stop loss, when you are in the red with your buy, but once your buy turns positive, -the system will utilize a new stop loss, which can be a different value. For example your default stop loss is 5%, but once you are in the -black, it will be changed to be only a 1% stop loss +Due to demand, it is possible to have a default stop loss, when you are in the red with your buy, but once your profit surpasses a certain percentage, +the system will utilize a new stop loss, which can be a different value. For example your default stop loss is 5%, but once you have 1.1% profit, +it will be changed to be only a 1% stop loss, which trails the green candles until it goes below them. -This can be configured in the main configuration file and requires `"trailing_stop": true` to be set to true. +Both values can be configured in the main configuration file and requires `"trailing_stop": true` to be set to true. ``` json "trailing_stop_positive": 0.01, + "trailing_stop_positive_offset": 0.011, ``` -The 0.01 would translate to a 1% stop loss, once you hit profit. +The 0.01 would translate to a 1% stop loss, once you hit 1.1% profit. + +You should also make sure to have this value higher than your minimal ROI, otherwise minimal ROI will apply first and sell your trade. diff --git a/freqtrade/arguments.py b/freqtrade/arguments.py index 022a2c739..501c1784f 100644 --- a/freqtrade/arguments.py +++ b/freqtrade/arguments.py @@ -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\ diff --git a/freqtrade/configuration.py b/freqtrade/configuration.py index dcc6e4332..3da432b1d 100644 --- a/freqtrade/configuration.py +++ b/freqtrade/configuration.py @@ -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}) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index c27cd875c..b30add71b 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -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': { diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 972ff49ca..a6ec70636 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -4,6 +4,7 @@ import logging from random import randint from typing import List, Dict, Any, Optional from datetime import datetime +from math import floor, ceil import ccxt import arrow @@ -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] diff --git a/freqtrade/exchange/exchange_helpers.py b/freqtrade/exchange/exchange_helpers.py index 254c16309..46f04328c 100644 --- a/freqtrade/exchange/exchange_helpers.py +++ b/freqtrade/exchange/exchange_helpers.py @@ -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'] diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 46fbb3a38..706435017 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -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) diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index a2259e19c..5591898fa 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -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) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 78cbe6d33..9e68318f7 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -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]: diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py index 59cc0f296..086cad5aa 100644 --- a/freqtrade/optimize/hyperopt.py +++ b/freqtrade/optimize/hyperopt.py @@ -75,7 +75,7 @@ class Hyperopt(Backtesting): return arg_dict @staticmethod - def populate_indicators(dataframe: DataFrame) -> DataFrame: + def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['adx'] = ta.ADX(dataframe) macd = ta.MACD(dataframe) dataframe['macd'] = macd['macd'] @@ -228,7 +228,7 @@ class Hyperopt(Backtesting): """ Define the buy strategy parameters to be used by hyperopt """ - def populate_buy_trend(dataframe: DataFrame) -> DataFrame: + def populate_buy_trend(dataframe: DataFrame, metadata: dict) -> DataFrame: """ Buy strategy Hyperopt will build and use """ @@ -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): diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 8fb01d074..80d49b895 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -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) diff --git a/freqtrade/strategy/default_strategy.py b/freqtrade/strategy/default_strategy.py index 22689f17a..f1646779b 100644 --- a/freqtrade/strategy/default_strategy.py +++ b/freqtrade/strategy/default_strategy.py @@ -28,13 +28,16 @@ class DefaultStrategy(IStrategy): # Optimal ticker interval for the strategy ticker_interval = '5m' - def populate_indicators(self, dataframe: DataFrame) -> DataFrame: + def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Adds several different TA indicators to the given DataFrame Performance Note: For the best performance be frugal on the number of indicators you are using. Let uncomment only the indicator you are using in your strategies or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + :param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe() + :param metadata: Additional information, like the currently traded pair + :return: a Dataframe with all mandatory indicators for the strategies """ # Momentum Indicator @@ -196,10 +199,11 @@ class DefaultStrategy(IStrategy): return dataframe - def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the buy signal for the given dataframe :param dataframe: DataFrame + :param metadata: Additional information, like the currently traded pair :return: DataFrame with buy column """ dataframe.loc[ @@ -217,10 +221,11 @@ class DefaultStrategy(IStrategy): return dataframe - def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: + def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ Based on TA indicators, populates the sell signal for the given dataframe :param dataframe: DataFrame + :param metadata: Additional information, like the currently traded pair :return: DataFrame with buy column """ dataframe.loc[ diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py index a5145aabb..dfd624393 100644 --- a/freqtrade/strategy/interface.py +++ b/freqtrade/strategy/interface.py @@ -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) diff --git a/freqtrade/strategy/resolver.py b/freqtrade/strategy/resolver.py index 3360cd44a..7aeec300e 100644 --- a/freqtrade/strategy/resolver.py +++ b/freqtrade/strategy/resolver.py @@ -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) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 8d0809367..d18016e16 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -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 diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index 282d8ef01..6918e9da1 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -1,7 +1,6 @@ # pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement # pragma pylint: disable=protected-access import logging -from copy import deepcopy from datetime import datetime from random import randint from unittest.mock import MagicMock, PropertyMock @@ -15,8 +14,6 @@ from freqtrade.tests.conftest import get_patched_exchange, log_has def ccxt_exceptionhandlers(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs): - """Function to test ccxt exception handling """ - with pytest.raises(TemporaryError): api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError) exchange = get_patched_exchange(mocker, default_conf, api_mock) @@ -52,6 +49,93 @@ def test_init_exception(default_conf, mocker): Exchange(default_conf) +def test_symbol_amount_prec(default_conf, mocker): + ''' + Test rounds down to 4 Decimal places + ''' + api_mock = MagicMock() + api_mock.load_markets = MagicMock(return_value={ + 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' + }) + mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='binance')) + + markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'amount': 4}}}) + type(api_mock).markets = markets + + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) + exchange = Exchange(default_conf) + + amount = 2.34559 + pair = 'ETH/BTC' + amount = exchange.symbol_amount_prec(pair, amount) + assert amount == 2.3455 + + +def test_symbol_price_prec(default_conf, mocker): + ''' + Test rounds up to 4 decimal places + ''' + api_mock = MagicMock() + api_mock.load_markets = MagicMock(return_value={ + 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' + }) + mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='binance')) + + markets = PropertyMock(return_value={'ETH/BTC': {'precision': {'price': 4}}}) + type(api_mock).markets = markets + + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) + exchange = Exchange(default_conf) + + price = 2.34559 + pair = 'ETH/BTC' + price = exchange.symbol_price_prec(pair, price) + assert price == 2.3456 + + +def test_set_sandbox(default_conf, mocker): + """ + Test working scenario + """ + api_mock = MagicMock() + api_mock.load_markets = MagicMock(return_value={ + 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' + }) + url_mock = PropertyMock(return_value={'test': "api-public.sandbox.gdax.com", + 'api': 'https://api.gdax.com'}) + type(api_mock).urls = url_mock + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) + + exchange = Exchange(default_conf) + liveurl = exchange._api.urls['api'] + default_conf['exchange']['sandbox'] = True + exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname') + assert exchange._api.urls['api'] != liveurl + + +def test_set_sandbox_exception(default_conf, mocker): + """ + Test Fail scenario + """ + api_mock = MagicMock() + api_mock.load_markets = MagicMock(return_value={ + 'ETH/BTC': '', 'LTC/BTC': '', 'XRP/BTC': '', 'NEO/BTC': '' + }) + url_mock = PropertyMock(return_value={'api': 'https://api.gdax.com'}) + type(api_mock).urls = url_mock + + mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock)) + mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock()) + + with pytest.raises(OperationalException, match=r'does not provide a sandbox api'): + exchange = Exchange(default_conf) + default_conf['exchange']['sandbox'] = True + exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname') + + def test_validate_pairs(default_conf, mocker): api_mock = MagicMock() api_mock.load_markets = MagicMock(return_value={ @@ -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 diff --git a/freqtrade/tests/exchange/test_exchange_helpers.py b/freqtrade/tests/exchange/test_exchange_helpers.py index 6a3bc9eb6..82525e805 100644 --- a/freqtrade/tests/exchange/test_exchange_helpers.py +++ b/freqtrade/tests/exchange/test_exchange_helpers.py @@ -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 diff --git a/freqtrade/tests/optimize/test_backtesting.py b/freqtrade/tests/optimize/test_backtesting.py index 836c7c302..32a5229c0 100644 --- a/freqtrade/tests/optimize/test_backtesting.py +++ b/freqtrade/tests/optimize/test_backtesting.py @@ -3,7 +3,6 @@ import json import math import random -from copy import deepcopy from typing import List from unittest.mock import MagicMock @@ -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) diff --git a/freqtrade/tests/optimize/test_hyperopt.py b/freqtrade/tests/optimize/test_hyperopt.py index 72a102c22..65a3c2fdb 100644 --- a/freqtrade/tests/optimize/test_hyperopt.py +++ b/freqtrade/tests/optimize/test_hyperopt.py @@ -1,6 +1,5 @@ # pragma pylint: disable=missing-docstring,W0212,C0103 import os -from copy import deepcopy from unittest.mock import MagicMock import pandas as pd @@ -12,29 +11,22 @@ from freqtrade.strategy.resolver import StrategyResolver from freqtrade.tests.conftest import log_has, patch_exchange from freqtrade.tests.optimize.test_backtesting import get_args -# Avoid to reinit the same object again and again -_HYPEROPT_INITIALIZED = False -_HYPEROPT = None - @pytest.fixture(scope='function') -def init_hyperopt(default_conf, mocker): - global _HYPEROPT_INITIALIZED, _HYPEROPT - if not _HYPEROPT_INITIALIZED: - patch_exchange(mocker) - _HYPEROPT = Hyperopt(default_conf) - _HYPEROPT_INITIALIZED = True +def hyperopt(default_conf, mocker): + patch_exchange(mocker) + return Hyperopt(default_conf) # Functions for recurrent object patching -def create_trials(mocker) -> None: +def create_trials(mocker, hyperopt) -> None: """ When creating trials, mock the hyperopt Trials so that *by default* - we don't create any pickle'd files in the filesystem - we might have a pickle'd file so make sure that we return false when looking for it """ - _HYPEROPT.trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle') + hyperopt.trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle') mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=False) mocker.patch('freqtrade.optimize.hyperopt.os.path.getsize', return_value=1) @@ -45,9 +37,6 @@ def create_trials(mocker) -> None: def test_start(mocker, default_conf, caplog) -> None: - """ - Test start() function - """ start_mock = MagicMock() mocker.patch( 'freqtrade.configuration.Configuration._load_config_file', @@ -76,11 +65,7 @@ def test_start(mocker, default_conf, caplog) -> None: assert start_mock.call_count == 1 -def test_loss_calculation_prefer_correct_trade_count(init_hyperopt) -> None: - """ - Test Hyperopt.calculate_loss() - """ - hyperopt = _HYPEROPT +def test_loss_calculation_prefer_correct_trade_count(hyperopt) -> None: StrategyResolver({'strategy': 'DefaultStrategy'}) correct = hyperopt.calculate_loss(1, hyperopt.target_trades, 20) @@ -90,20 +75,13 @@ def test_loss_calculation_prefer_correct_trade_count(init_hyperopt) -> None: assert under > correct -def test_loss_calculation_prefer_shorter_trades(init_hyperopt) -> None: - """ - Test Hyperopt.calculate_loss() - """ - hyperopt = _HYPEROPT - +def test_loss_calculation_prefer_shorter_trades(hyperopt) -> None: shorter = hyperopt.calculate_loss(1, 100, 20) longer = hyperopt.calculate_loss(1, 100, 30) assert shorter < longer -def test_loss_calculation_has_limited_profit(init_hyperopt) -> None: - hyperopt = _HYPEROPT - +def test_loss_calculation_has_limited_profit(hyperopt) -> None: correct = hyperopt.calculate_loss(hyperopt.expected_max_profit, hyperopt.target_trades, 20) over = hyperopt.calculate_loss(hyperopt.expected_max_profit * 2, hyperopt.target_trades, 20) under = hyperopt.calculate_loss(hyperopt.expected_max_profit / 2, hyperopt.target_trades, 20) @@ -111,8 +89,7 @@ def test_loss_calculation_has_limited_profit(init_hyperopt) -> None: assert under > correct -def test_log_results_if_loss_improves(init_hyperopt, capsys) -> None: - hyperopt = _HYPEROPT +def test_log_results_if_loss_improves(hyperopt, capsys) -> None: hyperopt.current_best_loss = 2 hyperopt.log_results( { @@ -123,11 +100,10 @@ def test_log_results_if_loss_improves(init_hyperopt, capsys) -> None: } ) out, err = capsys.readouterr() - assert ' 1/2: foo. Loss 1.00000'in out + assert ' 1/2: foo. Loss 1.00000' in out -def test_no_log_if_loss_does_not_improve(init_hyperopt, caplog) -> None: - hyperopt = _HYPEROPT +def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None: hyperopt.current_best_loss = 2 hyperopt.log_results( { @@ -137,13 +113,10 @@ def test_no_log_if_loss_does_not_improve(init_hyperopt, caplog) -> None: assert caplog.record_tuples == [] -def test_save_trials_saves_trials(mocker, init_hyperopt, caplog) -> None: - trials = create_trials(mocker) +def test_save_trials_saves_trials(mocker, hyperopt, caplog) -> None: + trials = create_trials(mocker, hyperopt) mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None) - - hyperopt = _HYPEROPT - _HYPEROPT.trials = trials - + hyperopt.trials = trials hyperopt.save_trials() trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle') @@ -154,11 +127,9 @@ def test_save_trials_saves_trials(mocker, init_hyperopt, caplog) -> None: mock_dump.assert_called_once() -def test_read_trials_returns_trials_file(mocker, init_hyperopt, caplog) -> None: - trials = create_trials(mocker) +def test_read_trials_returns_trials_file(mocker, hyperopt, caplog) -> None: + trials = create_trials(mocker, hyperopt) mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=trials) - - hyperopt = _HYPEROPT hyperopt_trial = hyperopt.read_trials() trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle') assert log_has( @@ -169,7 +140,7 @@ def test_read_trials_returns_trials_file(mocker, init_hyperopt, caplog) -> None: mock_load.assert_called_once() -def test_roi_table_generation(init_hyperopt) -> None: +def test_roi_table_generation(hyperopt) -> None: params = { 'roi_t1': 5, 'roi_t2': 10, @@ -179,11 +150,10 @@ def test_roi_table_generation(init_hyperopt) -> None: 'roi_p3': 3, } - hyperopt = _HYPEROPT assert hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0} -def test_start_calls_optimizer(mocker, init_hyperopt, default_conf, caplog) -> None: +def test_start_calls_optimizer(mocker, default_conf, caplog) -> None: dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.multiprocessing.cpu_count', MagicMock(return_value=1)) @@ -193,13 +163,12 @@ def test_start_calls_optimizer(mocker, init_hyperopt, default_conf, caplog) -> N ) patch_exchange(mocker) - conf = deepcopy(default_conf) - conf.update({'config': 'config.json.example'}) - conf.update({'epochs': 1}) - conf.update({'timerange': None}) - conf.update({'spaces': 'all'}) + default_conf.update({'config': 'config.json.example'}) + default_conf.update({'epochs': 1}) + default_conf.update({'timerange': None}) + default_conf.update({'spaces': 'all'}) - hyperopt = Hyperopt(conf) + hyperopt = Hyperopt(default_conf) hyperopt.tickerdata_to_dataframe = MagicMock() hyperopt.start() @@ -209,11 +178,7 @@ def test_start_calls_optimizer(mocker, init_hyperopt, default_conf, caplog) -> N assert dumper.called -def test_format_results(init_hyperopt): - """ - Test Hyperopt.format_results() - """ - +def test_format_results(hyperopt): # Test with BTC as stake_currency trades = [ ('ETH/BTC', 2, 2, 123), @@ -223,7 +188,7 @@ def test_format_results(init_hyperopt): labels = ['currency', 'profit_percent', 'profit_abs', 'trade_duration'] df = pd.DataFrame.from_records(trades, columns=labels) - result = _HYPEROPT.format_results(df) + result = hyperopt.format_results(df) assert result.find(' 66.67%') assert result.find('Total profit 1.00000000 BTC') assert result.find('2.0000Σ %') @@ -235,31 +200,25 @@ def test_format_results(init_hyperopt): ('XPR/EUR', -1, -2, -246) ] df = pd.DataFrame.from_records(trades, columns=labels) - result = _HYPEROPT.format_results(df) + result = hyperopt.format_results(df) assert result.find('Total profit 1.00000000 EUR') -def test_has_space(init_hyperopt): - """ - Test Hyperopt.has_space() method - """ - _HYPEROPT.config.update({'spaces': ['buy', 'roi']}) - assert _HYPEROPT.has_space('roi') - assert _HYPEROPT.has_space('buy') - assert not _HYPEROPT.has_space('stoploss') +def test_has_space(hyperopt): + hyperopt.config.update({'spaces': ['buy', 'roi']}) + assert hyperopt.has_space('roi') + assert hyperopt.has_space('buy') + assert not hyperopt.has_space('stoploss') - _HYPEROPT.config.update({'spaces': ['all']}) - assert _HYPEROPT.has_space('buy') + hyperopt.config.update({'spaces': ['all']}) + assert hyperopt.has_space('buy') -def test_populate_indicators(init_hyperopt) -> None: - """ - Test Hyperopt.populate_indicators() - """ +def test_populate_indicators(hyperopt) -> None: tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') tickerlist = {'UNITTEST/BTC': tick} - dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist) - dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC']) + dataframes = hyperopt.tickerdata_to_dataframe(tickerlist) + dataframe = hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'}) # Check if some indicators are generated. We will not test all of them assert 'adx' in dataframe @@ -267,16 +226,13 @@ def test_populate_indicators(init_hyperopt) -> None: assert 'rsi' in dataframe -def test_buy_strategy_generator(init_hyperopt) -> None: - """ - Test Hyperopt.buy_strategy_generator() - """ +def test_buy_strategy_generator(hyperopt) -> None: tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m') tickerlist = {'UNITTEST/BTC': tick} - dataframes = _HYPEROPT.tickerdata_to_dataframe(tickerlist) - dataframe = _HYPEROPT.populate_indicators(dataframes['UNITTEST/BTC']) + dataframes = hyperopt.tickerdata_to_dataframe(tickerlist) + dataframe = hyperopt.populate_indicators(dataframes['UNITTEST/BTC'], {'pair': 'UNITTEST/BTC'}) - populate_buy_trend = _HYPEROPT.buy_strategy_generator( + populate_buy_trend = hyperopt.buy_strategy_generator( { 'adx-value': 20, 'fastd-value': 20, @@ -289,20 +245,16 @@ def test_buy_strategy_generator(init_hyperopt) -> None: 'trigger': 'bb_lower' } ) - result = populate_buy_trend(dataframe) + result = populate_buy_trend(dataframe, {'pair': 'UNITTEST/BTC'}) # Check if some indicators are generated. We will not test all of them assert 'buy' in result assert 1 in result['buy'] -def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None: - """ - Test Hyperopt.generate_optimizer() function - """ - conf = deepcopy(default_conf) - conf.update({'config': 'config.json.example'}) - conf.update({'timerange': None}) - conf.update({'spaces': 'all'}) +def test_generate_optimizer(mocker, default_conf) -> None: + default_conf.update({'config': 'config.json.example'}) + default_conf.update({'timerange': None}) + default_conf.update({'spaces': 'all'}) trades = [ ('POWR/BTC', 0.023117, 0.000233, 100) @@ -335,7 +287,6 @@ def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None: 'roi_p3': 0.1, 'stoploss': -0.4, } - response_expected = { 'loss': 1.9840569076926293, 'result': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC ' @@ -343,6 +294,6 @@ def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None: 'params': optimizer_param } - hyperopt = Hyperopt(conf) + hyperopt = Hyperopt(default_conf) generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values())) assert generate_optimizer_value == response_expected diff --git a/freqtrade/tests/optimize/test_optimize.py b/freqtrade/tests/optimize/test_optimize.py index 4ab32343a..13f65fbf5 100644 --- a/freqtrade/tests/optimize/test_optimize.py +++ b/freqtrade/tests/optimize/test_optimize.py @@ -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'} diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index e3530a149..70b7dcfd9 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -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( diff --git a/freqtrade/tests/rpc/test_rpc_manager.py b/freqtrade/tests/rpc/test_rpc_manager.py index 4686cf5ca..90c693830 100644 --- a/freqtrade/tests/rpc/test_rpc_manager.py +++ b/freqtrade/tests/rpc/test_rpc_manager.py @@ -1,50 +1,31 @@ -""" -Unit test file for rpc/rpc_manager.py -""" +# pragma pylint: disable=missing-docstring, C0103 import logging -from copy import deepcopy from unittest.mock import MagicMock from freqtrade.rpc 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] diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index b2cab6d37..4b2fe4cf5 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -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 diff --git a/freqtrade/tests/rpc/test_rpc_webhook.py b/freqtrade/tests/rpc/test_rpc_webhook.py index b9c005d73..bfe0416b0 100644 --- a/freqtrade/tests/rpc/test_rpc_webhook.py +++ b/freqtrade/tests/rpc/test_rpc_webhook.py @@ -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', diff --git a/freqtrade/tests/strategy/legacy_strategy.py b/freqtrade/tests/strategy/legacy_strategy.py new file mode 100644 index 000000000..2cd13b791 --- /dev/null +++ b/freqtrade/tests/strategy/legacy_strategy.py @@ -0,0 +1,235 @@ + +# --- Do not remove these libs --- +from freqtrade.strategy.interface import IStrategy +from pandas import DataFrame +# -------------------------------- + +# Add your lib to import here +import talib.abstract as ta +import freqtrade.vendor.qtpylib.indicators as qtpylib +import numpy # noqa + + +# This class is a sample. Feel free to customize it. +class TestStrategyLegacy(IStrategy): + """ + This is a test strategy using the legacy function headers, which will be + removed in a future update. + Please do not use this as a template, but refer to user_data/strategy/TestStrategy.py + for a uptodate version of this template. + + """ + + # Minimal ROI designed for the strategy. + # This attribute will be overridden if the config file contains "minimal_roi" + minimal_roi = { + "40": 0.0, + "30": 0.01, + "20": 0.02, + "0": 0.04 + } + + # Optimal stoploss designed for the strategy + # This attribute will be overridden if the config file contains "stoploss" + stoploss = -0.10 + + # Optimal ticker interval for the strategy + ticker_interval = '5m' + + def populate_indicators(self, dataframe: DataFrame) -> DataFrame: + """ + Adds several different TA indicators to the given DataFrame + + Performance Note: For the best performance be frugal on the number of indicators + you are using. Let uncomment only the indicator you are using in your strategies + or your hyperopt configuration, otherwise you will waste your memory and CPU usage. + """ + + # Momentum Indicator + # ------------------------------------ + + # ADX + dataframe['adx'] = ta.ADX(dataframe) + + """ + # Awesome oscillator + dataframe['ao'] = qtpylib.awesome_oscillator(dataframe) + + # Commodity Channel Index: values Oversold:<-100, Overbought:>100 + dataframe['cci'] = ta.CCI(dataframe) + + # MACD + macd = ta.MACD(dataframe) + dataframe['macd'] = macd['macd'] + dataframe['macdsignal'] = macd['macdsignal'] + dataframe['macdhist'] = macd['macdhist'] + + # MFI + dataframe['mfi'] = ta.MFI(dataframe) + + # Minus Directional Indicator / Movement + dataframe['minus_dm'] = ta.MINUS_DM(dataframe) + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # Plus Directional Indicator / Movement + dataframe['plus_dm'] = ta.PLUS_DM(dataframe) + dataframe['plus_di'] = ta.PLUS_DI(dataframe) + dataframe['minus_di'] = ta.MINUS_DI(dataframe) + + # ROC + dataframe['roc'] = ta.ROC(dataframe) + + # RSI + dataframe['rsi'] = ta.RSI(dataframe) + + # Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy) + rsi = 0.1 * (dataframe['rsi'] - 50) + dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1) + + # Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy) + dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1) + + # Stoch + stoch = ta.STOCH(dataframe) + dataframe['slowd'] = stoch['slowd'] + dataframe['slowk'] = stoch['slowk'] + + # Stoch fast + stoch_fast = ta.STOCHF(dataframe) + dataframe['fastd'] = stoch_fast['fastd'] + dataframe['fastk'] = stoch_fast['fastk'] + + # Stoch RSI + stoch_rsi = ta.STOCHRSI(dataframe) + dataframe['fastd_rsi'] = stoch_rsi['fastd'] + dataframe['fastk_rsi'] = stoch_rsi['fastk'] + """ + + # Overlap Studies + # ------------------------------------ + + # Bollinger bands + bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) + dataframe['bb_lowerband'] = bollinger['lower'] + dataframe['bb_middleband'] = bollinger['mid'] + dataframe['bb_upperband'] = bollinger['upper'] + + """ + # EMA - Exponential Moving Average + dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3) + dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5) + dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) + dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50) + dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100) + + # SAR Parabol + dataframe['sar'] = ta.SAR(dataframe) + + # SMA - Simple Moving Average + dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) + """ + + # TEMA - Triple Exponential Moving Average + dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9) + + # Cycle Indicator + # ------------------------------------ + # Hilbert Transform Indicator - SineWave + hilbert = ta.HT_SINE(dataframe) + dataframe['htsine'] = hilbert['sine'] + dataframe['htleadsine'] = hilbert['leadsine'] + + # Pattern Recognition - Bullish candlestick patterns + # ------------------------------------ + """ + # Hammer: values [0, 100] + dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe) + # Inverted Hammer: values [0, 100] + dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe) + # Dragonfly Doji: values [0, 100] + dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe) + # Piercing Line: values [0, 100] + dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100] + # Morningstar: values [0, 100] + dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100] + # Three White Soldiers: values [0, 100] + dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100] + """ + + # Pattern Recognition - Bearish candlestick patterns + # ------------------------------------ + """ + # Hanging Man: values [0, 100] + dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe) + # Shooting Star: values [0, 100] + dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe) + # Gravestone Doji: values [0, 100] + dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe) + # Dark Cloud Cover: values [0, 100] + dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe) + # Evening Doji Star: values [0, 100] + dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe) + # Evening Star: values [0, 100] + dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe) + """ + + # Pattern Recognition - Bullish/Bearish candlestick patterns + # ------------------------------------ + """ + # Three Line Strike: values [0, -100, 100] + dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe) + # Spinning Top: values [0, -100, 100] + dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100] + # Engulfing: values [0, -100, 100] + dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100] + # Harami: values [0, -100, 100] + dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100] + # Three Outside Up/Down: values [0, -100, 100] + dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100] + # Three Inside Up/Down: values [0, -100, 100] + dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100] + """ + + # Chart type + # ------------------------------------ + """ + # Heikinashi stategy + heikinashi = qtpylib.heikinashi(dataframe) + dataframe['ha_open'] = heikinashi['open'] + dataframe['ha_close'] = heikinashi['close'] + dataframe['ha_high'] = heikinashi['high'] + dataframe['ha_low'] = heikinashi['low'] + """ + + return dataframe + + def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the buy signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + (dataframe['adx'] > 30) & + (dataframe['tema'] <= dataframe['bb_middleband']) & + (dataframe['tema'] > dataframe['tema'].shift(1)) + ), + 'buy'] = 1 + + return dataframe + + def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame: + """ + Based on TA indicators, populates the sell signal for the given dataframe + :param dataframe: DataFrame + :return: DataFrame with buy column + """ + dataframe.loc[ + ( + (dataframe['adx'] > 70) & + (dataframe['tema'] > dataframe['bb_middleband']) & + (dataframe['tema'] < dataframe['tema'].shift(1)) + ), + 'sell'] = 1 + return dataframe diff --git a/freqtrade/tests/strategy/test_default_strategy.py b/freqtrade/tests/strategy/test_default_strategy.py index 37df1748f..6acfc439f 100644 --- a/freqtrade/tests/strategy/test_default_strategy.py +++ b/freqtrade/tests/strategy/test_default_strategy.py @@ -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 diff --git a/freqtrade/tests/strategy/test_interface.py b/freqtrade/tests/strategy/test_interface.py index de11a9d2c..ec4ab0fd4 100644 --- a/freqtrade/tests/strategy/test_interface.py +++ b/freqtrade/tests/strategy/test_interface.py @@ -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) diff --git a/freqtrade/tests/strategy/test_strategy.py b/freqtrade/tests/strategy/test_strategy.py index 2f9221467..0cbd9f22c 100644 --- a/freqtrade/tests/strategy/test_strategy.py +++ b/freqtrade/tests/strategy/test_strategy.py @@ -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 diff --git a/freqtrade/tests/test_acl_pair.py b/freqtrade/tests/test_acl_pair.py index 094c166b8..535684b22 100644 --- a/freqtrade/tests/test_acl_pair.py +++ b/freqtrade/tests/test_acl_pair.py @@ -11,7 +11,6 @@ import freqtrade.tests.conftest as tt # test tools def whitelist_conf(): config = tt.default_conf() - config['stake_currency'] = 'BTC' config['exchange']['pair_whitelist'] = [ 'ETH/BTC', @@ -20,7 +19,6 @@ def whitelist_conf(): 'SWT/BTC', 'BCC/BTC' ] - config['exchange']['pair_blacklist'] = [ 'BLK/BTC' ] diff --git a/freqtrade/tests/test_arguments.py b/freqtrade/tests/test_arguments.py index 07018c79e..e09aeb1df 100644 --- a/freqtrade/tests/test_arguments.py +++ b/freqtrade/tests/test_arguments.py @@ -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: diff --git a/freqtrade/tests/test_configuration.py b/freqtrade/tests/test_configuration.py index a8a2c5fce..bf41aab83 100644 --- a/freqtrade/tests/test_configuration.py +++ b/freqtrade/tests/test_configuration.py @@ -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) diff --git a/freqtrade/tests/test_constants.py b/freqtrade/tests/test_constants.py deleted file mode 100644 index 541c6e533..000000000 --- a/freqtrade/tests/test_constants.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -Unit test file for constants.py -""" - -from freqtrade import constants - - -def test_constant_object() -> None: - """ - Test the Constants object has the mandatory Constants - """ - assert hasattr(constants, 'CONF_SCHEMA') - assert hasattr(constants, 'DYNAMIC_WHITELIST') - assert hasattr(constants, 'PROCESS_THROTTLE_SECS') - assert hasattr(constants, 'TICKER_INTERVAL') - assert hasattr(constants, 'HYPEROPT_EPOCH') - assert hasattr(constants, 'RETRY_TIMEOUT') - assert hasattr(constants, 'DEFAULT_STRATEGY') - - -def test_conf_schema() -> None: - """ - Test the CONF_SCHEMA is from the right type - """ - assert isinstance(constants.CONF_SCHEMA, dict) diff --git a/freqtrade/tests/test_dataframe.py b/freqtrade/tests/test_dataframe.py index 019587af1..dc030d630 100644 --- a/freqtrade/tests/test_dataframe.py +++ b/freqtrade/tests/test_dataframe.py @@ -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 diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index fb6687a2c..89adae6ab 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -1,9 +1,6 @@ +# pragma pylint: disable=missing-docstring, C0103 # pragma pylint: disable=protected-access, too-many-lines, invalid-name, too-many-arguments -""" -Unit test file for freqtradebot.py -""" - import logging import re import time @@ -21,7 +18,7 @@ from freqtrade.persistence import Trade from freqtrade.rpc import RPCMessageType from freqtrade.state import State from freqtrade.strategy.interface import SellType, SellCheckTuple -from freqtrade.tests.conftest import log_has, patch_coinmarketcap, patch_exchange +from freqtrade.tests.conftest import log_has, patch_exchange # Functions for recurrent object patching @@ -35,7 +32,6 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot: mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.persistence.init', MagicMock()) patch_exchange(mocker) - patch_coinmarketcap(mocker) return FreqtradeBot(config) @@ -47,7 +43,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None: :return: None """ freqtrade.strategy.get_signal = lambda e, s, t: value - freqtrade.exchange.get_ticker_history = lambda p, i: None + freqtrade.exchange.get_candle_history = lambda p, i: None def patch_RPCManager(mocker) -> MagicMock: @@ -62,40 +58,17 @@ def patch_RPCManager(mocker) -> MagicMock: # Unit tests -def test_freqtradebot_object() -> None: - """ - Test the FreqtradeBot object has the mandatory public methods - """ - assert hasattr(FreqtradeBot, 'worker') - assert hasattr(FreqtradeBot, 'cleanup') - assert hasattr(FreqtradeBot, 'create_trade') - assert hasattr(FreqtradeBot, 'get_target_bid') - assert hasattr(FreqtradeBot, 'process_maybe_execute_buy') - assert hasattr(FreqtradeBot, 'process_maybe_execute_sell') - assert hasattr(FreqtradeBot, 'handle_trade') - assert hasattr(FreqtradeBot, 'check_handle_timedout') - assert hasattr(FreqtradeBot, 'handle_timedout_limit_buy') - assert hasattr(FreqtradeBot, 'handle_timedout_limit_sell') - assert hasattr(FreqtradeBot, 'execute_sell') - def test_freqtradebot(mocker, default_conf) -> None: - """ - Test __init__, _init_modules, update_state, and get_state methods - """ freqtrade = get_patched_freqtradebot(mocker, default_conf) assert freqtrade.state is State.RUNNING - conf = deepcopy(default_conf) - conf.pop('initial_state') - freqtrade = FreqtradeBot(conf) + default_conf.pop('initial_state') + freqtrade = FreqtradeBot(default_conf) assert freqtrade.state is State.STOPPED def test_cleanup(mocker, default_conf, caplog) -> None: - """ - Test clean() method - """ mock_cleanup = MagicMock() mocker.patch('freqtrade.persistence.cleanup', mock_cleanup) @@ -106,9 +79,6 @@ def test_cleanup(mocker, default_conf, caplog) -> None: def test_worker_running(mocker, default_conf, caplog) -> None: - """ - Test worker() method. Test when we start the bot - """ mock_throttle = MagicMock() mocker.patch('freqtrade.freqtradebot.FreqtradeBot._throttle', mock_throttle) @@ -121,9 +91,6 @@ def test_worker_running(mocker, default_conf, caplog) -> None: def test_worker_stopped(mocker, default_conf, caplog) -> None: - """ - Test worker() method. Test when we stop the bot - """ mock_throttle = MagicMock() mocker.patch('freqtrade.freqtradebot.FreqtradeBot._throttle', mock_throttle) mock_sleep = mocker.patch('time.sleep', return_value=None) @@ -138,53 +105,38 @@ def test_worker_stopped(mocker, default_conf, caplog) -> None: def test_throttle(mocker, default_conf, caplog) -> None: - """ - Test _throttle() method - """ - def func(): - """ - Test function to throttle - """ + def throttled_func(): return 42 caplog.set_level(logging.DEBUG) freqtrade = get_patched_freqtradebot(mocker, default_conf) start = time.time() - result = freqtrade._throttle(func, min_secs=0.1) + result = freqtrade._throttle(throttled_func, min_secs=0.1) end = time.time() assert result == 42 assert end - start > 0.1 - assert log_has('Throttling func for 0.10 seconds', caplog.record_tuples) + assert log_has('Throttling throttled_func for 0.10 seconds', caplog.record_tuples) - result = freqtrade._throttle(func, min_secs=-1) + result = freqtrade._throttle(throttled_func, min_secs=-1) assert result == 42 def test_throttle_with_assets(mocker, default_conf) -> None: - """ - Test _throttle() method when the function passed can have parameters - """ - def func(nb_assets=-1): - """ - Test function to throttle - """ + def throttled_func(nb_assets=-1): return nb_assets freqtrade = get_patched_freqtradebot(mocker, default_conf) - result = freqtrade._throttle(func, min_secs=0.1, nb_assets=666) + result = freqtrade._throttle(throttled_func, min_secs=0.1, nb_assets=666) assert result == 666 - result = freqtrade._throttle(func, min_secs=0.1) + result = freqtrade._throttle(throttled_func, min_secs=0.1) assert result == -1 def test_gen_pair_whitelist(mocker, default_conf, tickers) -> None: - """ - Test _gen_pair_whitelist() method - """ freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers) mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True)) @@ -209,17 +161,10 @@ def test_gen_pair_whitelist(mocker, default_conf, tickers) -> None: @pytest.mark.skip(reason="Test not implemented") def test_refresh_whitelist() -> None: - """ - Test _refresh_whitelist() method - """ pass def test_get_trade_stake_amount(default_conf, ticker, limit_buy_order, fee, mocker) -> None: - """ - Test get_trade_stake_amount() method - """ - patch_RPCManager(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -230,7 +175,7 @@ def test_get_trade_stake_amount(default_conf, ticker, limit_buy_order, fee, mock freqtrade = FreqtradeBot(default_conf) result = freqtrade._get_trade_stake_amount() - assert(result == default_conf['stake_amount']) + assert result == default_conf['stake_amount'] def test_get_trade_stake_amount_no_stake_amount(default_conf, @@ -238,17 +183,12 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, limit_buy_order, fee, mocker) -> None: - """ - Test get_trade_stake_amount() method - """ patch_RPCManager(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5) ) - - # test defined stake amount freqtrade = FreqtradeBot(default_conf) with pytest.raises(DependencyException, match=r'.*stake amount.*'): @@ -261,11 +201,7 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, fee, markets, mocker) -> None: - """ - Test get_trade_stake_amount() method - """ patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -307,10 +243,6 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf, def test_get_min_pair_stake_amount(mocker, default_conf) -> None: - """ - Test get_trade_stake_amount() method - """ - patch_RPCManager(mocker) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock()) freqtrade = FreqtradeBot(default_conf) @@ -446,11 +378,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None: def test_create_trade(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: - """ - Test create_trade() method - """ patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -484,11 +412,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, markets, mocke def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: - """ - Test create_trade() method - """ patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -507,11 +431,7 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: - """ - Test create_trade() method - """ patch_RPCManager(mocker) - patch_coinmarketcap(mocker) buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -521,24 +441,18 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, get_fee=fee, get_markets=markets ) - - conf = deepcopy(default_conf) - conf['stake_amount'] = 0.0005 - freqtrade = FreqtradeBot(conf) + default_conf['stake_amount'] = 0.0005 + freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.create_trade() rate, amount = buy_mock.call_args[0][1], buy_mock.call_args[0][2] - assert rate * amount >= conf['stake_amount'] + assert rate * amount >= default_conf['stake_amount'] def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: - """ - Test create_trade() method - """ patch_RPCManager(mocker) - patch_coinmarketcap(mocker) buy_mock = MagicMock(return_value={'id': limit_buy_order['id']}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -549,9 +463,8 @@ def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_ord get_markets=markets ) - conf = deepcopy(default_conf) - conf['stake_amount'] = 0.000000005 - freqtrade = FreqtradeBot(conf) + default_conf['stake_amount'] = 0.000000005 + freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) result = freqtrade.create_trade() @@ -560,11 +473,7 @@ def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_ord def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: - """ - Test create_trade() method - """ patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -574,11 +483,10 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order, get_fee=fee, get_markets=markets ) - conf = deepcopy(default_conf) - conf['max_open_trades'] = 0 - conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT + default_conf['max_open_trades'] = 0 + default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT - freqtrade = FreqtradeBot(conf) + freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) assert freqtrade.create_trade() is False @@ -586,11 +494,7 @@ def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order, def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: - """ - Test create_trade() method - """ patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -600,10 +504,9 @@ def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, marke get_markets=markets ) - conf = deepcopy(default_conf) - conf['exchange']['pair_whitelist'] = ["ETH/BTC"] - conf['exchange']['pair_blacklist'] = ["ETH/BTC"] - freqtrade = FreqtradeBot(conf) + default_conf['exchange']['pair_whitelist'] = ["ETH/BTC"] + default_conf['exchange']['pair_blacklist'] = ["ETH/BTC"] + freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.create_trade() @@ -614,11 +517,7 @@ def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, marke def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: - """ - Test create_trade() method - """ patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -627,11 +526,9 @@ def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, get_fee=fee, get_markets=markets ) - - conf = deepcopy(default_conf) - conf['exchange']['pair_whitelist'] = ["ETH/BTC"] - conf['exchange']['pair_blacklist'] = ["ETH/BTC"] - freqtrade = FreqtradeBot(conf) + default_conf['exchange']['pair_whitelist'] = ["ETH/BTC"] + default_conf['exchange']['pair_blacklist'] = ["ETH/BTC"] + freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.create_trade() @@ -641,25 +538,18 @@ def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, def test_create_trade_no_signal(default_conf, fee, mocker) -> None: - """ - Test create_trade() method - """ - conf = deepcopy(default_conf) - conf['dry_run'] = True + default_conf['dry_run'] = True patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), - get_ticker_history=MagicMock(return_value=20), + get_candle_history=MagicMock(return_value=20), get_balance=MagicMock(return_value=20), get_fee=fee, ) - - conf = deepcopy(default_conf) - conf['stake_amount'] = 10 - freqtrade = FreqtradeBot(conf) + default_conf['stake_amount'] = 10 + freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade, value=(False, False)) Trade.query = MagicMock() @@ -669,11 +559,7 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None: def test_process_trade_creation(default_conf, ticker, limit_buy_order, markets, fee, mocker, caplog) -> None: - """ - Test the trade creation in _process() method - """ patch_RPCManager(mocker) - patch_coinmarketcap(mocker, value={'price_usd': 12345.0}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -710,11 +596,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> None: - """ - Test _process() method when a RequestException happens - """ patch_RPCManager(mocker) - patch_coinmarketcap(mocker, value={'price_usd': 12345.0}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -733,11 +615,7 @@ def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> Non def test_process_operational_exception(default_conf, ticker, markets, mocker) -> None: - """ - Test _process() method when an OperationalException happens - """ msg_mock = patch_RPCManager(mocker) - patch_coinmarketcap(mocker, value={'price_usd': 12345.0}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -758,11 +636,7 @@ def test_process_operational_exception(default_conf, ticker, markets, mocker) -> def test_process_trade_handling( default_conf, ticker, limit_buy_order, markets, fee, mocker) -> None: - """ - Test _process() - """ patch_RPCManager(mocker) - patch_coinmarketcap(mocker, value={'price_usd': 12345.0}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -787,9 +661,6 @@ def test_process_trade_handling( def test_balance_fully_ask_side(mocker, default_conf) -> None: - """ - Test get_target_bid() method - """ default_conf['bid_strategy']['ask_last_balance'] = 0.0 freqtrade = get_patched_freqtradebot(mocker, default_conf) @@ -797,9 +668,6 @@ def test_balance_fully_ask_side(mocker, default_conf) -> None: def test_balance_fully_last_side(mocker, default_conf) -> None: - """ - Test get_target_bid() method - """ default_conf['bid_strategy']['ask_last_balance'] = 1.0 freqtrade = get_patched_freqtradebot(mocker, default_conf) @@ -807,9 +675,6 @@ def test_balance_fully_last_side(mocker, default_conf) -> None: def test_balance_bigger_last_ask(mocker, default_conf) -> None: - """ - Test get_target_bid() method - """ default_conf['bid_strategy']['ask_last_balance'] = 1.0 freqtrade = get_patched_freqtradebot(mocker, default_conf) @@ -817,9 +682,6 @@ def test_balance_bigger_last_ask(mocker, default_conf) -> None: def test_process_maybe_execute_buy(mocker, default_conf) -> None: - """ - Test process_maybe_execute_buy() method - """ freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.create_trade', MagicMock(return_value=True)) @@ -830,9 +692,6 @@ def test_process_maybe_execute_buy(mocker, default_conf) -> None: def test_process_maybe_execute_buy_exception(mocker, default_conf, caplog) -> None: - """ - Test exception on process_maybe_execute_buy() method - """ freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch( @@ -844,9 +703,6 @@ def test_process_maybe_execute_buy_exception(mocker, default_conf, caplog) -> No def test_process_maybe_execute_sell(mocker, default_conf, limit_buy_order, caplog) -> None: - """ - Test process_maybe_execute_sell() method - """ freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.freqtradebot.FreqtradeBot.handle_trade', MagicMock(return_value=True)) @@ -880,9 +736,6 @@ def test_process_maybe_execute_sell(mocker, default_conf, limit_buy_order, caplo def test_process_maybe_execute_sell_exception(mocker, default_conf, limit_buy_order, caplog) -> None: - """ - Test the exceptions in process_maybe_execute_sell() - """ freqtrade = get_patched_freqtradebot(mocker, default_conf) mocker.patch('freqtrade.exchange.Exchange.get_order', return_value=limit_buy_order) @@ -909,9 +762,6 @@ def test_process_maybe_execute_sell_exception(mocker, default_conf, def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, markets, mocker) -> None: - """ - Test check_handle() method - """ patch_RPCManager(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -926,8 +776,6 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, get_fee=fee, get_markets=markets ) - patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) - freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) @@ -955,14 +803,9 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None: - """ - Test check_handle() method - """ - conf = deepcopy(default_conf) - conf.update({'experimental': {'use_sell_signal': True}}) + default_conf.update({'experimental': {'use_sell_signal': True}}) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -972,7 +815,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, get_markets=markets ) - freqtrade = FreqtradeBot(conf) + freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade, value=(True, True)) freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False @@ -1015,15 +858,10 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, def test_handle_trade_roi(default_conf, ticker, limit_buy_order, fee, mocker, markets, caplog) -> None: - """ - Test check_handle() method - """ caplog.set_level(logging.DEBUG) - conf = deepcopy(default_conf) - conf.update({'experimental': {'use_sell_signal': True}}) + default_conf.update({'experimental': {'use_sell_signal': True}}) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1033,7 +871,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, get_markets=markets ) - freqtrade = FreqtradeBot(conf) + freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade, value=(True, False)) freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: True @@ -1054,15 +892,9 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, def test_handle_trade_experimental( default_conf, ticker, limit_buy_order, fee, mocker, markets, caplog) -> None: - """ - Test check_handle() method - """ caplog.set_level(logging.DEBUG) - conf = deepcopy(default_conf) - conf.update({'experimental': {'use_sell_signal': True}}) - + default_conf.update({'experimental': {'use_sell_signal': True}}) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1072,7 +904,7 @@ def test_handle_trade_experimental( get_markets=markets ) - freqtrade = FreqtradeBot(conf) + freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False freqtrade.create_trade() @@ -1090,11 +922,7 @@ def test_handle_trade_experimental( def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, fee, markets, mocker) -> None: - """ - Test check_handle() method - """ patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1121,12 +949,8 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, fee, mocker) -> None: - """ - Test check_handle_timedout() method - """ rpc_mock = patch_RPCManager(mocker) cancel_order_mock = MagicMock() - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1162,11 +986,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, fe def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker) -> None: - """ - Test check_handle_timedout() method - """ rpc_mock = patch_RPCManager(mocker) - patch_coinmarketcap(mocker) cancel_order_mock = MagicMock() mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -1202,11 +1022,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old_partial, mocker) -> None: - """ - Test check_handle_timedout() method - """ rpc_mock = patch_RPCManager(mocker) - patch_coinmarketcap(mocker) cancel_order_mock = MagicMock() mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -1244,12 +1060,8 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) -> None: - """ - Test check_handle_timedout() method when get_order throw an exception - """ patch_RPCManager(mocker) cancel_order_mock = MagicMock() - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.freqtradebot.FreqtradeBot', @@ -1290,11 +1102,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) - def test_handle_timedout_limit_buy(mocker, default_conf) -> None: - """ - Test handle_timedout_limit_buy() method - """ patch_RPCManager(mocker) - patch_coinmarketcap(mocker) cancel_order_mock = MagicMock() mocker.patch.multiple( 'freqtrade.exchange.Exchange', @@ -1316,12 +1124,8 @@ def test_handle_timedout_limit_buy(mocker, default_conf) -> None: def test_handle_timedout_limit_sell(mocker, default_conf) -> None: - """ - Test handle_timedout_limit_sell() method - """ patch_RPCManager(mocker) cancel_order_mock = MagicMock() - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1342,11 +1146,7 @@ def test_handle_timedout_limit_sell(mocker, default_conf) -> None: def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, mocker) -> None: - """ - Test execute_sell() method with a ticker going UP - """ rpc_mock = patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1392,11 +1192,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, mocker) -> None: - """ - Test execute_sell() method with a ticker going DOWN - """ rpc_mock = patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1444,11 +1240,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, mocker) -> None: - """ - Test execute_sell() method with a ticker going DOWN and with a bot config empty - """ rpc_mock = patch_RPCManager(mocker) - patch_coinmarketcap(mocker, value={'price_usd': 12345.0}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1494,11 +1286,7 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee, def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, mocker) -> None: - """ - Test execute_sell() method with a ticker going DOWN and with a bot config empty - """ rpc_mock = patch_RPCManager(mocker) - patch_coinmarketcap(mocker, value={'price_usd': 12345.0}) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1545,11 +1333,7 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee, def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, fee, markets, mocker) -> None: - """ - Test sell_profit_only feature when enabled - """ patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1562,12 +1346,11 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, get_fee=fee, get_markets=markets ) - conf = deepcopy(default_conf) - conf['experimental'] = { + default_conf['experimental'] = { 'use_sell_signal': True, 'sell_profit_only': True, } - freqtrade = FreqtradeBot(conf) + freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False @@ -1582,11 +1365,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, fee, markets, mocker) -> None: - """ - Test sell_profit_only feature when disabled - """ patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1599,12 +1378,11 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, get_fee=fee, get_markets=markets ) - conf = deepcopy(default_conf) - conf['experimental'] = { + default_conf['experimental'] = { 'use_sell_signal': True, 'sell_profit_only': False, } - freqtrade = FreqtradeBot(conf) + freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False freqtrade.create_trade() @@ -1617,11 +1395,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, markets, mocker) -> None: - """ - Test sell_profit_only feature when enabled and we have a loss - """ patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1634,12 +1408,11 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, market get_fee=fee, get_markets=markets ) - conf = deepcopy(default_conf) - conf['experimental'] = { + default_conf['experimental'] = { 'use_sell_signal': True, 'sell_profit_only': True, } - freqtrade = FreqtradeBot(conf) + freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.stop_loss_reached = \ lambda current_rate, trade, current_time, current_profit: SellCheckTuple( @@ -1653,11 +1426,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, market def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, markets, mocker) -> None: - """ - Test sell_profit_only feature when enabled and we have a loss - """ patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1670,14 +1439,12 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, marke get_fee=fee, get_markets=markets ) - - conf = deepcopy(default_conf) - conf['experimental'] = { + default_conf['experimental'] = { 'use_sell_signal': True, 'sell_profit_only': False, } - freqtrade = FreqtradeBot(conf) + freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False @@ -1691,11 +1458,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, marke def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, mocker) -> None: - """ - Test sell_profit_only feature when enabled and we have a loss - """ patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1708,13 +1471,10 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, m get_fee=fee, get_markets=markets ) - - conf = deepcopy(default_conf) - conf['experimental'] = { + default_conf['experimental'] = { 'ignore_roi_if_buy_signal': True } - - freqtrade = FreqtradeBot(conf) + freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: True @@ -1732,11 +1492,7 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, m def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog, mocker) -> None: - """ - Test sell_profit_only feature when enabled and we have a loss - """ patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1749,11 +1505,8 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog, get_fee=fee, get_markets=markets, ) - - conf = deepcopy(default_conf) - conf['trailing_stop'] = True - print(limit_buy_order) - freqtrade = FreqtradeBot(conf) + default_conf['trailing_stop'] = True + freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False @@ -1772,12 +1525,8 @@ def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog, def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, markets, caplog, mocker) -> None: - """ - Test sell_profit_only feature when enabled and we have a loss - """ buy_price = limit_buy_order['price'] patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1790,11 +1539,9 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, markets get_fee=fee, get_markets=markets, ) - - conf = deepcopy(default_conf) - conf['trailing_stop'] = True - conf['trailing_stop_positive'] = 0.01 - freqtrade = FreqtradeBot(conf) + default_conf['trailing_stop'] = True + default_conf['trailing_stop_positive'] = 0.01 + freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False freqtrade.create_trade() @@ -1814,7 +1561,68 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, markets })) # stop-loss not reached, adjusted stoploss assert freqtrade.handle_trade(trade) is False - assert log_has(f'using positive stop loss mode: 0.01 since we have profit 0.26662643', + assert log_has(f'using positive stop loss mode: 0.01 with offset 0 ' + f'since we have profit 0.2666%', + caplog.record_tuples) + assert log_has(f'adjusted stop loss', caplog.record_tuples) + assert trade.stop_loss == 0.0000138501 + + mocker.patch('freqtrade.exchange.Exchange.get_ticker', + MagicMock(return_value={ + 'bid': buy_price + 0.000002, + 'ask': buy_price + 0.000002, + 'last': buy_price + 0.000002 + })) + # Lower price again (but still positive) + assert freqtrade.handle_trade(trade) is True + assert log_has( + f'HIT STOP: current price at {buy_price + 0.000002:.6f}, ' + f'stop loss is {trade.stop_loss:.6f}, ' + f'initial stop loss was at 0.000010, trade opened at 0.000011', caplog.record_tuples) + + +def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee, + caplog, mocker, markets) -> None: + buy_price = limit_buy_order['price'] + patch_RPCManager(mocker) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + validate_pairs=MagicMock(), + get_ticker=MagicMock(return_value={ + 'bid': buy_price - 0.000001, + 'ask': buy_price - 0.000001, + 'last': buy_price - 0.000001 + }), + buy=MagicMock(return_value={'id': limit_buy_order['id']}), + get_fee=fee, + get_markets=markets, + ) + + default_conf['trailing_stop'] = True + default_conf['trailing_stop_positive'] = 0.01 + default_conf['trailing_stop_positive_offset'] = 0.011 + freqtrade = FreqtradeBot(default_conf) + patch_get_signal(freqtrade) + freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: False + freqtrade.create_trade() + + trade = Trade.query.first() + trade.update(limit_buy_order) + caplog.set_level(logging.DEBUG) + # stop-loss not reached + assert freqtrade.handle_trade(trade) is False + + # Raise ticker above buy price + mocker.patch('freqtrade.exchange.Exchange.get_ticker', + MagicMock(return_value={ + 'bid': buy_price + 0.000003, + 'ask': buy_price + 0.000003, + 'last': buy_price + 0.000003 + })) + # stop-loss not reached, adjusted stoploss + assert freqtrade.handle_trade(trade) is False + assert log_has(f'using positive stop loss mode: 0.01 with offset 0.011 ' + f'since we have profit 0.2666%', caplog.record_tuples) assert log_has(f'adjusted stop loss', caplog.record_tuples) assert trade.stop_loss == 0.0000138501 @@ -1836,11 +1644,7 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, markets def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, mocker) -> None: - """ - Test sell_profit_only feature when enabled and we have a loss - """ patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch.multiple( 'freqtrade.exchange.Exchange', validate_pairs=MagicMock(), @@ -1853,13 +1657,10 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, get_fee=fee, get_markets=markets ) - - conf = deepcopy(default_conf) - conf['experimental'] = { + default_conf['experimental'] = { 'ignore_roi_if_buy_signal': False } - - freqtrade = FreqtradeBot(conf) + freqtrade = FreqtradeBot(default_conf) patch_get_signal(freqtrade) freqtrade.strategy.min_roi_reached = lambda trade, current_profit, current_time: True @@ -1878,14 +1679,8 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, caplog, mocker): - """ - Test get_real_amount - fee in quote currency - """ - mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) - patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) amount = sum(x['amount'] for x in trades_for_order) trade = Trade( @@ -1906,14 +1701,9 @@ def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, ca def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker): - """ - Test get_real_amount - fee in quote currency - """ - mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) amount = buy_order_fee['amount'] trade = Trade( @@ -1934,13 +1724,9 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker): def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, mocker): - """ - Test get_real_amount - fees in Stake currency - """ trades_for_order[0]['fee']['currency'] = 'ETH' patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) amount = sum(x['amount'] for x in trades_for_order) @@ -1959,15 +1745,10 @@ def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, mo def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, mocker): - """ - Test get_real_amount - Fees in BNB - """ - trades_for_order[0]['fee']['currency'] = 'BNB' trades_for_order[0]['fee']['cost'] = 0.00094518 patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) amount = sum(x['amount'] for x in trades_for_order) @@ -1986,12 +1767,7 @@ def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, mock def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, caplog, mocker): - """ - Test get_real_amount with split trades (multiple trades for this order) - """ - patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order2) amount = float(sum(x['amount'] for x in trades_for_order2)) @@ -2013,14 +1789,10 @@ def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, c def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee, caplog, mocker): - """ - Test get_real_amount with split trades (multiple trades for this order) - """ limit_buy_order = deepcopy(buy_order_fee) limit_buy_order['fee'] = {'cost': 0.004, 'currency': 'LTC'} patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[trades_for_order]) @@ -2043,14 +1815,10 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order_fee, mocker): - """ - Test get_real_amount with split trades (multiple trades for this order) - """ limit_buy_order = deepcopy(buy_order_fee) limit_buy_order['fee'] = {'cost': 0.004} patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[]) amount = float(sum(x['amount'] for x in trades_for_order)) @@ -2069,14 +1837,10 @@ def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee, mocker): - """ - Test get_real_amount - fees in Stake currency - """ # Remove "Currency" from fee dict trades_for_order[0]['fee'] = {'cost': 0.008} patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order) amount = sum(x['amount'] for x in trades_for_order) @@ -2094,11 +1858,7 @@ def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee, def test_get_real_amount_open_trade(default_conf, mocker): - """ - Test get_real_amount condition trade.fee_open == 0 or order['status'] == 'open' - """ patch_RPCManager(mocker) - patch_coinmarketcap(mocker) mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True)) amount = 12345 trade = Trade( diff --git a/freqtrade/tests/test_indicator_helpers.py b/freqtrade/tests/test_indicator_helpers.py index f3d34ec0b..99d6cd79c 100644 --- a/freqtrade/tests/test_indicator_helpers.py +++ b/freqtrade/tests/test_indicator_helpers.py @@ -1,3 +1,5 @@ +# pragma pylint: disable=missing-docstring + import pandas as pd from freqtrade.indicator_helpers import went_down, went_up diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 80f5367b8..7aae98ebe 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -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', diff --git a/freqtrade/tests/test_misc.py b/freqtrade/tests/test_misc.py index 76290c6ca..26e0c5ee6 100644 --- a/freqtrade/tests/test_misc.py +++ b/freqtrade/tests/test_misc.py @@ -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) diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 7baddf60a..e52500071 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -1,5 +1,4 @@ # pragma pylint: disable=missing-docstring, C0103 -from copy import deepcopy from unittest.mock import MagicMock import pytest @@ -23,46 +22,40 @@ def test_init_create_session(default_conf): def test_init_custom_db_url(default_conf, mocker): - conf = deepcopy(default_conf) - # Update path to a value other than default, but still in-memory - conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'}) + default_conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'}) create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) - init(conf) + init(default_conf) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite' def test_init_invalid_db_url(default_conf): - conf = deepcopy(default_conf) - # Update path to a value other than default, but still in-memory - conf.update({'db_url': 'unknown:///some.url'}) + default_conf.update({'db_url': 'unknown:///some.url'}) with pytest.raises(OperationalException, match=r'.*no valid database URL*'): - init(conf) + init(default_conf) def test_init_prod_db(default_conf, mocker): - conf = deepcopy(default_conf) - conf.update({'dry_run': False}) - conf.update({'db_url': constants.DEFAULT_DB_PROD_URL}) + default_conf.update({'dry_run': False}) + default_conf.update({'db_url': constants.DEFAULT_DB_PROD_URL}) create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) - init(conf) + init(default_conf) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite' def test_init_dryrun_db(default_conf, mocker): - conf = deepcopy(default_conf) - conf.update({'dry_run': True}) - conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL}) + default_conf.update({'dry_run': True}) + default_conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL}) create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock()) - init(conf) + init(default_conf) assert create_engine_mock.call_count == 1 assert create_engine_mock.mock_calls[0][1][0] == 'sqlite://' @@ -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 diff --git a/freqtrade/tests/test_state.py b/freqtrade/tests/test_state.py deleted file mode 100644 index 51fa06cc2..000000000 --- a/freqtrade/tests/test_state.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -Unit test file for constants.py -""" - -from freqtrade.state import State - - -def test_state_object() -> None: - """ - Test the State object has the mandatory states - :return: None - """ - assert hasattr(State, 'RUNNING') - assert hasattr(State, 'STOPPED') diff --git a/freqtrade/tests/test_talib.py b/freqtrade/tests/test_talib.py new file mode 100644 index 000000000..093c3023c --- /dev/null +++ b/freqtrade/tests/test_talib.py @@ -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]) diff --git a/install_ta-lib.sh b/install_ta-lib.sh index 21e69cbba..18e7b8bbb 100755 --- a/install_ta-lib.sh +++ b/install_ta-lib.sh @@ -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 .. diff --git a/requirements.txt b/requirements.txt index 4255e2c38..d3ff4e6d7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/scripts/get_market_pairs.py b/scripts/get_market_pairs.py new file mode 100644 index 000000000..6ee6464d3 --- /dev/null +++ b/scripts/get_market_pairs.py @@ -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() + diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 11f1f85d5..f2f2e0c7f 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -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 ' diff --git a/setup.py b/setup.py index cd0574fa2..8853ef7f8 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ setup(name='freqtrade', license='GPLv3', packages=['freqtrade'], scripts=['bin/freqtrade'], - setup_requires=['pytest-runner'], + setup_requires=['pytest-runner', 'numpy'], tests_require=['pytest', 'pytest-mock', 'pytest-cov'], install_requires=[ 'ccxt', diff --git a/user_data/strategies/test_strategy.py b/user_data/strategies/test_strategy.py index c04f4935f..80c238d92 100644 --- a/user_data/strategies/test_strategy.py +++ b/user_data/strategies/test_strategy.py @@ -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[