diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 102e6ed78..4169661c6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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 }}"
diff --git a/README.md b/README.md
index c3a665c47..916f9cf17 100644
--- a/README.md
+++ b/README.md
@@ -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)
diff --git a/config_full.json.example b/config_full.json.example
index 717797933..973afe2c8 100644
--- a/config_full.json.example
+++ b/config_full.json.example
@@ -163,7 +163,9 @@
"warning": "on",
"startup": "on",
"buy": "on",
+ "buy_fill": "on",
"sell": "on",
+ "sell_fill": "on",
"buy_cancel": "on",
"sell_cancel": "on"
}
diff --git a/docs/advanced-hyperopt.md b/docs/advanced-hyperopt.md
index cc71f39a7..c86978b80 100644
--- a/docs/advanced-hyperopt.md
+++ b/docs/advanced-hyperopt.md
@@ -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
diff --git a/docs/assets/ccxt-logo.svg b/docs/assets/ccxt-logo.svg
new file mode 100644
index 000000000..e52682546
--- /dev/null
+++ b/docs/assets/ccxt-logo.svg
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/docs/assets/freqtrade_poweredby.svg b/docs/assets/freqtrade_poweredby.svg
new file mode 100644
index 000000000..957ec6401
--- /dev/null
+++ b/docs/assets/freqtrade_poweredby.svg
@@ -0,0 +1,44 @@
+
+
diff --git a/docs/backtesting.md b/docs/backtesting.md
index e16225f94..ee9926f32 100644
--- a/docs/backtesting.md
+++ b/docs/backtesting.md
@@ -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).
diff --git a/docs/configuration.md b/docs/configuration.md
index eb3351b8f..0ade558f1 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -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.
diff --git a/docs/data-download.md b/docs/data-download.md
index 04f444a8b..01561c89b 100644
--- a/docs/data-download.md
+++ b/docs/data-download.md
@@ -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).
diff --git a/docs/docker_quickstart.md b/docs/docker_quickstart.md
index ca0515281..9096000c1 100644
--- a/docs/docker_quickstart.md
+++ b/docs/docker_quickstart.md
@@ -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
diff --git a/docs/edge.md b/docs/edge.md
index 7f0a9cb2d..237ff36f6 100644
--- a/docs/edge.md
+++ b/docs/edge.md
@@ -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"
diff --git a/docs/exchanges.md b/docs/exchanges.md
index 1c5956088..8797ade8c 100644
--- a/docs/exchanges.md
+++ b/docs/exchanges.md
@@ -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/"` 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/"` 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.
diff --git a/docs/hyperopt.md b/docs/hyperopt.md
index 07cc963cf..51905e616 100644
--- a/docs/hyperopt.md
+++ b/docs/hyperopt.md
@@ -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,23 +466,26 @@ 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.
If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default.
-Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps).
+Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps).
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.
diff --git a/docs/includes/pairlists.md b/docs/includes/pairlists.md
index 8688494cc..85d157e75 100644
--- a/docs/includes/pairlists.md
+++ b/docs/includes/pairlists.md
@@ -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.
diff --git a/docs/index.md b/docs/index.md
index 61f2276c3..c2b6d5629 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -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 ](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
### Community tested
diff --git a/docs/plotting.md b/docs/plotting.md
index 63afa16b6..5d454c414 100644
--- a/docs/plotting.md
+++ b/docs/plotting.md
@@ -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.
diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt
index cfd63d1d0..4d7082a7f 100644
--- a/docs/requirements-docs.txt
+++ b/docs/requirements-docs.txt
@@ -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
diff --git a/docs/rest-api.md b/docs/rest-api.md
index c41c3f24c..a0029a44c 100644
--- a/docs/rest-api.md
+++ b/docs/rest-api.md
@@ -124,7 +124,8 @@ python3 scripts/rest_client.py --config rest_config.json [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/` | Get specific trade.
| `delete_trade ` | 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 logs. No limit to get all the trades.
+ :param limit: Limits log messages to the last logs. No limit to get the entire log.
pair_candles
Return live dataframe for .
@@ -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
diff --git a/docs/strategy-advanced.md b/docs/strategy-advanced.md
index 7fa824a5b..96c927965 100644
--- a/docs/strategy-advanced.md
+++ b/docs/strategy-advanced.md
@@ -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
```
diff --git a/docs/telegram-usage.md b/docs/telegram-usage.md
index 377977892..824cb17c7 100644
--- a/docs/telegram-usage.md
+++ b/docs/telegram-usage.md
@@ -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)
diff --git a/docs/webhook-config.md b/docs/webhook-config.md
index 2e41ad2cc..8ce6edc18 100644
--- a/docs/webhook-config.md
+++ b/docs/webhook-config.md
@@ -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`
diff --git a/freqtrade/commands/arguments.py b/freqtrade/commands/arguments.py
index 9468a7f7d..ffd317799 100644
--- a/freqtrade/commands/arguments.py
+++ b/freqtrade/commands/arguments.py
@@ -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",
diff --git a/freqtrade/commands/cli_options.py b/freqtrade/commands/cli_options.py
index 4fac8ac72..b583b47ba 100644
--- a/freqtrade/commands/cli_options.py
+++ b/freqtrade/commands/cli_options.py
@@ -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 '
diff --git a/freqtrade/commands/data_commands.py b/freqtrade/commands/data_commands.py
index 1ce02eee5..58191ddb4 100644
--- a/freqtrade/commands/data_commands.py
+++ b/freqtrade/commands/data_commands.py
@@ -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 ...")
diff --git a/freqtrade/configuration/configuration.py b/freqtrade/configuration/configuration.py
index 9acd532cc..86f337c1b 100644
--- a/freqtrade/configuration/configuration.py
+++ b/freqtrade/configuration/configuration.py
@@ -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"]:
diff --git a/freqtrade/constants.py b/freqtrade/constants.py
index 7b955c37d..bfd1e72f1 100644
--- a/freqtrade/constants.py
+++ b/freqtrade/constants.py
@@ -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'
+ },
}
}
},
diff --git a/freqtrade/data/converter.py b/freqtrade/data/converter.py
index c9d4ef19f..af6c6a2ef 100644
--- a/freqtrade/data/converter.py
+++ b/freqtrade/data/converter.py
@@ -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:
diff --git a/freqtrade/data/history/history_utils.py b/freqtrade/data/history/history_utils.py
index 3b8b5a2f0..58965abe0 100644
--- a/freqtrade/data/history/history_utils.py
+++ b/freqtrade/data/history/history_utils.py
@@ -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
diff --git a/freqtrade/edge/edge_positioning.py b/freqtrade/edge/edge_positioning.py
index d1f76c21f..334aabfab 100644
--- a/freqtrade/edge/edge_positioning.py
+++ b/freqtrade/edge/edge_positioning.py
@@ -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')
diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py
index 8a5563623..889bb49c2 100644
--- a/freqtrade/exchange/__init__.py
+++ b/freqtrade/exchange/__init__.py
@@ -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
diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py
index fd7d47668..69e2f2b8d 100644
--- a/freqtrade/exchange/bittrex.py
+++ b/freqtrade/exchange/bittrex.py
@@ -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 = {
diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py
index 3224255d0..ed7918b36 100644
--- a/freqtrade/exchange/exchange.py
+++ b/freqtrade/exchange/exchange.py
@@ -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)
diff --git a/freqtrade/exchange/kucoin.py b/freqtrade/exchange/kucoin.py
new file mode 100644
index 000000000..22886a1d8
--- /dev/null
+++ b/freqtrade/exchange/kucoin.py
@@ -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,
+ }
diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py
index 1ebf28ebd..c3a4bc0e0 100644
--- a/freqtrade/freqtradebot.py
+++ b/freqtrade/freqtradebot.py
@@ -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,
diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py
index ff1dd934c..4731e6a38 100644
--- a/freqtrade/optimize/backtesting.py
+++ b/freqtrade/optimize/backtesting.py
@@ -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)
diff --git a/freqtrade/optimize/hyperopt.py b/freqtrade/optimize/hyperopt.py
index d6003cf86..d1dabff36 100644
--- a/freqtrade/optimize/hyperopt.py
+++ b/freqtrade/optimize/hyperopt.py
@@ -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
diff --git a/freqtrade/optimize/hyperopt_interface.py b/freqtrade/optimize/hyperopt_interface.py
index 633c8bdd5..889854cad 100644
--- a/freqtrade/optimize/hyperopt_interface.py
+++ b/freqtrade/optimize/hyperopt_interface.py
@@ -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'),
]
diff --git a/freqtrade/optimize/space/__init__.py b/freqtrade/optimize/space/__init__.py
new file mode 100644
index 000000000..bbdac4ab9
--- /dev/null
+++ b/freqtrade/optimize/space/__init__.py
@@ -0,0 +1,4 @@
+# flake8: noqa: F401
+from skopt.space import Categorical, Dimension, Integer, Real
+
+from .decimalspace import SKDecimal
diff --git a/freqtrade/optimize/decimalspace.py b/freqtrade/optimize/space/decimalspace.py
similarity index 83%
rename from freqtrade/optimize/decimalspace.py
rename to freqtrade/optimize/space/decimalspace.py
index f5370b6d6..643999cc1 100644
--- a/freqtrade/optimize/decimalspace.py
+++ b/freqtrade/optimize/space/decimalspace.py
@@ -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)
diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py
index 8b4aa325a..e7fd488c7 100644
--- a/freqtrade/persistence/models.py
+++ b/freqtrade/persistence/models.py
@@ -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}")
diff --git a/freqtrade/plugins/pairlist/IPairList.py b/freqtrade/plugins/pairlist/IPairList.py
index 184feff9e..c4a9c3e40 100644
--- a/freqtrade/plugins/pairlist/IPairList.py
+++ b/freqtrade/plugins/pairlist/IPairList.py
@@ -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 "
diff --git a/freqtrade/plugins/pairlist/StaticPairList.py b/freqtrade/plugins/pairlist/StaticPairList.py
index c5ced48c9..13d30fc47 100644
--- a/freqtrade/plugins/pairlist/StaticPairList.py
+++ b/freqtrade/plugins/pairlist/StaticPairList.py
@@ -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:
diff --git a/freqtrade/plugins/pairlist/VolumePairList.py b/freqtrade/plugins/pairlist/VolumePairList.py
index dd8fc64fd..e85fb1805 100644
--- a/freqtrade/plugins/pairlist/VolumePairList.py
+++ b/freqtrade/plugins/pairlist/VolumePairList.py
@@ -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
diff --git a/freqtrade/plugins/protections/max_drawdown_protection.py b/freqtrade/plugins/protections/max_drawdown_protection.py
index d1c6b192d..67e204039 100644
--- a/freqtrade/plugins/protections/max_drawdown_protection.py
+++ b/freqtrade/plugins/protections/max_drawdown_protection.py
@@ -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)
diff --git a/freqtrade/resolvers/iresolver.py b/freqtrade/resolvers/iresolver.py
index 37cfd70e6..b51795e9e 100644
--- a/freqtrade/resolvers/iresolver.py
+++ b/freqtrade/resolvers/iresolver.py
@@ -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:
diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py
index 41de0134c..e582f6aa8 100644
--- a/freqtrade/rpc/api_server/api_schemas.py
+++ b/freqtrade/rpc/api_server/api_schemas.py
@@ -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):
diff --git a/freqtrade/rpc/api_server/api_v1.py b/freqtrade/rpc/api_server/api_v1.py
index 663cc9ff4..e907b92f0 100644
--- a/freqtrade/rpc/api_server/api_v1.py
+++ b/freqtrade/rpc/api_server/api_v1.py
@@ -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'])
diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
index 59758a573..a7a4dcf5c 100644
--- a/freqtrade/rpc/rpc.py
+++ b/freqtrade/rpc/rpc.py
@@ -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.')
diff --git a/freqtrade/rpc/rpc_manager.py b/freqtrade/rpc/rpc_manager.py
index 7977d68de..f819b55b4 100644
--- a/freqtrade/rpc/rpc_manager.py
+++ b/freqtrade/rpc/rpc_manager.py
@@ -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}'
})
diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index a8c629149..3eeedcd12 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -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']})\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
diff --git a/freqtrade/rpc/webhook.py b/freqtrade/rpc/webhook.py
index 5a30a9be8..24e1348f1 100644
--- a/freqtrade/rpc/webhook.py
+++ b/freqtrade/rpc/webhook.py
@@ -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']))
diff --git a/freqtrade/strategy/hyper.py b/freqtrade/strategy/hyper.py
index 3fedda974..16b576a73 100644
--- a/freqtrade/strategy/hyper.py
+++ b/freqtrade/strategy/hyper.py
@@ -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
diff --git a/freqtrade/templates/sample_hyperopt_advanced.py b/freqtrade/templates/sample_hyperopt_advanced.py
index 7736570f7..cc13b6ba3 100644
--- a/freqtrade/templates/sample_hyperopt_advanced.py
+++ b/freqtrade/templates/sample_hyperopt_advanced.py
@@ -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'),
]
diff --git a/requirements-dev.txt b/requirements-dev.txt
index cd93f2433..5e96e2cc2 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -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
diff --git a/requirements.txt b/requirements.txt
index 129bb05b2..a89eb2383 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -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
diff --git a/scripts/rest_client.py b/scripts/rest_client.py
index 4d667879d..900b784f2 100755
--- a/scripts/rest_client.py
+++ b/scripts/rest_client.py
@@ -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 logs. No limit to get all the trades.
+ :param limit: Limits log messages to the last 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.
diff --git a/setup.sh b/setup.sh
index d0ca1f643..631c31df2 100755
--- a/setup.sh
+++ b/setup.sh
@@ -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
}
diff --git a/tests/commands/test_commands.py b/tests/commands/test_commands.py
index 232fc4e2c..d86bced5d 100644
--- a/tests/commands/test_commands.py
+++ b/tests/commands/test_commands.py
@@ -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 = [
diff --git a/tests/config_test_comments.json b/tests/config_test_comments.json
index 4f201f86c..48a087dec 100644
--- a/tests/config_test_comments.json
+++ b/tests/config_test_comments.json
@@ -59,7 +59,7 @@
}
},
"exchange": {
- "name": "bittrex",
+ "name": "binance",
"sandbox": false,
"key": "your_exchange_key",
"secret": "your_exchange_secret",
diff --git a/tests/conftest.py b/tests/conftest.py
index 4a2106a4d..788586134 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -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,
diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py
index 34fc58aee..b92b51144 100644
--- a/tests/conftest_trades.py
+++ b/tests/conftest_trades.py
@@ -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,
diff --git a/tests/edge/test_edge.py b/tests/edge/test_edge.py
index 5142dd985..25e0da5e2 100644
--- a/tests/edge/test_edge.py
+++ b/tests/edge/test_edge.py
@@ -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))
diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py
index 870e6cabd..dce10da84 100644
--- a/tests/exchange/test_ccxt_compat.py
+++ b/tests/exchange/test_ccxt_compat.py
@@ -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
diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py
index 4ceba6eba..27f4d0db9 100644
--- a/tests/exchange/test_exchange.py
+++ b/tests/exchange/test_exchange.py
@@ -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
diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py
index 129fe53d9..59bc4aefb 100644
--- a/tests/optimize/test_hyperopt.py
+++ b/tests/optimize/test_hyperopt.py
@@ -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,
diff --git a/tests/plugins/test_protections.py b/tests/plugins/test_protections.py
index 545387eaa..a39301145 100644
--- a/tests/plugins/test_protections.py
+++ b/tests/plugins/test_protections.py
@@ -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:
diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py
index a97f6b65e..6d31e7635 100644
--- a/tests/rpc/test_rpc.py
+++ b/tests/rpc/test_rpc.py
@@ -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',
diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py
index 2b6d96c61..69d312e65 100644
--- a/tests/rpc/test_rpc_apiserver.py
+++ b/tests/rpc/test_rpc_apiserver.py
@@ -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',
}
diff --git a/tests/rpc/test_rpc_manager.py b/tests/rpc/test_rpc_manager.py
index 3068e9764..69a757fcf 100644
--- a/tests/rpc/test_rpc_manager.py
+++ b/tests/rpc/test_rpc_manager.py
@@ -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
diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py
index 34bf057cb..6a36c12a7 100644
--- a/tests/rpc/test_rpc_telegram.py
+++ b/tests/rpc/test_rpc_telegram.py
@@ -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',
diff --git a/tests/rpc/test_rpc_webhook.py b/tests/rpc/test_rpc_webhook.py
index 5361cd947..0560f8d53 100644
--- a/tests/rpc/test_rpc_webhook.py
+++ b/tests/rpc/test_rpc_webhook.py
@@ -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',
diff --git a/tests/strategy/test_interface.py b/tests/strategy/test_interface.py
index 0ee80e0c5..78fa368e4 100644
--- a/tests/strategy/test_interface.py
+++ b/tests/strategy/test_interface.py
@@ -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)
diff --git a/tests/test_configuration.py b/tests/test_configuration.py
index a512bf58a..b2c883108 100644
--- a/tests/test_configuration.py
+++ b/tests/test_configuration.py
@@ -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'
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index c91015766..44791f928 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -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)
diff --git a/tests/test_persistence.py b/tests/test_persistence.py
index 0a3d6858d..dad0e275e 100644
--- a/tests/test_persistence.py
+++ b/tests/test_persistence.py
@@ -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,
)
diff --git a/tests/test_wallets.py b/tests/test_wallets.py
index b7aead0c4..562957790 100644
--- a/tests/test_wallets.py
+++ b/tests/test_wallets.py
@@ -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