Merge branch 'develop' into feat/new_args_system
This commit is contained in:
commit
6f01d7f8ea
@ -32,14 +32,15 @@ jobs:
|
|||||||
name: backtest
|
name: backtest
|
||||||
- script:
|
- script:
|
||||||
- cp config.json.example config.json
|
- cp config.json.example config.json
|
||||||
- freqtrade hyperopt --datadir tests/testdata -e 5 --strategy DefaultStrategy --hyperopt DefaultHyperOpt
|
- freqtrade hyperopt --datadir tests/testdata -e 5 --strategy SampleStrategy --hyperopt SampleHyperOpts
|
||||||
name: hyperopt
|
name: hyperopt
|
||||||
- script: flake8
|
- script: flake8
|
||||||
name: flake8
|
name: flake8
|
||||||
- script:
|
- script:
|
||||||
# Test Documentation boxes -
|
# Test Documentation boxes -
|
||||||
# !!! <TYPE>: is not allowed!
|
# !!! <TYPE>: is not allowed!
|
||||||
- grep -Er '^!{3}\s\S+:' docs/*; test $? -ne 0
|
# !!! <TYPE> "title" - Title needs to be quoted!
|
||||||
|
- grep -Er '^!{3}\s\S+:|^!{3}\s\S+\s[^"]' docs/*; test $? -ne 0
|
||||||
name: doc syntax
|
name: doc syntax
|
||||||
- script: mypy freqtrade scripts
|
- script: mypy freqtrade scripts
|
||||||
name: mypy
|
name: mypy
|
||||||
|
@ -215,6 +215,11 @@ If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and
|
|||||||
`emergencysell` is an optional value, which defaults to `market` and is used when creating stoploss on exchange orders fails.
|
`emergencysell` is an optional value, which defaults to `market` and is used when creating stoploss on exchange orders fails.
|
||||||
The below is the default which is used if this is not configured in either strategy or configuration file.
|
The below is the default which is used if this is not configured in either strategy or configuration file.
|
||||||
|
|
||||||
|
Since `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price.
|
||||||
|
`stoploss` defines the stop-price - and limit should be slightly below this. This defaults to 0.99 / 1%.
|
||||||
|
Calculation example: we bought the asset at 100$.
|
||||||
|
Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the stoploss will happen between 95$ and 94.05$.
|
||||||
|
|
||||||
Syntax for Strategy:
|
Syntax for Strategy:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@ -224,7 +229,8 @@ order_types = {
|
|||||||
"emergencysell": "market",
|
"emergencysell": "market",
|
||||||
"stoploss": "market",
|
"stoploss": "market",
|
||||||
"stoploss_on_exchange": False,
|
"stoploss_on_exchange": False,
|
||||||
"stoploss_on_exchange_interval": 60
|
"stoploss_on_exchange_interval": 60,
|
||||||
|
"stoploss_on_exchange_limit_ratio": 0.99,
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -254,7 +260,7 @@ Configuration:
|
|||||||
!!! Note
|
!!! Note
|
||||||
If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new order.
|
If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new order.
|
||||||
|
|
||||||
!!! Warning stoploss_on_exchange failures
|
!!! Warning "Warning: stoploss_on_exchange failures"
|
||||||
If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however this is not advised.
|
If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however this is not advised.
|
||||||
|
|
||||||
### Understand order_time_in_force
|
### Understand order_time_in_force
|
||||||
|
@ -8,7 +8,7 @@ If no additional parameter is specified, freqtrade will download data for `"1m"`
|
|||||||
Exchange and pairs will come from `config.json` (if specified using `-c/--config`).
|
Exchange and pairs will come from `config.json` (if specified using `-c/--config`).
|
||||||
Otherwise `--exchange` becomes mandatory.
|
Otherwise `--exchange` becomes mandatory.
|
||||||
|
|
||||||
!!! Tip Updating existing data
|
!!! Tip "Tip: Updating existing data"
|
||||||
If you already have backtesting data available in your data-directory and would like to refresh this data up to today, use `--days xx` with a number slightly higher than the missing number of days. Freqtrade will keep the available data and only download the missing data.
|
If you already have backtesting data available in your data-directory and would like to refresh this data up to today, use `--days xx` with a number slightly higher than the missing number of days. Freqtrade will keep the available data and only download the missing data.
|
||||||
Be carefull though: If the number is too small (which would result in a few missing days), the whole dataset will be removed and only xx days will be downloaded.
|
Be carefull though: If the number is too small (which would result in a few missing days), the whole dataset will be removed and only xx days will be downloaded.
|
||||||
|
|
||||||
|
@ -204,14 +204,15 @@ This part of the documentation is aimed at maintainers, and shows how to create
|
|||||||
|
|
||||||
### Create release branch
|
### Create release branch
|
||||||
|
|
||||||
``` bash
|
First, pick a commit that's about one week old (to not include latest additions to releases).
|
||||||
# make sure you're in develop branch
|
|
||||||
git checkout develop
|
|
||||||
|
|
||||||
|
``` bash
|
||||||
# create new branch
|
# create new branch
|
||||||
git checkout -b new_release
|
git checkout -b new_release <commitid>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Determine if crucial bugfixes have been made between this commit and the current state, and eventually cherry-pick these.
|
||||||
|
|
||||||
* Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for July 2019). Minor versions can be `2019.7-1` should we need to do a second release that month.
|
* Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for July 2019). Minor versions can be `2019.7-1` should we need to do a second release that month.
|
||||||
* Commit this part
|
* Commit this part
|
||||||
* push that branch to the remote and create a PR against the master branch
|
* push that branch to the remote and create a PR against the master branch
|
||||||
@ -219,23 +220,18 @@ git checkout -b new_release
|
|||||||
### Create changelog from git commits
|
### Create changelog from git commits
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Make sure that both master and develop are up-todate!.
|
Make sure that the master branch is uptodate!
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
# Needs to be done before merging / pulling that branch.
|
# Needs to be done before merging / pulling that branch.
|
||||||
git log --oneline --no-decorate --no-merges master..develop
|
git log --oneline --no-decorate --no-merges master..new_release
|
||||||
```
|
```
|
||||||
|
|
||||||
### Create github release / tag
|
### Create github release / tag
|
||||||
|
|
||||||
Once the PR against master is merged (best right after merging):
|
Once the PR against master is merged (best right after merging):
|
||||||
|
|
||||||
* Use the button "Draft a new release" in the Github UI (subsection releases)
|
* Use the button "Draft a new release" in the Github UI (subsection releases).
|
||||||
* Use the version-number specified as tag.
|
* Use the version-number specified as tag.
|
||||||
* Use "master" as reference (this step comes after the above PR is merged).
|
* Use "master" as reference (this step comes after the above PR is merged).
|
||||||
* Use the above changelog as release comment (as codeblock)
|
* Use the above changelog as release comment (as codeblock).
|
||||||
|
|
||||||
### After-release
|
|
||||||
|
|
||||||
* Update version in develop by postfixing that with `-dev` (`2019.6 -> 2019.6-dev`).
|
|
||||||
* Create a PR against develop to update that branch.
|
|
||||||
|
@ -26,7 +26,7 @@ To update the image, simply run the above commands again and restart your runnin
|
|||||||
|
|
||||||
Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image).
|
Should you require additional libraries, please [build the image yourself](#build-your-own-docker-image).
|
||||||
|
|
||||||
!!! Note Docker image update frequency
|
!!! Note "Docker image update frequency"
|
||||||
The official docker images with tags `master`, `develop` and `latest` are automatically rebuild once a week to keep the base image uptodate.
|
The official docker images with tags `master`, `develop` and `latest` are automatically rebuild once a week to keep the base image uptodate.
|
||||||
In addition to that, every merge to `develop` will trigger a rebuild for `develop` and `latest`.
|
In addition to that, every merge to `develop` will trigger a rebuild for `develop` and `latest`.
|
||||||
|
|
||||||
|
38
docs/faq.md
38
docs/faq.md
@ -55,6 +55,44 @@ If you have restricted pairs in your whitelist, you'll get a warning message in
|
|||||||
If you're an "International" Customer on the Bittrex exchange, then this warning will probably not impact you.
|
If you're an "International" Customer on the Bittrex exchange, then this warning will probably not impact you.
|
||||||
If you're a US customer, the bot will fail to create orders for these pairs, and you should remove them from your Whitelist.
|
If you're a US customer, the bot will fail to create orders for these pairs, and you should remove them from your Whitelist.
|
||||||
|
|
||||||
|
### How do I search the bot logs for something?
|
||||||
|
|
||||||
|
By default, the bot writes its log into stderr stream. This is implemented this way so that you can easily separate the bot's diagnostics messages from Backtesting, Edge and Hyperopt results, output from other various Freqtrade utility subcommands, as well as from the output of your custom `print()`'s you may have inserted into your strategy. So if you need to search the log messages with the grep utility, you need to redirect stderr to stdout and disregard stdout.
|
||||||
|
|
||||||
|
* In unix shells, this normally can be done as simple as:
|
||||||
|
```shell
|
||||||
|
$ freqtrade --some-options 2>&1 >/dev/null | grep 'something'
|
||||||
|
```
|
||||||
|
(note, `2>&1` and `>/dev/null` should be written in this order)
|
||||||
|
|
||||||
|
* Bash interpreter also supports so called process substitution syntax, you can grep the log for a string with it as:
|
||||||
|
```shell
|
||||||
|
$ freqtrade --some-options 2> >(grep 'something') >/dev/null
|
||||||
|
```
|
||||||
|
or
|
||||||
|
```shell
|
||||||
|
$ freqtrade --some-options 2> >(grep -v 'something' 1>&2)
|
||||||
|
```
|
||||||
|
|
||||||
|
* You can also write the copy of Freqtrade log messages to a file with the `--logfile` option:
|
||||||
|
```shell
|
||||||
|
$ freqtrade --logfile /path/to/mylogfile.log --some-options
|
||||||
|
```
|
||||||
|
and then grep it as:
|
||||||
|
```shell
|
||||||
|
$ cat /path/to/mylogfile.log | grep 'something'
|
||||||
|
```
|
||||||
|
or even on the fly, as the bot works and the logfile grows:
|
||||||
|
```shell
|
||||||
|
$ tail -f /path/to/mylogfile.log | grep 'something'
|
||||||
|
```
|
||||||
|
from a separate terminal window.
|
||||||
|
|
||||||
|
On Windows, the `--logfilename` option is also supported by Freqtrade and you can use the `findstr` command to search the log for the string of interest:
|
||||||
|
```
|
||||||
|
> type \path\to\mylogfile.log | findstr "something"
|
||||||
|
```
|
||||||
|
|
||||||
## Hyperopt module
|
## Hyperopt module
|
||||||
|
|
||||||
### How many epoch do I need to get a good Hyperopt result?
|
### How many epoch do I need to get a good Hyperopt result?
|
||||||
|
@ -23,17 +23,23 @@ Configuring hyperopt is similar to writing your own strategy, and many tasks wil
|
|||||||
|
|
||||||
Depending on the space you want to optimize, only some of the below are required:
|
Depending on the space you want to optimize, only some of the below are required:
|
||||||
|
|
||||||
* fill `populate_indicators` - probably a copy from your strategy
|
|
||||||
* fill `buy_strategy_generator` - for buy signal optimization
|
* fill `buy_strategy_generator` - for buy signal optimization
|
||||||
* fill `indicator_space` - for buy signal optimzation
|
* fill `indicator_space` - for buy signal optimzation
|
||||||
* fill `sell_strategy_generator` - for sell signal optimization
|
* fill `sell_strategy_generator` - for sell signal optimization
|
||||||
* fill `sell_indicator_space` - for sell signal optimzation
|
* fill `sell_indicator_space` - for sell signal optimzation
|
||||||
|
|
||||||
Optional, but recommended:
|
!!! Note
|
||||||
|
`populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work.
|
||||||
|
|
||||||
|
Optional - can also be loaded from a strategy:
|
||||||
|
|
||||||
|
* copy `populate_indicators` from your strategy - otherwise default-strategy will be used
|
||||||
* copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used
|
* copy `populate_buy_trend` from your strategy - otherwise default-strategy will be used
|
||||||
* copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used
|
* copy `populate_sell_trend` from your strategy - otherwise default-strategy will be used
|
||||||
|
|
||||||
|
!!! Note
|
||||||
|
Assuming the optional methods are not in your hyperopt file, please use `--strategy AweSomeStrategy` which contains these methods so hyperopt can use these methods instead.
|
||||||
|
|
||||||
Rarely you may also need to override:
|
Rarely you may also need to override:
|
||||||
|
|
||||||
* `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default)
|
* `roi_space` - for custom ROI optimization (if you need the ranges for the ROI parameters in the optimization hyperspace that differ from default)
|
||||||
@ -156,7 +162,7 @@ that minimizes the value of the [loss function](#loss-functions).
|
|||||||
|
|
||||||
The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators.
|
The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators.
|
||||||
When you want to test an indicator that isn't used by the bot currently, remember to
|
When you want to test an indicator that isn't used by the bot currently, remember to
|
||||||
add it to the `populate_indicators()` method in `hyperopt.py`.
|
add it to the `populate_indicators()` method in your custom hyperopt file.
|
||||||
|
|
||||||
## Loss-functions
|
## Loss-functions
|
||||||
|
|
||||||
@ -270,6 +276,14 @@ For example, to use one month of data, pass the following parameter to the hyper
|
|||||||
freqtrade hyperopt --timerange 20180401-20180501
|
freqtrade hyperopt --timerange 20180401-20180501
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Running Hyperopt using methods from a strategy
|
||||||
|
|
||||||
|
Hyperopt can reuse `populate_indicators`, `populate_buy_trend`, `populate_sell_trend` from your strategy, assuming these methods are **not** in your custom hyperopt file, and a strategy is provided.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
freqtrade hyperopt --strategy SampleStrategy --customhyperopt SampleHyperopt
|
||||||
|
```
|
||||||
|
|
||||||
### Running Hyperopt with Smaller Search Space
|
### Running Hyperopt with Smaller Search Space
|
||||||
|
|
||||||
Use the `--spaces` argument to limit the search space used by hyperopt.
|
Use the `--spaces` argument to limit the search space used by hyperopt.
|
||||||
@ -341,8 +355,7 @@ So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that t
|
|||||||
(dataframe['rsi'] < 29.0)
|
(dataframe['rsi'] < 29.0)
|
||||||
```
|
```
|
||||||
|
|
||||||
Translating your whole hyperopt result as the new buy-signal
|
Translating your whole hyperopt result as the new buy-signal would then look like:
|
||||||
would then look like:
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
|
||||||
|
@ -95,29 +95,26 @@ sudo apt-get update
|
|||||||
sudo apt-get install build-essential git
|
sudo apt-get install build-essential git
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Raspberry Pi / Raspbian
|
### 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 the latest [Raspbian Buster lite image](https://www.raspberrypi.org/downloads/raspbian/) from at least September 2019.
|
||||||
|
This image comes with python3.7 preinstalled, making it easy to get freqtrade up and running.
|
||||||
|
|
||||||
The following assumes that miniconda3 is installed and available in your environment. Since the last miniconda3 installation file uses python 3.4, we will update to python 3.6 on this installation.
|
Tested using a Raspberry Pi 3 with the Raspbian Buster lite image, all updates applied.
|
||||||
It's recommended to use (mini)conda for this as installation/compilation of `numpy` and `pandas` takes a long time.
|
|
||||||
|
|
||||||
Additional package to install on your Raspbian, `libffi-dev` required by cryptography (from python-telegram-bot).
|
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
conda config --add channels rpi
|
sudo apt-get install python3-venv libatlas-base-dev
|
||||||
conda install python=3.6
|
git clone https://github.com/freqtrade/freqtrade.git
|
||||||
conda create -n freqtrade python=3.6
|
cd freqtrade
|
||||||
conda activate freqtrade
|
|
||||||
conda install pandas numpy
|
|
||||||
|
|
||||||
sudo apt install libffi-dev
|
bash setup.sh -i
|
||||||
python3 -m pip install -r requirements-common.txt
|
|
||||||
python3 -m pip install -e .
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
!!! Note "Installation duration"
|
||||||
|
Depending on your internet speed and the Raspberry Pi version, installation can take multiple hours to complete.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
This does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`.
|
The above does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`.
|
||||||
We do not advise to run hyperopt on a Raspberry Pi, since this is a very resource-heavy operation, which should be done on powerful machine.
|
We do not advise to run hyperopt on a Raspberry Pi, since this is a very resource-heavy operation, which should be done on powerful machine.
|
||||||
|
|
||||||
### Common
|
### Common
|
||||||
|
@ -103,7 +103,7 @@ The `-p/--pairs` argument can be used to specify pairs you would like to plot.
|
|||||||
Specify custom indicators.
|
Specify custom indicators.
|
||||||
Use `--indicators1` for the main plot and `--indicators2` for the subplot below (if values are in a different range than prices).
|
Use `--indicators1` for the main plot and `--indicators2` for the subplot below (if values are in a different range than prices).
|
||||||
|
|
||||||
!!! tip
|
!!! Tip
|
||||||
You will almost certainly want to specify a custom strategy! This can be done by adding `-s Classname` / `--strategy ClassName` to the command.
|
You will almost certainly want to specify a custom strategy! This can be done by adding `-s Classname` / `--strategy ClassName` to the command.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
|
@ -16,11 +16,11 @@ Sample configuration:
|
|||||||
},
|
},
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Danger Security warning
|
!!! Danger "Security warning"
|
||||||
By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot.
|
By default, the configuration listens on localhost only (so it's not reachable from other systems). We strongly recommend to not expose this API to the internet and choose a strong, unique password, since others will potentially be able to control your bot.
|
||||||
|
|
||||||
!!! Danger Password selection
|
!!! Danger "Password selection"
|
||||||
Please make sure to select a very strong, unique password to protect your bot from unauthorized access.
|
Please make sure to select a very strong, unique password to protect your bot from unauthorized access.
|
||||||
|
|
||||||
You can then access the API by going to `http://127.0.0.1:8080/api/v1/version` to check if the API is running correctly.
|
You can then access the API by going to `http://127.0.0.1:8080/api/v1/version` to check if the API is running correctly.
|
||||||
|
|
||||||
|
@ -51,13 +51,13 @@ freqtrade trade --strategy AwesomeStrategy
|
|||||||
**For the following section we will use the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py)
|
**For the following section we will use the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py)
|
||||||
file as reference.**
|
file as reference.**
|
||||||
|
|
||||||
!!! Note Strategies and Backtesting
|
!!! Note "Strategies and Backtesting"
|
||||||
To avoid problems and unexpected differences between Backtesting and dry/live modes, please be aware
|
To avoid problems and unexpected differences between Backtesting and dry/live modes, please be aware
|
||||||
that during backtesting the full time-interval is passed to the `populate_*()` methods at once.
|
that during backtesting the full time-interval is passed to the `populate_*()` methods at once.
|
||||||
It is therefore best to use vectorized operations (across the whole dataframe, not loops) and
|
It is therefore best to use vectorized operations (across the whole dataframe, not loops) and
|
||||||
avoid index referencing (`df.iloc[-1]`), but instead use `df.shift()` to get to the previous candle.
|
avoid index referencing (`df.iloc[-1]`), but instead use `df.shift()` to get to the previous candle.
|
||||||
|
|
||||||
!!! Warning Using future data
|
!!! Warning "Warning: Using future data"
|
||||||
Since backtesting passes the full time interval to the `populate_*()` methods, the strategy author
|
Since backtesting passes the full time interval to the `populate_*()` methods, the strategy author
|
||||||
needs to take care to avoid having the strategy utilize data from the future.
|
needs to take care to avoid having the strategy utilize data from the future.
|
||||||
Some common patterns for this are listed in the [Common Mistakes](#common-mistakes-when-developing-strategies) section of this document.
|
Some common patterns for this are listed in the [Common Mistakes](#common-mistakes-when-developing-strategies) section of this document.
|
||||||
@ -330,12 +330,12 @@ if self.dp:
|
|||||||
ticker_interval=inf_timeframe)
|
ticker_interval=inf_timeframe)
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Warning Warning about backtesting
|
!!! Warning "Warning about backtesting"
|
||||||
Be carefull when using dataprovider in backtesting. `historic_ohlcv()` (and `get_pair_dataframe()`
|
Be carefull when using dataprovider in backtesting. `historic_ohlcv()` (and `get_pair_dataframe()`
|
||||||
for the backtesting runmode) provides the full time-range in one go,
|
for the backtesting runmode) provides the full time-range in one go,
|
||||||
so please be aware of it and make sure to not "look into the future" to avoid surprises when running in dry/live mode).
|
so please be aware of it and make sure to not "look into the future" to avoid surprises when running in dry/live mode).
|
||||||
|
|
||||||
!!! Warning Warning in hyperopt
|
!!! Warning "Warning in hyperopt"
|
||||||
This option cannot currently be used during hyperopt.
|
This option cannot currently be used during hyperopt.
|
||||||
|
|
||||||
#### Orderbook
|
#### Orderbook
|
||||||
@ -405,6 +405,52 @@ if self.wallets:
|
|||||||
- `get_used(asset)` - currently tied up balance (open orders)
|
- `get_used(asset)` - currently tied up balance (open orders)
|
||||||
- `get_total(asset)` - total available balance - sum of the 2 above
|
- `get_total(asset)` - total available balance - sum of the 2 above
|
||||||
|
|
||||||
|
### Additional data (Trades)
|
||||||
|
|
||||||
|
A history of Trades can be retrieved in the strategy by querying the database.
|
||||||
|
|
||||||
|
At the top of the file, import Trade.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
```
|
||||||
|
|
||||||
|
The following example queries for the current pair and trades from today, however other filters can easily be added.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
if self.config['runmode'] in ('live', 'dry_run'):
|
||||||
|
trades = Trade.get_trades([Trade.pair == metadata['pair'],
|
||||||
|
Trade.open_date > datetime.utcnow() - timedelta(days=1),
|
||||||
|
Trade.is_open == False,
|
||||||
|
]).order_by(Trade.close_date).all()
|
||||||
|
# Summarize profit for this pair.
|
||||||
|
curdayprofit = sum(trade.close_profit for trade in trades)
|
||||||
|
```
|
||||||
|
|
||||||
|
Get amount of stake_currency currently invested in Trades:
|
||||||
|
|
||||||
|
``` python
|
||||||
|
if self.config['runmode'] in ('live', 'dry_run'):
|
||||||
|
total_stakes = Trade.total_open_trades_stakes()
|
||||||
|
```
|
||||||
|
|
||||||
|
Retrieve performance per pair.
|
||||||
|
Returns a List of dicts per pair.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
if self.config['runmode'] in ('live', 'dry_run'):
|
||||||
|
performance = Trade.get_overall_performance()
|
||||||
|
```
|
||||||
|
|
||||||
|
Sample return value: ETH/BTC had 5 trades, with a total profit of 1.5% (ratio of 0.015).
|
||||||
|
|
||||||
|
``` json
|
||||||
|
{'pair': "ETH/BTC", 'profit': 0.015, 'count': 5}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Warning
|
||||||
|
Trade history is not available during backtesting or hyperopt.
|
||||||
|
|
||||||
### Print created dataframe
|
### Print created dataframe
|
||||||
|
|
||||||
To inspect the created dataframe, you can issue a print-statement in either `populate_buy_trend()` or `populate_sell_trend()`.
|
To inspect the created dataframe, you can issue a print-statement in either `populate_buy_trend()` or `populate_sell_trend()`.
|
||||||
|
@ -107,6 +107,22 @@ trades = load_trades_from_db("sqlite:///tradesv3.sqlite")
|
|||||||
trades.groupby("pair")["sell_reason"].value_counts()
|
trades.groupby("pair")["sell_reason"].value_counts()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Analyze the loaded trades for trade parallelism
|
||||||
|
This can be useful to find the best `max_open_trades` parameter, when used with backtesting in conjunction with `--disable-max-market-positions`.
|
||||||
|
|
||||||
|
`analyze_trade_parallelism()` returns a timeseries dataframe with an "open_trades" column, specifying the number of open trades for each candle.
|
||||||
|
|
||||||
|
|
||||||
|
```python
|
||||||
|
from freqtrade.data.btanalysis import analyze_trade_parallelism
|
||||||
|
|
||||||
|
# Analyze the above
|
||||||
|
parallel_trades = analyze_trade_parallelism(trades, '5m')
|
||||||
|
|
||||||
|
|
||||||
|
parallel_trades.plot()
|
||||||
|
```
|
||||||
|
|
||||||
## Plot results
|
## Plot results
|
||||||
|
|
||||||
Freqtrade offers interactive plotting capabilities based on plotly.
|
Freqtrade offers interactive plotting capabilities based on plotly.
|
||||||
|
@ -93,7 +93,7 @@ Once all positions are sold, run `/stop` to completely stop the bot.
|
|||||||
|
|
||||||
`/reload_conf` resets "max_open_trades" to the value set in the configuration and resets this command.
|
`/reload_conf` resets "max_open_trades" to the value set in the configuration and resets this command.
|
||||||
|
|
||||||
!!! warning
|
!!! Warning
|
||||||
The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset.
|
The stop-buy signal is ONLY active while the bot is running, and is not persisted anyway, so restarting the bot will cause this to reset.
|
||||||
|
|
||||||
### /status
|
### /status
|
||||||
|
@ -21,7 +21,8 @@ def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool:
|
|||||||
and thus is not known for the Freqtrade at all.
|
and thus is not known for the Freqtrade at all.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if config['runmode'] in [RunMode.PLOT] and not config.get('exchange', {}).get('name'):
|
if (config['runmode'] in [RunMode.PLOT, RunMode.UTIL_NO_EXCHANGE]
|
||||||
|
and not config.get('exchange', {}).get('name')):
|
||||||
# Skip checking exchange in plot mode, since it requires no exchange
|
# Skip checking exchange in plot mode, since it requires no exchange
|
||||||
return True
|
return True
|
||||||
logger.info("Checking exchange...")
|
logger.info("Checking exchange...")
|
||||||
|
@ -118,7 +118,8 @@ def _validate_whitelist(conf: Dict[str, Any]) -> None:
|
|||||||
"""
|
"""
|
||||||
Dynamic whitelist does not require pair_whitelist to be set - however StaticWhitelist does.
|
Dynamic whitelist does not require pair_whitelist to be set - however StaticWhitelist does.
|
||||||
"""
|
"""
|
||||||
if conf.get('runmode', RunMode.OTHER) in [RunMode.OTHER, RunMode.PLOT]:
|
if conf.get('runmode', RunMode.OTHER) in [RunMode.OTHER, RunMode.PLOT,
|
||||||
|
RunMode.UTIL_NO_EXCHANGE, RunMode.UTIL_EXCHANGE]:
|
||||||
return
|
return
|
||||||
|
|
||||||
if (conf.get('pairlist', {}).get('method', 'StaticPairList') == 'StaticPairList'
|
if (conf.get('pairlist', {}).get('method', 'StaticPairList') == 'StaticPairList'
|
||||||
|
@ -17,7 +17,7 @@ from freqtrade.configuration.directory_operations import (create_datadir,
|
|||||||
from freqtrade.configuration.load_config import load_config_file
|
from freqtrade.configuration.load_config import load_config_file
|
||||||
from freqtrade.loggers import setup_logging
|
from freqtrade.loggers import setup_logging
|
||||||
from freqtrade.misc import deep_merge_dicts, json_load
|
from freqtrade.misc import deep_merge_dicts, json_load
|
||||||
from freqtrade.state import RunMode
|
from freqtrade.state import RunMode, TRADING_MODES, NON_UTIL_MODES
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -98,14 +98,16 @@ class Configuration:
|
|||||||
# Keep a copy of the original configuration file
|
# Keep a copy of the original configuration file
|
||||||
config['original_config'] = deepcopy(config)
|
config['original_config'] = deepcopy(config)
|
||||||
|
|
||||||
|
self._process_runmode(config)
|
||||||
|
|
||||||
self._process_common_options(config)
|
self._process_common_options(config)
|
||||||
|
|
||||||
|
self._process_trading_options(config)
|
||||||
|
|
||||||
self._process_optimize_options(config)
|
self._process_optimize_options(config)
|
||||||
|
|
||||||
self._process_plot_options(config)
|
self._process_plot_options(config)
|
||||||
|
|
||||||
self._process_runmode(config)
|
|
||||||
|
|
||||||
# Check if the exchange set by the user is supported
|
# Check if the exchange set by the user is supported
|
||||||
check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True))
|
check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True))
|
||||||
|
|
||||||
@ -130,6 +132,22 @@ class Configuration:
|
|||||||
|
|
||||||
setup_logging(config)
|
setup_logging(config)
|
||||||
|
|
||||||
|
def _process_trading_options(self, config: Dict[str, Any]) -> None:
|
||||||
|
if config['runmode'] not in TRADING_MODES:
|
||||||
|
return
|
||||||
|
|
||||||
|
if config.get('dry_run', False):
|
||||||
|
logger.info('Dry run is enabled')
|
||||||
|
if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]:
|
||||||
|
# Default to in-memory db for dry_run if not specified
|
||||||
|
config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL
|
||||||
|
else:
|
||||||
|
if not config.get('db_url', None):
|
||||||
|
config['db_url'] = constants.DEFAULT_DB_PROD_URL
|
||||||
|
logger.info('Dry run is disabled')
|
||||||
|
|
||||||
|
logger.info(f'Using DB: "{config["db_url"]}"')
|
||||||
|
|
||||||
def _process_common_options(self, config: Dict[str, Any]) -> None:
|
def _process_common_options(self, config: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
self._process_logging_options(config)
|
self._process_logging_options(config)
|
||||||
@ -146,25 +164,9 @@ class Configuration:
|
|||||||
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 ...')
|
||||||
|
|
||||||
if config.get('dry_run', False):
|
|
||||||
logger.info('Dry run is enabled')
|
|
||||||
if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]:
|
|
||||||
# Default to in-memory db for dry_run if not specified
|
|
||||||
config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL
|
|
||||||
else:
|
|
||||||
if not config.get('db_url', None):
|
|
||||||
config['db_url'] = constants.DEFAULT_DB_PROD_URL
|
|
||||||
logger.info('Dry run is disabled')
|
|
||||||
|
|
||||||
logger.info(f'Using DB: "{config["db_url"]}"')
|
|
||||||
|
|
||||||
if config.get('forcebuy_enable', False):
|
if config.get('forcebuy_enable', False):
|
||||||
logger.warning('`forcebuy` RPC message enabled.')
|
logger.warning('`forcebuy` RPC message enabled.')
|
||||||
|
|
||||||
# Setting max_open_trades to infinite if -1
|
|
||||||
if config.get('max_open_trades') == -1:
|
|
||||||
config['max_open_trades'] = float('inf')
|
|
||||||
|
|
||||||
# Support for sd_notify
|
# Support for sd_notify
|
||||||
if 'sd_notify' in self.args and self.args["sd_notify"]:
|
if 'sd_notify' in self.args and self.args["sd_notify"]:
|
||||||
config['internals'].update({'sd_notify': True})
|
config['internals'].update({'sd_notify': True})
|
||||||
@ -216,6 +218,10 @@ class Configuration:
|
|||||||
self._args_to_config(config, argname='position_stacking',
|
self._args_to_config(config, argname='position_stacking',
|
||||||
logstring='Parameter --enable-position-stacking detected ...')
|
logstring='Parameter --enable-position-stacking detected ...')
|
||||||
|
|
||||||
|
# Setting max_open_trades to infinite if -1
|
||||||
|
if config.get('max_open_trades') == -1:
|
||||||
|
config['max_open_trades'] = float('inf')
|
||||||
|
|
||||||
if 'use_max_market_positions' in self.args and not self.args["use_max_market_positions"]:
|
if 'use_max_market_positions' in self.args and not self.args["use_max_market_positions"]:
|
||||||
config.update({'use_max_market_positions': False})
|
config.update({'use_max_market_positions': False})
|
||||||
logger.info('Parameter --disable-max-market-positions detected ...')
|
logger.info('Parameter --disable-max-market-positions detected ...')
|
||||||
@ -224,7 +230,7 @@ class Configuration:
|
|||||||
config.update({'max_open_trades': self.args["max_open_trades"]})
|
config.update({'max_open_trades': self.args["max_open_trades"]})
|
||||||
logger.info('Parameter --max_open_trades detected, '
|
logger.info('Parameter --max_open_trades detected, '
|
||||||
'overriding max_open_trades to: %s ...', config.get('max_open_trades'))
|
'overriding max_open_trades to: %s ...', config.get('max_open_trades'))
|
||||||
else:
|
elif config['runmode'] in NON_UTIL_MODES:
|
||||||
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
|
logger.info('Using max_open_trades: %s ...', config.get('max_open_trades'))
|
||||||
|
|
||||||
self._args_to_config(config, argname='stake_amount',
|
self._args_to_config(config, argname='stake_amount',
|
||||||
|
@ -52,16 +52,18 @@ def load_backtest_data(filename) -> pd.DataFrame:
|
|||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
def evaluate_result_multi(results: pd.DataFrame, freq: str, max_open_trades: int) -> pd.DataFrame:
|
def analyze_trade_parallelism(results: pd.DataFrame, timeframe: str) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Find overlapping trades by expanding each trade once per period it was open
|
Find overlapping trades by expanding each trade once per period it was open
|
||||||
and then counting overlaps
|
and then counting overlaps.
|
||||||
:param results: Results Dataframe - can be loaded
|
:param results: Results Dataframe - can be loaded
|
||||||
:param freq: Frequency used for the backtest
|
:param timeframe: Timeframe used for backtest
|
||||||
:param max_open_trades: parameter max_open_trades used during backtest run
|
:return: dataframe with open-counts per time-period in timeframe
|
||||||
:return: dataframe with open-counts per time-period in freq
|
|
||||||
"""
|
"""
|
||||||
dates = [pd.Series(pd.date_range(row[1].open_time, row[1].close_time, freq=freq))
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
|
timeframe_min = timeframe_to_minutes(timeframe)
|
||||||
|
dates = [pd.Series(pd.date_range(row[1].open_time, row[1].close_time,
|
||||||
|
freq=f"{timeframe_min}min"))
|
||||||
for row in results[['open_time', 'close_time']].iterrows()]
|
for row in results[['open_time', 'close_time']].iterrows()]
|
||||||
deltas = [len(x) for x in dates]
|
deltas = [len(x) for x in dates]
|
||||||
dates = pd.Series(pd.concat(dates).values, name='date')
|
dates = pd.Series(pd.concat(dates).values, name='date')
|
||||||
@ -69,8 +71,23 @@ def evaluate_result_multi(results: pd.DataFrame, freq: str, max_open_trades: int
|
|||||||
|
|
||||||
df2 = pd.concat([dates, df2], axis=1)
|
df2 = pd.concat([dates, df2], axis=1)
|
||||||
df2 = df2.set_index('date')
|
df2 = df2.set_index('date')
|
||||||
df_final = df2.resample(freq)[['pair']].count()
|
df_final = df2.resample(f"{timeframe_min}min")[['pair']].count()
|
||||||
return df_final[df_final['pair'] > max_open_trades]
|
df_final = df_final.rename({'pair': 'open_trades'}, axis=1)
|
||||||
|
return df_final
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_result_multi(results: pd.DataFrame, timeframe: str,
|
||||||
|
max_open_trades: int) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Find overlapping trades by expanding each trade once per period it was open
|
||||||
|
and then counting overlaps
|
||||||
|
:param results: Results Dataframe - can be loaded
|
||||||
|
:param timeframe: Frequency used for the backtest
|
||||||
|
:param max_open_trades: parameter max_open_trades used during backtest run
|
||||||
|
:return: dataframe with open-counts per time-period in freq
|
||||||
|
"""
|
||||||
|
df_final = analyze_trade_parallelism(results, timeframe)
|
||||||
|
return df_final[df_final['open_trades'] > max_open_trades]
|
||||||
|
|
||||||
|
|
||||||
def load_trades_from_db(db_url: str) -> pd.DataFrame:
|
def load_trades_from_db(db_url: str) -> pd.DataFrame:
|
||||||
@ -106,7 +123,7 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame:
|
|||||||
t.stop_loss, t.initial_stop_loss,
|
t.stop_loss, t.initial_stop_loss,
|
||||||
t.strategy, t.ticker_interval
|
t.strategy, t.ticker_interval
|
||||||
)
|
)
|
||||||
for t in Trade.query.all()],
|
for t in Trade.get_trades().all()],
|
||||||
columns=columns)
|
columns=columns)
|
||||||
|
|
||||||
return trades
|
return trades
|
||||||
|
@ -148,7 +148,6 @@ def load_pair_history(pair: str,
|
|||||||
|
|
||||||
timerange_startup = deepcopy(timerange)
|
timerange_startup = deepcopy(timerange)
|
||||||
if startup_candles > 0 and timerange_startup:
|
if startup_candles > 0 and timerange_startup:
|
||||||
logger.info('Using indicator startup period: %s ...', startup_candles)
|
|
||||||
timerange_startup.subtract_start(timeframe_to_seconds(ticker_interval) * startup_candles)
|
timerange_startup.subtract_start(timeframe_to_seconds(ticker_interval) * startup_candles)
|
||||||
|
|
||||||
# The user forced the refresh of pairs
|
# The user forced the refresh of pairs
|
||||||
@ -204,6 +203,8 @@ def load_data(datadir: Path,
|
|||||||
exchange and refresh_pairs are then not needed here nor in load_pair_history.
|
exchange and refresh_pairs are then not needed here nor in load_pair_history.
|
||||||
"""
|
"""
|
||||||
result: Dict[str, DataFrame] = {}
|
result: Dict[str, DataFrame] = {}
|
||||||
|
if startup_candles > 0 and timerange:
|
||||||
|
logger.info(f'Using indicator startup period: {startup_candles} ...')
|
||||||
|
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
hist = load_pair_history(pair=pair, ticker_interval=ticker_interval,
|
hist = load_pair_history(pair=pair, ticker_interval=ticker_interval,
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from freqtrade.exchange.exchange import Exchange, MAP_EXCHANGE_CHILDCLASS # noqa: F401
|
from freqtrade.exchange.common import MAP_EXCHANGE_CHILDCLASS # noqa: F401
|
||||||
|
from freqtrade.exchange.exchange import Exchange # noqa: F401
|
||||||
from freqtrade.exchange.exchange import (get_exchange_bad_reason, # noqa: F401
|
from freqtrade.exchange.exchange import (get_exchange_bad_reason, # noqa: F401
|
||||||
is_exchange_bad,
|
is_exchange_bad,
|
||||||
is_exchange_known_ccxt,
|
is_exchange_known_ccxt,
|
||||||
|
124
freqtrade/exchange/common.py
Normal file
124
freqtrade/exchange/common.py
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from freqtrade import DependencyException, TemporaryError
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
API_RETRY_COUNT = 4
|
||||||
|
BAD_EXCHANGES = {
|
||||||
|
"bitmex": "Various reasons.",
|
||||||
|
"bitstamp": "Does not provide history. "
|
||||||
|
"Details in https://github.com/freqtrade/freqtrade/issues/1983",
|
||||||
|
"hitbtc": "This API cannot be used with Freqtrade. "
|
||||||
|
"Use `hitbtc2` exchange id to access this exchange.",
|
||||||
|
**dict.fromkeys([
|
||||||
|
'adara',
|
||||||
|
'anxpro',
|
||||||
|
'bigone',
|
||||||
|
'coinbase',
|
||||||
|
'coinexchange',
|
||||||
|
'coinmarketcap',
|
||||||
|
'lykke',
|
||||||
|
'xbtce',
|
||||||
|
], "Does not provide timeframes. ccxt fetchOHLCV: False"),
|
||||||
|
**dict.fromkeys([
|
||||||
|
'bcex',
|
||||||
|
'bit2c',
|
||||||
|
'bitbay',
|
||||||
|
'bitflyer',
|
||||||
|
'bitforex',
|
||||||
|
'bithumb',
|
||||||
|
'bitso',
|
||||||
|
'bitstamp1',
|
||||||
|
'bl3p',
|
||||||
|
'braziliex',
|
||||||
|
'btcbox',
|
||||||
|
'btcchina',
|
||||||
|
'btctradeim',
|
||||||
|
'btctradeua',
|
||||||
|
'bxinth',
|
||||||
|
'chilebit',
|
||||||
|
'coincheck',
|
||||||
|
'coinegg',
|
||||||
|
'coinfalcon',
|
||||||
|
'coinfloor',
|
||||||
|
'coingi',
|
||||||
|
'coinmate',
|
||||||
|
'coinone',
|
||||||
|
'coinspot',
|
||||||
|
'coolcoin',
|
||||||
|
'crypton',
|
||||||
|
'deribit',
|
||||||
|
'exmo',
|
||||||
|
'exx',
|
||||||
|
'flowbtc',
|
||||||
|
'foxbit',
|
||||||
|
'fybse',
|
||||||
|
# 'hitbtc',
|
||||||
|
'ice3x',
|
||||||
|
'independentreserve',
|
||||||
|
'indodax',
|
||||||
|
'itbit',
|
||||||
|
'lakebtc',
|
||||||
|
'latoken',
|
||||||
|
'liquid',
|
||||||
|
'livecoin',
|
||||||
|
'luno',
|
||||||
|
'mixcoins',
|
||||||
|
'negociecoins',
|
||||||
|
'nova',
|
||||||
|
'paymium',
|
||||||
|
'southxchange',
|
||||||
|
'stronghold',
|
||||||
|
'surbitcoin',
|
||||||
|
'therock',
|
||||||
|
'tidex',
|
||||||
|
'vaultoro',
|
||||||
|
'vbtc',
|
||||||
|
'virwox',
|
||||||
|
'yobit',
|
||||||
|
'zaif',
|
||||||
|
], "Does not provide timeframes. ccxt fetchOHLCV: emulated"),
|
||||||
|
}
|
||||||
|
|
||||||
|
MAP_EXCHANGE_CHILDCLASS = {
|
||||||
|
'binanceus': 'binance',
|
||||||
|
'binanceje': 'binance',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 wrapper(*args, **kwargs):
|
||||||
|
count = kwargs.pop('count', API_RETRY_COUNT)
|
||||||
|
try:
|
||||||
|
return 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 wrapper(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
logger.warning('Giving up retrying: %s()', f.__name__)
|
||||||
|
raise ex
|
||||||
|
return wrapper
|
@ -14,137 +14,18 @@ from typing import Any, Dict, List, Optional, Tuple
|
|||||||
import arrow
|
import arrow
|
||||||
import ccxt
|
import ccxt
|
||||||
import ccxt.async_support as ccxt_async
|
import ccxt.async_support as ccxt_async
|
||||||
from ccxt.base.decimal_to_precision import ROUND_UP, ROUND_DOWN
|
from ccxt.base.decimal_to_precision import ROUND_DOWN, ROUND_UP
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade import (DependencyException, InvalidOrderException,
|
from freqtrade import (DependencyException, InvalidOrderException,
|
||||||
OperationalException, TemporaryError, constants)
|
OperationalException, TemporaryError, constants)
|
||||||
from freqtrade.data.converter import parse_ticker_dataframe
|
from freqtrade.data.converter import parse_ticker_dataframe
|
||||||
|
from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async
|
||||||
from freqtrade.misc import deep_merge_dicts
|
from freqtrade.misc import deep_merge_dicts
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
API_RETRY_COUNT = 4
|
|
||||||
BAD_EXCHANGES = {
|
|
||||||
"bitmex": "Various reasons.",
|
|
||||||
"bitstamp": "Does not provide history. "
|
|
||||||
"Details in https://github.com/freqtrade/freqtrade/issues/1983",
|
|
||||||
"hitbtc": "This API cannot be used with Freqtrade. "
|
|
||||||
"Use `hitbtc2` exchange id to access this exchange.",
|
|
||||||
**dict.fromkeys([
|
|
||||||
'adara',
|
|
||||||
'anxpro',
|
|
||||||
'bigone',
|
|
||||||
'coinbase',
|
|
||||||
'coinexchange',
|
|
||||||
'coinmarketcap',
|
|
||||||
'lykke',
|
|
||||||
'xbtce',
|
|
||||||
], "Does not provide timeframes. ccxt fetchOHLCV: False"),
|
|
||||||
**dict.fromkeys([
|
|
||||||
'bcex',
|
|
||||||
'bit2c',
|
|
||||||
'bitbay',
|
|
||||||
'bitflyer',
|
|
||||||
'bitforex',
|
|
||||||
'bithumb',
|
|
||||||
'bitso',
|
|
||||||
'bitstamp1',
|
|
||||||
'bl3p',
|
|
||||||
'braziliex',
|
|
||||||
'btcbox',
|
|
||||||
'btcchina',
|
|
||||||
'btctradeim',
|
|
||||||
'btctradeua',
|
|
||||||
'bxinth',
|
|
||||||
'chilebit',
|
|
||||||
'coincheck',
|
|
||||||
'coinegg',
|
|
||||||
'coinfalcon',
|
|
||||||
'coinfloor',
|
|
||||||
'coingi',
|
|
||||||
'coinmate',
|
|
||||||
'coinone',
|
|
||||||
'coinspot',
|
|
||||||
'coolcoin',
|
|
||||||
'crypton',
|
|
||||||
'deribit',
|
|
||||||
'exmo',
|
|
||||||
'exx',
|
|
||||||
'flowbtc',
|
|
||||||
'foxbit',
|
|
||||||
'fybse',
|
|
||||||
# 'hitbtc',
|
|
||||||
'ice3x',
|
|
||||||
'independentreserve',
|
|
||||||
'indodax',
|
|
||||||
'itbit',
|
|
||||||
'lakebtc',
|
|
||||||
'latoken',
|
|
||||||
'liquid',
|
|
||||||
'livecoin',
|
|
||||||
'luno',
|
|
||||||
'mixcoins',
|
|
||||||
'negociecoins',
|
|
||||||
'nova',
|
|
||||||
'paymium',
|
|
||||||
'southxchange',
|
|
||||||
'stronghold',
|
|
||||||
'surbitcoin',
|
|
||||||
'therock',
|
|
||||||
'tidex',
|
|
||||||
'vaultoro',
|
|
||||||
'vbtc',
|
|
||||||
'virwox',
|
|
||||||
'yobit',
|
|
||||||
'zaif',
|
|
||||||
], "Does not provide timeframes. ccxt fetchOHLCV: emulated"),
|
|
||||||
}
|
|
||||||
|
|
||||||
MAP_EXCHANGE_CHILDCLASS = {
|
|
||||||
'binanceus': 'binance',
|
|
||||||
'binanceje': 'binance',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
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 wrapper(*args, **kwargs):
|
|
||||||
count = kwargs.pop('count', API_RETRY_COUNT)
|
|
||||||
try:
|
|
||||||
return 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 wrapper(*args, **kwargs)
|
|
||||||
else:
|
|
||||||
logger.warning('Giving up retrying: %s()', f.__name__)
|
|
||||||
raise ex
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
class Exchange:
|
class Exchange:
|
||||||
|
|
||||||
_config: Dict = {}
|
_config: Dict = {}
|
||||||
|
@ -634,8 +634,8 @@ class FreqtradeBot:
|
|||||||
Force-sells the pair (using EmergencySell reason) in case of Problems creating the order.
|
Force-sells the pair (using EmergencySell reason) in case of Problems creating the order.
|
||||||
:return: True if the order succeeded, and False in case of problems.
|
:return: True if the order succeeded, and False in case of problems.
|
||||||
"""
|
"""
|
||||||
# Limit price threshold: As limit price should always be below price
|
# Limit price threshold: As limit price should always be below stop-price
|
||||||
LIMIT_PRICE_PCT = 0.99
|
LIMIT_PRICE_PCT = self.strategy.order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
stoploss_order = self.exchange.stoploss_limit(pair=trade.pair, amount=trade.amount,
|
stoploss_order = self.exchange.stoploss_limit(pair=trade.pair, amount=trade.amount,
|
||||||
@ -768,7 +768,7 @@ class FreqtradeBot:
|
|||||||
buy_timeout_threshold = arrow.utcnow().shift(minutes=-buy_timeout).datetime
|
buy_timeout_threshold = arrow.utcnow().shift(minutes=-buy_timeout).datetime
|
||||||
sell_timeout_threshold = arrow.utcnow().shift(minutes=-sell_timeout).datetime
|
sell_timeout_threshold = arrow.utcnow().shift(minutes=-sell_timeout).datetime
|
||||||
|
|
||||||
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
|
for trade in Trade.get_open_order_trades():
|
||||||
try:
|
try:
|
||||||
# FIXME: Somehow the query above returns results
|
# FIXME: Somehow the query above returns results
|
||||||
# where the open_order_id is in fact None.
|
# where the open_order_id is in fact None.
|
||||||
|
@ -33,8 +33,8 @@ def setup_logging(config: Dict[str, Any]) -> None:
|
|||||||
# Log level
|
# Log level
|
||||||
verbosity = config['verbosity']
|
verbosity = config['verbosity']
|
||||||
|
|
||||||
# Log to stdout, not stderr
|
# Log to stderr
|
||||||
log_handlers: List[logging.Handler] = [logging.StreamHandler(sys.stdout)]
|
log_handlers: List[logging.Handler] = [logging.StreamHandler(sys.stderr)]
|
||||||
|
|
||||||
if config.get('logfile'):
|
if config.get('logfile'):
|
||||||
log_handlers.append(RotatingFileHandler(config['logfile'],
|
log_handlers.append(RotatingFileHandler(config['logfile'],
|
||||||
|
@ -5,10 +5,9 @@ This module defines the interface to apply for hyperopts
|
|||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
|
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC
|
||||||
from typing import Dict, Any, Callable, List
|
from typing import Dict, Any, Callable, List
|
||||||
|
|
||||||
from pandas import DataFrame
|
|
||||||
from skopt.space import Dimension, Integer, Real
|
from skopt.space import Dimension, Integer, Real
|
||||||
|
|
||||||
from freqtrade import OperationalException
|
from freqtrade import OperationalException
|
||||||
@ -42,15 +41,6 @@ class IHyperOpt(ABC):
|
|||||||
# Assign ticker_interval to be used in hyperopt
|
# Assign ticker_interval to be used in hyperopt
|
||||||
IHyperOpt.ticker_interval = str(config['ticker_interval'])
|
IHyperOpt.ticker_interval = str(config['ticker_interval'])
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
@abstractmethod
|
|
||||||
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Populate indicators that will be used in the Buy and Sell strategy.
|
|
||||||
:param dataframe: Raw data from the exchange and parsed by parse_ticker_dataframe().
|
|
||||||
:return: A Dataframe with all mandatory indicators for the strategies.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||||
"""
|
"""
|
||||||
|
@ -8,17 +8,16 @@ from typing import Any, Dict, List, Optional
|
|||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String,
|
from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String,
|
||||||
create_engine, inspect)
|
create_engine, desc, func, inspect)
|
||||||
from sqlalchemy.exc import NoSuchModuleError
|
from sqlalchemy.exc import NoSuchModuleError
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
|
from sqlalchemy.orm import Query
|
||||||
from sqlalchemy.orm.scoping import scoped_session
|
from sqlalchemy.orm.scoping import scoped_session
|
||||||
from sqlalchemy.orm.session import sessionmaker
|
from sqlalchemy.orm.session import sessionmaker
|
||||||
from sqlalchemy import func
|
|
||||||
from sqlalchemy.pool import StaticPool
|
from sqlalchemy.pool import StaticPool
|
||||||
|
|
||||||
from freqtrade import OperationalException
|
from freqtrade import OperationalException
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -52,9 +51,11 @@ def init(db_url: str, clean_open_orders: bool = False) -> None:
|
|||||||
raise OperationalException(f"Given value for db_url: '{db_url}' "
|
raise OperationalException(f"Given value for db_url: '{db_url}' "
|
||||||
f"is no valid database URL! (See {_SQL_DOCS_URL})")
|
f"is no valid database URL! (See {_SQL_DOCS_URL})")
|
||||||
|
|
||||||
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
|
# https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope
|
||||||
Trade.session = session()
|
# Scoped sessions proxy requests to the appropriate thread-local session.
|
||||||
Trade.query = session.query_property()
|
# We should use the scoped_session object - not a seperately initialized version
|
||||||
|
Trade.session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
|
||||||
|
Trade.query = Trade.session.query_property()
|
||||||
_DECL_BASE.metadata.create_all(engine)
|
_DECL_BASE.metadata.create_all(engine)
|
||||||
check_migrate(engine)
|
check_migrate(engine)
|
||||||
|
|
||||||
@ -393,6 +394,37 @@ class Trade(_DECL_BASE):
|
|||||||
profit_percent = (close_trade_price / open_trade_price) - 1
|
profit_percent = (close_trade_price / open_trade_price) - 1
|
||||||
return float(f"{profit_percent:.8f}")
|
return float(f"{profit_percent:.8f}")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_trades(trade_filter=None) -> Query:
|
||||||
|
"""
|
||||||
|
Helper function to query Trades using filters.
|
||||||
|
:param trade_filter: Optional filter to apply to trades
|
||||||
|
Can be either a Filter object, or a List of filters
|
||||||
|
e.g. `(trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True),])`
|
||||||
|
e.g. `(trade_filter=Trade.id == trade_id)`
|
||||||
|
:return: unsorted query object
|
||||||
|
"""
|
||||||
|
if trade_filter is not None:
|
||||||
|
if not isinstance(trade_filter, list):
|
||||||
|
trade_filter = [trade_filter]
|
||||||
|
return Trade.query.filter(*trade_filter)
|
||||||
|
else:
|
||||||
|
return Trade.query
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_open_trades() -> List[Any]:
|
||||||
|
"""
|
||||||
|
Query trades from persistence layer
|
||||||
|
"""
|
||||||
|
return Trade.get_trades(Trade.is_open.is_(True)).all()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_open_order_trades():
|
||||||
|
"""
|
||||||
|
Returns all open trades
|
||||||
|
"""
|
||||||
|
return Trade.get_trades(Trade.open_order_id.isnot(None)).all()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def total_open_trades_stakes() -> float:
|
def total_open_trades_stakes() -> float:
|
||||||
"""
|
"""
|
||||||
@ -405,11 +437,38 @@ class Trade(_DECL_BASE):
|
|||||||
return total_open_stake_amount or 0
|
return total_open_stake_amount or 0
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_open_trades() -> List[Any]:
|
def get_overall_performance() -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Query trades from persistence layer
|
Returns List of dicts containing all Trades, including profit and trade count
|
||||||
"""
|
"""
|
||||||
return Trade.query.filter(Trade.is_open.is_(True)).all()
|
pair_rates = Trade.session.query(
|
||||||
|
Trade.pair,
|
||||||
|
func.sum(Trade.close_profit).label('profit_sum'),
|
||||||
|
func.count(Trade.pair).label('count')
|
||||||
|
).filter(Trade.is_open.is_(False))\
|
||||||
|
.group_by(Trade.pair) \
|
||||||
|
.order_by(desc('profit_sum')) \
|
||||||
|
.all()
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'pair': pair,
|
||||||
|
'profit': rate,
|
||||||
|
'count': count
|
||||||
|
}
|
||||||
|
for pair, rate, count in pair_rates
|
||||||
|
]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_best_pair():
|
||||||
|
"""
|
||||||
|
Get best pair with closed trade.
|
||||||
|
"""
|
||||||
|
best_pair = Trade.session.query(
|
||||||
|
Trade.pair, func.sum(Trade.close_profit).label('profit_sum')
|
||||||
|
).filter(Trade.is_open.is_(False)) \
|
||||||
|
.group_by(Trade.pair) \
|
||||||
|
.order_by(desc('profit_sum')).first()
|
||||||
|
return best_pair
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def stoploss_reinitialization(desired_stoploss):
|
def stoploss_reinitialization(desired_stoploss):
|
||||||
|
@ -36,6 +36,9 @@ class HyperOptResolver(IResolver):
|
|||||||
self.hyperopt = self._load_hyperopt(hyperopt_name, config,
|
self.hyperopt = self._load_hyperopt(hyperopt_name, config,
|
||||||
extra_dir=config.get('hyperopt_path'))
|
extra_dir=config.get('hyperopt_path'))
|
||||||
|
|
||||||
|
if not hasattr(self.hyperopt, 'populate_indicators'):
|
||||||
|
logger.warning("Hyperopt class does not provide populate_indicators() method. "
|
||||||
|
"Using populate_indicators from the strategy.")
|
||||||
if not hasattr(self.hyperopt, 'populate_buy_trend'):
|
if not hasattr(self.hyperopt, 'populate_buy_trend'):
|
||||||
logger.warning("Hyperopt class does not provide populate_buy_trend() method. "
|
logger.warning("Hyperopt class does not provide populate_buy_trend() method. "
|
||||||
"Using populate_buy_trend from the strategy.")
|
"Using populate_buy_trend from the strategy.")
|
||||||
|
@ -9,7 +9,6 @@ from enum import Enum
|
|||||||
from typing import Dict, Any, List, Optional
|
from typing import Dict, Any, List, Optional
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import sqlalchemy as sql
|
|
||||||
from numpy import mean, NAN
|
from numpy import mean, NAN
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
@ -154,12 +153,11 @@ class RPC:
|
|||||||
|
|
||||||
for day in range(0, timescale):
|
for day in range(0, timescale):
|
||||||
profitday = today - timedelta(days=day)
|
profitday = today - timedelta(days=day)
|
||||||
trades = Trade.query \
|
trades = Trade.get_trades(trade_filter=[
|
||||||
.filter(Trade.is_open.is_(False)) \
|
Trade.is_open.is_(False),
|
||||||
.filter(Trade.close_date >= profitday)\
|
Trade.close_date >= profitday,
|
||||||
.filter(Trade.close_date < (profitday + timedelta(days=1)))\
|
Trade.close_date < (profitday + timedelta(days=1))
|
||||||
.order_by(Trade.close_date)\
|
]).order_by(Trade.close_date).all()
|
||||||
.all()
|
|
||||||
curdayprofit = sum(trade.calc_profit() for trade in trades)
|
curdayprofit = sum(trade.calc_profit() for trade in trades)
|
||||||
profit_days[profitday] = {
|
profit_days[profitday] = {
|
||||||
'amount': f'{curdayprofit:.8f}',
|
'amount': f'{curdayprofit:.8f}',
|
||||||
@ -192,7 +190,7 @@ class RPC:
|
|||||||
def _rpc_trade_statistics(
|
def _rpc_trade_statistics(
|
||||||
self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
|
self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
|
||||||
""" Returns cumulative profit statistics """
|
""" Returns cumulative profit statistics """
|
||||||
trades = Trade.query.order_by(Trade.id).all()
|
trades = Trade.get_trades().order_by(Trade.id).all()
|
||||||
|
|
||||||
profit_all_coin = []
|
profit_all_coin = []
|
||||||
profit_all_perc = []
|
profit_all_perc = []
|
||||||
@ -225,11 +223,7 @@ class RPC:
|
|||||||
)
|
)
|
||||||
profit_all_perc.append(profit_percent)
|
profit_all_perc.append(profit_percent)
|
||||||
|
|
||||||
best_pair = Trade.session.query(
|
best_pair = Trade.get_best_pair()
|
||||||
Trade.pair, sql.func.sum(Trade.close_profit).label('profit_sum')
|
|
||||||
).filter(Trade.is_open.is_(False)) \
|
|
||||||
.group_by(Trade.pair) \
|
|
||||||
.order_by(sql.text('profit_sum DESC')).first()
|
|
||||||
|
|
||||||
if not best_pair:
|
if not best_pair:
|
||||||
raise RPCException('no closed trade')
|
raise RPCException('no closed trade')
|
||||||
@ -389,11 +383,8 @@ class RPC:
|
|||||||
return {'result': 'Created sell orders for all open trades.'}
|
return {'result': 'Created sell orders for all open trades.'}
|
||||||
|
|
||||||
# Query for trade
|
# Query for trade
|
||||||
trade = Trade.query.filter(
|
trade = Trade.get_trades(
|
||||||
sql.and_(
|
trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ]
|
||||||
Trade.id == trade_id,
|
|
||||||
Trade.is_open.is_(True)
|
|
||||||
)
|
|
||||||
).first()
|
).first()
|
||||||
if not trade:
|
if not trade:
|
||||||
logger.warning('forcesell: Invalid argument received')
|
logger.warning('forcesell: Invalid argument received')
|
||||||
@ -423,7 +414,7 @@ class RPC:
|
|||||||
# check if valid pair
|
# check if valid pair
|
||||||
|
|
||||||
# check if pair already has an open pair
|
# check if pair already has an open pair
|
||||||
trade = Trade.query.filter(Trade.is_open.is_(True)).filter(Trade.pair.is_(pair)).first()
|
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair.is_(pair)]).first()
|
||||||
if trade:
|
if trade:
|
||||||
raise RPCException(f'position for {pair} already open - id: {trade.id}')
|
raise RPCException(f'position for {pair} already open - id: {trade.id}')
|
||||||
|
|
||||||
@ -432,28 +423,20 @@ class RPC:
|
|||||||
|
|
||||||
# execute buy
|
# execute buy
|
||||||
if self._freqtrade.execute_buy(pair, stakeamount, price):
|
if self._freqtrade.execute_buy(pair, stakeamount, price):
|
||||||
trade = Trade.query.filter(Trade.is_open.is_(True)).filter(Trade.pair.is_(pair)).first()
|
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair.is_(pair)]).first()
|
||||||
return trade
|
return trade
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _rpc_performance(self) -> List[Dict]:
|
def _rpc_performance(self) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Handler for performance.
|
Handler for performance.
|
||||||
Shows a performance statistic from finished trades
|
Shows a performance statistic from finished trades
|
||||||
"""
|
"""
|
||||||
|
pair_rates = Trade.get_overall_performance()
|
||||||
pair_rates = Trade.session.query(Trade.pair,
|
# Round and convert to %
|
||||||
sql.func.sum(Trade.close_profit).label('profit_sum'),
|
[x.update({'profit': round(x['profit'] * 100, 2)}) for x in pair_rates]
|
||||||
sql.func.count(Trade.pair).label('count')) \
|
return pair_rates
|
||||||
.filter(Trade.is_open.is_(False)) \
|
|
||||||
.group_by(Trade.pair) \
|
|
||||||
.order_by(sql.text('profit_sum DESC')) \
|
|
||||||
.all()
|
|
||||||
return [
|
|
||||||
{'pair': pair, 'profit': round(rate * 100, 2), 'count': count}
|
|
||||||
for pair, rate, count in pair_rates
|
|
||||||
]
|
|
||||||
|
|
||||||
def _rpc_count(self) -> Dict[str, float]:
|
def _rpc_count(self) -> Dict[str, float]:
|
||||||
""" Returns the number of trades running """
|
""" Returns the number of trades running """
|
||||||
|
@ -25,5 +25,12 @@ class RunMode(Enum):
|
|||||||
BACKTEST = "backtest"
|
BACKTEST = "backtest"
|
||||||
EDGE = "edge"
|
EDGE = "edge"
|
||||||
HYPEROPT = "hyperopt"
|
HYPEROPT = "hyperopt"
|
||||||
|
UTIL_EXCHANGE = "util_exchange"
|
||||||
|
UTIL_NO_EXCHANGE = "util_no_exchange"
|
||||||
PLOT = "plot"
|
PLOT = "plot"
|
||||||
OTHER = "other" # Used for plotting scripts and test
|
OTHER = "other"
|
||||||
|
|
||||||
|
|
||||||
|
TRADING_MODES = [RunMode.LIVE, RunMode.DRY_RUN]
|
||||||
|
OPTIMIZE_MODES = [RunMode.BACKTEST, RunMode.EDGE, RunMode.HYPEROPT]
|
||||||
|
NON_UTIL_MODES = TRADING_MODES + OPTIMIZE_MODES
|
||||||
|
@ -85,7 +85,7 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
|||||||
"""
|
"""
|
||||||
Download data (former download_backtest_data.py script)
|
Download data (former download_backtest_data.py script)
|
||||||
"""
|
"""
|
||||||
config = setup_utils_configuration(args, RunMode.OTHER)
|
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||||
|
|
||||||
timerange = TimeRange()
|
timerange = TimeRange()
|
||||||
if 'days' in config:
|
if 'days' in config:
|
||||||
@ -134,7 +134,7 @@ def start_list_timeframes(args: Dict[str, Any]) -> None:
|
|||||||
"""
|
"""
|
||||||
Print ticker intervals (timeframes) available on Exchange
|
Print ticker intervals (timeframes) available on Exchange
|
||||||
"""
|
"""
|
||||||
config = setup_utils_configuration(args, RunMode.OTHER)
|
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||||
# Do not use ticker_interval set in the config
|
# Do not use ticker_interval set in the config
|
||||||
config['ticker_interval'] = None
|
config['ticker_interval'] = None
|
||||||
|
|
||||||
@ -155,7 +155,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
|
|||||||
:param pairs_only: if True print only pairs, otherwise print all instruments (markets)
|
:param pairs_only: if True print only pairs, otherwise print all instruments (markets)
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
config = setup_utils_configuration(args, RunMode.OTHER)
|
config = setup_utils_configuration(args, RunMode.UTIL_EXCHANGE)
|
||||||
|
|
||||||
# Init exchange
|
# Init exchange
|
||||||
exchange = ExchangeResolver(config['exchange']['name'], config, validate=False).exchange
|
exchange = ExchangeResolver(config['exchange']['name'], config, validate=False).exchange
|
||||||
|
@ -10,7 +10,7 @@ from freqtrade.data.btanalysis import (BT_DATA_COLUMNS,
|
|||||||
create_cum_profit,
|
create_cum_profit,
|
||||||
extract_trades_of_period,
|
extract_trades_of_period,
|
||||||
load_backtest_data, load_trades,
|
load_backtest_data, load_trades,
|
||||||
load_trades_from_db)
|
load_trades_from_db, analyze_trade_parallelism)
|
||||||
from freqtrade.data.history import load_data, load_pair_history
|
from freqtrade.data.history import load_data, load_pair_history
|
||||||
from tests.test_persistence import create_mock_trades
|
from tests.test_persistence import create_mock_trades
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ def test_load_backtest_data(testdatadir):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_load_trades_db(default_conf, fee, mocker):
|
def test_load_trades_from_db(default_conf, fee, mocker):
|
||||||
|
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee)
|
||||||
# remove init so it does not init again
|
# remove init so it does not init again
|
||||||
@ -84,6 +84,17 @@ def test_extract_trades_of_period(testdatadir):
|
|||||||
assert trades1.iloc[-1].close_time == Arrow(2017, 11, 14, 15, 25, 0).datetime
|
assert trades1.iloc[-1].close_time == Arrow(2017, 11, 14, 15, 25, 0).datetime
|
||||||
|
|
||||||
|
|
||||||
|
def test_analyze_trade_parallelism(default_conf, mocker, testdatadir):
|
||||||
|
filename = testdatadir / "backtest-result_test.json"
|
||||||
|
bt_data = load_backtest_data(filename)
|
||||||
|
|
||||||
|
res = analyze_trade_parallelism(bt_data, "5m")
|
||||||
|
assert isinstance(res, DataFrame)
|
||||||
|
assert 'open_trades' in res.columns
|
||||||
|
assert res['open_trades'].max() == 3
|
||||||
|
assert res['open_trades'].min() == 0
|
||||||
|
|
||||||
|
|
||||||
def test_load_trades(default_conf, mocker):
|
def test_load_trades(default_conf, mocker):
|
||||||
db_mock = mocker.patch("freqtrade.data.btanalysis.load_trades_from_db", MagicMock())
|
db_mock = mocker.patch("freqtrade.data.btanalysis.load_trades_from_db", MagicMock())
|
||||||
bt_mock = mocker.patch("freqtrade.data.btanalysis.load_backtest_data", MagicMock())
|
bt_mock = mocker.patch("freqtrade.data.btanalysis.load_backtest_data", MagicMock())
|
||||||
|
@ -103,9 +103,7 @@ def test_load_data_startup_candles(mocker, caplog, default_conf, testdatadir) ->
|
|||||||
datadir=testdatadir, timerange=timerange,
|
datadir=testdatadir, timerange=timerange,
|
||||||
startup_candles=20,
|
startup_candles=20,
|
||||||
)
|
)
|
||||||
assert log_has(
|
|
||||||
'Using indicator startup period: 20 ...', caplog
|
|
||||||
)
|
|
||||||
assert ltfmock.call_count == 1
|
assert ltfmock.call_count == 1
|
||||||
assert ltfmock.call_args_list[0][1]['timerange'] != timerange
|
assert ltfmock.call_args_list[0][1]['timerange'] != timerange
|
||||||
# startts is 20 minutes earlier
|
# startts is 20 minutes earlier
|
||||||
@ -354,8 +352,12 @@ def test_load_partial_missing(testdatadir, caplog) -> None:
|
|||||||
start = arrow.get('2018-01-01T00:00:00')
|
start = arrow.get('2018-01-01T00:00:00')
|
||||||
end = arrow.get('2018-01-11T00:00:00')
|
end = arrow.get('2018-01-11T00:00:00')
|
||||||
tickerdata = history.load_data(testdatadir, '5m', ['UNITTEST/BTC'],
|
tickerdata = history.load_data(testdatadir, '5m', ['UNITTEST/BTC'],
|
||||||
|
startup_candles=20,
|
||||||
timerange=TimeRange('date', 'date',
|
timerange=TimeRange('date', 'date',
|
||||||
start.timestamp, end.timestamp))
|
start.timestamp, end.timestamp))
|
||||||
|
assert log_has(
|
||||||
|
'Using indicator startup period: 20 ...', caplog
|
||||||
|
)
|
||||||
# timedifference in 5 minutes
|
# timedifference in 5 minutes
|
||||||
td = ((end - start).total_seconds() // 60 // 5) + 1
|
td = ((end - start).total_seconds() // 60 // 5) + 1
|
||||||
assert td != len(tickerdata['UNITTEST/BTC'])
|
assert td != len(tickerdata['UNITTEST/BTC'])
|
||||||
|
@ -14,13 +14,13 @@ from pandas import DataFrame
|
|||||||
from freqtrade import (DependencyException, InvalidOrderException,
|
from freqtrade import (DependencyException, InvalidOrderException,
|
||||||
OperationalException, TemporaryError)
|
OperationalException, TemporaryError)
|
||||||
from freqtrade.exchange import Binance, Exchange, Kraken
|
from freqtrade.exchange import Binance, Exchange, Kraken
|
||||||
from freqtrade.exchange.exchange import (API_RETRY_COUNT, timeframe_to_minutes,
|
from freqtrade.exchange.common import API_RETRY_COUNT
|
||||||
|
from freqtrade.exchange.exchange import (market_is_active, symbol_is_pair,
|
||||||
|
timeframe_to_minutes,
|
||||||
timeframe_to_msecs,
|
timeframe_to_msecs,
|
||||||
timeframe_to_next_date,
|
timeframe_to_next_date,
|
||||||
timeframe_to_prev_date,
|
timeframe_to_prev_date,
|
||||||
timeframe_to_seconds,
|
timeframe_to_seconds)
|
||||||
symbol_is_pair,
|
|
||||||
market_is_active)
|
|
||||||
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||||
from tests.conftest import get_patched_exchange, log_has, log_has_re
|
from tests.conftest import get_patched_exchange, log_has, log_has_re
|
||||||
|
|
||||||
|
@ -714,9 +714,9 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
|
|||||||
results = backtesting.backtest(backtest_conf)
|
results = backtesting.backtest(backtest_conf)
|
||||||
|
|
||||||
# Make sure we have parallel trades
|
# Make sure we have parallel trades
|
||||||
assert len(evaluate_result_multi(results, '5min', 2)) > 0
|
assert len(evaluate_result_multi(results, '5m', 2)) > 0
|
||||||
# make sure we don't have trades with more than configured max_open_trades
|
# make sure we don't have trades with more than configured max_open_trades
|
||||||
assert len(evaluate_result_multi(results, '5min', 3)) == 0
|
assert len(evaluate_result_multi(results, '5m', 3)) == 0
|
||||||
|
|
||||||
backtest_conf = {
|
backtest_conf = {
|
||||||
'stake_amount': default_conf['stake_amount'],
|
'stake_amount': default_conf['stake_amount'],
|
||||||
@ -727,7 +727,7 @@ def test_backtest_multi_pair(default_conf, fee, mocker, tres, pair, testdatadir)
|
|||||||
'end_date': max_date,
|
'end_date': max_date,
|
||||||
}
|
}
|
||||||
results = backtesting.backtest(backtest_conf)
|
results = backtesting.backtest(backtest_conf)
|
||||||
assert len(evaluate_result_multi(results, '5min', 1)) == 0
|
assert len(evaluate_result_multi(results, '5m', 1)) == 0
|
||||||
|
|
||||||
|
|
||||||
def test_backtest_record(default_conf, fee, mocker):
|
def test_backtest_record(default_conf, fee, mocker):
|
||||||
|
@ -154,6 +154,7 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
|
|||||||
patched_configuration_load_config_file(mocker, default_conf)
|
patched_configuration_load_config_file(mocker, default_conf)
|
||||||
|
|
||||||
hyperopt = DefaultHyperOpt
|
hyperopt = DefaultHyperOpt
|
||||||
|
delattr(hyperopt, 'populate_indicators')
|
||||||
delattr(hyperopt, 'populate_buy_trend')
|
delattr(hyperopt, 'populate_buy_trend')
|
||||||
delattr(hyperopt, 'populate_sell_trend')
|
delattr(hyperopt, 'populate_sell_trend')
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
@ -162,8 +163,11 @@ def test_hyperoptresolver(mocker, default_conf, caplog) -> None:
|
|||||||
)
|
)
|
||||||
default_conf.update({'hyperopt': 'DefaultHyperOpt'})
|
default_conf.update({'hyperopt': 'DefaultHyperOpt'})
|
||||||
x = HyperOptResolver(default_conf).hyperopt
|
x = HyperOptResolver(default_conf).hyperopt
|
||||||
|
assert not hasattr(x, 'populate_indicators')
|
||||||
assert not hasattr(x, 'populate_buy_trend')
|
assert not hasattr(x, 'populate_buy_trend')
|
||||||
assert not hasattr(x, 'populate_sell_trend')
|
assert not hasattr(x, 'populate_sell_trend')
|
||||||
|
assert log_has("Hyperopt class does not provide populate_indicators() method. "
|
||||||
|
"Using populate_indicators from the strategy.", caplog)
|
||||||
assert log_has("Hyperopt class does not provide populate_sell_trend() method. "
|
assert log_has("Hyperopt class does not provide populate_sell_trend() method. "
|
||||||
"Using populate_sell_trend from the strategy.", caplog)
|
"Using populate_sell_trend from the strategy.", caplog)
|
||||||
assert log_has("Hyperopt class does not provide populate_buy_trend() method. "
|
assert log_has("Hyperopt class does not provide populate_buy_trend() method. "
|
||||||
|
@ -14,7 +14,6 @@ import requests
|
|||||||
from freqtrade import (DependencyException, InvalidOrderException,
|
from freqtrade import (DependencyException, InvalidOrderException,
|
||||||
OperationalException, TemporaryError, constants)
|
OperationalException, TemporaryError, constants)
|
||||||
from freqtrade.constants import MATH_CLOSE_PREC
|
from freqtrade.constants import MATH_CLOSE_PREC
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
|
||||||
from freqtrade.freqtradebot import FreqtradeBot
|
from freqtrade.freqtradebot import FreqtradeBot
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc import RPCMessageType
|
from freqtrade.rpc import RPCMessageType
|
||||||
@ -49,16 +48,6 @@ def test_freqtradebot_state(mocker, default_conf, markets) -> None:
|
|||||||
assert freqtrade.state is State.STOPPED
|
assert freqtrade.state is State.STOPPED
|
||||||
|
|
||||||
|
|
||||||
def test_worker_state(mocker, default_conf, markets) -> None:
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
|
|
||||||
worker = get_patched_worker(mocker, default_conf)
|
|
||||||
assert worker.state is State.RUNNING
|
|
||||||
|
|
||||||
default_conf.pop('initial_state')
|
|
||||||
worker = Worker(args=None, config=default_conf)
|
|
||||||
assert worker.state is State.STOPPED
|
|
||||||
|
|
||||||
|
|
||||||
def test_cleanup(mocker, default_conf, caplog) -> None:
|
def test_cleanup(mocker, default_conf, caplog) -> None:
|
||||||
mock_cleanup = MagicMock()
|
mock_cleanup = MagicMock()
|
||||||
mocker.patch('freqtrade.persistence.cleanup', mock_cleanup)
|
mocker.patch('freqtrade.persistence.cleanup', mock_cleanup)
|
||||||
@ -68,69 +57,6 @@ def test_cleanup(mocker, default_conf, caplog) -> None:
|
|||||||
assert mock_cleanup.call_count == 1
|
assert mock_cleanup.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_worker_running(mocker, default_conf, caplog) -> None:
|
|
||||||
mock_throttle = MagicMock()
|
|
||||||
mocker.patch('freqtrade.worker.Worker._throttle', mock_throttle)
|
|
||||||
mocker.patch('freqtrade.persistence.Trade.stoploss_reinitialization', MagicMock())
|
|
||||||
|
|
||||||
worker = get_patched_worker(mocker, default_conf)
|
|
||||||
|
|
||||||
state = worker._worker(old_state=None)
|
|
||||||
assert state is State.RUNNING
|
|
||||||
assert log_has('Changing state to: RUNNING', caplog)
|
|
||||||
assert mock_throttle.call_count == 1
|
|
||||||
# Check strategy is loaded, and received a dataprovider object
|
|
||||||
assert worker.freqtrade.strategy
|
|
||||||
assert worker.freqtrade.strategy.dp
|
|
||||||
assert isinstance(worker.freqtrade.strategy.dp, DataProvider)
|
|
||||||
|
|
||||||
|
|
||||||
def test_worker_stopped(mocker, default_conf, caplog) -> None:
|
|
||||||
mock_throttle = MagicMock()
|
|
||||||
mocker.patch('freqtrade.worker.Worker._throttle', mock_throttle)
|
|
||||||
mock_sleep = mocker.patch('time.sleep', return_value=None)
|
|
||||||
|
|
||||||
worker = get_patched_worker(mocker, default_conf)
|
|
||||||
worker.state = State.STOPPED
|
|
||||||
state = worker._worker(old_state=State.RUNNING)
|
|
||||||
assert state is State.STOPPED
|
|
||||||
assert log_has('Changing state to: STOPPED', caplog)
|
|
||||||
assert mock_throttle.call_count == 0
|
|
||||||
assert mock_sleep.call_count == 1
|
|
||||||
|
|
||||||
|
|
||||||
def test_throttle(mocker, default_conf, caplog) -> None:
|
|
||||||
def throttled_func():
|
|
||||||
return 42
|
|
||||||
|
|
||||||
caplog.set_level(logging.DEBUG)
|
|
||||||
worker = get_patched_worker(mocker, default_conf)
|
|
||||||
|
|
||||||
start = time.time()
|
|
||||||
result = worker._throttle(throttled_func, min_secs=0.1)
|
|
||||||
end = time.time()
|
|
||||||
|
|
||||||
assert result == 42
|
|
||||||
assert end - start > 0.1
|
|
||||||
assert log_has('Throttling throttled_func for 0.10 seconds', caplog)
|
|
||||||
|
|
||||||
result = worker._throttle(throttled_func, min_secs=-1)
|
|
||||||
assert result == 42
|
|
||||||
|
|
||||||
|
|
||||||
def test_throttle_with_assets(mocker, default_conf) -> None:
|
|
||||||
def throttled_func(nb_assets=-1):
|
|
||||||
return nb_assets
|
|
||||||
|
|
||||||
worker = get_patched_worker(mocker, default_conf)
|
|
||||||
|
|
||||||
result = worker._throttle(throttled_func, min_secs=0.1, nb_assets=666)
|
|
||||||
assert result == 666
|
|
||||||
|
|
||||||
result = worker._throttle(throttled_func, min_secs=0.1)
|
|
||||||
assert result == -1
|
|
||||||
|
|
||||||
|
|
||||||
def test_order_dict_dry_run(default_conf, mocker, caplog) -> None:
|
def test_order_dict_dry_run(default_conf, mocker, caplog) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
@ -224,18 +150,13 @@ def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None:
|
|||||||
freqtrade._get_trade_stake_amount('ETH/BTC')
|
freqtrade._get_trade_stake_amount('ETH/BTC')
|
||||||
|
|
||||||
|
|
||||||
def test_get_trade_stake_amount_unlimited_amount(default_conf,
|
def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker,
|
||||||
ticker,
|
limit_buy_order, fee, mocker) -> None:
|
||||||
limit_buy_order,
|
|
||||||
fee,
|
|
||||||
markets,
|
|
||||||
mocker) -> None:
|
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
patch_wallet(mocker, free=default_conf['stake_amount'])
|
patch_wallet(mocker, free=default_conf['stake_amount'])
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
markets=PropertyMock(return_value=markets),
|
|
||||||
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
|
||||||
@ -296,7 +217,7 @@ def test_edge_overrides_stake_amount(mocker, edge_conf) -> None:
|
|||||||
assert freqtrade._get_trade_stake_amount('LTC/BTC') == (999.9 * 0.5 * 0.01) / 0.21
|
assert freqtrade._get_trade_stake_amount('LTC/BTC') == (999.9 * 0.5 * 0.01) / 0.21
|
||||||
|
|
||||||
|
|
||||||
def test_edge_overrides_stoploss(limit_buy_order, fee, markets, caplog, mocker, edge_conf) -> None:
|
def test_edge_overrides_stoploss(limit_buy_order, fee, caplog, mocker, edge_conf) -> None:
|
||||||
|
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
@ -317,7 +238,6 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, markets, caplog, mocker,
|
|||||||
}),
|
}),
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
#############################################
|
#############################################
|
||||||
|
|
||||||
@ -337,7 +257,7 @@ def test_edge_overrides_stoploss(limit_buy_order, fee, markets, caplog, mocker,
|
|||||||
assert trade.sell_reason == SellType.STOP_LOSS.value
|
assert trade.sell_reason == SellType.STOP_LOSS.value
|
||||||
|
|
||||||
|
|
||||||
def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, markets,
|
def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee,
|
||||||
mocker, edge_conf) -> None:
|
mocker, edge_conf) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
@ -358,7 +278,6 @@ def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, markets,
|
|||||||
}),
|
}),
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets),
|
|
||||||
)
|
)
|
||||||
#############################################
|
#############################################
|
||||||
|
|
||||||
@ -377,7 +296,7 @@ def test_edge_should_ignore_strategy_stoploss(limit_buy_order, fee, markets,
|
|||||||
|
|
||||||
|
|
||||||
def test_total_open_trades_stakes(mocker, default_conf, ticker,
|
def test_total_open_trades_stakes(mocker, default_conf, ticker,
|
||||||
limit_buy_order, fee, markets) -> None:
|
limit_buy_order, fee) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
default_conf['stake_amount'] = 0.0000098751
|
default_conf['stake_amount'] = 0.0000098751
|
||||||
@ -387,7 +306,6 @@ def test_total_open_trades_stakes(mocker, default_conf, ticker,
|
|||||||
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,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
@ -522,7 +440,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
|
|||||||
assert result == min(8, 2 * 2) / 0.9
|
assert result == min(8, 2 * 2) / 0.9
|
||||||
|
|
||||||
|
|
||||||
def test_create_trades(default_conf, ticker, limit_buy_order, fee, markets, mocker) -> None:
|
def test_create_trades(default_conf, ticker, limit_buy_order, fee, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -530,7 +448,6 @@ def test_create_trades(default_conf, ticker, limit_buy_order, fee, markets, mock
|
|||||||
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,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Save state of current whitelist
|
# Save state of current whitelist
|
||||||
@ -556,7 +473,7 @@ def test_create_trades(default_conf, ticker, limit_buy_order, fee, markets, mock
|
|||||||
|
|
||||||
|
|
||||||
def test_create_trades_no_stake_amount(default_conf, ticker, limit_buy_order,
|
def test_create_trades_no_stake_amount(default_conf, ticker, limit_buy_order,
|
||||||
fee, markets, mocker) -> None:
|
fee, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5)
|
patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5)
|
||||||
@ -565,7 +482,6 @@ def test_create_trades_no_stake_amount(default_conf, ticker, limit_buy_order,
|
|||||||
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,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
@ -575,7 +491,7 @@ def test_create_trades_no_stake_amount(default_conf, ticker, limit_buy_order,
|
|||||||
|
|
||||||
|
|
||||||
def test_create_trades_minimal_amount(default_conf, ticker, limit_buy_order,
|
def test_create_trades_minimal_amount(default_conf, ticker, limit_buy_order,
|
||||||
fee, markets, mocker) -> None:
|
fee, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
buy_mock = MagicMock(return_value={'id': limit_buy_order['id']})
|
buy_mock = MagicMock(return_value={'id': limit_buy_order['id']})
|
||||||
@ -584,7 +500,6 @@ def test_create_trades_minimal_amount(default_conf, ticker, limit_buy_order,
|
|||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=buy_mock,
|
buy=buy_mock,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
default_conf['stake_amount'] = 0.0005
|
default_conf['stake_amount'] = 0.0005
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
@ -596,7 +511,7 @@ def test_create_trades_minimal_amount(default_conf, ticker, limit_buy_order,
|
|||||||
|
|
||||||
|
|
||||||
def test_create_trades_too_small_stake_amount(default_conf, ticker, limit_buy_order,
|
def test_create_trades_too_small_stake_amount(default_conf, ticker, limit_buy_order,
|
||||||
fee, markets, mocker) -> None:
|
fee, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
buy_mock = MagicMock(return_value={'id': limit_buy_order['id']})
|
buy_mock = MagicMock(return_value={'id': limit_buy_order['id']})
|
||||||
@ -605,7 +520,6 @@ def test_create_trades_too_small_stake_amount(default_conf, ticker, limit_buy_or
|
|||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=buy_mock,
|
buy=buy_mock,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
default_conf['stake_amount'] = 0.000000005
|
default_conf['stake_amount'] = 0.000000005
|
||||||
@ -625,7 +539,6 @@ def test_create_trades_limit_reached(default_conf, ticker, limit_buy_order,
|
|||||||
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']),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
default_conf['max_open_trades'] = 0
|
default_conf['max_open_trades'] = 0
|
||||||
default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
default_conf['stake_amount'] = constants.UNLIMITED_STAKE_AMOUNT
|
||||||
@ -638,7 +551,7 @@ def test_create_trades_limit_reached(default_conf, ticker, limit_buy_order,
|
|||||||
|
|
||||||
|
|
||||||
def test_create_trades_no_pairs_let(default_conf, ticker, limit_buy_order, fee,
|
def test_create_trades_no_pairs_let(default_conf, ticker, limit_buy_order, fee,
|
||||||
markets, mocker, caplog) -> None:
|
mocker, caplog) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -646,7 +559,6 @@ def test_create_trades_no_pairs_let(default_conf, ticker, limit_buy_order, fee,
|
|||||||
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,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
default_conf['exchange']['pair_whitelist'] = ["ETH/BTC"]
|
default_conf['exchange']['pair_whitelist'] = ["ETH/BTC"]
|
||||||
@ -660,7 +572,7 @@ def test_create_trades_no_pairs_let(default_conf, ticker, limit_buy_order, fee,
|
|||||||
|
|
||||||
|
|
||||||
def test_create_trades_no_pairs_in_whitelist(default_conf, ticker, limit_buy_order, fee,
|
def test_create_trades_no_pairs_in_whitelist(default_conf, ticker, limit_buy_order, fee,
|
||||||
markets, mocker, caplog) -> None:
|
mocker, caplog) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -668,7 +580,6 @@ def test_create_trades_no_pairs_in_whitelist(default_conf, ticker, limit_buy_ord
|
|||||||
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,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
default_conf['exchange']['pair_whitelist'] = []
|
default_conf['exchange']['pair_whitelist'] = []
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
@ -699,7 +610,7 @@ def test_create_trades_no_signal(default_conf, fee, mocker) -> None:
|
|||||||
|
|
||||||
@pytest.mark.parametrize("max_open", range(0, 5))
|
@pytest.mark.parametrize("max_open", range(0, 5))
|
||||||
def test_create_trades_multiple_trades(default_conf, ticker,
|
def test_create_trades_multiple_trades(default_conf, ticker,
|
||||||
fee, markets, mocker, max_open) -> None:
|
fee, mocker, max_open) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
default_conf['max_open_trades'] = max_open
|
default_conf['max_open_trades'] = max_open
|
||||||
@ -708,7 +619,6 @@ def test_create_trades_multiple_trades(default_conf, ticker,
|
|||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value={'id': "12355555"}),
|
buy=MagicMock(return_value={'id': "12355555"}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
@ -719,7 +629,7 @@ def test_create_trades_multiple_trades(default_conf, ticker,
|
|||||||
assert len(trades) == max_open
|
assert len(trades) == max_open
|
||||||
|
|
||||||
|
|
||||||
def test_create_trades_preopen(default_conf, ticker, fee, markets, mocker) -> None:
|
def test_create_trades_preopen(default_conf, ticker, fee, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
default_conf['max_open_trades'] = 4
|
default_conf['max_open_trades'] = 4
|
||||||
@ -728,7 +638,6 @@ def test_create_trades_preopen(default_conf, ticker, fee, markets, mocker) -> No
|
|||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
buy=MagicMock(return_value={'id': "12355555"}),
|
buy=MagicMock(return_value={'id': "12355555"}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
@ -747,13 +656,12 @@ def test_create_trades_preopen(default_conf, ticker, fee, markets, mocker) -> No
|
|||||||
|
|
||||||
|
|
||||||
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:
|
fee, mocker, caplog) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
markets=PropertyMock(return_value=markets),
|
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
get_order=MagicMock(return_value=limit_buy_order),
|
get_order=MagicMock(return_value=limit_buy_order),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
@ -782,13 +690,12 @@ 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, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
markets=PropertyMock(return_value=markets),
|
|
||||||
buy=MagicMock(side_effect=TemporaryError)
|
buy=MagicMock(side_effect=TemporaryError)
|
||||||
)
|
)
|
||||||
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
|
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
|
||||||
@ -800,13 +707,12 @@ def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> Non
|
|||||||
assert sleep_mock.has_calls()
|
assert sleep_mock.has_calls()
|
||||||
|
|
||||||
|
|
||||||
def test_process_operational_exception(default_conf, ticker, markets, mocker) -> None:
|
def test_process_operational_exception(default_conf, ticker, mocker) -> None:
|
||||||
msg_mock = patch_RPCManager(mocker)
|
msg_mock = patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
markets=PropertyMock(return_value=markets),
|
|
||||||
buy=MagicMock(side_effect=OperationalException)
|
buy=MagicMock(side_effect=OperationalException)
|
||||||
)
|
)
|
||||||
worker = Worker(args=None, config=default_conf)
|
worker = Worker(args=None, config=default_conf)
|
||||||
@ -819,14 +725,12 @@ def test_process_operational_exception(default_conf, ticker, markets, mocker) ->
|
|||||||
assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]['status']
|
assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]['status']
|
||||||
|
|
||||||
|
|
||||||
def test_process_trade_handling(
|
def test_process_trade_handling(default_conf, ticker, limit_buy_order, fee, mocker) -> None:
|
||||||
default_conf, ticker, limit_buy_order, markets, fee, mocker) -> None:
|
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
markets=PropertyMock(return_value=markets),
|
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
get_order=MagicMock(return_value=limit_buy_order),
|
get_order=MagicMock(return_value=limit_buy_order),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
@ -846,15 +750,14 @@ def test_process_trade_handling(
|
|||||||
assert len(trades) == 1
|
assert len(trades) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_process_trade_no_whitelist_pair(
|
def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order,
|
||||||
default_conf, ticker, limit_buy_order, markets, fee, mocker) -> None:
|
fee, mocker) -> None:
|
||||||
""" Test process with trade not in pair list """
|
""" Test process with trade not in pair list """
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
markets=PropertyMock(return_value=markets),
|
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
get_order=MagicMock(return_value=limit_buy_order),
|
get_order=MagicMock(return_value=limit_buy_order),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
@ -891,7 +794,7 @@ def test_process_trade_no_whitelist_pair(
|
|||||||
assert len(freqtrade.active_pair_whitelist) == len(set(freqtrade.active_pair_whitelist))
|
assert len(freqtrade.active_pair_whitelist) == len(set(freqtrade.active_pair_whitelist))
|
||||||
|
|
||||||
|
|
||||||
def test_process_informative_pairs_added(default_conf, ticker, markets, mocker) -> None:
|
def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
|
||||||
@ -902,7 +805,6 @@ def test_process_informative_pairs_added(default_conf, ticker, markets, mocker)
|
|||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
markets=PropertyMock(return_value=markets),
|
|
||||||
buy=MagicMock(side_effect=TemporaryError),
|
buy=MagicMock(side_effect=TemporaryError),
|
||||||
refresh_latest_ohlcv=refresh_mock,
|
refresh_latest_ohlcv=refresh_mock,
|
||||||
)
|
)
|
||||||
@ -948,7 +850,7 @@ def test_balance_bigger_last_ask(mocker, default_conf) -> None:
|
|||||||
assert freqtrade.get_target_bid('ETH/BTC') == 5
|
assert freqtrade.get_target_bid('ETH/BTC') == 5
|
||||||
|
|
||||||
|
|
||||||
def test_execute_buy(mocker, default_conf, fee, markets, limit_buy_order) -> None:
|
def test_execute_buy(mocker, default_conf, fee, limit_buy_order) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
@ -970,7 +872,6 @@ def test_execute_buy(mocker, default_conf, fee, markets, limit_buy_order) -> Non
|
|||||||
}),
|
}),
|
||||||
buy=buy_mm,
|
buy=buy_mm,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
pair = 'ETH/BTC'
|
pair = 'ETH/BTC'
|
||||||
|
|
||||||
@ -1067,7 +968,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None
|
|||||||
|
|
||||||
|
|
||||||
def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
||||||
markets, limit_buy_order, limit_sell_order) -> None:
|
limit_buy_order, limit_sell_order) -> None:
|
||||||
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
@ -1081,7 +982,6 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
|||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets),
|
|
||||||
stoploss_limit=stoploss_limit
|
stoploss_limit=stoploss_limit
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
@ -1168,7 +1068,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
|||||||
|
|
||||||
|
|
||||||
def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
|
def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
|
||||||
markets, limit_buy_order, limit_sell_order) -> None:
|
limit_buy_order, limit_sell_order) -> None:
|
||||||
# Sixth case: stoploss order was cancelled but couldn't create new one
|
# Sixth case: stoploss order was cancelled but couldn't create new one
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
@ -1182,7 +1082,6 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
|
|||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets),
|
|
||||||
get_order=MagicMock(return_value={'status': 'canceled'}),
|
get_order=MagicMock(return_value={'status': 'canceled'}),
|
||||||
stoploss_limit=MagicMock(side_effect=DependencyException()),
|
stoploss_limit=MagicMock(side_effect=DependencyException()),
|
||||||
)
|
)
|
||||||
@ -1203,7 +1102,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
|
|||||||
|
|
||||||
|
|
||||||
def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee,
|
def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee,
|
||||||
markets, limit_buy_order, limit_sell_order):
|
limit_buy_order, limit_sell_order):
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
sell_mock = MagicMock(return_value={'id': limit_sell_order['id']})
|
sell_mock = MagicMock(return_value={'id': limit_sell_order['id']})
|
||||||
@ -1217,7 +1116,6 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee,
|
|||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
sell=sell_mock,
|
sell=sell_mock,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets),
|
|
||||||
get_order=MagicMock(return_value={'status': 'canceled'}),
|
get_order=MagicMock(return_value={'status': 'canceled'}),
|
||||||
stoploss_limit=MagicMock(side_effect=InvalidOrderException()),
|
stoploss_limit=MagicMock(side_effect=InvalidOrderException()),
|
||||||
)
|
)
|
||||||
@ -1340,8 +1238,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
|||||||
|
|
||||||
|
|
||||||
def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog,
|
def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog,
|
||||||
markets, limit_buy_order,
|
limit_buy_order, limit_sell_order) -> None:
|
||||||
limit_sell_order) -> None:
|
|
||||||
# When trailing stoploss is set
|
# When trailing stoploss is set
|
||||||
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
@ -1356,7 +1253,6 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
|
|||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets),
|
|
||||||
stoploss_limit=stoploss_limit
|
stoploss_limit=stoploss_limit
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1409,7 +1305,7 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
|
|||||||
|
|
||||||
|
|
||||||
def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
||||||
markets, limit_buy_order, limit_sell_order) -> None:
|
limit_buy_order, limit_sell_order) -> None:
|
||||||
|
|
||||||
# When trailing stoploss is set
|
# When trailing stoploss is set
|
||||||
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
||||||
@ -1427,7 +1323,6 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
|||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets),
|
|
||||||
stoploss_limit=stoploss_limit
|
stoploss_limit=stoploss_limit
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1728,8 +1623,7 @@ def test_update_trade_state_sell(default_conf, trades_for_order, limit_sell_orde
|
|||||||
assert not trade.is_open
|
assert not trade.is_open
|
||||||
|
|
||||||
|
|
||||||
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, mocker) -> None:
|
||||||
fee, markets, mocker) -> None:
|
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -1742,7 +1636,6 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order,
|
|||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
@ -1769,8 +1662,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order,
|
|||||||
assert trade.close_date is not None
|
assert trade.close_date is not None
|
||||||
|
|
||||||
|
|
||||||
def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order,
|
def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, fee, mocker) -> None:
|
||||||
fee, markets, mocker) -> None:
|
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -1778,7 +1670,6 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order,
|
|||||||
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,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
@ -1884,7 +1775,7 @@ def test_handle_trade_use_sell_signal(
|
|||||||
|
|
||||||
|
|
||||||
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, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -1892,7 +1783,6 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order,
|
|||||||
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,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
@ -2222,14 +2112,13 @@ def test_handle_timedout_limit_sell(mocker, default_conf) -> None:
|
|||||||
assert cancel_order_mock.call_count == 1
|
assert cancel_order_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, mocker) -> None:
|
def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> None:
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
patch_whitelist(mocker, default_conf)
|
patch_whitelist(mocker, default_conf)
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
@ -2269,14 +2158,13 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc
|
|||||||
} == last_msg
|
} == last_msg
|
||||||
|
|
||||||
|
|
||||||
def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets, mocker) -> None:
|
def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker) -> None:
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
patch_whitelist(mocker, default_conf)
|
patch_whitelist(mocker, default_conf)
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
@ -2318,15 +2206,13 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets,
|
|||||||
|
|
||||||
|
|
||||||
def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fee,
|
def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fee,
|
||||||
ticker_sell_down,
|
ticker_sell_down, mocker) -> None:
|
||||||
markets, mocker) -> None:
|
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
patch_whitelist(mocker, default_conf)
|
patch_whitelist(mocker, default_conf)
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
@ -2374,8 +2260,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
|
|||||||
} == last_msg
|
} == last_msg
|
||||||
|
|
||||||
|
|
||||||
def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee,
|
def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee, caplog) -> None:
|
||||||
markets, caplog) -> None:
|
|
||||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException())
|
mocker.patch('freqtrade.exchange.Exchange.cancel_order', side_effect=InvalidOrderException())
|
||||||
sellmock = MagicMock()
|
sellmock = MagicMock()
|
||||||
@ -2384,7 +2269,6 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee,
|
|||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets),
|
|
||||||
sell=sellmock
|
sell=sellmock
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -2404,9 +2288,8 @@ def test_execute_sell_sloe_cancel_exception(mocker, default_conf, ticker, fee,
|
|||||||
assert log_has('Could not cancel stoploss order abcd', caplog)
|
assert log_has('Could not cancel stoploss order abcd', caplog)
|
||||||
|
|
||||||
|
|
||||||
def test_execute_sell_with_stoploss_on_exchange(default_conf,
|
def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticker_sell_up,
|
||||||
ticker, fee, ticker_sell_up,
|
mocker) -> None:
|
||||||
markets, mocker) -> None:
|
|
||||||
|
|
||||||
default_conf['exchange']['name'] = 'binance'
|
default_conf['exchange']['name'] = 'binance'
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
@ -2423,7 +2306,6 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf,
|
|||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets),
|
|
||||||
symbol_amount_prec=lambda s, x, y: y,
|
symbol_amount_prec=lambda s, x, y: y,
|
||||||
symbol_price_prec=lambda s, x, y: y,
|
symbol_price_prec=lambda s, x, y: y,
|
||||||
stoploss_limit=stoploss_limit,
|
stoploss_limit=stoploss_limit,
|
||||||
@ -2458,10 +2340,8 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf,
|
|||||||
assert rpc_mock.call_count == 2
|
assert rpc_mock.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf,
|
def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, fee,
|
||||||
ticker, fee,
|
limit_buy_order, mocker) -> None:
|
||||||
limit_buy_order,
|
|
||||||
markets, mocker) -> None:
|
|
||||||
default_conf['exchange']['name'] = 'binance'
|
default_conf['exchange']['name'] = 'binance'
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
@ -2469,7 +2349,6 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf,
|
|||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets),
|
|
||||||
symbol_amount_prec=lambda s, x, y: y,
|
symbol_amount_prec=lambda s, x, y: y,
|
||||||
symbol_price_prec=lambda s, x, y: y,
|
symbol_price_prec=lambda s, x, y: y,
|
||||||
)
|
)
|
||||||
@ -2526,124 +2405,14 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf,
|
|||||||
assert rpc_mock.call_count == 2
|
assert rpc_mock.call_count == 2
|
||||||
|
|
||||||
|
|
||||||
def test_may_execute_sell_stoploss_on_exchange_multi(default_conf,
|
|
||||||
ticker, fee,
|
|
||||||
limit_buy_order,
|
|
||||||
markets, mocker) -> None:
|
|
||||||
"""
|
|
||||||
Tests workflow of selling stoploss_on_exchange.
|
|
||||||
Sells
|
|
||||||
* first trade as stoploss
|
|
||||||
* 2nd trade is kept
|
|
||||||
* 3rd trade is sold via sell-signal
|
|
||||||
"""
|
|
||||||
default_conf['max_open_trades'] = 3
|
|
||||||
default_conf['exchange']['name'] = 'binance'
|
|
||||||
patch_RPCManager(mocker)
|
|
||||||
patch_exchange(mocker)
|
|
||||||
|
|
||||||
stoploss_limit = {
|
|
||||||
'id': 123,
|
|
||||||
'info': {}
|
|
||||||
}
|
|
||||||
stoploss_order_open = {
|
|
||||||
"id": "123",
|
|
||||||
"timestamp": 1542707426845,
|
|
||||||
"datetime": "2018-11-20T09:50:26.845Z",
|
|
||||||
"lastTradeTimestamp": None,
|
|
||||||
"symbol": "BTC/USDT",
|
|
||||||
"type": "stop_loss_limit",
|
|
||||||
"side": "sell",
|
|
||||||
"price": 1.08801,
|
|
||||||
"amount": 90.99181074,
|
|
||||||
"cost": 0.0,
|
|
||||||
"average": 0.0,
|
|
||||||
"filled": 0.0,
|
|
||||||
"remaining": 0.0,
|
|
||||||
"status": "open",
|
|
||||||
"fee": None,
|
|
||||||
"trades": None
|
|
||||||
}
|
|
||||||
stoploss_order_closed = stoploss_order_open.copy()
|
|
||||||
stoploss_order_closed['status'] = 'closed'
|
|
||||||
# Sell first trade based on stoploss, keep 2nd and 3rd trade open
|
|
||||||
stoploss_order_mock = MagicMock(
|
|
||||||
side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open])
|
|
||||||
# Sell 3rd trade (not called for the first trade)
|
|
||||||
should_sell_mock = MagicMock(side_effect=[
|
|
||||||
SellCheckTuple(sell_flag=False, sell_type=SellType.NONE),
|
|
||||||
SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)]
|
|
||||||
)
|
|
||||||
cancel_order_mock = MagicMock()
|
|
||||||
mocker.patch('freqtrade.exchange.Binance.stoploss_limit', stoploss_limit)
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.exchange.Exchange',
|
|
||||||
get_ticker=ticker,
|
|
||||||
get_fee=fee,
|
|
||||||
markets=PropertyMock(return_value=markets),
|
|
||||||
symbol_amount_prec=lambda s, x, y: y,
|
|
||||||
symbol_price_prec=lambda s, x, y: y,
|
|
||||||
get_order=stoploss_order_mock,
|
|
||||||
cancel_order=cancel_order_mock,
|
|
||||||
)
|
|
||||||
|
|
||||||
wallets_mock = MagicMock()
|
|
||||||
mocker.patch.multiple(
|
|
||||||
'freqtrade.freqtradebot.FreqtradeBot',
|
|
||||||
create_stoploss_order=MagicMock(return_value=True),
|
|
||||||
update_trade_state=MagicMock(),
|
|
||||||
_notify_sell=MagicMock(),
|
|
||||||
)
|
|
||||||
mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock)
|
|
||||||
mocker.patch("freqtrade.wallets.Wallets.update", wallets_mock)
|
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
|
||||||
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
|
||||||
# Switch ordertype to market to close trade immediately
|
|
||||||
freqtrade.strategy.order_types['sell'] = 'market'
|
|
||||||
patch_get_signal(freqtrade)
|
|
||||||
|
|
||||||
# Create some test data
|
|
||||||
freqtrade.create_trades()
|
|
||||||
wallets_mock.reset_mock()
|
|
||||||
Trade.session = MagicMock()
|
|
||||||
|
|
||||||
trades = Trade.query.all()
|
|
||||||
# Make sure stoploss-order is open and trade is bought (since we mock update_trade_state)
|
|
||||||
for trade in trades:
|
|
||||||
trade.stoploss_order_id = 3
|
|
||||||
trade.open_order_id = None
|
|
||||||
|
|
||||||
freqtrade.process_maybe_execute_sells(trades)
|
|
||||||
assert should_sell_mock.call_count == 2
|
|
||||||
|
|
||||||
# Only order for 3rd trade needs to be cancelled
|
|
||||||
assert cancel_order_mock.call_count == 1
|
|
||||||
# Wallets should only be called once per sell cycle
|
|
||||||
assert wallets_mock.call_count == 1
|
|
||||||
|
|
||||||
trade = trades[0]
|
|
||||||
assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value
|
|
||||||
assert not trade.is_open
|
|
||||||
|
|
||||||
trade = trades[1]
|
|
||||||
assert not trade.sell_reason
|
|
||||||
assert trade.is_open
|
|
||||||
|
|
||||||
trade = trades[2]
|
|
||||||
assert trade.sell_reason == SellType.SELL_SIGNAL.value
|
|
||||||
assert not trade.is_open
|
|
||||||
|
|
||||||
|
|
||||||
def test_execute_sell_market_order(default_conf, ticker, fee,
|
def test_execute_sell_market_order(default_conf, ticker, fee,
|
||||||
ticker_sell_up, markets, mocker) -> None:
|
ticker_sell_up, mocker) -> None:
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
get_ticker=ticker,
|
get_ticker=ticker,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
patch_whitelist(mocker, default_conf)
|
patch_whitelist(mocker, default_conf)
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
@ -2689,7 +2458,7 @@ def test_execute_sell_market_order(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, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -2701,7 +2470,6 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order,
|
|||||||
}),
|
}),
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
default_conf['ask_strategy'] = {
|
default_conf['ask_strategy'] = {
|
||||||
'use_sell_signal': True,
|
'use_sell_signal': True,
|
||||||
@ -2721,7 +2489,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order,
|
|||||||
|
|
||||||
|
|
||||||
def test_sell_profit_only_disable_profit(default_conf, limit_buy_order,
|
def test_sell_profit_only_disable_profit(default_conf, limit_buy_order,
|
||||||
fee, markets, mocker) -> None:
|
fee, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -2733,7 +2501,6 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order,
|
|||||||
}),
|
}),
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
default_conf['ask_strategy'] = {
|
default_conf['ask_strategy'] = {
|
||||||
'use_sell_signal': True,
|
'use_sell_signal': True,
|
||||||
@ -2751,7 +2518,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order,
|
|||||||
assert trade.sell_reason == SellType.SELL_SIGNAL.value
|
assert trade.sell_reason == SellType.SELL_SIGNAL.value
|
||||||
|
|
||||||
|
|
||||||
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, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -2763,7 +2530,6 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, market
|
|||||||
}),
|
}),
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
default_conf['ask_strategy'] = {
|
default_conf['ask_strategy'] = {
|
||||||
'use_sell_signal': True,
|
'use_sell_signal': True,
|
||||||
@ -2781,7 +2547,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, market
|
|||||||
assert freqtrade.handle_trade(trade) is False
|
assert freqtrade.handle_trade(trade) is False
|
||||||
|
|
||||||
|
|
||||||
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, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -2793,7 +2559,6 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, marke
|
|||||||
}),
|
}),
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
default_conf['ask_strategy'] = {
|
default_conf['ask_strategy'] = {
|
||||||
'use_sell_signal': True,
|
'use_sell_signal': True,
|
||||||
@ -3115,7 +2880,7 @@ def test_tsl_only_offset_reached(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, mocker) -> None:
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -3127,7 +2892,6 @@ def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order,
|
|||||||
}),
|
}),
|
||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
default_conf['ask_strategy'] = {
|
default_conf['ask_strategy'] = {
|
||||||
'ignore_roi_if_buy_signal': False
|
'ignore_roi_if_buy_signal': False
|
||||||
@ -3422,7 +3186,7 @@ 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,
|
def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee, mocker,
|
||||||
order_book_l2):
|
order_book_l2):
|
||||||
default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True
|
default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True
|
||||||
default_conf['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = 0.1
|
default_conf['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = 0.1
|
||||||
@ -3434,7 +3198,6 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee,
|
|||||||
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,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Save state of current whitelist
|
# Save state of current whitelist
|
||||||
@ -3458,7 +3221,7 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order, fee,
|
|||||||
|
|
||||||
|
|
||||||
def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_order,
|
def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_order,
|
||||||
fee, markets, mocker, order_book_l2):
|
fee, mocker, order_book_l2):
|
||||||
default_conf['bid_strategy']['check_depth_of_market']['enabled'] = True
|
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
|
# 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
|
default_conf['bid_strategy']['check_depth_of_market']['bids_to_ask_delta'] = 100
|
||||||
@ -3470,7 +3233,6 @@ def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_o
|
|||||||
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,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
# Save state of current whitelist
|
# Save state of current whitelist
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
@ -3481,7 +3243,7 @@ def test_order_book_depth_of_market_high_delta(default_conf, ticker, limit_buy_o
|
|||||||
assert trade is None
|
assert trade is None
|
||||||
|
|
||||||
|
|
||||||
def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2, markets) -> None:
|
def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2) -> None:
|
||||||
"""
|
"""
|
||||||
test if function get_target_bid will return the order book price
|
test if function get_target_bid will return the order book price
|
||||||
instead of the ask rate
|
instead of the ask rate
|
||||||
@ -3490,7 +3252,6 @@ def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2, markets)
|
|||||||
ticker_mock = MagicMock(return_value={'ask': 0.045, 'last': 0.046})
|
ticker_mock = MagicMock(return_value={'ask': 0.045, 'last': 0.046})
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
markets=PropertyMock(return_value=markets),
|
|
||||||
get_order_book=order_book_l2,
|
get_order_book=order_book_l2,
|
||||||
get_ticker=ticker_mock,
|
get_ticker=ticker_mock,
|
||||||
|
|
||||||
@ -3506,7 +3267,7 @@ def test_order_book_bid_strategy1(mocker, default_conf, order_book_l2, markets)
|
|||||||
assert ticker_mock.call_count == 0
|
assert ticker_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2, markets) -> None:
|
def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2) -> None:
|
||||||
"""
|
"""
|
||||||
test if function get_target_bid will return the ask rate (since its value is lower)
|
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)
|
instead of the order book rate (even if enabled)
|
||||||
@ -3515,7 +3276,6 @@ def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2, markets)
|
|||||||
ticker_mock = MagicMock(return_value={'ask': 0.042, 'last': 0.046})
|
ticker_mock = MagicMock(return_value={'ask': 0.042, 'last': 0.046})
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
markets=PropertyMock(return_value=markets),
|
|
||||||
get_order_book=order_book_l2,
|
get_order_book=order_book_l2,
|
||||||
get_ticker=ticker_mock,
|
get_ticker=ticker_mock,
|
||||||
|
|
||||||
@ -3532,14 +3292,13 @@ def test_order_book_bid_strategy2(mocker, default_conf, order_book_l2, markets)
|
|||||||
assert ticker_mock.call_count == 0
|
assert ticker_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2, markets) -> None:
|
def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2) -> None:
|
||||||
"""
|
"""
|
||||||
test check depth of market
|
test check depth of market
|
||||||
"""
|
"""
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
markets=PropertyMock(return_value=markets),
|
|
||||||
get_order_book=order_book_l2
|
get_order_book=order_book_l2
|
||||||
)
|
)
|
||||||
default_conf['telegram']['enabled'] = False
|
default_conf['telegram']['enabled'] = False
|
||||||
@ -3554,7 +3313,7 @@ def test_check_depth_of_market_buy(default_conf, mocker, order_book_l2, markets)
|
|||||||
|
|
||||||
|
|
||||||
def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order,
|
def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order,
|
||||||
fee, markets, mocker, order_book_l2) -> None:
|
fee, mocker, order_book_l2) -> None:
|
||||||
"""
|
"""
|
||||||
test order book ask strategy
|
test order book ask strategy
|
||||||
"""
|
"""
|
||||||
@ -3576,7 +3335,6 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order
|
|||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
markets=PropertyMock(return_value=markets)
|
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
|
159
tests/test_integration.py
Normal file
159
tests/test_integration.py
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
|
from freqtrade.strategy.interface import SellCheckTuple, SellType
|
||||||
|
from tests.conftest import get_patched_freqtradebot, patch_get_signal
|
||||||
|
from freqtrade.rpc.rpc import RPC
|
||||||
|
|
||||||
|
|
||||||
|
def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
||||||
|
limit_buy_order, mocker) -> None:
|
||||||
|
"""
|
||||||
|
Tests workflow of selling stoploss_on_exchange.
|
||||||
|
Sells
|
||||||
|
* first trade as stoploss
|
||||||
|
* 2nd trade is kept
|
||||||
|
* 3rd trade is sold via sell-signal
|
||||||
|
"""
|
||||||
|
default_conf['max_open_trades'] = 3
|
||||||
|
default_conf['exchange']['name'] = 'binance'
|
||||||
|
|
||||||
|
stoploss_limit = {
|
||||||
|
'id': 123,
|
||||||
|
'info': {}
|
||||||
|
}
|
||||||
|
stoploss_order_open = {
|
||||||
|
"id": "123",
|
||||||
|
"timestamp": 1542707426845,
|
||||||
|
"datetime": "2018-11-20T09:50:26.845Z",
|
||||||
|
"lastTradeTimestamp": None,
|
||||||
|
"symbol": "BTC/USDT",
|
||||||
|
"type": "stop_loss_limit",
|
||||||
|
"side": "sell",
|
||||||
|
"price": 1.08801,
|
||||||
|
"amount": 90.99181074,
|
||||||
|
"cost": 0.0,
|
||||||
|
"average": 0.0,
|
||||||
|
"filled": 0.0,
|
||||||
|
"remaining": 0.0,
|
||||||
|
"status": "open",
|
||||||
|
"fee": None,
|
||||||
|
"trades": None
|
||||||
|
}
|
||||||
|
stoploss_order_closed = stoploss_order_open.copy()
|
||||||
|
stoploss_order_closed['status'] = 'closed'
|
||||||
|
# Sell first trade based on stoploss, keep 2nd and 3rd trade open
|
||||||
|
stoploss_order_mock = MagicMock(
|
||||||
|
side_effect=[stoploss_order_closed, stoploss_order_open, stoploss_order_open])
|
||||||
|
# Sell 3rd trade (not called for the first trade)
|
||||||
|
should_sell_mock = MagicMock(side_effect=[
|
||||||
|
SellCheckTuple(sell_flag=False, sell_type=SellType.NONE),
|
||||||
|
SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)]
|
||||||
|
)
|
||||||
|
cancel_order_mock = MagicMock()
|
||||||
|
mocker.patch('freqtrade.exchange.Binance.stoploss_limit', stoploss_limit)
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
symbol_amount_prec=lambda s, x, y: y,
|
||||||
|
symbol_price_prec=lambda s, x, y: y,
|
||||||
|
get_order=stoploss_order_mock,
|
||||||
|
cancel_order=cancel_order_mock,
|
||||||
|
)
|
||||||
|
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
|
create_stoploss_order=MagicMock(return_value=True),
|
||||||
|
update_trade_state=MagicMock(),
|
||||||
|
_notify_sell=MagicMock(),
|
||||||
|
)
|
||||||
|
mocker.patch("freqtrade.strategy.interface.IStrategy.should_sell", should_sell_mock)
|
||||||
|
wallets_mock = mocker.patch("freqtrade.wallets.Wallets.update", MagicMock())
|
||||||
|
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
|
# Switch ordertype to market to close trade immediately
|
||||||
|
freqtrade.strategy.order_types['sell'] = 'market'
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
|
# Create some test data
|
||||||
|
freqtrade.create_trades()
|
||||||
|
wallets_mock.reset_mock()
|
||||||
|
Trade.session = MagicMock()
|
||||||
|
|
||||||
|
trades = Trade.query.all()
|
||||||
|
# Make sure stoploss-order is open and trade is bought (since we mock update_trade_state)
|
||||||
|
for trade in trades:
|
||||||
|
trade.stoploss_order_id = 3
|
||||||
|
trade.open_order_id = None
|
||||||
|
|
||||||
|
freqtrade.process_maybe_execute_sells(trades)
|
||||||
|
assert should_sell_mock.call_count == 2
|
||||||
|
|
||||||
|
# Only order for 3rd trade needs to be cancelled
|
||||||
|
assert cancel_order_mock.call_count == 1
|
||||||
|
# Wallets should only be called once per sell cycle
|
||||||
|
assert wallets_mock.call_count == 1
|
||||||
|
|
||||||
|
trade = trades[0]
|
||||||
|
assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value
|
||||||
|
assert not trade.is_open
|
||||||
|
|
||||||
|
trade = trades[1]
|
||||||
|
assert not trade.sell_reason
|
||||||
|
assert trade.is_open
|
||||||
|
|
||||||
|
trade = trades[2]
|
||||||
|
assert trade.sell_reason == SellType.SELL_SIGNAL.value
|
||||||
|
assert not trade.is_open
|
||||||
|
|
||||||
|
|
||||||
|
def test_forcebuy_last_unlimited(default_conf, ticker, fee, limit_buy_order, mocker) -> None:
|
||||||
|
"""
|
||||||
|
Tests workflow
|
||||||
|
"""
|
||||||
|
default_conf['max_open_trades'] = 5
|
||||||
|
default_conf['forcebuy_enable'] = True
|
||||||
|
default_conf['stake_amount'] = 'unlimited'
|
||||||
|
default_conf['exchange']['name'] = 'binance'
|
||||||
|
default_conf['telegram']['enabled'] = True
|
||||||
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
|
mocker.patch('freqtrade.wallets.Wallets.get_free', MagicMock(
|
||||||
|
side_effect=[1000, 800, 600, 400, 200]
|
||||||
|
))
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.exchange.Exchange',
|
||||||
|
get_ticker=ticker,
|
||||||
|
get_fee=fee,
|
||||||
|
symbol_amount_prec=lambda s, x, y: y,
|
||||||
|
symbol_price_prec=lambda s, x, y: y,
|
||||||
|
)
|
||||||
|
|
||||||
|
mocker.patch.multiple(
|
||||||
|
'freqtrade.freqtradebot.FreqtradeBot',
|
||||||
|
create_stoploss_order=MagicMock(return_value=True),
|
||||||
|
update_trade_state=MagicMock(),
|
||||||
|
_notify_sell=MagicMock(),
|
||||||
|
)
|
||||||
|
|
||||||
|
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||||
|
rpc = RPC(freqtrade)
|
||||||
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
|
# Switch ordertype to market to close trade immediately
|
||||||
|
freqtrade.strategy.order_types['sell'] = 'market'
|
||||||
|
patch_get_signal(freqtrade)
|
||||||
|
|
||||||
|
# Create 4 trades
|
||||||
|
freqtrade.create_trades()
|
||||||
|
|
||||||
|
trades = Trade.query.all()
|
||||||
|
assert len(trades) == 4
|
||||||
|
rpc._rpc_forcebuy('TKN/BTC', None)
|
||||||
|
|
||||||
|
trades = Trade.query.all()
|
||||||
|
assert len(trades) == 5
|
||||||
|
|
||||||
|
for trade in trades:
|
||||||
|
assert trade.stake_amount == 200
|
@ -35,6 +35,8 @@ def create_mock_trades(fee):
|
|||||||
fee_open=fee.return_value,
|
fee_open=fee.return_value,
|
||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
open_rate=0.123,
|
open_rate=0.123,
|
||||||
|
close_rate=0.128,
|
||||||
|
close_profit=0.005,
|
||||||
exchange='bittrex',
|
exchange='bittrex',
|
||||||
is_open=False,
|
is_open=False,
|
||||||
open_order_id='dry_run_sell_12345'
|
open_order_id='dry_run_sell_12345'
|
||||||
@ -59,7 +61,7 @@ def test_init_create_session(default_conf):
|
|||||||
# Check if init create a session
|
# Check if init create a session
|
||||||
init(default_conf['db_url'], default_conf['dry_run'])
|
init(default_conf['db_url'], default_conf['dry_run'])
|
||||||
assert hasattr(Trade, 'session')
|
assert hasattr(Trade, 'session')
|
||||||
assert 'Session' in type(Trade.session).__name__
|
assert 'scoped_session' in type(Trade.session).__name__
|
||||||
|
|
||||||
|
|
||||||
def test_init_custom_db_url(default_conf, mocker):
|
def test_init_custom_db_url(default_conf, mocker):
|
||||||
@ -835,3 +837,38 @@ def test_stoploss_reinitialization(default_conf, fee):
|
|||||||
assert trade_adj.stop_loss_pct == -0.04
|
assert trade_adj.stop_loss_pct == -0.04
|
||||||
assert trade_adj.initial_stop_loss == 0.96
|
assert trade_adj.initial_stop_loss == 0.96
|
||||||
assert trade_adj.initial_stop_loss_pct == -0.04
|
assert trade_adj.initial_stop_loss_pct == -0.04
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
def test_total_open_trades_stakes(fee):
|
||||||
|
|
||||||
|
res = Trade.total_open_trades_stakes()
|
||||||
|
assert res == 0
|
||||||
|
create_mock_trades(fee)
|
||||||
|
res = Trade.total_open_trades_stakes()
|
||||||
|
assert res == 0.002
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
def test_get_overall_performance(fee):
|
||||||
|
|
||||||
|
create_mock_trades(fee)
|
||||||
|
res = Trade.get_overall_performance()
|
||||||
|
|
||||||
|
assert len(res) == 1
|
||||||
|
assert 'pair' in res[0]
|
||||||
|
assert 'profit' in res[0]
|
||||||
|
assert 'count' in res[0]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
def test_get_best_pair(fee):
|
||||||
|
|
||||||
|
res = Trade.get_best_pair()
|
||||||
|
assert res is None
|
||||||
|
|
||||||
|
create_mock_trades(fee)
|
||||||
|
res = Trade.get_best_pair()
|
||||||
|
assert len(res) == 2
|
||||||
|
assert res[0] == 'ETC/BTC'
|
||||||
|
assert res[1] == 0.005
|
||||||
|
81
tests/test_worker.py
Normal file
81
tests/test_worker.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from unittest.mock import MagicMock, PropertyMock
|
||||||
|
|
||||||
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
|
from freqtrade.state import State
|
||||||
|
from freqtrade.worker import Worker
|
||||||
|
from tests.conftest import get_patched_worker, log_has
|
||||||
|
|
||||||
|
|
||||||
|
def test_worker_state(mocker, default_conf, markets) -> None:
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
|
||||||
|
worker = get_patched_worker(mocker, default_conf)
|
||||||
|
assert worker.state is State.RUNNING
|
||||||
|
|
||||||
|
default_conf.pop('initial_state')
|
||||||
|
worker = Worker(args=None, config=default_conf)
|
||||||
|
assert worker.state is State.STOPPED
|
||||||
|
|
||||||
|
|
||||||
|
def test_worker_running(mocker, default_conf, caplog) -> None:
|
||||||
|
mock_throttle = MagicMock()
|
||||||
|
mocker.patch('freqtrade.worker.Worker._throttle', mock_throttle)
|
||||||
|
mocker.patch('freqtrade.persistence.Trade.stoploss_reinitialization', MagicMock())
|
||||||
|
|
||||||
|
worker = get_patched_worker(mocker, default_conf)
|
||||||
|
|
||||||
|
state = worker._worker(old_state=None)
|
||||||
|
assert state is State.RUNNING
|
||||||
|
assert log_has('Changing state to: RUNNING', caplog)
|
||||||
|
assert mock_throttle.call_count == 1
|
||||||
|
# Check strategy is loaded, and received a dataprovider object
|
||||||
|
assert worker.freqtrade.strategy
|
||||||
|
assert worker.freqtrade.strategy.dp
|
||||||
|
assert isinstance(worker.freqtrade.strategy.dp, DataProvider)
|
||||||
|
|
||||||
|
|
||||||
|
def test_worker_stopped(mocker, default_conf, caplog) -> None:
|
||||||
|
mock_throttle = MagicMock()
|
||||||
|
mocker.patch('freqtrade.worker.Worker._throttle', mock_throttle)
|
||||||
|
mock_sleep = mocker.patch('time.sleep', return_value=None)
|
||||||
|
|
||||||
|
worker = get_patched_worker(mocker, default_conf)
|
||||||
|
worker.state = State.STOPPED
|
||||||
|
state = worker._worker(old_state=State.RUNNING)
|
||||||
|
assert state is State.STOPPED
|
||||||
|
assert log_has('Changing state to: STOPPED', caplog)
|
||||||
|
assert mock_throttle.call_count == 0
|
||||||
|
assert mock_sleep.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_throttle(mocker, default_conf, caplog) -> None:
|
||||||
|
def throttled_func():
|
||||||
|
return 42
|
||||||
|
|
||||||
|
caplog.set_level(logging.DEBUG)
|
||||||
|
worker = get_patched_worker(mocker, default_conf)
|
||||||
|
|
||||||
|
start = time.time()
|
||||||
|
result = worker._throttle(throttled_func, min_secs=0.1)
|
||||||
|
end = time.time()
|
||||||
|
|
||||||
|
assert result == 42
|
||||||
|
assert end - start > 0.1
|
||||||
|
assert log_has('Throttling throttled_func for 0.10 seconds', caplog)
|
||||||
|
|
||||||
|
result = worker._throttle(throttled_func, min_secs=-1)
|
||||||
|
assert result == 42
|
||||||
|
|
||||||
|
|
||||||
|
def test_throttle_with_assets(mocker, default_conf) -> None:
|
||||||
|
def throttled_func(nb_assets=-1):
|
||||||
|
return nb_assets
|
||||||
|
|
||||||
|
worker = get_patched_worker(mocker, default_conf)
|
||||||
|
|
||||||
|
result = worker._throttle(throttled_func, min_secs=0.1, nb_assets=666)
|
||||||
|
assert result == 666
|
||||||
|
|
||||||
|
result = worker._throttle(throttled_func, min_secs=0.1)
|
||||||
|
assert result == -1
|
@ -2,12 +2,11 @@
|
|||||||
|
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from typing import Any, Callable, Dict, List
|
from typing import Any, Callable, Dict, List
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np # noqa
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
from skopt.space import Categorical, Dimension, Integer, Real
|
from skopt.space import Categorical, Dimension, Integer, Real # noqa
|
||||||
|
|
||||||
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
import freqtrade.vendor.qtpylib.indicators as qtpylib
|
||||||
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
from freqtrade.optimize.hyperopt_interface import IHyperOpt
|
||||||
@ -34,34 +33,6 @@ class SampleHyperOpts(IHyperOpt):
|
|||||||
Sample implementation of these methods can be found in
|
Sample implementation of these methods can be found in
|
||||||
https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py
|
https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py
|
||||||
"""
|
"""
|
||||||
@staticmethod
|
|
||||||
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
|
||||||
"""
|
|
||||||
Add several indicators needed for buy and sell strategies defined below.
|
|
||||||
"""
|
|
||||||
# ADX
|
|
||||||
dataframe['adx'] = ta.ADX(dataframe)
|
|
||||||
# MACD
|
|
||||||
macd = ta.MACD(dataframe)
|
|
||||||
dataframe['macd'] = macd['macd']
|
|
||||||
dataframe['macdsignal'] = macd['macdsignal']
|
|
||||||
# MFI
|
|
||||||
dataframe['mfi'] = ta.MFI(dataframe)
|
|
||||||
# RSI
|
|
||||||
dataframe['rsi'] = ta.RSI(dataframe)
|
|
||||||
# Stochastic Fast
|
|
||||||
stoch_fast = ta.STOCHF(dataframe)
|
|
||||||
dataframe['fastd'] = stoch_fast['fastd']
|
|
||||||
# Minus-DI
|
|
||||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
|
||||||
# Bollinger bands
|
|
||||||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
|
||||||
dataframe['bb_lowerband'] = bollinger['lower']
|
|
||||||
dataframe['bb_upperband'] = bollinger['upper']
|
|
||||||
# SAR
|
|
||||||
dataframe['sar'] = ta.SAR(dataframe)
|
|
||||||
|
|
||||||
return dataframe
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
|
||||||
|
@ -37,6 +37,9 @@ class AdvancedSampleHyperOpts(IHyperOpt):
|
|||||||
"""
|
"""
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
|
"""
|
||||||
|
This method can also be loaded from the strategy, if it doesn't exist in the hyperopt class.
|
||||||
|
"""
|
||||||
dataframe['adx'] = ta.ADX(dataframe)
|
dataframe['adx'] = ta.ADX(dataframe)
|
||||||
macd = ta.MACD(dataframe)
|
macd = ta.MACD(dataframe)
|
||||||
dataframe['macd'] = macd['macd']
|
dataframe['macd'] = macd['macd']
|
||||||
@ -229,8 +232,10 @@ class AdvancedSampleHyperOpts(IHyperOpt):
|
|||||||
|
|
||||||
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Based on TA indicators. Should be a copy of from strategy
|
Based on TA indicators.
|
||||||
must align to populate_indicators in this file
|
Can be a copy of the corresponding method from the strategy,
|
||||||
|
or will be loaded from the strategy.
|
||||||
|
Must align to populate_indicators used (either from this File, or from the strategy)
|
||||||
Only used when --spaces does not include buy
|
Only used when --spaces does not include buy
|
||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
@ -246,8 +251,10 @@ class AdvancedSampleHyperOpts(IHyperOpt):
|
|||||||
|
|
||||||
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Based on TA indicators. Should be a copy of from strategy
|
Based on TA indicators.
|
||||||
must align to populate_indicators in this file
|
Can be a copy of the corresponding method from the strategy,
|
||||||
|
or will be loaded from the strategy.
|
||||||
|
Must align to populate_indicators used (either from this File, or from the strategy)
|
||||||
Only used when --spaces does not include sell
|
Only used when --spaces does not include sell
|
||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.loc[
|
||||||
|
@ -68,9 +68,7 @@
|
|||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"execution_count": null,
|
"execution_count": null,
|
||||||
"metadata": {
|
"metadata": {},
|
||||||
"scrolled": true
|
|
||||||
},
|
|
||||||
"outputs": [],
|
"outputs": [],
|
||||||
"source": [
|
"source": [
|
||||||
"# Load strategy using values set above\n",
|
"# Load strategy using values set above\n",
|
||||||
@ -169,6 +167,31 @@
|
|||||||
"trades.groupby(\"pair\")[\"sell_reason\"].value_counts()"
|
"trades.groupby(\"pair\")[\"sell_reason\"].value_counts()"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Analyze the loaded trades for trade parallelism\n",
|
||||||
|
"This can be useful to find the best `max_open_trades` parameter, when used with backtesting in conjunction with `--disable-max-market-positions`.\n",
|
||||||
|
"\n",
|
||||||
|
"`analyze_trade_parallelism()` returns a timeseries dataframe with an \"open_trades\" column, specifying the number of open trades for each candle."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from freqtrade.data.btanalysis import analyze_trade_parallelism\n",
|
||||||
|
"\n",
|
||||||
|
"# Analyze the above\n",
|
||||||
|
"parallel_trades = analyze_trade_parallelism(trades, '5m')\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"parallel_trades.plot()"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "markdown",
|
"cell_type": "markdown",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
|
@ -107,16 +107,16 @@ class SampleStrategy(IStrategy):
|
|||||||
# RSI
|
# RSI
|
||||||
dataframe['rsi'] = ta.RSI(dataframe)
|
dataframe['rsi'] = ta.RSI(dataframe)
|
||||||
|
|
||||||
"""
|
|
||||||
# ADX
|
# ADX
|
||||||
dataframe['adx'] = ta.ADX(dataframe)
|
dataframe['adx'] = ta.ADX(dataframe)
|
||||||
|
"""
|
||||||
# Awesome oscillator
|
# Awesome oscillator
|
||||||
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
|
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
|
||||||
|
|
||||||
# Commodity Channel Index: values Oversold:<-100, Overbought:>100
|
# Commodity Channel Index: values Oversold:<-100, Overbought:>100
|
||||||
dataframe['cci'] = ta.CCI(dataframe)
|
dataframe['cci'] = ta.CCI(dataframe)
|
||||||
|
"""
|
||||||
# MACD
|
# MACD
|
||||||
macd = ta.MACD(dataframe)
|
macd = ta.MACD(dataframe)
|
||||||
dataframe['macd'] = macd['macd']
|
dataframe['macd'] = macd['macd']
|
||||||
@ -126,6 +126,7 @@ class SampleStrategy(IStrategy):
|
|||||||
# MFI
|
# MFI
|
||||||
dataframe['mfi'] = ta.MFI(dataframe)
|
dataframe['mfi'] = ta.MFI(dataframe)
|
||||||
|
|
||||||
|
"""
|
||||||
# Minus Directional Indicator / Movement
|
# Minus Directional Indicator / Movement
|
||||||
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
|
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
|
||||||
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
|
||||||
@ -149,12 +150,13 @@ class SampleStrategy(IStrategy):
|
|||||||
stoch = ta.STOCH(dataframe)
|
stoch = ta.STOCH(dataframe)
|
||||||
dataframe['slowd'] = stoch['slowd']
|
dataframe['slowd'] = stoch['slowd']
|
||||||
dataframe['slowk'] = stoch['slowk']
|
dataframe['slowk'] = stoch['slowk']
|
||||||
|
"""
|
||||||
# Stoch fast
|
# Stoch fast
|
||||||
stoch_fast = ta.STOCHF(dataframe)
|
stoch_fast = ta.STOCHF(dataframe)
|
||||||
dataframe['fastd'] = stoch_fast['fastd']
|
dataframe['fastd'] = stoch_fast['fastd']
|
||||||
dataframe['fastk'] = stoch_fast['fastk']
|
dataframe['fastk'] = stoch_fast['fastk']
|
||||||
|
|
||||||
|
"""
|
||||||
# Stoch RSI
|
# Stoch RSI
|
||||||
stoch_rsi = ta.STOCHRSI(dataframe)
|
stoch_rsi = ta.STOCHRSI(dataframe)
|
||||||
dataframe['fastd_rsi'] = stoch_rsi['fastd']
|
dataframe['fastd_rsi'] = stoch_rsi['fastd']
|
||||||
@ -178,12 +180,11 @@ class SampleStrategy(IStrategy):
|
|||||||
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
||||||
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
|
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
|
||||||
|
|
||||||
# SAR Parabol
|
|
||||||
dataframe['sar'] = ta.SAR(dataframe)
|
|
||||||
|
|
||||||
# SMA - Simple Moving Average
|
# SMA - Simple Moving Average
|
||||||
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
|
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
|
||||||
"""
|
"""
|
||||||
|
# SAR Parabol
|
||||||
|
dataframe['sar'] = ta.SAR(dataframe)
|
||||||
|
|
||||||
# TEMA - Triple Exponential Moving Average
|
# TEMA - Triple Exponential Moving Average
|
||||||
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
||||||
|
Loading…
Reference in New Issue
Block a user