mirror of
https://github.com/freqtrade/freqtrade.git
synced 2024-11-11 02:33:55 +00:00
Prepare master for release 0.17.2
This commit is contained in:
commit
d549fe351c
|
@ -13,7 +13,7 @@ addons:
|
||||||
install:
|
install:
|
||||||
- ./install_ta-lib.sh
|
- ./install_ta-lib.sh
|
||||||
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
|
||||||
- pip install --upgrade flake8 coveralls pytest-random-order mypy
|
- pip install --upgrade flake8 coveralls pytest-random-order pytest-asyncio mypy
|
||||||
- pip install -r requirements.txt
|
- pip install -r requirements.txt
|
||||||
- pip install -e .
|
- pip install -e .
|
||||||
jobs:
|
jobs:
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM python:3.6.6-slim-stretch
|
FROM python:3.7.0-slim-stretch
|
||||||
|
|
||||||
# Install TA-lib
|
# Install TA-lib
|
||||||
RUN apt-get update && apt-get -y install curl build-essential && apt-get clean
|
RUN apt-get update && apt-get -y install curl build-essential && apt-get clean
|
||||||
|
@ -16,10 +16,10 @@ WORKDIR /freqtrade
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
COPY requirements.txt /freqtrade/
|
COPY requirements.txt /freqtrade/
|
||||||
RUN pip install numpy \
|
RUN pip install numpy --no-cache-dir \
|
||||||
&& pip install -r requirements.txt
|
&& pip install -r requirements.txt --no-cache-dir
|
||||||
|
|
||||||
# Install and execute
|
# Install and execute
|
||||||
COPY . /freqtrade/
|
COPY . /freqtrade/
|
||||||
RUN pip install -e .
|
RUN pip install -e . --no-cache-dir
|
||||||
ENTRYPOINT ["freqtrade"]
|
ENTRYPOINT ["freqtrade"]
|
||||||
|
|
|
@ -152,7 +152,7 @@ The project is currently setup in two main branches:
|
||||||
|
|
||||||
- `develop` - This branch has often new features, but might also cause breaking changes.
|
- `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.
|
- `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.
|
- `feat/*` - These are feature branches, which are being worked on heavily. Please don't use these unless you want to test a specific feature.
|
||||||
|
|
||||||
|
|
||||||
## A note on Binance
|
## A note on Binance
|
||||||
|
@ -200,6 +200,10 @@ to understand the requirements before sending your pull-requests.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
### Uptodate clock
|
||||||
|
|
||||||
|
The clock must be accurate, syncronized to a NTP server very frequently to avoid problems with communication to the exchanges.
|
||||||
|
|
||||||
### Min hardware required
|
### Min hardware required
|
||||||
|
|
||||||
To run this bot we recommend you a cloud instance with a minimum of:
|
To run this bot we recommend you a cloud instance with a minimum of:
|
||||||
|
|
|
@ -11,7 +11,18 @@
|
||||||
"sell": 30
|
"sell": 30
|
||||||
},
|
},
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0
|
"ask_last_balance": 0.0,
|
||||||
|
"use_order_book": false,
|
||||||
|
"order_book_top": 1,
|
||||||
|
"check_depth_of_market": {
|
||||||
|
"enabled": false,
|
||||||
|
"bids_to_ask_delta": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ask_strategy":{
|
||||||
|
"use_order_book": false,
|
||||||
|
"order_book_min": 1,
|
||||||
|
"order_book_max": 9
|
||||||
},
|
},
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
|
|
|
@ -20,7 +20,18 @@
|
||||||
"sell": 30
|
"sell": 30
|
||||||
},
|
},
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0
|
"ask_last_balance": 0.0,
|
||||||
|
"use_order_book": false,
|
||||||
|
"order_book_top": 1,
|
||||||
|
"check_depth_of_market": {
|
||||||
|
"enabled": false,
|
||||||
|
"bids_to_ask_delta": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ask_strategy":{
|
||||||
|
"use_order_book": false,
|
||||||
|
"order_book_min": 1,
|
||||||
|
"order_book_max": 9
|
||||||
},
|
},
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
|
@ -41,7 +52,8 @@
|
||||||
],
|
],
|
||||||
"pair_blacklist": [
|
"pair_blacklist": [
|
||||||
"DOGE/BTC"
|
"DOGE/BTC"
|
||||||
]
|
],
|
||||||
|
"outdated_offset": 5
|
||||||
},
|
},
|
||||||
"experimental": {
|
"experimental": {
|
||||||
"use_sell_signal": false,
|
"use_sell_signal": false,
|
||||||
|
|
|
@ -23,19 +23,28 @@ The table below will list all configuration parameters.
|
||||||
| `ticker_interval` | [1m, 5m, 30m, 1h, 1d] | No | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Default is 5 minutes
|
| `ticker_interval` | [1m, 5m, 30m, 1h, 1d] | No | The ticker interval to use (1min, 5 min, 30 min, 1 hour or 1 day). Default is 5 minutes
|
||||||
| `fiat_display_currency` | USD | Yes | Fiat currency used to show your profits. More information below.
|
| `fiat_display_currency` | USD | Yes | Fiat currency used to show your profits. More information below.
|
||||||
| `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode.
|
| `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode.
|
||||||
|
| `process_only_new_candles` | false | No | If set to true indicators are processed only once a new candle arrives. If false each loop populates the indicators, this will mean the same candle is processed many times creating system load but can be useful of your strategy depends on tick data not only candle. Can be set either in Configuration or in the strategy.
|
||||||
| `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file.
|
| `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file.
|
||||||
| `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.
|
| `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_stop` | 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_stop_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.
|
| `trailing_stop_positve_offset` | 0 | No | Offset on when to apply `trailing_stop_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.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.
|
| `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.
|
| `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below.
|
||||||
|
| `bid_strategy.use_order_book` | false | No | Allows buying of pair using the rates in Order Book Bids.
|
||||||
|
| `bid_strategy.order_book_top` | 0 | No | Bot will use the top N rate in Order Book Bids. Ie. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids.
|
||||||
|
| `bid_strategy.check_depth_of_market.enabled` | false | No | Does not buy if the % difference of buy orders and sell orders is met in Order Book.
|
||||||
|
| `bid_strategy.check_depth_of_market.bids_to_ask_delta` | 0 | No | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher.
|
||||||
|
| `ask_strategy.use_order_book` | false | No | Allows selling of open traded pair using the rates in Order Book Asks.
|
||||||
|
| `ask_strategy.order_book_min` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
||||||
|
| `ask_strategy.order_book_max` | 0 | No | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
|
||||||
| `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
|
| `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
|
||||||
| `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode.
|
| `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode.
|
||||||
| `exchange.secret` | secret | No | API secret to use for the exchange. Only required when you are in production mode.
|
| `exchange.secret` | secret | No | API secret to use for the exchange. Only required when you are in production mode.
|
||||||
| `exchange.pair_whitelist` | [] | No | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param.
|
| `exchange.pair_whitelist` | [] | No | List of currency to use by the bot. Can be overrided with `--dynamic-whitelist` param.
|
||||||
| `exchange.pair_blacklist` | [] | No | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param.
|
| `exchange.pair_blacklist` | [] | No | List of currency the bot must avoid. Useful when using `--dynamic-whitelist` param.
|
||||||
|
| `exchange.ccxt_rate_limit` | True | No | Have CCXT handle Exchange rate limits. Depending on the exchange, having this to false can lead to temporary bans from the exchange.
|
||||||
| `experimental.use_sell_signal` | false | No | Use your sell strategy in addition of the `minimal_roi`.
|
| `experimental.use_sell_signal` | false | No | Use your sell strategy in addition of the `minimal_roi`.
|
||||||
| `experimental.sell_profit_only` | false | No | waits until you have made a positive profit before taking a sell decision.
|
| `experimental.sell_profit_only` | false | No | waits until you have made a positive profit before taking a sell decision.
|
||||||
| `experimental.ignore_roi_if_buy_signal` | false | No | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`
|
| `experimental.ignore_roi_if_buy_signal` | false | No | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`
|
||||||
|
|
|
@ -5,6 +5,8 @@ algorithms included in the `scikit-optimize` package to accomplish this. The
|
||||||
search will burn all your CPU cores, make your laptop sound like a fighter jet
|
search will burn all your CPU cores, make your laptop sound like a fighter jet
|
||||||
and still take a long time.
|
and still take a long time.
|
||||||
|
|
||||||
|
*Note:* Hyperopt will crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133)
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
- [Prepare your Hyperopt](#prepare-hyperopt)
|
- [Prepare your Hyperopt](#prepare-hyperopt)
|
||||||
- [Configure your Guards and Triggers](#configure-your-guards-and-triggers)
|
- [Configure your Guards and Triggers](#configure-your-guards-and-triggers)
|
||||||
|
|
|
@ -8,7 +8,6 @@ To understand how to set up the bot please read the [Bot Configuration](https://
|
||||||
|
|
||||||
* [Table of Contents](#table-of-contents)
|
* [Table of Contents](#table-of-contents)
|
||||||
* [Easy Installation - Linux Script](#easy-installation---linux-script)
|
* [Easy Installation - Linux Script](#easy-installation---linux-script)
|
||||||
* [Manual installation](#manual-installation)
|
|
||||||
* [Automatic Installation - Docker](#automatic-installation---docker)
|
* [Automatic Installation - Docker](#automatic-installation---docker)
|
||||||
* [Custom Linux MacOS Installation](#custom-installation)
|
* [Custom Linux MacOS Installation](#custom-installation)
|
||||||
- [Requirements](#requirements)
|
- [Requirements](#requirements)
|
||||||
|
@ -56,34 +55,6 @@ 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`.
|
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
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone git@github.com:freqtrade/freqtrade.git
|
|
||||||
git checkout develop
|
|
||||||
cd freqtrade
|
|
||||||
```
|
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build -t freqtrade .
|
|
||||||
docker run --rm -v /etc/localtime:/etc/localtime:ro -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
|
|
||||||
```
|
|
||||||
|
|
||||||
------
|
------
|
||||||
|
|
||||||
## Automatic Installation - Docker
|
## Automatic Installation - Docker
|
||||||
|
@ -196,7 +167,7 @@ docker run -d \
|
||||||
freqtrade --db-url sqlite:///tradesv3.sqlite
|
freqtrade --db-url sqlite:///tradesv3.sqlite
|
||||||
```
|
```
|
||||||
|
|
||||||
NOTE: db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used.
|
*Note*: db-url defaults to `sqlite:///tradesv3.sqlite` but it defaults to `sqlite://` if `dry_run=True` is being used.
|
||||||
To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite`
|
To override this behaviour use a custom db-url value: i.e.: `--db-url sqlite:///tradesv3.dryrun.sqlite`
|
||||||
|
|
||||||
### 6. Monitor your Docker instance
|
### 6. Monitor your Docker instance
|
||||||
|
@ -211,14 +182,15 @@ docker stop freqtrade
|
||||||
docker start freqtrade
|
docker start freqtrade
|
||||||
```
|
```
|
||||||
|
|
||||||
You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container.
|
For more information on how to operate Docker, please refer to the [official Docker documentation](https://docs.docker.com/).
|
||||||
|
|
||||||
|
*Note*: You do not need to rebuild the image for configuration changes, it will suffice to edit `config.json` and restart the container.
|
||||||
|
|
||||||
### 7. Backtest with docker
|
### 7. Backtest with docker
|
||||||
|
|
||||||
The following assumes that the above steps (1-4) have been completed successfully.
|
The following assumes that the above steps (1-4) have been completed successfully.
|
||||||
Also, backtest-data should be available at `~/.freqtrade/user_data/`.
|
Also, backtest-data should be available at `~/.freqtrade/user_data/`.
|
||||||
|
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
docker run -d \
|
docker run -d \
|
||||||
--name freqtrade \
|
--name freqtrade \
|
||||||
|
@ -238,12 +210,13 @@ Head over to the [Backtesting Documentation](https://github.com/freqtrade/freqtr
|
||||||
## Custom Installation
|
## Custom Installation
|
||||||
|
|
||||||
We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros.
|
We've included/collected install instructions for Ubuntu 16.04, MacOS, and Windows. These are guidelines and your success may vary with other distros.
|
||||||
|
OS Specific steps are listed first, the [common](#common) section below is necessary for all systems.
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
Click each one for install guide:
|
Click each one for install guide:
|
||||||
|
|
||||||
* [Python 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/), note the bot was not tested on Python >= 3.7.x
|
* [Python >= 3.6.x](http://docs.python-guide.org/en/latest/starting/installation/)
|
||||||
* [pip](https://pip.pypa.io/en/stable/installing/)
|
* [pip](https://pip.pypa.io/en/stable/installing/)
|
||||||
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
|
||||||
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
|
* [virtualenv](https://virtualenv.pypa.io/en/stable/installation/) (Recommended)
|
||||||
|
@ -251,7 +224,7 @@ Click each one for install guide:
|
||||||
|
|
||||||
### Linux - Ubuntu 16.04
|
### Linux - Ubuntu 16.04
|
||||||
|
|
||||||
#### 1. Install Python 3.6, Git, and wget
|
#### Install Python 3.6, Git, and wget
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo add-apt-repository ppa:jonathonf/python-3.6
|
sudo add-apt-repository ppa:jonathonf/python-3.6
|
||||||
|
@ -259,7 +232,34 @@ sudo apt-get update
|
||||||
sudo apt-get install python3.6 python3.6-venv python3.6-dev build-essential autoconf libtool pkg-config make wget git
|
sudo apt-get install python3.6 python3.6-venv python3.6-dev build-essential autoconf libtool pkg-config make wget git
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 2. Install TA-Lib
|
#### Raspberry Pi / Raspbian
|
||||||
|
|
||||||
|
Before installing FreqTrade on a Raspberry Pi running the official Raspbian Image, make sure you have at least Python 3.6 installed. The default image only provides Python 3.5. Probably the easiest way to get a recent version of python is [miniconda](https://repo.continuum.io/miniconda/).
|
||||||
|
|
||||||
|
The following assumes that miniconda3 is installed and available in your environment, and is installed.
|
||||||
|
It's recommended to use (mini)conda for this as installation/compilation of `scipy` and `pandas` takes a long time.
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
conda config --add channels rpi
|
||||||
|
conda install python=3.6
|
||||||
|
conda create -n freqtrade python=3.6
|
||||||
|
conda install scipy pandas
|
||||||
|
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
### MacOS
|
||||||
|
|
||||||
|
#### Install Python 3.6, git, wget and ta-lib
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install python3 git wget
|
||||||
|
```
|
||||||
|
|
||||||
|
### common
|
||||||
|
|
||||||
|
#### 1. Install TA-Lib
|
||||||
|
|
||||||
Official webpage: https://mrjbq7.github.io/ta-lib/install.html
|
Official webpage: https://mrjbq7.github.io/ta-lib/install.html
|
||||||
|
|
||||||
|
@ -267,7 +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
|
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
|
tar xvzf ta-lib-0.4.0-src.tar.gz
|
||||||
cd ta-lib
|
cd ta-lib
|
||||||
sed -i "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h
|
sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h
|
||||||
./configure --prefix=/usr
|
./configure --prefix=/usr
|
||||||
make
|
make
|
||||||
make install
|
make install
|
||||||
|
@ -275,15 +275,60 @@ cd ..
|
||||||
rm -rf ./ta-lib*
|
rm -rf ./ta-lib*
|
||||||
```
|
```
|
||||||
|
|
||||||
|
*Note*: An already downloaded version of ta-lib is included in the repository, as the sourceforge.net source seems to have problems frequently.
|
||||||
|
|
||||||
|
#### 2. Setup your Python virtual environment (virtualenv)
|
||||||
|
|
||||||
|
*Note*: This step is optional but strongly recommended to keep your system organized
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 -m venv .env
|
||||||
|
source .env/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
#### 3. Install FreqTrade
|
#### 3. Install FreqTrade
|
||||||
|
|
||||||
Clone the git repository:
|
Clone the git repository:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/freqtrade/freqtrade.git
|
git clone https://github.com/freqtrade/freqtrade.git
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 4. Configure `freqtrade` as a `systemd` service
|
Optionally checkout the stable/master branch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout master
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Initialize the configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd freqtrade
|
||||||
|
cp config.json.example config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
> *To edit the config please refer to [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md).*
|
||||||
|
|
||||||
|
#### 5. Install python dependencies
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
pip3 install --upgrade pip
|
||||||
|
pip3 install -r requirements.txt
|
||||||
|
pip3 install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 6. Run the Bot
|
||||||
|
|
||||||
|
If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3.6 ./freqtrade/main.py -c config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
*Note*: If you run the bot on a server, you should consider using [Docker](#automatic-installation---docker) a terminal multiplexer like `screen` or [`tmux`](https://en.wikipedia.org/wiki/Tmux) to avoid that the bot is stopped on logout.
|
||||||
|
|
||||||
|
#### 7. [Optional] Configure `freqtrade` as a `systemd` service
|
||||||
|
|
||||||
From the freqtrade repo... copy `freqtrade.service` to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup.
|
From the freqtrade repo... copy `freqtrade.service` to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup.
|
||||||
|
|
||||||
|
@ -299,57 +344,6 @@ For this to be persistent (run when user is logged out) you'll need to enable `l
|
||||||
sudo loginctl enable-linger "$USER"
|
sudo loginctl enable-linger "$USER"
|
||||||
```
|
```
|
||||||
|
|
||||||
### MacOS
|
|
||||||
|
|
||||||
#### 1. Install Python 3.6, git, wget and ta-lib
|
|
||||||
|
|
||||||
```bash
|
|
||||||
brew install python3 git wget ta-lib
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Install FreqTrade
|
|
||||||
|
|
||||||
Clone the git repository:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/freqtrade/freqtrade.git
|
|
||||||
```
|
|
||||||
|
|
||||||
Optionally checkout the develop branch:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git checkout develop
|
|
||||||
```
|
|
||||||
|
|
||||||
### Setup Config and virtual env
|
|
||||||
|
|
||||||
#### 1. Initialize the configuration
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd freqtrade
|
|
||||||
cp config.json.example config.json
|
|
||||||
```
|
|
||||||
|
|
||||||
> *To edit the config please refer to [Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md).*
|
|
||||||
|
|
||||||
#### 2. Setup your Python virtual environment (virtualenv)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3.6 -m venv .env
|
|
||||||
source .env/bin/activate
|
|
||||||
pip3.6 install --upgrade pip
|
|
||||||
pip3.6 install -r requirements.txt
|
|
||||||
pip3.6 install -e .
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. Run the Bot
|
|
||||||
|
|
||||||
If this is the first time you run the bot, ensure you are running it in Dry-run `"dry_run": true,` otherwise it will start to buy and sell coins.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3.6 ./freqtrade/main.py -c config.json
|
|
||||||
```
|
|
||||||
|
|
||||||
------
|
------
|
||||||
|
|
||||||
## Windows
|
## Windows
|
||||||
|
@ -369,7 +363,7 @@ git clone https://github.com/freqtrade/freqtrade.git
|
||||||
|
|
||||||
copy paste `config.json` to ``\path\freqtrade-develop\freqtrade`
|
copy paste `config.json` to ``\path\freqtrade-develop\freqtrade`
|
||||||
|
|
||||||
#### install ta-lib
|
#### Install ta-lib
|
||||||
|
|
||||||
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
|
Install ta-lib according to the [ta-lib documentation](https://github.com/mrjbq7/ta-lib#windows).
|
||||||
|
|
||||||
|
@ -390,5 +384,17 @@ REM >pip install TA_Lib‑0.4.17‑cp36‑cp36m‑win32.whl
|
||||||
|
|
||||||
> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222)
|
> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222)
|
||||||
|
|
||||||
|
#### Error during installation under Windows
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
error: Microsoft Visual C++ 14.0 is required. Get it with "Microsoft Visual C++ Build Tools": http://landinghub.visualstudio.com/visual-cpp-build-tools
|
||||||
|
```
|
||||||
|
|
||||||
|
Unfortunately, many packages requiring compilation don't provide a pre-build wheel. It is therefore mandatory to have a C/C++ compiler installed and available for your python environment to use.
|
||||||
|
|
||||||
|
The easiest way is to download install Microsoft Visual Studio Community [here](https://visualstudio.microsoft.com/downloads/) and make sure to install "Common Tools for Visual C++" to enable building c code on Windows. Unfortunately, this is a heavy download / dependency (~4Gb) so you might want to consider WSL or docker first.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
Now you have an environment ready, the next step is
|
Now you have an environment ready, the next step is
|
||||||
[Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)...
|
[Bot Configuration](https://github.com/freqtrade/freqtrade/blob/develop/docs/configuration.md)...
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
# Sandbox API testing
|
# Sandbox API testing
|
||||||
|
|
||||||
Where an exchange provides a sandbox for risk-free integration, or end-to-end, testing CCXT provides access to these.
|
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 document is a *light overview of configuring Freqtrade and GDAX sandbox.
|
||||||
|
@ -11,8 +12,11 @@ https://public.sandbox.gdax.com
|
||||||
https://api-public.sandbox.gdax.com
|
https://api-public.sandbox.gdax.com
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Configure a Sandbox account on Gdax
|
# Configure a Sandbox account on Gdax
|
||||||
|
|
||||||
Aim of this document section
|
Aim of this document section
|
||||||
|
|
||||||
- An sanbox account
|
- An sanbox account
|
||||||
- create 2FA (needed to create an API)
|
- create 2FA (needed to create an API)
|
||||||
- Add test 50BTC to account
|
- Add test 50BTC to account
|
||||||
|
@ -30,29 +34,34 @@ After registration and Email confimation you wil be redirected into your sanbox
|
||||||
> https://public.sandbox.pro.coinbase.com/
|
> https://public.sandbox.pro.coinbase.com/
|
||||||
|
|
||||||
## Enable 2Fa (a prerequisite to creating sandbox API Keys)
|
## Enable 2Fa (a prerequisite to creating sandbox API Keys)
|
||||||
|
|
||||||
From within sand box site select your profile, top right.
|
From within sand box site select your profile, top right.
|
||||||
>Or as a direct link: https://public.sandbox.pro.coinbase.com/profile
|
>Or as a direct link: https://public.sandbox.pro.coinbase.com/profile
|
||||||
|
|
||||||
From the menu panel to the left of the screen select
|
From the menu panel to the left of the screen select
|
||||||
|
|
||||||
> Security: "*View or Update*"
|
> Security: "*View or Update*"
|
||||||
|
|
||||||
In the new site select "enable authenticator" as typical google Authenticator.
|
In the new site select "enable authenticator" as typical google Authenticator.
|
||||||
|
|
||||||
- open Google Authenticator on your phone
|
- open Google Authenticator on your phone
|
||||||
- scan barcode
|
- scan barcode
|
||||||
- enter your generated 2fa
|
- enter your generated 2fa
|
||||||
|
|
||||||
## Enable API Access
|
## Enable API Access
|
||||||
|
|
||||||
From within sandbox select profile>api>create api-keys
|
From within sandbox select profile>api>create api-keys
|
||||||
>or as a direct link: https://public.sandbox.pro.coinbase.com/profile/api
|
>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
|
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 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 Secret** popup into a notepad this will needed later
|
||||||
- **Copy and paste the API Key** 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
|
## Add 50 BTC test funds
|
||||||
To add funds, use the web interface deposit and withdraw buttons.
|
|
||||||
|
|
||||||
|
To add funds, use the web interface deposit and withdraw buttons.
|
||||||
|
|
||||||
To begin select 'Wallets' from the top menu.
|
To begin select 'Wallets' from the top menu.
|
||||||
> Or as a direct link: https://public.sandbox.pro.coinbase.com/wallets
|
> Or as a direct link: https://public.sandbox.pro.coinbase.com/wallets
|
||||||
|
@ -64,88 +73,69 @@ To begin select 'Wallets' from the top menu.
|
||||||
- - - - - Deposit
|
- - - - - Deposit
|
||||||
|
|
||||||
*This process may be repeated for other currencies, ETH as example*
|
*This process may be repeated for other currencies, ETH as example*
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# Configure Freqtrade to use Gax Sandbox
|
# Configure Freqtrade to use Gax Sandbox
|
||||||
|
|
||||||
The aim of this document section
|
The aim of this document section
|
||||||
- Enable sandbox URLs in Freqtrade
|
|
||||||
- Configure API
|
- Enable sandbox URLs in Freqtrade
|
||||||
- - secret
|
- Configure API
|
||||||
- - key
|
- - secret
|
||||||
- - passphrase
|
- - key
|
||||||
|
- - passphrase
|
||||||
|
|
||||||
## Sandbox URLs
|
## Sandbox URLs
|
||||||
|
|
||||||
Freqtrade makes use of CCXT which in turn provides a list of URLs to Freqtrade.
|
Freqtrade makes use of CCXT which in turn provides a list of URLs to Freqtrade.
|
||||||
These include `['test']` and `['api']`.
|
These include `['test']` and `['api']`.
|
||||||
|
|
||||||
- `[Test]` if available will point to an Exchanges sandbox.
|
- `[Test]` if available will point to an Exchanges sandbox.
|
||||||
- `[Api]` normally used, and resolves to live API target on the exchange
|
- `[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
|
To make use of sandbox / test add "sandbox": true, to your config.json
|
||||||
```
|
|
||||||
|
```json
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "gdax",
|
"name": "gdax",
|
||||||
"sandbox": true,
|
"sandbox": true,
|
||||||
"key": "5wowfxemogxeowo;heiohgmd",
|
"key": "5wowfxemogxeowo;heiohgmd",
|
||||||
"secret": "/ZMH1P62rCVmwefewrgcewX8nh4gob+lywxfwfxwwfxwfNsH1ySgvWCUR/w==",
|
"secret": "/ZMH1P62rCVmwefewrgcewX8nh4gob+lywxfwfxwwfxwfNsH1ySgvWCUR/w==",
|
||||||
"password": "1bkjfkhfhfu6sr",
|
"password": "1bkjfkhfhfu6sr",
|
||||||
|
"outdated_offset": 5
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
"BTC/USD"
|
"BTC/USD"
|
||||||
```
|
```
|
||||||
|
|
||||||
Also insert your
|
Also insert your
|
||||||
|
|
||||||
- api-key (noted earlier)
|
- api-key (noted earlier)
|
||||||
- api-secret (noted earlier)
|
- api-secret (noted earlier)
|
||||||
- password (the passphrase - noted earlier)
|
- password (the passphrase - noted earlier)
|
||||||
|
|
||||||
---
|
---
|
||||||
## You should now be ready to test your sandbox!
|
|
||||||
|
## You should now be ready to test your sandbox
|
||||||
|
|
||||||
Ensure Freqtrade logs show the sandbox URL, and trades made are shown in 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.
|
** Typically the BTC/USD has the most activity in sandbox to test against.
|
||||||
|
|
||||||
## GDAX - Old Candles problem
|
## 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:
|
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.
|
||||||
>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:
|
To disable this check, add / change the `"outdated_offset"` parameter in the exchange section of your configuration to adjust for this delay.
|
||||||
```
|
Example based on the above configuration:
|
||||||
# # 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.
|
```json
|
||||||
|
"exchange": {
|
||||||
As example, to allow an additional 30 minutes. "(interval_minutes * 2 + 5 + 30)"
|
"name": "gdax",
|
||||||
```
|
"sandbox": true,
|
||||||
# Check if dataframe is out of date
|
"key": "5wowfxemogxeowo;heiohgmd",
|
||||||
signal_date = arrow.get(latest['date'])
|
"secret": "/ZMH1P62rCVmwefewrgcewX8nh4gob+lywxfwfxwwfxwfNsH1ySgvWCUR/w==",
|
||||||
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
|
"password": "1bkjfkhfhfu6sr",
|
||||||
if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5 + 30))):
|
"outdated_offset": 30
|
||||||
logger.warning(
|
"pair_whitelist": [
|
||||||
'Outdated history for pair %s. Last tick is %s minutes old',
|
"BTC/USD"
|
||||||
pair,
|
|
||||||
(arrow.utcnow() - signal_date).seconds // 60
|
|
||||||
)
|
|
||||||
return False, False
|
|
||||||
```
|
```
|
|
@ -119,7 +119,6 @@ class Arguments(object):
|
||||||
help='Override trades database URL, this is useful if dry_run is enabled'
|
help='Override trades database URL, this is useful if dry_run is enabled'
|
||||||
' or in custom deployments (default: %(default)s)',
|
' or in custom deployments (default: %(default)s)',
|
||||||
dest='db_url',
|
dest='db_url',
|
||||||
default=constants.DEFAULT_DB_PROD_URL,
|
|
||||||
type=str,
|
type=str,
|
||||||
metavar='PATH',
|
metavar='PATH',
|
||||||
)
|
)
|
||||||
|
|
|
@ -110,9 +110,12 @@ class Configuration(object):
|
||||||
'(not applicable with Backtesting and Hyperopt)'
|
'(not applicable with Backtesting and Hyperopt)'
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.args.db_url != constants.DEFAULT_DB_PROD_URL:
|
if self.args.db_url and self.args.db_url != constants.DEFAULT_DB_PROD_URL:
|
||||||
config.update({'db_url': self.args.db_url})
|
config.update({'db_url': self.args.db_url})
|
||||||
logger.info('Parameter --db-url detected ...')
|
logger.info('Parameter --db-url detected ...')
|
||||||
|
else:
|
||||||
|
# Set default here
|
||||||
|
config.update({'db_url': constants.DEFAULT_DB_PROD_URL})
|
||||||
|
|
||||||
if config.get('dry_run', False):
|
if config.get('dry_run', False):
|
||||||
logger.info('Dry run is enabled')
|
logger.info('Dry run is enabled')
|
||||||
|
|
|
@ -53,6 +53,7 @@ CONF_SCHEMA = {
|
||||||
},
|
},
|
||||||
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
|
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
|
||||||
'dry_run': {'type': 'boolean'},
|
'dry_run': {'type': 'boolean'},
|
||||||
|
'process_only_new_candles': {'type': 'boolean'},
|
||||||
'minimal_roi': {
|
'minimal_roi': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'patternProperties': {
|
'patternProperties': {
|
||||||
|
@ -78,18 +79,35 @@ CONF_SCHEMA = {
|
||||||
'type': 'number',
|
'type': 'number',
|
||||||
'minimum': 0,
|
'minimum': 0,
|
||||||
'maximum': 1,
|
'maximum': 1,
|
||||||
'exclusiveMaximum': False
|
'exclusiveMaximum': False,
|
||||||
|
'use_order_book': {'type': 'boolean'},
|
||||||
|
'order_book_top': {'type': 'number', 'maximum': 20, 'minimum': 1},
|
||||||
|
'check_depth_of_market': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'enabled': {'type': 'boolean'},
|
||||||
|
'bids_to_ask_delta': {'type': 'number', 'minimum': 0},
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'required': ['ask_last_balance']
|
'required': ['ask_last_balance']
|
||||||
},
|
},
|
||||||
|
'ask_strategy': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'use_order_book': {'type': 'boolean'},
|
||||||
|
'order_book_min': {'type': 'number', 'minimum': 1},
|
||||||
|
'order_book_max': {'type': 'number', 'minimum': 1, 'maximum': 50}
|
||||||
|
}
|
||||||
|
},
|
||||||
'exchange': {'$ref': '#/definitions/exchange'},
|
'exchange': {'$ref': '#/definitions/exchange'},
|
||||||
'experimental': {
|
'experimental': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'use_sell_signal': {'type': 'boolean'},
|
'use_sell_signal': {'type': 'boolean'},
|
||||||
'sell_profit_only': {'type': 'boolean'},
|
'sell_profit_only': {'type': 'boolean'},
|
||||||
"ignore_roi_if_buy_signal_true": {'type': 'boolean'}
|
'ignore_roi_if_buy_signal_true': {'type': 'boolean'}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'telegram': {
|
'telegram': {
|
||||||
|
@ -145,7 +163,8 @@ CONF_SCHEMA = {
|
||||||
'pattern': '^[0-9A-Z]+/[0-9A-Z]+$'
|
'pattern': '^[0-9A-Z]+/[0-9A-Z]+$'
|
||||||
},
|
},
|
||||||
'uniqueItems': True
|
'uniqueItems': True
|
||||||
}
|
},
|
||||||
|
'outdated_offset': {'type': 'integer', 'minimum': 1}
|
||||||
},
|
},
|
||||||
'required': ['name', 'key', 'secret', 'pair_whitelist']
|
'required': ['name', 'key', 'secret', 'pair_whitelist']
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
# pragma pylint: disable=W0603
|
# pragma pylint: disable=W0603
|
||||||
""" Cryptocurrency Exchanges support """
|
""" Cryptocurrency Exchanges support """
|
||||||
import logging
|
import logging
|
||||||
|
import inspect
|
||||||
from random import randint
|
from random import randint
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Tuple, Any, Optional
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from math import floor, ceil
|
from math import floor, ceil
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import ccxt
|
import ccxt
|
||||||
|
import ccxt.async_support as ccxt_async
|
||||||
import arrow
|
import arrow
|
||||||
|
|
||||||
from freqtrade import constants, OperationalException, DependencyException, TemporaryError
|
from freqtrade import constants, OperationalException, DependencyException, TemporaryError
|
||||||
|
@ -23,6 +26,24 @@ _EXCHANGE_URLS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def retrier_async(f):
|
||||||
|
async def wrapper(*args, **kwargs):
|
||||||
|
count = kwargs.pop('count', API_RETRY_COUNT)
|
||||||
|
try:
|
||||||
|
return await f(*args, **kwargs)
|
||||||
|
except (TemporaryError, DependencyException) as ex:
|
||||||
|
logger.warning('%s() returned exception: "%s"', f.__name__, ex)
|
||||||
|
if count > 0:
|
||||||
|
count -= 1
|
||||||
|
kwargs.update({'count': count})
|
||||||
|
logger.warning('retrying %s() still for %s times', f.__name__, count)
|
||||||
|
return await wrapper(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
logger.warning('Giving up retrying: %s()', f.__name__)
|
||||||
|
raise ex
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def retrier(f):
|
def retrier(f):
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
count = kwargs.pop('count', API_RETRY_COUNT)
|
count = kwargs.pop('count', API_RETRY_COUNT)
|
||||||
|
@ -45,8 +66,8 @@ class Exchange(object):
|
||||||
|
|
||||||
# Current selected exchange
|
# Current selected exchange
|
||||||
_api: ccxt.Exchange = None
|
_api: ccxt.Exchange = None
|
||||||
|
_api_async: ccxt_async.Exchange = None
|
||||||
_conf: Dict = {}
|
_conf: Dict = {}
|
||||||
_cached_ticker: Dict[str, Any] = {}
|
|
||||||
|
|
||||||
# Holds all open sell orders for dry_run
|
# Holds all open sell orders for dry_run
|
||||||
_dry_run_open_orders: Dict[str, Any] = {}
|
_dry_run_open_orders: Dict[str, Any] = {}
|
||||||
|
@ -60,14 +81,24 @@ class Exchange(object):
|
||||||
"""
|
"""
|
||||||
self._conf.update(config)
|
self._conf.update(config)
|
||||||
|
|
||||||
|
self._cached_ticker: Dict[str, Any] = {}
|
||||||
|
|
||||||
|
# Holds last candle refreshed time of each pair
|
||||||
|
self._pairs_last_refresh_time: Dict[str, int] = {}
|
||||||
|
|
||||||
|
# Holds candles
|
||||||
|
self.klines: Dict[str, Any] = {}
|
||||||
|
|
||||||
if config['dry_run']:
|
if config['dry_run']:
|
||||||
logger.info('Instance is running with dry_run enabled')
|
logger.info('Instance is running with dry_run enabled')
|
||||||
|
|
||||||
exchange_config = config['exchange']
|
exchange_config = config['exchange']
|
||||||
self._api = self._init_ccxt(exchange_config)
|
self._api = self._init_ccxt(exchange_config)
|
||||||
|
self._api_async = self._init_ccxt(exchange_config, ccxt_async)
|
||||||
|
|
||||||
logger.info('Using Exchange "%s"', self.name)
|
logger.info('Using Exchange "%s"', self.name)
|
||||||
|
|
||||||
|
self.markets = self._load_markets()
|
||||||
# Check if all pairs are available
|
# Check if all pairs are available
|
||||||
self.validate_pairs(config['exchange']['pair_whitelist'])
|
self.validate_pairs(config['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
|
@ -75,7 +106,15 @@ class Exchange(object):
|
||||||
# Check if timeframe is available
|
# Check if timeframe is available
|
||||||
self.validate_timeframes(config['ticker_interval'])
|
self.validate_timeframes(config['ticker_interval'])
|
||||||
|
|
||||||
def _init_ccxt(self, exchange_config: dict) -> ccxt.Exchange:
|
def __del__(self):
|
||||||
|
"""
|
||||||
|
Destructor - clean up async stuff
|
||||||
|
"""
|
||||||
|
logger.debug("Exchange object destroyed, closing async loop")
|
||||||
|
if self._api_async and inspect.iscoroutinefunction(self._api_async.close):
|
||||||
|
asyncio.get_event_loop().run_until_complete(self._api_async.close())
|
||||||
|
|
||||||
|
def _init_ccxt(self, exchange_config: dict, ccxt_module=ccxt) -> ccxt.Exchange:
|
||||||
"""
|
"""
|
||||||
Initialize ccxt with given config and return valid
|
Initialize ccxt with given config and return valid
|
||||||
ccxt instance.
|
ccxt instance.
|
||||||
|
@ -83,15 +122,15 @@ class Exchange(object):
|
||||||
# Find matching class for the given exchange name
|
# Find matching class for the given exchange name
|
||||||
name = exchange_config['name']
|
name = exchange_config['name']
|
||||||
|
|
||||||
if name not in ccxt.exchanges:
|
if name not in ccxt_module.exchanges:
|
||||||
raise OperationalException(f'Exchange {name} is not supported')
|
raise OperationalException(f'Exchange {name} is not supported')
|
||||||
try:
|
try:
|
||||||
api = getattr(ccxt, name.lower())({
|
api = getattr(ccxt_module, name.lower())({
|
||||||
'apiKey': exchange_config.get('key'),
|
'apiKey': exchange_config.get('key'),
|
||||||
'secret': exchange_config.get('secret'),
|
'secret': exchange_config.get('secret'),
|
||||||
'password': exchange_config.get('password'),
|
'password': exchange_config.get('password'),
|
||||||
'uid': exchange_config.get('uid', ''),
|
'uid': exchange_config.get('uid', ''),
|
||||||
'enableRateLimit': exchange_config.get('ccxt_rate_limit', True),
|
'enableRateLimit': exchange_config.get('ccxt_rate_limit', True)
|
||||||
})
|
})
|
||||||
except (KeyError, AttributeError):
|
except (KeyError, AttributeError):
|
||||||
raise OperationalException(f'Exchange {name} is not supported')
|
raise OperationalException(f'Exchange {name} is not supported')
|
||||||
|
@ -116,10 +155,29 @@ class Exchange(object):
|
||||||
api.urls['api'] = api.urls['test']
|
api.urls['api'] = api.urls['test']
|
||||||
logger.info("Enabled Sandbox API on %s", name)
|
logger.info("Enabled Sandbox API on %s", name)
|
||||||
else:
|
else:
|
||||||
logger.warning(self._api.name, "No Sandbox URL in CCXT, exiting. "
|
logger.warning(name, "No Sandbox URL in CCXT, exiting. "
|
||||||
"Please check your config.json")
|
"Please check your config.json")
|
||||||
raise OperationalException(f'Exchange {name} does not provide a sandbox api')
|
raise OperationalException(f'Exchange {name} does not provide a sandbox api')
|
||||||
|
|
||||||
|
def _load_async_markets(self) -> None:
|
||||||
|
try:
|
||||||
|
if self._api_async:
|
||||||
|
asyncio.get_event_loop().run_until_complete(self._api_async.load_markets())
|
||||||
|
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
logger.warning('Could not load async markets. Reason: %s', e)
|
||||||
|
return
|
||||||
|
|
||||||
|
def _load_markets(self) -> Dict[str, Any]:
|
||||||
|
""" Initialize markets both sync and async """
|
||||||
|
try:
|
||||||
|
markets = self._api.load_markets()
|
||||||
|
self._load_async_markets()
|
||||||
|
return markets
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
logger.warning('Unable to initialize markets. Reason: %s', e)
|
||||||
|
return {}
|
||||||
|
|
||||||
def validate_pairs(self, pairs: List[str]) -> None:
|
def validate_pairs(self, pairs: List[str]) -> None:
|
||||||
"""
|
"""
|
||||||
Checks if all given pairs are tradable on the current exchange.
|
Checks if all given pairs are tradable on the current exchange.
|
||||||
|
@ -128,11 +186,9 @@ class Exchange(object):
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
if not self.markets:
|
||||||
markets = self._api.load_markets()
|
logger.warning('Unable to validate pairs (assuming they are correct).')
|
||||||
except ccxt.BaseError as e:
|
# return
|
||||||
logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e)
|
|
||||||
return
|
|
||||||
|
|
||||||
stake_cur = self._conf['stake_currency']
|
stake_cur = self._conf['stake_currency']
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
|
@ -141,7 +197,7 @@ class Exchange(object):
|
||||||
if not pair.endswith(stake_cur):
|
if not pair.endswith(stake_cur):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Pair {pair} not compatible with stake_currency: {stake_cur}')
|
f'Pair {pair} not compatible with stake_currency: {stake_cur}')
|
||||||
if pair not in markets:
|
if self.markets and pair not in self.markets:
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Pair {pair} is not available at {self.name}')
|
f'Pair {pair} is not available at {self.name}')
|
||||||
|
|
||||||
|
@ -322,13 +378,109 @@ class Exchange(object):
|
||||||
return data
|
return data
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(
|
raise TemporaryError(
|
||||||
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}')
|
f'Could not load ticker due to {e.__class__.__name__}. Message: {e}')
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
else:
|
else:
|
||||||
logger.info("returning cached ticker-data for %s", pair)
|
logger.info("returning cached ticker-data for %s", pair)
|
||||||
return self._cached_ticker[pair]
|
return self._cached_ticker[pair]
|
||||||
|
|
||||||
|
def get_history(self, pair: str, tick_interval: str,
|
||||||
|
since_ms: int) -> List:
|
||||||
|
"""
|
||||||
|
Gets candle history using asyncio and returns the list of candles.
|
||||||
|
Handles all async doing.
|
||||||
|
"""
|
||||||
|
return asyncio.get_event_loop().run_until_complete(
|
||||||
|
self._async_get_history(pair=pair, tick_interval=tick_interval,
|
||||||
|
since_ms=since_ms))
|
||||||
|
|
||||||
|
async def _async_get_history(self, pair: str,
|
||||||
|
tick_interval: str,
|
||||||
|
since_ms: int) -> List:
|
||||||
|
# Assume exchange returns 500 candles
|
||||||
|
_LIMIT = 500
|
||||||
|
|
||||||
|
one_call = constants.TICKER_INTERVAL_MINUTES[tick_interval] * 60 * _LIMIT * 1000
|
||||||
|
logger.debug("one_call: %s", one_call)
|
||||||
|
input_coroutines = [self._async_get_candle_history(
|
||||||
|
pair, tick_interval, since) for since in
|
||||||
|
range(since_ms, arrow.utcnow().timestamp * 1000, one_call)]
|
||||||
|
tickers = await asyncio.gather(*input_coroutines, return_exceptions=True)
|
||||||
|
|
||||||
|
# Combine tickers
|
||||||
|
data: List = []
|
||||||
|
for tick in tickers:
|
||||||
|
if tick[0] == pair:
|
||||||
|
data.extend(tick[1])
|
||||||
|
# Sort data again after extending the result - above calls return in "async order" order
|
||||||
|
data = sorted(data, key=lambda x: x[0])
|
||||||
|
logger.info("downloaded %s with length %s.", pair, len(data))
|
||||||
|
return data
|
||||||
|
|
||||||
|
def refresh_tickers(self, pair_list: List[str], ticker_interval: str) -> None:
|
||||||
|
"""
|
||||||
|
Refresh tickers asyncronously and return the result.
|
||||||
|
"""
|
||||||
|
logger.debug("Refreshing klines for %d pairs", len(pair_list))
|
||||||
|
asyncio.get_event_loop().run_until_complete(
|
||||||
|
self.async_get_candles_history(pair_list, ticker_interval))
|
||||||
|
|
||||||
|
async def async_get_candles_history(self, pairs: List[str],
|
||||||
|
tick_interval: str) -> List[Tuple[str, List]]:
|
||||||
|
"""Download ohlcv history for pair-list asyncronously """
|
||||||
|
input_coroutines = [self._async_get_candle_history(
|
||||||
|
symbol, tick_interval) for symbol in pairs]
|
||||||
|
tickers = await asyncio.gather(*input_coroutines, return_exceptions=True)
|
||||||
|
return tickers
|
||||||
|
|
||||||
|
@retrier_async
|
||||||
|
async def _async_get_candle_history(self, pair: str, tick_interval: str,
|
||||||
|
since_ms: Optional[int] = None) -> Tuple[str, List]:
|
||||||
|
try:
|
||||||
|
# fetch ohlcv asynchronously
|
||||||
|
logger.debug("fetching %s since %s ...", pair, since_ms)
|
||||||
|
|
||||||
|
# Calculating ticker interval in second
|
||||||
|
interval_in_sec = constants.TICKER_INTERVAL_MINUTES[tick_interval] * 60
|
||||||
|
|
||||||
|
# If (last update time) + (interval in second) is greater or equal than now
|
||||||
|
# that means we don't have to hit the API as there is no new candle
|
||||||
|
# so we fetch it from local cache
|
||||||
|
if (not since_ms and
|
||||||
|
self._pairs_last_refresh_time.get(pair, 0) + interval_in_sec >=
|
||||||
|
arrow.utcnow().timestamp):
|
||||||
|
data = self.klines[pair]
|
||||||
|
logger.debug("Using cached klines data for %s ...", pair)
|
||||||
|
else:
|
||||||
|
data = await self._api_async.fetch_ohlcv(pair, timeframe=tick_interval,
|
||||||
|
since=since_ms)
|
||||||
|
|
||||||
|
# Because some exchange sort Tickers ASC and other DESC.
|
||||||
|
# Ex: Bittrex returns a list of tickers ASC (oldest first, newest last)
|
||||||
|
# when GDAX returns a list of tickers DESC (newest first, oldest last)
|
||||||
|
data = sorted(data, key=lambda x: x[0])
|
||||||
|
|
||||||
|
# keeping last candle time as last refreshed time of the pair
|
||||||
|
if data:
|
||||||
|
self._pairs_last_refresh_time[pair] = data[-1][0] // 1000
|
||||||
|
|
||||||
|
# keeping candles in cache
|
||||||
|
self.klines[pair] = data
|
||||||
|
|
||||||
|
logger.debug("done fetching %s ...", pair)
|
||||||
|
return pair, data
|
||||||
|
|
||||||
|
except ccxt.NotSupported as e:
|
||||||
|
raise OperationalException(
|
||||||
|
f'Exchange {self._api.name} does not support fetching historical candlestick data.'
|
||||||
|
f'Message: {e}')
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}')
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(f'Could not fetch ticker data. Msg: {e}')
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def get_candle_history(self, pair: str, tick_interval: str,
|
def get_candle_history(self, pair: str, tick_interval: str,
|
||||||
since_ms: Optional[int] = None) -> List[Dict]:
|
since_ms: Optional[int] = None) -> List[Dict]:
|
||||||
|
@ -409,6 +561,37 @@ class Exchange(object):
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
|
@retrier
|
||||||
|
def get_order_book(self, pair: str, limit: int = 100) -> dict:
|
||||||
|
"""
|
||||||
|
get order book level 2 from exchange
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
20180619: bittrex doesnt support limits -.-
|
||||||
|
20180619: binance support limits but only on specific range
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if self._api.name == 'Binance':
|
||||||
|
limit_range = [5, 10, 20, 50, 100, 500, 1000]
|
||||||
|
# get next-higher step in the limit_range list
|
||||||
|
limit = min(list(filter(lambda x: limit <= x, limit_range)))
|
||||||
|
# above script works like loop below (but with slightly better performance):
|
||||||
|
# for limitx in limit_range:
|
||||||
|
# if limit <= limitx:
|
||||||
|
# limit = limitx
|
||||||
|
# break
|
||||||
|
|
||||||
|
return self._api.fetch_l2_order_book(pair, limit)
|
||||||
|
except ccxt.NotSupported as e:
|
||||||
|
raise OperationalException(
|
||||||
|
f'Exchange {self._api.name} does not support fetching order book.'
|
||||||
|
f'Message: {e}')
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not get order book due to {e.__class__.__name__}. Message: {e}')
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e)
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List:
|
def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List:
|
||||||
if self._conf['dry_run']:
|
if self._conf['dry_run']:
|
||||||
|
@ -416,7 +599,8 @@ class Exchange(object):
|
||||||
if not self.exchange_has('fetchMyTrades'):
|
if not self.exchange_has('fetchMyTrades'):
|
||||||
return []
|
return []
|
||||||
try:
|
try:
|
||||||
my_trades = self._api.fetch_my_trades(pair, since.timestamp())
|
# Allow 5s offset to catch slight time offsets (discovered in #1185)
|
||||||
|
my_trades = self._api.fetch_my_trades(pair, since.timestamp() - 5)
|
||||||
matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
|
matched_trades = [trade for trade in my_trades if trade['order'] == order_id]
|
||||||
|
|
||||||
return matched_trades
|
return matched_trades
|
||||||
|
@ -462,12 +646,3 @@ class Exchange(object):
|
||||||
f'Could not get fee info due to {e.__class__.__name__}. Message: {e}')
|
f'Could not get fee info due to {e.__class__.__name__}. Message: {e}')
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e)
|
raise OperationalException(e)
|
||||||
|
|
||||||
def get_amount_lots(self, pair: str, amount: float) -> float:
|
|
||||||
"""
|
|
||||||
get buyable amount rounding, ..
|
|
||||||
"""
|
|
||||||
# validate that markets are loaded before trying to get fee
|
|
||||||
if not self._api.markets:
|
|
||||||
self._api.load_markets()
|
|
||||||
return self._api.amount_to_lots(pair, amount)
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
Functions to analyze ticker data with indicators and produce buy and sell signals
|
Functions to analyze ticker data with indicators and produce buy and sell signals
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
import pandas as pd
|
||||||
from pandas import DataFrame, to_datetime
|
from pandas import DataFrame, to_datetime
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -31,3 +32,27 @@ def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
||||||
})
|
})
|
||||||
frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle
|
frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle
|
||||||
return frame
|
return frame
|
||||||
|
|
||||||
|
|
||||||
|
def order_book_to_dataframe(bids: list, asks: list) -> DataFrame:
|
||||||
|
"""
|
||||||
|
Gets order book list, returns dataframe with below format per suggested by creslin
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
b_sum b_size bids asks a_size a_sum
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
"""
|
||||||
|
cols = ['bids', 'b_size']
|
||||||
|
|
||||||
|
bids_frame = DataFrame(bids, columns=cols)
|
||||||
|
# add cumulative sum column
|
||||||
|
bids_frame['b_sum'] = bids_frame['b_size'].cumsum()
|
||||||
|
cols2 = ['asks', 'a_size']
|
||||||
|
asks_frame = DataFrame(asks, columns=cols2)
|
||||||
|
# add cumulative sum column
|
||||||
|
asks_frame['a_sum'] = asks_frame['a_size'].cumsum()
|
||||||
|
|
||||||
|
frame = pd.concat([bids_frame['b_sum'], bids_frame['b_size'], bids_frame['bids'],
|
||||||
|
asks_frame['asks'], asks_frame['a_size'], asks_frame['a_sum']], axis=1,
|
||||||
|
keys=['b_sum', 'b_size', 'bids', 'asks', 'a_size', 'a_sum'])
|
||||||
|
# logger.info('order book %s', frame )
|
||||||
|
return frame
|
||||||
|
|
|
@ -10,7 +10,8 @@ from datetime import datetime
|
||||||
from typing import Any, Callable, Dict, List, Optional
|
from typing import Any, Callable, Dict, List, Optional
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import requests
|
from requests.exceptions import RequestException
|
||||||
|
|
||||||
from cachetools import TTLCache, cached
|
from cachetools import TTLCache, cached
|
||||||
|
|
||||||
from freqtrade import (DependencyException, OperationalException,
|
from freqtrade import (DependencyException, OperationalException,
|
||||||
|
@ -21,6 +22,7 @@ from freqtrade.rpc import RPCManager, RPCMessageType
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.strategy.interface import SellType
|
from freqtrade.strategy.interface import SellType
|
||||||
from freqtrade.strategy.resolver import IStrategy, StrategyResolver
|
from freqtrade.strategy.resolver import IStrategy, StrategyResolver
|
||||||
|
from freqtrade.exchange.exchange_helpers import order_book_to_dataframe
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -180,6 +182,9 @@ class FreqtradeBot(object):
|
||||||
final_list = sanitized_list[:nb_assets] if nb_assets else sanitized_list
|
final_list = sanitized_list[:nb_assets] if nb_assets else sanitized_list
|
||||||
self.config['exchange']['pair_whitelist'] = final_list
|
self.config['exchange']['pair_whitelist'] = final_list
|
||||||
|
|
||||||
|
# Refreshing candles
|
||||||
|
self.exchange.refresh_tickers(final_list, self.strategy.ticker_interval)
|
||||||
|
|
||||||
# Query trades from persistence layer
|
# Query trades from persistence layer
|
||||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||||
|
|
||||||
|
@ -267,16 +272,40 @@ class FreqtradeBot(object):
|
||||||
|
|
||||||
return final_list
|
return final_list
|
||||||
|
|
||||||
def get_target_bid(self, ticker: Dict[str, float]) -> float:
|
def get_target_bid(self, pair: str, ticker: Dict[str, float]) -> float:
|
||||||
"""
|
"""
|
||||||
Calculates bid target between current ask price and last price
|
Calculates bid target between current ask price and last price
|
||||||
:param ticker: Ticker to use for getting Ask and Last Price
|
:param ticker: Ticker to use for getting Ask and Last Price
|
||||||
:return: float: Price
|
:return: float: Price
|
||||||
"""
|
"""
|
||||||
if ticker['ask'] < ticker['last']:
|
if ticker['ask'] < ticker['last']:
|
||||||
return ticker['ask']
|
ticker_rate = ticker['ask']
|
||||||
|
else:
|
||||||
balance = self.config['bid_strategy']['ask_last_balance']
|
balance = self.config['bid_strategy']['ask_last_balance']
|
||||||
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
ticker_rate = ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
||||||
|
|
||||||
|
used_rate = ticker_rate
|
||||||
|
config_bid_strategy = self.config.get('bid_strategy', {})
|
||||||
|
if 'use_order_book' in config_bid_strategy and\
|
||||||
|
config_bid_strategy.get('use_order_book', False):
|
||||||
|
logger.info('Getting price from order book')
|
||||||
|
order_book_top = config_bid_strategy.get('order_book_top', 1)
|
||||||
|
order_book = self.exchange.get_order_book(pair, order_book_top)
|
||||||
|
logger.debug('order_book %s', order_book)
|
||||||
|
# top 1 = index 0
|
||||||
|
order_book_rate = order_book['bids'][order_book_top - 1][0]
|
||||||
|
# if ticker has lower rate, then use ticker ( usefull if down trending )
|
||||||
|
logger.info('...top %s order book buy rate %0.8f', order_book_top, order_book_rate)
|
||||||
|
if ticker_rate < order_book_rate:
|
||||||
|
logger.info('...using ticker rate instead %0.8f', ticker_rate)
|
||||||
|
used_rate = ticker_rate
|
||||||
|
else:
|
||||||
|
used_rate = order_book_rate
|
||||||
|
else:
|
||||||
|
logger.info('Using Last Ask / Last Price')
|
||||||
|
used_rate = ticker_rate
|
||||||
|
|
||||||
|
return used_rate
|
||||||
|
|
||||||
def _get_trade_stake_amount(self) -> Optional[float]:
|
def _get_trade_stake_amount(self) -> Optional[float]:
|
||||||
"""
|
"""
|
||||||
|
@ -333,7 +362,7 @@ class FreqtradeBot(object):
|
||||||
amount_reserve_percent += self.strategy.stoploss
|
amount_reserve_percent += self.strategy.stoploss
|
||||||
# it should not be more than 50%
|
# it should not be more than 50%
|
||||||
amount_reserve_percent = max(amount_reserve_percent, 0.5)
|
amount_reserve_percent = max(amount_reserve_percent, 0.5)
|
||||||
return min(min_stake_amounts)/amount_reserve_percent
|
return min(min_stake_amounts) / amount_reserve_percent
|
||||||
|
|
||||||
def create_trade(self) -> bool:
|
def create_trade(self) -> bool:
|
||||||
"""
|
"""
|
||||||
|
@ -362,13 +391,38 @@ class FreqtradeBot(object):
|
||||||
if not whitelist:
|
if not whitelist:
|
||||||
raise DependencyException('No currency pairs in whitelist')
|
raise DependencyException('No currency pairs in whitelist')
|
||||||
|
|
||||||
# Pick pair based on buy signals
|
# running get_signal on historical data fetched
|
||||||
|
# to find buy signals
|
||||||
for _pair in whitelist:
|
for _pair in whitelist:
|
||||||
thistory = self.exchange.get_candle_history(_pair, interval)
|
(buy, sell) = self.strategy.get_signal(_pair, interval, self.exchange.klines.get(_pair))
|
||||||
(buy, sell) = self.strategy.get_signal(_pair, interval, thistory)
|
|
||||||
|
|
||||||
if buy and not sell:
|
if buy and not sell:
|
||||||
|
bidstrat_check_depth_of_market = self.config.get('bid_strategy', {}).\
|
||||||
|
get('check_depth_of_market', {})
|
||||||
|
if (bidstrat_check_depth_of_market.get('enabled', False)) and\
|
||||||
|
(bidstrat_check_depth_of_market.get('bids_to_ask_delta', 0) > 0):
|
||||||
|
if self._check_depth_of_market_buy(_pair, bidstrat_check_depth_of_market):
|
||||||
return self.execute_buy(_pair, stake_amount)
|
return self.execute_buy(_pair, stake_amount)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
return self.execute_buy(_pair, stake_amount)
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool:
|
||||||
|
"""
|
||||||
|
Checks depth of market before executing a buy
|
||||||
|
"""
|
||||||
|
conf_bids_to_ask_delta = conf.get('bids_to_ask_delta', 0)
|
||||||
|
logger.info('checking depth of market for %s', pair)
|
||||||
|
order_book = self.exchange.get_order_book(pair, 1000)
|
||||||
|
order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks'])
|
||||||
|
order_book_bids = order_book_data_frame['b_size'].sum()
|
||||||
|
order_book_asks = order_book_data_frame['a_size'].sum()
|
||||||
|
bids_ask_delta = order_book_bids / order_book_asks
|
||||||
|
logger.info('bids: %s, asks: %s, delta: %s', order_book_bids,
|
||||||
|
order_book_asks, bids_ask_delta)
|
||||||
|
if bids_ask_delta >= conf_bids_to_ask_delta:
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def execute_buy(self, pair: str, stake_amount: float) -> bool:
|
def execute_buy(self, pair: str, stake_amount: float) -> bool:
|
||||||
|
@ -383,7 +437,7 @@ class FreqtradeBot(object):
|
||||||
fiat_currency = self.config.get('fiat_display_currency', None)
|
fiat_currency = self.config.get('fiat_display_currency', None)
|
||||||
|
|
||||||
# Calculate amount
|
# Calculate amount
|
||||||
buy_limit = self.get_target_bid(self.exchange.get_ticker(pair))
|
buy_limit = self.get_target_bid(pair, self.exchange.get_ticker(pair))
|
||||||
|
|
||||||
min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit)
|
min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit)
|
||||||
if min_stake_amount is not None and min_stake_amount > stake_amount:
|
if min_stake_amount is not None and min_stake_amount > stake_amount:
|
||||||
|
@ -526,22 +580,52 @@ class FreqtradeBot(object):
|
||||||
raise ValueError(f'attempt to handle closed trade: {trade}')
|
raise ValueError(f'attempt to handle closed trade: {trade}')
|
||||||
|
|
||||||
logger.debug('Handling %s ...', trade)
|
logger.debug('Handling %s ...', trade)
|
||||||
current_rate = self.exchange.get_ticker(trade.pair)['bid']
|
sell_rate = self.exchange.get_ticker(trade.pair)['bid']
|
||||||
|
|
||||||
(buy, sell) = (False, False)
|
(buy, sell) = (False, False)
|
||||||
experimental = self.config.get('experimental', {})
|
experimental = self.config.get('experimental', {})
|
||||||
if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'):
|
if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'):
|
||||||
ticker = self.exchange.get_candle_history(trade.pair, self.strategy.ticker_interval)
|
ticker = self.exchange.klines.get(trade.pair)
|
||||||
(buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.ticker_interval,
|
(buy, sell) = self.strategy.get_signal(trade.pair, self.strategy.ticker_interval,
|
||||||
ticker)
|
ticker)
|
||||||
|
|
||||||
should_sell = self.strategy.should_sell(trade, current_rate, datetime.utcnow(), buy, sell)
|
config_ask_strategy = self.config.get('ask_strategy', {})
|
||||||
if should_sell.sell_flag:
|
if config_ask_strategy.get('use_order_book', False):
|
||||||
self.execute_sell(trade, current_rate, should_sell.sell_type)
|
logger.info('Using order book for selling...')
|
||||||
|
# logger.debug('Order book %s',orderBook)
|
||||||
|
order_book_min = config_ask_strategy.get('order_book_min', 1)
|
||||||
|
order_book_max = config_ask_strategy.get('order_book_max', 1)
|
||||||
|
|
||||||
|
order_book = self.exchange.get_order_book(trade.pair, order_book_max)
|
||||||
|
|
||||||
|
for i in range(order_book_min, order_book_max + 1):
|
||||||
|
order_book_rate = order_book['asks'][i - 1][0]
|
||||||
|
|
||||||
|
# if orderbook has higher rate (high profit),
|
||||||
|
# use orderbook, otherwise just use bids rate
|
||||||
|
logger.info(' order book asks top %s: %0.8f', i, order_book_rate)
|
||||||
|
if sell_rate < order_book_rate:
|
||||||
|
sell_rate = order_book_rate
|
||||||
|
|
||||||
|
if self.check_sell(trade, sell_rate, buy, sell):
|
||||||
return True
|
return True
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
logger.info('checking sell')
|
||||||
|
if self.check_sell(trade, sell_rate, buy, sell):
|
||||||
|
return True
|
||||||
|
|
||||||
logger.info('Found no sell signals for whitelisted currencies. Trying again..')
|
logger.info('Found no sell signals for whitelisted currencies. Trying again..')
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool:
|
||||||
|
should_sell = self.strategy.should_sell(trade, sell_rate, datetime.utcnow(), buy, sell)
|
||||||
|
if should_sell.sell_flag:
|
||||||
|
self.execute_sell(trade, sell_rate, should_sell.sell_type)
|
||||||
|
logger.info('excuted sell')
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def check_handle_timedout(self) -> None:
|
def check_handle_timedout(self) -> None:
|
||||||
"""
|
"""
|
||||||
Check if any orders are timed out and cancel if neccessary
|
Check if any orders are timed out and cancel if neccessary
|
||||||
|
@ -562,7 +646,7 @@ class FreqtradeBot(object):
|
||||||
if not trade.open_order_id:
|
if not trade.open_order_id:
|
||||||
continue
|
continue
|
||||||
order = self.exchange.get_order(trade.open_order_id, trade.pair)
|
order = self.exchange.get_order(trade.open_order_id, trade.pair)
|
||||||
except requests.exceptions.RequestException:
|
except (RequestException, DependencyException):
|
||||||
logger.info(
|
logger.info(
|
||||||
'Cannot query order for %s due to %s',
|
'Cannot query order for %s due to %s',
|
||||||
trade,
|
trade,
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring
|
||||||
|
|
||||||
import gzip
|
import gzip
|
||||||
import json
|
try:
|
||||||
|
import ujson as json
|
||||||
|
_UJSON = True
|
||||||
|
except ImportError:
|
||||||
|
# see mypy/issues/1153
|
||||||
|
import json # type: ignore
|
||||||
|
_UJSON = False
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from typing import Optional, List, Dict, Tuple, Any
|
from typing import Optional, List, Dict, Tuple, Any
|
||||||
|
@ -14,6 +20,14 @@ from freqtrade.arguments import TimeRange
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def json_load(data):
|
||||||
|
"""Try to load data with ujson"""
|
||||||
|
if _UJSON:
|
||||||
|
return json.load(data, precise_float=True)
|
||||||
|
else:
|
||||||
|
return json.load(data)
|
||||||
|
|
||||||
|
|
||||||
def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
|
def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
|
||||||
if not tickerlist:
|
if not tickerlist:
|
||||||
return tickerlist
|
return tickerlist
|
||||||
|
@ -163,7 +177,7 @@ def load_cached_data_for_updating(filename: str,
|
||||||
# read the cached file
|
# read the cached file
|
||||||
if os.path.isfile(filename):
|
if os.path.isfile(filename):
|
||||||
with open(filename, "rt") as file:
|
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
|
# remove the last item, because we are not sure if it is correct
|
||||||
# it could be fetched when the candle was incompleted
|
# it could be fetched when the candle was incompleted
|
||||||
if data:
|
if data:
|
||||||
|
@ -191,19 +205,18 @@ def download_backtesting_testdata(datadir: str,
|
||||||
timerange: Optional[TimeRange] = None) -> None:
|
timerange: Optional[TimeRange] = None) -> None:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Download the latest ticker intervals from the exchange for the pairs passed in parameters
|
Download the latest ticker intervals from the exchange for the pair passed in parameters
|
||||||
The data is downloaded starting from the last correct ticker interval data that
|
The data is downloaded starting from the last correct ticker interval data that
|
||||||
esists in a cache. If timerange starts earlier than the data in the cache,
|
exists in a cache. If timerange starts earlier than the data in the cache,
|
||||||
the full data will be redownloaded
|
the full data will be redownloaded
|
||||||
|
|
||||||
Based on @Rybolov work: https://github.com/rybolov/freqtrade-data
|
Based on @Rybolov work: https://github.com/rybolov/freqtrade-data
|
||||||
:param pairs: list of pairs to download
|
:param pair: pair to download
|
||||||
:param tick_interval: ticker interval
|
:param tick_interval: ticker interval
|
||||||
:param timerange: range of time to download
|
:param timerange: range of time to download
|
||||||
:return: None
|
:return: None
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
path = make_testdata_path(datadir)
|
path = make_testdata_path(datadir)
|
||||||
filepair = pair.replace("/", "_")
|
filepair = pair.replace("/", "_")
|
||||||
filename = os.path.join(path, f'{filepair}-{tick_interval}.json')
|
filename = os.path.join(path, f'{filepair}-{tick_interval}.json')
|
||||||
|
@ -219,8 +232,11 @@ 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 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')
|
logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None')
|
||||||
|
|
||||||
new_data = exchange.get_candle_history(pair=pair, tick_interval=tick_interval,
|
# Default since_ms to 30 days if nothing is given
|
||||||
since_ms=since_ms)
|
new_data = exchange.get_history(pair=pair, tick_interval=tick_interval,
|
||||||
|
since_ms=since_ms if since_ms
|
||||||
|
else
|
||||||
|
int(arrow.utcnow().shift(days=-30).float_timestamp) * 1000)
|
||||||
data.extend(new_data)
|
data.extend(new_data)
|
||||||
|
|
||||||
logger.debug("New Start: %s", misc.format_ms_time(data[0][0]))
|
logger.debug("New Start: %s", misc.format_ms_time(data[0][0]))
|
||||||
|
|
|
@ -75,8 +75,6 @@ class Backtesting(object):
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# only one strategy
|
# only one strategy
|
||||||
strat = StrategyResolver(self.config).strategy
|
|
||||||
|
|
||||||
self.strategylist.append(StrategyResolver(self.config).strategy)
|
self.strategylist.append(StrategyResolver(self.config).strategy)
|
||||||
# Load one strategy
|
# Load one strategy
|
||||||
self._set_strategy(self.strategylist[0])
|
self._set_strategy(self.strategylist[0])
|
||||||
|
@ -108,7 +106,8 @@ class Backtesting(object):
|
||||||
return min(timeframe, key=operator.itemgetter(0))[0], \
|
return min(timeframe, key=operator.itemgetter(0))[0], \
|
||||||
max(timeframe, key=operator.itemgetter(1))[1]
|
max(timeframe, key=operator.itemgetter(1))[1]
|
||||||
|
|
||||||
def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame) -> str:
|
def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame,
|
||||||
|
skip_nan: bool = False) -> str:
|
||||||
"""
|
"""
|
||||||
Generates and returns a text table for the given backtest data and the results dataframe
|
Generates and returns a text table for the given backtest data and the results dataframe
|
||||||
:return: pretty printed table with tabulate as str
|
:return: pretty printed table with tabulate as str
|
||||||
|
@ -121,6 +120,9 @@ class Backtesting(object):
|
||||||
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
|
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
|
||||||
for pair in data:
|
for pair in data:
|
||||||
result = results[results.pair == pair]
|
result = results[results.pair == pair]
|
||||||
|
if skip_nan and result.profit_abs.isnull().all():
|
||||||
|
continue
|
||||||
|
|
||||||
tabular_data.append([
|
tabular_data.append([
|
||||||
pair,
|
pair,
|
||||||
len(result.index),
|
len(result.index),
|
||||||
|
@ -330,15 +332,15 @@ class Backtesting(object):
|
||||||
Run a backtesting end-to-end
|
Run a backtesting end-to-end
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
data = {}
|
data: Dict[str, Any] = {}
|
||||||
pairs = self.config['exchange']['pair_whitelist']
|
pairs = self.config['exchange']['pair_whitelist']
|
||||||
logger.info('Using stake_currency: %s ...', self.config['stake_currency'])
|
logger.info('Using stake_currency: %s ...', self.config['stake_currency'])
|
||||||
logger.info('Using stake_amount: %s ...', self.config['stake_amount'])
|
logger.info('Using stake_amount: %s ...', self.config['stake_amount'])
|
||||||
|
|
||||||
if self.config.get('live'):
|
if self.config.get('live'):
|
||||||
logger.info('Downloading data for all pairs in whitelist ...')
|
logger.info('Downloading data for all pairs in whitelist ...')
|
||||||
for pair in pairs:
|
self.exchange.refresh_tickers(pairs, self.ticker_interval)
|
||||||
data[pair] = self.exchange.get_candle_history(pair, self.ticker_interval)
|
data = self.exchange.klines
|
||||||
else:
|
else:
|
||||||
logger.info('Using local backtesting data (using whitelist in given config) ...')
|
logger.info('Using local backtesting data (using whitelist in given config) ...')
|
||||||
|
|
||||||
|
@ -404,7 +406,7 @@ class Backtesting(object):
|
||||||
print(self._generate_text_table_sell_reason(data, results))
|
print(self._generate_text_table_sell_reason(data, results))
|
||||||
|
|
||||||
print(' LEFT OPEN TRADES REPORT '.center(119, '='))
|
print(' LEFT OPEN TRADES REPORT '.center(119, '='))
|
||||||
print(self._generate_text_table(data, results.loc[results.open_at_end]))
|
print(self._generate_text_table(data, results.loc[results.open_at_end], True))
|
||||||
print()
|
print()
|
||||||
if len(all_results) > 1:
|
if len(all_results) > 1:
|
||||||
# Print Strategy summary table
|
# Print Strategy summary table
|
||||||
|
|
|
@ -152,7 +152,7 @@ class Hyperopt(Backtesting):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
def generate_roi_table(params: Dict) -> Dict[int, float]:
|
||||||
"""
|
"""
|
||||||
Generate the ROI table thqt will be used by Hyperopt
|
Generate the ROI table that will be used by Hyperopt
|
||||||
"""
|
"""
|
||||||
roi_table = {}
|
roi_table = {}
|
||||||
roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
|
roi_table[0] = params['roi_p1'] + params['roi_p2'] + params['roi_p3']
|
||||||
|
@ -402,6 +402,13 @@ def start(args: Namespace) -> None:
|
||||||
config['exchange']['key'] = ''
|
config['exchange']['key'] = ''
|
||||||
config['exchange']['secret'] = ''
|
config['exchange']['secret'] = ''
|
||||||
|
|
||||||
|
if config.get('strategy') and config.get('strategy') != 'DefaultStrategy':
|
||||||
|
logger.error("Please don't use --strategy for hyperopt.")
|
||||||
|
logger.error(
|
||||||
|
"Read the documentation at "
|
||||||
|
"https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md "
|
||||||
|
"to understand how to configure hyperopt.")
|
||||||
|
raise ValueError("--strategy configured but not supported for hyperopt")
|
||||||
# Initialize backtesting object
|
# Initialize backtesting object
|
||||||
hyperopt = Hyperopt(config)
|
hyperopt = Hyperopt(config)
|
||||||
hyperopt.start()
|
hyperopt.start()
|
||||||
|
|
|
@ -79,10 +79,12 @@ def check_migrate(engine) -> None:
|
||||||
table_back_name = 'trades_bak'
|
table_back_name = 'trades_bak'
|
||||||
for i, table_back_name in enumerate(tabs):
|
for i, table_back_name in enumerate(tabs):
|
||||||
table_back_name = f'trades_bak{i}'
|
table_back_name = f'trades_bak{i}'
|
||||||
logger.info(f'trying {table_back_name}')
|
logger.debug(f'trying {table_back_name}')
|
||||||
|
|
||||||
# Check for latest column
|
# Check for latest column
|
||||||
if not has_column(cols, 'ticker_interval'):
|
if not has_column(cols, 'ticker_interval'):
|
||||||
|
logger.info(f'Running database migration - backup available as {table_back_name}')
|
||||||
|
|
||||||
fee_open = get_column_def(cols, 'fee_open', 'fee')
|
fee_open = get_column_def(cols, 'fee_open', 'fee')
|
||||||
fee_close = get_column_def(cols, 'fee_close', 'fee')
|
fee_close = get_column_def(cols, 'fee_close', 'fee')
|
||||||
open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null')
|
open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null')
|
||||||
|
|
|
@ -13,6 +13,7 @@ import sqlalchemy as sql
|
||||||
from numpy import mean, nan_to_num
|
from numpy import mean, nan_to_num
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade import TemporaryError
|
||||||
from freqtrade.fiat_convert import CryptoToFiatConverter
|
from freqtrade.fiat_convert import CryptoToFiatConverter
|
||||||
from freqtrade.misc import shorten_date
|
from freqtrade.misc import shorten_date
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
@ -273,10 +274,13 @@ class RPC(object):
|
||||||
if coin == 'BTC':
|
if coin == 'BTC':
|
||||||
rate = 1.0
|
rate = 1.0
|
||||||
else:
|
else:
|
||||||
|
try:
|
||||||
if coin == 'USDT':
|
if coin == 'USDT':
|
||||||
rate = 1.0 / self._freqtrade.exchange.get_ticker('BTC/USDT', False)['bid']
|
rate = 1.0 / self._freqtrade.exchange.get_ticker('BTC/USDT', False)['bid']
|
||||||
else:
|
else:
|
||||||
rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid']
|
rate = self._freqtrade.exchange.get_ticker(coin + '/BTC', False)['bid']
|
||||||
|
except TemporaryError:
|
||||||
|
continue
|
||||||
est_btc: float = rate * balance['total']
|
est_btc: float = rate * balance['total']
|
||||||
total = total + est_btc
|
total = total + est_btc
|
||||||
output.append({
|
output.append({
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
|
# Import Default-Strategy to have hyperopt correctly resolve
|
||||||
|
from freqtrade.strategy.default_strategy import DefaultStrategy # noqa: F401
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -12,8 +14,18 @@ def import_strategy(strategy: IStrategy, config: dict) -> IStrategy:
|
||||||
Imports given Strategy instance to global scope
|
Imports given Strategy instance to global scope
|
||||||
of freqtrade.strategy and returns an instance of it
|
of freqtrade.strategy and returns an instance of it
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Copy all attributes from base class and class
|
# Copy all attributes from base class and class
|
||||||
attr = deepcopy({**strategy.__class__.__dict__, **strategy.__dict__})
|
|
||||||
|
comb = {**strategy.__class__.__dict__, **strategy.__dict__}
|
||||||
|
|
||||||
|
# Delete '_abc_impl' from dict as deepcopy fails on 3.7 with
|
||||||
|
# `TypeError: can't pickle _abc_data objects``
|
||||||
|
# This will only apply to python 3.7
|
||||||
|
if sys.version_info.major == 3 and sys.version_info.minor == 7 and '_abc_impl' in comb:
|
||||||
|
del comb['_abc_impl']
|
||||||
|
|
||||||
|
attr = deepcopy(comb)
|
||||||
# Adjust module name
|
# Adjust module name
|
||||||
attr['__module__'] = 'freqtrade.strategy'
|
attr['__module__'] = 'freqtrade.strategy'
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ import logging
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Dict, List, NamedTuple, Tuple
|
from typing import Dict, List, NamedTuple, Optional, Tuple
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
|
@ -70,8 +70,15 @@ class IStrategy(ABC):
|
||||||
# associated ticker interval
|
# associated ticker interval
|
||||||
ticker_interval: str
|
ticker_interval: str
|
||||||
|
|
||||||
|
# run "populate_indicators" only for new candle
|
||||||
|
process_only_new_candles: bool = False
|
||||||
|
|
||||||
|
# Dict to determine if analysis is necessary
|
||||||
|
_last_candle_seen_per_pair: Dict[str, datetime] = {}
|
||||||
|
|
||||||
def __init__(self, config: dict) -> None:
|
def __init__(self, config: dict) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
|
self._last_candle_seen_per_pair = {}
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
@ -112,13 +119,34 @@ class IStrategy(ABC):
|
||||||
add several TA indicators and buy signal to it
|
add several TA indicators and buy signal to it
|
||||||
:return DataFrame with ticker data and indicator data
|
:return DataFrame with ticker data and indicator data
|
||||||
"""
|
"""
|
||||||
|
|
||||||
dataframe = parse_ticker_dataframe(ticker_history)
|
dataframe = parse_ticker_dataframe(ticker_history)
|
||||||
|
|
||||||
|
pair = str(metadata.get('pair'))
|
||||||
|
|
||||||
|
# Test if seen this pair and last candle before.
|
||||||
|
# always run if process_only_new_candles is set to true
|
||||||
|
if (not self.process_only_new_candles or
|
||||||
|
self._last_candle_seen_per_pair.get(pair, None) != dataframe.iloc[-1]['date']):
|
||||||
|
# Defs that only make change on new candle data.
|
||||||
|
logging.debug("TA Analysis Launched")
|
||||||
dataframe = self.advise_indicators(dataframe, metadata)
|
dataframe = self.advise_indicators(dataframe, metadata)
|
||||||
dataframe = self.advise_buy(dataframe, metadata)
|
dataframe = self.advise_buy(dataframe, metadata)
|
||||||
dataframe = self.advise_sell(dataframe, metadata)
|
dataframe = self.advise_sell(dataframe, metadata)
|
||||||
|
self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date']
|
||||||
|
else:
|
||||||
|
logging.debug("Skippinig TA Analysis for already analyzed candle")
|
||||||
|
dataframe['buy'] = 0
|
||||||
|
dataframe['sell'] = 0
|
||||||
|
|
||||||
|
# Other Defs in strategy that want to be called every loop here
|
||||||
|
# twitter_sell = self.watch_twitter_feed(dataframe, metadata)
|
||||||
|
logging.debug("Loop Analysis Launched")
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def get_signal(self, pair: str, interval: str, ticker_hist: List[Dict]) -> Tuple[bool, bool]:
|
def get_signal(self, pair: str, interval: str,
|
||||||
|
ticker_hist: Optional[List[Dict]]) -> Tuple[bool, bool]:
|
||||||
"""
|
"""
|
||||||
Calculates current signal based several technical analysis indicators
|
Calculates current signal based several technical analysis indicators
|
||||||
:param pair: pair in format ANT/BTC
|
:param pair: pair in format ANT/BTC
|
||||||
|
@ -155,7 +183,8 @@ class IStrategy(ABC):
|
||||||
# Check if dataframe is out of date
|
# Check if dataframe is out of date
|
||||||
signal_date = arrow.get(latest['date'])
|
signal_date = arrow.get(latest['date'])
|
||||||
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
|
interval_minutes = constants.TICKER_INTERVAL_MINUTES[interval]
|
||||||
if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + 5))):
|
offset = self.config.get('exchange', {}).get('outdated_offset', 5)
|
||||||
|
if signal_date < (arrow.utcnow().shift(minutes=-(interval_minutes * 2 + offset))):
|
||||||
logger.warning(
|
logger.warning(
|
||||||
'Outdated history for pair %s. Last tick is %s minutes old',
|
'Outdated history for pair %s. Last tick is %s minutes old',
|
||||||
pair,
|
pair,
|
||||||
|
|
|
@ -44,14 +44,15 @@ class StrategyResolver(object):
|
||||||
# Check if we need to override configuration
|
# Check if we need to override configuration
|
||||||
if 'minimal_roi' in config:
|
if 'minimal_roi' in config:
|
||||||
self.strategy.minimal_roi = config['minimal_roi']
|
self.strategy.minimal_roi = config['minimal_roi']
|
||||||
logger.info("Override strategy \'minimal_roi\' with value in config file.")
|
logger.info("Override strategy 'minimal_roi' with value in config file: %s.",
|
||||||
|
config['minimal_roi'])
|
||||||
else:
|
else:
|
||||||
config['minimal_roi'] = self.strategy.minimal_roi
|
config['minimal_roi'] = self.strategy.minimal_roi
|
||||||
|
|
||||||
if 'stoploss' in config:
|
if 'stoploss' in config:
|
||||||
self.strategy.stoploss = config['stoploss']
|
self.strategy.stoploss = config['stoploss']
|
||||||
logger.info(
|
logger.info(
|
||||||
"Override strategy \'stoploss\' with value in config file: %s.", config['stoploss']
|
"Override strategy 'stoploss' with value in config file: %s.", config['stoploss']
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
config['stoploss'] = self.strategy.stoploss
|
config['stoploss'] = self.strategy.stoploss
|
||||||
|
@ -59,12 +60,21 @@ class StrategyResolver(object):
|
||||||
if 'ticker_interval' in config:
|
if 'ticker_interval' in config:
|
||||||
self.strategy.ticker_interval = config['ticker_interval']
|
self.strategy.ticker_interval = config['ticker_interval']
|
||||||
logger.info(
|
logger.info(
|
||||||
"Override strategy \'ticker_interval\' with value in config file: %s.",
|
"Override strategy 'ticker_interval' with value in config file: %s.",
|
||||||
config['ticker_interval']
|
config['ticker_interval']
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
config['ticker_interval'] = self.strategy.ticker_interval
|
config['ticker_interval'] = self.strategy.ticker_interval
|
||||||
|
|
||||||
|
if 'process_only_new_candles' in config:
|
||||||
|
self.strategy.process_only_new_candles = config['process_only_new_candles']
|
||||||
|
logger.info(
|
||||||
|
"Override process_only_new_candles 'process_only_new_candles' "
|
||||||
|
"with value in config file: %s.", config['process_only_new_candles']
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
config['process_only_new_candles'] = self.strategy.process_only_new_candles
|
||||||
|
|
||||||
# Sort and apply type conversions
|
# Sort and apply type conversions
|
||||||
self.strategy.minimal_roi = OrderedDict(sorted(
|
self.strategy.minimal_roi = OrderedDict(sorted(
|
||||||
{int(key): value for (key, value) in self.strategy.minimal_roi.items()}.items(),
|
{int(key): value for (key, value) in self.strategy.minimal_roi.items()}.items(),
|
||||||
|
|
|
@ -4,7 +4,7 @@ import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock, PropertyMock
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import pytest
|
import pytest
|
||||||
|
@ -26,8 +26,10 @@ def log_has(line, logs):
|
||||||
|
|
||||||
|
|
||||||
def patch_exchange(mocker, api_mock=None) -> None:
|
def patch_exchange(mocker, api_mock=None) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value="Bittrex"))
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.id', PropertyMock(return_value="bittrex"))
|
||||||
if api_mock:
|
if api_mock:
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
else:
|
else:
|
||||||
|
@ -102,7 +104,18 @@ def default_conf():
|
||||||
"sell": 30
|
"sell": 30
|
||||||
},
|
},
|
||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0
|
"ask_last_balance": 0.0,
|
||||||
|
"use_order_book": False,
|
||||||
|
"order_book_top": 1,
|
||||||
|
"check_depth_of_market": {
|
||||||
|
"enabled": False,
|
||||||
|
"bids_to_ask_delta": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ask_strategy": {
|
||||||
|
"use_order_book": False,
|
||||||
|
"order_book_min": 1,
|
||||||
|
"order_book_max": 1
|
||||||
},
|
},
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
|
@ -403,6 +416,39 @@ def limit_sell_order():
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def order_book_l2():
|
||||||
|
return MagicMock(return_value={
|
||||||
|
'bids': [
|
||||||
|
[0.043936, 10.442],
|
||||||
|
[0.043935, 31.865],
|
||||||
|
[0.043933, 11.212],
|
||||||
|
[0.043928, 0.088],
|
||||||
|
[0.043925, 10.0],
|
||||||
|
[0.043921, 10.0],
|
||||||
|
[0.04392, 37.64],
|
||||||
|
[0.043899, 0.066],
|
||||||
|
[0.043885, 0.676],
|
||||||
|
[0.04387, 22.758]
|
||||||
|
],
|
||||||
|
'asks': [
|
||||||
|
[0.043949, 0.346],
|
||||||
|
[0.04395, 0.608],
|
||||||
|
[0.043951, 3.948],
|
||||||
|
[0.043954, 0.288],
|
||||||
|
[0.043958, 9.277],
|
||||||
|
[0.043995, 1.566],
|
||||||
|
[0.044, 0.588],
|
||||||
|
[0.044002, 0.992],
|
||||||
|
[0.044003, 0.095],
|
||||||
|
[0.04402, 37.64]
|
||||||
|
],
|
||||||
|
'timestamp': None,
|
||||||
|
'datetime': None,
|
||||||
|
'nonce': 288004540
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def ticker_history():
|
def ticker_history():
|
||||||
return [
|
return [
|
||||||
|
|
|
@ -3,8 +3,9 @@
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from random import randint
|
from random import randint
|
||||||
from unittest.mock import MagicMock, PropertyMock
|
from unittest.mock import Mock, MagicMock, PropertyMock
|
||||||
|
|
||||||
|
import arrow
|
||||||
import ccxt
|
import ccxt
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
@ -13,6 +14,14 @@ from freqtrade.exchange import API_RETRY_COUNT, Exchange
|
||||||
from freqtrade.tests.conftest import get_patched_exchange, log_has
|
from freqtrade.tests.conftest import get_patched_exchange, log_has
|
||||||
|
|
||||||
|
|
||||||
|
# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines
|
||||||
|
def get_mock_coro(return_value):
|
||||||
|
async def mock_coro(*args, **kwargs):
|
||||||
|
return return_value
|
||||||
|
|
||||||
|
return Mock(wraps=mock_coro)
|
||||||
|
|
||||||
|
|
||||||
def ccxt_exceptionhandlers(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs):
|
def ccxt_exceptionhandlers(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs):
|
||||||
with pytest.raises(TemporaryError):
|
with pytest.raises(TemporaryError):
|
||||||
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError)
|
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError)
|
||||||
|
@ -27,12 +36,32 @@ def ccxt_exceptionhandlers(mocker, default_conf, api_mock, fun, mock_ccxt_fun, *
|
||||||
assert api_mock.__dict__[mock_ccxt_fun].call_count == 1
|
assert api_mock.__dict__[mock_ccxt_fun].call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def async_ccxt_exception(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs):
|
||||||
|
with pytest.raises(TemporaryError):
|
||||||
|
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
await getattr(exchange, fun)(**kwargs)
|
||||||
|
assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
await getattr(exchange, fun)(**kwargs)
|
||||||
|
assert api_mock.__dict__[mock_ccxt_fun].call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_init(default_conf, mocker, caplog):
|
def test_init(default_conf, mocker, caplog):
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
get_patched_exchange(mocker, default_conf)
|
get_patched_exchange(mocker, default_conf)
|
||||||
assert log_has('Instance is running with dry_run enabled', caplog.record_tuples)
|
assert log_has('Instance is running with dry_run enabled', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_destroy(default_conf, mocker, caplog):
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
get_patched_exchange(mocker, default_conf)
|
||||||
|
assert log_has('Exchange object destroyed, closing async loop', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_init_exception(default_conf, mocker):
|
def test_init_exception(default_conf, mocker):
|
||||||
default_conf['exchange']['name'] = 'wrong_exchange_name'
|
default_conf['exchange']['name'] = 'wrong_exchange_name'
|
||||||
|
|
||||||
|
@ -64,6 +93,7 @@ def test_symbol_amount_prec(default_conf, mocker):
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||||
exchange = Exchange(default_conf)
|
exchange = Exchange(default_conf)
|
||||||
|
|
||||||
amount = 2.34559
|
amount = 2.34559
|
||||||
|
@ -87,6 +117,7 @@ def test_symbol_price_prec(default_conf, mocker):
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||||
exchange = Exchange(default_conf)
|
exchange = Exchange(default_conf)
|
||||||
|
|
||||||
price = 2.34559
|
price = 2.34559
|
||||||
|
@ -108,6 +139,7 @@ def test_set_sandbox(default_conf, mocker):
|
||||||
type(api_mock).urls = url_mock
|
type(api_mock).urls = url_mock
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||||
|
|
||||||
exchange = Exchange(default_conf)
|
exchange = Exchange(default_conf)
|
||||||
liveurl = exchange._api.urls['api']
|
liveurl = exchange._api.urls['api']
|
||||||
|
@ -129,6 +161,7 @@ def test_set_sandbox_exception(default_conf, mocker):
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r'does not provide a sandbox api'):
|
with pytest.raises(OperationalException, match=r'does not provide a sandbox api'):
|
||||||
exchange = Exchange(default_conf)
|
exchange = Exchange(default_conf)
|
||||||
|
@ -136,6 +169,43 @@ def test_set_sandbox_exception(default_conf, mocker):
|
||||||
exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname')
|
exchange.set_sandbox(exchange._api, default_conf['exchange'], 'Logname')
|
||||||
|
|
||||||
|
|
||||||
|
def test__load_async_markets(default_conf, mocker, caplog):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
exchange._api_async.load_markets = get_mock_coro(None)
|
||||||
|
exchange._load_async_markets()
|
||||||
|
assert exchange._api_async.load_markets.call_count == 1
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
|
||||||
|
exchange._api_async.load_markets = Mock(side_effect=ccxt.BaseError("deadbeef"))
|
||||||
|
exchange._load_async_markets()
|
||||||
|
|
||||||
|
assert log_has('Could not load async markets. Reason: deadbeef',
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test__load_markets(default_conf, mocker, caplog):
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
api_mock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.name', PropertyMock(return_value='Binance'))
|
||||||
|
|
||||||
|
api_mock.load_markets = MagicMock(return_value={})
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||||
|
|
||||||
|
expected_return = {'ETH/BTC': 'available'}
|
||||||
|
api_mock.load_markets = MagicMock(return_value=expected_return)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
default_conf['exchange']['pair_whitelist'] = ['ETH/BTC']
|
||||||
|
ex = Exchange(default_conf)
|
||||||
|
assert ex.markets == expected_return
|
||||||
|
|
||||||
|
api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
|
Exchange(default_conf)
|
||||||
|
assert log_has('Unable to initialize markets. Reason: ', caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_validate_pairs(default_conf, mocker):
|
def test_validate_pairs(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.load_markets = MagicMock(return_value={
|
api_mock.load_markets = MagicMock(return_value={
|
||||||
|
@ -146,14 +216,16 @@ def test_validate_pairs(default_conf, mocker):
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
def test_validate_pairs_not_available(default_conf, mocker):
|
def test_validate_pairs_not_available(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
api_mock.load_markets = MagicMock(return_value={})
|
api_mock.load_markets = MagicMock(return_value={'XRP/BTC': 'inactive'})
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r'not available'):
|
with pytest.raises(OperationalException, match=r'not available'):
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
@ -167,6 +239,7 @@ def test_validate_pairs_not_compatible(default_conf, mocker):
|
||||||
default_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._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||||
with pytest.raises(OperationalException, match=r'not compatible'):
|
with pytest.raises(OperationalException, match=r'not compatible'):
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
@ -179,15 +252,14 @@ def test_validate_pairs_exception(default_conf, mocker, caplog):
|
||||||
api_mock.load_markets = MagicMock(return_value={})
|
api_mock.load_markets = MagicMock(return_value={})
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available at Binance'):
|
with pytest.raises(OperationalException, match=r'Pair ETH/BTC is not available at Binance'):
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
api_mock.load_markets = MagicMock(side_effect=ccxt.BaseError())
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
assert log_has('Unable to validate pairs (assuming they are correct). Reason: ',
|
assert log_has('Unable to validate pairs (assuming they are correct).',
|
||||||
caplog.record_tuples)
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
@ -198,6 +270,7 @@ def test_validate_pairs_stake_exception(default_conf, mocker, caplog):
|
||||||
api_mock.name = MagicMock(return_value='binance')
|
api_mock.name = MagicMock(return_value='binance')
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', api_mock)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||||
|
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
OperationalException,
|
OperationalException,
|
||||||
|
@ -218,7 +291,7 @@ def test_validate_timeframes(default_conf, mocker):
|
||||||
type(api_mock).timeframes = timeframes
|
type(api_mock).timeframes = timeframes
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
@ -234,7 +307,7 @@ def test_validate_timeframes_failed(default_conf, mocker):
|
||||||
type(api_mock).timeframes = timeframes
|
type(api_mock).timeframes = timeframes
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
with pytest.raises(OperationalException, match=r'Invalid ticker 3m, this Exchange supports.*'):
|
with pytest.raises(OperationalException, match=r'Invalid ticker 3m, this Exchange supports.*'):
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
@ -251,7 +324,7 @@ def test_validate_timeframes_not_in_config(default_conf, mocker):
|
||||||
type(api_mock).timeframes = timeframes
|
type(api_mock).timeframes = timeframes
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
Exchange(default_conf)
|
Exchange(default_conf)
|
||||||
|
|
||||||
|
|
||||||
|
@ -515,6 +588,189 @@ def test_get_ticker(default_conf, mocker):
|
||||||
exchange.get_ticker(pair='ETH/BTC', refresh=True)
|
exchange.get_ticker(pair='ETH/BTC', refresh=True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_history(default_conf, mocker, caplog):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
tick = [
|
||||||
|
[
|
||||||
|
arrow.utcnow().timestamp * 1000, # unix timestamp ms
|
||||||
|
1, # open
|
||||||
|
2, # high
|
||||||
|
3, # low
|
||||||
|
4, # close
|
||||||
|
5, # volume (in quote currency)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
pair = 'ETH/BTC'
|
||||||
|
|
||||||
|
async def mock_candle_hist(pair, tick_interval, since_ms):
|
||||||
|
return pair, tick
|
||||||
|
|
||||||
|
exchange._async_get_candle_history = Mock(wraps=mock_candle_hist)
|
||||||
|
# one_call calculation * 1.8 should do 2 calls
|
||||||
|
since = 5 * 60 * 500 * 1.8
|
||||||
|
print(f"since = {since}")
|
||||||
|
ret = exchange.get_history(pair, "5m", int((arrow.utcnow().timestamp - since) * 1000))
|
||||||
|
|
||||||
|
assert exchange._async_get_candle_history.call_count == 2
|
||||||
|
# Returns twice the above tick
|
||||||
|
assert len(ret) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_refresh_tickers(mocker, default_conf, caplog) -> None:
|
||||||
|
tick = [
|
||||||
|
[
|
||||||
|
1511686200000, # unix timestamp ms
|
||||||
|
1, # open
|
||||||
|
2, # high
|
||||||
|
3, # low
|
||||||
|
4, # close
|
||||||
|
5, # volume (in quote currency)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
exchange._api_async.fetch_ohlcv = get_mock_coro(tick)
|
||||||
|
|
||||||
|
pairs = ['IOTA/ETH', 'XRP/ETH']
|
||||||
|
# empty dicts
|
||||||
|
assert not exchange.klines
|
||||||
|
exchange.refresh_tickers(['IOTA/ETH', 'XRP/ETH'], '5m')
|
||||||
|
|
||||||
|
assert log_has(f'Refreshing klines for {len(pairs)} pairs', caplog.record_tuples)
|
||||||
|
assert exchange.klines
|
||||||
|
for pair in pairs:
|
||||||
|
assert exchange.klines[pair]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test__async_get_candle_history(default_conf, mocker, caplog):
|
||||||
|
tick = [
|
||||||
|
[
|
||||||
|
arrow.utcnow().timestamp * 1000, # unix timestamp ms
|
||||||
|
1, # open
|
||||||
|
2, # high
|
||||||
|
3, # low
|
||||||
|
4, # close
|
||||||
|
5, # volume (in quote currency)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
# Monkey-patch async function
|
||||||
|
exchange._api_async.fetch_ohlcv = get_mock_coro(tick)
|
||||||
|
|
||||||
|
exchange = Exchange(default_conf)
|
||||||
|
pair = 'ETH/BTC'
|
||||||
|
res = await exchange._async_get_candle_history(pair, "5m")
|
||||||
|
assert type(res) is tuple
|
||||||
|
assert len(res) == 2
|
||||||
|
assert res[0] == pair
|
||||||
|
assert res[1] == tick
|
||||||
|
assert exchange._api_async.fetch_ohlcv.call_count == 1
|
||||||
|
assert not log_has(f"Using cached klines data for {pair} ...", caplog.record_tuples)
|
||||||
|
# test caching
|
||||||
|
res = await exchange._async_get_candle_history(pair, "5m")
|
||||||
|
assert exchange._api_async.fetch_ohlcv.call_count == 1
|
||||||
|
assert log_has(f"Using cached klines data for {pair} ...", caplog.record_tuples)
|
||||||
|
|
||||||
|
# exchange = Exchange(default_conf)
|
||||||
|
await async_ccxt_exception(mocker, default_conf, MagicMock(),
|
||||||
|
"_async_get_candle_history", "fetch_ohlcv",
|
||||||
|
pair='ABCD/BTC', tick_interval=default_conf['ticker_interval'])
|
||||||
|
|
||||||
|
api_mock = MagicMock()
|
||||||
|
with pytest.raises(OperationalException, match=r'Could not fetch ticker data*'):
|
||||||
|
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
await exchange._async_get_candle_history(pair, "5m",
|
||||||
|
(arrow.utcnow().timestamp - 2000) * 1000)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test__async_get_candle_history_empty(default_conf, mocker, caplog):
|
||||||
|
""" Test empty exchange result """
|
||||||
|
tick = []
|
||||||
|
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
# Monkey-patch async function
|
||||||
|
exchange._api_async.fetch_ohlcv = get_mock_coro([])
|
||||||
|
|
||||||
|
exchange = Exchange(default_conf)
|
||||||
|
pair = 'ETH/BTC'
|
||||||
|
res = await exchange._async_get_candle_history(pair, "5m")
|
||||||
|
assert type(res) is tuple
|
||||||
|
assert len(res) == 2
|
||||||
|
assert res[0] == pair
|
||||||
|
assert res[1] == tick
|
||||||
|
assert exchange._api_async.fetch_ohlcv.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_async_get_candles_history(default_conf, mocker):
|
||||||
|
tick = [
|
||||||
|
[
|
||||||
|
1511686200000, # unix timestamp ms
|
||||||
|
1, # open
|
||||||
|
2, # high
|
||||||
|
3, # low
|
||||||
|
4, # close
|
||||||
|
5, # volume (in quote currency)
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
async def mock_get_candle_hist(pair, tick_interval, since_ms=None):
|
||||||
|
return (pair, tick)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
# Monkey-patch async function
|
||||||
|
exchange._api_async.fetch_ohlcv = get_mock_coro(tick)
|
||||||
|
|
||||||
|
exchange._async_get_candle_history = Mock(wraps=mock_get_candle_hist)
|
||||||
|
|
||||||
|
pairs = ['ETH/BTC', 'XRP/BTC']
|
||||||
|
res = await exchange.async_get_candles_history(pairs, "5m")
|
||||||
|
assert type(res) is list
|
||||||
|
assert len(res) == 2
|
||||||
|
assert type(res[0]) is tuple
|
||||||
|
assert res[0][0] == pairs[0]
|
||||||
|
assert res[0][1] == tick
|
||||||
|
assert res[1][0] == pairs[1]
|
||||||
|
assert res[1][1] == tick
|
||||||
|
assert exchange._async_get_candle_history.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_order_book(default_conf, mocker, order_book_l2):
|
||||||
|
default_conf['exchange']['name'] = 'binance'
|
||||||
|
api_mock = MagicMock()
|
||||||
|
|
||||||
|
api_mock.fetch_l2_order_book = order_book_l2
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
order_book = exchange.get_order_book(pair='ETH/BTC', limit=10)
|
||||||
|
assert 'bids' in order_book
|
||||||
|
assert 'asks' in order_book
|
||||||
|
assert len(order_book['bids']) == 10
|
||||||
|
assert len(order_book['asks']) == 10
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_order_book_exception(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NotSupported)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.get_order_book(pair='ETH/BTC', limit=50)
|
||||||
|
with pytest.raises(TemporaryError):
|
||||||
|
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NetworkError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.get_order_book(pair='ETH/BTC', limit=50)
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.BaseError)
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||||
|
exchange.get_order_book(pair='ETH/BTC', limit=50)
|
||||||
|
|
||||||
|
|
||||||
def make_fetch_ohlcv_mock(data):
|
def make_fetch_ohlcv_mock(data):
|
||||||
def fetch_ohlcv_mock(pair, timeframe, since):
|
def fetch_ohlcv_mock(pair, timeframe, since):
|
||||||
if since:
|
if since:
|
||||||
|
@ -705,8 +961,7 @@ def test_get_order(default_conf, mocker):
|
||||||
|
|
||||||
|
|
||||||
def test_name(default_conf, mocker):
|
def test_name(default_conf, mocker):
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs',
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
side_effect=lambda s: True)
|
|
||||||
default_conf['exchange']['name'] = 'binance'
|
default_conf['exchange']['name'] = 'binance'
|
||||||
exchange = Exchange(default_conf)
|
exchange = Exchange(default_conf)
|
||||||
|
|
||||||
|
@ -714,16 +969,14 @@ def test_name(default_conf, mocker):
|
||||||
|
|
||||||
|
|
||||||
def test_id(default_conf, mocker):
|
def test_id(default_conf, mocker):
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs',
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
side_effect=lambda s: True)
|
|
||||||
default_conf['exchange']['name'] = 'binance'
|
default_conf['exchange']['name'] = 'binance'
|
||||||
exchange = Exchange(default_conf)
|
exchange = Exchange(default_conf)
|
||||||
assert exchange.id == 'binance'
|
assert exchange.id == 'binance'
|
||||||
|
|
||||||
|
|
||||||
def test_get_pair_detail_url(default_conf, mocker, caplog):
|
def test_get_pair_detail_url(default_conf, mocker, caplog):
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs',
|
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||||
side_effect=lambda s: True)
|
|
||||||
default_conf['exchange']['name'] = 'binance'
|
default_conf['exchange']['name'] = 'binance'
|
||||||
exchange = Exchange(default_conf)
|
exchange = Exchange(default_conf)
|
||||||
|
|
||||||
|
@ -823,15 +1076,3 @@ def test_get_fee(default_conf, mocker):
|
||||||
|
|
||||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||||
'get_fee', 'calculate_fee')
|
'get_fee', 'calculate_fee')
|
||||||
|
|
||||||
|
|
||||||
def test_get_amount_lots(default_conf, mocker):
|
|
||||||
api_mock = MagicMock()
|
|
||||||
api_mock.amount_to_lots = MagicMock(return_value=1.0)
|
|
||||||
api_mock.markets = None
|
|
||||||
marketmock = MagicMock()
|
|
||||||
api_mock.load_markets = marketmock
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
|
||||||
|
|
||||||
assert exchange.get_amount_lots('LTC/BTC', 1.54) == 1
|
|
||||||
assert marketmock.call_count == 1
|
|
||||||
|
|
|
@ -110,7 +110,7 @@ def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=Fals
|
||||||
return pairdata
|
return pairdata
|
||||||
|
|
||||||
|
|
||||||
# use for mock freqtrade.exchange.get_candle_history'
|
# use for mock ccxt.fetch_ohlvc'
|
||||||
def _load_pair_as_ticks(pair, tickfreq):
|
def _load_pair_as_ticks(pair, tickfreq):
|
||||||
ticks = optimize.load_data(None, ticker_interval=tickfreq, pairs=[pair])
|
ticks = optimize.load_data(None, ticker_interval=tickfreq, pairs=[pair])
|
||||||
ticks = trim_dictlist(ticks, -201)
|
ticks = trim_dictlist(ticks, -201)
|
||||||
|
@ -455,7 +455,7 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
|
||||||
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
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.optimize.load_data', mocked_load_data)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_candle_history')
|
mocker.patch('freqtrade.exchange.Exchange.refresh_tickers', MagicMock())
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.optimize.backtesting.Backtesting',
|
'freqtrade.optimize.backtesting.Backtesting',
|
||||||
|
@ -490,7 +490,7 @@ def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
|
||||||
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
|
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.optimize.load_data', MagicMock(return_value={}))
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_candle_history')
|
mocker.patch('freqtrade.exchange.Exchange.refresh_tickers', MagicMock())
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.optimize.backtesting.Backtesting',
|
'freqtrade.optimize.backtesting.Backtesting',
|
||||||
|
@ -733,9 +733,14 @@ def test_backtest_record(default_conf, fee, mocker):
|
||||||
|
|
||||||
def test_backtest_start_live(default_conf, mocker, caplog):
|
def test_backtest_start_live(default_conf, mocker, caplog):
|
||||||
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
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))
|
async def load_pairs(pair, timeframe, since):
|
||||||
patch_exchange(mocker)
|
return _load_pair_as_ticks(pair, timeframe)
|
||||||
|
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.fetch_ohlcv = load_pairs
|
||||||
|
|
||||||
|
patch_exchange(mocker, api_mock)
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', MagicMock())
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock())
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting._generate_text_table', MagicMock())
|
||||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||||
|
@ -776,9 +781,13 @@ def test_backtest_start_live(default_conf, mocker, caplog):
|
||||||
|
|
||||||
def test_backtest_start_multi_strat(default_conf, mocker, caplog):
|
def test_backtest_start_multi_strat(default_conf, mocker, caplog):
|
||||||
default_conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
|
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))
|
async def load_pairs(pair, timeframe, since):
|
||||||
patch_exchange(mocker)
|
return _load_pair_as_ticks(pair, timeframe)
|
||||||
|
api_mock = MagicMock()
|
||||||
|
api_mock.fetch_ohlcv = load_pairs
|
||||||
|
|
||||||
|
patch_exchange(mocker, api_mock)
|
||||||
backtestmock = MagicMock()
|
backtestmock = MagicMock()
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.backtest', backtestmock)
|
||||||
gen_table_mock = MagicMock()
|
gen_table_mock = MagicMock()
|
||||||
|
|
|
@ -65,6 +65,31 @@ def test_start(mocker, default_conf, caplog) -> None:
|
||||||
assert start_mock.call_count == 1
|
assert start_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_failure(mocker, default_conf, caplog) -> None:
|
||||||
|
start_mock = MagicMock()
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.configuration.Configuration._load_config_file',
|
||||||
|
lambda *args, **kwargs: default_conf
|
||||||
|
)
|
||||||
|
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.start', start_mock)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
|
||||||
|
args = [
|
||||||
|
'--config', 'config.json',
|
||||||
|
'--strategy', 'TestStrategy',
|
||||||
|
'hyperopt',
|
||||||
|
'--epochs', '5'
|
||||||
|
]
|
||||||
|
args = get_args(args)
|
||||||
|
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
start(args)
|
||||||
|
assert log_has(
|
||||||
|
"Please don't use --strategy for hyperopt.",
|
||||||
|
caplog.record_tuples
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_loss_calculation_prefer_correct_trade_count(hyperopt) -> None:
|
def test_loss_calculation_prefer_correct_trade_count(hyperopt) -> None:
|
||||||
StrategyResolver({'strategy': 'DefaultStrategy'})
|
StrategyResolver({'strategy': 'DefaultStrategy'})
|
||||||
|
|
||||||
|
|
|
@ -53,7 +53,7 @@ def _clean_test_file(file: str) -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_load_data_30min_ticker(ticker_history, mocker, caplog, default_conf) -> None:
|
def test_load_data_30min_ticker(ticker_history, mocker, caplog, default_conf) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history)
|
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
|
||||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-30m.json')
|
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-30m.json')
|
||||||
_backup_file(file, copy_file=True)
|
_backup_file(file, copy_file=True)
|
||||||
optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='30m')
|
optimize.load_data(None, pairs=['UNITTEST/BTC'], ticker_interval='30m')
|
||||||
|
@ -63,7 +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:
|
def test_load_data_5min_ticker(ticker_history, mocker, caplog, default_conf) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history)
|
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
|
||||||
|
|
||||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-5m.json')
|
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-5m.json')
|
||||||
_backup_file(file, copy_file=True)
|
_backup_file(file, copy_file=True)
|
||||||
|
@ -74,7 +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:
|
def test_load_data_1min_ticker(ticker_history, mocker, caplog) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history)
|
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
|
||||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json')
|
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'UNITTEST_BTC-1m.json')
|
||||||
_backup_file(file, copy_file=True)
|
_backup_file(file, copy_file=True)
|
||||||
optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
|
optimize.load_data(None, ticker_interval='1m', pairs=['UNITTEST/BTC'])
|
||||||
|
@ -87,7 +87,7 @@ def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog, default_co
|
||||||
"""
|
"""
|
||||||
Test load_data() with 1 min ticker
|
Test load_data() with 1 min ticker
|
||||||
"""
|
"""
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history)
|
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
||||||
|
|
||||||
|
@ -118,7 +118,7 @@ def test_testdata_path() -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_download_pairs(ticker_history, mocker, default_conf) -> None:
|
def test_download_pairs(ticker_history, mocker, default_conf) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history)
|
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
file1_1 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
|
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')
|
file1_5 = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-5m.json')
|
||||||
|
@ -261,7 +261,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_download_pairs_exception(ticker_history, mocker, caplog, default_conf) -> None:
|
def test_download_pairs_exception(ticker_history, mocker, caplog, default_conf) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history)
|
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
|
||||||
mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata',
|
mocker.patch('freqtrade.optimize.__init__.download_backtesting_testdata',
|
||||||
side_effect=BaseException('File Error'))
|
side_effect=BaseException('File Error'))
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
@ -279,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:
|
def test_download_backtesting_testdata(ticker_history, mocker, default_conf) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=ticker_history)
|
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=ticker_history)
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
|
|
||||||
# Download a 1 min ticker file
|
# Download a 1 min ticker file
|
||||||
|
@ -304,7 +304,7 @@ def test_download_backtesting_testdata2(mocker, default_conf) -> None:
|
||||||
[1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199]
|
[1509836580000, 0.00161, 0.00161, 0.00161, 0.00161, 82.390199]
|
||||||
]
|
]
|
||||||
json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
|
json_dump_mock = mocker.patch('freqtrade.misc.file_dump_json', return_value=None)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=tick)
|
mocker.patch('freqtrade.exchange.Exchange.get_history', return_value=tick)
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
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='1m')
|
||||||
download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='3m')
|
download_backtesting_testdata(None, exchange, pair="UNITTEST/BTC", tick_interval='3m')
|
||||||
|
|
|
@ -6,13 +6,14 @@ from unittest.mock import MagicMock, ANY
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade import TemporaryError
|
||||||
from freqtrade.fiat_convert import CryptoToFiatConverter
|
from freqtrade.fiat_convert import CryptoToFiatConverter
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc import RPC, RPCException
|
from freqtrade.rpc import RPC, RPCException
|
||||||
from freqtrade.state import State
|
from freqtrade.state import State
|
||||||
from freqtrade.tests.test_freqtradebot import patch_get_signal
|
from freqtrade.tests.test_freqtradebot import patch_get_signal
|
||||||
from freqtrade.tests.conftest import patch_coinmarketcap
|
from freqtrade.tests.conftest import patch_coinmarketcap, patch_exchange
|
||||||
|
|
||||||
|
|
||||||
# Functions for recurrent object patching
|
# Functions for recurrent object patching
|
||||||
|
@ -29,7 +30,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
_load_markets=MagicMock(return_value={}),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
get_markets=markets
|
get_markets=markets
|
||||||
|
@ -67,10 +68,10 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
|
||||||
|
|
||||||
def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
|
def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
get_markets=markets
|
get_markets=markets
|
||||||
|
@ -98,10 +99,10 @@ def test_rpc_status_table(default_conf, ticker, fee, markets, mocker) -> None:
|
||||||
def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
||||||
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
get_markets=markets
|
get_markets=markets
|
||||||
|
@ -151,11 +152,11 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
)
|
)
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
get_markets=markets
|
get_markets=markets
|
||||||
|
@ -181,7 +182,6 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||||
# Update the ticker with a market going up
|
# Update the ticker with a market going up
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker_sell_up
|
get_ticker=ticker_sell_up
|
||||||
)
|
)
|
||||||
trade.update(limit_sell_order)
|
trade.update(limit_sell_order)
|
||||||
|
@ -196,7 +196,6 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||||
# Update the ticker with a market going up
|
# Update the ticker with a market going up
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker_sell_up
|
get_ticker=ticker_sell_up
|
||||||
)
|
)
|
||||||
trade.update(limit_sell_order)
|
trade.update(limit_sell_order)
|
||||||
|
@ -222,6 +221,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
|
||||||
# trade.open_rate (it is set to None)
|
# trade.open_rate (it is set to None)
|
||||||
def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
|
def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
|
||||||
ticker_sell_up, limit_buy_order, limit_sell_order):
|
ticker_sell_up, limit_buy_order, limit_sell_order):
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.fiat_convert.Market',
|
'freqtrade.fiat_convert.Market',
|
||||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
|
@ -230,7 +230,6 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
get_markets=markets
|
get_markets=markets
|
||||||
|
@ -251,7 +250,6 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee, markets,
|
||||||
# Update the ticker with a market going up
|
# Update the ticker with a market going up
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker_sell_up,
|
get_ticker=ticker_sell_up,
|
||||||
get_fee=fee
|
get_fee=fee
|
||||||
)
|
)
|
||||||
|
@ -285,23 +283,25 @@ def test_rpc_balance_handle(default_conf, mocker):
|
||||||
'used': 2.0,
|
'used': 2.0,
|
||||||
},
|
},
|
||||||
'ETH': {
|
'ETH': {
|
||||||
'free': 0.0,
|
'free': 1.0,
|
||||||
'total': 0.0,
|
'total': 5.0,
|
||||||
'used': 0.0,
|
'used': 4.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
# ETH will be skipped due to mocked Error below
|
||||||
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.fiat_convert.Market',
|
'freqtrade.fiat_convert.Market',
|
||||||
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
ticker=MagicMock(return_value={'price_usd': 15000.0}),
|
||||||
)
|
)
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
get_balances=MagicMock(return_value=mock_balance),
|
||||||
get_balances=MagicMock(return_value=mock_balance)
|
get_ticker=MagicMock(side_effect=TemporaryError('Could not load ticker due to xxx'))
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
|
@ -320,14 +320,15 @@ def test_rpc_balance_handle(default_conf, mocker):
|
||||||
'pending': 2.0,
|
'pending': 2.0,
|
||||||
'est_btc': 12.0,
|
'est_btc': 12.0,
|
||||||
}]
|
}]
|
||||||
|
assert result['total'] == 12.0
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_start(mocker, default_conf) -> None:
|
def test_rpc_start(mocker, default_conf) -> None:
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=MagicMock()
|
get_ticker=MagicMock()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -347,10 +348,10 @@ def test_rpc_start(mocker, default_conf) -> None:
|
||||||
|
|
||||||
def test_rpc_stop(mocker, default_conf) -> None:
|
def test_rpc_stop(mocker, default_conf) -> None:
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=MagicMock()
|
get_ticker=MagicMock()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -371,12 +372,12 @@ def test_rpc_stop(mocker, default_conf) -> None:
|
||||||
|
|
||||||
def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
|
def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
|
|
||||||
cancel_order_mock = MagicMock()
|
cancel_order_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
cancel_order=cancel_order_mock,
|
cancel_order=cancel_order_mock,
|
||||||
get_order=MagicMock(
|
get_order=MagicMock(
|
||||||
|
@ -472,10 +473,10 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker, markets) -> None:
|
||||||
def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
||||||
limit_sell_order, markets, mocker) -> None:
|
limit_sell_order, markets, mocker) -> None:
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_balances=MagicMock(return_value=ticker),
|
get_balances=MagicMock(return_value=ticker),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
|
@ -508,10 +509,10 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
|
||||||
|
|
||||||
def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
|
def test_rpc_count(mocker, default_conf, ticker, fee, markets) -> None:
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_balances=MagicMock(return_value=ticker),
|
get_balances=MagicMock(return_value=ticker),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
|
|
|
@ -177,10 +177,9 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
|
||||||
default_conf['telegram']['chat_id'] = 123
|
default_conf['telegram']['chat_id'] = 123
|
||||||
|
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_pair_detail_url=MagicMock(),
|
get_pair_detail_url=MagicMock(),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
|
@ -228,9 +227,9 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
|
||||||
|
|
||||||
def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
get_markets=markets
|
get_markets=markets
|
||||||
|
@ -273,9 +272,9 @@ 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:
|
def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value={'id': 'mocked_order_id'}),
|
buy=MagicMock(return_value={'id': 'mocked_order_id'}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
|
@ -324,13 +323,13 @@ def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker)
|
||||||
def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||||
limit_sell_order, markets, mocker) -> None:
|
limit_sell_order, markets, mocker) -> None:
|
||||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
|
'freqtrade.rpc.rpc.CryptoToFiatConverter._find_price',
|
||||||
return_value=15000.0
|
return_value=15000.0
|
||||||
)
|
)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
get_markets=markets
|
get_markets=markets
|
||||||
|
@ -395,9 +394,9 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
|
||||||
|
|
||||||
def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
||||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker
|
get_ticker=ticker
|
||||||
)
|
)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
|
@ -431,10 +430,10 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
|
||||||
def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
||||||
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
get_markets=markets
|
get_markets=markets
|
||||||
|
@ -678,7 +677,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee,
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
_load_markets=MagicMock(return_value={}),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
get_markets=markets
|
get_markets=markets
|
||||||
|
@ -727,7 +726,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
_load_markets=MagicMock(return_value={}),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
get_markets=markets
|
get_markets=markets
|
||||||
|
@ -743,7 +742,6 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
|
||||||
# Decrease the price and sell it
|
# Decrease the price and sell it
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker_sell_down
|
get_ticker=ticker_sell_down
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -775,13 +773,13 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
|
||||||
|
|
||||||
def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
||||||
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=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())
|
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_pair_detail_url', MagicMock())
|
mocker.patch('freqtrade.exchange.Exchange.get_pair_detail_url', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
get_markets=markets
|
get_markets=markets
|
||||||
|
@ -827,7 +825,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
_send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
patch_exchange(mocker)
|
||||||
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtradebot, (True, False))
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
|
@ -860,6 +858,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
|
||||||
def test_performance_handle(default_conf, update, ticker, fee,
|
def test_performance_handle(default_conf, update, ticker, fee,
|
||||||
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
limit_buy_order, limit_sell_order, markets, mocker) -> None:
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
|
@ -868,7 +867,6 @@ def test_performance_handle(default_conf, update, ticker, fee,
|
||||||
)
|
)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
get_markets=markets
|
get_markets=markets
|
||||||
|
@ -899,13 +897,13 @@ def test_performance_handle(default_conf, update, ticker, fee,
|
||||||
|
|
||||||
def test_performance_handle_invalid(default_conf, update, mocker) -> None:
|
def test_performance_handle_invalid(default_conf, update, mocker) -> None:
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
_init=MagicMock(),
|
_init=MagicMock(),
|
||||||
_send_msg=msg_mock
|
_send_msg=msg_mock
|
||||||
)
|
)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
|
||||||
freqtradebot = FreqtradeBot(default_conf)
|
freqtradebot = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtradebot, (True, False))
|
patch_get_signal(freqtradebot, (True, False))
|
||||||
telegram = Telegram(freqtradebot)
|
telegram = Telegram(freqtradebot)
|
||||||
|
@ -919,6 +917,7 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None:
|
||||||
|
|
||||||
def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
||||||
patch_coinmarketcap(mocker)
|
patch_coinmarketcap(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.rpc.telegram.Telegram',
|
'freqtrade.rpc.telegram.Telegram',
|
||||||
|
@ -927,7 +926,6 @@ def test_count_handle(default_conf, update, ticker, fee, markets, mocker) -> Non
|
||||||
)
|
)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value={'id': 'mocked_order_id'}),
|
buy=MagicMock(return_value={'id': 'mocked_order_id'}),
|
||||||
get_markets=markets
|
get_markets=markets
|
||||||
|
|
|
@ -8,6 +8,7 @@ from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.arguments import TimeRange
|
from freqtrade.arguments import TimeRange
|
||||||
from freqtrade.optimize.__init__ import load_tickerdata_file
|
from freqtrade.optimize.__init__ import load_tickerdata_file
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.tests.conftest import get_patched_exchange, log_has
|
from freqtrade.tests.conftest import get_patched_exchange, log_has
|
||||||
from freqtrade.strategy.default_strategy import DefaultStrategy
|
from freqtrade.strategy.default_strategy import DefaultStrategy
|
||||||
|
|
||||||
|
@ -88,7 +89,6 @@ def test_get_signal_old_dataframe(default_conf, mocker, caplog):
|
||||||
|
|
||||||
|
|
||||||
def test_get_signal_handles_exceptions(mocker, default_conf):
|
def test_get_signal_handles_exceptions(mocker, default_conf):
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_candle_history', return_value=MagicMock())
|
|
||||||
exchange = get_patched_exchange(mocker, default_conf)
|
exchange = get_patched_exchange(mocker, default_conf)
|
||||||
mocker.patch.object(
|
mocker.patch.object(
|
||||||
_STRATEGY, 'analyze_ticker',
|
_STRATEGY, 'analyze_ticker',
|
||||||
|
@ -105,3 +105,98 @@ def test_tickerdata_to_dataframe(default_conf) -> None:
|
||||||
tickerlist = {'UNITTEST/BTC': tick}
|
tickerlist = {'UNITTEST/BTC': tick}
|
||||||
data = strategy.tickerdata_to_dataframe(tickerlist)
|
data = strategy.tickerdata_to_dataframe(tickerlist)
|
||||||
assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed
|
assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed
|
||||||
|
|
||||||
|
|
||||||
|
def test_min_roi_reached(default_conf, fee) -> None:
|
||||||
|
strategy = DefaultStrategy(default_conf)
|
||||||
|
strategy.minimal_roi = {0: 0.1, 20: 0.05, 55: 0.01}
|
||||||
|
trade = Trade(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
stake_amount=0.001,
|
||||||
|
open_date=arrow.utcnow().shift(hours=-1).datetime,
|
||||||
|
fee_open=fee.return_value,
|
||||||
|
fee_close=fee.return_value,
|
||||||
|
exchange='bittrex',
|
||||||
|
open_rate=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert not strategy.min_roi_reached(trade, 0.01, arrow.utcnow().shift(minutes=-55).datetime)
|
||||||
|
assert strategy.min_roi_reached(trade, 0.12, arrow.utcnow().shift(minutes=-55).datetime)
|
||||||
|
|
||||||
|
assert not strategy.min_roi_reached(trade, 0.04, arrow.utcnow().shift(minutes=-39).datetime)
|
||||||
|
assert strategy.min_roi_reached(trade, 0.06, arrow.utcnow().shift(minutes=-39).datetime)
|
||||||
|
|
||||||
|
assert not strategy.min_roi_reached(trade, -0.01, arrow.utcnow().shift(minutes=-1).datetime)
|
||||||
|
assert strategy.min_roi_reached(trade, 0.02, arrow.utcnow().shift(minutes=-1).datetime)
|
||||||
|
|
||||||
|
|
||||||
|
def test_analyze_ticker_default(ticker_history, mocker, caplog) -> None:
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
ind_mock = MagicMock(side_effect=lambda x, meta: x)
|
||||||
|
buy_mock = MagicMock(side_effect=lambda x, meta: x)
|
||||||
|
sell_mock = MagicMock(side_effect=lambda x, meta: x)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.strategy.interface.IStrategy',
|
||||||
|
advise_indicators=ind_mock,
|
||||||
|
advise_buy=buy_mock,
|
||||||
|
advise_sell=sell_mock,
|
||||||
|
|
||||||
|
)
|
||||||
|
strategy = DefaultStrategy({})
|
||||||
|
strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
|
||||||
|
assert ind_mock.call_count == 1
|
||||||
|
assert buy_mock.call_count == 1
|
||||||
|
assert buy_mock.call_count == 1
|
||||||
|
|
||||||
|
assert log_has('TA Analysis Launched', caplog.record_tuples)
|
||||||
|
assert not log_has('Skippinig TA Analysis for already analyzed candle',
|
||||||
|
caplog.record_tuples)
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
|
||||||
|
# No analysis happens as process_only_new_candles is true
|
||||||
|
assert ind_mock.call_count == 2
|
||||||
|
assert buy_mock.call_count == 2
|
||||||
|
assert buy_mock.call_count == 2
|
||||||
|
assert log_has('TA Analysis Launched', caplog.record_tuples)
|
||||||
|
assert not log_has('Skippinig TA Analysis for already analyzed candle',
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
|
def test_analyze_ticker_skip_analyze(ticker_history, mocker, caplog) -> None:
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
ind_mock = MagicMock(side_effect=lambda x, meta: x)
|
||||||
|
buy_mock = MagicMock(side_effect=lambda x, meta: x)
|
||||||
|
sell_mock = MagicMock(side_effect=lambda x, meta: x)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.strategy.interface.IStrategy',
|
||||||
|
advise_indicators=ind_mock,
|
||||||
|
advise_buy=buy_mock,
|
||||||
|
advise_sell=sell_mock,
|
||||||
|
|
||||||
|
)
|
||||||
|
strategy = DefaultStrategy({})
|
||||||
|
strategy.process_only_new_candles = True
|
||||||
|
|
||||||
|
ret = strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
|
||||||
|
assert ind_mock.call_count == 1
|
||||||
|
assert buy_mock.call_count == 1
|
||||||
|
assert buy_mock.call_count == 1
|
||||||
|
assert log_has('TA Analysis Launched', caplog.record_tuples)
|
||||||
|
assert not log_has('Skippinig TA Analysis for already analyzed candle',
|
||||||
|
caplog.record_tuples)
|
||||||
|
caplog.clear()
|
||||||
|
|
||||||
|
ret = strategy.analyze_ticker(ticker_history, {'pair': 'ETH/BTC'})
|
||||||
|
# No analysis happens as process_only_new_candles is true
|
||||||
|
assert ind_mock.call_count == 1
|
||||||
|
assert buy_mock.call_count == 1
|
||||||
|
assert buy_mock.call_count == 1
|
||||||
|
# only skipped analyze adds buy and sell columns, otherwise it's all mocked
|
||||||
|
assert 'buy' in ret
|
||||||
|
assert 'sell' in ret
|
||||||
|
assert ret['buy'].sum() == 0
|
||||||
|
assert ret['sell'].sum() == 0
|
||||||
|
assert not log_has('TA Analysis Launched', caplog.record_tuples)
|
||||||
|
assert log_has('Skippinig TA Analysis for already analyzed candle',
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
|
@ -130,7 +130,7 @@ def test_strategy_override_minimal_roi(caplog):
|
||||||
assert resolver.strategy.minimal_roi[0] == 0.5
|
assert resolver.strategy.minimal_roi[0] == 0.5
|
||||||
assert ('freqtrade.strategy.resolver',
|
assert ('freqtrade.strategy.resolver',
|
||||||
logging.INFO,
|
logging.INFO,
|
||||||
'Override strategy \'minimal_roi\' with value in config file.'
|
"Override strategy 'minimal_roi' with value in config file: {'0': 0.5}."
|
||||||
) in caplog.record_tuples
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
|
@ -145,7 +145,7 @@ def test_strategy_override_stoploss(caplog):
|
||||||
assert resolver.strategy.stoploss == -0.5
|
assert resolver.strategy.stoploss == -0.5
|
||||||
assert ('freqtrade.strategy.resolver',
|
assert ('freqtrade.strategy.resolver',
|
||||||
logging.INFO,
|
logging.INFO,
|
||||||
'Override strategy \'stoploss\' with value in config file: -0.5.'
|
"Override strategy 'stoploss' with value in config file: -0.5."
|
||||||
) in caplog.record_tuples
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
|
@ -161,7 +161,24 @@ def test_strategy_override_ticker_interval(caplog):
|
||||||
assert resolver.strategy.ticker_interval == 60
|
assert resolver.strategy.ticker_interval == 60
|
||||||
assert ('freqtrade.strategy.resolver',
|
assert ('freqtrade.strategy.resolver',
|
||||||
logging.INFO,
|
logging.INFO,
|
||||||
'Override strategy \'ticker_interval\' with value in config file: 60.'
|
"Override strategy 'ticker_interval' with value in config file: 60."
|
||||||
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
|
def test_strategy_override_process_only_new_candles(caplog):
|
||||||
|
caplog.set_level(logging.INFO)
|
||||||
|
|
||||||
|
config = {
|
||||||
|
'strategy': 'DefaultStrategy',
|
||||||
|
'process_only_new_candles': True
|
||||||
|
}
|
||||||
|
resolver = StrategyResolver(config)
|
||||||
|
|
||||||
|
assert resolver.strategy.process_only_new_candles
|
||||||
|
assert ('freqtrade.strategy.resolver',
|
||||||
|
logging.INFO,
|
||||||
|
"Override process_only_new_candles 'process_only_new_candles' "
|
||||||
|
"with value in config file: True."
|
||||||
) in caplog.record_tuples
|
) in caplog.record_tuples
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,38 +2,39 @@
|
||||||
|
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import freqtrade.tests.conftest as tt # test tools
|
from freqtrade.tests.conftest import get_patched_freqtradebot
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
# whitelist, blacklist, filtering, all of that will
|
# whitelist, blacklist, filtering, all of that will
|
||||||
# eventually become some rules to run on a generic ACL engine
|
# eventually become some rules to run on a generic ACL engine
|
||||||
# perhaps try to anticipate that by using some python package
|
# perhaps try to anticipate that by using some python package
|
||||||
|
|
||||||
|
|
||||||
def whitelist_conf():
|
@pytest.fixture(scope="function")
|
||||||
config = tt.default_conf()
|
def whitelist_conf(default_conf):
|
||||||
config['stake_currency'] = 'BTC'
|
default_conf['stake_currency'] = 'BTC'
|
||||||
config['exchange']['pair_whitelist'] = [
|
default_conf['exchange']['pair_whitelist'] = [
|
||||||
'ETH/BTC',
|
'ETH/BTC',
|
||||||
'TKN/BTC',
|
'TKN/BTC',
|
||||||
'TRST/BTC',
|
'TRST/BTC',
|
||||||
'SWT/BTC',
|
'SWT/BTC',
|
||||||
'BCC/BTC'
|
'BCC/BTC'
|
||||||
]
|
]
|
||||||
config['exchange']['pair_blacklist'] = [
|
default_conf['exchange']['pair_blacklist'] = [
|
||||||
'BLK/BTC'
|
'BLK/BTC'
|
||||||
]
|
]
|
||||||
|
|
||||||
return config
|
return default_conf
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_market_pair_not_in_whitelist(mocker, markets):
|
def test_refresh_market_pair_not_in_whitelist(mocker, markets, whitelist_conf):
|
||||||
conf = whitelist_conf()
|
|
||||||
|
|
||||||
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets)
|
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets)
|
||||||
refreshedwhitelist = freqtradebot._refresh_whitelist(
|
refreshedwhitelist = freqtradebot._refresh_whitelist(
|
||||||
conf['exchange']['pair_whitelist'] + ['XXX/BTC']
|
whitelist_conf['exchange']['pair_whitelist'] + ['XXX/BTC']
|
||||||
)
|
)
|
||||||
# List ordered by BaseVolume
|
# List ordered by BaseVolume
|
||||||
whitelist = ['ETH/BTC', 'TKN/BTC']
|
whitelist = ['ETH/BTC', 'TKN/BTC']
|
||||||
|
@ -41,12 +42,12 @@ def test_refresh_market_pair_not_in_whitelist(mocker, markets):
|
||||||
assert whitelist == refreshedwhitelist
|
assert whitelist == refreshedwhitelist
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_whitelist(mocker, markets):
|
def test_refresh_whitelist(mocker, markets, whitelist_conf):
|
||||||
conf = whitelist_conf()
|
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||||
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets)
|
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets)
|
||||||
refreshedwhitelist = freqtradebot._refresh_whitelist(conf['exchange']['pair_whitelist'])
|
refreshedwhitelist = freqtradebot._refresh_whitelist(
|
||||||
|
whitelist_conf['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
# List ordered by BaseVolume
|
# List ordered by BaseVolume
|
||||||
whitelist = ['ETH/BTC', 'TKN/BTC']
|
whitelist = ['ETH/BTC', 'TKN/BTC']
|
||||||
|
@ -54,9 +55,8 @@ def test_refresh_whitelist(mocker, markets):
|
||||||
assert whitelist == refreshedwhitelist
|
assert whitelist == refreshedwhitelist
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_whitelist_dynamic(mocker, markets, tickers):
|
def test_refresh_whitelist_dynamic(mocker, markets, tickers, whitelist_conf):
|
||||||
conf = whitelist_conf()
|
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||||
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
get_markets=markets,
|
get_markets=markets,
|
||||||
|
@ -68,21 +68,20 @@ def test_refresh_whitelist_dynamic(mocker, markets, tickers):
|
||||||
whitelist = ['ETH/BTC', 'TKN/BTC']
|
whitelist = ['ETH/BTC', 'TKN/BTC']
|
||||||
|
|
||||||
refreshedwhitelist = freqtradebot._refresh_whitelist(
|
refreshedwhitelist = freqtradebot._refresh_whitelist(
|
||||||
freqtradebot._gen_pair_whitelist(conf['stake_currency'])
|
freqtradebot._gen_pair_whitelist(whitelist_conf['stake_currency'])
|
||||||
)
|
)
|
||||||
|
|
||||||
assert whitelist == refreshedwhitelist
|
assert whitelist == refreshedwhitelist
|
||||||
|
|
||||||
|
|
||||||
def test_refresh_whitelist_dynamic_empty(mocker, markets_empty):
|
def test_refresh_whitelist_dynamic_empty(mocker, markets_empty, whitelist_conf):
|
||||||
conf = whitelist_conf()
|
freqtradebot = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||||
freqtradebot = tt.get_patched_freqtradebot(mocker, conf)
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets_empty)
|
mocker.patch('freqtrade.exchange.Exchange.get_markets', markets_empty)
|
||||||
|
|
||||||
# argument: use the whitelist dynamically by exchange-volume
|
# argument: use the whitelist dynamically by exchange-volume
|
||||||
whitelist = []
|
whitelist = []
|
||||||
conf['exchange']['pair_whitelist'] = []
|
whitelist_conf['exchange']['pair_whitelist'] = []
|
||||||
freqtradebot._refresh_whitelist(whitelist)
|
freqtradebot._refresh_whitelist(whitelist)
|
||||||
pairslist = conf['exchange']['pair_whitelist']
|
pairslist = whitelist_conf['exchange']['pair_whitelist']
|
||||||
|
|
||||||
assert set(whitelist) == set(pairslist)
|
assert set(whitelist) == set(pairslist)
|
||||||
|
|
|
@ -43,7 +43,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, value=(True, False)) -> None:
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
freqtrade.strategy.get_signal = lambda e, s, t: value
|
freqtrade.strategy.get_signal = lambda e, s, t: value
|
||||||
freqtrade.exchange.get_candle_history = lambda p, i: None
|
freqtrade.exchange.refresh_tickers = lambda p, i: None
|
||||||
|
|
||||||
|
|
||||||
def patch_RPCManager(mocker) -> MagicMock:
|
def patch_RPCManager(mocker) -> MagicMock:
|
||||||
|
@ -140,7 +140,6 @@ def test_gen_pair_whitelist(mocker, default_conf, tickers) -> None:
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers)
|
mocker.patch('freqtrade.exchange.Exchange.get_tickers', tickers)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||||
# mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
|
|
||||||
|
|
||||||
# Test to retrieved BTC sorted on quoteVolume (default)
|
# Test to retrieved BTC sorted on quoteVolume (default)
|
||||||
whitelist = freqtrade._gen_pair_whitelist(base_currency='BTC')
|
whitelist = freqtrade._gen_pair_whitelist(base_currency='BTC')
|
||||||
|
@ -159,6 +158,15 @@ def test_gen_pair_whitelist(mocker, default_conf, tickers) -> None:
|
||||||
assert whitelist == []
|
assert whitelist == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_gen_pair_whitelist_not_supported(mocker, default_conf, tickers) -> None:
|
||||||
|
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=False))
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException):
|
||||||
|
freqtrade._gen_pair_whitelist(base_currency='BTC')
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skip(reason="Test not implemented")
|
@pytest.mark.skip(reason="Test not implemented")
|
||||||
def test_refresh_whitelist() -> None:
|
def test_refresh_whitelist() -> None:
|
||||||
pass
|
pass
|
||||||
|
@ -166,9 +174,9 @@ def test_refresh_whitelist() -> None:
|
||||||
|
|
||||||
def test_get_trade_stake_amount(default_conf, ticker, limit_buy_order, fee, mocker) -> None:
|
def test_get_trade_stake_amount(default_conf, ticker, limit_buy_order, fee, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 2)
|
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 2)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -184,9 +192,9 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf,
|
||||||
fee,
|
fee,
|
||||||
mocker) -> None:
|
mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5)
|
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5)
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
@ -202,6 +210,7 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf,
|
||||||
markets,
|
markets,
|
||||||
mocker) -> None:
|
mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
|
@ -244,7 +253,7 @@ def test_get_trade_stake_amount_unlimited_amount(default_conf,
|
||||||
|
|
||||||
def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
|
def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
patch_exchange(mocker)
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
freqtrade.strategy.stoploss = -0.05
|
freqtrade.strategy.stoploss = -0.05
|
||||||
# no pair found
|
# no pair found
|
||||||
|
@ -379,9 +388,9 @@ 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:
|
def test_create_trade(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
|
@ -413,9 +422,9 @@ 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,
|
def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order,
|
||||||
fee, markets, mocker) -> None:
|
fee, markets, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5),
|
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5),
|
||||||
|
@ -432,10 +441,10 @@ 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,
|
def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order,
|
||||||
fee, markets, mocker) -> None:
|
fee, markets, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
buy_mock = MagicMock(return_value={'id': limit_buy_order['id']})
|
buy_mock = MagicMock(return_value={'id': limit_buy_order['id']})
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=buy_mock,
|
buy=buy_mock,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
|
@ -453,10 +462,10 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order,
|
||||||
def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order,
|
def test_create_trade_too_small_stake_amount(default_conf, ticker, limit_buy_order,
|
||||||
fee, markets, mocker) -> None:
|
fee, markets, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
buy_mock = MagicMock(return_value={'id': limit_buy_order['id']})
|
buy_mock = MagicMock(return_value={'id': limit_buy_order['id']})
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=buy_mock,
|
buy=buy_mock,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
|
@ -474,9 +483,9 @@ 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,
|
def test_create_trade_limit_reached(default_conf, ticker, limit_buy_order,
|
||||||
fee, markets, mocker) -> None:
|
fee, markets, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
get_balance=MagicMock(return_value=default_conf['stake_amount']),
|
get_balance=MagicMock(return_value=default_conf['stake_amount']),
|
||||||
|
@ -495,9 +504,9 @@ 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:
|
def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
|
@ -518,9 +527,9 @@ 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,
|
def test_create_trade_no_pairs_after_blacklist(default_conf, ticker,
|
||||||
limit_buy_order, fee, markets, mocker) -> None:
|
limit_buy_order, fee, markets, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
|
@ -541,10 +550,9 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None:
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
|
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_candle_history=MagicMock(return_value=20),
|
|
||||||
get_balance=MagicMock(return_value=20),
|
get_balance=MagicMock(return_value=20),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
)
|
)
|
||||||
|
@ -560,9 +568,9 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None:
|
||||||
def test_process_trade_creation(default_conf, ticker, limit_buy_order,
|
def test_process_trade_creation(default_conf, ticker, limit_buy_order,
|
||||||
markets, fee, mocker, caplog) -> None:
|
markets, fee, mocker, caplog) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_markets=markets,
|
get_markets=markets,
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
|
@ -597,9 +605,9 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order,
|
||||||
|
|
||||||
def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> None:
|
def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_markets=markets,
|
get_markets=markets,
|
||||||
buy=MagicMock(side_effect=TemporaryError)
|
buy=MagicMock(side_effect=TemporaryError)
|
||||||
|
@ -616,9 +624,9 @@ def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> Non
|
||||||
|
|
||||||
def test_process_operational_exception(default_conf, ticker, markets, mocker) -> None:
|
def test_process_operational_exception(default_conf, ticker, markets, mocker) -> None:
|
||||||
msg_mock = patch_RPCManager(mocker)
|
msg_mock = patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_markets=markets,
|
get_markets=markets,
|
||||||
buy=MagicMock(side_effect=OperationalException)
|
buy=MagicMock(side_effect=OperationalException)
|
||||||
|
@ -637,9 +645,9 @@ def test_process_operational_exception(default_conf, ticker, markets, mocker) ->
|
||||||
def test_process_trade_handling(
|
def test_process_trade_handling(
|
||||||
default_conf, ticker, limit_buy_order, markets, fee, mocker) -> None:
|
default_conf, ticker, limit_buy_order, markets, fee, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_markets=markets,
|
get_markets=markets,
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
|
@ -664,21 +672,21 @@ def test_balance_fully_ask_side(mocker, default_conf) -> None:
|
||||||
default_conf['bid_strategy']['ask_last_balance'] = 0.0
|
default_conf['bid_strategy']['ask_last_balance'] = 0.0
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
assert freqtrade.get_target_bid({'ask': 20, 'last': 10}) == 20
|
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 20, 'last': 10}) == 20
|
||||||
|
|
||||||
|
|
||||||
def test_balance_fully_last_side(mocker, default_conf) -> None:
|
def test_balance_fully_last_side(mocker, default_conf) -> None:
|
||||||
default_conf['bid_strategy']['ask_last_balance'] = 1.0
|
default_conf['bid_strategy']['ask_last_balance'] = 1.0
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
assert freqtrade.get_target_bid({'ask': 20, 'last': 10}) == 10
|
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 20, 'last': 10}) == 10
|
||||||
|
|
||||||
|
|
||||||
def test_balance_bigger_last_ask(mocker, default_conf) -> None:
|
def test_balance_bigger_last_ask(mocker, default_conf) -> None:
|
||||||
default_conf['bid_strategy']['ask_last_balance'] = 1.0
|
default_conf['bid_strategy']['ask_last_balance'] = 1.0
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
||||||
assert freqtrade.get_target_bid({'ask': 5, 'last': 10}) == 5
|
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 5, 'last': 10}) == 5
|
||||||
|
|
||||||
|
|
||||||
def test_process_maybe_execute_buy(mocker, default_conf) -> None:
|
def test_process_maybe_execute_buy(mocker, default_conf) -> None:
|
||||||
|
@ -763,9 +771,9 @@ def test_process_maybe_execute_sell_exception(mocker, default_conf,
|
||||||
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order,
|
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order,
|
||||||
fee, markets, mocker) -> None:
|
fee, markets, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
'bid': 0.00001172,
|
'bid': 0.00001172,
|
||||||
'ask': 0.00001173,
|
'ask': 0.00001173,
|
||||||
|
@ -806,9 +814,9 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order,
|
||||||
default_conf.update({'experimental': {'use_sell_signal': True}})
|
default_conf.update({'experimental': {'use_sell_signal': True}})
|
||||||
|
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
|
@ -862,9 +870,9 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order,
|
||||||
default_conf.update({'experimental': {'use_sell_signal': True}})
|
default_conf.update({'experimental': {'use_sell_signal': True}})
|
||||||
|
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
|
@ -895,9 +903,9 @@ def test_handle_trade_experimental(
|
||||||
caplog.set_level(logging.DEBUG)
|
caplog.set_level(logging.DEBUG)
|
||||||
default_conf.update({'experimental': {'use_sell_signal': True}})
|
default_conf.update({'experimental': {'use_sell_signal': True}})
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
|
@ -923,9 +931,9 @@ def test_handle_trade_experimental(
|
||||||
def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order,
|
def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order,
|
||||||
fee, markets, mocker) -> None:
|
fee, markets, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
|
@ -951,9 +959,9 @@ 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:
|
def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, fee, mocker) -> None:
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
cancel_order_mock = MagicMock()
|
cancel_order_mock = MagicMock()
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_order=MagicMock(return_value=limit_buy_order_old),
|
get_order=MagicMock(return_value=limit_buy_order_old),
|
||||||
cancel_order=cancel_order_mock,
|
cancel_order=cancel_order_mock,
|
||||||
|
@ -985,13 +993,52 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, fe
|
||||||
assert nb_trades == 0
|
assert nb_trades == 0
|
||||||
|
|
||||||
|
|
||||||
def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker) -> None:
|
def test_check_handle_timedout_buy_exception(default_conf, ticker, limit_buy_order_old,
|
||||||
|
fee, mocker) -> None:
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
cancel_order_mock = MagicMock()
|
cancel_order_mock = MagicMock()
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
|
get_order=MagicMock(side_effect=DependencyException),
|
||||||
|
cancel_order=cancel_order_mock,
|
||||||
|
get_fee=fee
|
||||||
|
)
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
|
trade_buy = Trade(
|
||||||
|
pair='ETH/BTC',
|
||||||
|
open_rate=0.00001099,
|
||||||
|
exchange='bittrex',
|
||||||
|
open_order_id='123456789',
|
||||||
|
amount=90.99181073,
|
||||||
|
fee_open=0.0,
|
||||||
|
fee_close=0.0,
|
||||||
|
stake_amount=1,
|
||||||
|
open_date=arrow.utcnow().shift(minutes=-601).datetime,
|
||||||
|
is_open=True
|
||||||
|
)
|
||||||
|
|
||||||
|
Trade.session.add(trade_buy)
|
||||||
|
|
||||||
|
# check it does cancel buy orders over the time limit
|
||||||
|
freqtrade.check_handle_timedout()
|
||||||
|
assert cancel_order_mock.call_count == 0
|
||||||
|
assert rpc_mock.call_count == 0
|
||||||
|
trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
|
||||||
|
nb_trades = len(trades)
|
||||||
|
assert nb_trades == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, mocker) -> None:
|
||||||
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
|
cancel_order_mock = MagicMock()
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=ticker,
|
||||||
get_order=MagicMock(return_value=limit_sell_order_old),
|
get_order=MagicMock(return_value=limit_sell_order_old),
|
||||||
cancel_order=cancel_order_mock
|
cancel_order=cancel_order_mock
|
||||||
)
|
)
|
||||||
|
@ -1024,9 +1071,9 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old
|
||||||
mocker) -> None:
|
mocker) -> None:
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
cancel_order_mock = MagicMock()
|
cancel_order_mock = MagicMock()
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_order=MagicMock(return_value=limit_buy_order_old_partial),
|
get_order=MagicMock(return_value=limit_buy_order_old_partial),
|
||||||
cancel_order=cancel_order_mock
|
cancel_order=cancel_order_mock
|
||||||
|
@ -1061,6 +1108,7 @@ 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:
|
def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
cancel_order_mock = MagicMock()
|
cancel_order_mock = MagicMock()
|
||||||
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
|
@ -1070,7 +1118,6 @@ def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) -
|
||||||
)
|
)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_order=MagicMock(side_effect=requests.exceptions.RequestException('Oh snap')),
|
get_order=MagicMock(side_effect=requests.exceptions.RequestException('Oh snap')),
|
||||||
cancel_order=cancel_order_mock
|
cancel_order=cancel_order_mock
|
||||||
|
@ -1103,10 +1150,10 @@ def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) -
|
||||||
|
|
||||||
def test_handle_timedout_limit_buy(mocker, default_conf) -> None:
|
def test_handle_timedout_limit_buy(mocker, default_conf) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
cancel_order_mock = MagicMock()
|
cancel_order_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
cancel_order=cancel_order_mock
|
cancel_order=cancel_order_mock
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1125,10 +1172,10 @@ def test_handle_timedout_limit_buy(mocker, default_conf) -> None:
|
||||||
|
|
||||||
def test_handle_timedout_limit_sell(mocker, default_conf) -> None:
|
def test_handle_timedout_limit_sell(mocker, default_conf) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
cancel_order_mock = MagicMock()
|
cancel_order_mock = MagicMock()
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
cancel_order=cancel_order_mock
|
cancel_order=cancel_order_mock
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1149,7 +1196,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
_load_markets=MagicMock(return_value={}),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
get_markets=markets
|
get_markets=markets
|
||||||
|
@ -1166,7 +1213,6 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc
|
||||||
# Increase the price and sell it
|
# Increase the price and sell it
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker_sell_up
|
get_ticker=ticker_sell_up
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1195,7 +1241,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets,
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
_load_markets=MagicMock(return_value={}),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
get_markets=markets
|
get_markets=markets
|
||||||
|
@ -1212,7 +1258,6 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets,
|
||||||
# Decrease the price and sell it
|
# Decrease the price and sell it
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker_sell_down
|
get_ticker=ticker_sell_down
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1243,7 +1288,7 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee,
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
_load_markets=MagicMock(return_value={}),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
get_markets=markets
|
get_markets=markets
|
||||||
|
@ -1260,7 +1305,6 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee,
|
||||||
# Increase the price and sell it
|
# Increase the price and sell it
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker_sell_up
|
get_ticker=ticker_sell_up
|
||||||
)
|
)
|
||||||
freqtrade.config = {}
|
freqtrade.config = {}
|
||||||
|
@ -1289,7 +1333,7 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee,
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
_load_markets=MagicMock(return_value={}),
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
get_markets=markets
|
get_markets=markets
|
||||||
|
@ -1306,7 +1350,6 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee,
|
||||||
# Decrease the price and sell it
|
# Decrease the price and sell it
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=ticker_sell_down
|
get_ticker=ticker_sell_down
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1334,9 +1377,9 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee,
|
||||||
def test_sell_profit_only_enable_profit(default_conf, limit_buy_order,
|
def test_sell_profit_only_enable_profit(default_conf, limit_buy_order,
|
||||||
fee, markets, mocker) -> None:
|
fee, markets, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
'bid': 0.00002172,
|
'bid': 0.00002172,
|
||||||
'ask': 0.00002173,
|
'ask': 0.00002173,
|
||||||
|
@ -1366,9 +1409,9 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order,
|
||||||
def test_sell_profit_only_disable_profit(default_conf, limit_buy_order,
|
def test_sell_profit_only_disable_profit(default_conf, limit_buy_order,
|
||||||
fee, markets, mocker) -> None:
|
fee, markets, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
'bid': 0.00002172,
|
'bid': 0.00002172,
|
||||||
'ask': 0.00002173,
|
'ask': 0.00002173,
|
||||||
|
@ -1396,9 +1439,9 @@ 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:
|
def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, markets, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
'bid': 0.00000172,
|
'bid': 0.00000172,
|
||||||
'ask': 0.00000173,
|
'ask': 0.00000173,
|
||||||
|
@ -1427,9 +1470,9 @@ 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:
|
def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, markets, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
'bid': 0.0000172,
|
'bid': 0.0000172,
|
||||||
'ask': 0.0000173,
|
'ask': 0.0000173,
|
||||||
|
@ -1459,9 +1502,9 @@ 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:
|
def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
'bid': 0.0000172,
|
'bid': 0.0000172,
|
||||||
'ask': 0.0000173,
|
'ask': 0.0000173,
|
||||||
|
@ -1493,9 +1536,9 @@ 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:
|
def test_trailing_stop_loss(default_conf, limit_buy_order, fee, markets, caplog, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
'bid': 0.00000102,
|
'bid': 0.00000102,
|
||||||
'ask': 0.00000103,
|
'ask': 0.00000103,
|
||||||
|
@ -1527,9 +1570,9 @@ def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, markets
|
||||||
caplog, mocker) -> None:
|
caplog, mocker) -> None:
|
||||||
buy_price = limit_buy_order['price']
|
buy_price = limit_buy_order['price']
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
'bid': buy_price - 0.000001,
|
'bid': buy_price - 0.000001,
|
||||||
'ask': buy_price - 0.000001,
|
'ask': buy_price - 0.000001,
|
||||||
|
@ -1585,9 +1628,9 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee,
|
||||||
caplog, mocker, markets) -> None:
|
caplog, mocker, markets) -> None:
|
||||||
buy_price = limit_buy_order['price']
|
buy_price = limit_buy_order['price']
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
'bid': buy_price - 0.000001,
|
'bid': buy_price - 0.000001,
|
||||||
'ask': buy_price - 0.000001,
|
'ask': buy_price - 0.000001,
|
||||||
|
@ -1645,9 +1688,9 @@ def test_trailing_stop_loss_offset(default_conf, limit_buy_order, fee,
|
||||||
def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order,
|
def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order,
|
||||||
fee, markets, mocker) -> None:
|
fee, markets, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
validate_pairs=MagicMock(),
|
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
'bid': 0.00000172,
|
'bid': 0.00000172,
|
||||||
'ask': 0.00000173,
|
'ask': 0.00000173,
|
||||||
|
@ -1681,7 +1724,7 @@ 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):
|
def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, caplog, mocker):
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True))
|
patch_exchange(mocker)
|
||||||
amount = sum(x['amount'] for x in trades_for_order)
|
amount = sum(x['amount'] for x in trades_for_order)
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='LTC/ETH',
|
pair='LTC/ETH',
|
||||||
|
@ -1704,7 +1747,7 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker):
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
|
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
|
||||||
|
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True))
|
patch_exchange(mocker)
|
||||||
amount = buy_order_fee['amount']
|
amount = buy_order_fee['amount']
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='LTC/ETH',
|
pair='LTC/ETH',
|
||||||
|
@ -1727,7 +1770,7 @@ def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, mo
|
||||||
trades_for_order[0]['fee']['currency'] = 'ETH'
|
trades_for_order[0]['fee']['currency'] = 'ETH'
|
||||||
|
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True))
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
||||||
amount = sum(x['amount'] for x in trades_for_order)
|
amount = sum(x['amount'] for x in trades_for_order)
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
|
@ -1749,7 +1792,7 @@ def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, mock
|
||||||
trades_for_order[0]['fee']['cost'] = 0.00094518
|
trades_for_order[0]['fee']['cost'] = 0.00094518
|
||||||
|
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True))
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
||||||
amount = sum(x['amount'] for x in trades_for_order)
|
amount = sum(x['amount'] for x in trades_for_order)
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
|
@ -1768,7 +1811,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):
|
def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, caplog, mocker):
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True))
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order2)
|
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))
|
amount = float(sum(x['amount'] for x in trades_for_order2))
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
|
@ -1793,7 +1836,7 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee
|
||||||
limit_buy_order['fee'] = {'cost': 0.004, 'currency': 'LTC'}
|
limit_buy_order['fee'] = {'cost': 0.004, 'currency': 'LTC'}
|
||||||
|
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True))
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order',
|
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order',
|
||||||
return_value=[trades_for_order])
|
return_value=[trades_for_order])
|
||||||
amount = float(sum(x['amount'] for x in trades_for_order))
|
amount = float(sum(x['amount'] for x in trades_for_order))
|
||||||
|
@ -1819,7 +1862,7 @@ def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order
|
||||||
limit_buy_order['fee'] = {'cost': 0.004}
|
limit_buy_order['fee'] = {'cost': 0.004}
|
||||||
|
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True))
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
|
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=[])
|
||||||
amount = float(sum(x['amount'] for x in trades_for_order))
|
amount = float(sum(x['amount'] for x in trades_for_order))
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
|
@ -1841,7 +1884,7 @@ def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee,
|
||||||
trades_for_order[0]['fee'] = {'cost': 0.008}
|
trades_for_order[0]['fee'] = {'cost': 0.008}
|
||||||
|
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True))
|
patch_exchange(mocker)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
mocker.patch('freqtrade.exchange.Exchange.get_trades_for_order', return_value=trades_for_order)
|
||||||
amount = sum(x['amount'] for x in trades_for_order)
|
amount = sum(x['amount'] for x in trades_for_order)
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
|
@ -1859,7 +1902,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):
|
def test_get_real_amount_open_trade(default_conf, mocker):
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock(return_value=True))
|
patch_exchange(mocker)
|
||||||
amount = 12345
|
amount = 12345
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='LTC/ETH',
|
pair='LTC/ETH',
|
||||||
|
@ -1878,6 +1921,191 @@ def test_get_real_amount_open_trade(default_conf, mocker):
|
||||||
assert freqtrade.get_real_amount(trade, order) == amount
|
assert freqtrade.get_real_amount(trade, order) == amount
|
||||||
|
|
||||||
|
|
||||||
|
def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, markets, mocker,
|
||||||
|
order_book_l2):
|
||||||
|
default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True
|
||||||
|
default_conf['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = 0.1
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_order_book', order_book_l2)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=ticker,
|
||||||
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save state of current whitelist
|
||||||
|
whitelist = deepcopy(default_conf['exchange']['pair_whitelist'])
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
freqtrade.create_trade()
|
||||||
|
|
||||||
|
trade = Trade.query.first()
|
||||||
|
assert trade is not None
|
||||||
|
assert trade.stake_amount == 0.001
|
||||||
|
assert trade.is_open
|
||||||
|
assert trade.open_date is not None
|
||||||
|
assert trade.exchange == 'bittrex'
|
||||||
|
|
||||||
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
|
trade.update(limit_buy_order)
|
||||||
|
|
||||||
|
assert trade.open_rate == 0.00001099
|
||||||
|
assert whitelist == default_conf['exchange']['pair_whitelist']
|
||||||
|
|
||||||
|
|
||||||
|
def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_order,
|
||||||
|
fee, markets, mocker, order_book_l2):
|
||||||
|
default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True
|
||||||
|
# delta is 100 which is impossible to reach. hence check_depth_of_market will return false
|
||||||
|
default_conf['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = 100
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_order_book', order_book_l2)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=ticker,
|
||||||
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
|
)
|
||||||
|
# Save state of current whitelist
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
freqtrade.create_trade()
|
||||||
|
|
||||||
|
trade = Trade.query.first()
|
||||||
|
assert trade is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2, markets) -> None:
|
||||||
|
"""
|
||||||
|
test if function get_target_bid will return the order book price
|
||||||
|
instead of the ask rate
|
||||||
|
"""
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_markets=markets,
|
||||||
|
get_order_book=order_book_l2
|
||||||
|
)
|
||||||
|
default_conf['exchange']['name'] = 'binance'
|
||||||
|
default_conf['bid_strategy']['use_order_book'] = True
|
||||||
|
default_conf['bid_strategy']['order_book_top'] = 2
|
||||||
|
default_conf['bid_strategy']['ask_last_balance'] = 0
|
||||||
|
default_conf['telegram']['enabled'] = False
|
||||||
|
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 0.045, 'last': 0.046}) == 0.043935
|
||||||
|
|
||||||
|
|
||||||
|
def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2, markets) -> None:
|
||||||
|
"""
|
||||||
|
test if function get_target_bid will return the ask rate (since its value is lower)
|
||||||
|
instead of the order book rate (even if enabled)
|
||||||
|
"""
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_markets=markets,
|
||||||
|
get_order_book=order_book_l2
|
||||||
|
)
|
||||||
|
default_conf['exchange']['name'] = 'binance'
|
||||||
|
default_conf['bid_strategy']['use_order_book'] = True
|
||||||
|
default_conf['bid_strategy']['order_book_top'] = 2
|
||||||
|
default_conf['bid_strategy']['ask_last_balance'] = 0
|
||||||
|
default_conf['telegram']['enabled'] = False
|
||||||
|
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 0.042, 'last': 0.046}) == 0.042
|
||||||
|
|
||||||
|
|
||||||
|
def test_order_book_bid_strategy3(default_conf, mocker, order_book_l2, markets) -> None:
|
||||||
|
"""
|
||||||
|
test if function get_target_bid will return ask rate instead
|
||||||
|
of the order book rate
|
||||||
|
"""
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_markets=markets,
|
||||||
|
get_order_book=order_book_l2
|
||||||
|
)
|
||||||
|
default_conf['exchange']['name'] = 'binance'
|
||||||
|
default_conf['bid_strategy']['use_order_book'] = True
|
||||||
|
default_conf['bid_strategy']['order_book_top'] = 1
|
||||||
|
default_conf['bid_strategy']['ask_last_balance'] = 0
|
||||||
|
default_conf['telegram']['enabled'] = False
|
||||||
|
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
|
assert freqtrade.get_target_bid('ETH/BTC', {'ask': 0.03, 'last': 0.029}) == 0.03
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2, markets) -> None:
|
||||||
|
"""
|
||||||
|
test check depth of market
|
||||||
|
"""
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_markets=markets,
|
||||||
|
get_order_book=order_book_l2
|
||||||
|
)
|
||||||
|
default_conf['telegram']['enabled'] = False
|
||||||
|
default_conf['exchange']['name'] = 'binance'
|
||||||
|
default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True
|
||||||
|
# delta is 100 which is impossible to reach. hence function will return false
|
||||||
|
default_conf['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = 100
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
|
||||||
|
conf = default_conf['bid_strategy']['check_depth_of_market']
|
||||||
|
assert freqtrade._check_depth_of_market_buy('ETH/BTC', conf) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order,
|
||||||
|
fee, markets, mocker, order_book_l2) -> None:
|
||||||
|
"""
|
||||||
|
test order book ask strategy
|
||||||
|
"""
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_order_book', order_book_l2)
|
||||||
|
default_conf['exchange']['name'] = 'binance'
|
||||||
|
default_conf['ask_strategy']['use_order_book'] = True
|
||||||
|
default_conf['ask_strategy']['order_book_min'] = 1
|
||||||
|
default_conf['ask_strategy']['order_book_max'] = 2
|
||||||
|
default_conf['telegram']['enabled'] = False
|
||||||
|
patch_RPCManager(mocker)
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=MagicMock(return_value={
|
||||||
|
'bid': 0.00001172,
|
||||||
|
'ask': 0.00001173,
|
||||||
|
'last': 0.00001172
|
||||||
|
}),
|
||||||
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
|
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||||
|
get_fee=fee,
|
||||||
|
get_markets=markets
|
||||||
|
)
|
||||||
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
|
freqtrade.create_trade()
|
||||||
|
|
||||||
|
trade = Trade.query.first()
|
||||||
|
assert trade
|
||||||
|
|
||||||
|
time.sleep(0.01) # Race condition fix
|
||||||
|
trade.update(limit_buy_order)
|
||||||
|
assert trade.is_open is True
|
||||||
|
|
||||||
|
patch_get_signal(freqtrade, value=(False, True))
|
||||||
|
assert freqtrade.handle_trade(trade) is True
|
||||||
|
|
||||||
|
|
||||||
def test_startup_messages(default_conf, mocker):
|
def test_startup_messages(default_conf, mocker):
|
||||||
default_conf['dynamic_whitelist'] = 20
|
default_conf['dynamic_whitelist'] = 20
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# pragma pylint: disable=missing-docstring, C0103
|
# pragma pylint: disable=missing-docstring, C0103
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
import logging
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
|
@ -403,6 +404,7 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
|
||||||
"""
|
"""
|
||||||
Test Database migration (starting with new pairformat)
|
Test Database migration (starting with new pairformat)
|
||||||
"""
|
"""
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
amount = 103.223
|
amount = 103.223
|
||||||
# Always create all columns apart from the last!
|
# Always create all columns apart from the last!
|
||||||
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
|
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
|
||||||
|
@ -471,12 +473,15 @@ def test_migrate_new(mocker, default_conf, fee, caplog):
|
||||||
assert trade.ticker_interval is None
|
assert trade.ticker_interval is None
|
||||||
assert log_has("trying trades_bak1", caplog.record_tuples)
|
assert log_has("trying trades_bak1", caplog.record_tuples)
|
||||||
assert log_has("trying trades_bak2", caplog.record_tuples)
|
assert log_has("trying trades_bak2", caplog.record_tuples)
|
||||||
|
assert log_has("Running database migration - backup available as trades_bak2",
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_migrate_mid_state(mocker, default_conf, fee, caplog):
|
def test_migrate_mid_state(mocker, default_conf, fee, caplog):
|
||||||
"""
|
"""
|
||||||
Test Database migration (starting with new pairformat)
|
Test Database migration (starting with new pairformat)
|
||||||
"""
|
"""
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
amount = 103.223
|
amount = 103.223
|
||||||
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
|
create_table_old = """CREATE TABLE IF NOT EXISTS "trades" (
|
||||||
id INTEGER NOT NULL,
|
id INTEGER NOT NULL,
|
||||||
|
@ -530,6 +535,8 @@ def test_migrate_mid_state(mocker, default_conf, fee, caplog):
|
||||||
assert trade.stop_loss == 0.0
|
assert trade.stop_loss == 0.0
|
||||||
assert trade.initial_stop_loss == 0.0
|
assert trade.initial_stop_loss == 0.0
|
||||||
assert log_has("trying trades_bak0", caplog.record_tuples)
|
assert log_has("trying trades_bak0", caplog.record_tuples)
|
||||||
|
assert log_has("Running database migration - backup available as trades_bak0",
|
||||||
|
caplog.record_tuples)
|
||||||
|
|
||||||
|
|
||||||
def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee):
|
def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee):
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then
|
if [ ! -f "ta-lib/CHANGELOG.TXT" ]; then
|
||||||
tar zxvf ta-lib-0.4.0-src.tar.gz
|
tar zxvf 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 && make && sudo make install && cd ..
|
cd ta-lib && sed -i.bak "s|0.00000001|0.000000000000000001 |g" src/ta_func/ta_utility.h && ./configure && make && sudo make install && cd ..
|
||||||
else
|
else
|
||||||
echo "TA-lib already installed, skipping download and build."
|
echo "TA-lib already installed, skipping download and build."
|
||||||
cd ta-lib && sudo make install && cd ..
|
cd ta-lib && sudo make install && cd ..
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
ccxt==1.17.126
|
ccxt==1.17.363
|
||||||
SQLAlchemy==1.2.10
|
SQLAlchemy==1.2.12
|
||||||
python-telegram-bot==10.1.0
|
python-telegram-bot==11.1.0
|
||||||
arrow==0.12.1
|
arrow==0.12.1
|
||||||
cachetools==2.1.0
|
cachetools==2.1.0
|
||||||
requests==2.19.1
|
requests==2.19.1
|
||||||
urllib3==1.22
|
urllib3==1.22
|
||||||
wrapt==1.10.11
|
wrapt==1.10.11
|
||||||
pandas==0.23.4
|
pandas==0.23.4
|
||||||
scikit-learn==0.19.2
|
scikit-learn==0.20.0
|
||||||
scipy==1.1.0
|
scipy==1.1.0
|
||||||
jsonschema==2.6.0
|
jsonschema==2.6.0
|
||||||
numpy==1.15.0
|
numpy==1.15.2
|
||||||
TA-Lib==0.4.17
|
TA-Lib==0.4.17
|
||||||
pytest==3.7.1
|
pytest==3.8.1
|
||||||
pytest-mock==1.10.0
|
pytest-mock==1.10.0
|
||||||
pytest-cov==2.5.1
|
pytest-asyncio==0.9.0
|
||||||
|
pytest-cov==2.6.0
|
||||||
tabulate==0.8.2
|
tabulate==0.8.2
|
||||||
coinmarketcap==5.0.3
|
coinmarketcap==5.0.3
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
"""This script generate json data from bittrex"""
|
"""This script generate json data"""
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
@ -53,7 +53,8 @@ exchange = Exchange({'key': '',
|
||||||
'dry_run': True,
|
'dry_run': True,
|
||||||
'exchange': {
|
'exchange': {
|
||||||
'name': args.exchange,
|
'name': args.exchange,
|
||||||
'pair_whitelist': []
|
'pair_whitelist': [],
|
||||||
|
'ccxt_rate_limit': False
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
pairs_not_available = []
|
pairs_not_available = []
|
||||||
|
|
|
@ -73,7 +73,7 @@ def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFram
|
||||||
file = Path(args.exportfilename)
|
file = Path(args.exportfilename)
|
||||||
# must align with columns in backtest.py
|
# must align with columns in backtest.py
|
||||||
columns = ["pair", "profit", "opents", "closets", "index", "duration",
|
columns = ["pair", "profit", "opents", "closets", "index", "duration",
|
||||||
"open_rate", "close_rate", "open_at_end"]
|
"open_rate", "close_rate", "open_at_end", "sell_reason"]
|
||||||
with file.open() as f:
|
with file.open() as f:
|
||||||
data = json.load(f)
|
data = json.load(f)
|
||||||
trades = pd.DataFrame(data, columns=columns)
|
trades = pd.DataFrame(data, columns=columns)
|
||||||
|
@ -138,7 +138,8 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
|
||||||
tickers = {}
|
tickers = {}
|
||||||
if args.live:
|
if args.live:
|
||||||
logger.info('Downloading pair.')
|
logger.info('Downloading pair.')
|
||||||
tickers[pair] = exchange.get_candle_history(pair, tick_interval)
|
exchange.refresh_tickers([pair], tick_interval)
|
||||||
|
tickers[pair] = exchange.klines[pair]
|
||||||
else:
|
else:
|
||||||
tickers = optimize.load_data(
|
tickers = optimize.load_data(
|
||||||
datadir=_CONF.get("datadir"),
|
datadir=_CONF.get("datadir"),
|
||||||
|
|
29
setup.sh
29
setup.sh
|
@ -1,13 +1,31 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#encoding=utf8
|
#encoding=utf8
|
||||||
|
|
||||||
|
# Check which python version is installed
|
||||||
|
function check_installed_python() {
|
||||||
|
which python3.7
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "using Python 3.7"
|
||||||
|
PYTHON=python3.7
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
which python3.6
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
echo "using Python 3.6"
|
||||||
|
PYTHON=python3.6
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
function updateenv () {
|
function updateenv () {
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
echo "Update your virtual env"
|
echo "Update your virtual env"
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
source .env/bin/activate
|
source .env/bin/activate
|
||||||
echo "pip3 install in-progress. Please wait..."
|
echo "pip3 install in-progress. Please wait..."
|
||||||
pip3.6 install --quiet --upgrade pip
|
pip3 install --quiet --upgrade pip
|
||||||
pip3 install --quiet -r requirements.txt --upgrade
|
pip3 install --quiet -r requirements.txt --upgrade
|
||||||
pip3 install --quiet -r requirements.txt
|
pip3 install --quiet -r requirements.txt
|
||||||
pip3 install --quiet -e .
|
pip3 install --quiet -e .
|
||||||
|
@ -79,7 +97,7 @@ function reset () {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo
|
echo
|
||||||
python3.6 -m venv .env
|
${PYTHON} -m venv .env
|
||||||
updateenv
|
updateenv
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,7 +201,7 @@ function install () {
|
||||||
install_debian
|
install_debian
|
||||||
else
|
else
|
||||||
echo "This script does not support your OS."
|
echo "This script does not support your OS."
|
||||||
echo "If you have Python3.6, pip, virtualenv, ta-lib you can continue."
|
echo "If you have Python3.6 or Python3.7, pip, virtualenv, ta-lib you can continue."
|
||||||
echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell."
|
echo "Wait 10 seconds to continue the next install steps or use ctrl+c to interrupt this shell."
|
||||||
sleep 10
|
sleep 10
|
||||||
fi
|
fi
|
||||||
|
@ -193,7 +211,7 @@ function install () {
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
echo "Run the bot"
|
echo "Run the bot"
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
echo "You can now use the bot by executing 'source .env/bin/activate; python3.6 freqtrade/main.py'."
|
echo "You can now use the bot by executing 'source .env/bin/activate; python freqtrade/main.py'."
|
||||||
}
|
}
|
||||||
|
|
||||||
function plot () {
|
function plot () {
|
||||||
|
@ -214,6 +232,9 @@ function help () {
|
||||||
echo " -p,--plot Install dependencies for Plotting scripts."
|
echo " -p,--plot Install dependencies for Plotting scripts."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Verify if 3.6 or 3.7 is installed
|
||||||
|
check_installed_python
|
||||||
|
|
||||||
case $* in
|
case $* in
|
||||||
--install|-i)
|
--install|-i)
|
||||||
install
|
install
|
||||||
|
|
|
@ -45,6 +45,9 @@ class TestStrategy(IStrategy):
|
||||||
# Optimal ticker interval for the strategy
|
# Optimal ticker interval for the strategy
|
||||||
ticker_interval = '5m'
|
ticker_interval = '5m'
|
||||||
|
|
||||||
|
# run "populate_indicators" only for new candle
|
||||||
|
ta_on_candle = False
|
||||||
|
|
||||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Adds several different TA indicators to the given DataFrame
|
Adds several different TA indicators to the given DataFrame
|
||||||
|
|
Loading…
Reference in New Issue
Block a user