Merge branch 'feat/short' into funding-fee-backtesting
This commit is contained in:
commit
8060a3c822
@ -11,8 +11,13 @@ if [ ! -f "${INSTALL_LOC}/lib/libta_lib.a" ]; then
|
|||||||
&& curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess \
|
&& curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.guess;hb=HEAD' -o config.guess \
|
||||||
&& curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub \
|
&& curl 'http://git.savannah.gnu.org/gitweb/?p=config.git;a=blob_plain;f=config.sub;hb=HEAD' -o config.sub \
|
||||||
&& ./configure --prefix=${INSTALL_LOC}/ \
|
&& ./configure --prefix=${INSTALL_LOC}/ \
|
||||||
&& make -j$(nproc) \
|
&& make
|
||||||
&& which sudo && sudo make install || make install
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Failed building ta-lib."
|
||||||
|
cd .. && rm -rf ./ta-lib/
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
which sudo && sudo make install || make install
|
||||||
if [ -x "$(command -v apt-get)" ]; then
|
if [ -x "$(command -v apt-get)" ]; then
|
||||||
echo "Updating library path using ldconfig"
|
echo "Updating library path using ldconfig"
|
||||||
sudo ldconfig
|
sudo ldconfig
|
||||||
|
@ -52,6 +52,71 @@ freqtrade trade -c MyConfigUSDT.json -s MyCustomStrategy --db-url sqlite:///user
|
|||||||
|
|
||||||
For more information regarding usage of the sqlite databases, for example to manually enter or remove trades, please refer to the [SQL Cheatsheet](sql_cheatsheet.md).
|
For more information regarding usage of the sqlite databases, for example to manually enter or remove trades, please refer to the [SQL Cheatsheet](sql_cheatsheet.md).
|
||||||
|
|
||||||
|
### Multiple instances using docker
|
||||||
|
|
||||||
|
To run multiple instances of freqtrade using docker you will need to edit the docker-compose.yml file and add all the instances you want as separate services. Remember, you can separate your configuration into multiple files, so it's a good idea to think about making them modular, then if you need to edit something common to all bots, you can do that in a single config file.
|
||||||
|
``` yml
|
||||||
|
---
|
||||||
|
version: '3'
|
||||||
|
services:
|
||||||
|
freqtrade1:
|
||||||
|
image: freqtradeorg/freqtrade:stable
|
||||||
|
# image: freqtradeorg/freqtrade:develop
|
||||||
|
# Use plotting image
|
||||||
|
# image: freqtradeorg/freqtrade:develop_plot
|
||||||
|
# Build step - only needed when additional dependencies are needed
|
||||||
|
# build:
|
||||||
|
# context: .
|
||||||
|
# dockerfile: "./docker/Dockerfile.custom"
|
||||||
|
restart: always
|
||||||
|
container_name: freqtrade1
|
||||||
|
volumes:
|
||||||
|
- "./user_data:/freqtrade/user_data"
|
||||||
|
# Expose api on port 8080 (localhost only)
|
||||||
|
# Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation
|
||||||
|
# before enabling this.
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8080:8080"
|
||||||
|
# Default command used when running `docker compose up`
|
||||||
|
command: >
|
||||||
|
trade
|
||||||
|
--logfile /freqtrade/user_data/logs/freqtrade1.log
|
||||||
|
--db-url sqlite:////freqtrade/user_data/tradesv3_freqtrade1.sqlite
|
||||||
|
--config /freqtrade/user_data/config.json
|
||||||
|
--config /freqtrade/user_data/config.freqtrade1.json
|
||||||
|
--strategy SampleStrategy
|
||||||
|
|
||||||
|
freqtrade2:
|
||||||
|
image: freqtradeorg/freqtrade:stable
|
||||||
|
# image: freqtradeorg/freqtrade:develop
|
||||||
|
# Use plotting image
|
||||||
|
# image: freqtradeorg/freqtrade:develop_plot
|
||||||
|
# Build step - only needed when additional dependencies are needed
|
||||||
|
# build:
|
||||||
|
# context: .
|
||||||
|
# dockerfile: "./docker/Dockerfile.custom"
|
||||||
|
restart: always
|
||||||
|
container_name: freqtrade2
|
||||||
|
volumes:
|
||||||
|
- "./user_data:/freqtrade/user_data"
|
||||||
|
# Expose api on port 8080 (localhost only)
|
||||||
|
# Please read the https://www.freqtrade.io/en/latest/rest-api/ documentation
|
||||||
|
# before enabling this.
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8081:8080"
|
||||||
|
# Default command used when running `docker compose up`
|
||||||
|
command: >
|
||||||
|
trade
|
||||||
|
--logfile /freqtrade/user_data/logs/freqtrade2.log
|
||||||
|
--db-url sqlite:////freqtrade/user_data/tradesv3_freqtrade2.sqlite
|
||||||
|
--config /freqtrade/user_data/config.json
|
||||||
|
--config /freqtrade/user_data/config.freqtrade2.json
|
||||||
|
--strategy SampleStrategy
|
||||||
|
|
||||||
|
```
|
||||||
|
You can use whatever naming convention you want, freqtrade1 and 2 are arbitrary. Note, that you will need to use different database files, port mappings and telegram configurations for each instance, as mentioned above.
|
||||||
|
|
||||||
|
|
||||||
## Configure the bot running as a systemd service
|
## Configure the bot running as a systemd service
|
||||||
|
|
||||||
Copy the `freqtrade.service` file to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup.
|
Copy the `freqtrade.service` file to your systemd user directory (usually `~/.config/systemd/user`) and update `WorkingDirectory` and `ExecStart` to match your setup.
|
||||||
|
@ -22,6 +22,7 @@ usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
|||||||
[-d PATH] [--userdir PATH]
|
[-d PATH] [--userdir PATH]
|
||||||
[-p PAIRS [PAIRS ...]] [--pairs-file FILE]
|
[-p PAIRS [PAIRS ...]] [--pairs-file FILE]
|
||||||
[--days INT] [--new-pairs-days INT]
|
[--days INT] [--new-pairs-days INT]
|
||||||
|
[--include-inactive-pairs]
|
||||||
[--timerange TIMERANGE] [--dl-trades]
|
[--timerange TIMERANGE] [--dl-trades]
|
||||||
[--exchange EXCHANGE]
|
[--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} ...]]
|
[-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} ...]]
|
||||||
@ -38,6 +39,8 @@ optional arguments:
|
|||||||
--days INT Download data for given number of days.
|
--days INT Download data for given number of days.
|
||||||
--new-pairs-days INT Download data of new pairs for given number of days.
|
--new-pairs-days INT Download data of new pairs for given number of days.
|
||||||
Default: `None`.
|
Default: `None`.
|
||||||
|
--include-inactive-pairs
|
||||||
|
Also download data from inactive pairs.
|
||||||
--timerange TIMERANGE
|
--timerange TIMERANGE
|
||||||
Specify what timerange of data to use.
|
Specify what timerange of data to use.
|
||||||
--dl-trades Download trades instead of OHLCV data. The bot will
|
--dl-trades Download trades instead of OHLCV data. The bot will
|
||||||
@ -52,10 +55,10 @@ optional arguments:
|
|||||||
exchange/pairs/timeframes.
|
exchange/pairs/timeframes.
|
||||||
--data-format-ohlcv {json,jsongz,hdf5}
|
--data-format-ohlcv {json,jsongz,hdf5}
|
||||||
Storage format for downloaded candle (OHLCV) data.
|
Storage format for downloaded candle (OHLCV) data.
|
||||||
(default: `None`).
|
(default: `json`).
|
||||||
--data-format-trades {json,jsongz,hdf5}
|
--data-format-trades {json,jsongz,hdf5}
|
||||||
Storage format for downloaded trades data. (default:
|
Storage format for downloaded trades data. (default:
|
||||||
`None`).
|
`jsongz`).
|
||||||
|
|
||||||
Common arguments:
|
Common arguments:
|
||||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
@ -80,6 +83,82 @@ Common arguments:
|
|||||||
|
|
||||||
For that reason, `download-data` does not care about the "startup-period" defined in a strategy. It's up to the user to download additional days if the backtest should start at a specific point in time (while respecting startup period).
|
For that reason, `download-data` does not care about the "startup-period" defined in a strategy. It's up to the user to download additional days if the backtest should start at a specific point in time (while respecting startup period).
|
||||||
|
|
||||||
|
### Pairs file
|
||||||
|
|
||||||
|
In alternative to the whitelist from `config.json`, a `pairs.json` file can be used.
|
||||||
|
If you are using Binance for example:
|
||||||
|
|
||||||
|
- create a directory `user_data/data/binance` and copy or create the `pairs.json` file in that directory.
|
||||||
|
- update the `pairs.json` file to contain the currency pairs you are interested in.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p user_data/data/binance
|
||||||
|
touch user_data/data/binance/pairs.json
|
||||||
|
```
|
||||||
|
|
||||||
|
The format of the `pairs.json` file is a simple json list.
|
||||||
|
Mixing different stake-currencies is allowed for this file, since it's only used for downloading.
|
||||||
|
|
||||||
|
``` json
|
||||||
|
[
|
||||||
|
"ETH/BTC",
|
||||||
|
"ETH/USDT",
|
||||||
|
"BTC/USDT",
|
||||||
|
"XRP/ETH"
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! Tip "Downloading all data for one quote currency"
|
||||||
|
Often, you'll want to download data for all pairs of a specific quote-currency. In such cases, you can use the following shorthand:
|
||||||
|
`freqtrade download-data --exchange binance --pairs .*/USDT <...>`. The provided "pairs" string will be expanded to contain all active pairs on the exchange.
|
||||||
|
To also download data for inactive (delisted) pairs, add `--include-inactive-pairs` to the command.
|
||||||
|
|
||||||
|
??? Note "Permission denied errors"
|
||||||
|
If your configuration directory `user_data` was made by docker, you may get the following error:
|
||||||
|
|
||||||
|
```
|
||||||
|
cp: cannot create regular file 'user_data/data/binance/pairs.json': Permission denied
|
||||||
|
```
|
||||||
|
|
||||||
|
You can fix the permissions of your user-data directory as follows:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo chown -R $UID:$GID user_data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start download
|
||||||
|
|
||||||
|
Then run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
freqtrade download-data --exchange binance
|
||||||
|
```
|
||||||
|
|
||||||
|
This will download historical candle (OHLCV) data for all the currency pairs you defined in `pairs.json`.
|
||||||
|
|
||||||
|
Alternatively, specify the pairs directly
|
||||||
|
|
||||||
|
```bash
|
||||||
|
freqtrade download-data --exchange binance --pairs ETH/USDT XRP/USDT BTC/USDT
|
||||||
|
```
|
||||||
|
|
||||||
|
or as regex (to download all active USDT pairs)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
freqtrade download-data --exchange binance --pairs .*/USDT
|
||||||
|
```
|
||||||
|
|
||||||
|
### Other Notes
|
||||||
|
|
||||||
|
- To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`.
|
||||||
|
- To change the exchange used to download the historical data from, please use a different configuration file (you'll probably need to adjust rate limits etc.)
|
||||||
|
- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`.
|
||||||
|
- To download historical candle (OHLCV) data for only 10 days, use `--days 10` (defaults to 30 days).
|
||||||
|
- To download historical candle (OHLCV) data from a fixed starting point, use `--timerange 20200101-` - which will download all data from January 1st, 2020. Eventually set end dates are ignored.
|
||||||
|
- Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data.
|
||||||
|
- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
|
||||||
|
|
||||||
|
|
||||||
### Data format
|
### Data format
|
||||||
|
|
||||||
Freqtrade currently supports 3 data-formats for both OHLCV and trades data:
|
Freqtrade currently supports 3 data-formats for both OHLCV and trades data:
|
||||||
@ -312,64 +391,6 @@ ETH/BTC 5m, 15m, 30m, 1h, 2h, 4h, 6h, 12h, 1d
|
|||||||
ETH/USDT 5m, 15m, 30m, 1h, 2h, 4h
|
ETH/USDT 5m, 15m, 30m, 1h, 2h, 4h
|
||||||
```
|
```
|
||||||
|
|
||||||
### Pairs file
|
|
||||||
|
|
||||||
In alternative to the whitelist from `config.json`, a `pairs.json` file can be used.
|
|
||||||
|
|
||||||
If you are using Binance for example:
|
|
||||||
|
|
||||||
- create a directory `user_data/data/binance` and copy or create the `pairs.json` file in that directory.
|
|
||||||
- update the `pairs.json` file to contain the currency pairs you are interested in.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
mkdir -p user_data/data/binance
|
|
||||||
cp tests/testdata/pairs.json user_data/data/binance
|
|
||||||
```
|
|
||||||
|
|
||||||
If your configuration directory `user_data` was made by docker, you may get the following error:
|
|
||||||
|
|
||||||
```
|
|
||||||
cp: cannot create regular file 'user_data/data/binance/pairs.json': Permission denied
|
|
||||||
```
|
|
||||||
|
|
||||||
You can fix the permissions of your user-data directory as follows:
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo chown -R $UID:$GID user_data
|
|
||||||
```
|
|
||||||
|
|
||||||
The format of the `pairs.json` file is a simple json list.
|
|
||||||
Mixing different stake-currencies is allowed for this file, since it's only used for downloading.
|
|
||||||
|
|
||||||
``` json
|
|
||||||
[
|
|
||||||
"ETH/BTC",
|
|
||||||
"ETH/USDT",
|
|
||||||
"BTC/USDT",
|
|
||||||
"XRP/ETH"
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Start download
|
|
||||||
|
|
||||||
Then run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
freqtrade download-data --exchange binance
|
|
||||||
```
|
|
||||||
|
|
||||||
This will download historical candle (OHLCV) data for all the currency pairs you defined in `pairs.json`.
|
|
||||||
|
|
||||||
### Other Notes
|
|
||||||
|
|
||||||
- To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`.
|
|
||||||
- To change the exchange used to download the historical data from, please use a different configuration file (you'll probably need to adjust rate limits etc.)
|
|
||||||
- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`.
|
|
||||||
- To download historical candle (OHLCV) data for only 10 days, use `--days 10` (defaults to 30 days).
|
|
||||||
- To download historical candle (OHLCV) data from a fixed starting point, use `--timerange 20200101-` - which will download all data from January 1st, 2020. Eventually set end dates are ignored.
|
|
||||||
- Use `--timeframes` to specify what timeframe download the historical candle (OHLCV) data for. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute data.
|
|
||||||
- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
|
|
||||||
|
|
||||||
### Trades (tick) data
|
### Trades (tick) data
|
||||||
|
|
||||||
By default, `download-data` sub-command downloads Candles (OHLCV) data. Some exchanges also provide historic trade-data via their API.
|
By default, `download-data` sub-command downloads Candles (OHLCV) data. Some exchanges also provide historic trade-data via their API.
|
||||||
|
@ -8,7 +8,7 @@ All contributions, bug reports, bug fixes, documentation improvements, enhanceme
|
|||||||
|
|
||||||
Documentation is available at [https://freqtrade.io](https://www.freqtrade.io/) and needs to be provided with every new feature PR.
|
Documentation is available at [https://freqtrade.io](https://www.freqtrade.io/) and needs to be provided with every new feature PR.
|
||||||
|
|
||||||
Special fields for the documentation (like Note boxes, ...) can be found [here](https://squidfunk.github.io/mkdocs-material/extensions/admonition/).
|
Special fields for the documentation (like Note boxes, ...) can be found [here](https://squidfunk.github.io/mkdocs-material/reference/admonitions/).
|
||||||
|
|
||||||
To test the documentation locally use the following commands.
|
To test the documentation locally use the following commands.
|
||||||
|
|
||||||
|
@ -51,6 +51,7 @@ usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
|
|||||||
[--print-all] [--no-color] [--print-json] [-j JOBS]
|
[--print-all] [--no-color] [--print-json] [-j JOBS]
|
||||||
[--random-state INT] [--min-trades INT]
|
[--random-state INT] [--min-trades INT]
|
||||||
[--hyperopt-loss NAME] [--disable-param-export]
|
[--hyperopt-loss NAME] [--disable-param-export]
|
||||||
|
[--ignore-missing-spaces]
|
||||||
|
|
||||||
optional arguments:
|
optional arguments:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
@ -118,6 +119,9 @@ optional arguments:
|
|||||||
MaxDrawDownHyperOptLoss
|
MaxDrawDownHyperOptLoss
|
||||||
--disable-param-export
|
--disable-param-export
|
||||||
Disable automatic hyperopt parameter export.
|
Disable automatic hyperopt parameter export.
|
||||||
|
--ignore-missing-spaces, --ignore-unparameterized-spaces
|
||||||
|
Suppress errors for any requested Hyperopt spaces that
|
||||||
|
do not contain any parameters.
|
||||||
|
|
||||||
Common arguments:
|
Common arguments:
|
||||||
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
|
@ -52,6 +52,8 @@ To skip pair validation against active markets, set `"allow_inactive": true` wit
|
|||||||
This can be useful for backtesting expired pairs (like quarterly spot-markets).
|
This can be useful for backtesting expired pairs (like quarterly spot-markets).
|
||||||
This option must be configured along with `exchange.skip_pair_validation` in the exchange configuration.
|
This option must be configured along with `exchange.skip_pair_validation` in the exchange configuration.
|
||||||
|
|
||||||
|
When used in a "follow-up" position (e.g. after VolumePairlist), all pairs in `'pair_whitelist'` will be added to the end of the pairlist.
|
||||||
|
|
||||||
#### Volume Pair List
|
#### Volume Pair List
|
||||||
|
|
||||||
`VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`).
|
`VolumePairList` employs sorting/filtering of pairs by their trading volume. It selects `number_assets` top pairs with sorting based on the `sort_key` (which can only be `quoteVolume`).
|
||||||
@ -194,17 +196,22 @@ Trade count is used as a tie breaker.
|
|||||||
You can use the `minutes` parameter to only consider performance of the past X minutes (rolling window).
|
You can use the `minutes` parameter to only consider performance of the past X minutes (rolling window).
|
||||||
Not defining this parameter (or setting it to 0) will use all-time performance.
|
Not defining this parameter (or setting it to 0) will use all-time performance.
|
||||||
|
|
||||||
|
The optional `min_profit` parameter defines the minimum profit a pair must have to be considered.
|
||||||
|
Pairs below this level will be filtered out.
|
||||||
|
Using this parameter without `minutes` is highly discouraged, as it can lead to an empty pairlist without without a way to recover.
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"pairlists": [
|
"pairlists": [
|
||||||
// ...
|
// ...
|
||||||
{
|
{
|
||||||
"method": "PerformanceFilter",
|
"method": "PerformanceFilter",
|
||||||
"minutes": 1440 // rolling 24h
|
"minutes": 1440, // rolling 24h
|
||||||
|
"min_profit": 0.01
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
```
|
```
|
||||||
|
|
||||||
!!! Note
|
!!! Warning "Backtesting"
|
||||||
`PerformanceFilter` does not support backtesting mode.
|
`PerformanceFilter` does not support backtesting mode.
|
||||||
|
|
||||||
#### PrecisionFilter
|
#### PrecisionFilter
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
mkdocs==1.2.2
|
mkdocs==1.2.3
|
||||||
mkdocs-material==7.3.2
|
mkdocs-material==7.3.4
|
||||||
mdx_truly_sane_lists==1.2
|
mdx_truly_sane_lists==1.2
|
||||||
pymdown-extensions==9.0
|
pymdown-extensions==9.0
|
||||||
|
@ -281,7 +281,7 @@ bitmax True missing opt: fetchMyTrades
|
|||||||
bitmex False Various reasons.
|
bitmex False Various reasons.
|
||||||
bitpanda True
|
bitpanda True
|
||||||
bitso False missing: fetchOHLCV
|
bitso False missing: fetchOHLCV
|
||||||
bitstamp False Does not provide history. Details in https://github.com/freqtrade/freqtrade/issues/1983
|
bitstamp True missing opt: fetchTickers
|
||||||
bitstamp1 False missing: fetchOrder, fetchOHLCV
|
bitstamp1 False missing: fetchOrder, fetchOHLCV
|
||||||
bittrex True
|
bittrex True
|
||||||
bitvavo True
|
bitvavo True
|
||||||
|
@ -16,7 +16,6 @@ dependencies:
|
|||||||
- cachetools
|
- cachetools
|
||||||
- requests
|
- requests
|
||||||
- urllib3
|
- urllib3
|
||||||
- wrapt
|
|
||||||
- jsonschema
|
- jsonschema
|
||||||
- TA-Lib
|
- TA-Lib
|
||||||
- tabulate
|
- tabulate
|
||||||
@ -64,7 +63,6 @@ dependencies:
|
|||||||
- py_find_1st
|
- py_find_1st
|
||||||
- tables
|
- tables
|
||||||
- pytest-random-order
|
- pytest-random-order
|
||||||
- flake8-type-annotations
|
|
||||||
- ccxt
|
- ccxt
|
||||||
- flake8-tidy-imports
|
- flake8-tidy-imports
|
||||||
- -e .
|
- -e .
|
||||||
|
@ -31,7 +31,8 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
|
|||||||
"epochs", "spaces", "print_all",
|
"epochs", "spaces", "print_all",
|
||||||
"print_colorized", "print_json", "hyperopt_jobs",
|
"print_colorized", "print_json", "hyperopt_jobs",
|
||||||
"hyperopt_random_state", "hyperopt_min_trades",
|
"hyperopt_random_state", "hyperopt_min_trades",
|
||||||
"hyperopt_loss", "disableparamexport"]
|
"hyperopt_loss", "disableparamexport",
|
||||||
|
"hyperopt_ignore_missing_space"]
|
||||||
|
|
||||||
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
|
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
|
||||||
|
|
||||||
@ -62,9 +63,9 @@ ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "d
|
|||||||
|
|
||||||
ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"]
|
ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"]
|
||||||
|
|
||||||
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "timerange",
|
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "include_inactive",
|
||||||
"download_trades", "exchange", "timeframes", "erase", "dataformat_ohlcv",
|
"timerange", "download_trades", "exchange", "timeframes",
|
||||||
"dataformat_trades"]
|
"erase", "dataformat_ohlcv", "dataformat_trades"]
|
||||||
|
|
||||||
ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
|
ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
|
||||||
"db_url", "trade_source", "export", "exportfilename",
|
"db_url", "trade_source", "export", "exportfilename",
|
||||||
|
@ -355,6 +355,11 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
type=check_int_positive,
|
type=check_int_positive,
|
||||||
metavar='INT',
|
metavar='INT',
|
||||||
),
|
),
|
||||||
|
"include_inactive": Arg(
|
||||||
|
'--include-inactive-pairs',
|
||||||
|
help='Also download data from inactive pairs.',
|
||||||
|
action='store_true',
|
||||||
|
),
|
||||||
"new_pairs_days": Arg(
|
"new_pairs_days": Arg(
|
||||||
'--new-pairs-days',
|
'--new-pairs-days',
|
||||||
help='Download data of new pairs for given number of days. Default: `%(default)s`.',
|
help='Download data of new pairs for given number of days. Default: `%(default)s`.',
|
||||||
@ -558,4 +563,10 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
help='Do not print epoch details header.',
|
help='Do not print epoch details header.',
|
||||||
action='store_true',
|
action='store_true',
|
||||||
),
|
),
|
||||||
|
"hyperopt_ignore_missing_space": Arg(
|
||||||
|
"--ignore-missing-spaces", "--ignore-unparameterized-spaces",
|
||||||
|
help=("Suppress errors for any requested Hyperopt spaces "
|
||||||
|
"that do not contain any parameters."),
|
||||||
|
action="store_true",
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_oh
|
|||||||
from freqtrade.enums import RunMode
|
from freqtrade.enums import RunMode
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import timeframe_to_minutes
|
from freqtrade.exchange import timeframe_to_minutes
|
||||||
|
from freqtrade.exchange.exchange import market_is_active
|
||||||
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
|
||||||
from freqtrade.resolvers import ExchangeResolver
|
from freqtrade.resolvers import ExchangeResolver
|
||||||
|
|
||||||
@ -47,11 +48,13 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
# Init exchange
|
# Init exchange
|
||||||
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
|
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
|
||||||
|
markets = [p for p, m in exchange.markets.items() if market_is_active(m)
|
||||||
|
or config.get('include_inactive')]
|
||||||
|
expanded_pairs = expand_pairlist(config['pairs'], markets)
|
||||||
|
|
||||||
# Manual validations of relevant settings
|
# Manual validations of relevant settings
|
||||||
if not config['exchange'].get('skip_pair_validation', False):
|
if not config['exchange'].get('skip_pair_validation', False):
|
||||||
exchange.validate_pairs(config['pairs'])
|
exchange.validate_pairs(expanded_pairs)
|
||||||
expanded_pairs = expand_pairlist(config['pairs'], list(exchange.markets))
|
|
||||||
|
|
||||||
logger.info(f"About to download pairs: {expanded_pairs}, "
|
logger.info(f"About to download pairs: {expanded_pairs}, "
|
||||||
f"intervals: {config['timeframes']} to {config['datadir']}")
|
f"intervals: {config['timeframes']} to {config['datadir']}")
|
||||||
|
|
||||||
|
@ -369,6 +369,9 @@ class Configuration:
|
|||||||
self._args_to_config(config, argname='hyperopt_show_no_header',
|
self._args_to_config(config, argname='hyperopt_show_no_header',
|
||||||
logstring='Parameter --no-header detected: {}')
|
logstring='Parameter --no-header detected: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname="hyperopt_ignore_missing_space",
|
||||||
|
logstring="Paramter --ignore-missing-space detected: {}")
|
||||||
|
|
||||||
def _process_plot_options(self, config: Dict[str, Any]) -> None:
|
def _process_plot_options(self, config: Dict[str, Any]) -> None:
|
||||||
|
|
||||||
self._args_to_config(config, argname='pairs',
|
self._args_to_config(config, argname='pairs',
|
||||||
@ -404,6 +407,9 @@ class Configuration:
|
|||||||
self._args_to_config(config, argname='days',
|
self._args_to_config(config, argname='days',
|
||||||
logstring='Detected --days: {}')
|
logstring='Detected --days: {}')
|
||||||
|
|
||||||
|
self._args_to_config(config, argname='include_inactive',
|
||||||
|
logstring='Detected --include-inactive-pairs: {}')
|
||||||
|
|
||||||
self._args_to_config(config, argname='download_trades',
|
self._args_to_config(config, argname='download_trades',
|
||||||
logstring='Detected --dl-trades: {}')
|
logstring='Detected --dl-trades: {}')
|
||||||
|
|
||||||
|
@ -39,6 +39,8 @@ DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume']
|
|||||||
# Don't modify sequence of DEFAULT_TRADES_COLUMNS
|
# Don't modify sequence of DEFAULT_TRADES_COLUMNS
|
||||||
# it has wide consequences for stored trades files
|
# it has wide consequences for stored trades files
|
||||||
DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost']
|
DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost']
|
||||||
|
TRADING_MODES = ['spot', 'margin', 'futures']
|
||||||
|
COLLATERAL_TYPES = ['cross', 'isolated']
|
||||||
|
|
||||||
LAST_BT_RESULT_FN = '.last_result.json'
|
LAST_BT_RESULT_FN = '.last_result.json'
|
||||||
FTHYPT_FILEVERSION = 'fthypt_fileversion'
|
FTHYPT_FILEVERSION = 'fthypt_fileversion'
|
||||||
@ -146,6 +148,8 @@ CONF_SCHEMA = {
|
|||||||
'sell_profit_offset': {'type': 'number'},
|
'sell_profit_offset': {'type': 'number'},
|
||||||
'ignore_roi_if_buy_signal': {'type': 'boolean'},
|
'ignore_roi_if_buy_signal': {'type': 'boolean'},
|
||||||
'ignore_buying_expired_candle_after': {'type': 'number'},
|
'ignore_buying_expired_candle_after': {'type': 'number'},
|
||||||
|
'trading_mode': {'type': 'string', 'enum': TRADING_MODES},
|
||||||
|
'collateral_type': {'type': 'string', 'enum': COLLATERAL_TYPES},
|
||||||
'bot_name': {'type': 'string'},
|
'bot_name': {'type': 'string'},
|
||||||
'unfilledtimeout': {
|
'unfilledtimeout': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
@ -193,7 +197,7 @@ CONF_SCHEMA = {
|
|||||||
'required': ['price_side']
|
'required': ['price_side']
|
||||||
},
|
},
|
||||||
'custom_price_max_distance_ratio': {
|
'custom_price_max_distance_ratio': {
|
||||||
'type': 'number', 'minimum': 0.0
|
'type': 'number', 'minimum': 0.0
|
||||||
},
|
},
|
||||||
'order_types': {
|
'order_types': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
|
@ -5,15 +5,21 @@ class RPCMessageType(Enum):
|
|||||||
STATUS = 'status'
|
STATUS = 'status'
|
||||||
WARNING = 'warning'
|
WARNING = 'warning'
|
||||||
STARTUP = 'startup'
|
STARTUP = 'startup'
|
||||||
|
|
||||||
BUY = 'buy'
|
BUY = 'buy'
|
||||||
BUY_FILL = 'buy_fill'
|
BUY_FILL = 'buy_fill'
|
||||||
BUY_CANCEL = 'buy_cancel'
|
BUY_CANCEL = 'buy_cancel'
|
||||||
|
|
||||||
SELL = 'sell'
|
SELL = 'sell'
|
||||||
SELL_FILL = 'sell_fill'
|
SELL_FILL = 'sell_fill'
|
||||||
SELL_CANCEL = 'sell_cancel'
|
SELL_CANCEL = 'sell_cancel'
|
||||||
PROTECTION_TRIGGER = 'protection_trigger'
|
PROTECTION_TRIGGER = 'protection_trigger'
|
||||||
PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global'
|
PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global'
|
||||||
|
|
||||||
|
SHORT = 'short'
|
||||||
|
SHORT_FILL = 'short_fill'
|
||||||
|
SHORT_CANCEL = 'short_cancel'
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return self.value
|
return self.value
|
||||||
|
|
||||||
|
@ -16,8 +16,6 @@ API_FETCH_ORDER_RETRY_COUNT = 5
|
|||||||
|
|
||||||
BAD_EXCHANGES = {
|
BAD_EXCHANGES = {
|
||||||
"bitmex": "Various reasons.",
|
"bitmex": "Various reasons.",
|
||||||
"bitstamp": "Does not provide history. "
|
|
||||||
"Details in https://github.com/freqtrade/freqtrade/issues/1983",
|
|
||||||
"phemex": "Does not provide history. ",
|
"phemex": "Does not provide history. ",
|
||||||
"poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders.",
|
"poloniex": "Does not provide fetch_order endpoint to fetch both open and closed orders.",
|
||||||
}
|
}
|
||||||
|
@ -805,8 +805,14 @@ class Exchange:
|
|||||||
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
|
rate_for_order = self.price_to_precision(pair, rate) if needs_price else None
|
||||||
|
|
||||||
self._lev_prep(pair, leverage)
|
self._lev_prep(pair, leverage)
|
||||||
order = self._api.create_order(pair, ordertype, side,
|
order = self._api.create_order(
|
||||||
amount, rate_for_order, params)
|
pair,
|
||||||
|
ordertype,
|
||||||
|
side,
|
||||||
|
amount,
|
||||||
|
rate_for_order,
|
||||||
|
params
|
||||||
|
)
|
||||||
self._log_exchange_response('create_order', order)
|
self._log_exchange_response('create_order', order)
|
||||||
return order
|
return order
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import traceback
|
|||||||
from datetime import datetime, time, timezone
|
from datetime import datetime, time, timezone
|
||||||
from math import isclose
|
from math import isclose
|
||||||
from threading import Lock
|
from threading import Lock
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional, Tuple
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from schedule import Scheduler
|
from schedule import Scheduler
|
||||||
@ -17,7 +17,8 @@ from freqtrade.configuration import validate_config_consistency
|
|||||||
from freqtrade.data.converter import order_book_to_dataframe
|
from freqtrade.data.converter import order_book_to_dataframe
|
||||||
from freqtrade.data.dataprovider import DataProvider
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
from freqtrade.edge import Edge
|
from freqtrade.edge import Edge
|
||||||
from freqtrade.enums import RPCMessageType, SellType, State, TradingMode
|
from freqtrade.enums import (Collateral, RPCMessageType, SellType, SignalDirection, State,
|
||||||
|
TradingMode)
|
||||||
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
|
from freqtrade.exceptions import (DependencyException, ExchangeError, InsufficientFundsError,
|
||||||
InvalidOrderException, PricingError)
|
InvalidOrderException, PricingError)
|
||||||
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
|
||||||
@ -101,14 +102,19 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
initial_state = self.config.get('initial_state')
|
initial_state = self.config.get('initial_state')
|
||||||
self.state = State[initial_state.upper()] if initial_state else State.STOPPED
|
self.state = State[initial_state.upper()] if initial_state else State.STOPPED
|
||||||
|
|
||||||
# Protect sell-logic from forcesell and vice versa
|
# Protect exit-logic from forcesell and vice versa
|
||||||
self._exit_lock = Lock()
|
self._exit_lock = Lock()
|
||||||
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
|
LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe))
|
||||||
|
|
||||||
|
self.trading_mode: TradingMode = TradingMode.SPOT
|
||||||
|
self.collateral_type: Optional[Collateral] = None
|
||||||
|
|
||||||
if 'trading_mode' in self.config:
|
if 'trading_mode' in self.config:
|
||||||
self.trading_mode = TradingMode(self.config['trading_mode'])
|
self.trading_mode = TradingMode(self.config['trading_mode'])
|
||||||
else:
|
|
||||||
self.trading_mode = TradingMode.SPOT
|
if 'collateral_type' in self.config:
|
||||||
|
self.collateral_type = Collateral(self.config['collateral_type'])
|
||||||
|
|
||||||
self._schedule = Scheduler()
|
self._schedule = Scheduler()
|
||||||
|
|
||||||
if self.trading_mode == TradingMode.FUTURES:
|
if self.trading_mode == TradingMode.FUTURES:
|
||||||
@ -194,7 +200,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
# Protect from collisions with forceexit.
|
# Protect from collisions with forceexit.
|
||||||
# Without this, freqtrade my try to recreate stoploss_on_exchange orders
|
# Without this, freqtrade my try to recreate stoploss_on_exchange orders
|
||||||
# while selling is in process, since telegram messages arrive in an different thread.
|
# while exiting is in process, since telegram messages arrive in an different thread.
|
||||||
with self._exit_lock:
|
with self._exit_lock:
|
||||||
trades = Trade.get_open_trades()
|
trades = Trade.get_open_trades()
|
||||||
# First process current opened trades (positions)
|
# First process current opened trades (positions)
|
||||||
@ -305,21 +311,26 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
trades: List[Trade] = Trade.get_closed_trades_without_assigned_fees()
|
trades: List[Trade] = Trade.get_closed_trades_without_assigned_fees()
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
|
if not trade.is_open and not trade.fee_updated(trade.exit_side):
|
||||||
if not trade.is_open and not trade.fee_updated('sell'):
|
|
||||||
# Get sell fee
|
# Get sell fee
|
||||||
order = trade.select_order('sell', False)
|
order = trade.select_order(trade.exit_side, False)
|
||||||
if order:
|
if order:
|
||||||
logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.")
|
logger.info(
|
||||||
|
f"Updating {trade.exit_side}-fee on trade {trade}"
|
||||||
|
f"for order {order.order_id}."
|
||||||
|
)
|
||||||
self.update_trade_state(trade, order.order_id,
|
self.update_trade_state(trade, order.order_id,
|
||||||
stoploss_order=order.ft_order_side == 'stoploss')
|
stoploss_order=order.ft_order_side == 'stoploss')
|
||||||
|
|
||||||
trades: List[Trade] = Trade.get_open_trades_without_assigned_fees()
|
trades: List[Trade] = Trade.get_open_trades_without_assigned_fees()
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
if trade.is_open and not trade.fee_updated('buy'):
|
if trade.is_open and not trade.fee_updated(trade.enter_side):
|
||||||
order = trade.select_order('buy', False)
|
order = trade.select_order(trade.enter_side, False)
|
||||||
if order:
|
if order:
|
||||||
logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.")
|
logger.info(
|
||||||
|
f"Updating {trade.enter_side}-fee on trade {trade}"
|
||||||
|
f"for order {order.order_id}."
|
||||||
|
)
|
||||||
self.update_trade_state(trade, order.order_id)
|
self.update_trade_state(trade, order.order_id)
|
||||||
|
|
||||||
def handle_insufficient_funds(self, trade: Trade):
|
def handle_insufficient_funds(self, trade: Trade):
|
||||||
@ -327,8 +338,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
Determine if we ever opened a exiting order for this trade.
|
Determine if we ever opened a exiting order for this trade.
|
||||||
If not, try update entering fees - otherwise "refind" the open order we obviously lost.
|
If not, try update entering fees - otherwise "refind" the open order we obviously lost.
|
||||||
"""
|
"""
|
||||||
sell_order = trade.select_order('sell', None)
|
exit_order = trade.select_order(trade.exit_side, None)
|
||||||
if sell_order:
|
if exit_order:
|
||||||
self.refind_lost_order(trade)
|
self.refind_lost_order(trade)
|
||||||
else:
|
else:
|
||||||
self.reupdate_enter_order_fees(trade)
|
self.reupdate_enter_order_fees(trade)
|
||||||
@ -338,10 +349,11 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
Get buy order from database, and try to reupdate.
|
Get buy order from database, and try to reupdate.
|
||||||
Handles trades where the initial fee-update did not work.
|
Handles trades where the initial fee-update did not work.
|
||||||
"""
|
"""
|
||||||
logger.info(f"Trying to reupdate buy fees for {trade}")
|
logger.info(f"Trying to reupdate {trade.enter_side} fees for {trade}")
|
||||||
order = trade.select_order('buy', False)
|
order = trade.select_order(trade.enter_side, False)
|
||||||
if order:
|
if order:
|
||||||
logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.")
|
logger.info(
|
||||||
|
f"Updating {trade.enter_side}-fee on trade {trade} for order {order.order_id}.")
|
||||||
self.update_trade_state(trade, order.order_id)
|
self.update_trade_state(trade, order.order_id)
|
||||||
|
|
||||||
def refind_lost_order(self, trade):
|
def refind_lost_order(self, trade):
|
||||||
@ -357,7 +369,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if not order.ft_is_open:
|
if not order.ft_is_open:
|
||||||
logger.debug(f"Order {order} is no longer open.")
|
logger.debug(f"Order {order} is no longer open.")
|
||||||
continue
|
continue
|
||||||
if order.ft_order_side == 'buy':
|
if order.ft_order_side == trade.enter_side:
|
||||||
# Skip buy side - this is handled by reupdate_enter_order_fees
|
# Skip buy side - this is handled by reupdate_enter_order_fees
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
@ -367,7 +379,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if fo and fo['status'] == 'open':
|
if fo and fo['status'] == 'open':
|
||||||
# Assume this as the open stoploss order
|
# Assume this as the open stoploss order
|
||||||
trade.stoploss_order_id = order.order_id
|
trade.stoploss_order_id = order.order_id
|
||||||
elif order.ft_order_side == 'sell':
|
elif order.ft_order_side == trade.exit_side:
|
||||||
if fo and fo['status'] == 'open':
|
if fo and fo['status'] == 'open':
|
||||||
# Assume this as the open order
|
# Assume this as the open order
|
||||||
trade.open_order_id = order.order_id
|
trade.open_order_id = order.order_id
|
||||||
@ -456,7 +468,9 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
# running get_signal on historical data fetched
|
# running get_signal on historical data fetched
|
||||||
(signal, enter_tag) = self.strategy.get_entry_signal(
|
(signal, enter_tag) = self.strategy.get_entry_signal(
|
||||||
pair, self.strategy.timeframe, analyzed_df
|
pair,
|
||||||
|
self.strategy.timeframe,
|
||||||
|
analyzed_df
|
||||||
)
|
)
|
||||||
|
|
||||||
if signal:
|
if signal:
|
||||||
@ -465,19 +479,31 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {})
|
bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {})
|
||||||
if ((bid_check_dom.get('enabled', False)) and
|
if ((bid_check_dom.get('enabled', False)) and
|
||||||
(bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
|
(bid_check_dom.get('bids_to_ask_delta', 0) > 0)):
|
||||||
# TODO-lev: Does the below need to be adjusted for shorts?
|
if self._check_depth_of_market(pair, bid_check_dom, side=signal):
|
||||||
if self._check_depth_of_market_buy(pair, bid_check_dom):
|
return self.execute_entry(
|
||||||
# TODO-lev: pass in "enter" as side.
|
pair,
|
||||||
|
stake_amount,
|
||||||
return self.execute_entry(pair, stake_amount, enter_tag=enter_tag)
|
enter_tag=enter_tag,
|
||||||
|
is_short=(signal == SignalDirection.SHORT)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return self.execute_entry(pair, stake_amount, enter_tag=enter_tag)
|
return self.execute_entry(
|
||||||
|
pair,
|
||||||
|
stake_amount,
|
||||||
|
enter_tag=enter_tag,
|
||||||
|
is_short=(signal == SignalDirection.SHORT)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _check_depth_of_market_buy(self, pair: str, conf: Dict) -> bool:
|
def _check_depth_of_market(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
conf: Dict,
|
||||||
|
side: SignalDirection
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Checks depth of market before executing a buy
|
Checks depth of market before executing a buy
|
||||||
"""
|
"""
|
||||||
@ -487,9 +513,17 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks'])
|
order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks'])
|
||||||
order_book_bids = order_book_data_frame['b_size'].sum()
|
order_book_bids = order_book_data_frame['b_size'].sum()
|
||||||
order_book_asks = order_book_data_frame['a_size'].sum()
|
order_book_asks = order_book_data_frame['a_size'].sum()
|
||||||
bids_ask_delta = order_book_bids / order_book_asks
|
|
||||||
|
enter_side = order_book_bids if side == SignalDirection.LONG else order_book_asks
|
||||||
|
exit_side = order_book_asks if side == SignalDirection.LONG else order_book_bids
|
||||||
|
bids_ask_delta = enter_side / exit_side
|
||||||
|
|
||||||
|
bids = f"Bids: {order_book_bids}"
|
||||||
|
asks = f"Asks: {order_book_asks}"
|
||||||
|
delta = f"Delta: {bids_ask_delta}"
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Bids: {order_book_bids}, Asks: {order_book_asks}, Delta: {bids_ask_delta}, "
|
f"{bids}, {asks}, {delta}, Direction: {side.value}"
|
||||||
f"Bid Price: {order_book['bids'][0][0]}, Ask Price: {order_book['asks'][0][0]}, "
|
f"Bid Price: {order_book['bids'][0][0]}, Ask Price: {order_book['asks'][0][0]}, "
|
||||||
f"Immediate Bid Quantity: {order_book['bids'][0][1]}, "
|
f"Immediate Bid Quantity: {order_book['bids'][0][1]}, "
|
||||||
f"Immediate Ask Quantity: {order_book['asks'][0][1]}."
|
f"Immediate Ask Quantity: {order_book['asks'][0][1]}."
|
||||||
@ -501,21 +535,65 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
|
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None,
|
def leverage_prep(
|
||||||
forcebuy: bool = False, enter_tag: Optional[str] = None) -> bool:
|
self,
|
||||||
|
pair: str,
|
||||||
|
open_rate: float,
|
||||||
|
amount: float,
|
||||||
|
leverage: float,
|
||||||
|
is_short: bool
|
||||||
|
) -> Tuple[float, Optional[float]]:
|
||||||
|
|
||||||
|
interest_rate = 0.0
|
||||||
|
isolated_liq = None
|
||||||
|
|
||||||
|
# TODO-lev: Uncomment once liq and interest merged in
|
||||||
|
# if TradingMode == TradingMode.MARGIN:
|
||||||
|
# interest_rate = self.exchange.get_interest_rate(
|
||||||
|
# pair=pair,
|
||||||
|
# open_rate=open_rate,
|
||||||
|
# is_short=is_short
|
||||||
|
# )
|
||||||
|
|
||||||
|
# if self.collateral_type == Collateral.ISOLATED:
|
||||||
|
|
||||||
|
# isolated_liq = liquidation_price(
|
||||||
|
# exchange_name=self.exchange.name,
|
||||||
|
# trading_mode=self.trading_mode,
|
||||||
|
# open_rate=open_rate,
|
||||||
|
# amount=amount,
|
||||||
|
# leverage=leverage,
|
||||||
|
# is_short=is_short
|
||||||
|
# )
|
||||||
|
|
||||||
|
return interest_rate, isolated_liq
|
||||||
|
|
||||||
|
def execute_entry(
|
||||||
|
self,
|
||||||
|
pair: str,
|
||||||
|
stake_amount: float,
|
||||||
|
price: Optional[float] = None,
|
||||||
|
forcebuy: bool = False,
|
||||||
|
leverage: float = 1.0,
|
||||||
|
is_short: bool = False,
|
||||||
|
enter_tag: Optional[str] = None
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Executes a limit buy for the given pair
|
Executes a limit buy for the given pair
|
||||||
:param pair: pair for which we want to create a LIMIT_BUY
|
:param pair: pair for which we want to create a LIMIT_BUY
|
||||||
:param stake_amount: amount of stake-currency for the pair
|
:param stake_amount: amount of stake-currency for the pair
|
||||||
|
:param leverage: amount of leverage applied to this trade
|
||||||
:return: True if a buy order is created, false if it fails.
|
:return: True if a buy order is created, false if it fails.
|
||||||
"""
|
"""
|
||||||
time_in_force = self.strategy.order_time_in_force['buy']
|
time_in_force = self.strategy.order_time_in_force['buy']
|
||||||
|
|
||||||
|
[side, name] = ['sell', 'Short'] if is_short else ['buy', 'Long']
|
||||||
|
|
||||||
if price:
|
if price:
|
||||||
enter_limit_requested = price
|
enter_limit_requested = price
|
||||||
else:
|
else:
|
||||||
# Calculate price
|
# Calculate price
|
||||||
proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side="buy")
|
proposed_enter_rate = self.exchange.get_rate(pair, refresh=True, side=side)
|
||||||
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
|
custom_entry_price = strategy_safe_wrapper(self.strategy.custom_entry_price,
|
||||||
default_retval=proposed_enter_rate)(
|
default_retval=proposed_enter_rate)(
|
||||||
pair=pair, current_time=datetime.now(timezone.utc),
|
pair=pair, current_time=datetime.now(timezone.utc),
|
||||||
@ -524,10 +602,14 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate)
|
enter_limit_requested = self.get_valid_price(custom_entry_price, proposed_enter_rate)
|
||||||
|
|
||||||
if not enter_limit_requested:
|
if not enter_limit_requested:
|
||||||
raise PricingError('Could not determine buy price.')
|
raise PricingError(f'Could not determine {side} price.')
|
||||||
|
|
||||||
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, enter_limit_requested,
|
min_stake_amount = self.exchange.get_min_pair_stake_amount(
|
||||||
self.strategy.stoploss)
|
pair,
|
||||||
|
enter_limit_requested,
|
||||||
|
self.strategy.stoploss,
|
||||||
|
leverage=leverage
|
||||||
|
)
|
||||||
|
|
||||||
if not self.edge:
|
if not self.edge:
|
||||||
max_stake_amount = self.wallets.get_available_stake_amount()
|
max_stake_amount = self.wallets.get_available_stake_amount()
|
||||||
@ -543,10 +625,12 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if not stake_amount:
|
if not stake_amount:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
logger.info(f"Buy signal found: about create a new trade for {pair} with stake_amount: "
|
logger.info(
|
||||||
f"{stake_amount} ...")
|
f"{name} signal found: about create a new trade for {pair} with stake_amount: "
|
||||||
|
f"{stake_amount} ..."
|
||||||
|
)
|
||||||
|
|
||||||
amount = stake_amount / enter_limit_requested
|
amount = (stake_amount / enter_limit_requested) * leverage
|
||||||
order_type = self.strategy.order_types['buy']
|
order_type = self.strategy.order_types['buy']
|
||||||
if forcebuy:
|
if forcebuy:
|
||||||
# Forcebuy can define a different ordertype
|
# Forcebuy can define a different ordertype
|
||||||
@ -558,15 +642,21 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
||||||
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
|
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
|
||||||
time_in_force=time_in_force, current_time=datetime.now(timezone.utc),
|
time_in_force=time_in_force, current_time=datetime.now(timezone.utc),
|
||||||
side='long'
|
side='short' if is_short else 'long'
|
||||||
):
|
):
|
||||||
logger.info(f"User requested abortion of buying {pair}")
|
logger.info(f"User requested abortion of buying {pair}")
|
||||||
return False
|
return False
|
||||||
amount = self.exchange.amount_to_precision(pair, amount)
|
amount = self.exchange.amount_to_precision(pair, amount)
|
||||||
order = self.exchange.create_order(pair=pair, ordertype=order_type, side="buy",
|
order = self.exchange.create_order(
|
||||||
amount=amount, rate=enter_limit_requested,
|
pair=pair,
|
||||||
time_in_force=time_in_force)
|
ordertype=order_type,
|
||||||
order_obj = Order.parse_from_ccxt_object(order, pair, 'buy')
|
side=side,
|
||||||
|
amount=amount,
|
||||||
|
rate=enter_limit_requested,
|
||||||
|
time_in_force=time_in_force,
|
||||||
|
leverage=leverage
|
||||||
|
)
|
||||||
|
order_obj = Order.parse_from_ccxt_object(order, pair, side)
|
||||||
order_id = order['id']
|
order_id = order['id']
|
||||||
order_status = order.get('status', None)
|
order_status = order.get('status', None)
|
||||||
|
|
||||||
@ -579,17 +669,17 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
# return false if the order is not filled
|
# return false if the order is not filled
|
||||||
if float(order['filled']) == 0:
|
if float(order['filled']) == 0:
|
||||||
logger.warning('Buy %s order with time in force %s for %s is %s by %s.'
|
logger.warning('%s %s order with time in force %s for %s is %s by %s.'
|
||||||
' zero amount is fulfilled.',
|
' zero amount is fulfilled.',
|
||||||
order_tif, order_type, pair, order_status, self.exchange.name)
|
name, order_tif, order_type, pair, order_status, self.exchange.name)
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
# the order is partially fulfilled
|
# the order is partially fulfilled
|
||||||
# in case of IOC orders we can check immediately
|
# in case of IOC orders we can check immediately
|
||||||
# if the order is fulfilled fully or partially
|
# if the order is fulfilled fully or partially
|
||||||
logger.warning('Buy %s order with time in force %s for %s is %s by %s.'
|
logger.warning('%s %s order with time in force %s for %s is %s by %s.'
|
||||||
' %s amount fulfilled out of %s (%s remaining which is canceled).',
|
' %s amount fulfilled out of %s (%s remaining which is canceled).',
|
||||||
order_tif, order_type, pair, order_status, self.exchange.name,
|
name, order_tif, order_type, pair, order_status, self.exchange.name,
|
||||||
order['filled'], order['amount'], order['remaining']
|
order['filled'], order['amount'], order['remaining']
|
||||||
)
|
)
|
||||||
stake_amount = order['cost']
|
stake_amount = order['cost']
|
||||||
@ -602,6 +692,14 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
amount = safe_value_fallback(order, 'filled', 'amount')
|
amount = safe_value_fallback(order, 'filled', 'amount')
|
||||||
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price')
|
enter_limit_filled_price = safe_value_fallback(order, 'average', 'price')
|
||||||
|
|
||||||
|
interest_rate, isolated_liq = self.leverage_prep(
|
||||||
|
leverage=leverage,
|
||||||
|
pair=pair,
|
||||||
|
amount=amount,
|
||||||
|
open_rate=enter_limit_filled_price,
|
||||||
|
is_short=is_short
|
||||||
|
)
|
||||||
|
|
||||||
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||||
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
fee = self.exchange.get_fee(symbol=pair, taker_or_maker='maker')
|
||||||
open_date = datetime.now(timezone.utc)
|
open_date = datetime.now(timezone.utc)
|
||||||
@ -627,6 +725,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# TODO-lev: compatibility layer for buy_tag (!)
|
# TODO-lev: compatibility layer for buy_tag (!)
|
||||||
buy_tag=enter_tag,
|
buy_tag=enter_tag,
|
||||||
timeframe=timeframe_to_minutes(self.config['timeframe']),
|
timeframe=timeframe_to_minutes(self.config['timeframe']),
|
||||||
|
leverage=leverage,
|
||||||
|
is_short=is_short,
|
||||||
|
interest_rate=interest_rate,
|
||||||
|
isolated_liq=isolated_liq,
|
||||||
trading_mode=self.trading_mode,
|
trading_mode=self.trading_mode,
|
||||||
funding_fees=funding_fees
|
funding_fees=funding_fees
|
||||||
)
|
)
|
||||||
@ -652,7 +754,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
"""
|
"""
|
||||||
msg = {
|
msg = {
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
'type': RPCMessageType.BUY,
|
'type': RPCMessageType.SHORT if trade.is_short else RPCMessageType.BUY,
|
||||||
'buy_tag': trade.buy_tag,
|
'buy_tag': trade.buy_tag,
|
||||||
'exchange': self.exchange.name.capitalize(),
|
'exchange': self.exchange.name.capitalize(),
|
||||||
'pair': trade.pair,
|
'pair': trade.pair,
|
||||||
@ -673,11 +775,11 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
"""
|
"""
|
||||||
Sends rpc notification when a entry order cancel occurred.
|
Sends rpc notification when a entry order cancel occurred.
|
||||||
"""
|
"""
|
||||||
current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="buy")
|
current_rate = self.exchange.get_rate(trade.pair, refresh=False, side=trade.enter_side)
|
||||||
|
msg_type = RPCMessageType.SHORT_CANCEL if trade.is_short else RPCMessageType.BUY_CANCEL
|
||||||
msg = {
|
msg = {
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
'type': RPCMessageType.BUY_CANCEL,
|
'type': msg_type,
|
||||||
'buy_tag': trade.buy_tag,
|
'buy_tag': trade.buy_tag,
|
||||||
'exchange': self.exchange.name.capitalize(),
|
'exchange': self.exchange.name.capitalize(),
|
||||||
'pair': trade.pair,
|
'pair': trade.pair,
|
||||||
@ -696,9 +798,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self.rpc.send_msg(msg)
|
self.rpc.send_msg(msg)
|
||||||
|
|
||||||
def _notify_enter_fill(self, trade: Trade) -> None:
|
def _notify_enter_fill(self, trade: Trade) -> None:
|
||||||
|
msg_type = RPCMessageType.SHORT_FILL if trade.is_short else RPCMessageType.BUY_FILL
|
||||||
msg = {
|
msg = {
|
||||||
'trade_id': trade.id,
|
'trade_id': trade.id,
|
||||||
'type': RPCMessageType.BUY_FILL,
|
'type': msg_type,
|
||||||
'buy_tag': trade.buy_tag,
|
'buy_tag': trade.buy_tag,
|
||||||
'exchange': self.exchange.name.capitalize(),
|
'exchange': self.exchange.name.capitalize(),
|
||||||
'pair': trade.pair,
|
'pair': trade.pair,
|
||||||
@ -752,6 +855,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.debug('Handling %s ...', trade)
|
logger.debug('Handling %s ...', trade)
|
||||||
|
|
||||||
(enter, exit_) = (False, False)
|
(enter, exit_) = (False, False)
|
||||||
|
exit_signal_type = "exit_short" if trade.is_short else "exit_long"
|
||||||
|
|
||||||
# TODO-lev: change to use_exit_signal, ignore_roi_if_enter_signal
|
# TODO-lev: change to use_exit_signal, ignore_roi_if_enter_signal
|
||||||
if (self.config.get('use_sell_signal', True) or
|
if (self.config.get('use_sell_signal', True) or
|
||||||
@ -762,15 +866,16 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
(enter, exit_) = self.strategy.get_exit_signal(
|
(enter, exit_) = self.strategy.get_exit_signal(
|
||||||
trade.pair,
|
trade.pair,
|
||||||
self.strategy.timeframe,
|
self.strategy.timeframe,
|
||||||
analyzed_df, is_short=trade.is_short
|
analyzed_df,
|
||||||
|
is_short=trade.is_short
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO-lev: side should depend on trade side.
|
logger.debug('checking exit')
|
||||||
exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side="sell")
|
exit_rate = self.exchange.get_rate(trade.pair, refresh=True, side=trade.exit_side)
|
||||||
if self._check_and_execute_exit(trade, exit_rate, enter, exit_):
|
if self._check_and_execute_exit(trade, exit_rate, enter, exit_):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
logger.debug('Found no sell signal for %s.', trade)
|
logger.debug(f'Found no {exit_signal_type} signal for %s.', trade)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool:
|
def create_stoploss_order(self, trade: Trade, stop_price: float) -> bool:
|
||||||
@ -855,7 +960,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange
|
# If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange
|
||||||
if not stoploss_order:
|
if not stoploss_order:
|
||||||
stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss
|
stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss
|
||||||
stop_price = trade.open_rate * (1 + stoploss)
|
if trade.is_short:
|
||||||
|
stop_price = trade.open_rate * (1 - stoploss)
|
||||||
|
else:
|
||||||
|
stop_price = trade.open_rate * (1 + stoploss)
|
||||||
|
|
||||||
if self.create_stoploss_order(trade=trade, stop_price=stop_price):
|
if self.create_stoploss_order(trade=trade, stop_price=stop_price):
|
||||||
trade.stoploss_last_update = datetime.utcnow()
|
trade.stoploss_last_update = datetime.utcnow()
|
||||||
@ -880,11 +988,11 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# if trailing stoploss is enabled we check if stoploss value has changed
|
# if trailing stoploss is enabled we check if stoploss value has changed
|
||||||
# in which case we cancel stoploss order and put another one with new
|
# in which case we cancel stoploss order and put another one with new
|
||||||
# value immediately
|
# value immediately
|
||||||
self.handle_trailing_stoploss_on_exchange(trade, stoploss_order, side=trade.exit_side)
|
self.handle_trailing_stoploss_on_exchange(trade, stoploss_order)
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict, side: str) -> None:
|
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order: dict) -> None:
|
||||||
"""
|
"""
|
||||||
Check to see if stoploss on exchange should be updated
|
Check to see if stoploss on exchange should be updated
|
||||||
in case of trailing stoploss on exchange
|
in case of trailing stoploss on exchange
|
||||||
@ -892,7 +1000,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
:param order: Current on exchange stoploss order
|
:param order: Current on exchange stoploss order
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
if self.exchange.stoploss_adjust(trade.stop_loss, order, side):
|
if self.exchange.stoploss_adjust(trade.stop_loss, order, side=trade.exit_side):
|
||||||
# we check if the update is necessary
|
# we check if the update is necessary
|
||||||
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
|
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
|
||||||
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:
|
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:
|
||||||
@ -918,7 +1026,11 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
Check and execute trade exit
|
Check and execute trade exit
|
||||||
"""
|
"""
|
||||||
should_exit: SellCheckTuple = self.strategy.should_exit(
|
should_exit: SellCheckTuple = self.strategy.should_exit(
|
||||||
trade, exit_rate, datetime.now(timezone.utc), enter=enter, exit_=exit_,
|
trade,
|
||||||
|
exit_rate,
|
||||||
|
datetime.now(timezone.utc),
|
||||||
|
enter=enter,
|
||||||
|
exit_=exit_,
|
||||||
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
force_stoploss=self.edge.stoploss(trade.pair) if self.edge else 0
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -959,24 +1071,23 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order)
|
fully_cancelled = self.update_trade_state(trade, trade.open_order_id, order)
|
||||||
|
is_entering = order['side'] == trade.enter_side
|
||||||
|
not_closed = order['status'] == 'open' or fully_cancelled
|
||||||
|
side = trade.enter_side if is_entering else trade.exit_side
|
||||||
|
timed_out = self._check_timed_out(side, order)
|
||||||
|
time_method = 'check_sell_timeout' if order['side'] == 'sell' else 'check_buy_timeout'
|
||||||
|
|
||||||
if (order['side'] == 'buy' and (order['status'] == 'open' or fully_cancelled) and (
|
if not_closed and (fully_cancelled or timed_out or (
|
||||||
fully_cancelled
|
strategy_safe_wrapper(getattr(self.strategy, time_method), default_retval=False)(
|
||||||
or self._check_timed_out('buy', order)
|
pair=trade.pair,
|
||||||
or strategy_safe_wrapper(self.strategy.check_buy_timeout,
|
trade=trade,
|
||||||
default_retval=False)(pair=trade.pair,
|
order=order
|
||||||
trade=trade,
|
)
|
||||||
order=order))):
|
)):
|
||||||
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
if is_entering:
|
||||||
|
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||||
elif (order['side'] == 'sell' and (order['status'] == 'open' or fully_cancelled) and (
|
else:
|
||||||
fully_cancelled
|
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
||||||
or self._check_timed_out('sell', order)
|
|
||||||
or strategy_safe_wrapper(self.strategy.check_sell_timeout,
|
|
||||||
default_retval=False)(pair=trade.pair,
|
|
||||||
trade=trade,
|
|
||||||
order=order))):
|
|
||||||
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['TIMEOUT'])
|
|
||||||
|
|
||||||
def cancel_all_open_orders(self) -> None:
|
def cancel_all_open_orders(self) -> None:
|
||||||
"""
|
"""
|
||||||
@ -991,10 +1102,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
|
logger.info('Cannot query order for %s due to %s', trade, traceback.format_exc())
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if order['side'] == 'buy':
|
if order['side'] == trade.enter_side:
|
||||||
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
self.handle_cancel_enter(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
||||||
|
|
||||||
elif order['side'] == 'sell':
|
elif order['side'] == trade.exit_side:
|
||||||
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
self.handle_cancel_exit(trade, order, constants.CANCEL_REASON['ALL_CANCELLED'])
|
||||||
Trade.commit()
|
Trade.commit()
|
||||||
|
|
||||||
@ -1016,7 +1127,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
if filled_val > 0 and filled_stake < minstake:
|
if filled_val > 0 and filled_stake < minstake:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"Order {trade.open_order_id} for {trade.pair} not cancelled, "
|
f"Order {trade.open_order_id} for {trade.pair} not cancelled, "
|
||||||
f"as the filled amount of {filled_val} would result in an unsellable trade.")
|
f"as the filled amount of {filled_val} would result in an unexitable trade.")
|
||||||
return False
|
return False
|
||||||
corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
|
corder = self.exchange.cancel_order_with_result(trade.open_order_id, trade.pair,
|
||||||
trade.amount)
|
trade.amount)
|
||||||
@ -1031,12 +1142,16 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
corder = order
|
corder = order
|
||||||
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
||||||
|
|
||||||
logger.info('Buy order %s for %s.', reason, trade)
|
side = trade.enter_side.capitalize()
|
||||||
|
logger.info('%s order %s for %s.', side, reason, trade)
|
||||||
|
|
||||||
# Using filled to determine the filled amount
|
# Using filled to determine the filled amount
|
||||||
filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled')
|
filled_amount = safe_value_fallback2(corder, order, 'filled', 'filled')
|
||||||
if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC):
|
if isclose(filled_amount, 0.0, abs_tol=constants.MATH_CLOSE_PREC):
|
||||||
logger.info('Buy order fully cancelled. Removing %s from database.', trade)
|
logger.info(
|
||||||
|
'%s order fully cancelled. Removing %s from database.',
|
||||||
|
side, trade
|
||||||
|
)
|
||||||
# if trade is not partially completed, just delete the trade
|
# if trade is not partially completed, just delete the trade
|
||||||
trade.delete()
|
trade.delete()
|
||||||
was_trade_fully_canceled = True
|
was_trade_fully_canceled = True
|
||||||
@ -1054,11 +1169,11 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self.update_trade_state(trade, trade.open_order_id, corder)
|
self.update_trade_state(trade, trade.open_order_id, corder)
|
||||||
|
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
logger.info('Partial buy order timeout for %s.', trade)
|
logger.info('Partial %s order timeout for %s.', trade.enter_side, trade)
|
||||||
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
|
reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}"
|
||||||
|
|
||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
self._notify_enter_cancel(trade, order_type=self.strategy.order_types['buy'],
|
self._notify_enter_cancel(trade, order_type=self.strategy.order_types[trade.enter_side],
|
||||||
reason=reason)
|
reason=reason)
|
||||||
return was_trade_fully_canceled
|
return was_trade_fully_canceled
|
||||||
|
|
||||||
@ -1076,12 +1191,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trade.amount)
|
trade.amount)
|
||||||
trade.update_order(co)
|
trade.update_order(co)
|
||||||
except InvalidOrderException:
|
except InvalidOrderException:
|
||||||
logger.exception(f"Could not cancel sell order {trade.open_order_id}")
|
logger.exception(
|
||||||
|
f"Could not cancel {trade.exit_side} order {trade.open_order_id}")
|
||||||
return 'error cancelling order'
|
return 'error cancelling order'
|
||||||
logger.info('Sell order %s for %s.', reason, trade)
|
logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade)
|
||||||
else:
|
else:
|
||||||
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
reason = constants.CANCEL_REASON['CANCELLED_ON_EXCHANGE']
|
||||||
logger.info('Sell order %s for %s.', reason, trade)
|
logger.info('%s order %s for %s.', trade.exit_side.capitalize(), reason, trade)
|
||||||
trade.update_order(order)
|
trade.update_order(order)
|
||||||
|
|
||||||
trade.close_rate = None
|
trade.close_rate = None
|
||||||
@ -1098,7 +1214,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
self._notify_exit_cancel(
|
self._notify_exit_cancel(
|
||||||
trade,
|
trade,
|
||||||
order_type=self.strategy.order_types['sell'],
|
order_type=self.strategy.order_types[trade.exit_side],
|
||||||
reason=reason
|
reason=reason
|
||||||
)
|
)
|
||||||
return reason
|
return reason
|
||||||
@ -1129,7 +1245,12 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
raise DependencyException(
|
raise DependencyException(
|
||||||
f"Not enough amount to exit trade. Trade-amount: {amount}, Wallet: {wallet_amount}")
|
f"Not enough amount to exit trade. Trade-amount: {amount}, Wallet: {wallet_amount}")
|
||||||
|
|
||||||
def execute_trade_exit(self, trade: Trade, limit: float, sell_reason: SellCheckTuple) -> bool:
|
def execute_trade_exit(
|
||||||
|
self,
|
||||||
|
trade: Trade,
|
||||||
|
limit: float,
|
||||||
|
sell_reason: SellCheckTuple, # TODO-lev update to exit_reason
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Executes a trade exit for the given trade and limit
|
Executes a trade exit for the given trade and limit
|
||||||
:param trade: Trade instance
|
:param trade: Trade instance
|
||||||
@ -1137,13 +1258,13 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
:param sell_reason: Reason the sell was triggered
|
:param sell_reason: Reason the sell was triggered
|
||||||
:return: True if it succeeds (supported) False (not supported)
|
:return: True if it succeeds (supported) False (not supported)
|
||||||
"""
|
"""
|
||||||
sell_type = 'sell' # TODO-lev: Update to exit
|
exit_type = 'sell' # TODO-lev: Update to exit
|
||||||
if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
|
if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
|
||||||
sell_type = 'stoploss'
|
exit_type = 'stoploss'
|
||||||
|
|
||||||
# if stoploss is on exchange and we are on dry_run mode,
|
# if stoploss is on exchange and we are on dry_run mode,
|
||||||
# we consider the sell price stop price
|
# we consider the sell price stop price
|
||||||
if self.config['dry_run'] and sell_type == 'stoploss' \
|
if self.config['dry_run'] and exit_type == 'stoploss' \
|
||||||
and self.strategy.order_types['stoploss_on_exchange']:
|
and self.strategy.order_types['stoploss_on_exchange']:
|
||||||
limit = trade.stop_loss
|
limit = trade.stop_loss
|
||||||
|
|
||||||
@ -1167,7 +1288,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
except InvalidOrderException:
|
except InvalidOrderException:
|
||||||
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
||||||
|
|
||||||
order_type = self.strategy.order_types[sell_type]
|
order_type = self.strategy.order_types[exit_type]
|
||||||
if sell_reason.sell_type == SellType.EMERGENCY_SELL:
|
if sell_reason.sell_type == SellType.EMERGENCY_SELL:
|
||||||
# Emergency sells (default to market!)
|
# Emergency sells (default to market!)
|
||||||
order_type = self.strategy.order_types.get("emergencysell", "market")
|
order_type = self.strategy.order_types.get("emergencysell", "market")
|
||||||
@ -1177,7 +1298,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
order_type = self.strategy.order_types.get("forcesell", order_type)
|
order_type = self.strategy.order_types.get("forcesell", order_type)
|
||||||
|
|
||||||
amount = self._safe_exit_amount(trade.pair, trade.amount)
|
amount = self._safe_exit_amount(trade.pair, trade.amount)
|
||||||
time_in_force = self.strategy.order_time_in_force['sell']
|
time_in_force = self.strategy.order_time_in_force['sell'] # TODO-lev update to exit
|
||||||
|
|
||||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
|
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
|
||||||
pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
|
pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit,
|
||||||
@ -1191,7 +1312,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
order = self.exchange.create_order(
|
order = self.exchange.create_order(
|
||||||
pair=trade.pair,
|
pair=trade.pair,
|
||||||
ordertype=order_type,
|
ordertype=order_type,
|
||||||
side="sell",
|
side=trade.exit_side,
|
||||||
amount=amount,
|
amount=amount,
|
||||||
rate=limit,
|
rate=limit,
|
||||||
time_in_force=time_in_force
|
time_in_force=time_in_force
|
||||||
@ -1202,7 +1323,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self.handle_insufficient_funds(trade)
|
self.handle_insufficient_funds(trade)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
order_obj = Order.parse_from_ccxt_object(order, trade.pair, 'sell')
|
order_obj = Order.parse_from_ccxt_object(order, trade.pair, trade.exit_side)
|
||||||
trade.orders.append(order_obj)
|
trade.orders.append(order_obj)
|
||||||
|
|
||||||
trade.open_order_id = order['id']
|
trade.open_order_id = order['id']
|
||||||
@ -1230,7 +1351,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
profit_trade = trade.calc_profit(rate=profit_rate)
|
profit_trade = trade.calc_profit(rate=profit_rate)
|
||||||
# Use cached rates here - it was updated seconds ago.
|
# Use cached rates here - it was updated seconds ago.
|
||||||
current_rate = self.exchange.get_rate(
|
current_rate = self.exchange.get_rate(
|
||||||
trade.pair, refresh=False, side="sell") if not fill else None
|
trade.pair, refresh=False, side=trade.exit_side) if not fill else None
|
||||||
profit_ratio = trade.calc_profit_ratio(profit_rate)
|
profit_ratio = trade.calc_profit_ratio(profit_rate)
|
||||||
gain = "profit" if profit_ratio > 0 else "loss"
|
gain = "profit" if profit_ratio > 0 else "loss"
|
||||||
|
|
||||||
@ -1275,7 +1396,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
|
|
||||||
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
|
profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested
|
||||||
profit_trade = trade.calc_profit(rate=profit_rate)
|
profit_trade = trade.calc_profit(rate=profit_rate)
|
||||||
current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="sell")
|
current_rate = self.exchange.get_rate(trade.pair, refresh=False, side=trade.exit_side)
|
||||||
profit_ratio = trade.calc_profit_ratio(profit_rate)
|
profit_ratio = trade.calc_profit_ratio(profit_rate)
|
||||||
gain = "profit" if profit_ratio > 0 else "loss"
|
gain = "profit" if profit_ratio > 0 else "loss"
|
||||||
|
|
||||||
@ -1390,7 +1511,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
self.wallets.update()
|
self.wallets.update()
|
||||||
if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount:
|
if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount:
|
||||||
# Eat into dust if we own more than base currency
|
# Eat into dust if we own more than base currency
|
||||||
# TODO-lev: won't be in "base"(quote) currency for shorts
|
# TODO-lev: won't be in base currency for shorts
|
||||||
logger.info(f"Fee amount for {trade} was in base currency - "
|
logger.info(f"Fee amount for {trade} was in base currency - "
|
||||||
f"Eating Fee {fee_abs} into dust.")
|
f"Eating Fee {fee_abs} into dust.")
|
||||||
elif fee_abs != 0:
|
elif fee_abs != 0:
|
||||||
|
@ -45,7 +45,7 @@ progressbar.streams.wrap_stdout()
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
INITIAL_POINTS = 5
|
INITIAL_POINTS = 30
|
||||||
|
|
||||||
# Keep no more than SKOPT_MODEL_QUEUE_SIZE models
|
# Keep no more than SKOPT_MODEL_QUEUE_SIZE models
|
||||||
# in the skopt model queue, to optimize memory consumption
|
# in the skopt model queue, to optimize memory consumption
|
||||||
@ -258,6 +258,7 @@ class Hyperopt:
|
|||||||
if HyperoptTools.has_space(self.config, 'trailing'):
|
if HyperoptTools.has_space(self.config, 'trailing'):
|
||||||
logger.debug("Hyperopt has 'trailing' space")
|
logger.debug("Hyperopt has 'trailing' space")
|
||||||
self.trailing_space = self.custom_hyperopt.trailing_space()
|
self.trailing_space = self.custom_hyperopt.trailing_space()
|
||||||
|
|
||||||
self.dimensions = (self.buy_space + self.sell_space + self.protection_space
|
self.dimensions = (self.buy_space + self.sell_space + self.protection_space
|
||||||
+ self.roi_space + self.stoploss_space + self.trailing_space)
|
+ self.roi_space + self.stoploss_space + self.trailing_space)
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ HyperOptAuto class.
|
|||||||
This module implements a convenience auto-hyperopt class, which can be used together with strategies
|
This module implements a convenience auto-hyperopt class, which can be used together with strategies
|
||||||
that implement IHyperStrategy interface.
|
that implement IHyperStrategy interface.
|
||||||
"""
|
"""
|
||||||
|
import logging
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from typing import Callable, Dict, List
|
from typing import Callable, Dict, List
|
||||||
|
|
||||||
@ -15,12 +16,19 @@ with suppress(ImportError):
|
|||||||
from freqtrade.optimize.hyperopt_interface import EstimatorType, IHyperOpt
|
from freqtrade.optimize.hyperopt_interface import EstimatorType, IHyperOpt
|
||||||
|
|
||||||
|
|
||||||
def _format_exception_message(space: str) -> str:
|
logger = logging.getLogger(__name__)
|
||||||
raise OperationalException(
|
|
||||||
f"The '{space}' space is included into the hyperoptimization "
|
|
||||||
f"but no parameter for this space was not found in your Strategy. "
|
def _format_exception_message(space: str, ignore_missing_space: bool) -> None:
|
||||||
f"Please make sure to have parameters for this space enabled for optimization "
|
msg = (f"The '{space}' space is included into the hyperoptimization "
|
||||||
f"or remove the '{space}' space from hyperoptimization.")
|
f"but no parameter for this space was not found in your Strategy. "
|
||||||
|
)
|
||||||
|
if ignore_missing_space:
|
||||||
|
logger.warning(msg + "This space will be ignored.")
|
||||||
|
else:
|
||||||
|
raise OperationalException(
|
||||||
|
msg + f"Please make sure to have parameters for this space enabled for optimization "
|
||||||
|
f"or remove the '{space}' space from hyperoptimization.")
|
||||||
|
|
||||||
|
|
||||||
class HyperOptAuto(IHyperOpt):
|
class HyperOptAuto(IHyperOpt):
|
||||||
@ -48,13 +56,16 @@ class HyperOptAuto(IHyperOpt):
|
|||||||
if attr.optimize:
|
if attr.optimize:
|
||||||
yield attr.get_space(attr_name)
|
yield attr.get_space(attr_name)
|
||||||
|
|
||||||
def _get_indicator_space(self, category):
|
def _get_indicator_space(self, category) -> List:
|
||||||
# TODO: is this necessary, or can we call "generate_space" directly?
|
# TODO: is this necessary, or can we call "generate_space" directly?
|
||||||
indicator_space = list(self._generate_indicator_space(category))
|
indicator_space = list(self._generate_indicator_space(category))
|
||||||
if len(indicator_space) > 0:
|
if len(indicator_space) > 0:
|
||||||
return indicator_space
|
return indicator_space
|
||||||
else:
|
else:
|
||||||
_format_exception_message(category)
|
_format_exception_message(
|
||||||
|
category,
|
||||||
|
self.config.get("hyperopt_ignore_missing_space", False))
|
||||||
|
return []
|
||||||
|
|
||||||
def buy_indicator_space(self) -> List['Dimension']:
|
def buy_indicator_space(self) -> List['Dimension']:
|
||||||
return self._get_indicator_space('buy')
|
return self._get_indicator_space('buy')
|
||||||
|
@ -506,7 +506,6 @@ class LocalTrade():
|
|||||||
lower_stop = new_loss < self.stop_loss
|
lower_stop = new_loss < self.stop_loss
|
||||||
|
|
||||||
# stop losses only walk up, never down!,
|
# stop losses only walk up, never down!,
|
||||||
# TODO-lev
|
|
||||||
# ? But adding more to a leveraged trade would create a lower liquidation price,
|
# ? But adding more to a leveraged trade would create a lower liquidation price,
|
||||||
# ? decreasing the minimum stoploss
|
# ? decreasing the minimum stoploss
|
||||||
if (higher_stop and not self.is_short) or (lower_stop and self.is_short):
|
if (higher_stop and not self.is_short) or (lower_stop and self.is_short):
|
||||||
|
@ -21,6 +21,7 @@ class PerformanceFilter(IPairList):
|
|||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||||
|
|
||||||
self._minutes = pairlistconfig.get('minutes', 0)
|
self._minutes = pairlistconfig.get('minutes', 0)
|
||||||
|
self._min_profit = pairlistconfig.get('min_profit', None)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def needstickers(self) -> bool:
|
def needstickers(self) -> bool:
|
||||||
@ -68,6 +69,14 @@ class PerformanceFilter(IPairList):
|
|||||||
sorted_df = list_df.merge(performance, on='pair', how='left')\
|
sorted_df = list_df.merge(performance, on='pair', how='left')\
|
||||||
.fillna(0).sort_values(by=['count', 'pair'], ascending=True)\
|
.fillna(0).sort_values(by=['count', 'pair'], ascending=True)\
|
||||||
.sort_values(by=['profit'], ascending=False)
|
.sort_values(by=['profit'], ascending=False)
|
||||||
|
if self._min_profit is not None:
|
||||||
|
removed = sorted_df[sorted_df['profit'] < self._min_profit]
|
||||||
|
for _, row in removed.iterrows():
|
||||||
|
self.log_once(
|
||||||
|
f"Removing pair {row['pair']} since {row['profit']} is "
|
||||||
|
f"below {self._min_profit}", logger.info)
|
||||||
|
sorted_df = sorted_df[sorted_df['profit'] >= self._min_profit]
|
||||||
|
|
||||||
pairlist = sorted_df['pair'].tolist()
|
pairlist = sorted_df['pair'].tolist()
|
||||||
|
|
||||||
return pairlist
|
return pairlist
|
||||||
|
@ -4,9 +4,9 @@ Static Pair List provider
|
|||||||
Provides pair white list as it configured in config
|
Provides pair white list as it configured in config
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
|
from copy import deepcopy
|
||||||
from typing import Any, Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from freqtrade.exceptions import OperationalException
|
|
||||||
from freqtrade.plugins.pairlist.IPairList import IPairList
|
from freqtrade.plugins.pairlist.IPairList import IPairList
|
||||||
|
|
||||||
|
|
||||||
@ -20,10 +20,6 @@ class StaticPairList(IPairList):
|
|||||||
pairlist_pos: int) -> None:
|
pairlist_pos: int) -> None:
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||||
|
|
||||||
if self._pairlist_pos != 0:
|
|
||||||
raise OperationalException(f"{self.name} can only be used in the first position "
|
|
||||||
"in the list of Pairlist Handlers.")
|
|
||||||
|
|
||||||
self._allow_inactive = self._pairlistconfig.get('allow_inactive', False)
|
self._allow_inactive = self._pairlistconfig.get('allow_inactive', False)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -64,4 +60,8 @@ class StaticPairList(IPairList):
|
|||||||
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
|
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
|
||||||
:return: new whitelist
|
:return: new whitelist
|
||||||
"""
|
"""
|
||||||
return pairlist
|
pairlist_ = deepcopy(pairlist)
|
||||||
|
for pair in self._config['exchange']['pair_whitelist']:
|
||||||
|
if pair not in pairlist_:
|
||||||
|
pairlist_.append(pair)
|
||||||
|
return pairlist_
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Iterator, Optional
|
||||||
|
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc.rpc import RPC, RPCException
|
from freqtrade.rpc.rpc import RPC, RPCException
|
||||||
|
|
||||||
from .webserver import ApiServer
|
from .webserver import ApiServer
|
||||||
@ -11,10 +12,12 @@ def get_rpc_optional() -> Optional[RPC]:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_rpc() -> Optional[RPC]:
|
def get_rpc() -> Optional[Iterator[RPC]]:
|
||||||
_rpc = get_rpc_optional()
|
_rpc = get_rpc_optional()
|
||||||
if _rpc:
|
if _rpc:
|
||||||
return _rpc
|
Trade.query.session.rollback()
|
||||||
|
yield _rpc
|
||||||
|
Trade.query.session.rollback()
|
||||||
else:
|
else:
|
||||||
raise RPCException('Bot is not in the correct state')
|
raise RPCException('Bot is not in the correct state')
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ from freqtrade.constants import DUST_PER_COIN
|
|||||||
from freqtrade.enums import RPCMessageType
|
from freqtrade.enums import RPCMessageType
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.misc import chunks, plural, round_coin_value
|
from freqtrade.misc import chunks, plural, round_coin_value
|
||||||
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc import RPC, RPCException, RPCHandler
|
from freqtrade.rpc import RPC, RPCException, RPCHandler
|
||||||
|
|
||||||
|
|
||||||
@ -59,7 +60,8 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
|
|||||||
update.message.chat_id
|
update.message.chat_id
|
||||||
)
|
)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
# Rollback session to avoid getting data stored in a transaction.
|
||||||
|
Trade.query.session.rollback()
|
||||||
logger.debug(
|
logger.debug(
|
||||||
'Executing handler: %s for chat_id: %s',
|
'Executing handler: %s for chat_id: %s',
|
||||||
command_handler.__name__,
|
command_handler.__name__,
|
||||||
|
@ -840,28 +840,32 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
else:
|
else:
|
||||||
logger.warning("CustomStoploss function did not return valid stoploss")
|
logger.warning("CustomStoploss function did not return valid stoploss")
|
||||||
|
|
||||||
if self.trailing_stop and trade.stop_loss < (low or current_rate):
|
sl_lower_long = (trade.stop_loss < (low or current_rate) and not trade.is_short)
|
||||||
|
sl_higher_short = (trade.stop_loss > (high or current_rate) and trade.is_short)
|
||||||
|
if self.trailing_stop and (sl_lower_long or sl_higher_short):
|
||||||
# trailing stoploss handling
|
# trailing stoploss handling
|
||||||
sl_offset = self.trailing_stop_positive_offset
|
sl_offset = self.trailing_stop_positive_offset
|
||||||
|
|
||||||
# Make sure current_profit is calculated using high for backtesting.
|
# Make sure current_profit is calculated using high for backtesting.
|
||||||
# TODO-lev: Check this function - high / low usage must be inversed for short trades!
|
bound = low if trade.is_short else high
|
||||||
high_profit = current_profit if not high else trade.calc_profit_ratio(high)
|
bound_profit = current_profit if not bound else trade.calc_profit_ratio(bound)
|
||||||
|
|
||||||
# Don't update stoploss if trailing_only_offset_is_reached is true.
|
# Don't update stoploss if trailing_only_offset_is_reached is true.
|
||||||
if not (self.trailing_only_offset_is_reached and high_profit < sl_offset):
|
if not (self.trailing_only_offset_is_reached and bound_profit < sl_offset):
|
||||||
# Specific handling for trailing_stop_positive
|
# Specific handling for trailing_stop_positive
|
||||||
if self.trailing_stop_positive is not None and high_profit > sl_offset:
|
if self.trailing_stop_positive is not None and bound_profit > sl_offset:
|
||||||
stop_loss_value = self.trailing_stop_positive
|
stop_loss_value = self.trailing_stop_positive
|
||||||
logger.debug(f"{trade.pair} - Using positive stoploss: {stop_loss_value} "
|
logger.debug(f"{trade.pair} - Using positive stoploss: {stop_loss_value} "
|
||||||
f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%")
|
f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%")
|
||||||
|
|
||||||
trade.adjust_stop_loss(high or current_rate, stop_loss_value)
|
trade.adjust_stop_loss(bound or current_rate, stop_loss_value)
|
||||||
|
|
||||||
|
sl_higher_short = (trade.stop_loss >= (low or current_rate) and not trade.is_short)
|
||||||
|
sl_lower_long = ((trade.stop_loss <= (high or current_rate) and trade.is_short))
|
||||||
# evaluate if the stoploss was hit if stoploss is not on exchange
|
# evaluate if the stoploss was hit if stoploss is not on exchange
|
||||||
# in Dry-Run, this handles stoploss logic as well, as the logic will not be different to
|
# in Dry-Run, this handles stoploss logic as well, as the logic will not be different to
|
||||||
# regular stoploss handling.
|
# regular stoploss handling.
|
||||||
if ((trade.stop_loss >= (low or current_rate)) and
|
if ((sl_higher_short or sl_lower_long) and
|
||||||
(not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])):
|
(not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])):
|
||||||
|
|
||||||
sell_type = SellType.STOP_LOSS
|
sell_type = SellType.STOP_LOSS
|
||||||
@ -870,12 +874,18 @@ class IStrategy(ABC, HyperStrategyMixin):
|
|||||||
if trade.initial_stop_loss != trade.stop_loss:
|
if trade.initial_stop_loss != trade.stop_loss:
|
||||||
sell_type = SellType.TRAILING_STOP_LOSS
|
sell_type = SellType.TRAILING_STOP_LOSS
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"{trade.pair} - HIT STOP: current price at {(low or current_rate):.6f}, "
|
f"{trade.pair} - HIT STOP: current price at "
|
||||||
|
f"{((high if trade.is_short else low) or current_rate):.6f}, "
|
||||||
f"stoploss is {trade.stop_loss:.6f}, "
|
f"stoploss is {trade.stop_loss:.6f}, "
|
||||||
f"initial stoploss was at {trade.initial_stop_loss:.6f}, "
|
f"initial stoploss was at {trade.initial_stop_loss:.6f}, "
|
||||||
f"trade opened at {trade.open_rate:.6f}")
|
f"trade opened at {trade.open_rate:.6f}")
|
||||||
|
new_stoploss = (
|
||||||
|
trade.stop_loss + trade.initial_stop_loss
|
||||||
|
if trade.is_short else
|
||||||
|
trade.stop_loss - trade.initial_stop_loss
|
||||||
|
)
|
||||||
logger.debug(f"{trade.pair} - Trailing stop saved "
|
logger.debug(f"{trade.pair} - Trailing stop saved "
|
||||||
f"{trade.stop_loss - trade.initial_stop_loss:.6f}")
|
f"{new_stoploss:.6f}")
|
||||||
|
|
||||||
return SellCheckTuple(sell_type=sell_type)
|
return SellCheckTuple(sell_type=sell_type)
|
||||||
|
|
||||||
|
@ -58,6 +58,8 @@ class SampleStrategy(IStrategy):
|
|||||||
# Hyperoptable parameters
|
# Hyperoptable parameters
|
||||||
buy_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True)
|
buy_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True)
|
||||||
sell_rsi = IntParameter(low=50, high=100, default=70, space='sell', optimize=True, load=True)
|
sell_rsi = IntParameter(low=50, high=100, default=70, space='sell', optimize=True, load=True)
|
||||||
|
short_rsi = IntParameter(low=51, high=100, default=70, space='sell', optimize=True, load=True)
|
||||||
|
exit_short_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True)
|
||||||
|
|
||||||
# Optimal timeframe for the strategy.
|
# Optimal timeframe for the strategy.
|
||||||
timeframe = '5m'
|
timeframe = '5m'
|
||||||
@ -354,6 +356,16 @@ class SampleStrategy(IStrategy):
|
|||||||
),
|
),
|
||||||
'enter_long'] = 1
|
'enter_long'] = 1
|
||||||
|
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
# Signal: RSI crosses above 70
|
||||||
|
(qtpylib.crossed_above(dataframe['rsi'], self.short_rsi.value)) &
|
||||||
|
(dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle
|
||||||
|
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling
|
||||||
|
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
||||||
|
),
|
||||||
|
'enter_short'] = 1
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||||
@ -371,5 +383,18 @@ class SampleStrategy(IStrategy):
|
|||||||
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling
|
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling
|
||||||
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
||||||
),
|
),
|
||||||
|
|
||||||
'exit_long'] = 1
|
'exit_long'] = 1
|
||||||
|
|
||||||
|
dataframe.loc[
|
||||||
|
(
|
||||||
|
# Signal: RSI crosses above 30
|
||||||
|
(qtpylib.crossed_above(dataframe['rsi'], self.exit_short_rsi.value)) &
|
||||||
|
# Guard: tema below BB middle
|
||||||
|
(dataframe['tema'] <= dataframe['bb_middleband']) &
|
||||||
|
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising
|
||||||
|
(dataframe['volume'] > 0) # Make sure Volume is not 0
|
||||||
|
),
|
||||||
|
'exit_short'] = 1
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
10
freqtrade/vendor/qtpylib/indicators.py
vendored
10
freqtrade/vendor/qtpylib/indicators.py
vendored
@ -339,11 +339,13 @@ def vwap(bars):
|
|||||||
(input can be pandas series or numpy array)
|
(input can be pandas series or numpy array)
|
||||||
bars are usually mid [ (h+l)/2 ] or typical [ (h+l+c)/3 ]
|
bars are usually mid [ (h+l)/2 ] or typical [ (h+l+c)/3 ]
|
||||||
"""
|
"""
|
||||||
typical = ((bars['high'] + bars['low'] + bars['close']) / 3).values
|
raise ValueError("using `qtpylib.vwap` facilitates lookahead bias. Please use "
|
||||||
volume = bars['volume'].values
|
"`qtpylib.rolling_vwap` instead, which calculates vwap in a rolling manner.")
|
||||||
|
# typical = ((bars['high'] + bars['low'] + bars['close']) / 3).values
|
||||||
|
# volume = bars['volume'].values
|
||||||
|
|
||||||
return pd.Series(index=bars.index,
|
# return pd.Series(index=bars.index,
|
||||||
data=np.cumsum(volume * typical) / np.cumsum(volume))
|
# data=np.cumsum(volume * typical) / np.cumsum(volume))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
|
@ -55,8 +55,8 @@ theme:
|
|||||||
primary: "blue grey"
|
primary: "blue grey"
|
||||||
accent: "tear"
|
accent: "tear"
|
||||||
toggle:
|
toggle:
|
||||||
icon: material/toggle-switch-off-outline
|
icon: material/toggle-switch
|
||||||
name: Switch to dark mode
|
name: Switch to light mode
|
||||||
extra_css:
|
extra_css:
|
||||||
- "stylesheets/ft.extra.css"
|
- "stylesheets/ft.extra.css"
|
||||||
extra_javascript:
|
extra_javascript:
|
||||||
|
@ -4,11 +4,11 @@
|
|||||||
-r requirements-hyperopt.txt
|
-r requirements-hyperopt.txt
|
||||||
|
|
||||||
coveralls==3.2.0
|
coveralls==3.2.0
|
||||||
flake8==4.0.0
|
flake8==4.0.1
|
||||||
flake8-tidy-imports==4.5.0
|
flake8-tidy-imports==4.5.0
|
||||||
mypy==0.910
|
mypy==0.910
|
||||||
pytest==6.2.5
|
pytest==6.2.5
|
||||||
pytest-asyncio==0.15.1
|
pytest-asyncio==0.16.0
|
||||||
pytest-cov==3.0.0
|
pytest-cov==3.0.0
|
||||||
pytest-mock==3.6.1
|
pytest-mock==3.6.1
|
||||||
pytest-random-order==1.0.4
|
pytest-random-order==1.0.4
|
||||||
@ -20,7 +20,7 @@ time-machine==2.4.0
|
|||||||
nbconvert==6.2.0
|
nbconvert==6.2.0
|
||||||
|
|
||||||
# mypy types
|
# mypy types
|
||||||
types-cachetools==4.2.2
|
types-cachetools==4.2.4
|
||||||
types-filelock==3.2.0
|
types-filelock==3.2.1
|
||||||
types-requests==2.25.9
|
types-requests==2.25.11
|
||||||
types-tabulate==0.8.2
|
types-tabulate==0.8.3
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
scipy==1.7.1
|
scipy==1.7.1
|
||||||
scikit-learn==1.0
|
scikit-learn==1.0
|
||||||
scikit-optimize==0.9.0
|
scikit-optimize==0.9.0
|
||||||
filelock==3.3.0
|
filelock==3.3.1
|
||||||
joblib==1.1.0
|
joblib==1.1.0
|
||||||
psutil==5.8.0
|
psutil==5.8.0
|
||||||
progressbar2==3.53.3
|
progressbar2==3.55.0
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
numpy==1.21.2
|
numpy==1.21.2
|
||||||
pandas==1.3.3
|
pandas==1.3.4
|
||||||
pandas-ta==0.3.14b
|
pandas-ta==0.3.14b
|
||||||
|
|
||||||
ccxt==1.57.94
|
ccxt==1.58.47
|
||||||
# Pin cryptography for now due to rust build errors with piwheels
|
# Pin cryptography for now due to rust build errors with piwheels
|
||||||
cryptography==35.0.0
|
cryptography==35.0.0
|
||||||
aiohttp==3.7.4.post0
|
aiohttp==3.7.4.post0
|
||||||
@ -12,7 +12,6 @@ arrow==1.2.0
|
|||||||
cachetools==4.2.2
|
cachetools==4.2.2
|
||||||
requests==2.26.0
|
requests==2.26.0
|
||||||
urllib3==1.26.7
|
urllib3==1.26.7
|
||||||
wrapt==1.13.1
|
|
||||||
jsonschema==4.1.0
|
jsonschema==4.1.0
|
||||||
TA-Lib==0.4.21
|
TA-Lib==0.4.21
|
||||||
technical==1.3.0
|
technical==1.3.0
|
||||||
@ -26,15 +25,15 @@ blosc==1.10.6
|
|||||||
py_find_1st==1.1.5
|
py_find_1st==1.1.5
|
||||||
|
|
||||||
# Load ticker files 30% faster
|
# Load ticker files 30% faster
|
||||||
python-rapidjson==1.4
|
python-rapidjson==1.5
|
||||||
|
|
||||||
# Notify systemd
|
# Notify systemd
|
||||||
sdnotify==0.3.2
|
sdnotify==0.3.2
|
||||||
|
|
||||||
# API Server
|
# API Server
|
||||||
fastapi==0.68.1
|
fastapi==0.70.0
|
||||||
uvicorn==0.15.0
|
uvicorn==0.15.0
|
||||||
pyjwt==2.2.0
|
pyjwt==2.3.0
|
||||||
aiofiles==0.7.0
|
aiofiles==0.7.0
|
||||||
psutil==5.8.0
|
psutil==5.8.0
|
||||||
|
|
||||||
|
2
setup.py
2
setup.py
@ -16,7 +16,6 @@ hyperopt = [
|
|||||||
develop = [
|
develop = [
|
||||||
'coveralls',
|
'coveralls',
|
||||||
'flake8',
|
'flake8',
|
||||||
'flake8-type-annotations',
|
|
||||||
'flake8-tidy-imports',
|
'flake8-tidy-imports',
|
||||||
'mypy',
|
'mypy',
|
||||||
'pytest',
|
'pytest',
|
||||||
@ -51,7 +50,6 @@ setup(
|
|||||||
'cachetools',
|
'cachetools',
|
||||||
'requests',
|
'requests',
|
||||||
'urllib3',
|
'urllib3',
|
||||||
'wrapt',
|
|
||||||
'jsonschema',
|
'jsonschema',
|
||||||
'TA-Lib',
|
'TA-Lib',
|
||||||
'pandas-ta',
|
'pandas-ta',
|
||||||
|
20
setup.sh
20
setup.sh
@ -30,7 +30,7 @@ function check_installed_python() {
|
|||||||
check_installed_pip
|
check_installed_pip
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "No usable python found. Please make sure to have python3.7 or newer installed"
|
echo "No usable python found. Please make sure to have python3.7 or newer installed"
|
||||||
exit 1
|
exit 1
|
||||||
@ -95,11 +95,19 @@ function install_talib() {
|
|||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
|
|
||||||
cd build_helpers && ./install_ta-lib.sh && cd ..
|
cd build_helpers && ./install_ta-lib.sh
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Quitting. Please fix the above error before continuing."
|
||||||
|
cd ..
|
||||||
|
exit 1
|
||||||
|
fi;
|
||||||
|
|
||||||
|
cd ..
|
||||||
}
|
}
|
||||||
|
|
||||||
function install_mac_newer_python_dependencies() {
|
function install_mac_newer_python_dependencies() {
|
||||||
|
|
||||||
if [ ! $(brew --prefix --installed hdf5 2>/dev/null) ]
|
if [ ! $(brew --prefix --installed hdf5 2>/dev/null) ]
|
||||||
then
|
then
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
@ -115,7 +123,7 @@ function install_mac_newer_python_dependencies() {
|
|||||||
echo "Installing c-blosc"
|
echo "Installing c-blosc"
|
||||||
echo "-------------------------"
|
echo "-------------------------"
|
||||||
brew install c-blosc
|
brew install c-blosc
|
||||||
fi
|
fi
|
||||||
export CBLOSC_DIR=$(brew --prefix)
|
export CBLOSC_DIR=$(brew --prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,7 +138,7 @@ function install_macos() {
|
|||||||
fi
|
fi
|
||||||
#Gets number after decimal in python version
|
#Gets number after decimal in python version
|
||||||
version=$(egrep -o 3.\[0-9\]+ <<< $PYTHON | sed 's/3.//g')
|
version=$(egrep -o 3.\[0-9\]+ <<< $PYTHON | sed 's/3.//g')
|
||||||
|
|
||||||
if [[ $version -ge 9 ]]; then #Checks if python version >= 3.9
|
if [[ $version -ge 9 ]]; then #Checks if python version >= 3.9
|
||||||
install_mac_newer_python_dependencies
|
install_mac_newer_python_dependencies
|
||||||
fi
|
fi
|
||||||
|
@ -754,6 +754,46 @@ def test_download_data_no_pairs(mocker, caplog):
|
|||||||
start_download_data(pargs)
|
start_download_data(pargs)
|
||||||
|
|
||||||
|
|
||||||
|
def test_download_data_all_pairs(mocker, markets):
|
||||||
|
|
||||||
|
mocker.patch.object(Path, "exists", MagicMock(return_value=False))
|
||||||
|
|
||||||
|
dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_ohlcv_data',
|
||||||
|
MagicMock(return_value=["ETH/BTC", "XRP/BTC"]))
|
||||||
|
patch_exchange(mocker)
|
||||||
|
mocker.patch(
|
||||||
|
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
|
||||||
|
)
|
||||||
|
args = [
|
||||||
|
"download-data",
|
||||||
|
"--exchange",
|
||||||
|
"binance",
|
||||||
|
"--pairs",
|
||||||
|
".*/USDT"
|
||||||
|
]
|
||||||
|
pargs = get_args(args)
|
||||||
|
pargs['config'] = None
|
||||||
|
start_download_data(pargs)
|
||||||
|
expected = set(['ETH/USDT', 'XRP/USDT', 'NEO/USDT', 'TKN/USDT'])
|
||||||
|
assert set(dl_mock.call_args_list[0][1]['pairs']) == expected
|
||||||
|
assert dl_mock.call_count == 1
|
||||||
|
|
||||||
|
dl_mock.reset_mock()
|
||||||
|
args = [
|
||||||
|
"download-data",
|
||||||
|
"--exchange",
|
||||||
|
"binance",
|
||||||
|
"--pairs",
|
||||||
|
".*/USDT",
|
||||||
|
"--include-inactive-pairs",
|
||||||
|
]
|
||||||
|
pargs = get_args(args)
|
||||||
|
pargs['config'] = None
|
||||||
|
start_download_data(pargs)
|
||||||
|
expected = set(['ETH/USDT', 'LTC/USDT', 'XRP/USDT', 'NEO/USDT', 'TKN/USDT'])
|
||||||
|
assert set(dl_mock.call_args_list[0][1]['pairs']) == expected
|
||||||
|
|
||||||
|
|
||||||
def test_download_data_trades(mocker, caplog):
|
def test_download_data_trades(mocker, caplog):
|
||||||
dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_trades_data',
|
dl_mock = mocker.patch('freqtrade.commands.data_commands.refresh_backtest_trades_data',
|
||||||
MagicMock(return_value=[]))
|
MagicMock(return_value=[]))
|
||||||
@ -903,7 +943,7 @@ def test_hyperopt_list(mocker, capsys, caplog, saved_hyperopt_results, tmpdir):
|
|||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.optimize.hyperopt_tools.HyperoptTools._test_hyperopt_results_exist',
|
'freqtrade.optimize.hyperopt_tools.HyperoptTools._test_hyperopt_results_exist',
|
||||||
return_value=True
|
return_value=True
|
||||||
)
|
)
|
||||||
|
|
||||||
def fake_iterator(*args, **kwargs):
|
def fake_iterator(*args, **kwargs):
|
||||||
yield from [saved_hyperopt_results]
|
yield from [saved_hyperopt_results]
|
||||||
@ -1309,9 +1349,10 @@ def test_start_list_data(testdatadir, capsys):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
# TODO-lev: Short trades?
|
||||||
def test_show_trades(mocker, fee, capsys, caplog):
|
def test_show_trades(mocker, fee, capsys, caplog):
|
||||||
mocker.patch("freqtrade.persistence.init_db")
|
mocker.patch("freqtrade.persistence.init_db")
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee, False)
|
||||||
args = [
|
args = [
|
||||||
"show-trades",
|
"show-trades",
|
||||||
"--db-url",
|
"--db-url",
|
||||||
|
@ -209,8 +209,14 @@ def get_patched_worker(mocker, config) -> Worker:
|
|||||||
return Worker(args=None, config=config)
|
return Worker(args=None, config=config)
|
||||||
|
|
||||||
|
|
||||||
def patch_get_signal(freqtrade: FreqtradeBot, enter_long=True, exit_long=False,
|
def patch_get_signal(
|
||||||
enter_short=False, exit_short=False, enter_tag: Optional[str] = None) -> None:
|
freqtrade: FreqtradeBot,
|
||||||
|
enter_long=True,
|
||||||
|
exit_long=False,
|
||||||
|
enter_short=False,
|
||||||
|
exit_short=False,
|
||||||
|
enter_tag: Optional[str] = None
|
||||||
|
) -> None:
|
||||||
"""
|
"""
|
||||||
:param mocker: mocker to patch IStrategy class
|
:param mocker: mocker to patch IStrategy class
|
||||||
:param value: which value IStrategy.get_signal() must return
|
:param value: which value IStrategy.get_signal() must return
|
||||||
@ -241,7 +247,7 @@ def patch_get_signal(freqtrade: FreqtradeBot, enter_long=True, exit_long=False,
|
|||||||
freqtrade.exchange.refresh_latest_ohlcv = lambda p: None
|
freqtrade.exchange.refresh_latest_ohlcv = lambda p: None
|
||||||
|
|
||||||
|
|
||||||
def create_mock_trades(fee, use_db: bool = True):
|
def create_mock_trades(fee, is_short: bool, use_db: bool = True):
|
||||||
"""
|
"""
|
||||||
Create some fake trades ...
|
Create some fake trades ...
|
||||||
"""
|
"""
|
||||||
@ -252,26 +258,26 @@ def create_mock_trades(fee, use_db: bool = True):
|
|||||||
LocalTrade.add_bt_trade(trade)
|
LocalTrade.add_bt_trade(trade)
|
||||||
|
|
||||||
# Simulate dry_run entries
|
# Simulate dry_run entries
|
||||||
trade = mock_trade_1(fee)
|
trade = mock_trade_1(fee, is_short)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_2(fee)
|
trade = mock_trade_2(fee, is_short)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_3(fee)
|
trade = mock_trade_3(fee, is_short)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_4(fee)
|
trade = mock_trade_4(fee, is_short)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_5(fee)
|
trade = mock_trade_5(fee, is_short)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_6(fee)
|
trade = mock_trade_6(fee, is_short)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
if use_db:
|
if use_db:
|
||||||
Trade.query.session.flush()
|
Trade.commit()
|
||||||
|
|
||||||
|
|
||||||
def create_mock_trades_with_leverage(fee, use_db: bool = True):
|
def create_mock_trades_with_leverage(fee, use_db: bool = True):
|
||||||
@ -285,22 +291,22 @@ def create_mock_trades_with_leverage(fee, use_db: bool = True):
|
|||||||
LocalTrade.add_bt_trade(trade)
|
LocalTrade.add_bt_trade(trade)
|
||||||
|
|
||||||
# Simulate dry_run entries
|
# Simulate dry_run entries
|
||||||
trade = mock_trade_1(fee)
|
trade = mock_trade_1(fee, False)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_2(fee)
|
trade = mock_trade_2(fee, False)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_3(fee)
|
trade = mock_trade_3(fee, False)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_4(fee)
|
trade = mock_trade_4(fee, False)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_5(fee)
|
trade = mock_trade_5(fee, False)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = mock_trade_6(fee)
|
trade = mock_trade_6(fee, False)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
trade = short_trade(fee)
|
trade = short_trade(fee)
|
||||||
@ -323,7 +329,7 @@ def create_mock_trades_usdt(fee, use_db: bool = True):
|
|||||||
else:
|
else:
|
||||||
LocalTrade.add_bt_trade(trade)
|
LocalTrade.add_bt_trade(trade)
|
||||||
|
|
||||||
# Simulate dry_run entries
|
# Simulate dry_run entries
|
||||||
trade = mock_trade_usdt_1(fee)
|
trade = mock_trade_usdt_1(fee)
|
||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
@ -343,7 +349,7 @@ def create_mock_trades_usdt(fee, use_db: bool = True):
|
|||||||
add_trade(trade)
|
add_trade(trade)
|
||||||
|
|
||||||
if use_db:
|
if use_db:
|
||||||
Trade.query.session.flush()
|
Trade.commit()
|
||||||
|
|
||||||
|
|
||||||
def get_sides(is_short: bool) -> Tuple[str, str]:
|
def get_sides(is_short: bool) -> Tuple[str, str]:
|
||||||
@ -2295,6 +2301,7 @@ def limit_sell_order_usdt_open():
|
|||||||
'timestamp': arrow.utcnow().int_timestamp,
|
'timestamp': arrow.utcnow().int_timestamp,
|
||||||
'price': 2.20,
|
'price': 2.20,
|
||||||
'amount': 30.0,
|
'amount': 30.0,
|
||||||
|
'cost': 66.0,
|
||||||
'filled': 0.0,
|
'filled': 0.0,
|
||||||
'remaining': 30.0,
|
'remaining': 30.0,
|
||||||
'status': 'open'
|
'status': 'open'
|
||||||
@ -2340,3 +2347,27 @@ def market_sell_order_usdt():
|
|||||||
'remaining': 0.0,
|
'remaining': 0.0,
|
||||||
'status': 'closed'
|
'status': 'closed'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def limit_order(limit_buy_order_usdt, limit_sell_order_usdt):
|
||||||
|
return {
|
||||||
|
'buy': limit_buy_order_usdt,
|
||||||
|
'sell': limit_sell_order_usdt
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def market_order(market_buy_order_usdt, market_sell_order_usdt):
|
||||||
|
return {
|
||||||
|
'buy': market_buy_order_usdt,
|
||||||
|
'sell': market_sell_order_usdt
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='function')
|
||||||
|
def limit_order_open(limit_buy_order_usdt_open, limit_sell_order_usdt_open):
|
||||||
|
return {
|
||||||
|
'buy': limit_buy_order_usdt_open,
|
||||||
|
'sell': limit_sell_order_usdt_open
|
||||||
|
}
|
||||||
|
@ -6,12 +6,24 @@ from freqtrade.persistence.models import Order, Trade
|
|||||||
MOCK_TRADE_COUNT = 6
|
MOCK_TRADE_COUNT = 6
|
||||||
|
|
||||||
|
|
||||||
def mock_order_1():
|
def enter_side(is_short: bool):
|
||||||
|
return "sell" if is_short else "buy"
|
||||||
|
|
||||||
|
|
||||||
|
def exit_side(is_short: bool):
|
||||||
|
return "buy" if is_short else "sell"
|
||||||
|
|
||||||
|
|
||||||
|
def direc(is_short: bool):
|
||||||
|
return "short" if is_short else "long"
|
||||||
|
|
||||||
|
|
||||||
|
def mock_order_1(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': '1234',
|
'id': f'1234_{direc(is_short)}',
|
||||||
'symbol': 'ETH/BTC',
|
'symbol': 'ETH/BTC',
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'side': 'buy',
|
'side': enter_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 0.123,
|
'price': 0.123,
|
||||||
'amount': 123.0,
|
'amount': 123.0,
|
||||||
@ -20,7 +32,7 @@ def mock_order_1():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def mock_trade_1(fee):
|
def mock_trade_1(fee, is_short: bool):
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='ETH/BTC',
|
pair='ETH/BTC',
|
||||||
stake_amount=0.001,
|
stake_amount=0.001,
|
||||||
@ -32,21 +44,22 @@ def mock_trade_1(fee):
|
|||||||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17),
|
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=17),
|
||||||
open_rate=0.123,
|
open_rate=0.123,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
open_order_id='dry_run_buy_12345',
|
open_order_id=f'dry_run_buy_{direc(is_short)}_12345',
|
||||||
strategy='StrategyTestV3',
|
strategy='StrategyTestV3',
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
|
is_short=is_short
|
||||||
)
|
)
|
||||||
o = Order.parse_from_ccxt_object(mock_order_1(), 'ETH/BTC', 'buy')
|
o = Order.parse_from_ccxt_object(mock_order_1(is_short), 'ETH/BTC', enter_side(is_short))
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
|
|
||||||
def mock_order_2():
|
def mock_order_2(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': '1235',
|
'id': f'1235_{direc(is_short)}',
|
||||||
'symbol': 'ETC/BTC',
|
'symbol': 'ETC/BTC',
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'side': 'buy',
|
'side': enter_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 0.123,
|
'price': 0.123,
|
||||||
'amount': 123.0,
|
'amount': 123.0,
|
||||||
@ -55,12 +68,12 @@ def mock_order_2():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def mock_order_2_sell():
|
def mock_order_2_sell(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': '12366',
|
'id': f'12366_{direc(is_short)}',
|
||||||
'symbol': 'ETC/BTC',
|
'symbol': 'ETC/BTC',
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'side': 'sell',
|
'side': exit_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 0.128,
|
'price': 0.128,
|
||||||
'amount': 123.0,
|
'amount': 123.0,
|
||||||
@ -69,7 +82,7 @@ def mock_order_2_sell():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def mock_trade_2(fee):
|
def mock_trade_2(fee, is_short: bool):
|
||||||
"""
|
"""
|
||||||
Closed trade...
|
Closed trade...
|
||||||
"""
|
"""
|
||||||
@ -82,30 +95,31 @@ def mock_trade_2(fee):
|
|||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
open_rate=0.123,
|
open_rate=0.123,
|
||||||
close_rate=0.128,
|
close_rate=0.128,
|
||||||
close_profit=0.005,
|
close_profit=-0.005 if is_short else 0.005,
|
||||||
close_profit_abs=0.000584127,
|
close_profit_abs=-0.005584127 if is_short else 0.000584127,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
is_open=False,
|
is_open=False,
|
||||||
open_order_id='dry_run_sell_12345',
|
open_order_id=f'dry_run_sell_{direc(is_short)}_12345',
|
||||||
strategy='StrategyTestV3',
|
strategy='StrategyTestV3',
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
sell_reason='sell_signal',
|
sell_reason='sell_signal',
|
||||||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
||||||
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
|
close_date=datetime.now(tz=timezone.utc) - timedelta(minutes=2),
|
||||||
|
is_short=is_short
|
||||||
)
|
)
|
||||||
o = Order.parse_from_ccxt_object(mock_order_2(), 'ETC/BTC', 'buy')
|
o = Order.parse_from_ccxt_object(mock_order_2(is_short), 'ETC/BTC', enter_side(is_short))
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
o = Order.parse_from_ccxt_object(mock_order_2_sell(), 'ETC/BTC', 'sell')
|
o = Order.parse_from_ccxt_object(mock_order_2_sell(is_short), 'ETC/BTC', exit_side(is_short))
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
|
|
||||||
def mock_order_3():
|
def mock_order_3(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': '41231a12a',
|
'id': f'41231a12a_{direc(is_short)}',
|
||||||
'symbol': 'XRP/BTC',
|
'symbol': 'XRP/BTC',
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'side': 'buy',
|
'side': enter_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 0.05,
|
'price': 0.05,
|
||||||
'amount': 123.0,
|
'amount': 123.0,
|
||||||
@ -114,12 +128,12 @@ def mock_order_3():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def mock_order_3_sell():
|
def mock_order_3_sell(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': '41231a666a',
|
'id': f'41231a666a_{direc(is_short)}',
|
||||||
'symbol': 'XRP/BTC',
|
'symbol': 'XRP/BTC',
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'side': 'sell',
|
'side': exit_side(is_short),
|
||||||
'type': 'stop_loss_limit',
|
'type': 'stop_loss_limit',
|
||||||
'price': 0.06,
|
'price': 0.06,
|
||||||
'average': 0.06,
|
'average': 0.06,
|
||||||
@ -129,7 +143,7 @@ def mock_order_3_sell():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def mock_trade_3(fee):
|
def mock_trade_3(fee, is_short: bool):
|
||||||
"""
|
"""
|
||||||
Closed trade
|
Closed trade
|
||||||
"""
|
"""
|
||||||
@ -142,8 +156,8 @@ def mock_trade_3(fee):
|
|||||||
fee_close=fee.return_value,
|
fee_close=fee.return_value,
|
||||||
open_rate=0.05,
|
open_rate=0.05,
|
||||||
close_rate=0.06,
|
close_rate=0.06,
|
||||||
close_profit=0.01,
|
close_profit=-0.01 if is_short else 0.01,
|
||||||
close_profit_abs=0.000155,
|
close_profit_abs=-0.001155 if is_short else 0.000155,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
is_open=False,
|
is_open=False,
|
||||||
strategy='StrategyTestV3',
|
strategy='StrategyTestV3',
|
||||||
@ -151,20 +165,21 @@ def mock_trade_3(fee):
|
|||||||
sell_reason='roi',
|
sell_reason='roi',
|
||||||
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
open_date=datetime.now(tz=timezone.utc) - timedelta(minutes=20),
|
||||||
close_date=datetime.now(tz=timezone.utc),
|
close_date=datetime.now(tz=timezone.utc),
|
||||||
|
is_short=is_short
|
||||||
)
|
)
|
||||||
o = Order.parse_from_ccxt_object(mock_order_3(), 'XRP/BTC', 'buy')
|
o = Order.parse_from_ccxt_object(mock_order_3(is_short), 'XRP/BTC', enter_side(is_short))
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
o = Order.parse_from_ccxt_object(mock_order_3_sell(), 'XRP/BTC', 'sell')
|
o = Order.parse_from_ccxt_object(mock_order_3_sell(is_short), 'XRP/BTC', exit_side(is_short))
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
|
|
||||||
def mock_order_4():
|
def mock_order_4(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': 'prod_buy_12345',
|
'id': f'prod_buy_{direc(is_short)}_12345',
|
||||||
'symbol': 'ETC/BTC',
|
'symbol': 'ETC/BTC',
|
||||||
'status': 'open',
|
'status': 'open',
|
||||||
'side': 'buy',
|
'side': enter_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 0.123,
|
'price': 0.123,
|
||||||
'amount': 123.0,
|
'amount': 123.0,
|
||||||
@ -173,7 +188,7 @@ def mock_order_4():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def mock_trade_4(fee):
|
def mock_trade_4(fee, is_short: bool):
|
||||||
"""
|
"""
|
||||||
Simulate prod entry
|
Simulate prod entry
|
||||||
"""
|
"""
|
||||||
@ -188,21 +203,22 @@ def mock_trade_4(fee):
|
|||||||
is_open=True,
|
is_open=True,
|
||||||
open_rate=0.123,
|
open_rate=0.123,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
open_order_id='prod_buy_12345',
|
open_order_id=f'prod_buy_{direc(is_short)}_12345',
|
||||||
strategy='StrategyTestV3',
|
strategy='StrategyTestV3',
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
|
is_short=is_short
|
||||||
)
|
)
|
||||||
o = Order.parse_from_ccxt_object(mock_order_4(), 'ETC/BTC', 'buy')
|
o = Order.parse_from_ccxt_object(mock_order_4(is_short), 'ETC/BTC', enter_side(is_short))
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
|
|
||||||
def mock_order_5():
|
def mock_order_5(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': 'prod_buy_3455',
|
'id': f'prod_buy_{direc(is_short)}_3455',
|
||||||
'symbol': 'XRP/BTC',
|
'symbol': 'XRP/BTC',
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'side': 'buy',
|
'side': enter_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 0.123,
|
'price': 0.123,
|
||||||
'amount': 123.0,
|
'amount': 123.0,
|
||||||
@ -211,12 +227,12 @@ def mock_order_5():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def mock_order_5_stoploss():
|
def mock_order_5_stoploss(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': 'prod_stoploss_3455',
|
'id': f'prod_stoploss_{direc(is_short)}_3455',
|
||||||
'symbol': 'XRP/BTC',
|
'symbol': 'XRP/BTC',
|
||||||
'status': 'open',
|
'status': 'open',
|
||||||
'side': 'sell',
|
'side': exit_side(is_short),
|
||||||
'type': 'stop_loss_limit',
|
'type': 'stop_loss_limit',
|
||||||
'price': 0.123,
|
'price': 0.123,
|
||||||
'amount': 123.0,
|
'amount': 123.0,
|
||||||
@ -225,7 +241,7 @@ def mock_order_5_stoploss():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def mock_trade_5(fee):
|
def mock_trade_5(fee, is_short: bool):
|
||||||
"""
|
"""
|
||||||
Simulate prod entry with stoploss
|
Simulate prod entry with stoploss
|
||||||
"""
|
"""
|
||||||
@ -241,22 +257,23 @@ def mock_trade_5(fee):
|
|||||||
open_rate=0.123,
|
open_rate=0.123,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
strategy='SampleStrategy',
|
strategy='SampleStrategy',
|
||||||
stoploss_order_id='prod_stoploss_3455',
|
stoploss_order_id=f'prod_stoploss_{direc(is_short)}_3455',
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
|
is_short=is_short
|
||||||
)
|
)
|
||||||
o = Order.parse_from_ccxt_object(mock_order_5(), 'XRP/BTC', 'buy')
|
o = Order.parse_from_ccxt_object(mock_order_5(is_short), 'XRP/BTC', enter_side(is_short))
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
o = Order.parse_from_ccxt_object(mock_order_5_stoploss(), 'XRP/BTC', 'stoploss')
|
o = Order.parse_from_ccxt_object(mock_order_5_stoploss(is_short), 'XRP/BTC', 'stoploss')
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
|
|
||||||
def mock_order_6():
|
def mock_order_6(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': 'prod_buy_6',
|
'id': f'prod_buy_{direc(is_short)}_6',
|
||||||
'symbol': 'LTC/BTC',
|
'symbol': 'LTC/BTC',
|
||||||
'status': 'closed',
|
'status': 'closed',
|
||||||
'side': 'buy',
|
'side': enter_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 0.15,
|
'price': 0.15,
|
||||||
'amount': 2.0,
|
'amount': 2.0,
|
||||||
@ -265,23 +282,23 @@ def mock_order_6():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def mock_order_6_sell():
|
def mock_order_6_sell(is_short: bool):
|
||||||
return {
|
return {
|
||||||
'id': 'prod_sell_6',
|
'id': f'prod_sell_{direc(is_short)}_6',
|
||||||
'symbol': 'LTC/BTC',
|
'symbol': 'LTC/BTC',
|
||||||
'status': 'open',
|
'status': 'open',
|
||||||
'side': 'sell',
|
'side': exit_side(is_short),
|
||||||
'type': 'limit',
|
'type': 'limit',
|
||||||
'price': 0.20,
|
'price': 0.15 if is_short else 0.20,
|
||||||
'amount': 2.0,
|
'amount': 2.0,
|
||||||
'filled': 0.0,
|
'filled': 0.0,
|
||||||
'remaining': 2.0,
|
'remaining': 2.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def mock_trade_6(fee):
|
def mock_trade_6(fee, is_short: bool):
|
||||||
"""
|
"""
|
||||||
Simulate prod entry with open sell order
|
Simulate prod entry with open exit order
|
||||||
"""
|
"""
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
pair='LTC/BTC',
|
pair='LTC/BTC',
|
||||||
@ -295,12 +312,12 @@ def mock_trade_6(fee):
|
|||||||
open_rate=0.15,
|
open_rate=0.15,
|
||||||
exchange='binance',
|
exchange='binance',
|
||||||
strategy='SampleStrategy',
|
strategy='SampleStrategy',
|
||||||
open_order_id="prod_sell_6",
|
open_order_id=f"prod_sell_{direc(is_short)}_6",
|
||||||
timeframe=5,
|
timeframe=5,
|
||||||
)
|
)
|
||||||
o = Order.parse_from_ccxt_object(mock_order_6(), 'LTC/BTC', 'buy')
|
o = Order.parse_from_ccxt_object(mock_order_6(is_short), 'LTC/BTC', enter_side(is_short))
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
o = Order.parse_from_ccxt_object(mock_order_6_sell(), 'LTC/BTC', 'sell')
|
o = Order.parse_from_ccxt_object(mock_order_6_sell(is_short), 'LTC/BTC', exit_side(is_short))
|
||||||
trade.orders.append(o)
|
trade.orders.append(o)
|
||||||
return trade
|
return trade
|
||||||
|
|
||||||
|
@ -111,9 +111,10 @@ def test_load_backtest_data_multi(testdatadir):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_load_trades_from_db(default_conf, fee, mocker):
|
@pytest.mark.parametrize('is_short', [False, True])
|
||||||
|
def test_load_trades_from_db(default_conf, fee, is_short, mocker):
|
||||||
|
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee, is_short)
|
||||||
# remove init so it does not init again
|
# remove init so it does not init again
|
||||||
init_mock = mocker.patch('freqtrade.data.btanalysis.init_db', MagicMock())
|
init_mock = mocker.patch('freqtrade.data.btanalysis.init_db', MagicMock())
|
||||||
|
|
||||||
|
@ -3248,6 +3248,33 @@ def test_validate_trading_mode_and_collateral(
|
|||||||
exchange.validate_trading_mode_and_collateral(trading_mode, collateral)
|
exchange.validate_trading_mode_and_collateral(trading_mode, collateral)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("exchange_name,trading_mode,ccxt_config", [
|
||||||
|
("binance", "spot", {}),
|
||||||
|
("binance", "margin", {"options": {"defaultType": "margin"}}),
|
||||||
|
("binance", "futures", {"options": {"defaultType": "future"}}),
|
||||||
|
("kraken", "spot", {}),
|
||||||
|
("kraken", "margin", {}),
|
||||||
|
("kraken", "futures", {}),
|
||||||
|
("ftx", "spot", {}),
|
||||||
|
("ftx", "margin", {}),
|
||||||
|
("ftx", "futures", {}),
|
||||||
|
("bittrex", "spot", {}),
|
||||||
|
("bittrex", "margin", {}),
|
||||||
|
("bittrex", "futures", {}),
|
||||||
|
])
|
||||||
|
def test__ccxt_config(
|
||||||
|
default_conf,
|
||||||
|
mocker,
|
||||||
|
exchange_name,
|
||||||
|
trading_mode,
|
||||||
|
ccxt_config
|
||||||
|
):
|
||||||
|
default_conf['trading_mode'] = trading_mode
|
||||||
|
default_conf['collateral'] = 'isolated'
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||||
|
assert exchange._ccxt_config == ccxt_config
|
||||||
|
|
||||||
|
|
||||||
def test_get_mark_price():
|
def test_get_mark_price():
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -164,6 +164,8 @@ def test_get_balances_prod(default_conf, mocker):
|
|||||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken",
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken",
|
||||||
"get_balances", "fetch_balance")
|
"get_balances", "fetch_balance")
|
||||||
|
|
||||||
|
# TODO-lev: All these stoploss tests with shorts
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('ordertype', ['market', 'limit'])
|
@pytest.mark.parametrize('ordertype', ['market', 'limit'])
|
||||||
@pytest.mark.parametrize('side,adjustedprice', [
|
@pytest.mark.parametrize('side,adjustedprice', [
|
||||||
|
@ -705,7 +705,7 @@ def test_simplified_interface_roi_stoploss(mocker, hyperopt_conf, capsys) -> Non
|
|||||||
assert hasattr(hyperopt, "position_stacking")
|
assert hasattr(hyperopt, "position_stacking")
|
||||||
|
|
||||||
|
|
||||||
def test_simplified_interface_all_failed(mocker, hyperopt_conf) -> None:
|
def test_simplified_interface_all_failed(mocker, hyperopt_conf, caplog) -> None:
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
|
mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
|
||||||
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
|
mocker.patch('freqtrade.optimize.hyperopt.file_dump_json')
|
||||||
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
|
mocker.patch('freqtrade.optimize.backtesting.Backtesting.load_bt_data',
|
||||||
@ -727,7 +727,13 @@ def test_simplified_interface_all_failed(mocker, hyperopt_conf) -> None:
|
|||||||
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
hyperopt.custom_hyperopt.generate_roi_table = MagicMock(return_value={})
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r"The 'protection' space is included into *"):
|
with pytest.raises(OperationalException, match=r"The 'protection' space is included into *"):
|
||||||
hyperopt.start()
|
hyperopt.init_spaces()
|
||||||
|
|
||||||
|
hyperopt.config['hyperopt_ignore_missing_space'] = True
|
||||||
|
caplog.clear()
|
||||||
|
hyperopt.init_spaces()
|
||||||
|
assert log_has_re(r"The 'protection' space is included into *", caplog)
|
||||||
|
assert hyperopt.protection_space == []
|
||||||
|
|
||||||
|
|
||||||
def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
|
def test_simplified_interface_buy(mocker, hyperopt_conf, capsys) -> None:
|
||||||
|
@ -415,10 +415,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
|||||||
# SpreadFilter only
|
# SpreadFilter only
|
||||||
([{"method": "SpreadFilter", "max_spread_ratio": 0.005}],
|
([{"method": "SpreadFilter", "max_spread_ratio": 0.005}],
|
||||||
"BTC", 'filter_at_the_beginning'), # OperationalException expected
|
"BTC", 'filter_at_the_beginning'), # OperationalException expected
|
||||||
# Static Pairlist after VolumePairList, on a non-first position
|
# Static Pairlist after VolumePairList, on a non-first position (appends pairs)
|
||||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
([{"method": "VolumePairList", "number_assets": 2, "sort_key": "quoteVolume"},
|
||||||
{"method": "StaticPairList"}],
|
{"method": "StaticPairList"}],
|
||||||
"BTC", 'static_in_the_middle'),
|
"BTC", ['ETH/BTC', 'TKN/BTC', 'TRST/BTC', 'SWT/BTC', 'BCC/BTC', 'HOT/BTC']),
|
||||||
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
|
([{"method": "VolumePairList", "number_assets": 20, "sort_key": "quoteVolume"},
|
||||||
{"method": "PriceFilter", "low_price_ratio": 0.02}],
|
{"method": "PriceFilter", "low_price_ratio": 0.02}],
|
||||||
"USDT", ['ETH/USDT', 'NANO/USDT']),
|
"USDT", ['ETH/USDT', 'NANO/USDT']),
|
||||||
@ -469,13 +469,6 @@ def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, t
|
|||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||||
|
|
||||||
if whitelist_result == 'static_in_the_middle':
|
|
||||||
with pytest.raises(OperationalException,
|
|
||||||
match=r"StaticPairList can only be used in the first position "
|
|
||||||
r"in the list of Pairlist Handlers."):
|
|
||||||
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
|
|
||||||
return
|
|
||||||
|
|
||||||
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
|
freqtrade = get_patched_freqtradebot(mocker, whitelist_conf)
|
||||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||||
get_tickers=tickers,
|
get_tickers=tickers,
|
||||||
@ -665,11 +658,11 @@ def test_PerformanceFilter_error(mocker, whitelist_conf, caplog) -> None:
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee) -> None:
|
def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee, caplog) -> None:
|
||||||
whitelist_conf['exchange']['pair_whitelist'].append('XRP/BTC')
|
whitelist_conf['exchange']['pair_whitelist'].append('XRP/BTC')
|
||||||
whitelist_conf['pairlists'] = [
|
whitelist_conf['pairlists'] = [
|
||||||
{"method": "StaticPairList"},
|
{"method": "StaticPairList"},
|
||||||
{"method": "PerformanceFilter", "minutes": 60}
|
{"method": "PerformanceFilter", "minutes": 60, "min_profit": 0.01}
|
||||||
]
|
]
|
||||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||||
exchange = get_patched_exchange(mocker, whitelist_conf)
|
exchange = get_patched_exchange(mocker, whitelist_conf)
|
||||||
@ -679,9 +672,10 @@ def test_PerformanceFilter_lookback(mocker, whitelist_conf, fee) -> None:
|
|||||||
assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC']
|
assert pm.whitelist == ['ETH/BTC', 'TKN/BTC', 'XRP/BTC']
|
||||||
|
|
||||||
with time_machine.travel("2021-09-01 05:00:00 +00:00") as t:
|
with time_machine.travel("2021-09-01 05:00:00 +00:00") as t:
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee, False)
|
||||||
pm.refresh_pairlist()
|
pm.refresh_pairlist()
|
||||||
assert pm.whitelist == ['XRP/BTC', 'ETH/BTC', 'TKN/BTC']
|
assert pm.whitelist == ['XRP/BTC']
|
||||||
|
assert log_has_re(r'Removing pair .* since .* is below .*', caplog)
|
||||||
|
|
||||||
# Move to "outside" of lookback window, so original sorting is restored.
|
# Move to "outside" of lookback window, so original sorting is restored.
|
||||||
t.move_to("2021-09-01 07:00:00 +00:00")
|
t.move_to("2021-09-01 07:00:00 +00:00")
|
||||||
|
@ -289,7 +289,8 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
|
|||||||
rpc._rpc_daily_profit(0, stake_currency, fiat_display_currency)
|
rpc._rpc_daily_profit(0, stake_currency, fiat_display_currency)
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_trade_history(mocker, default_conf, markets, fee):
|
@pytest.mark.parametrize('is_short', [True, False])
|
||||||
|
def test_rpc_trade_history(mocker, default_conf, markets, fee, is_short):
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
@ -297,7 +298,7 @@ def test_rpc_trade_history(mocker, default_conf, markets, fee):
|
|||||||
)
|
)
|
||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee, is_short)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
rpc._fiat_converter = CryptoToFiatConverter()
|
rpc._fiat_converter = CryptoToFiatConverter()
|
||||||
trades = rpc._rpc_trade_history(2)
|
trades = rpc._rpc_trade_history(2)
|
||||||
@ -314,7 +315,8 @@ def test_rpc_trade_history(mocker, default_conf, markets, fee):
|
|||||||
assert trades['trades'][0]['pair'] == 'XRP/BTC'
|
assert trades['trades'][0]['pair'] == 'XRP/BTC'
|
||||||
|
|
||||||
|
|
||||||
def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog):
|
@pytest.mark.parametrize('is_short', [True, False])
|
||||||
|
def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog, is_short):
|
||||||
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
|
||||||
stoploss_mock = MagicMock()
|
stoploss_mock = MagicMock()
|
||||||
cancel_mock = MagicMock()
|
cancel_mock = MagicMock()
|
||||||
@ -327,7 +329,7 @@ def test_rpc_delete_trade(mocker, default_conf, fee, markets, caplog):
|
|||||||
|
|
||||||
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
|
||||||
freqtradebot.strategy.order_types['stoploss_on_exchange'] = True
|
freqtradebot.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee, is_short)
|
||||||
rpc = RPC(freqtradebot)
|
rpc = RPC(freqtradebot)
|
||||||
with pytest.raises(RPCException, match='invalid argument'):
|
with pytest.raises(RPCException, match='invalid argument'):
|
||||||
rpc._rpc_delete('200')
|
rpc._rpc_delete('200')
|
||||||
|
@ -95,7 +95,7 @@ def test_api_not_found(botclient):
|
|||||||
assert rc.json() == {"detail": "Not Found"}
|
assert rc.json() == {"detail": "Not Found"}
|
||||||
|
|
||||||
|
|
||||||
def test_api_ui_fallback(botclient):
|
def test_api_ui_fallback(botclient, mocker):
|
||||||
ftbot, client = botclient
|
ftbot, client = botclient
|
||||||
|
|
||||||
rc = client_get(client, "/favicon.ico")
|
rc = client_get(client, "/favicon.ico")
|
||||||
@ -109,9 +109,16 @@ def test_api_ui_fallback(botclient):
|
|||||||
rc = client_get(client, "/something")
|
rc = client_get(client, "/something")
|
||||||
assert rc.status_code == 200
|
assert rc.status_code == 200
|
||||||
|
|
||||||
# Test directory traversal
|
# Test directory traversal without mock
|
||||||
rc = client_get(client, '%2F%2F%2Fetc/passwd')
|
rc = client_get(client, '%2F%2F%2Fetc/passwd')
|
||||||
assert rc.status_code == 200
|
assert rc.status_code == 200
|
||||||
|
# Allow both fallback or real UI
|
||||||
|
assert '`freqtrade install-ui`' in rc.text or '<!DOCTYPE html>' in rc.text
|
||||||
|
|
||||||
|
mocker.patch.object(Path, 'is_file', MagicMock(side_effect=[True, False]))
|
||||||
|
rc = client_get(client, '%2F%2F%2Fetc/passwd')
|
||||||
|
assert rc.status_code == 200
|
||||||
|
|
||||||
assert '`freqtrade install-ui`' in rc.text
|
assert '`freqtrade install-ui`' in rc.text
|
||||||
|
|
||||||
|
|
||||||
@ -451,7 +458,8 @@ def test_api_balance(botclient, mocker, rpc_balance, tickers):
|
|||||||
assert 'starting_capital_ratio' in response
|
assert 'starting_capital_ratio' in response
|
||||||
|
|
||||||
|
|
||||||
def test_api_count(botclient, mocker, ticker, fee, markets):
|
@pytest.mark.parametrize('is_short', [True, False])
|
||||||
|
def test_api_count(botclient, mocker, ticker, fee, markets, is_short):
|
||||||
ftbot, client = botclient
|
ftbot, client = botclient
|
||||||
patch_get_signal(ftbot)
|
patch_get_signal(ftbot)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -468,7 +476,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets):
|
|||||||
assert rc.json()["max"] == 1
|
assert rc.json()["max"] == 1
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee, is_short)
|
||||||
rc = client_get(client, f"{BASE_URI}/count")
|
rc = client_get(client, f"{BASE_URI}/count")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
assert rc.json()["current"] == 4
|
assert rc.json()["current"] == 4
|
||||||
@ -549,7 +557,8 @@ def test_api_daily(botclient, mocker, ticker, fee, markets):
|
|||||||
assert rc.json()['data'][0]['date'] == str(datetime.utcnow().date())
|
assert rc.json()['data'][0]['date'] == str(datetime.utcnow().date())
|
||||||
|
|
||||||
|
|
||||||
def test_api_trades(botclient, mocker, fee, markets):
|
@pytest.mark.parametrize('is_short', [True, False])
|
||||||
|
def test_api_trades(botclient, mocker, fee, markets, is_short):
|
||||||
ftbot, client = botclient
|
ftbot, client = botclient
|
||||||
patch_get_signal(ftbot)
|
patch_get_signal(ftbot)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -562,7 +571,7 @@ def test_api_trades(botclient, mocker, fee, markets):
|
|||||||
assert rc.json()['trades_count'] == 0
|
assert rc.json()['trades_count'] == 0
|
||||||
assert rc.json()['total_trades'] == 0
|
assert rc.json()['total_trades'] == 0
|
||||||
|
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee, is_short)
|
||||||
Trade.query.session.flush()
|
Trade.query.session.flush()
|
||||||
|
|
||||||
rc = client_get(client, f"{BASE_URI}/trades")
|
rc = client_get(client, f"{BASE_URI}/trades")
|
||||||
@ -577,6 +586,7 @@ def test_api_trades(botclient, mocker, fee, markets):
|
|||||||
assert rc.json()['total_trades'] == 2
|
assert rc.json()['total_trades'] == 2
|
||||||
|
|
||||||
|
|
||||||
|
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
|
||||||
def test_api_trade_single(botclient, mocker, fee, ticker, markets):
|
def test_api_trade_single(botclient, mocker, fee, ticker, markets):
|
||||||
ftbot, client = botclient
|
ftbot, client = botclient
|
||||||
patch_get_signal(ftbot)
|
patch_get_signal(ftbot)
|
||||||
@ -589,7 +599,7 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets):
|
|||||||
assert_response(rc, 404)
|
assert_response(rc, 404)
|
||||||
assert rc.json()['detail'] == 'Trade not found.'
|
assert rc.json()['detail'] == 'Trade not found.'
|
||||||
|
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee, False)
|
||||||
Trade.query.session.flush()
|
Trade.query.session.flush()
|
||||||
|
|
||||||
rc = client_get(client, f"{BASE_URI}/trade/3")
|
rc = client_get(client, f"{BASE_URI}/trade/3")
|
||||||
@ -597,6 +607,7 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets):
|
|||||||
assert rc.json()['trade_id'] == 3
|
assert rc.json()['trade_id'] == 3
|
||||||
|
|
||||||
|
|
||||||
|
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
|
||||||
def test_api_delete_trade(botclient, mocker, fee, markets):
|
def test_api_delete_trade(botclient, mocker, fee, markets):
|
||||||
ftbot, client = botclient
|
ftbot, client = botclient
|
||||||
patch_get_signal(ftbot)
|
patch_get_signal(ftbot)
|
||||||
@ -612,11 +623,12 @@ def test_api_delete_trade(botclient, mocker, fee, markets):
|
|||||||
# Error - trade won't exist yet.
|
# Error - trade won't exist yet.
|
||||||
assert_response(rc, 502)
|
assert_response(rc, 502)
|
||||||
|
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee, False)
|
||||||
Trade.query.session.flush()
|
|
||||||
ftbot.strategy.order_types['stoploss_on_exchange'] = True
|
ftbot.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
trades = Trade.query.all()
|
trades = Trade.query.all()
|
||||||
trades[1].stoploss_order_id = '1234'
|
trades[1].stoploss_order_id = '1234'
|
||||||
|
Trade.commit()
|
||||||
assert len(trades) > 2
|
assert len(trades) > 2
|
||||||
|
|
||||||
rc = client_delete(client, f"{BASE_URI}/trades/1")
|
rc = client_delete(client, f"{BASE_URI}/trades/1")
|
||||||
@ -686,7 +698,7 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets):
|
|||||||
assert rc.json() == {"error": "Error querying /api/v1/edge: Edge is not enabled."}
|
assert rc.json() == {"error": "Error querying /api/v1/edge: Edge is not enabled."}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
|
||||||
def test_api_profit(botclient, mocker, ticker, fee, markets):
|
def test_api_profit(botclient, mocker, ticker, fee, markets):
|
||||||
ftbot, client = botclient
|
ftbot, client = botclient
|
||||||
patch_get_signal(ftbot)
|
patch_get_signal(ftbot)
|
||||||
@ -702,7 +714,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets):
|
|||||||
assert_response(rc, 200)
|
assert_response(rc, 200)
|
||||||
assert rc.json()['trade_count'] == 0
|
assert rc.json()['trade_count'] == 0
|
||||||
|
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee, False)
|
||||||
# Simulate fulfilled LIMIT_BUY order for trade
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
|
|
||||||
rc = client_get(client, f"{BASE_URI}/profit")
|
rc = client_get(client, f"{BASE_URI}/profit")
|
||||||
@ -737,7 +749,7 @@ def test_api_profit(botclient, mocker, ticker, fee, markets):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
|
||||||
def test_api_stats(botclient, mocker, ticker, fee, markets,):
|
def test_api_stats(botclient, mocker, ticker, fee, markets,):
|
||||||
ftbot, client = botclient
|
ftbot, client = botclient
|
||||||
patch_get_signal(ftbot)
|
patch_get_signal(ftbot)
|
||||||
@ -754,7 +766,7 @@ def test_api_stats(botclient, mocker, ticker, fee, markets,):
|
|||||||
assert 'durations' in rc.json()
|
assert 'durations' in rc.json()
|
||||||
assert 'sell_reasons' in rc.json()
|
assert 'sell_reasons' in rc.json()
|
||||||
|
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee, False)
|
||||||
|
|
||||||
rc = client_get(client, f"{BASE_URI}/stats")
|
rc = client_get(client, f"{BASE_URI}/stats")
|
||||||
assert_response(rc, 200)
|
assert_response(rc, 200)
|
||||||
@ -803,7 +815,7 @@ def test_api_performance(botclient, fee):
|
|||||||
trade.close_profit_abs = trade.calc_profit()
|
trade.close_profit_abs = trade.calc_profit()
|
||||||
|
|
||||||
Trade.query.session.add(trade)
|
Trade.query.session.add(trade)
|
||||||
Trade.query.session.flush()
|
Trade.commit()
|
||||||
|
|
||||||
rc = client_get(client, f"{BASE_URI}/performance")
|
rc = client_get(client, f"{BASE_URI}/performance")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
@ -812,6 +824,10 @@ def test_api_performance(botclient, fee):
|
|||||||
{'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57, 'profit_abs': -0.1150375}]
|
{'count': 1, 'pair': 'XRP/ETH', 'profit': -5.57, 'profit_abs': -0.1150375}]
|
||||||
|
|
||||||
|
|
||||||
|
# TODO-lev: @pytest.mark.parametrize('is_short,side', [
|
||||||
|
# (True, "short"),
|
||||||
|
# (False, "long")
|
||||||
|
# ])
|
||||||
def test_api_status(botclient, mocker, ticker, fee, markets):
|
def test_api_status(botclient, mocker, ticker, fee, markets):
|
||||||
ftbot, client = botclient
|
ftbot, client = botclient
|
||||||
patch_get_signal(ftbot)
|
patch_get_signal(ftbot)
|
||||||
@ -827,7 +843,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
|||||||
rc = client_get(client, f"{BASE_URI}/status")
|
rc = client_get(client, f"{BASE_URI}/status")
|
||||||
assert_response(rc, 200)
|
assert_response(rc, 200)
|
||||||
assert rc.json() == []
|
assert rc.json() == []
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee, False)
|
||||||
|
|
||||||
rc = client_get(client, f"{BASE_URI}/status")
|
rc = client_get(client, f"{BASE_URI}/status")
|
||||||
assert_response(rc)
|
assert_response(rc)
|
||||||
@ -880,7 +896,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
|
|||||||
'is_open': True,
|
'is_open': True,
|
||||||
'max_rate': ANY,
|
'max_rate': ANY,
|
||||||
'min_rate': ANY,
|
'min_rate': ANY,
|
||||||
'open_order_id': 'dry_run_buy_12345',
|
'open_order_id': 'dry_run_buy_long_12345',
|
||||||
'open_rate_requested': ANY,
|
'open_rate_requested': ANY,
|
||||||
'open_trade_value': 15.1668225,
|
'open_trade_value': 15.1668225,
|
||||||
'sell_reason': None,
|
'sell_reason': None,
|
||||||
|
@ -33,6 +33,7 @@ class DummyCls(Telegram):
|
|||||||
"""
|
"""
|
||||||
Dummy class for testing the Telegram @authorized_only decorator
|
Dummy class for testing the Telegram @authorized_only decorator
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, rpc: RPC, config) -> None:
|
def __init__(self, rpc: RPC, config) -> None:
|
||||||
super().__init__(rpc, config)
|
super().__init__(rpc, config)
|
||||||
self.state = {'called': False}
|
self.state = {'called': False}
|
||||||
@ -479,8 +480,9 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
|
|||||||
assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0]
|
assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('is_short', [True, False])
|
||||||
def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee,
|
def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee,
|
||||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
limit_buy_order, limit_sell_order, mocker, is_short) -> None:
|
||||||
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=15000.0)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
@ -496,7 +498,7 @@ def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee,
|
|||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee, is_short)
|
||||||
|
|
||||||
telegram._stats(update=update, context=MagicMock())
|
telegram._stats(update=update, context=MagicMock())
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
@ -997,9 +999,9 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
|
|||||||
|
|
||||||
msg = ('<pre> current max total stake\n--------- ----- -------------\n'
|
msg = ('<pre> current max total stake\n--------- ----- -------------\n'
|
||||||
' 1 {} {}</pre>').format(
|
' 1 {} {}</pre>').format(
|
||||||
default_conf['max_open_trades'],
|
default_conf['max_open_trades'],
|
||||||
default_conf['stake_amount']
|
default_conf['stake_amount']
|
||||||
)
|
)
|
||||||
assert msg in msg_mock.call_args_list[0][0][0]
|
assert msg in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
@ -1159,6 +1161,7 @@ def test_edge_enabled(edge_conf, update, mocker) -> None:
|
|||||||
assert 'Winrate' not in msg_mock.call_args_list[0][0][0]
|
assert 'Winrate' not in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
|
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
|
||||||
def test_telegram_trades(mocker, update, default_conf, fee):
|
def test_telegram_trades(mocker, update, default_conf, fee):
|
||||||
|
|
||||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
@ -1177,7 +1180,7 @@ def test_telegram_trades(mocker, update, default_conf, fee):
|
|||||||
assert "<pre>" not in msg_mock.call_args_list[0][0][0]
|
assert "<pre>" not in msg_mock.call_args_list[0][0][0]
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
|
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee, False)
|
||||||
|
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
context.args = [5]
|
context.args = [5]
|
||||||
@ -1191,6 +1194,7 @@ def test_telegram_trades(mocker, update, default_conf, fee):
|
|||||||
msg_mock.call_args_list[0][0][0]))
|
msg_mock.call_args_list[0][0][0]))
|
||||||
|
|
||||||
|
|
||||||
|
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
|
||||||
def test_telegram_delete_trade(mocker, update, default_conf, fee):
|
def test_telegram_delete_trade(mocker, update, default_conf, fee):
|
||||||
|
|
||||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||||
@ -1201,7 +1205,7 @@ def test_telegram_delete_trade(mocker, update, default_conf, fee):
|
|||||||
assert "Trade-id not set." in msg_mock.call_args_list[0][0][0]
|
assert "Trade-id not set." in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
msg_mock.reset_mock()
|
msg_mock.reset_mock()
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee, False)
|
||||||
|
|
||||||
context = MagicMock()
|
context = MagicMock()
|
||||||
context.args = [1]
|
context.args = [1]
|
||||||
|
@ -47,8 +47,8 @@ def test_returns_latest_signal(ohlcv_history):
|
|||||||
mocked_history.loc[1, 'exit_long'] = 0
|
mocked_history.loc[1, 'exit_long'] = 0
|
||||||
mocked_history.loc[1, 'enter_long'] = 1
|
mocked_history.loc[1, 'enter_long'] = 1
|
||||||
|
|
||||||
assert _STRATEGY.get_entry_signal('ETH/BTC', '5m', mocked_history
|
assert _STRATEGY.get_entry_signal(
|
||||||
) == (SignalDirection.LONG, None)
|
'ETH/BTC', '5m', mocked_history) == (SignalDirection.LONG, None)
|
||||||
assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (True, False)
|
assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history) == (True, False)
|
||||||
assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, False)
|
assert _STRATEGY.get_exit_signal('ETH/BTC', '5m', mocked_history, True) == (False, False)
|
||||||
mocked_history.loc[1, 'exit_long'] = 0
|
mocked_history.loc[1, 'exit_long'] = 0
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1514,11 +1514,12 @@ def test_adjust_min_max_rates(fee):
|
|||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
@pytest.mark.parametrize('use_db', [True, False])
|
@pytest.mark.parametrize('use_db', [True, False])
|
||||||
def test_get_open(fee, use_db):
|
@pytest.mark.parametrize('is_short', [True, False])
|
||||||
|
def test_get_open(fee, is_short, use_db):
|
||||||
Trade.use_db = use_db
|
Trade.use_db = use_db
|
||||||
Trade.reset_trades()
|
Trade.reset_trades()
|
||||||
|
|
||||||
create_mock_trades(fee, use_db)
|
create_mock_trades(fee, is_short, use_db)
|
||||||
assert len(Trade.get_open_trades()) == 4
|
assert len(Trade.get_open_trades()) == 4
|
||||||
|
|
||||||
Trade.use_db = True
|
Trade.use_db = True
|
||||||
@ -1874,14 +1875,15 @@ def test_fee_updated(fee):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
@pytest.mark.parametrize('is_short', [True, False])
|
||||||
@pytest.mark.parametrize('use_db', [True, False])
|
@pytest.mark.parametrize('use_db', [True, False])
|
||||||
def test_total_open_trades_stakes(fee, use_db):
|
def test_total_open_trades_stakes(fee, is_short, use_db):
|
||||||
|
|
||||||
Trade.use_db = use_db
|
Trade.use_db = use_db
|
||||||
Trade.reset_trades()
|
Trade.reset_trades()
|
||||||
res = Trade.total_open_trades_stakes()
|
res = Trade.total_open_trades_stakes()
|
||||||
assert res == 0
|
assert res == 0
|
||||||
create_mock_trades(fee, use_db)
|
create_mock_trades(fee, is_short, use_db)
|
||||||
res = Trade.total_open_trades_stakes()
|
res = Trade.total_open_trades_stakes()
|
||||||
assert res == 0.004
|
assert res == 0.004
|
||||||
|
|
||||||
@ -1889,6 +1891,7 @@ def test_total_open_trades_stakes(fee, use_db):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
|
||||||
@pytest.mark.parametrize('use_db', [True, False])
|
@pytest.mark.parametrize('use_db', [True, False])
|
||||||
def test_get_total_closed_profit(fee, use_db):
|
def test_get_total_closed_profit(fee, use_db):
|
||||||
|
|
||||||
@ -1896,7 +1899,7 @@ def test_get_total_closed_profit(fee, use_db):
|
|||||||
Trade.reset_trades()
|
Trade.reset_trades()
|
||||||
res = Trade.get_total_closed_profit()
|
res = Trade.get_total_closed_profit()
|
||||||
assert res == 0
|
assert res == 0
|
||||||
create_mock_trades(fee, use_db)
|
create_mock_trades(fee, False, use_db)
|
||||||
res = Trade.get_total_closed_profit()
|
res = Trade.get_total_closed_profit()
|
||||||
assert res == 0.000739127
|
assert res == 0.000739127
|
||||||
|
|
||||||
@ -1904,11 +1907,12 @@ def test_get_total_closed_profit(fee, use_db):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
|
||||||
@pytest.mark.parametrize('use_db', [True, False])
|
@pytest.mark.parametrize('use_db', [True, False])
|
||||||
def test_get_trades_proxy(fee, use_db):
|
def test_get_trades_proxy(fee, use_db):
|
||||||
Trade.use_db = use_db
|
Trade.use_db = use_db
|
||||||
Trade.reset_trades()
|
Trade.reset_trades()
|
||||||
create_mock_trades(fee, use_db)
|
create_mock_trades(fee, False, use_db)
|
||||||
trades = Trade.get_trades_proxy()
|
trades = Trade.get_trades_proxy()
|
||||||
assert len(trades) == 6
|
assert len(trades) == 6
|
||||||
|
|
||||||
@ -1937,9 +1941,10 @@ def test_get_trades_backtest():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
# @pytest.mark.parametrize('is_short', [True, False])
|
||||||
def test_get_overall_performance(fee):
|
def test_get_overall_performance(fee):
|
||||||
|
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee, False)
|
||||||
res = Trade.get_overall_performance()
|
res = Trade.get_overall_performance()
|
||||||
|
|
||||||
assert len(res) == 2
|
assert len(res) == 2
|
||||||
@ -1949,12 +1954,13 @@ def test_get_overall_performance(fee):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
|
||||||
def test_get_best_pair(fee):
|
def test_get_best_pair(fee):
|
||||||
|
|
||||||
res = Trade.get_best_pair()
|
res = Trade.get_best_pair()
|
||||||
assert res is None
|
assert res is None
|
||||||
|
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee, False)
|
||||||
res = Trade.get_best_pair()
|
res = Trade.get_best_pair()
|
||||||
assert len(res) == 2
|
assert len(res) == 2
|
||||||
assert res[0] == 'XRP/BTC'
|
assert res[0] == 'XRP/BTC'
|
||||||
@ -2036,8 +2042,9 @@ def test_update_order_from_ccxt(caplog):
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("init_persistence")
|
@pytest.mark.usefixtures("init_persistence")
|
||||||
|
# TODO-lev: @pytest.mark.parametrize('is_short', [True, False])
|
||||||
def test_select_order(fee):
|
def test_select_order(fee):
|
||||||
create_mock_trades(fee)
|
create_mock_trades(fee, False)
|
||||||
|
|
||||||
trades = Trade.get_trades().all()
|
trades = Trade.get_trades().all()
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user