Merge pull request #1 from freqtrade/develop

updating from the base repo
This commit is contained in:
wr0ngc0degen 2021-04-24 05:47:08 +02:00 committed by GitHub
commit e3c86643e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
76 changed files with 982 additions and 431 deletions

View File

@ -301,7 +301,7 @@ jobs:
runs-on: ubuntu-20.04
steps:
- name: Cleanup previous runs on this branch
uses: rokroskar/workflow-run-cleanup-action@v0.2.2
uses: rokroskar/workflow-run-cleanup-action@v0.3.2
if: "!startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/stable' && github.repository == 'freqtrade/freqtrade'"
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"

View File

@ -1,4 +1,4 @@
# Freqtrade
# ![freqtrade](docs/assets/freqtrade_poweredby.svg)
[![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/)
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)

View File

@ -163,7 +163,9 @@
"warning": "on",
"startup": "on",
"buy": "on",
"buy_fill": "on",
"sell": "on",
"sell_fill": "on",
"buy_cancel": "on",
"sell_cancel": "on"
}

View File

@ -79,9 +79,31 @@ class MyAwesomeStrategy(IStrategy):
class HyperOpt:
# Define a custom stoploss space.
def stoploss_space(self):
return [Real(-0.05, -0.01, name='stoploss')]
return [SKDecimal(-0.05, -0.01, decimals=3, name='stoploss')]
```
## Space options
For the additional spaces, scikit-optimize (in combination with Freqtrade) provides the following space types:
* `Categorical` - Pick from a list of categories (e.g. `Categorical(['a', 'b', 'c'], name="cat")`)
* `Integer` - Pick from a range of whole numbers (e.g. `Integer(1, 10, name='rsi')`)
* `SKDecimal` - Pick from a range of decimal numbers with limited precision (e.g. `SKDecimal(0.1, 0.5, decimals=3, name='adx')`). *Available only with freqtrade*.
* `Real` - Pick from a range of decimal numbers with full precision (e.g. `Real(0.1, 0.5, name='adx')`
You can import all of these from `freqtrade.optimize.space`, although `Categorical`, `Integer` and `Real` are only aliases for their corresponding scikit-optimize Spaces. `SKDecimal` is provided by freqtrade for faster optimizations.
``` python
from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal, Real # noqa
```
!!! Hint "SKDecimal vs. Real"
We recommend to use `SKDecimal` instead of the `Real` space in almost all cases. While the Real space provides full accuracy (up to ~16 decimal places) - this precision is rarely needed, and leads to unnecessary long hyperopt times.
Assuming the definition of a rather small space (`SKDecimal(0.10, 0.15, decimals=2, name='xxx')`) - SKDecimal will have 5 possibilities (`[0.10, 0.11, 0.12, 0.13, 0.14, 0.15]`).
A corresponding real space `Real(0.10, 0.15 name='xxx')` on the other hand has an almost unlimited number of possibilities (`[0.10, 0.010000000001, 0.010000000002, ... 0.014999999999, 0.01500000000]`).
---
## Legacy Hyperopt

View File

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" viewBox="0 0 90 90" width="100" height="100"><defs><path d="M0 90L0 0L90 0L90 90L0 90ZM50 60L60 60L60 80L70 80L70 60L80 60L80 50L50 50L50 60ZM30 80L40 80L40 70L30 70L30 80ZM30 60L20 60L20 70L10 70L10 80L20 80L20 70L30 70L30 60L40 60L40 50L30 50L30 60ZM10 60L20 60L20 50L10 50L10 60ZM10 40L40 40L40 30L20 30L20 20L40 20L40 10L10 10L10 40ZM50 40L80 40L80 30L60 30L60 20L80 20L80 10L50 10L50 40Z" id="c6g67PWSoP"></path></defs><g><g><g><use xlink:href="#c6g67PWSoP" opacity="1" fill="#000000" fill-opacity="1"></use></g></g></g></svg>

After

Width:  |  Height:  |  Size: 818 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -15,7 +15,8 @@ usage: freqtrade backtesting [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[--data-format-ohlcv {json,jsongz,hdf5}]
[--max-open-trades INT]
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
[--eps] [--dmmp] [--enable-protections]
[-p PAIRS [PAIRS ...]] [--eps] [--dmmp]
[--enable-protections]
[--dry-run-wallet DRY_RUN_WALLET]
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
[--export EXPORT] [--export-filename PATH]
@ -37,6 +38,9 @@ optional arguments:
setting.
--fee FLOAT Specify fee ratio. Will be applied twice (on trade
entry and exit).
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
Limit command to these pairs. Pairs are space-
separated.
--eps, --enable-position-stacking
Allow buying the same pair multiple times (position
stacking).

View File

@ -167,7 +167,7 @@ This exchange has also a limit on USD - where all orders must be > 10$ - which h
To guarantee safe execution, freqtrade will not allow buying with a stake-amount of 10.1$, instead, it'll make sure that there's enough space to place a stoploss below the pair (+ an offset, defined by `amount_reserve_percent`, which defaults to 5%).
With a stoploss of 10% - we'd therefore end up with a value of ~13.8$ (`12 * (1 + 0.05 + 0.1)`).
With a reserve of 5%, the minimum stake amount would be ~12.6$ (`12 * (1 + 0.05)`). If we take in account a stoploss of 10% on top of that - we'd end up with a value of ~14$ (`12.6 / (1 - 0.1)`).
To limit this calculation in case of large stoploss values, the calculated minimum stake-limit will never be more than 50% above the real limit.

View File

@ -11,8 +11,9 @@ Otherwise `--exchange` becomes mandatory.
You can use a relative timerange (`--days 20`) or an absolute starting point (`--timerange 20200101-`). For incremental downloads, the relative approach should be used.
!!! 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.
Be careful 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.
If you already have backtesting data available in your data-directory and would like to refresh this data up to today, do not use `--days` or `--timerange` parameters. Freqtrade will keep the available data and only download the missing data.
If you are updating existing data after inserting new pairs that you have no data for, use `--new-pairs-days xx` parameter. Specified number of days will be downloaded for new pairs while old pairs will be updated with missing data only.
If you use `--days xx` parameter alone - data for specified number of days will be downloaded for _all_ pairs. Be careful, if specified number of days is smaller than gap between now and last downloaded candle - freqtrade will delete all existing data to avoid gaps in candle data.
### Usage
@ -20,8 +21,9 @@ You can use a relative timerange (`--days 20`) or an absolute starting point (`-
usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[-d PATH] [--userdir PATH]
[-p PAIRS [PAIRS ...]] [--pairs-file FILE]
[--days INT] [--timerange TIMERANGE]
[--dl-trades] [--exchange EXCHANGE]
[--days INT] [--new-pairs-days INT]
[--timerange TIMERANGE] [--dl-trades]
[--exchange EXCHANGE]
[-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]]
[--erase]
[--data-format-ohlcv {json,jsongz,hdf5}]
@ -30,10 +32,12 @@ usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
optional arguments:
-h, --help show this help message and exit
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
Show profits for only these pairs. Pairs are space-
Limit command to these pairs. Pairs are space-
separated.
--pairs-file FILE File containing a list of pairs to download.
--days INT Download data for given number of days.
--new-pairs-days INT Download data of new pairs for given number of days.
Default: `None`.
--timerange TIMERANGE
Specify what timerange of data to use.
--dl-trades Download trades instead of OHLCV data. The bot will
@ -48,10 +52,10 @@ optional arguments:
exchange/pairs/timeframes.
--data-format-ohlcv {json,jsongz,hdf5}
Storage format for downloaded candle (OHLCV) data.
(default: `json`).
(default: `None`).
--data-format-trades {json,jsongz,hdf5}
Storage format for downloaded trades data. (default:
`jsongz`).
`None`).
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).

View File

@ -14,7 +14,7 @@ To simplify running freqtrade, please install [`docker-compose`](https://docs.do
## Freqtrade with docker-compose
Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) ready for usage.
Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.com/r/freqtradeorg/freqtrade/), as well as a [docker-compose file](https://github.com/freqtrade/freqtrade/blob/stable/docker-compose.yml) ready for usage.
!!! Note
- The following section assumes that `docker` and `docker-compose` are installed and available to the logged in user.
@ -22,7 +22,7 @@ Freqtrade provides an official Docker image on [Dockerhub](https://hub.docker.co
### Docker quick start
Create a new directory and place the [docker-compose file](https://github.com/freqtrade/freqtrade/blob/develop/docker-compose.yml) in this directory.
Create a new directory and place the [docker-compose file](https://raw.githubusercontent.com/freqtrade/freqtrade/stable/docker-compose.yml) in this directory.
=== "PC/MAC/Linux"
``` bash

View File

@ -215,8 +215,10 @@ Let's say the stake currency is **ETH** and there is $10$ **ETH** on the wallet.
usage: freqtrade edge [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--userdir PATH] [-s NAME] [--strategy-path PATH]
[-i TIMEFRAME] [--timerange TIMERANGE]
[--data-format-ohlcv {json,jsongz,hdf5}]
[--max-open-trades INT] [--stake-amount STAKE_AMOUNT]
[--fee FLOAT] [--stoplosses STOPLOSS_RANGE]
[--fee FLOAT] [-p PAIRS [PAIRS ...]]
[--stoplosses STOPLOSS_RANGE]
optional arguments:
-h, --help show this help message and exit
@ -224,6 +226,9 @@ optional arguments:
Specify timeframe (`1m`, `5m`, `30m`, `1h`, `1d`).
--timerange TIMERANGE
Specify what timerange of data to use.
--data-format-ohlcv {json,jsongz,hdf5}
Storage format for downloaded candle (OHLCV) data.
(default: `None`).
--max-open-trades INT
Override the value of the `max_open_trades`
configuration setting.
@ -232,6 +237,9 @@ optional arguments:
setting.
--fee FLOAT Specify fee ratio. Will be applied twice (on trade
entry and exit).
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
Limit command to these pairs. Pairs are space-
separated.
--stoplosses STOPLOSS_RANGE
Defines a range of stoploss values against which edge
will assess the strategy. The format is "min,max,step"

View File

@ -7,10 +7,10 @@ This page combines common gotchas and informations which are exchange-specific a
!!! Tip "Stoploss on Exchange"
Binance supports `stoploss_on_exchange` and uses stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
### Blacklists
### Binance Blacklist
For Binance, please add `"BNB/<STAKE>"` to your blacklist to avoid issues.
Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB order unsellable as the expected amount is not there anymore.
Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB trade unsellable as the expected amount is not there anymore.
### Binance sites
@ -100,6 +100,23 @@ To use subaccounts with FTX, you need to edit the configuration and add the foll
}
```
## Kucoin
Kucoin requries a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
```json
"exchange": {
"name": "kucoin",
"key": "your_exchange_key",
"secret": "your_exchange_secret",
"password": "your_exchange_api_key_password",
```
### Kucoin Blacklists
For Kucoin, please add `"KCS/<STAKE>"` to your blacklist to avoid issues.
Accounts having KCS accounts use this to pay for fees - if your first trade happens to be on `KCS`, further trades will consume this position and make the initial KCS trade unsellable as the expected amount is not there anymore.
## All exchanges
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.

View File

@ -44,8 +44,9 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--data-format-ohlcv {json,jsongz,hdf5}]
[--max-open-trades INT]
[--stake-amount STAKE_AMOUNT] [--fee FLOAT]
[--hyperopt NAME] [--hyperopt-path PATH] [--eps]
[--dmmp] [--enable-protections]
[-p PAIRS [PAIRS ...]] [--hyperopt NAME]
[--hyperopt-path PATH] [--eps] [--dmmp]
[--enable-protections]
[--dry-run-wallet DRY_RUN_WALLET] [-e INT]
[--spaces {all,buy,sell,roi,stoploss,trailing,default} [{all,buy,sell,roi,stoploss,trailing,default} ...]]
[--print-all] [--no-color] [--print-json] [-j JOBS]
@ -69,6 +70,9 @@ optional arguments:
setting.
--fee FLOAT Specify fee ratio. Will be applied twice (on trade
entry and exit).
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
Limit command to these pairs. Pairs are space-
separated.
--hyperopt NAME Specify hyperopt class name which will be used by the
bot.
--hyperopt-path PATH Specify additional lookup path for Hyperopt and
@ -105,7 +109,8 @@ optional arguments:
reproducible hyperopt results.
--min-trades INT Set minimal desired number of trades for evaluations
in the hyperopt optimization path (default: 1).
--hyperopt-loss NAME Specify the class name of the hyperopt loss function
--hyperopt-loss NAME, --hyperoptloss NAME
Specify the class name of the hyperopt loss function
class (IHyperOptLoss). Different functions can
generate completely different results, since the
target for optimization is different. Built-in
@ -294,6 +299,7 @@ Based on the results, hyperopt will tell you which parameter combination produce
## Parameter types
There are four parameter types each suited for different purposes.
* `IntParameter` - defines an integral parameter with upper and lower boundaries of search space.
* `DecimalParameter` - defines a floating point parameter with a limited number of decimals (default 3). Should be preferred instead of `RealParameter` in most cases.
* `RealParameter` - defines a floating point parameter with upper and lower boundaries and no precision limit. Rarely used as it creates a space with a near infinite number of possibilities.
@ -460,14 +466,14 @@ As stated in the comment, you can also use it as the value of the `minimal_roi`
#### Default ROI Search Space
If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the timeframe used. By default the values vary in the following ranges (for some of the most used timeframes, values are rounded to 5 digits after the decimal point):
If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the timeframe used. By default the values vary in the following ranges (for some of the most used timeframes, values are rounded to 3 digits after the decimal point):
| # step | 1m | | 5m | | 1h | | 1d | |
| ------ | ------ | ----------------- | -------- | ----------- | ---------- | ----------------- | ------------ | ----------------- |
| 1 | 0 | 0.01161...0.11992 | 0 | 0.03...0.31 | 0 | 0.06883...0.71124 | 0 | 0.12178...1.25835 |
| 2 | 2...8 | 0.00774...0.04255 | 10...40 | 0.02...0.11 | 120...480 | 0.04589...0.25238 | 2880...11520 | 0.08118...0.44651 |
| 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 |
| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 |
| # step | 1m | | 5m | | 1h | | 1d | |
| ------ | ------ | ------------- | -------- | ----------- | ---------- | ------------- | ------------ | ------------- |
| 1 | 0 | 0.011...0.119 | 0 | 0.03...0.31 | 0 | 0.068...0.711 | 0 | 0.121...1.258 |
| 2 | 2...8 | 0.007...0.042 | 10...40 | 0.02...0.11 | 120...480 | 0.045...0.252 | 2880...11520 | 0.081...0.446 |
| 3 | 4...20 | 0.003...0.015 | 20...100 | 0.01...0.04 | 240...1200 | 0.022...0.091 | 5760...28800 | 0.040...0.162 |
| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 |
These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the timeframe used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the timeframe used.
@ -477,6 +483,9 @@ Override the `roi_space()` method if you need components of the ROI tables to va
A sample for these methods can be found in [sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py).
!!! Note "Reduced search space"
To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs.
### Understand Hyperopt Stoploss results
If you are optimizing stoploss values (i.e. if optimization search-space contains 'all', 'default' or 'stoploss'), your result will look as follows and include stoploss:
@ -516,6 +525,9 @@ If you have the `stoploss_space()` method in your custom hyperopt file, remove i
Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py).
!!! Note "Reduced search space"
To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs.
### Understand Hyperopt Trailing Stop results
If you are optimizing trailing stop values (i.e. if optimization search-space contains 'all' or 'trailing'), your result will look as follows and include trailing stop parameters:
@ -551,6 +563,9 @@ If you are optimizing trailing stop values, Freqtrade creates the 'trailing' opt
Override the `trailing_space()` method and define the desired range in it if you need values of the trailing stop parameters to vary in other ranges during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/templates/sample_hyperopt_advanced.py).
!!! Note "Reduced search space"
To limit the search space further, Decimals are limited to 3 decimal places (a precision of 0.001). This is usually sufficient, every value more precise than this will usually result in overfitted results. You can however [overriding pre-defined spaces](advanced-hyperopt.md#pverriding-pre-defined-spaces) to change this to your needs.
### Reproducible results
The search for optimal parameters starts with a few (currently 30) random combinations in the hyperspace of parameters, random Hyperopt epochs. These random epochs are marked with an asterisk character (`*`) in the first column in the Hyperopt output.

View File

@ -60,6 +60,8 @@ When used in the chain of Pairlist Handlers in a non-leading position (after Sta
When used on the leading position of the chain of Pairlist Handlers, it does not consider `pair_whitelist` configuration setting, but selects the top assets from all available markets (with matching stake-currency) on the exchange.
The `refresh_period` setting allows to define the period (in seconds), at which the pairlist will be refreshed. Defaults to 1800s (30 minutes).
The pairlist cache (`refresh_period`) on `VolumePairList` is only applicable to generating pairlists.
Filtering instances (not the first position in the list) will not apply any cache and will always use up-to-date data.
`VolumePairList` is based on the ticker data from exchange, as reported by the ccxt library:
@ -90,6 +92,7 @@ This filter allows freqtrade to ignore pairs until they have been listed for at
#### PerformanceFilter
Sorts pairs by past trade performance, as follows:
1. Positive performance.
2. No closed trades yet.
3. Negative performance.

View File

@ -1,4 +1,5 @@
# Freqtrade
![freqtrade](assets/freqtrade_poweredby.svg)
[![Freqtrade CI](https://github.com/freqtrade/freqtrade/workflows/Freqtrade%20CI/badge.svg)](https://github.com/freqtrade/freqtrade/actions/)
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
@ -39,7 +40,7 @@ Please read the [exchange specific notes](exchanges.md) to learn about eventual,
- [X] [Bittrex](https://bittrex.com/)
- [X] [FTX](https://ftx.com)
- [X] [Kraken](https://kraken.com/)
- [ ] [potentially many others](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
- [ ] [potentially many others through <img alt="ccxt" width="30px" src="assets/ccxt-logo.svg" />](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
### Community tested

View File

@ -37,7 +37,7 @@ usage: freqtrade plot-dataframe [-h] [-v] [--logfile FILE] [-V] [-c PATH]
optional arguments:
-h, --help show this help message and exit
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
Show profits for only these pairs. Pairs are space-
Limit command to these pairs. Pairs are space-
separated.
--indicators1 INDICATORS1 [INDICATORS1 ...]
Set indicators from your strategy you want in the
@ -90,6 +90,7 @@ Strategy arguments:
Specify strategy class name which will be used by the
bot.
--strategy-path PATH Specify additional strategy lookup path.
```
Example:
@ -244,7 +245,7 @@ usage: freqtrade plot-profit [-h] [-v] [--logfile FILE] [-V] [-c PATH]
optional arguments:
-h, --help show this help message and exit
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
Show profits for only these pairs. Pairs are space-
Limit command to these pairs. Pairs are space-
separated.
--timerange TIMERANGE
Specify what timerange of data to use.
@ -286,6 +287,7 @@ Strategy arguments:
Specify strategy class name which will be used by the
bot.
--strategy-path PATH Specify additional strategy lookup path.
```
The `-p/--pairs` argument, can be used to limit the pairs that are considered for this calculation.

View File

@ -1,3 +1,3 @@
mkdocs-material==7.1.1
mkdocs-material==7.1.2
mdx_truly_sane_lists==1.2
pymdown-extensions==8.1.1

View File

@ -124,7 +124,8 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
| `stop` | Stops the trader.
| `stopbuy` | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
| `reload_config` | Reloads the configuration file.
| `trades` | List last trades.
| `trades` | List last trades. Limited to 500 trades per call.
| `trade/<tradeid>` | Get specific trade.
| `delete_trade <trade_id>` | Remove trade from the database. Tries to close open orders. Requires manual handling of this trade on the exchange.
| `show_config` | Shows part of the current configuration with relevant settings to operation.
| `logs` | Shows last log messages.
@ -181,7 +182,7 @@ count
Return the amount of open trades.
daily
Return the amount of open trades.
Return the profits for each day, and amount of trades.
delete_lock
Delete (disable) lock from the database.
@ -214,7 +215,7 @@ locks
logs
Show latest logs.
:param limit: Limits log messages to the last <limit> logs. No limit to get all the trades.
:param limit: Limits log messages to the last <limit> logs. No limit to get the entire log.
pair_candles
Return live dataframe for <pair><timeframe>.
@ -234,6 +235,9 @@ pair_history
performance
Return the performance of the different coins.
ping
simple ping
plot_config
Return plot configuration if the strategy defines one.
@ -270,17 +274,22 @@ strategy
:param strategy: Strategy class name
trades
Return trades history.
trade
Return specific trade
:param limit: Limits trades to the X last trades. No limit to get all the trades.
:param trade_id: Specify which trade to get.
trades
Return trades history, sorted by id
:param limit: Limits trades to the X last trades. Max 500 trades.
:param offset: Offset by this amount of trades.
version
Return the version of the bot.
whitelist
Show the current whitelist.
```
### OpenAPI interface

View File

@ -57,7 +57,7 @@ class AwesomeStrategy(IStrategy):
dataframe['atr'] = ta.ATR(dataframe)
if self.dp.runmode.value in ('backtest', 'hyperopt'):
# add indicator mapped to correct DatetimeIndex to custom_info
self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].copy().set_index('date')
self.custom_info[metadata['pair']] = dataframe[['date', 'atr']].set_index('date')
return dataframe
```

View File

@ -82,12 +82,19 @@ Example configuration showing the different settings:
"buy": "silent",
"sell": "on",
"buy_cancel": "silent",
"sell_cancel": "on"
"sell_cancel": "on",
"buy_fill": "off",
"sell_fill": "off"
},
"balance_dust_level": 0.01
},
```
`buy` notifications are sent when the order is placed, while `buy_fill` notifications are sent when the order is filled on the exchange.
`sell` notifications are sent when the order is placed, while `sell_fill` notifications are sent when the order is filled on the exchange.
`*_fill` notifications are off by default and must be explicitly enabled.
`balance_dust_level` will define what the `/balance` command takes as "dust" - Currencies with a balance below this will be shown.
## Create a custom keyboard (command shortcut buttons)

View File

@ -19,6 +19,11 @@ Sample configuration (tested using IFTTT).
"value1": "Cancelling Open Buy Order for {pair}",
"value2": "limit {limit:8f}",
"value3": "{stake_amount:8f} {stake_currency}"
},
"webhookbuyfill": {
"value1": "Buy Order for {pair} filled",
"value2": "at {open_rate:8f}",
"value3": ""
},
"webhooksell": {
"value1": "Selling {pair}",
@ -30,6 +35,11 @@ Sample configuration (tested using IFTTT).
"value2": "limit {limit:8f}",
"value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})"
},
"webhooksellfill": {
"value1": "Sell Order for {pair} filled",
"value2": "at {close_rate:8f}.",
"value3": ""
},
"webhookstatus": {
"value1": "Status: {status}",
"value2": "",
@ -91,6 +101,21 @@ Possible parameters are:
* `order_type`
* `current_rate`
### Webhookbuyfill
The fields in `webhook.webhookbuyfill` are filled when the bot filled a buy order. Parameters are filled using string.format.
Possible parameters are:
* `trade_id`
* `exchange`
* `pair`
* `open_rate`
* `amount`
* `open_date`
* `stake_amount`
* `stake_currency`
* `fiat_currency`
### Webhooksell
The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format.
@ -103,6 +128,27 @@ Possible parameters are:
* `limit`
* `amount`
* `open_rate`
* `profit_amount`
* `profit_ratio`
* `stake_currency`
* `fiat_currency`
* `sell_reason`
* `order_type`
* `open_date`
* `close_date`
### Webhooksellfill
The fields in `webhook.webhooksellfill` are filled when the bot fills a sell order (closes a Trae). Parameters are filled using string.format.
Possible parameters are:
* `trade_id`
* `exchange`
* `pair`
* `gain`
* `close_rate`
* `amount`
* `open_rate`
* `current_rate`
* `profit_amount`
* `profit_ratio`

View File

@ -17,7 +17,7 @@ ARGS_STRATEGY = ["strategy", "strategy_path"]
ARGS_TRADE = ["db_url", "sd_notify", "dry_run", "dry_run_wallet", "fee"]
ARGS_COMMON_OPTIMIZE = ["timeframe", "timerange", "dataformat_ohlcv",
"max_open_trades", "stake_amount", "fee"]
"max_open_trades", "stake_amount", "fee", "pairs"]
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
"enable_protections", "dry_run_wallet",
@ -60,8 +60,9 @@ ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"]
ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"]
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "timerange", "download_trades", "exchange",
"timeframes", "erase", "dataformat_ohlcv", "dataformat_trades"]
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "timerange",
"download_trades", "exchange", "timeframes", "erase", "dataformat_ohlcv",
"dataformat_trades"]
ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
"db_url", "trade_source", "export", "exportfilename",

View File

@ -330,7 +330,7 @@ AVAILABLE_CLI_OPTIONS = {
# Script options
"pairs": Arg(
'-p', '--pairs',
help='Show profits for only these pairs. Pairs are space-separated.',
help='Limit command to these pairs. Pairs are space-separated.',
nargs='+',
),
# Download data
@ -345,6 +345,12 @@ AVAILABLE_CLI_OPTIONS = {
type=check_int_positive,
metavar='INT',
),
"new_pairs_days": Arg(
'--new-pairs-days',
help='Download data of new pairs for given number of days. Default: `%(default)s`.',
type=check_int_positive,
metavar='INT',
),
"download_trades": Arg(
'--dl-trades',
help='Download trades instead of OHLCV data. The bot will resample trades to the '

View File

@ -62,8 +62,8 @@ def start_download_data(args: Dict[str, Any]) -> None:
if config.get('download_trades'):
pairs_not_available = refresh_backtest_trades_data(
exchange, pairs=expanded_pairs, datadir=config['datadir'],
timerange=timerange, erase=bool(config.get('erase')),
data_format=config['dataformat_trades'])
timerange=timerange, new_pairs_days=config['new_pairs_days'],
erase=bool(config.get('erase')), data_format=config['dataformat_trades'])
# Convert downloaded trade data to different timeframes
convert_trades_to_ohlcv(
@ -75,8 +75,9 @@ def start_download_data(args: Dict[str, Any]) -> None:
else:
pairs_not_available = refresh_backtest_ohlcv_data(
exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
datadir=config['datadir'], timerange=timerange, erase=bool(config.get('erase')),
data_format=config['dataformat_ohlcv'])
datadir=config['datadir'], timerange=timerange,
new_pairs_days=config['new_pairs_days'],
erase=bool(config.get('erase')), data_format=config['dataformat_ohlcv'])
except KeyboardInterrupt:
sys.exit("SIGINT received, aborting ...")

View File

@ -108,6 +108,8 @@ class Configuration:
self._process_plot_options(config)
self._process_data_options(config)
# Check if the exchange set by the user is supported
check_exchange(config, config.get('experimental', {}).get('block_bad_exchanges', True))
@ -399,6 +401,11 @@ class Configuration:
self._args_to_config(config, argname='dataformat_trades',
logstring='Using "{}" to store trades data.')
def _process_data_options(self, config: Dict[str, Any]) -> None:
self._args_to_config(config, argname='new_pairs_days',
logstring='Detected --new-pairs-days: {}')
def _process_runmode(self, config: Dict[str, Any]) -> None:
self._args_to_config(config, argname='dry_run',
@ -445,6 +452,7 @@ class Configuration:
"""
if "pairs" in config:
config['exchange']['pair_whitelist'] = config['pairs']
return
if "pairs_file" in self.args and self.args["pairs_file"]:

View File

@ -96,6 +96,7 @@ CONF_SCHEMA = {
'type': 'object',
'properties': {
'max_open_trades': {'type': ['integer', 'number'], 'minimum': -1},
'new_pairs_days': {'type': 'integer', 'default': 30},
'timeframe': {'type': 'string'},
'stake_currency': {'type': 'string'},
'stake_amount': {
@ -246,14 +247,24 @@ CONF_SCHEMA = {
'balance_dust_level': {'type': 'number', 'minimum': 0.0},
'notification_settings': {
'type': 'object',
'default': {},
'properties': {
'status': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'warning': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'startup': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'buy': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'sell': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'buy_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'sell_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS}
'buy_fill': {'type': 'string',
'enum': TELEGRAM_SETTING_OPTIONS,
'default': 'off'
},
'sell': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'sell_cancel': {'type': 'string', 'enum': TELEGRAM_SETTING_OPTIONS},
'sell_fill': {
'type': 'string',
'enum': TELEGRAM_SETTING_OPTIONS,
'default': 'off'
},
}
}
},

View File

@ -113,7 +113,7 @@ def ohlcv_fill_up_missing_data(dataframe: DataFrame, timeframe: str, pair: str)
pct_missing = (len_after - len_before) / len_before if len_before > 0 else 0
if len_before != len_after:
message = (f"Missing data fillup for {pair}: before: {len_before} - after: {len_after}"
f" - {round(pct_missing * 100, 2)} %")
f" - {round(pct_missing * 100, 2)}%")
if pct_missing > 0.01:
logger.info(message)
else:

View File

@ -155,6 +155,7 @@ def _load_cached_data_for_updating(pair: str, timeframe: str, timerange: Optiona
def _download_pair_history(datadir: Path,
exchange: Exchange,
pair: str, *,
new_pairs_days: int = 30,
timeframe: str = '5m',
timerange: Optional[TimeRange] = None,
data_handler: IDataHandler = None) -> bool:
@ -193,7 +194,7 @@ def _download_pair_history(datadir: Path,
timeframe=timeframe,
since_ms=since_ms if since_ms else
int(arrow.utcnow().shift(
days=-30).float_timestamp) * 1000
days=-new_pairs_days).float_timestamp) * 1000
)
# TODO: Maybe move parsing to exchange class (?)
new_dataframe = ohlcv_to_dataframe(new_data, timeframe, pair,
@ -223,7 +224,8 @@ def _download_pair_history(datadir: Path,
def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes: List[str],
datadir: Path, timerange: Optional[TimeRange] = None,
erase: bool = False, data_format: str = None) -> List[str]:
new_pairs_days: int = 30, erase: bool = False,
data_format: str = None) -> List[str]:
"""
Refresh stored ohlcv data for backtesting and hyperopt operations.
Used by freqtrade download-data subcommand.
@ -246,12 +248,14 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
logger.info(f'Downloading pair {pair}, interval {timeframe}.')
_download_pair_history(datadir=datadir, exchange=exchange,
pair=pair, timeframe=str(timeframe),
new_pairs_days=new_pairs_days,
timerange=timerange, data_handler=data_handler)
return pairs_not_available
def _download_trades_history(exchange: Exchange,
pair: str, *,
new_pairs_days: int = 30,
timerange: Optional[TimeRange] = None,
data_handler: IDataHandler
) -> bool:
@ -263,7 +267,7 @@ def _download_trades_history(exchange: Exchange,
since = timerange.startts * 1000 if \
(timerange and timerange.starttype == 'date') else int(arrow.utcnow().shift(
days=-30).float_timestamp) * 1000
days=-new_pairs_days).float_timestamp) * 1000
trades = data_handler.trades_load(pair)
@ -311,8 +315,8 @@ def _download_trades_history(exchange: Exchange,
def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: Path,
timerange: TimeRange, erase: bool = False,
data_format: str = 'jsongz') -> List[str]:
timerange: TimeRange, new_pairs_days: int = 30,
erase: bool = False, data_format: str = 'jsongz') -> List[str]:
"""
Refresh stored trades data for backtesting and hyperopt operations.
Used by freqtrade download-data subcommand.
@ -333,6 +337,7 @@ def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir:
logger.info(f'Downloading trades for pair {pair}.')
_download_trades_history(exchange=exchange,
pair=pair,
new_pairs_days=new_pairs_days,
timerange=timerange,
data_handler=data_handler)
return pairs_not_available

View File

@ -81,10 +81,15 @@ class Edge:
if config.get('fee'):
self.fee = config['fee']
else:
self.fee = self.exchange.get_fee(symbol=expand_pairlist(
self.config['exchange']['pair_whitelist'], list(self.exchange.markets))[0])
try:
self.fee = self.exchange.get_fee(symbol=expand_pairlist(
self.config['exchange']['pair_whitelist'], list(self.exchange.markets))[0])
except IndexError:
self.fee = None
def calculate(self, pairs: List[str]) -> bool:
if self.fee is None and pairs:
self.fee = self.exchange.get_fee(pairs[0])
heartbeat = self.edge_config.get('process_throttle_secs')

View File

@ -15,3 +15,4 @@ from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges,
validate_exchanges)
from freqtrade.exchange.ftx import Ftx
from freqtrade.exchange.kraken import Kraken
from freqtrade.exchange.kucoin import Kucoin

View File

@ -12,10 +12,6 @@ class Bittrex(Exchange):
"""
Bittrex exchange class. Contains adjustments needed for Freqtrade to work
with this exchange.
Please note that this exchange is not included in the list of exchanges
officially supported by the Freqtrade development team. So some features
may still not work as expected.
"""
_ft_has: Dict = {

View File

@ -14,6 +14,7 @@ from typing import Any, Dict, List, Optional, Tuple
import arrow
import ccxt
import ccxt.async_support as ccxt_async
from cachetools import TTLCache
from ccxt.base.decimal_to_precision import (ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE,
decimal_to_precision)
from pandas import DataFrame
@ -63,6 +64,7 @@ class Exchange:
"trades_pagination": "time", # Possible are "time" or "id"
"trades_pagination_arg": "since",
"l2_limit_range": None,
"l2_limit_range_required": True, # Allow Empty L2 limit (kucoin)
}
_ft_has: Dict = {}
@ -83,6 +85,9 @@ class Exchange:
# Timestamp of last markets refresh
self._last_markets_refresh: int = 0
# Cache for 10 minutes ...
self._fetch_tickers_cache: TTLCache = TTLCache(maxsize=1, ttl=60 * 10)
# Holds candles
self._klines: Dict[Tuple[str, str], DataFrame] = {}
@ -534,7 +539,9 @@ class Exchange:
# reserve some percent defined in config (5% default) + stoploss
amount_reserve_percent = 1.0 + self._config.get('amount_reserve_percent',
DEFAULT_AMOUNT_RESERVE_PERCENT)
amount_reserve_percent += abs(stoploss)
amount_reserve_percent = (
amount_reserve_percent / (1 - abs(stoploss)) if abs(stoploss) != 1 else 1.5
)
# it should not be more than 50%
amount_reserve_percent = max(min(amount_reserve_percent, 1.5), 1)
@ -692,9 +699,19 @@ class Exchange:
raise OperationalException(e) from e
@retrier
def get_tickers(self) -> Dict:
def get_tickers(self, cached: bool = False) -> Dict:
"""
:param cached: Allow cached result
:return: fetch_tickers result
"""
if cached:
tickers = self._fetch_tickers_cache.get('fetch_tickers')
if tickers:
return tickers
try:
return self._api.fetch_tickers()
tickers = self._api.fetch_tickers()
self._fetch_tickers_cache['fetch_tickers'] = tickers
return tickers
except ccxt.NotSupported as e:
raise OperationalException(
f'Exchange {self._api.name} does not support fetching tickers in batch. '
@ -1154,14 +1171,20 @@ class Exchange:
return self.fetch_order(order_id, pair)
@staticmethod
def get_next_limit_in_list(limit: int, limit_range: Optional[List[int]]):
def get_next_limit_in_list(limit: int, limit_range: Optional[List[int]],
range_required: bool = True):
"""
Get next greater value in the list.
Used by fetch_l2_order_book if the api only supports a limited range
"""
if not limit_range:
return limit
return min([x for x in limit_range if limit <= x] + [max(limit_range)])
result = min([x for x in limit_range if limit <= x] + [max(limit_range)])
if not range_required and limit > result:
# Range is not required - we can use None as parameter.
return None
return result
@retrier
def fetch_l2_order_book(self, pair: str, limit: int = 100) -> dict:
@ -1171,7 +1194,8 @@ class Exchange:
Returns a dict in the format
{'asks': [price, volume], 'bids': [price, volume]}
"""
limit1 = self.get_next_limit_in_list(limit, self._ft_has['l2_limit_range'])
limit1 = self.get_next_limit_in_list(limit, self._ft_has['l2_limit_range'],
self._ft_has['l2_limit_range_required'])
try:
return self._api.fetch_l2_order_book(pair, limit1)

View File

@ -0,0 +1,24 @@
""" Kucoin exchange subclass """
import logging
from typing import Dict
from freqtrade.exchange import Exchange
logger = logging.getLogger(__name__)
class Kucoin(Exchange):
"""
Kucoin exchange class. Contains adjustments needed for Freqtrade to work
with this exchange.
Please note that this exchange is not included in the list of exchanges
officially supported by the Freqtrade development team. So some features
may still not work as expected.
"""
_ft_has: Dict = {
"l2_limit_range": [20, 100],
"l2_limit_range_required": False,
}

View File

@ -113,7 +113,7 @@ class FreqtradeBot(LoggingMixin):
via RPC about changes in the bot status.
"""
self.rpc.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'type': RPCMessageType.STATUS,
'status': msg
})
@ -205,7 +205,7 @@ class FreqtradeBot(LoggingMixin):
if len(open_trades) != 0:
msg = {
'type': RPCMessageType.WARNING_NOTIFICATION,
'type': RPCMessageType.WARNING,
'status': f"{len(open_trades)} open trades active.\n\n"
f"Handle these trades manually on {self.exchange.name}, "
f"or '/start' the bot again and use '/stopbuy' "
@ -378,7 +378,7 @@ class FreqtradeBot(LoggingMixin):
if lock:
self.log_once(f"Global pairlock active until "
f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)}. "
"Not creating new trades.", logger.info)
f"Not creating new trades, reason: {lock.reason}.", logger.info)
else:
self.log_once("Global pairlock active. Not creating new trades.", logger.info)
return trades_created
@ -456,7 +456,8 @@ class FreqtradeBot(LoggingMixin):
lock = PairLocks.get_pair_longest_lock(pair, nowtime)
if lock:
self.log_once(f"Pair {pair} is still locked until "
f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)}.",
f"{lock.lock_end_time.strftime(constants.DATETIME_PRINT_FORMAT)} "
f"due to {lock.reason}.",
logger.info)
else:
self.log_once(f"Pair {pair} is still locked.", logger.info)
@ -634,7 +635,7 @@ class FreqtradeBot(LoggingMixin):
"""
msg = {
'trade_id': trade.id,
'type': RPCMessageType.BUY_NOTIFICATION,
'type': RPCMessageType.BUY,
'exchange': self.exchange.name.capitalize(),
'pair': trade.pair,
'limit': trade.open_rate,
@ -658,7 +659,7 @@ class FreqtradeBot(LoggingMixin):
msg = {
'trade_id': trade.id,
'type': RPCMessageType.BUY_CANCEL_NOTIFICATION,
'type': RPCMessageType.BUY_CANCEL,
'exchange': self.exchange.name.capitalize(),
'pair': trade.pair,
'limit': trade.open_rate,
@ -675,6 +676,21 @@ class FreqtradeBot(LoggingMixin):
# Send the message
self.rpc.send_msg(msg)
def _notify_buy_fill(self, trade: Trade) -> None:
msg = {
'trade_id': trade.id,
'type': RPCMessageType.BUY_FILL,
'exchange': self.exchange.name.capitalize(),
'pair': trade.pair,
'open_rate': trade.open_rate,
'stake_amount': trade.stake_amount,
'stake_currency': self.config['stake_currency'],
'fiat_currency': self.config.get('fiat_display_currency', None),
'amount': trade.amount,
'open_date': trade.open_date,
}
self.rpc.send_msg(msg)
#
# SELL / exit positions / close trades logic and methods
#
@ -1212,19 +1228,20 @@ class FreqtradeBot(LoggingMixin):
return True
def _notify_sell(self, trade: Trade, order_type: str) -> None:
def _notify_sell(self, trade: Trade, order_type: str, fill: bool = False) -> None:
"""
Sends rpc notification when a sell occured.
"""
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
profit_trade = trade.calc_profit(rate=profit_rate)
# Use cached rates here - it was updated seconds ago.
current_rate = self.get_sell_rate(trade.pair, False)
current_rate = self.get_sell_rate(trade.pair, False) if not fill else None
profit_ratio = trade.calc_profit_ratio(profit_rate)
gain = "profit" if profit_ratio > 0 else "loss"
msg = {
'type': RPCMessageType.SELL_NOTIFICATION,
'type': (RPCMessageType.SELL_FILL if fill
else RPCMessageType.SELL),
'trade_id': trade.id,
'exchange': trade.exchange.capitalize(),
'pair': trade.pair,
@ -1233,6 +1250,7 @@ class FreqtradeBot(LoggingMixin):
'order_type': order_type,
'amount': trade.amount,
'open_rate': trade.open_rate,
'close_rate': trade.close_rate,
'current_rate': current_rate,
'profit_amount': profit_trade,
'profit_ratio': profit_ratio,
@ -1267,7 +1285,7 @@ class FreqtradeBot(LoggingMixin):
gain = "profit" if profit_ratio > 0 else "loss"
msg = {
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
'type': RPCMessageType.SELL_CANCEL,
'trade_id': trade.id,
'exchange': trade.exchange.capitalize(),
'pair': trade.pair,
@ -1344,9 +1362,15 @@ class FreqtradeBot(LoggingMixin):
# Updating wallets when order is closed
if not trade.is_open:
if not stoploss_order and not trade.open_order_id:
self._notify_sell(trade, '', True)
self.protections.stop_per_pair(trade.pair)
self.protections.global_stop()
self.wallets.update()
elif not trade.open_order_id:
# Buy fill
self._notify_buy_fill(trade)
return False
def apply_fee_conditional(self, trade: Trade, trade_base_currency: str,

View File

@ -239,7 +239,7 @@ class Backtesting:
# Use the maximum between close_rate and low as we
# cannot sell outside of a candle.
# Applies when a new ROI setting comes in place and the whole candle is above that.
return max(close_rate, sell_row[LOW_IDX])
return min(max(close_rate, sell_row[LOW_IDX]), sell_row[HIGH_IDX])
else:
# This should not be reached...
@ -478,6 +478,7 @@ class Backtesting:
data: Dict[str, Any] = {}
data, timerange = self.load_bt_data()
logger.info("Dataload complete. Calculating indicators")
for strat in self.strategylist:
min_date, max_date = self.backtest_one_strategy(strat, data, timerange)

View File

@ -379,7 +379,7 @@ class Hyperopt:
logger.info(f"Using optimizer random state: {self.random_state}")
self.hyperopt_table_header = -1
data, timerange = self.backtesting.load_bt_data()
logger.info("Dataload complete. Calculating indicators")
preprocessed = self.backtesting.strategy.ohlcvdata_to_dataframe(data)
# Trim startup period from analyzed dataframe

View File

@ -7,11 +7,12 @@ import math
from abc import ABC
from typing import Any, Callable, Dict, List
from skopt.space import Categorical, Dimension, Integer, Real
from skopt.space import Categorical, Dimension, Integer
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes
from freqtrade.misc import round_dict
from freqtrade.optimize.space import SKDecimal
from freqtrade.strategy import IStrategy
@ -139,7 +140,7 @@ class IHyperOpt(ABC):
'roi_p2': roi_limits['roi_p2_min'],
'roi_p3': roi_limits['roi_p3_min'],
}
logger.info(f"Min roi table: {round_dict(self.generate_roi_table(p), 5)}")
logger.info(f"Min roi table: {round_dict(self.generate_roi_table(p), 3)}")
p = {
'roi_t1': roi_limits['roi_t1_max'],
'roi_t2': roi_limits['roi_t2_max'],
@ -148,15 +149,18 @@ class IHyperOpt(ABC):
'roi_p2': roi_limits['roi_p2_max'],
'roi_p3': roi_limits['roi_p3_max'],
}
logger.info(f"Max roi table: {round_dict(self.generate_roi_table(p), 5)}")
logger.info(f"Max roi table: {round_dict(self.generate_roi_table(p), 3)}")
return [
Integer(roi_limits['roi_t1_min'], roi_limits['roi_t1_max'], name='roi_t1'),
Integer(roi_limits['roi_t2_min'], roi_limits['roi_t2_max'], name='roi_t2'),
Integer(roi_limits['roi_t3_min'], roi_limits['roi_t3_max'], name='roi_t3'),
Real(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], name='roi_p1'),
Real(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], name='roi_p2'),
Real(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], name='roi_p3'),
SKDecimal(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], decimals=3,
name='roi_p1'),
SKDecimal(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], decimals=3,
name='roi_p2'),
SKDecimal(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], decimals=3,
name='roi_p3'),
]
def stoploss_space(self) -> List[Dimension]:
@ -167,7 +171,7 @@ class IHyperOpt(ABC):
You may override it in your custom Hyperopt class.
"""
return [
Real(-0.35, -0.02, name='stoploss'),
SKDecimal(-0.35, -0.02, decimals=3, name='stoploss'),
]
def generate_trailing_params(self, params: Dict) -> Dict:
@ -197,14 +201,14 @@ class IHyperOpt(ABC):
# other 'trailing' hyperspace parameters.
Categorical([True], name='trailing_stop'),
Real(0.01, 0.35, name='trailing_stop_positive'),
SKDecimal(0.01, 0.35, decimals=3, name='trailing_stop_positive'),
# 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive',
# so this intermediate parameter is used as the value of the difference between
# them. The value of the 'trailing_stop_positive_offset' is constructed in the
# generate_trailing_params() method.
# This is similar to the hyperspace dimensions used for constructing the ROI tables.
Real(0.001, 0.1, name='trailing_stop_positive_offset_p1'),
SKDecimal(0.001, 0.1, decimals=3, name='trailing_stop_positive_offset_p1'),
Categorical([True, False], name='trailing_only_offset_is_reached'),
]

View File

@ -0,0 +1,4 @@
# flake8: noqa: F401
from skopt.space import Categorical, Dimension, Integer, Real
from .decimalspace import SKDecimal

View File

@ -9,8 +9,9 @@ class SKDecimal(Integer):
self.decimals = decimals
_low = int(low * pow(10, self.decimals))
_high = int(high * pow(10, self.decimals))
self.low_orig = low
self.high_orig = high
# trunc to precision to avoid points out of space
self.low_orig = round(_low * pow(0.1, self.decimals), self.decimals)
self.high_orig = round(_high * pow(0.1, self.decimals), self.decimals)
super().__init__(_low, _high, prior, base, transform, name, dtype)

View File

@ -6,7 +6,6 @@ from datetime import datetime, timezone
from decimal import Decimal
from typing import Any, Dict, List, Optional
import arrow
from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, String,
create_engine, desc, func, inspect)
from sqlalchemy.exc import NoSuchModuleError
@ -160,8 +159,8 @@ class Order(_DECL_BASE):
if self.status in ('closed', 'canceled', 'cancelled'):
self.ft_is_open = False
if order.get('filled', 0) > 0:
self.order_filled_date = arrow.utcnow().datetime
self.order_update_date = arrow.utcnow().datetime
self.order_filled_date = datetime.now(timezone.utc)
self.order_update_date = datetime.now(timezone.utc)
@staticmethod
def update_orders(orders: List['Order'], order: Dict[str, Any]):
@ -548,6 +547,8 @@ class LocalTrade():
rate=(rate or self.close_rate),
fee=(fee or self.fee_close)
)
if self.open_trade_value == 0.0:
return 0.0
profit_ratio = (close_trade_value / self.open_trade_value) - 1
return float(f"{profit_ratio:.8f}")

View File

@ -85,7 +85,7 @@ class IPairList(LoggingMixin, ABC):
position in the chain.
:param cached_pairlist: Previously generated pairlist (cached)
:param tickers: Tickers (from exchange.get_tickers()).
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: List of pairs
"""
raise OperationalException("This Pairlist Handler should not be used "

View File

@ -46,7 +46,7 @@ class StaticPairList(IPairList):
"""
Generate the pairlist
:param cached_pairlist: Previously generated pairlist (cached)
:param tickers: Tickers (from exchange.get_tickers()).
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: List of pairs
"""
if self._allow_inactive:

View File

@ -67,7 +67,7 @@ class VolumePairList(IPairList):
"""
Generate the pairlist
:param cached_pairlist: Previously generated pairlist (cached)
:param tickers: Tickers (from exchange.get_tickers()).
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: List of pairs
"""
# Generate dynamic whitelist

View File

@ -61,7 +61,7 @@ class MaxDrawdown(IProtection):
if drawdown > self._max_allowed_drawdown:
self.log_once(
f"Trading stopped due to Max Drawdown {drawdown:.2f} < {self._max_allowed_drawdown}"
f"Trading stopped due to Max Drawdown {drawdown:.2f} > {self._max_allowed_drawdown}"
f" within {self.lookback_period_str}.", logger.info)
until = self.calculate_lock_end(trades, self._stop_duration)

View File

@ -61,7 +61,7 @@ class IResolver:
module = importlib.util.module_from_spec(spec)
try:
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
except (ModuleNotFoundError, SyntaxError, ImportError) as err:
except (ModuleNotFoundError, SyntaxError, ImportError, NameError) as err:
# Catch errors in case a specific module is not installed
logger.warning(f"Could not import {module_path} due to '{err}'")
if enum_failed:

View File

@ -189,7 +189,6 @@ class OpenTradeSchema(TradeSchema):
stoploss_current_dist_ratio: Optional[float]
stoploss_entry_dist: Optional[float]
stoploss_entry_dist_ratio: Optional[float]
base_currency: str
current_profit: float
current_profit_abs: float
current_profit_pct: float
@ -200,6 +199,7 @@ class OpenTradeSchema(TradeSchema):
class TradeResponse(BaseModel):
trades: List[TradeSchema]
trades_count: int
total_trades: int
class ForceBuyResponse(BaseModel):

View File

@ -85,8 +85,16 @@ def status(rpc: RPC = Depends(get_rpc)):
# Using the responsemodel here will cause a ~100% increase in response time (from 1s to 2s)
# on big databases. Correct response model: response_model=TradeResponse,
@router.get('/trades', tags=['info', 'trading'])
def trades(limit: int = 0, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_trade_history(limit)
def trades(limit: int = 500, offset: int = 0, rpc: RPC = Depends(get_rpc)):
return rpc._rpc_trade_history(limit, offset=offset, order_by_id=True)
@router.get('/trade/{tradeid}', response_model=OpenTradeSchema, tags=['info', 'trading'])
def trade(tradeid: int = 0, rpc: RPC = Depends(get_rpc)):
try:
return rpc._rpc_trade_status([tradeid])[0]
except (RPCException, KeyError):
raise HTTPException(status_code=404, detail='Trade not found.')
@router.delete('/trades/{tradeid}', response_model=DeleteTrade, tags=['info', 'trading'])

View File

@ -31,13 +31,15 @@ logger = logging.getLogger(__name__)
class RPCMessageType(Enum):
STATUS_NOTIFICATION = 'status'
WARNING_NOTIFICATION = 'warning'
STARTUP_NOTIFICATION = 'startup'
BUY_NOTIFICATION = 'buy'
BUY_CANCEL_NOTIFICATION = 'buy_cancel'
SELL_NOTIFICATION = 'sell'
SELL_CANCEL_NOTIFICATION = 'sell_cancel'
STATUS = 'status'
WARNING = 'warning'
STARTUP = 'startup'
BUY = 'buy'
BUY_FILL = 'buy_fill'
BUY_CANCEL = 'buy_cancel'
SELL = 'sell'
SELL_FILL = 'sell_fill'
SELL_CANCEL = 'sell_cancel'
def __repr__(self):
return self.value
@ -167,10 +169,13 @@ class RPC:
if trade.open_order_id:
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
# calculate profit and send message to user
try:
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
except (ExchangeError, PricingError):
current_rate = NAN
if trade.is_open:
try:
current_rate = self._freqtrade.get_sell_rate(trade.pair, False)
except (ExchangeError, PricingError):
current_rate = NAN
else:
current_rate = trade.close_rate
current_profit = trade.calc_profit_ratio(current_rate)
current_profit_abs = trade.calc_profit(current_rate)
@ -295,11 +300,12 @@ class RPC:
'data': data
}
def _rpc_trade_history(self, limit: int) -> Dict:
def _rpc_trade_history(self, limit: int, offset: int = 0, order_by_id: bool = False) -> Dict:
""" Returns the X last trades """
if limit > 0:
order_by = Trade.id if order_by_id else Trade.close_date.desc()
if limit:
trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
Trade.close_date.desc()).limit(limit)
order_by).limit(limit).offset(offset)
else:
trades = Trade.get_trades([Trade.is_open.is_(False)]).order_by(
Trade.close_date.desc()).all()
@ -308,7 +314,8 @@ class RPC:
return {
"trades": output,
"trades_count": len(output)
"trades_count": len(output),
"total_trades": Trade.get_trades([Trade.is_open.is_(False)]).count(),
}
def _rpc_stats(self) -> Dict[str, Any]:
@ -442,7 +449,7 @@ class RPC:
output = []
total = 0.0
try:
tickers = self._freqtrade.exchange.get_tickers()
tickers = self._freqtrade.exchange.get_tickers(cached=True)
except (ExchangeError):
raise RPCException('Error getting current tickers.')

View File

@ -67,7 +67,7 @@ class RPCManager:
def startup_messages(self, config: Dict[str, Any], pairlist, protections) -> None:
if config['dry_run']:
self.send_msg({
'type': RPCMessageType.WARNING_NOTIFICATION,
'type': RPCMessageType.WARNING,
'status': 'Dry run is enabled. All trades are simulated.'
})
stake_currency = config['stake_currency']
@ -79,7 +79,7 @@ class RPCManager:
exchange_name = config['exchange']['name']
strategy_name = config.get('strategy', '')
self.send_msg({
'type': RPCMessageType.STARTUP_NOTIFICATION,
'type': RPCMessageType.STARTUP,
'status': f'*Exchange:* `{exchange_name}`\n'
f'*Stake per trade:* `{stake_amount} {stake_currency}`\n'
f'*Minimum ROI:* `{minimal_roi}`\n'
@ -88,13 +88,13 @@ class RPCManager:
f'*Strategy:* `{strategy_name}`'
})
self.send_msg({
'type': RPCMessageType.STARTUP_NOTIFICATION,
'type': RPCMessageType.STARTUP,
'status': f'Searching for {stake_currency} pairs to buy and sell '
f'based on {pairlist.short_desc()}'
})
if len(protections.name_list) > 0:
prots = '\n'.join([p for prot in protections.short_desc() for k, p in prot.items()])
self.send_msg({
'type': RPCMessageType.STARTUP_NOTIFICATION,
'type': RPCMessageType.STARTUP,
'status': f'Using Protections: \n{prots}'
})

View File

@ -176,6 +176,53 @@ class Telegram(RPCHandler):
"""
self._updater.stop()
def _format_buy_msg(self, msg: Dict[str, Any]) -> str:
if self._rpc._fiat_converter:
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
else:
msg['stake_amount_fiat'] = 0
message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}"
f" (#{msg['trade_id']})\n"
f"*Amount:* `{msg['amount']:.8f}`\n"
f"*Open Rate:* `{msg['limit']:.8f}`\n"
f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}")
if msg.get('fiat_currency', None):
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
message += ")`"
return message
def _format_sell_msg(self, msg: Dict[str, Any]) -> str:
msg['amount'] = round(msg['amount'], 8)
msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2)
msg['duration'] = msg['close_date'].replace(
microsecond=0) - msg['open_date'].replace(microsecond=0)
msg['duration_min'] = msg['duration'].total_seconds() / 60
msg['emoji'] = self._get_sell_emoji(msg)
message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n"
"*Amount:* `{amount:.8f}`\n"
"*Open Rate:* `{open_rate:.8f}`\n"
"*Current Rate:* `{current_rate:.8f}`\n"
"*Close Rate:* `{limit:.8f}`\n"
"*Sell Reason:* `{sell_reason}`\n"
"*Duration:* `{duration} ({duration_min:.1f} min)`\n"
"*Profit:* `{profit_percent:.2f}%`").format(**msg)
# Check if all sell properties are available.
# This might not be the case if the message origin is triggered by /forcesell
if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency'])
and self._rpc._fiat_converter):
msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount(
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
message += (' `({gain}: {profit_amount:.8f} {stake_currency}'
' / {profit_fiat:.3f} {fiat_currency})`').format(**msg)
return message
def send_msg(self, msg: Dict[str, Any]) -> None:
""" Send a message to telegram channel """
@ -186,67 +233,33 @@ class Telegram(RPCHandler):
# Notification disabled
return
if msg['type'] == RPCMessageType.BUY_NOTIFICATION:
if self._rpc._fiat_converter:
msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount(
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
else:
msg['stake_amount_fiat'] = 0
if msg['type'] == RPCMessageType.BUY:
message = self._format_buy_msg(msg)
message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}"
f" (#{msg['trade_id']})\n"
f"*Amount:* `{msg['amount']:.8f}`\n"
f"*Open Rate:* `{msg['limit']:.8f}`\n"
f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}")
if msg.get('fiat_currency', None):
message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
message += ")`"
elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION:
elif msg['type'] in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL):
msg['message_side'] = 'buy' if msg['type'] == RPCMessageType.BUY_CANCEL else 'sell'
message = ("\N{WARNING SIGN} *{exchange}:* "
"Cancelling open buy Order for {pair} (#{trade_id}). "
"Cancelling open {message_side} Order for {pair} (#{trade_id}). "
"Reason: {reason}.".format(**msg))
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
msg['amount'] = round(msg['amount'], 8)
msg['profit_percent'] = round(msg['profit_ratio'] * 100, 2)
msg['duration'] = msg['close_date'].replace(
microsecond=0) - msg['open_date'].replace(microsecond=0)
msg['duration_min'] = msg['duration'].total_seconds() / 60
elif msg['type'] == RPCMessageType.BUY_FILL:
message = ("\N{LARGE CIRCLE} *{exchange}:* "
"Buy order for {pair} (#{trade_id}) filled "
"for {open_rate}.".format(**msg))
elif msg['type'] == RPCMessageType.SELL_FILL:
message = ("\N{LARGE CIRCLE} *{exchange}:* "
"Sell order for {pair} (#{trade_id}) filled "
"for {close_rate}.".format(**msg))
elif msg['type'] == RPCMessageType.SELL:
message = self._format_sell_msg(msg)
msg['emoji'] = self._get_sell_emoji(msg)
message = ("{emoji} *{exchange}:* Selling {pair} (#{trade_id})\n"
"*Amount:* `{amount:.8f}`\n"
"*Open Rate:* `{open_rate:.8f}`\n"
"*Current Rate:* `{current_rate:.8f}`\n"
"*Close Rate:* `{limit:.8f}`\n"
"*Sell Reason:* `{sell_reason}`\n"
"*Duration:* `{duration} ({duration_min:.1f} min)`\n"
"*Profit:* `{profit_percent:.2f}%`").format(**msg)
# Check if all sell properties are available.
# This might not be the case if the message origin is triggered by /forcesell
if (all(prop in msg for prop in ['gain', 'fiat_currency', 'stake_currency'])
and self._rpc._fiat_converter):
msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount(
msg['profit_amount'], msg['stake_currency'], msg['fiat_currency'])
message += (' `({gain}: {profit_amount:.8f} {stake_currency}'
' / {profit_fiat:.3f} {fiat_currency})`').format(**msg)
elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION:
message = ("\N{WARNING SIGN} *{exchange}:* Cancelling Open Sell Order "
"for {pair} (#{trade_id}). Reason: {reason}").format(**msg)
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION:
elif msg['type'] == RPCMessageType.STATUS:
message = '*Status:* `{status}`'.format(**msg)
elif msg['type'] == RPCMessageType.WARNING_NOTIFICATION:
elif msg['type'] == RPCMessageType.WARNING:
message = '\N{WARNING SIGN} *Warning:* `{status}`'.format(**msg)
elif msg['type'] == RPCMessageType.STARTUP_NOTIFICATION:
elif msg['type'] == RPCMessageType.STARTUP:
message = '{status}'.format(**msg)
else:
@ -702,7 +715,7 @@ class Telegram(RPCHandler):
f"({trade['count']})</code>\n")
if len(output + stat_line) >= MAX_TELEGRAM_MESSAGE_LENGTH:
self._send_msg(output)
self._send_msg(output, parse_mode=ParseMode.HTML)
output = stat_line
else:
output += stat_line

View File

@ -45,17 +45,21 @@ class Webhook(RPCHandler):
""" Send a message to telegram channel """
try:
if msg['type'] == RPCMessageType.BUY_NOTIFICATION:
if msg['type'] == RPCMessageType.BUY:
valuedict = self._config['webhook'].get('webhookbuy', None)
elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION:
elif msg['type'] == RPCMessageType.BUY_CANCEL:
valuedict = self._config['webhook'].get('webhookbuycancel', None)
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
elif msg['type'] == RPCMessageType.BUY_FILL:
valuedict = self._config['webhook'].get('webhookbuyfill', None)
elif msg['type'] == RPCMessageType.SELL:
valuedict = self._config['webhook'].get('webhooksell', None)
elif msg['type'] == RPCMessageType.SELL_CANCEL_NOTIFICATION:
elif msg['type'] == RPCMessageType.SELL_FILL:
valuedict = self._config['webhook'].get('webhooksellfill', None)
elif msg['type'] == RPCMessageType.SELL_CANCEL:
valuedict = self._config['webhook'].get('webhooksellcancel', None)
elif msg['type'] in (RPCMessageType.STATUS_NOTIFICATION,
RPCMessageType.STARTUP_NOTIFICATION,
RPCMessageType.WARNING_NOTIFICATION):
elif msg['type'] in (RPCMessageType.STATUS,
RPCMessageType.STARTUP,
RPCMessageType.WARNING):
valuedict = self._config['webhook'].get('webhookstatus', None)
else:
raise NotImplementedError('Unknown message type: {}'.format(msg['type']))

View File

@ -10,7 +10,7 @@ from typing import Any, Iterator, Optional, Sequence, Tuple, Union
with suppress(ImportError):
from skopt.space import Integer, Real, Categorical
from freqtrade.optimize.decimalspace import SKDecimal
from freqtrade.optimize.space import SKDecimal
from freqtrade.exceptions import OperationalException

View File

@ -7,7 +7,7 @@ from typing import Any, Callable, Dict, List
import numpy as np # noqa
import pandas as pd # noqa
from pandas import DataFrame
from skopt.space import Categorical, Dimension, Integer, Real # noqa
from freqtrade.optimize.space import Categorical, Dimension, Integer, SKDecimal, Real # noqa
from freqtrade.optimize.hyperopt_interface import IHyperOpt
@ -223,9 +223,9 @@ class AdvancedSampleHyperOpt(IHyperOpt):
Integer(10, 120, name='roi_t1'),
Integer(10, 60, name='roi_t2'),
Integer(10, 40, name='roi_t3'),
Real(0.01, 0.04, name='roi_p1'),
Real(0.01, 0.07, name='roi_p2'),
Real(0.01, 0.20, name='roi_p3'),
SKDecimal(0.01, 0.04, decimals=3, name='roi_p1'),
SKDecimal(0.01, 0.07, decimals=3, name='roi_p2'),
SKDecimal(0.01, 0.20, decimals=3, name='roi_p3'),
]
@staticmethod
@ -237,7 +237,7 @@ class AdvancedSampleHyperOpt(IHyperOpt):
'stoploss' optimization hyperspace.
"""
return [
Real(-0.35, -0.02, name='stoploss'),
SKDecimal(-0.35, -0.02, decimals=3, name='stoploss'),
]
@staticmethod
@ -256,14 +256,14 @@ class AdvancedSampleHyperOpt(IHyperOpt):
# other 'trailing' hyperspace parameters.
Categorical([True], name='trailing_stop'),
Real(0.01, 0.35, name='trailing_stop_positive'),
SKDecimal(0.01, 0.35, decimals=3, name='trailing_stop_positive'),
# 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive',
# so this intermediate parameter is used as the value of the difference between
# them. The value of the 'trailing_stop_positive_offset' is constructed in the
# generate_trailing_params() method.
# This is similar to the hyperspace dimensions used for constructing the ROI tables.
Real(0.001, 0.1, name='trailing_stop_positive_offset_p1'),
SKDecimal(0.001, 0.1, decimals=3, name='trailing_stop_positive_offset_p1'),
Categorical([True, False], name='trailing_only_offset_is_reached'),
]

View File

@ -4,12 +4,12 @@
-r requirements-hyperopt.txt
coveralls==3.0.1
flake8==3.9.0
flake8==3.9.1
flake8-type-annotations==0.1.0
flake8-tidy-imports==4.2.1
mypy==0.812
pytest==6.2.3
pytest-asyncio==0.14.0
pytest-asyncio==0.15.0
pytest-cov==2.11.1
pytest-mock==3.5.1
pytest-random-order==1.0.4

View File

@ -1,11 +1,11 @@
numpy==1.20.2
pandas==1.2.3
pandas==1.2.4
ccxt==1.47.47
ccxt==1.48.22
# Pin cryptography for now due to rust build errors with piwheels
cryptography==3.4.7
aiohttp==3.7.4.post0
SQLAlchemy==1.4.7
SQLAlchemy==1.4.9
python-telegram-bot==13.4.1
arrow==1.0.3
cachetools==4.2.1

View File

@ -127,7 +127,7 @@ class FtRestClient():
return self._delete("locks/{}".format(lock_id))
def daily(self, days=None):
"""Return the amount of open trades.
"""Return the profits for each day, and amount of trades.
:return: json object
"""
@ -195,18 +195,32 @@ class FtRestClient():
def logs(self, limit=None):
"""Show latest logs.
:param limit: Limits log messages to the last <limit> logs. No limit to get all the trades.
:param limit: Limits log messages to the last <limit> logs. No limit to get the entire log.
:return: json object
"""
return self._get("logs", params={"limit": limit} if limit else 0)
def trades(self, limit=None):
"""Return trades history.
def trades(self, limit=None, offset=None):
"""Return trades history, sorted by id
:param limit: Limits trades to the X last trades. No limit to get all the trades.
:param limit: Limits trades to the X last trades. Max 500 trades.
:param offset: Offset by this amount of trades.
:return: json object
"""
return self._get("trades", params={"limit": limit} if limit else 0)
params = {}
if limit:
params['limit'] = limit
if offset:
params['offset'] = offset
return self._get("trades", params)
def trade(self, trade_id):
"""Return specific trade
:param trade_id: Specify which trade to get.
:return: json object
"""
return self._get("trade/{}".format(trade_id))
def delete_trade(self, trade_id):
"""Delete trade from the database.

View File

@ -138,7 +138,7 @@ function install_macos() {
# Install bot Debian_ubuntu
function install_debian() {
sudo apt-get update
sudo apt-get install -y build-essential autoconf libtool pkg-config make wget git
sudo apt-get install -y build-essential autoconf libtool pkg-config make wget git libpython3-dev
install_talib
}

View File

@ -116,7 +116,7 @@ def test_list_timeframes(mocker, capsys):
'1h': 'hour',
'1d': 'day',
}
patch_exchange(mocker, api_mock=api_mock)
patch_exchange(mocker, api_mock=api_mock, id='bittrex')
args = [
"list-timeframes",
]
@ -201,7 +201,7 @@ def test_list_markets(mocker, markets, capsys):
api_mock = MagicMock()
api_mock.markets = markets
patch_exchange(mocker, api_mock=api_mock)
patch_exchange(mocker, api_mock=api_mock, id='bittrex')
# Test with no --config
args = [

View File

@ -59,7 +59,7 @@
}
},
"exchange": {
"name": "bittrex",
"name": "binance",
"sandbox": false,
"key": "your_exchange_key",
"secret": "your_exchange_secret",

View File

@ -79,7 +79,7 @@ def patched_configuration_load_config_file(mocker, config) -> None:
)
def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> None:
def patch_exchange(mocker, api_mock=None, id='binance', mock_markets=True) -> None:
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
@ -98,7 +98,7 @@ def patch_exchange(mocker, api_mock=None, id='bittrex', mock_markets=True) -> No
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock())
def get_patched_exchange(mocker, config, api_mock=None, id='bittrex',
def get_patched_exchange(mocker, config, api_mock=None, id='binance',
mock_markets=True) -> Exchange:
patch_exchange(mocker, api_mock, id, mock_markets)
config['exchange']['name'] = id
@ -293,7 +293,7 @@ def get_default_conf(testdatadir):
"order_book_max": 1
},
"exchange": {
"name": "bittrex",
"name": "binance",
"enabled": True,
"key": "key",
"secret": "secret",
@ -314,7 +314,8 @@ def get_default_conf(testdatadir):
"telegram": {
"enabled": True,
"token": "token",
"chat_id": "0"
"chat_id": "0",
"notification_settings": {},
},
"datadir": str(testdatadir),
"initial_state": "running",
@ -1765,7 +1766,7 @@ def open_trade():
return Trade(
pair='ETH/BTC',
open_rate=0.00001099,
exchange='bittrex',
exchange='binance',
open_order_id='123456789',
amount=90.99181073,
fee_open=0.0,

View File

@ -31,7 +31,7 @@ def mock_trade_1(fee):
is_open=True,
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17),
open_rate=0.123,
exchange='bittrex',
exchange='binance',
open_order_id='dry_run_buy_12345',
strategy='DefaultStrategy',
timeframe=5,
@ -84,7 +84,7 @@ def mock_trade_2(fee):
close_rate=0.128,
close_profit=0.005,
close_profit_abs=0.000584127,
exchange='bittrex',
exchange='binance',
is_open=False,
open_order_id='dry_run_sell_12345',
strategy='DefaultStrategy',
@ -144,7 +144,7 @@ def mock_trade_3(fee):
close_rate=0.06,
close_profit=0.01,
close_profit_abs=0.000155,
exchange='bittrex',
exchange='binance',
is_open=False,
strategy='DefaultStrategy',
timeframe=5,
@ -187,7 +187,7 @@ def mock_trade_4(fee):
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=14),
is_open=True,
open_rate=0.123,
exchange='bittrex',
exchange='binance',
open_order_id='prod_buy_12345',
strategy='DefaultStrategy',
timeframe=5,
@ -239,7 +239,7 @@ def mock_trade_5(fee):
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=12),
is_open=True,
open_rate=0.123,
exchange='bittrex',
exchange='binance',
strategy='SampleStrategy',
stoploss_order_id='prod_stoploss_3455',
timeframe=5,
@ -293,7 +293,7 @@ def mock_trade_6(fee):
fee_close=fee.return_value,
is_open=True,
open_rate=0.15,
exchange='bittrex',
exchange='binance',
strategy='SampleStrategy',
open_order_id="prod_sell_6",
timeframe=5,

View File

@ -330,11 +330,11 @@ def test_edge_process_no_data(mocker, edge_conf, caplog):
def test_edge_process_no_trades(mocker, edge_conf, caplog):
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))
mocker.patch('freqtrade.edge.edge_positioning.refresh_data', MagicMock())
mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0.001)
mocker.patch('freqtrade.edge.edge_positioning.refresh_data', )
mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data)
# Return empty
mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', MagicMock(return_value=[]))
mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', return_value=[])
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
assert not edge.calculate(edge_conf['exchange']['pair_whitelist'])
@ -342,6 +342,23 @@ def test_edge_process_no_trades(mocker, edge_conf, caplog):
assert log_has("No trades found.", caplog)
def test_edge_process_no_pairs(mocker, edge_conf, caplog):
edge_conf['exchange']['pair_whitelist'] = []
freqtrade = get_patched_freqtradebot(mocker, edge_conf)
fee_mock = mocker.patch('freqtrade.exchange.Exchange.get_fee', return_value=0.001)
mocker.patch('freqtrade.edge.edge_positioning.refresh_data')
mocker.patch('freqtrade.edge.edge_positioning.load_data', mocked_load_data)
# Return empty
mocker.patch('freqtrade.edge.Edge._find_trades_for_stoploss_range', return_value=[])
edge = Edge(edge_conf, freqtrade.exchange, freqtrade.strategy)
assert fee_mock.call_count == 0
assert edge.fee is None
assert not edge.calculate(['XRP/USDT'])
assert fee_mock.call_count == 1
assert edge.fee == 0.001
def test_edge_init_error(mocker, edge_conf,):
edge_conf['stake_amount'] = 0.5
mocker.patch('freqtrade.exchange.Exchange.get_fee', MagicMock(return_value=0.001))

View File

@ -36,7 +36,12 @@ EXCHANGES = {
'pair': 'BTC/USDT',
'hasQuoteVolume': True,
'timeframe': '5m',
}
},
'kucoin': {
'pair': 'BTC/USDT',
'hasQuoteVolume': True,
'timeframe': '5m',
},
}
@ -100,14 +105,16 @@ class TestCCXTExchange():
assert 'asks' in l2
assert 'bids' in l2
l2_limit_range = exchange._ft_has['l2_limit_range']
l2_limit_range_required = exchange._ft_has['l2_limit_range_required']
for val in [1, 2, 5, 25, 100]:
l2 = exchange.fetch_l2_order_book(pair, val)
if not l2_limit_range or val in l2_limit_range:
assert len(l2['asks']) == val
assert len(l2['bids']) == val
else:
next_limit = exchange.get_next_limit_in_list(val, l2_limit_range)
if next_limit > 200:
next_limit = exchange.get_next_limit_in_list(
val, l2_limit_range, l2_limit_range_required)
if next_limit is None or next_limit > 200:
# Large orderbook sizes can be a problem for some exchanges (bitrex ...)
assert len(l2['asks']) > 200
assert len(l2['asks']) > 200

View File

@ -371,7 +371,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
PropertyMock(return_value=markets)
)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 1, stoploss)
assert isclose(result, 2 * 1.1)
assert isclose(result, 2 * (1+0.05) / (1-abs(stoploss)))
# min amount is set
markets["ETH/BTC"]["limits"] = {
@ -383,7 +383,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
PropertyMock(return_value=markets)
)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
assert isclose(result, 2 * 2 * 1.1)
assert isclose(result, 2 * 2 * (1+0.05) / (1-abs(stoploss)))
# min amount and cost are set (cost is minimal)
markets["ETH/BTC"]["limits"] = {
@ -395,7 +395,7 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
PropertyMock(return_value=markets)
)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
assert isclose(result, max(2, 2 * 2) * 1.1)
assert isclose(result, max(2, 2 * 2) * (1+0.05) / (1-abs(stoploss)))
# min amount and cost are set (amount is minial)
markets["ETH/BTC"]["limits"] = {
@ -407,10 +407,10 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
PropertyMock(return_value=markets)
)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, stoploss)
assert isclose(result, max(8, 2 * 2) * 1.1)
assert isclose(result, max(8, 2 * 2) * (1+0.05) / (1-abs(stoploss)))
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -0.4)
assert isclose(result, max(8, 2 * 2) * 1.45)
assert isclose(result, max(8, 2 * 2) * 1.5)
# Really big stoploss
result = exchange.get_min_pair_stake_amount('ETH/BTC', 2, -1)
@ -432,7 +432,10 @@ def test_get_min_pair_stake_amount_real_data(mocker, default_conf) -> None:
PropertyMock(return_value=markets)
)
result = exchange.get_min_pair_stake_amount('ETH/BTC', 0.020405, stoploss)
assert round(result, 8) == round(max(0.0001, 0.001 * 0.020405) * 1.1, 8)
assert round(result, 8) == round(
max(0.0001, 0.001 * 0.020405) * (1+0.05) / (1-abs(stoploss)),
8
)
def test_set_sandbox(default_conf, mocker):
@ -1319,6 +1322,16 @@ def test_get_tickers(default_conf, mocker, exchange_name):
assert tickers['ETH/BTC']['ask'] == 1
assert tickers['BCH/BTC']['bid'] == 0.6
assert tickers['BCH/BTC']['ask'] == 0.5
assert api_mock.fetch_tickers.call_count == 1
api_mock.fetch_tickers.reset_mock()
# Cached ticker should not call api again
tickers2 = exchange.get_tickers(cached=True)
assert tickers2 == tickers
assert api_mock.fetch_tickers.call_count == 0
tickers2 = exchange.get_tickers(cached=False)
assert api_mock.fetch_tickers.call_count == 1
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
"get_tickers", "fetch_tickers")
@ -1641,6 +1654,9 @@ def test_get_next_limit_in_list():
# Going over the limit ...
assert Exchange.get_next_limit_in_list(1001, limit_range) == 1000
assert Exchange.get_next_limit_in_list(2000, limit_range) == 1000
# Without required range
assert Exchange.get_next_limit_in_list(2000, limit_range, False) is None
assert Exchange.get_next_limit_in_list(15, limit_range, False) == 20
assert Exchange.get_next_limit_in_list(21, None) == 21
assert Exchange.get_next_limit_in_list(100, None) == 100

View File

@ -15,10 +15,10 @@ from filelock import Timeout
from freqtrade.commands.optimize_commands import setup_optimize_configuration, start_hyperopt
from freqtrade.data.history import load_data
from freqtrade.exceptions import OperationalException
from freqtrade.optimize.decimalspace import SKDecimal
from freqtrade.optimize.hyperopt import Hyperopt
from freqtrade.optimize.hyperopt_auto import HyperOptAuto
from freqtrade.optimize.hyperopt_tools import HyperoptTools
from freqtrade.optimize.space import SKDecimal
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver
from freqtrade.state import RunMode
from tests.conftest import (get_args, log_has, log_has_re, patch_exchange,

View File

@ -27,7 +27,7 @@ def generate_mock_trade(pair: str, fee: float, is_open: bool,
open_rate=open_rate,
is_open=is_open,
amount=0.01 / open_rate,
exchange='bittrex',
exchange='binance',
)
trade.recalc_open_trade_value()
if not is_open:

View File

@ -106,7 +106,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'stoploss_entry_dist': -0.00010475,
'stoploss_entry_dist_ratio': -0.10448878,
'open_order': None,
'exchange': 'bittrex',
'exchange': 'binance',
}
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
@ -172,7 +172,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'stoploss_entry_dist': -0.00010475,
'stoploss_entry_dist_ratio': -0.10448878,
'open_order': None,
'exchange': 'bittrex',
'exchange': 'binance',
}
@ -569,6 +569,8 @@ def test_rpc_balance_handle(default_conf, mocker, tickers):
result = rpc._rpc_balance(default_conf['stake_currency'], default_conf['fiat_display_currency'])
assert prec_satoshi(result['total'], 12.309096315)
assert prec_satoshi(result['value'], 184636.44472997)
assert tickers.call_count == 1
assert tickers.call_args_list[0][1]['cached'] is True
assert 'USD' == result['symbol']
assert result['currencies'] == [
{'currency': 'BTC',

View File

@ -468,7 +468,7 @@ def test_api_show_config(botclient, mocker):
rc = client_get(client, f"{BASE_URI}/show_config")
assert_response(rc)
assert 'dry_run' in rc.json()
assert rc.json()['exchange'] == 'bittrex'
assert rc.json()['exchange'] == 'binance'
assert rc.json()['timeframe'] == '5m'
assert rc.json()['timeframe_ms'] == 300000
assert rc.json()['timeframe_min'] == 5
@ -506,8 +506,9 @@ def test_api_trades(botclient, mocker, fee, markets):
)
rc = client_get(client, f"{BASE_URI}/trades")
assert_response(rc)
assert len(rc.json()) == 2
assert len(rc.json()) == 3
assert rc.json()['trades_count'] == 0
assert rc.json()['total_trades'] == 0
create_mock_trades(fee)
Trade.query.session.flush()
@ -516,10 +517,32 @@ def test_api_trades(botclient, mocker, fee, markets):
assert_response(rc)
assert len(rc.json()['trades']) == 2
assert rc.json()['trades_count'] == 2
assert rc.json()['total_trades'] == 2
rc = client_get(client, f"{BASE_URI}/trades?limit=1")
assert_response(rc)
assert len(rc.json()['trades']) == 1
assert rc.json()['trades_count'] == 1
assert rc.json()['total_trades'] == 2
def test_api_trade_single(botclient, mocker, fee, ticker, markets):
ftbot, client = botclient
patch_get_signal(ftbot, (True, False))
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
markets=PropertyMock(return_value=markets),
fetch_ticker=ticker,
)
rc = client_get(client, f"{BASE_URI}/trade/3")
assert_response(rc, 404)
assert rc.json()['detail'] == 'Trade not found.'
create_mock_trades(fee)
Trade.query.session.flush()
rc = client_get(client, f"{BASE_URI}/trade/3")
assert_response(rc)
assert rc.json()['trade_id'] == 3
def test_api_delete_trade(botclient, mocker, fee, markets):
@ -753,7 +776,6 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
assert rc.json()[0] == {
'amount': 123.0,
'amount_requested': 123.0,
'base_currency': 'BTC',
'close_date': None,
'close_timestamp': None,
'close_profit': None,
@ -806,7 +828,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
'sell_order_status': None,
'strategy': 'DefaultStrategy',
'timeframe': 5,
'exchange': 'bittrex',
'exchange': 'binance',
}
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
@ -897,7 +919,7 @@ def test_api_forcebuy(botclient, mocker, fee):
pair='ETH/ETH',
amount=1,
amount_requested=1,
exchange='bittrex',
exchange='binance',
stake_amount=1,
open_rate=0.245441,
open_order_id="123456",
@ -960,7 +982,7 @@ def test_api_forcebuy(botclient, mocker, fee):
'sell_order_status': None,
'strategy': 'DefaultStrategy',
'timeframe': 5,
'exchange': 'bittrex',
'exchange': 'binance',
}

View File

@ -71,7 +71,7 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None:
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc_manager = RPCManager(freqtradebot)
rpc_manager.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'type': RPCMessageType.STATUS,
'status': 'test'
})
@ -86,7 +86,7 @@ def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc_manager = RPCManager(freqtradebot)
rpc_manager.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'type': RPCMessageType.STATUS,
'status': 'test'
})
@ -124,7 +124,7 @@ def test_send_msg_webhook_CustomMessagetype(mocker, default_conf, caplog) -> Non
rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
assert 'webhook' in [mod.name for mod in rpc_manager.registered_modules]
rpc_manager.send_msg({'type': RPCMessageType.STARTUP_NOTIFICATION,
rpc_manager.send_msg({'type': RPCMessageType.STARTUP,
'status': 'TestMessage'})
assert log_has(
"Message type 'startup' not implemented by handler webhook.",
@ -140,7 +140,7 @@ def test_startupmessages_telegram_enabled(mocker, default_conf, caplog) -> None:
rpc_manager.startup_messages(default_conf, freqtradebot.pairlists, freqtradebot.protections)
assert telegram_mock.call_count == 3
assert "*Exchange:* `bittrex`" in telegram_mock.call_args_list[1][0][0]['status']
assert "*Exchange:* `binance`" in telegram_mock.call_args_list[1][0][0]['status']
telegram_mock.reset_mock()
default_conf['dry_run'] = True

View File

@ -683,12 +683,12 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
context.args = ["1"]
telegram._forcesell(update=update, context=context)
assert msg_mock.call_count == 3
assert msg_mock.call_count == 4
last_msg = msg_mock.call_args_list[-1][0][0]
assert {
'type': RPCMessageType.SELL_NOTIFICATION,
'type': RPCMessageType.SELL,
'trade_id': 1,
'exchange': 'Bittrex',
'exchange': 'Binance',
'pair': 'ETH/BTC',
'gain': 'profit',
'limit': 1.173e-05,
@ -703,6 +703,7 @@ def test_telegram_forcesell_handle(default_conf, update, ticker, fee,
'sell_reason': SellType.FORCE_SELL.value,
'open_date': ANY,
'close_date': ANY,
'close_rate': ANY,
} == last_msg
@ -743,13 +744,13 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
context.args = ["1"]
telegram._forcesell(update=update, context=context)
assert msg_mock.call_count == 3
assert msg_mock.call_count == 4
last_msg = msg_mock.call_args_list[-1][0][0]
assert {
'type': RPCMessageType.SELL_NOTIFICATION,
'type': RPCMessageType.SELL,
'trade_id': 1,
'exchange': 'Bittrex',
'exchange': 'Binance',
'pair': 'ETH/BTC',
'gain': 'loss',
'limit': 1.043e-05,
@ -764,6 +765,7 @@ def test_telegram_forcesell_down_handle(default_conf, update, ticker, fee,
'sell_reason': SellType.FORCE_SELL.value,
'open_date': ANY,
'close_date': ANY,
'close_rate': ANY,
} == last_msg
@ -794,13 +796,13 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
context.args = ["all"]
telegram._forcesell(update=update, context=context)
# Called for each trade 3 times
assert msg_mock.call_count == 8
msg = msg_mock.call_args_list[1][0][0]
# Called for each trade 4 times
assert msg_mock.call_count == 12
msg = msg_mock.call_args_list[2][0][0]
assert {
'type': RPCMessageType.SELL_NOTIFICATION,
'type': RPCMessageType.SELL,
'trade_id': 1,
'exchange': 'Bittrex',
'exchange': 'Binance',
'pair': 'ETH/BTC',
'gain': 'loss',
'limit': 1.099e-05,
@ -815,6 +817,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
'sell_reason': SellType.FORCE_SELL.value,
'open_date': ANY,
'close_date': ANY,
'close_rate': ANY,
} == msg
@ -1178,7 +1181,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None:
telegram._show_config(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0]
assert '*Exchange:* `bittrex`' in msg_mock.call_args_list[0][0][0]
assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0]
assert '*Strategy:* `DefaultStrategy`' in msg_mock.call_args_list[0][0][0]
assert '*Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0]
@ -1187,7 +1190,7 @@ def test_show_config_handle(default_conf, update, mocker) -> None:
telegram._show_config(update=update, context=MagicMock())
assert msg_mock.call_count == 1
assert '*Mode:* `{}`'.format('Dry-run') in msg_mock.call_args_list[0][0][0]
assert '*Exchange:* `bittrex`' in msg_mock.call_args_list[0][0][0]
assert '*Exchange:* `binance`' in msg_mock.call_args_list[0][0][0]
assert '*Strategy:* `DefaultStrategy`' in msg_mock.call_args_list[0][0][0]
assert '*Initial Stoploss:* `-0.1`' in msg_mock.call_args_list[0][0][0]
@ -1195,9 +1198,9 @@ def test_show_config_handle(default_conf, update, mocker) -> None:
def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None:
msg = {
'type': RPCMessageType.BUY_NOTIFICATION,
'type': RPCMessageType.BUY,
'trade_id': 1,
'exchange': 'Bittrex',
'exchange': 'Binance',
'pair': 'ETH/BTC',
'limit': 1.099e-05,
'order_type': 'limit',
@ -1213,7 +1216,7 @@ def test_send_msg_buy_notification(default_conf, mocker, caplog) -> None:
telegram.send_msg(msg)
assert msg_mock.call_args[0][0] \
== '\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC (#1)\n' \
== '\N{LARGE BLUE CIRCLE} *Binance:* Buying ETH/BTC (#1)\n' \
'*Amount:* `1333.33333333`\n' \
'*Open Rate:* `0.00001099`\n' \
'*Current Rate:* `0.00001099`\n' \
@ -1240,17 +1243,36 @@ def test_send_msg_buy_cancel_notification(default_conf, mocker) -> None:
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
telegram.send_msg({
'type': RPCMessageType.BUY_CANCEL_NOTIFICATION,
'type': RPCMessageType.BUY_CANCEL,
'trade_id': 1,
'exchange': 'Bittrex',
'exchange': 'Binance',
'pair': 'ETH/BTC',
'reason': CANCEL_REASON['TIMEOUT']
})
assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Bittrex:* '
assert (msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Binance:* '
'Cancelling open buy Order for ETH/BTC (#1). '
'Reason: cancelled due to timeout.')
def test_send_msg_buy_fill_notification(default_conf, mocker) -> None:
default_conf['telegram']['notification_settings']['buy_fill'] = 'on'
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
telegram.send_msg({
'type': RPCMessageType.BUY_FILL,
'trade_id': 1,
'exchange': 'Binance',
'pair': 'ETH/USDT',
'open_rate': 200,
'stake_amount': 100,
'amount': 0.5,
'open_date': arrow.utcnow().datetime
})
assert (msg_mock.call_args[0][0] == '\N{LARGE CIRCLE} *Binance:* '
'Buy order for ETH/USDT (#1) filled for 200.')
def test_send_msg_sell_notification(default_conf, mocker) -> None:
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
@ -1258,7 +1280,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
old_convamount = telegram._rpc._fiat_converter.convert_amount
telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812
telegram.send_msg({
'type': RPCMessageType.SELL_NOTIFICATION,
'type': RPCMessageType.SELL,
'trade_id': 1,
'exchange': 'Binance',
'pair': 'KEY/ETH',
@ -1288,7 +1310,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
msg_mock.reset_mock()
telegram.send_msg({
'type': RPCMessageType.SELL_NOTIFICATION,
'type': RPCMessageType.SELL,
'trade_id': 1,
'exchange': 'Binance',
'pair': 'KEY/ETH',
@ -1325,36 +1347,65 @@ def test_send_msg_sell_cancel_notification(default_conf, mocker) -> None:
old_convamount = telegram._rpc._fiat_converter.convert_amount
telegram._rpc._fiat_converter.convert_amount = lambda a, b, c: -24.812
telegram.send_msg({
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
'type': RPCMessageType.SELL_CANCEL,
'trade_id': 1,
'exchange': 'Binance',
'pair': 'KEY/ETH',
'reason': 'Cancelled on exchange'
})
assert msg_mock.call_args[0][0] \
== ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH (#1).'
' Reason: Cancelled on exchange')
== ('\N{WARNING SIGN} *Binance:* Cancelling open sell Order for KEY/ETH (#1).'
' Reason: Cancelled on exchange.')
msg_mock.reset_mock()
telegram.send_msg({
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
'type': RPCMessageType.SELL_CANCEL,
'trade_id': 1,
'exchange': 'Binance',
'pair': 'KEY/ETH',
'reason': 'timeout'
})
assert msg_mock.call_args[0][0] \
== ('\N{WARNING SIGN} *Binance:* Cancelling Open Sell Order for KEY/ETH (#1).'
' Reason: timeout')
== ('\N{WARNING SIGN} *Binance:* Cancelling open sell Order for KEY/ETH (#1).'
' Reason: timeout.')
# Reset singleton function to avoid random breaks
telegram._rpc._fiat_converter.convert_amount = old_convamount
def test_send_msg_sell_fill_notification(default_conf, mocker) -> None:
default_conf['telegram']['notification_settings']['sell_fill'] = 'on'
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
telegram.send_msg({
'type': RPCMessageType.SELL_FILL,
'trade_id': 1,
'exchange': 'Binance',
'pair': 'ETH/USDT',
'gain': 'loss',
'limit': 3.201e-05,
'amount': 0.1,
'order_type': 'market',
'open_rate': 500,
'close_rate': 550,
'current_rate': 3.201e-05,
'profit_amount': -0.05746268,
'profit_ratio': -0.57405275,
'stake_currency': 'ETH',
'fiat_currency': 'USD',
'sell_reason': SellType.STOP_LOSS.value,
'open_date': arrow.utcnow().shift(hours=-1),
'close_date': arrow.utcnow(),
})
assert msg_mock.call_args[0][0] \
== ('\N{LARGE CIRCLE} *Binance:* Sell order for ETH/USDT (#1) filled for 550.')
def test_send_msg_status_notification(default_conf, mocker) -> None:
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
telegram.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'type': RPCMessageType.STATUS,
'status': 'running'
})
assert msg_mock.call_args[0][0] == '*Status:* `running`'
@ -1363,7 +1414,7 @@ def test_send_msg_status_notification(default_conf, mocker) -> None:
def test_warning_notification(default_conf, mocker) -> None:
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
telegram.send_msg({
'type': RPCMessageType.WARNING_NOTIFICATION,
'type': RPCMessageType.WARNING,
'status': 'message'
})
assert msg_mock.call_args[0][0] == '\N{WARNING SIGN} *Warning:* `message`'
@ -1372,7 +1423,7 @@ def test_warning_notification(default_conf, mocker) -> None:
def test_startup_notification(default_conf, mocker) -> None:
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
telegram.send_msg({
'type': RPCMessageType.STARTUP_NOTIFICATION,
'type': RPCMessageType.STARTUP,
'status': '*Custom:* `Hello World`'
})
assert msg_mock.call_args[0][0] == '*Custom:* `Hello World`'
@ -1391,9 +1442,9 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
telegram.send_msg({
'type': RPCMessageType.BUY_NOTIFICATION,
'type': RPCMessageType.BUY,
'trade_id': 1,
'exchange': 'Bittrex',
'exchange': 'Binance',
'pair': 'ETH/BTC',
'limit': 1.099e-05,
'order_type': 'limit',
@ -1405,7 +1456,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
'amount': 1333.3333333333335,
'open_date': arrow.utcnow().shift(hours=-1)
})
assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Bittrex:* Buying ETH/BTC (#1)\n'
assert msg_mock.call_args[0][0] == ('\N{LARGE BLUE CIRCLE} *Binance:* Buying ETH/BTC (#1)\n'
'*Amount:* `1333.33333333`\n'
'*Open Rate:* `0.00001099`\n'
'*Current Rate:* `0.00001099`\n'
@ -1417,7 +1468,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
telegram.send_msg({
'type': RPCMessageType.SELL_NOTIFICATION,
'type': RPCMessageType.SELL,
'trade_id': 1,
'exchange': 'Binance',
'pair': 'KEY/ETH',

View File

@ -25,6 +25,11 @@ def get_webhook_dict() -> dict:
"value2": "limit {limit:8f}",
"value3": "{stake_amount:8f} {stake_currency}"
},
"webhookbuyfill": {
"value1": "Buy Order for {pair} filled",
"value2": "at {open_rate:8f}",
"value3": "{stake_amount:8f} {stake_currency}"
},
"webhooksell": {
"value1": "Selling {pair}",
"value2": "limit {limit:8f}",
@ -35,6 +40,11 @@ def get_webhook_dict() -> dict:
"value2": "limit {limit:8f}",
"value3": "profit: {profit_amount:8f} {stake_currency} ({profit_ratio})"
},
"webhooksellfill": {
"value1": "Sell Order for {pair} filled",
"value2": "at {close_rate:8f}",
"value3": ""
},
"webhookstatus": {
"value1": "Status: {status}",
"value2": "",
@ -49,7 +59,7 @@ def test__init__(mocker, default_conf):
assert webhook._config == default_conf
def test_send_msg(default_conf, mocker):
def test_send_msg_webhook(default_conf, mocker):
default_conf["webhook"] = get_webhook_dict()
msg_mock = MagicMock()
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
@ -58,8 +68,8 @@ def test_send_msg(default_conf, mocker):
msg_mock = MagicMock()
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
msg = {
'type': RPCMessageType.BUY_NOTIFICATION,
'exchange': 'Bittrex',
'type': RPCMessageType.BUY,
'exchange': 'Binance',
'pair': 'ETH/BTC',
'limit': 0.005,
'stake_amount': 0.8,
@ -76,11 +86,11 @@ def test_send_msg(default_conf, mocker):
assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhookbuy"]["value3"].format(**msg))
# Test buy cancel
msg_mock = MagicMock()
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
msg_mock.reset_mock()
msg = {
'type': RPCMessageType.BUY_CANCEL_NOTIFICATION,
'exchange': 'Bittrex',
'type': RPCMessageType.BUY_CANCEL,
'exchange': 'Binance',
'pair': 'ETH/BTC',
'limit': 0.005,
'stake_amount': 0.8,
@ -96,12 +106,32 @@ def test_send_msg(default_conf, mocker):
default_conf["webhook"]["webhookbuycancel"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhookbuycancel"]["value3"].format(**msg))
# Test sell
msg_mock = MagicMock()
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
# Test buy fill
msg_mock.reset_mock()
msg = {
'type': RPCMessageType.SELL_NOTIFICATION,
'exchange': 'Bittrex',
'type': RPCMessageType.BUY_FILL,
'exchange': 'Binance',
'pair': 'ETH/BTC',
'open_rate': 0.005,
'stake_amount': 0.8,
'stake_amount_fiat': 500,
'stake_currency': 'BTC',
'fiat_currency': 'EUR'
}
webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhookbuyfill"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhookbuyfill"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhookbuyfill"]["value3"].format(**msg))
# Test sell
msg_mock.reset_mock()
msg = {
'type': RPCMessageType.SELL,
'exchange': 'Binance',
'pair': 'ETH/BTC',
'gain': "profit",
'limit': 0.005,
@ -123,11 +153,10 @@ def test_send_msg(default_conf, mocker):
assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhooksell"]["value3"].format(**msg))
# Test sell cancel
msg_mock = MagicMock()
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
msg_mock.reset_mock()
msg = {
'type': RPCMessageType.SELL_CANCEL_NOTIFICATION,
'exchange': 'Bittrex',
'type': RPCMessageType.SELL_CANCEL,
'exchange': 'Binance',
'pair': 'ETH/BTC',
'gain': "profit",
'limit': 0.005,
@ -148,9 +177,35 @@ def test_send_msg(default_conf, mocker):
default_conf["webhook"]["webhooksellcancel"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhooksellcancel"]["value3"].format(**msg))
for msgtype in [RPCMessageType.STATUS_NOTIFICATION,
RPCMessageType.WARNING_NOTIFICATION,
RPCMessageType.STARTUP_NOTIFICATION]:
# Test Sell fill
msg_mock.reset_mock()
msg = {
'type': RPCMessageType.SELL_FILL,
'exchange': 'Binance',
'pair': 'ETH/BTC',
'gain': "profit",
'close_rate': 0.005,
'amount': 0.8,
'order_type': 'limit',
'open_rate': 0.004,
'current_rate': 0.005,
'profit_amount': 0.001,
'profit_ratio': 0.20,
'stake_currency': 'BTC',
'sell_reason': SellType.STOP_LOSS.value
}
webhook.send_msg(msg=msg)
assert msg_mock.call_count == 1
assert (msg_mock.call_args[0][0]["value1"] ==
default_conf["webhook"]["webhooksellfill"]["value1"].format(**msg))
assert (msg_mock.call_args[0][0]["value2"] ==
default_conf["webhook"]["webhooksellfill"]["value2"].format(**msg))
assert (msg_mock.call_args[0][0]["value3"] ==
default_conf["webhook"]["webhooksellfill"]["value3"].format(**msg))
for msgtype in [RPCMessageType.STATUS,
RPCMessageType.WARNING,
RPCMessageType.STARTUP]:
# Test notification
msg = {
'type': msgtype,
@ -173,8 +228,8 @@ def test_exception_send_msg(default_conf, mocker, caplog):
del default_conf["webhook"]["webhookbuy"]
webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
webhook.send_msg({'type': RPCMessageType.BUY_NOTIFICATION})
assert log_has(f"Message type '{RPCMessageType.BUY_NOTIFICATION}' not configured for webhooks",
webhook.send_msg({'type': RPCMessageType.BUY})
assert log_has(f"Message type '{RPCMessageType.BUY}' not configured for webhooks",
caplog)
default_conf["webhook"] = get_webhook_dict()
@ -183,8 +238,8 @@ def test_exception_send_msg(default_conf, mocker, caplog):
mocker.patch("freqtrade.rpc.webhook.Webhook._send_msg", msg_mock)
webhook = Webhook(RPC(get_patched_freqtradebot(mocker, default_conf)), default_conf)
msg = {
'type': RPCMessageType.BUY_NOTIFICATION,
'exchange': 'Bittrex',
'type': RPCMessageType.BUY,
'exchange': 'Binance',
'pair': 'ETH/BTC',
'limit': 0.005,
'order_type': 'limit',

View File

@ -219,7 +219,7 @@ def test_min_roi_reached(default_conf, fee) -> None:
open_date=arrow.utcnow().shift(hours=-1).datetime,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
exchange='binance',
open_rate=1,
)
@ -258,7 +258,7 @@ def test_min_roi_reached2(default_conf, fee) -> None:
open_date=arrow.utcnow().shift(hours=-1).datetime,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
exchange='binance',
open_rate=1,
)
@ -293,7 +293,7 @@ def test_min_roi_reached3(default_conf, fee) -> None:
open_date=arrow.utcnow().shift(hours=-1).datetime,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
exchange='binance',
open_rate=1,
)
@ -346,7 +346,7 @@ def test_stop_loss_reached(default_conf, fee, profit, adjusted, expected, traili
open_date=arrow.utcnow().shift(hours=-1).datetime,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
exchange='binance',
open_rate=1,
)
trade.adjust_min_max_rates(trade.open_rate)

View File

@ -1002,6 +1002,7 @@ def test_pairlist_resolving():
config = configuration.get_config()
assert config['pairs'] == ['ETH/BTC', 'XRP/BTC']
assert config['exchange']['pair_whitelist'] == ['ETH/BTC', 'XRP/BTC']
assert config['exchange']['name'] == 'binance'

View File

@ -207,65 +207,6 @@ def test_check_available_stake_amount(default_conf, ticker, mocker, fee, limit_b
freqtrade.get_free_open_trades())
def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5)
freqtrade = FreqtradeBot(default_conf)
patch_get_signal(freqtrade)
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades())
@pytest.mark.parametrize("balance_ratio,result1", [
(1, 0.005),
(0.99, 0.00495),
(0.50, 0.0025),
])
def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1,
limit_buy_order_open, fee, mocker) -> None:
patch_RPCManager(mocker)
patch_exchange(mocker)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
buy=MagicMock(return_value=limit_buy_order_open),
get_fee=fee
)
conf = deepcopy(default_conf)
conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT
conf['dry_run_wallet'] = 0.01
conf['max_open_trades'] = 2
conf['tradable_balance_ratio'] = balance_ratio
freqtrade = FreqtradeBot(conf)
patch_get_signal(freqtrade)
# no open trades, order amount should be 'balance / max_open_trades'
result = freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades())
assert result == result1
# create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)'
freqtrade.execute_buy('ETH/BTC', result)
result = freqtrade.wallets.get_trade_stake_amount('LTC/BTC', freqtrade.get_free_open_trades())
assert result == result1
# create 2 trades, order amount should be None
freqtrade.execute_buy('LTC/BTC', result)
result = freqtrade.wallets.get_trade_stake_amount('XRP/BTC', freqtrade.get_free_open_trades())
assert result == 0
# set max_open_trades = None, so do not trade
conf['max_open_trades'] = 0
freqtrade = FreqtradeBot(conf)
result = freqtrade.wallets.get_trade_stake_amount('NEO/BTC', freqtrade.get_free_open_trades())
assert result == 0
def test_edge_called_in_process(mocker, edge_conf) -> None:
patch_RPCManager(mocker)
patch_edge(mocker)
@ -421,7 +362,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> Non
assert trade.stake_amount == 0.001
assert trade.is_open
assert trade.open_date is not None
assert trade.exchange == 'bittrex'
assert trade.exchange == 'binance'
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
@ -680,7 +621,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, limit_buy
assert trade.stake_amount == default_conf['stake_amount']
assert trade.is_open
assert trade.open_date is not None
assert trade.exchange == 'bittrex'
assert trade.exchange == 'binance'
assert trade.open_rate == 0.00001098
assert trade.amount == 91.07468123
@ -777,7 +718,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order,
is_open=True,
amount=20,
open_rate=0.01,
exchange='bittrex',
exchange='binance',
))
Trade.query.session.add(Trade(
pair='ETH/BTC',
@ -787,7 +728,7 @@ def test_process_trade_no_whitelist_pair(default_conf, ticker, limit_buy_order,
is_open=True,
amount=12,
open_rate=0.001,
exchange='bittrex',
exchange='binance',
))
assert pair not in freqtrade.active_pair_whitelist
@ -1028,7 +969,7 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None
return_value=limit_buy_order['amount'])
stoploss = MagicMock(return_value={'id': 13434334})
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
freqtrade = FreqtradeBot(default_conf)
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
@ -1060,6 +1001,9 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
get_fee=fee,
)
mocker.patch.multiple(
'freqtrade.exchange.Binance',
stoploss=stoploss
)
freqtrade = FreqtradeBot(default_conf)
@ -1084,7 +1028,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
trade.stoploss_order_id = 100
hanging_stoploss_order = MagicMock(return_value={'status': 'open'})
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', hanging_stoploss_order)
mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', hanging_stoploss_order)
assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert trade.stoploss_order_id == 100
@ -1097,7 +1041,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
trade.stoploss_order_id = 100
canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'})
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', canceled_stoploss_order)
mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', canceled_stoploss_order)
stoploss.reset_mock()
assert freqtrade.handle_stoploss_on_exchange(trade) is False
@ -1123,14 +1067,14 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
'average': 2,
'amount': limit_buy_order['amount'],
})
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hit)
mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hit)
assert freqtrade.handle_stoploss_on_exchange(trade) is True
assert log_has_re(r'STOP_LOSS_LIMIT is hit for Trade\(id=1, .*\)\.', caplog)
assert trade.stoploss_order_id is None
assert trade.is_open is False
mocker.patch(
'freqtrade.exchange.Exchange.stoploss',
'freqtrade.exchange.Binance.stoploss',
side_effect=ExchangeError()
)
trade.is_open = True
@ -1142,9 +1086,9 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
# It should try to add stoploss order
trade.stoploss_order_id = 100
stoploss.reset_mock()
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order',
mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order',
side_effect=InvalidOrderException())
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
freqtrade.handle_stoploss_on_exchange(trade)
assert stoploss.call_count == 1
@ -1154,7 +1098,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
trade.is_open = False
stoploss.reset_mock()
mocker.patch('freqtrade.exchange.Exchange.fetch_order')
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
assert freqtrade.handle_stoploss_on_exchange(trade) is False
assert stoploss.call_count == 0
@ -1174,6 +1118,9 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
get_fee=fee,
)
mocker.patch.multiple(
'freqtrade.exchange.Binance',
fetch_stoploss_order=MagicMock(return_value={'status': 'canceled', 'id': 100}),
stoploss=MagicMock(side_effect=ExchangeError()),
)
@ -1208,6 +1155,9 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee,
buy=MagicMock(return_value=limit_buy_order_open),
sell=sell_mock,
get_fee=fee,
)
mocker.patch.multiple(
'freqtrade.exchange.Binance',
fetch_order=MagicMock(return_value={'status': 'canceled'}),
stoploss=MagicMock(side_effect=InvalidOrderException()),
)
@ -1253,6 +1203,9 @@ def test_create_stoploss_order_insufficient_funds(mocker, default_conf, caplog,
sell=sell_mock,
get_fee=fee,
fetch_order=MagicMock(return_value={'status': 'canceled'}),
)
mocker.patch.multiple(
'freqtrade.exchange.Binance',
stoploss=MagicMock(side_effect=InsufficientFundsError()),
)
patch_get_signal(freqtrade)
@ -1290,6 +1243,9 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee,
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
get_fee=fee,
)
mocker.patch.multiple(
'freqtrade.exchange.Binance',
stoploss=stoploss,
stoploss_adjust=MagicMock(return_value=True),
)
@ -1330,7 +1286,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee,
}
})
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging)
mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hanging)
# stoploss initially at 5%
assert freqtrade.handle_trade(trade) is False
@ -1345,8 +1301,8 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee,
cancel_order_mock = MagicMock()
stoploss_order_mock = MagicMock(return_value={'id': 13434334})
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock)
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock)
mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order', cancel_order_mock)
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock)
# stoploss should not be updated as the interval is 60 seconds
assert freqtrade.handle_trade(trade) is False
@ -1393,6 +1349,9 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
get_fee=fee,
)
mocker.patch.multiple(
'freqtrade.exchange.Binance',
stoploss=stoploss,
stoploss_adjust=MagicMock(return_value=True),
)
@ -1428,9 +1387,9 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
'stopPrice': '0.1'
}
}
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order',
mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order',
side_effect=InvalidOrderException())
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging)
mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hanging)
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog)
@ -1439,8 +1398,8 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
# Fail creating stoploss order
caplog.clear()
cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_stoploss_order", MagicMock())
mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=ExchangeError())
cancel_mock = mocker.patch("freqtrade.exchange.Binance.cancel_stoploss_order", MagicMock())
mocker.patch("freqtrade.exchange.Binance.stoploss", side_effect=ExchangeError())
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
assert cancel_mock.call_count == 1
assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog)
@ -1462,6 +1421,9 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee,
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
get_fee=fee,
)
mocker.patch.multiple(
'freqtrade.exchange.Binance',
stoploss=stoploss,
stoploss_adjust=MagicMock(return_value=True),
)
@ -1502,7 +1464,7 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee,
}
})
mocker.patch('freqtrade.exchange.Exchange.fetch_stoploss_order', stoploss_order_hanging)
mocker.patch('freqtrade.exchange.Binance.fetch_stoploss_order', stoploss_order_hanging)
assert freqtrade.handle_trade(trade) is False
assert freqtrade.handle_stoploss_on_exchange(trade) is False
@ -1516,8 +1478,8 @@ def test_handle_stoploss_on_exchange_custom_stop(mocker, default_conf, fee,
cancel_order_mock = MagicMock()
stoploss_order_mock = MagicMock(return_value={'id': 13434334})
mocker.patch('freqtrade.exchange.Exchange.cancel_stoploss_order', cancel_order_mock)
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock)
mocker.patch('freqtrade.exchange.Binance.cancel_stoploss_order', cancel_order_mock)
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock)
# stoploss should not be updated as the interval is 60 seconds
assert freqtrade.handle_trade(trade) is False
@ -1748,6 +1710,7 @@ def test_update_trade_state(mocker, default_conf, limit_buy_order, caplog) -> No
open_rate=0.01,
open_date=arrow.utcnow().datetime,
amount=11,
exchange="binance",
)
assert not freqtrade.update_trade_state(trade, None)
assert log_has_re(r'Orderid for trade .* is empty.', caplog)
@ -2357,7 +2320,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old
# note this is for a partially-complete buy order
freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1
assert rpc_mock.call_count == 2
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
assert len(trades) == 1
assert trades[0].amount == 23.0
@ -2392,7 +2355,7 @@ def test_check_handle_timedout_partial_fee(default_conf, ticker, open_trade, cap
assert log_has_re(r"Applying fee on amount for Trade.*", caplog)
assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1
assert rpc_mock.call_count == 2
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
assert len(trades) == 1
# Verify that trade has been updated
@ -2432,7 +2395,7 @@ def test_check_handle_timedout_partial_except(default_conf, ticker, open_trade,
assert log_has_re(r"Could not update trade amount: .*", caplog)
assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1
assert rpc_mock.call_count == 2
trades = Trade.query.filter(Trade.open_order_id.is_(open_trade.open_order_id)).all()
assert len(trades) == 1
# Verify that trade has been updated
@ -2661,8 +2624,8 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N
last_msg = rpc_mock.call_args_list[-1][0][0]
assert {
'trade_id': 1,
'type': RPCMessageType.SELL_NOTIFICATION,
'exchange': 'Bittrex',
'type': RPCMessageType.SELL,
'exchange': 'Binance',
'pair': 'ETH/BTC',
'gain': 'profit',
'limit': 1.172e-05,
@ -2677,6 +2640,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N
'sell_reason': SellType.ROI.value,
'open_date': ANY,
'close_date': ANY,
'close_rate': ANY,
} == last_msg
@ -2710,9 +2674,9 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker)
assert rpc_mock.call_count == 2
last_msg = rpc_mock.call_args_list[-1][0][0]
assert {
'type': RPCMessageType.SELL_NOTIFICATION,
'type': RPCMessageType.SELL,
'trade_id': 1,
'exchange': 'Bittrex',
'exchange': 'Binance',
'pair': 'ETH/BTC',
'gain': 'loss',
'limit': 1.044e-05,
@ -2727,6 +2691,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker)
'sell_reason': SellType.STOP_LOSS.value,
'open_date': ANY,
'close_date': ANY,
'close_rate': ANY,
} == last_msg
@ -2767,9 +2732,9 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
last_msg = rpc_mock.call_args_list[-1][0][0]
assert {
'type': RPCMessageType.SELL_NOTIFICATION,
'type': RPCMessageType.SELL,
'trade_id': 1,
'exchange': 'Bittrex',
'exchange': 'Binance',
'pair': 'ETH/BTC',
'gain': 'loss',
'limit': 1.08801e-05,
@ -2784,7 +2749,7 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
'sell_reason': SellType.STOP_LOSS.value,
'open_date': ANY,
'close_date': ANY,
'close_rate': ANY,
} == last_msg
@ -2868,7 +2833,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
trade = Trade.query.first()
assert trade
assert cancel_order.call_count == 1
assert rpc_mock.call_count == 2
assert rpc_mock.call_count == 3
def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, fee,
@ -2936,7 +2901,10 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
assert trade.stoploss_order_id is None
assert trade.is_open is False
assert trade.sell_reason == SellType.STOPLOSS_ON_EXCHANGE.value
assert rpc_mock.call_count == 2
assert rpc_mock.call_count == 3
assert rpc_mock.call_args_list[0][0][0]['type'] == RPCMessageType.BUY
assert rpc_mock.call_args_list[1][0][0]['type'] == RPCMessageType.BUY_FILL
assert rpc_mock.call_args_list[2][0][0]['type'] == RPCMessageType.SELL
def test_execute_sell_market_order(default_conf, ticker, fee,
@ -2970,12 +2938,12 @@ def test_execute_sell_market_order(default_conf, ticker, fee,
assert not trade.is_open
assert trade.close_profit == 0.0620716
assert rpc_mock.call_count == 2
assert rpc_mock.call_count == 3
last_msg = rpc_mock.call_args_list[-1][0][0]
assert {
'type': RPCMessageType.SELL_NOTIFICATION,
'type': RPCMessageType.SELL,
'trade_id': 1,
'exchange': 'Bittrex',
'exchange': 'Binance',
'pair': 'ETH/BTC',
'gain': 'profit',
'limit': 1.172e-05,
@ -2990,6 +2958,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee,
'sell_reason': SellType.ROI.value,
'open_date': ANY,
'close_date': ANY,
'close_rate': ANY,
} == last_msg
@ -3958,7 +3927,7 @@ def test_order_book_depth_of_market(default_conf, ticker, limit_buy_order_open,
assert trade.stake_amount == 0.001
assert trade.is_open
assert trade.open_date is not None
assert trade.exchange == 'bittrex'
assert trade.exchange == 'binance'
assert len(Trade.query.all()) == 1
@ -4414,7 +4383,7 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog):
is_open=True,
amount=20,
open_rate=0.01,
exchange='bittrex',
exchange='binance',
)
Trade.query.session.add(trade)

View File

@ -64,7 +64,7 @@ def test_init_dryrun_db(default_conf, tmpdir):
@pytest.mark.usefixtures("init_persistence")
def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog):
def test_update_with_binance(limit_buy_order, limit_sell_order, fee, caplog):
"""
On this test we will buy and sell a crypto currency.
@ -102,7 +102,7 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order, fee, caplog):
open_date=arrow.utcnow().datetime,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
exchange='binance',
)
assert trade.open_order_id is None
assert trade.close_profit is None
@ -142,7 +142,7 @@ def test_update_market_order(market_buy_order, market_sell_order, fee, caplog):
fee_open=fee.return_value,
fee_close=fee.return_value,
open_date=arrow.utcnow().datetime,
exchange='bittrex',
exchange='binance',
)
trade.open_order_id = 'something'
@ -177,7 +177,7 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order, fee):
amount=5,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
exchange='binance',
)
trade.open_order_id = 'something'
@ -205,7 +205,7 @@ def test_trade_close(limit_buy_order, limit_sell_order, fee):
fee_open=fee.return_value,
fee_close=fee.return_value,
open_date=arrow.Arrow(2020, 2, 1, 15, 5, 1).datetime,
exchange='bittrex',
exchange='binance',
)
assert trade.close_profit is None
assert trade.close_date is None
@ -233,7 +233,7 @@ def test_calc_close_trade_price_exception(limit_buy_order, fee):
amount=5,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
exchange='binance',
)
trade.open_order_id = 'something'
@ -250,7 +250,7 @@ def test_update_open_order(limit_buy_order):
amount=5,
fee_open=0.1,
fee_close=0.1,
exchange='bittrex',
exchange='binance',
)
assert trade.open_order_id is None
@ -274,7 +274,7 @@ def test_update_invalid_order(limit_buy_order):
open_rate=0.001,
fee_open=0.1,
fee_close=0.1,
exchange='bittrex',
exchange='binance',
)
limit_buy_order['type'] = 'invalid'
with pytest.raises(ValueError, match=r'Unknown order type'):
@ -290,7 +290,7 @@ def test_calc_open_trade_value(limit_buy_order, fee):
open_rate=0.00001099,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
exchange='binance',
)
trade.open_order_id = 'open_trade'
trade.update(limit_buy_order) # Buy @ 0.00001099
@ -311,7 +311,7 @@ def test_calc_close_trade_price(limit_buy_order, limit_sell_order, fee):
open_rate=0.00001099,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
exchange='binance',
)
trade.open_order_id = 'close_trade'
trade.update(limit_buy_order) # Buy @ 0.00001099
@ -336,7 +336,7 @@ def test_calc_profit(limit_buy_order, limit_sell_order, fee):
open_rate=0.00001099,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
exchange='binance',
)
trade.open_order_id = 'something'
trade.update(limit_buy_order) # Buy @ 0.00001099
@ -370,7 +370,7 @@ def test_calc_profit_ratio(limit_buy_order, limit_sell_order, fee):
open_rate=0.00001099,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
exchange='binance',
)
trade.open_order_id = 'something'
trade.update(limit_buy_order) # Buy @ 0.00001099
@ -388,6 +388,9 @@ def test_calc_profit_ratio(limit_buy_order, limit_sell_order, fee):
# Test with a custom fee rate on the close trade
assert trade.calc_profit_ratio(fee=0.003) == 0.06147824
trade.open_trade_value = 0.0
assert trade.calc_profit_ratio(fee=0.003) == 0.0
@pytest.mark.usefixtures("init_persistence")
def test_clean_dry_run_db(default_conf, fee):
@ -400,7 +403,7 @@ def test_clean_dry_run_db(default_conf, fee):
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
exchange='bittrex',
exchange='binance',
open_order_id='dry_run_buy_12345'
)
Trade.query.session.add(trade)
@ -412,7 +415,7 @@ def test_clean_dry_run_db(default_conf, fee):
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
exchange='bittrex',
exchange='binance',
open_order_id='dry_run_sell_12345'
)
Trade.query.session.add(trade)
@ -425,7 +428,7 @@ def test_clean_dry_run_db(default_conf, fee):
fee_open=fee.return_value,
fee_close=fee.return_value,
open_rate=0.123,
exchange='bittrex',
exchange='binance',
open_order_id='prod_buy_12345'
)
Trade.query.session.add(trade)
@ -463,7 +466,7 @@ def test_migrate_old(mocker, default_conf, fee):
);"""
insert_table_old = """INSERT INTO trades (exchange, pair, is_open, open_order_id, fee,
open_rate, stake_amount, amount, open_date)
VALUES ('BITTREX', 'BTC_ETC', 1, '123123', {fee},
VALUES ('binance', 'BTC_ETC', 1, '123123', {fee},
0.00258580, {stake}, {amount},
'2017-11-28 12:44:24.000000')
""".format(fee=fee.return_value,
@ -472,7 +475,7 @@ def test_migrate_old(mocker, default_conf, fee):
)
insert_table_old2 = """INSERT INTO trades (exchange, pair, is_open, fee,
open_rate, close_rate, stake_amount, amount, open_date)
VALUES ('BITTREX', 'BTC_ETC', 0, {fee},
VALUES ('binance', 'BTC_ETC', 0, {fee},
0.00258580, 0.00268580, {stake}, {amount},
'2017-11-28 12:44:24.000000')
""".format(fee=fee.return_value,
@ -500,7 +503,7 @@ def test_migrate_old(mocker, default_conf, fee):
assert trade.amount_requested == amount
assert trade.stake_amount == default_conf.get("stake_amount")
assert trade.pair == "ETC/BTC"
assert trade.exchange == "bittrex"
assert trade.exchange == "binance"
assert trade.max_rate == 0.0
assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0
@ -694,7 +697,7 @@ def test_adjust_stop_loss(fee):
amount=5,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
exchange='binance',
open_rate=1,
max_rate=1,
)
@ -746,7 +749,7 @@ def test_adjust_min_max_rates(fee):
amount=5,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
exchange='binance',
open_rate=1,
)
@ -790,7 +793,7 @@ def test_to_json(default_conf, fee):
fee_close=fee.return_value,
open_date=arrow.utcnow().shift(hours=-2).datetime,
open_rate=0.123,
exchange='bittrex',
exchange='binance',
open_order_id='dry_run_buy_12345'
)
result = trade.to_json()
@ -841,7 +844,7 @@ def test_to_json(default_conf, fee):
'max_rate': None,
'strategy': None,
'timeframe': None,
'exchange': 'bittrex',
'exchange': 'binance',
}
# Simulate dry_run entries
@ -856,7 +859,7 @@ def test_to_json(default_conf, fee):
close_date=arrow.utcnow().shift(hours=-1).datetime,
open_rate=0.123,
close_rate=0.125,
exchange='bittrex',
exchange='binance',
)
result = trade.to_json()
assert isinstance(result, dict)
@ -906,7 +909,7 @@ def test_to_json(default_conf, fee):
'sell_order_status': None,
'strategy': None,
'timeframe': None,
'exchange': 'bittrex',
'exchange': 'binance',
}
@ -919,7 +922,7 @@ def test_stoploss_reinitialization(default_conf, fee):
open_date=arrow.utcnow().shift(hours=-2).datetime,
amount=10,
fee_close=fee.return_value,
exchange='bittrex',
exchange='binance',
open_rate=1,
max_rate=1,
)
@ -978,7 +981,7 @@ def test_update_fee(fee):
open_date=arrow.utcnow().shift(hours=-2).datetime,
amount=10,
fee_close=fee.return_value,
exchange='bittrex',
exchange='binance',
open_rate=1,
max_rate=1,
)
@ -1017,7 +1020,7 @@ def test_fee_updated(fee):
open_date=arrow.utcnow().shift(hours=-2).datetime,
amount=10,
fee_close=fee.return_value,
exchange='bittrex',
exchange='binance',
open_rate=1,
max_rate=1,
)

View File

@ -1,7 +1,12 @@
# pragma pylint: disable=missing-docstring
from copy import deepcopy
from unittest.mock import MagicMock
from tests.conftest import get_patched_freqtradebot
import pytest
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
from freqtrade.exceptions import DependencyException
from tests.conftest import get_patched_freqtradebot, patch_wallet
def test_sync_wallet_at_boot(mocker, default_conf):
@ -106,3 +111,55 @@ def test_sync_wallet_missing_data(mocker, default_conf):
assert freqtrade.wallets._wallets['GAS'].used is None
assert freqtrade.wallets._wallets['GAS'].total == 0.260739
assert freqtrade.wallets.get_free('GAS') == 0.260739
def test_get_trade_stake_amount_no_stake_amount(default_conf, mocker) -> None:
patch_wallet(mocker, free=default_conf['stake_amount'] * 0.5)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
freqtrade.wallets.get_trade_stake_amount('ETH/BTC', freqtrade.get_free_open_trades())
@pytest.mark.parametrize("balance_ratio,result1", [
(1, 50),
(0.99, 49.5),
(0.50, 25),
])
def test_get_trade_stake_amount_unlimited_amount(default_conf, ticker, balance_ratio, result1,
limit_buy_order_open, fee, mocker) -> None:
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
fetch_ticker=ticker,
buy=MagicMock(return_value=limit_buy_order_open),
get_fee=fee
)
conf = deepcopy(default_conf)
conf['stake_amount'] = UNLIMITED_STAKE_AMOUNT
conf['dry_run_wallet'] = 100
conf['max_open_trades'] = 2
conf['tradable_balance_ratio'] = balance_ratio
freqtrade = get_patched_freqtradebot(mocker, conf)
# no open trades, order amount should be 'balance / max_open_trades'
result = freqtrade.wallets.get_trade_stake_amount('ETH/USDT', freqtrade.get_free_open_trades())
assert result == result1
# create one trade, order amount should be 'balance / (max_open_trades - num_open_trades)'
freqtrade.execute_buy('ETH/USDT', result)
result = freqtrade.wallets.get_trade_stake_amount('LTC/USDDT', freqtrade.get_free_open_trades())
assert result == result1
# create 2 trades, order amount should be None
freqtrade.execute_buy('LTC/BTC', result)
result = freqtrade.wallets.get_trade_stake_amount('XRP/USDT', freqtrade.get_free_open_trades())
assert result == 0
# set max_open_trades = None, so do not trade
freqtrade.config['max_open_trades'] = 0
result = freqtrade.wallets.get_trade_stake_amount('NEO/USDT', freqtrade.get_free_open_trades())
assert result == 0