Merge branch 'develop' into feat/new_config
This commit is contained in:
commit
abf10aec98
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -18,7 +18,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ ubuntu-18.04, macos-latest ]
|
os: [ ubuntu-18.04, macos-latest ]
|
||||||
python-version: [3.7]
|
python-version: [3.7, 3.8]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
@ -68,7 +68,7 @@ jobs:
|
|||||||
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
|
pytest --random-order --cov=freqtrade --cov-config=.coveragerc
|
||||||
|
|
||||||
- name: Coveralls
|
- name: Coveralls
|
||||||
if: startsWith(matrix.os, 'ubuntu')
|
if: (startsWith(matrix.os, 'ubuntu') && matrix.python-version == '3.8')
|
||||||
env:
|
env:
|
||||||
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories
|
# Coveralls token. Not used as secret due to github not providing secrets to forked repositories
|
||||||
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
|
COVERALLS_REPO_TOKEN: 6D1m0xupS3FgutfuGao8keFf9Hc0FpIXu
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM python:3.7.6-slim-stretch
|
FROM python:3.8.1-slim-buster
|
||||||
|
|
||||||
RUN apt-get update \
|
RUN apt-get update \
|
||||||
&& apt-get -y install curl build-essential libssl-dev \
|
&& apt-get -y install curl build-essential libssl-dev \
|
||||||
|
@ -62,8 +62,8 @@
|
|||||||
"refresh_period": 1800
|
"refresh_period": 1800
|
||||||
},
|
},
|
||||||
{"method": "PrecisionFilter"},
|
{"method": "PrecisionFilter"},
|
||||||
{"method": "PriceFilter", "low_price_ratio": 0.01
|
{"method": "PriceFilter", "low_price_ratio": 0.01},
|
||||||
}
|
{"method": "SpreadFilter", "max_spread_ratio": 0.005}
|
||||||
],
|
],
|
||||||
"exchange": {
|
"exchange": {
|
||||||
"name": "bittrex",
|
"name": "bittrex",
|
||||||
|
@ -237,8 +237,8 @@ There will be an additional table comparing win/losses of the different strategi
|
|||||||
Detailed output for all strategies one after the other will be available, so make sure to scroll up to see the details per strategy.
|
Detailed output for all strategies one after the other will be available, so make sure to scroll up to see the details per strategy.
|
||||||
|
|
||||||
```
|
```
|
||||||
=========================================================== Strategy Summary ===========================================================
|
=========================================================== STRATEGY SUMMARY ===========================================================
|
||||||
| Strategy | buy count | avg profit % | cum profit % | tot profit BTC | tot profit % | avg duration | profit | loss |
|
| Strategy | Buy Count | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Losses |
|
||||||
|:------------|------------:|---------------:|---------------:|-----------------:|---------------:|:---------------|---------:|-------:|
|
|:------------|------------:|---------------:|---------------:|-----------------:|---------------:|:---------------|---------:|-------:|
|
||||||
| Strategy1 | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 243 |
|
| Strategy1 | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 243 |
|
||||||
| Strategy2 | 1487 | -0.13 | -197.58 | -0.00988917 | -98.79 | 4:43:00 | 662 | 825 |
|
| Strategy2 | 1487 | -0.13 | -197.58 | -0.00988917 | -98.79 | 4:43:00 | 662 | 825 |
|
||||||
|
@ -337,8 +337,8 @@ optional arguments:
|
|||||||
generate completely different results, since the
|
generate completely different results, since the
|
||||||
target for optimization is different. Built-in
|
target for optimization is different. Built-in
|
||||||
Hyperopt-loss-functions are: DefaultHyperOptLoss,
|
Hyperopt-loss-functions are: DefaultHyperOptLoss,
|
||||||
OnlyProfitHyperOptLoss, SharpeHyperOptLoss (default:
|
OnlyProfitHyperOptLoss, SharpeHyperOptLoss,
|
||||||
`DefaultHyperOptLoss`).
|
SharpeHyperOptLossDaily (default: `DefaultHyperOptLoss`).
|
||||||
|
|
||||||
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).
|
||||||
|
@ -278,7 +278,7 @@ If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and
|
|||||||
The below is the default which is used if this is not configured in either strategy or configuration file.
|
The below is the default which is used if this is not configured in either strategy or configuration file.
|
||||||
|
|
||||||
Since `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price.
|
Since `stoploss_on_exchange` uses limit orders, the exchange needs 2 prices, the stoploss_price and the Limit price.
|
||||||
`stoploss` defines the stop-price - and limit should be slightly below this. This defaults to 0.99 / 1%.
|
`stoploss` defines the stop-price - and limit should be slightly below this. This defaults to 0.99 / 1% (configurable via `stoploss_on_exchange_limit_ratio`).
|
||||||
Calculation example: we bought the asset at 100$.
|
Calculation example: we bought the asset at 100$.
|
||||||
Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the stoploss will happen between 95$ and 94.05$.
|
Stop-price is 95$, then limit would be `95 * 0.99 = 94.05$` - so the stoploss will happen between 95$ and 94.05$.
|
||||||
|
|
||||||
@ -503,6 +503,7 @@ Inactive markets and blacklisted pairs are always removed from the resulting `pa
|
|||||||
* [`VolumePairList`](#volume-pair-list)
|
* [`VolumePairList`](#volume-pair-list)
|
||||||
* [`PrecisionFilter`](#precision-filter)
|
* [`PrecisionFilter`](#precision-filter)
|
||||||
* [`PriceFilter`](#price-pair-filter)
|
* [`PriceFilter`](#price-pair-filter)
|
||||||
|
* [`SpreadFilter`](#spread-filter)
|
||||||
|
|
||||||
!!! Tip "Testing pairlists"
|
!!! Tip "Testing pairlists"
|
||||||
Pairlist configurations can be quite tricky to get right. Best use the [`test-pairlist`](utils.md#test-pairlist) subcommand to test your configuration quickly.
|
Pairlist configurations can be quite tricky to get right. Best use the [`test-pairlist`](utils.md#test-pairlist) subcommand to test your configuration quickly.
|
||||||
@ -551,6 +552,11 @@ Min price precision is 8 decimals. If price is 0.00000011 - one step would be 0.
|
|||||||
|
|
||||||
These pairs are dangerous since it may be impossible to place the desired stoploss - and often result in high losses.
|
These pairs are dangerous since it may be impossible to place the desired stoploss - and often result in high losses.
|
||||||
|
|
||||||
|
#### Spread Filter
|
||||||
|
Removes pairs that have a difference between asks and bids above the specified ratio (default `0.005`).
|
||||||
|
Example:
|
||||||
|
If `DOGE/BTC` maximum bid is 0.00000026 and minimum ask is 0.00000027 the ratio is calculated as: `1 - bid/ask ~= 0.037` which is `> 0.005`
|
||||||
|
|
||||||
### Full Pairlist example
|
### Full Pairlist example
|
||||||
|
|
||||||
The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting by `quoteVolume` and applies both [`PrecisionFilter`](#precision-filter) and [`PriceFilter`](#price-pair-filter), filtering all assets where 1 priceunit is > 1%.
|
The below example blacklists `BNB/BTC`, uses `VolumePairList` with `20` assets, sorting by `quoteVolume` and applies both [`PrecisionFilter`](#precision-filter) and [`PriceFilter`](#price-pair-filter), filtering all assets where 1 priceunit is > 1%.
|
||||||
|
@ -5,7 +5,7 @@ This page combines common gotchas and informations which are exchange-specific a
|
|||||||
## Binance
|
## Binance
|
||||||
|
|
||||||
!!! Tip "Stoploss on Exchange"
|
!!! Tip "Stoploss on Exchange"
|
||||||
Binance is currently the only exchange supporting `stoploss_on_exchange`. It provides great advantages, so we recommend to benefit from it.
|
Binance supports `stoploss_on_exchange` and uses stop-loss-limit orders. It provides great advantages, so we recommend to benefit from it.
|
||||||
|
|
||||||
### Blacklists
|
### Blacklists
|
||||||
|
|
||||||
@ -22,6 +22,9 @@ Binance has been split into 3, and users must use the correct ccxt exchange ID f
|
|||||||
|
|
||||||
## Kraken
|
## Kraken
|
||||||
|
|
||||||
|
!!! Tip "Stoploss on Exchange"
|
||||||
|
Kraken supports `stoploss_on_exchange` and uses stop-loss-market orders. It provides great advantages, so we recommend to benefit from it, however since the resulting order is a stoploss-market order, sell-rates are not guaranteed, which makes this feature less secure than on other exchanges. This limitation is based on kraken's policy [source](https://blog.kraken.com/post/1234/announcement-delisting-pairs-and-temporary-suspension-of-advanced-order-types/) and [source2](https://blog.kraken.com/post/1494/kraken-enables-advanced-orders-and-adds-10-currency-pairs/) - which has stoploss-limit orders disabled.
|
||||||
|
|
||||||
### Historic Kraken data
|
### Historic Kraken data
|
||||||
|
|
||||||
The Kraken API does only provide 720 historic candles, which is sufficient for Freqtrade dry-run and live trade modes, but is a problem for backtesting.
|
The Kraken API does only provide 720 historic candles, which is sufficient for Freqtrade dry-run and live trade modes, but is a problem for backtesting.
|
||||||
|
@ -57,7 +57,7 @@ Rarely you may also need to override:
|
|||||||
!!! Tip "Quickly optimize ROI, stoploss and trailing stoploss"
|
!!! Tip "Quickly optimize ROI, stoploss and trailing stoploss"
|
||||||
You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything (i.e. without creation of a "complete" Hyperopt class with dimensions, parameters, triggers and guards, as described in this document) from the default hyperopt template by relying on your strategy to do most of the calculations.
|
You can quickly optimize the spaces `roi`, `stoploss` and `trailing` without changing anything (i.e. without creation of a "complete" Hyperopt class with dimensions, parameters, triggers and guards, as described in this document) from the default hyperopt template by relying on your strategy to do most of the calculations.
|
||||||
|
|
||||||
``` python
|
```python
|
||||||
# Have a working strategy at hand.
|
# Have a working strategy at hand.
|
||||||
freqtrade new-hyperopt --hyperopt EmptyHyperopt
|
freqtrade new-hyperopt --hyperopt EmptyHyperopt
|
||||||
|
|
||||||
@ -75,8 +75,8 @@ Copy the file `user_data/hyperopts/sample_hyperopt.py` into `user_data/hyperopts
|
|||||||
|
|
||||||
There are two places you need to change in your hyperopt file to add a new buy hyperopt for testing:
|
There are two places you need to change in your hyperopt file to add a new buy hyperopt for testing:
|
||||||
|
|
||||||
- Inside `indicator_space()` - the parameters hyperopt shall be optimizing.
|
* Inside `indicator_space()` - the parameters hyperopt shall be optimizing.
|
||||||
- Inside `populate_buy_trend()` - applying the parameters.
|
* Inside `populate_buy_trend()` - applying the parameters.
|
||||||
|
|
||||||
There you have two different types of indicators: 1. `guards` and 2. `triggers`.
|
There you have two different types of indicators: 1. `guards` and 2. `triggers`.
|
||||||
|
|
||||||
@ -141,7 +141,7 @@ one we call `trigger` and use it to decide which buy trigger we want to use.
|
|||||||
|
|
||||||
So let's write the buy strategy using these values:
|
So let's write the buy strategy using these values:
|
||||||
|
|
||||||
``` python
|
```python
|
||||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||||
conditions = []
|
conditions = []
|
||||||
# GUARDS AND TRENDS
|
# GUARDS AND TRENDS
|
||||||
@ -192,6 +192,7 @@ Currently, the following loss functions are builtin:
|
|||||||
* `DefaultHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function)
|
* `DefaultHyperOptLoss` (default legacy Freqtrade hyperoptimization loss function)
|
||||||
* `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration)
|
* `OnlyProfitHyperOptLoss` (which takes only amount of profit into consideration)
|
||||||
* `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on the trade returns)
|
* `SharpeHyperOptLoss` (optimizes Sharpe Ratio calculated on the trade returns)
|
||||||
|
* `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on daily trade returns)
|
||||||
|
|
||||||
Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation.
|
Creation of a custom loss function is covered in the [Advanced Hyperopt](advanced-hyperopt.md) part of the documentation.
|
||||||
|
|
||||||
@ -323,7 +324,7 @@ method, what those values match to.
|
|||||||
|
|
||||||
So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block:
|
So for example you had `rsi-value: 29.0` so we would look at `rsi`-block, that translates to the following code block:
|
||||||
|
|
||||||
``` python
|
```python
|
||||||
(dataframe['rsi'] < 29.0)
|
(dataframe['rsi'] < 29.0)
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -372,6 +373,7 @@ In order to use this best ROI table found by Hyperopt in backtesting and for liv
|
|||||||
118: 0
|
118: 0
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
As stated in the comment, you can also use it as the value of the `minimal_roi` setting in the configuration file.
|
As stated in the comment, you can also use it as the value of the `minimal_roi` setting in the configuration file.
|
||||||
|
|
||||||
#### Default ROI Search Space
|
#### Default ROI Search Space
|
||||||
@ -379,7 +381,7 @@ As stated in the comment, you can also use it as the value of the `minimal_roi`
|
|||||||
If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the ticker_interval used. By default the values vary in the following ranges (for some of the most used ticker intervals, values are rounded to 5 digits after the decimal point):
|
If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the ticker_interval used. By default the values vary in the following ranges (for some of the most used ticker intervals, values are rounded to 5 digits after the decimal point):
|
||||||
|
|
||||||
| # step | 1m | | 5m | | 1h | | 1d | |
|
| # step | 1m | | 5m | | 1h | | 1d | |
|
||||||
|---|---|---|---|---|---|---|---|---|
|
| ------ | ------ | ----------------- | -------- | ----------- | ---------- | ----------------- | ------------ | ----------------- |
|
||||||
| 1 | 0 | 0.01161...0.11992 | 0 | 0.03...0.31 | 0 | 0.06883...0.71124 | 0 | 0.12178...1.25835 |
|
| 1 | 0 | 0.01161...0.11992 | 0 | 0.03...0.31 | 0 | 0.06883...0.71124 | 0 | 0.12178...1.25835 |
|
||||||
| 2 | 2...8 | 0.00774...0.04255 | 10...40 | 0.02...0.11 | 120...480 | 0.04589...0.25238 | 2880...11520 | 0.08118...0.44651 |
|
| 2 | 2...8 | 0.00774...0.04255 | 10...40 | 0.02...0.11 | 120...480 | 0.04589...0.25238 | 2880...11520 | 0.08118...0.44651 |
|
||||||
| 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 |
|
| 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 |
|
||||||
@ -416,6 +418,7 @@ In order to use this best stoploss value found by Hyperopt in backtesting and fo
|
|||||||
# This attribute will be overridden if the config file contains "stoploss"
|
# This attribute will be overridden if the config file contains "stoploss"
|
||||||
stoploss = -0.27996
|
stoploss = -0.27996
|
||||||
```
|
```
|
||||||
|
|
||||||
As stated in the comment, you can also use it as the value of the `stoploss` setting in the configuration file.
|
As stated in the comment, you can also use it as the value of the `stoploss` setting in the configuration file.
|
||||||
|
|
||||||
#### Default Stoploss Search Space
|
#### Default Stoploss Search Space
|
||||||
@ -452,6 +455,7 @@ In order to use these best trailing stop parameters found by Hyperopt in backtes
|
|||||||
trailing_stop_positive_offset = 0.06038
|
trailing_stop_positive_offset = 0.06038
|
||||||
trailing_only_offset_is_reached = True
|
trailing_only_offset_is_reached = True
|
||||||
```
|
```
|
||||||
|
|
||||||
As stated in the comment, you can also use it as the values of the corresponding settings in the configuration file.
|
As stated in the comment, you can also use it as the values of the corresponding settings in the configuration file.
|
||||||
|
|
||||||
#### Default Trailing Stop Search Space
|
#### Default Trailing Stop Search Space
|
||||||
|
@ -27,7 +27,7 @@ So this parameter will tell the bot how often it should update the stoploss orde
|
|||||||
This same logic will reapply a stoploss order on the exchange should you cancel it accidentally.
|
This same logic will reapply a stoploss order on the exchange should you cancel it accidentally.
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
Stoploss on exchange is only supported for Binance as of now.
|
Stoploss on exchange is only supported for Binance (stop-loss-limit) and Kraken (stop-loss-market) as of now.
|
||||||
|
|
||||||
## Static Stop Loss
|
## Static Stop Loss
|
||||||
|
|
||||||
|
@ -141,9 +141,9 @@ With custom user directory
|
|||||||
freqtrade new-hyperopt --userdir ~/.freqtrade/ --hyperopt AwesomeHyperopt
|
freqtrade new-hyperopt --userdir ~/.freqtrade/ --hyperopt AwesomeHyperopt
|
||||||
```
|
```
|
||||||
|
|
||||||
## List Strategies
|
## List Strategies and List Hyperopts
|
||||||
|
|
||||||
Use the `list-strategies` subcommand to see all strategies in one particular directory.
|
Use the `list-strategies` subcommand to see all strategies in one particular directory and the `list-hyperopts` subcommand to list custom Hyperopts.
|
||||||
|
|
||||||
```
|
```
|
||||||
freqtrade list-strategies --help
|
freqtrade list-strategies --help
|
||||||
@ -166,22 +166,63 @@ Common arguments:
|
|||||||
--userdir PATH, --user-data-dir PATH
|
--userdir PATH, --user-data-dir PATH
|
||||||
Path to userdata directory.
|
Path to userdata directory.
|
||||||
```
|
```
|
||||||
|
```
|
||||||
|
freqtrade list-hyperopts --help
|
||||||
|
usage: freqtrade list-hyperopts [-h] [-v] [--logfile FILE] [-V] [-c PATH]
|
||||||
|
[-d PATH] [--userdir PATH]
|
||||||
|
[--hyperopt-path PATH] [-1]
|
||||||
|
|
||||||
|
optional arguments:
|
||||||
|
-h, --help show this help message and exit
|
||||||
|
--hyperopt-path PATH Specify additional lookup path for Hyperopt and
|
||||||
|
Hyperopt Loss functions.
|
||||||
|
-1, --one-column Print output in one column.
|
||||||
|
|
||||||
|
Common arguments:
|
||||||
|
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
|
||||||
|
--logfile FILE Log to the file specified. Special values are:
|
||||||
|
'syslog', 'journald'. See the documentation for more
|
||||||
|
details.
|
||||||
|
-V, --version show program's version number and exit
|
||||||
|
-c PATH, --config PATH
|
||||||
|
Specify configuration file (default: `config.json`).
|
||||||
|
Multiple --config options may be used. Can be set to
|
||||||
|
`-` to read config from stdin.
|
||||||
|
-d PATH, --datadir PATH
|
||||||
|
Path to directory with historical backtesting data.
|
||||||
|
--userdir PATH, --user-data-dir PATH
|
||||||
|
Path to userdata directory.
|
||||||
|
```
|
||||||
|
|
||||||
!!! Warning
|
!!! Warning
|
||||||
Using this command will try to load all python files from a directory. This can be a security risk if untrusted files reside in this directory, since all module-level code is executed.
|
Using these commands will try to load all python files from a directory. This can be a security risk if untrusted files reside in this directory, since all module-level code is executed.
|
||||||
|
|
||||||
Example: search default strategy directory within userdir
|
Example: Search default strategies and hyperopts directories (within the default userdir).
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
freqtrade list-strategies
|
||||||
|
freqtrade list-hyperopts
|
||||||
|
```
|
||||||
|
|
||||||
|
Example: Search strategies and hyperopts directory within the userdir.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade list-strategies --userdir ~/.freqtrade/
|
freqtrade list-strategies --userdir ~/.freqtrade/
|
||||||
|
freqtrade list-hyperopts --userdir ~/.freqtrade/
|
||||||
```
|
```
|
||||||
|
|
||||||
Example: search dedicated strategy path
|
Example: Search dedicated strategy path.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
freqtrade list-strategies --strategy-path ~/.freqtrade/strategies/
|
freqtrade list-strategies --strategy-path ~/.freqtrade/strategies/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Example: Search dedicated hyperopt path.
|
||||||
|
|
||||||
|
``` bash
|
||||||
|
freqtrade list-hyperopt --hyperopt-path ~/.freqtrade/hyperopts/
|
||||||
|
```
|
||||||
|
|
||||||
## List Exchanges
|
## List Exchanges
|
||||||
|
|
||||||
Use the `list-exchanges` subcommand to see the exchanges available for the bot.
|
Use the `list-exchanges` subcommand to see the exchanges available for the bot.
|
||||||
|
@ -15,6 +15,7 @@ from freqtrade.commands.deploy_commands import (start_create_userdir,
|
|||||||
from freqtrade.commands.hyperopt_commands import (start_hyperopt_list,
|
from freqtrade.commands.hyperopt_commands import (start_hyperopt_list,
|
||||||
start_hyperopt_show)
|
start_hyperopt_show)
|
||||||
from freqtrade.commands.list_commands import (start_list_exchanges,
|
from freqtrade.commands.list_commands import (start_list_exchanges,
|
||||||
|
start_list_hyperopts,
|
||||||
start_list_markets,
|
start_list_markets,
|
||||||
start_list_strategies,
|
start_list_strategies,
|
||||||
start_list_timeframes)
|
start_list_timeframes)
|
||||||
|
@ -32,6 +32,8 @@ ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
|
|||||||
|
|
||||||
ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column"]
|
ARGS_LIST_STRATEGIES = ["strategy_path", "print_one_column"]
|
||||||
|
|
||||||
|
ARGS_LIST_HYPEROPTS = ["hyperopt_path", "print_one_column"]
|
||||||
|
|
||||||
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
|
ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
|
||||||
|
|
||||||
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
|
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
|
||||||
@ -66,8 +68,8 @@ ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperop
|
|||||||
"print_json", "hyperopt_show_no_header"]
|
"print_json", "hyperopt_show_no_header"]
|
||||||
|
|
||||||
NO_CONF_REQURIED = ["download-data", "list-timeframes", "list-markets", "list-pairs",
|
NO_CONF_REQURIED = ["download-data", "list-timeframes", "list-markets", "list-pairs",
|
||||||
"list-strategies", "hyperopt-list", "hyperopt-show", "plot-dataframe",
|
"list-strategies", "list-hyperopts", "hyperopt-list", "hyperopt-show",
|
||||||
"plot-profit"]
|
"plot-dataframe", "plot-profit"]
|
||||||
|
|
||||||
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-hyperopt", "new-strategy"]
|
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges", "new-hyperopt", "new-strategy"]
|
||||||
|
|
||||||
@ -134,9 +136,9 @@ class Arguments:
|
|||||||
|
|
||||||
from freqtrade.commands import (start_create_userdir, start_download_data,
|
from freqtrade.commands import (start_create_userdir, start_download_data,
|
||||||
start_hyperopt_list, start_hyperopt_show,
|
start_hyperopt_list, start_hyperopt_show,
|
||||||
start_list_exchanges, start_list_markets,
|
start_list_exchanges, start_list_hyperopts,
|
||||||
start_list_strategies, start_list_timeframes,
|
start_list_markets, start_list_strategies,
|
||||||
start_new_config,
|
start_list_timeframes, start_new_config,
|
||||||
start_new_hyperopt, start_new_strategy,
|
start_new_hyperopt, start_new_strategy,
|
||||||
start_plot_dataframe, start_plot_profit,
|
start_plot_dataframe, start_plot_profit,
|
||||||
start_backtesting, start_hyperopt, start_edge,
|
start_backtesting, start_hyperopt, start_edge,
|
||||||
@ -207,6 +209,15 @@ class Arguments:
|
|||||||
list_strategies_cmd.set_defaults(func=start_list_strategies)
|
list_strategies_cmd.set_defaults(func=start_list_strategies)
|
||||||
self._build_args(optionlist=ARGS_LIST_STRATEGIES, parser=list_strategies_cmd)
|
self._build_args(optionlist=ARGS_LIST_STRATEGIES, parser=list_strategies_cmd)
|
||||||
|
|
||||||
|
# Add list-hyperopts subcommand
|
||||||
|
list_hyperopts_cmd = subparsers.add_parser(
|
||||||
|
'list-hyperopts',
|
||||||
|
help='Print available hyperopt classes.',
|
||||||
|
parents=[_common_parser],
|
||||||
|
)
|
||||||
|
list_hyperopts_cmd.set_defaults(func=start_list_hyperopts)
|
||||||
|
self._build_args(optionlist=ARGS_LIST_HYPEROPTS, parser=list_hyperopts_cmd)
|
||||||
|
|
||||||
# Add list-exchanges subcommand
|
# Add list-exchanges subcommand
|
||||||
list_exchanges_cmd = subparsers.add_parser(
|
list_exchanges_cmd = subparsers.add_parser(
|
||||||
'list-exchanges',
|
'list-exchanges',
|
||||||
|
@ -256,7 +256,7 @@ AVAILABLE_CLI_OPTIONS = {
|
|||||||
help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). '
|
help='Specify the class name of the hyperopt loss function class (IHyperOptLoss). '
|
||||||
'Different functions can generate completely different results, '
|
'Different functions can generate completely different results, '
|
||||||
'since the target for optimization is different. Built-in Hyperopt-loss-functions are: '
|
'since the target for optimization is different. Built-in Hyperopt-loss-functions are: '
|
||||||
'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss.'
|
'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, SharpeHyperOptLossDaily.'
|
||||||
'(default: `%(default)s`).',
|
'(default: `%(default)s`).',
|
||||||
metavar='NAME',
|
metavar='NAME',
|
||||||
default=constants.DEFAULT_HYPEROPT_LOSS,
|
default=constants.DEFAULT_HYPEROPT_LOSS,
|
||||||
|
@ -43,16 +43,18 @@ def start_download_data(args: Dict[str, Any]) -> None:
|
|||||||
if config.get('download_trades'):
|
if config.get('download_trades'):
|
||||||
pairs_not_available = refresh_backtest_trades_data(
|
pairs_not_available = refresh_backtest_trades_data(
|
||||||
exchange, pairs=config["pairs"], datadir=config['datadir'],
|
exchange, pairs=config["pairs"], datadir=config['datadir'],
|
||||||
timerange=timerange, erase=config.get("erase"))
|
timerange=timerange, erase=bool(config.get("erase")))
|
||||||
|
|
||||||
# Convert downloaded trade data to different timeframes
|
# Convert downloaded trade data to different timeframes
|
||||||
convert_trades_to_ohlcv(
|
convert_trades_to_ohlcv(
|
||||||
pairs=config["pairs"], timeframes=config["timeframes"],
|
pairs=config["pairs"], timeframes=config["timeframes"],
|
||||||
datadir=config['datadir'], timerange=timerange, erase=config.get("erase"))
|
datadir=config['datadir'], timerange=timerange,
|
||||||
|
erase=bool(config.get("erase")))
|
||||||
else:
|
else:
|
||||||
pairs_not_available = refresh_backtest_ohlcv_data(
|
pairs_not_available = refresh_backtest_ohlcv_data(
|
||||||
exchange, pairs=config["pairs"], timeframes=config["timeframes"],
|
exchange, pairs=config["pairs"], timeframes=config["timeframes"],
|
||||||
datadir=config['datadir'], timerange=timerange, erase=config.get("erase"))
|
datadir=config['datadir'], timerange=timerange,
|
||||||
|
erase=bool(config.get("erase")))
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
sys.exit("SIGINT received, aborting ...")
|
sys.exit("SIGINT received, aborting ...")
|
||||||
|
@ -6,7 +6,7 @@ from typing import Any, Dict
|
|||||||
from freqtrade.configuration import setup_utils_configuration
|
from freqtrade.configuration import setup_utils_configuration
|
||||||
from freqtrade.configuration.directory_operations import (copy_sample_files,
|
from freqtrade.configuration.directory_operations import (copy_sample_files,
|
||||||
create_userdata_dir)
|
create_userdata_dir)
|
||||||
from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGY
|
from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.misc import render_template
|
from freqtrade.misc import render_template
|
||||||
from freqtrade.state import RunMode
|
from freqtrade.state import RunMode
|
||||||
@ -28,7 +28,7 @@ def start_create_userdir(args: Dict[str, Any]) -> None:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def deploy_new_strategy(strategy_name, strategy_path: Path, subtemplate: str):
|
def deploy_new_strategy(strategy_name: str, strategy_path: Path, subtemplate: str) -> None:
|
||||||
"""
|
"""
|
||||||
Deploy new strategy from template to strategy_path
|
Deploy new strategy from template to strategy_path
|
||||||
"""
|
"""
|
||||||
@ -57,7 +57,7 @@ def start_new_strategy(args: Dict[str, Any]) -> None:
|
|||||||
if args["strategy"] == "DefaultStrategy":
|
if args["strategy"] == "DefaultStrategy":
|
||||||
raise OperationalException("DefaultStrategy is not allowed as name.")
|
raise OperationalException("DefaultStrategy is not allowed as name.")
|
||||||
|
|
||||||
new_path = config['user_data_dir'] / USERPATH_STRATEGY / (args["strategy"] + ".py")
|
new_path = config['user_data_dir'] / USERPATH_STRATEGIES / (args["strategy"] + ".py")
|
||||||
|
|
||||||
if new_path.exists():
|
if new_path.exists():
|
||||||
raise OperationalException(f"`{new_path}` already exists. "
|
raise OperationalException(f"`{new_path}` already exists. "
|
||||||
@ -69,7 +69,7 @@ def start_new_strategy(args: Dict[str, Any]) -> None:
|
|||||||
raise OperationalException("`new-strategy` requires --strategy to be set.")
|
raise OperationalException("`new-strategy` requires --strategy to be set.")
|
||||||
|
|
||||||
|
|
||||||
def deploy_new_hyperopt(hyperopt_name, hyperopt_path: Path, subtemplate: str):
|
def deploy_new_hyperopt(hyperopt_name: str, hyperopt_path: Path, subtemplate: str) -> None:
|
||||||
"""
|
"""
|
||||||
Deploys a new hyperopt template to hyperopt_path
|
Deploys a new hyperopt template to hyperopt_path
|
||||||
"""
|
"""
|
||||||
|
@ -9,7 +9,7 @@ import rapidjson
|
|||||||
from tabulate import tabulate
|
from tabulate import tabulate
|
||||||
|
|
||||||
from freqtrade.configuration import setup_utils_configuration
|
from freqtrade.configuration import setup_utils_configuration
|
||||||
from freqtrade.constants import USERPATH_STRATEGY
|
from freqtrade.constants import USERPATH_HYPEROPTS, USERPATH_STRATEGIES
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.exchange import (available_exchanges, ccxt_exchanges,
|
from freqtrade.exchange import (available_exchanges, ccxt_exchanges,
|
||||||
market_is_active, symbol_is_pair)
|
market_is_active, symbol_is_pair)
|
||||||
@ -38,11 +38,11 @@ def start_list_exchanges(args: Dict[str, Any]) -> None:
|
|||||||
|
|
||||||
def start_list_strategies(args: Dict[str, Any]) -> None:
|
def start_list_strategies(args: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
Print Strategies available in a directory
|
Print files with Strategy custom classes available in the directory
|
||||||
"""
|
"""
|
||||||
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
|
|
||||||
directory = Path(config.get('strategy_path', config['user_data_dir'] / USERPATH_STRATEGY))
|
directory = Path(config.get('strategy_path', config['user_data_dir'] / USERPATH_STRATEGIES))
|
||||||
strategies = StrategyResolver.search_all_objects(directory)
|
strategies = StrategyResolver.search_all_objects(directory)
|
||||||
# Sort alphabetically
|
# Sort alphabetically
|
||||||
strategies = sorted(strategies, key=lambda x: x['name'])
|
strategies = sorted(strategies, key=lambda x: x['name'])
|
||||||
@ -54,6 +54,26 @@ def start_list_strategies(args: Dict[str, Any]) -> None:
|
|||||||
print(tabulate(strats_to_print, headers='keys', tablefmt='pipe'))
|
print(tabulate(strats_to_print, headers='keys', tablefmt='pipe'))
|
||||||
|
|
||||||
|
|
||||||
|
def start_list_hyperopts(args: Dict[str, Any]) -> None:
|
||||||
|
"""
|
||||||
|
Print files with HyperOpt custom classes available in the directory
|
||||||
|
"""
|
||||||
|
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver
|
||||||
|
|
||||||
|
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
|
||||||
|
|
||||||
|
directory = Path(config.get('hyperopt_path', config['user_data_dir'] / USERPATH_HYPEROPTS))
|
||||||
|
hyperopts = HyperOptResolver.search_all_objects(directory)
|
||||||
|
# Sort alphabetically
|
||||||
|
hyperopts = sorted(hyperopts, key=lambda x: x['name'])
|
||||||
|
hyperopts_to_print = [{'name': s['name'], 'location': s['location'].name} for s in hyperopts]
|
||||||
|
|
||||||
|
if args['print_one_column']:
|
||||||
|
print('\n'.join([s['name'] for s in hyperopts]))
|
||||||
|
else:
|
||||||
|
print(tabulate(hyperopts_to_print, headers='keys', tablefmt='pipe'))
|
||||||
|
|
||||||
|
|
||||||
def start_list_timeframes(args: Dict[str, Any]) -> None:
|
def start_list_timeframes(args: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
Print ticker intervals (timeframes) available on Exchange
|
Print ticker intervals (timeframes) available on Exchange
|
||||||
|
@ -5,7 +5,7 @@ from freqtrade.exceptions import OperationalException
|
|||||||
from freqtrade.state import RunMode
|
from freqtrade.state import RunMode
|
||||||
|
|
||||||
|
|
||||||
def validate_plot_args(args: Dict[str, Any]):
|
def validate_plot_args(args: Dict[str, Any]) -> None:
|
||||||
if not args.get('datadir') and not args.get('config'):
|
if not args.get('datadir') and not args.get('config'):
|
||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
"You need to specify either `--datadir` or `--config` "
|
"You need to specify either `--datadir` or `--config` "
|
||||||
|
@ -10,7 +10,7 @@ from freqtrade.state import RunMode
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def remove_credentials(config: Dict[str, Any]):
|
def remove_credentials(config: Dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
Removes exchange keys from the configuration and specifies dry-run
|
Removes exchange keys from the configuration and specifies dry-run
|
||||||
Used for backtesting / hyperopt / edge and utils.
|
Used for backtesting / hyperopt / edge and utils.
|
||||||
|
@ -13,7 +13,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def check_conflicting_settings(config: Dict[str, Any],
|
def check_conflicting_settings(config: Dict[str, Any],
|
||||||
section1: str, name1: str,
|
section1: str, name1: str,
|
||||||
section2: str, name2: str):
|
section2: str, name2: str) -> None:
|
||||||
section1_config = config.get(section1, {})
|
section1_config = config.get(section1, {})
|
||||||
section2_config = config.get(section2, {})
|
section2_config = config.get(section2, {})
|
||||||
if name1 in section1_config and name2 in section2_config:
|
if name1 in section1_config and name2 in section2_config:
|
||||||
@ -28,7 +28,7 @@ def check_conflicting_settings(config: Dict[str, Any],
|
|||||||
|
|
||||||
def process_deprecated_setting(config: Dict[str, Any],
|
def process_deprecated_setting(config: Dict[str, Any],
|
||||||
section1: str, name1: str,
|
section1: str, name1: str,
|
||||||
section2: str, name2: str):
|
section2: str, name2: str) -> None:
|
||||||
section2_config = config.get(section2, {})
|
section2_config = config.get(section2, {})
|
||||||
|
|
||||||
if name2 in section2_config:
|
if name2 in section2_config:
|
||||||
|
@ -23,7 +23,7 @@ def create_datadir(config: Dict[str, Any], datadir: Optional[str] = None) -> Pat
|
|||||||
return folder
|
return folder
|
||||||
|
|
||||||
|
|
||||||
def create_userdata_dir(directory: str, create_dir=False) -> Path:
|
def create_userdata_dir(directory: str, create_dir: bool = False) -> Path:
|
||||||
"""
|
"""
|
||||||
Create userdata directory structure.
|
Create userdata directory structure.
|
||||||
if create_dir is True, then the parent-directory will be created if it does not exist.
|
if create_dir is True, then the parent-directory will be created if it does not exist.
|
||||||
|
@ -7,6 +7,7 @@ from typing import Optional
|
|||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -30,7 +31,7 @@ class TimeRange:
|
|||||||
return (self.starttype == other.starttype and self.stoptype == other.stoptype
|
return (self.starttype == other.starttype and self.stoptype == other.stoptype
|
||||||
and self.startts == other.startts and self.stopts == other.stopts)
|
and self.startts == other.startts and self.stopts == other.stopts)
|
||||||
|
|
||||||
def subtract_start(self, seconds) -> None:
|
def subtract_start(self, seconds: int) -> None:
|
||||||
"""
|
"""
|
||||||
Subtracts <seconds> from startts if startts is set.
|
Subtracts <seconds> from startts if startts is set.
|
||||||
:param seconds: Seconds to subtract from starttime
|
:param seconds: Seconds to subtract from starttime
|
||||||
@ -59,7 +60,7 @@ class TimeRange:
|
|||||||
self.starttype = 'date'
|
self.starttype = 'date'
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def parse_timerange(text: Optional[str]):
|
def parse_timerange(text: Optional[str]) -> 'TimeRange':
|
||||||
"""
|
"""
|
||||||
Parse the value of the argument --timerange to determine what is the range desired
|
Parse the value of the argument --timerange to determine what is the range desired
|
||||||
:param text: value from --timerange
|
:param text: value from --timerange
|
||||||
|
@ -17,20 +17,22 @@ REQUIRED_ORDERTIF = ['buy', 'sell']
|
|||||||
REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
|
||||||
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
|
||||||
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
|
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
|
||||||
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'PrecisionFilter', 'PriceFilter']
|
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
|
||||||
|
'PrecisionFilter', 'PriceFilter', 'SpreadFilter']
|
||||||
DRY_RUN_WALLET = 1000
|
DRY_RUN_WALLET = 1000
|
||||||
MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons
|
MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons
|
||||||
|
|
||||||
USERPATH_HYPEROPTS = 'hyperopts'
|
USERPATH_HYPEROPTS = 'hyperopts'
|
||||||
USERPATH_STRATEGY = 'strategies'
|
USERPATH_STRATEGIES = 'strategies'
|
||||||
|
USERPATH_NOTEBOOKS = 'notebooks'
|
||||||
|
|
||||||
# Soure files with destination directories within user-directory
|
# Soure files with destination directories within user-directory
|
||||||
USER_DATA_FILES = {
|
USER_DATA_FILES = {
|
||||||
'sample_strategy.py': USERPATH_STRATEGY,
|
'sample_strategy.py': USERPATH_STRATEGIES,
|
||||||
'sample_hyperopt_advanced.py': USERPATH_HYPEROPTS,
|
'sample_hyperopt_advanced.py': USERPATH_HYPEROPTS,
|
||||||
'sample_hyperopt_loss.py': USERPATH_HYPEROPTS,
|
'sample_hyperopt_loss.py': USERPATH_HYPEROPTS,
|
||||||
'sample_hyperopt.py': USERPATH_HYPEROPTS,
|
'sample_hyperopt.py': USERPATH_HYPEROPTS,
|
||||||
'strategy_analysis_example.ipynb': 'notebooks',
|
'strategy_analysis_example.ipynb': USERPATH_NOTEBOOKS,
|
||||||
}
|
}
|
||||||
|
|
||||||
SUPPORTED_FIAT = [
|
SUPPORTED_FIAT = [
|
||||||
|
@ -3,7 +3,7 @@ Helpers when analyzing backtest data
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict
|
from typing import Dict, Union
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@ -20,7 +20,7 @@ BT_DATA_COLUMNS = ["pair", "profitperc", "open_time", "close_time", "index", "du
|
|||||||
"open_rate", "close_rate", "open_at_end", "sell_reason"]
|
"open_rate", "close_rate", "open_at_end", "sell_reason"]
|
||||||
|
|
||||||
|
|
||||||
def load_backtest_data(filename) -> pd.DataFrame:
|
def load_backtest_data(filename: Union[Path, str]) -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Load backtest data file.
|
Load backtest data file.
|
||||||
:param filename: pathlib.Path object, or string pointing to the file.
|
:param filename: pathlib.Path object, or string pointing to the file.
|
||||||
@ -151,7 +151,8 @@ def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> p
|
|||||||
return trades
|
return trades
|
||||||
|
|
||||||
|
|
||||||
def combine_tickers_with_mean(tickers: Dict[str, pd.DataFrame], column: str = "close"):
|
def combine_tickers_with_mean(tickers: Dict[str, pd.DataFrame],
|
||||||
|
column: str = "close") -> pd.DataFrame:
|
||||||
"""
|
"""
|
||||||
Combine multiple dataframes "column"
|
Combine multiple dataframes "column"
|
||||||
:param tickers: Dict of Dataframes, dict key should be pair.
|
:param tickers: Dict of Dataframes, dict key should be pair.
|
||||||
|
@ -86,7 +86,7 @@ def load_tickerdata_file(datadir: Path, pair: str, timeframe: str,
|
|||||||
|
|
||||||
|
|
||||||
def store_tickerdata_file(datadir: Path, pair: str,
|
def store_tickerdata_file(datadir: Path, pair: str,
|
||||||
timeframe: str, data: list, is_zip: bool = False):
|
timeframe: str, data: list, is_zip: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
Stores tickerdata to file
|
Stores tickerdata to file
|
||||||
"""
|
"""
|
||||||
@ -109,7 +109,7 @@ def load_trades_file(datadir: Path, pair: str,
|
|||||||
|
|
||||||
|
|
||||||
def store_trades_file(datadir: Path, pair: str,
|
def store_trades_file(datadir: Path, pair: str,
|
||||||
data: list, is_zip: bool = True):
|
data: list, is_zip: bool = True) -> None:
|
||||||
"""
|
"""
|
||||||
Stores tickerdata to file
|
Stores tickerdata to file
|
||||||
"""
|
"""
|
||||||
@ -117,7 +117,7 @@ def store_trades_file(datadir: Path, pair: str,
|
|||||||
misc.file_dump_json(filename, data, is_zip=is_zip)
|
misc.file_dump_json(filename, data, is_zip=is_zip)
|
||||||
|
|
||||||
|
|
||||||
def _validate_pairdata(pair, pairdata, timerange: TimeRange):
|
def _validate_pairdata(pair: str, pairdata: List[Dict], timerange: TimeRange) -> None:
|
||||||
if timerange.starttype == 'date' and pairdata[0][0] > timerange.startts * 1000:
|
if timerange.starttype == 'date' and pairdata[0][0] > timerange.startts * 1000:
|
||||||
logger.warning('Missing data at start for pair %s, data starts at %s',
|
logger.warning('Missing data at start for pair %s, data starts at %s',
|
||||||
pair, arrow.get(pairdata[0][0] // 1000).strftime('%Y-%m-%d %H:%M:%S'))
|
pair, arrow.get(pairdata[0][0] // 1000).strftime('%Y-%m-%d %H:%M:%S'))
|
||||||
@ -331,7 +331,7 @@ def _download_pair_history(datadir: Path,
|
|||||||
|
|
||||||
def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes: List[str],
|
def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes: List[str],
|
||||||
datadir: Path, timerange: Optional[TimeRange] = None,
|
datadir: Path, timerange: Optional[TimeRange] = None,
|
||||||
erase=False) -> List[str]:
|
erase: bool = False) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Refresh stored ohlcv data for backtesting and hyperopt operations.
|
Refresh stored ohlcv data for backtesting and hyperopt operations.
|
||||||
Used by freqtrade download-data subcommand.
|
Used by freqtrade download-data subcommand.
|
||||||
@ -401,7 +401,7 @@ def _download_trades_history(datadir: Path,
|
|||||||
|
|
||||||
|
|
||||||
def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: Path,
|
def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: Path,
|
||||||
timerange: TimeRange, erase=False) -> List[str]:
|
timerange: TimeRange, erase: bool = False) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Refresh stored trades data for backtesting and hyperopt operations.
|
Refresh stored trades data for backtesting and hyperopt operations.
|
||||||
Used by freqtrade download-data subcommand.
|
Used by freqtrade download-data subcommand.
|
||||||
@ -428,7 +428,7 @@ def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir:
|
|||||||
|
|
||||||
|
|
||||||
def convert_trades_to_ohlcv(pairs: List[str], timeframes: List[str],
|
def convert_trades_to_ohlcv(pairs: List[str], timeframes: List[str],
|
||||||
datadir: Path, timerange: TimeRange, erase=False) -> None:
|
datadir: Path, timerange: TimeRange, erase: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
Convert stored trades data to ohlcv data
|
Convert stored trades data to ohlcv data
|
||||||
"""
|
"""
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# pragma pylint: disable=W0603
|
# pragma pylint: disable=W0603
|
||||||
""" Edge positioning package """
|
""" Edge positioning package """
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, NamedTuple
|
from typing import Any, Dict, List, NamedTuple
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -181,7 +181,7 @@ class Edge:
|
|||||||
'strategy stoploss is returned instead.')
|
'strategy stoploss is returned instead.')
|
||||||
return self.strategy.stoploss
|
return self.strategy.stoploss
|
||||||
|
|
||||||
def adjust(self, pairs) -> list:
|
def adjust(self, pairs: List[str]) -> list:
|
||||||
"""
|
"""
|
||||||
Filters out and sorts "pairs" according to Edge calculated pairs
|
Filters out and sorts "pairs" according to Edge calculated pairs
|
||||||
"""
|
"""
|
||||||
|
@ -32,13 +32,23 @@ class Binance(Exchange):
|
|||||||
|
|
||||||
return super().get_order_book(pair, limit)
|
return super().get_order_book(pair, limit)
|
||||||
|
|
||||||
def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict:
|
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||||
|
"""
|
||||||
|
Verify stop_loss against stoploss-order value (limit or price)
|
||||||
|
Returns True if adjustment is necessary.
|
||||||
|
"""
|
||||||
|
return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice'])
|
||||||
|
|
||||||
|
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||||
"""
|
"""
|
||||||
creates a stoploss limit order.
|
creates a stoploss limit order.
|
||||||
this stoploss-limit is binance-specific.
|
this stoploss-limit is binance-specific.
|
||||||
It may work with a limited number of other exchanges, but this has not been tested yet.
|
It may work with a limited number of other exchanges, but this has not been tested yet.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# Limit price threshold: As limit price should always be below stop-price
|
||||||
|
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
||||||
|
rate = stop_price * limit_price_pct
|
||||||
|
|
||||||
ordertype = "stop_loss_limit"
|
ordertype = "stop_loss_limit"
|
||||||
|
|
||||||
stop_price = self.price_to_precision(pair, stop_price)
|
stop_price = self.price_to_precision(pair, stop_price)
|
||||||
@ -61,8 +71,8 @@ class Binance(Exchange):
|
|||||||
|
|
||||||
rate = self.price_to_precision(pair, rate)
|
rate = self.price_to_precision(pair, rate)
|
||||||
|
|
||||||
order = self._api.create_order(pair, ordertype, 'sell',
|
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
||||||
amount, rate, params)
|
amount=amount, price=stop_price, params=params)
|
||||||
logger.info('stoploss limit order added for %s. '
|
logger.info('stoploss limit order added for %s. '
|
||||||
'stop price: %s. limit: %s', pair, stop_price, rate)
|
'stop price: %s. limit: %s', pair, stop_price, rate)
|
||||||
return order
|
return order
|
||||||
|
@ -24,6 +24,10 @@ from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
|||||||
from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async
|
from freqtrade.exchange.common import BAD_EXCHANGES, retrier, retrier_async
|
||||||
from freqtrade.misc import deep_merge_dicts
|
from freqtrade.misc import deep_merge_dicts
|
||||||
|
|
||||||
|
|
||||||
|
CcxtModuleType = Any
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@ -51,7 +55,7 @@ class Exchange:
|
|||||||
}
|
}
|
||||||
_ft_has: Dict = {}
|
_ft_has: Dict = {}
|
||||||
|
|
||||||
def __init__(self, config: dict, validate: bool = True) -> None:
|
def __init__(self, config: Dict[str, Any], validate: bool = True) -> None:
|
||||||
"""
|
"""
|
||||||
Initializes this module with the given config,
|
Initializes this module with the given config,
|
||||||
it does basic validation whether the specified exchange and pairs are valid.
|
it does basic validation whether the specified exchange and pairs are valid.
|
||||||
@ -135,7 +139,7 @@ class Exchange:
|
|||||||
if self._api_async and inspect.iscoroutinefunction(self._api_async.close):
|
if self._api_async and inspect.iscoroutinefunction(self._api_async.close):
|
||||||
asyncio.get_event_loop().run_until_complete(self._api_async.close())
|
asyncio.get_event_loop().run_until_complete(self._api_async.close())
|
||||||
|
|
||||||
def _init_ccxt(self, exchange_config: dict, ccxt_module=ccxt,
|
def _init_ccxt(self, exchange_config: Dict[str, Any], ccxt_module: CcxtModuleType = ccxt,
|
||||||
ccxt_kwargs: dict = None) -> ccxt.Exchange:
|
ccxt_kwargs: dict = None) -> ccxt.Exchange:
|
||||||
"""
|
"""
|
||||||
Initialize ccxt with given config and return valid
|
Initialize ccxt with given config and return valid
|
||||||
@ -224,13 +228,13 @@ class Exchange:
|
|||||||
markets = self.markets
|
markets = self.markets
|
||||||
return sorted(set([x['quote'] for _, x in markets.items()]))
|
return sorted(set([x['quote'] for _, x in markets.items()]))
|
||||||
|
|
||||||
def klines(self, pair_interval: Tuple[str, str], copy=True) -> DataFrame:
|
def klines(self, pair_interval: Tuple[str, str], copy: bool = True) -> DataFrame:
|
||||||
if pair_interval in self._klines:
|
if pair_interval in self._klines:
|
||||||
return self._klines[pair_interval].copy() if copy else self._klines[pair_interval]
|
return self._klines[pair_interval].copy() if copy else self._klines[pair_interval]
|
||||||
else:
|
else:
|
||||||
return DataFrame()
|
return DataFrame()
|
||||||
|
|
||||||
def set_sandbox(self, api, exchange_config: dict, name: str):
|
def set_sandbox(self, api: ccxt.Exchange, exchange_config: dict, name: str) -> None:
|
||||||
if exchange_config.get('sandbox'):
|
if exchange_config.get('sandbox'):
|
||||||
if api.urls.get('test'):
|
if api.urls.get('test'):
|
||||||
api.urls['api'] = api.urls['test']
|
api.urls['api'] = api.urls['test']
|
||||||
@ -240,7 +244,7 @@ class Exchange:
|
|||||||
"Please check your config.json")
|
"Please check your config.json")
|
||||||
raise OperationalException(f'Exchange {name} does not provide a sandbox api')
|
raise OperationalException(f'Exchange {name} does not provide a sandbox api')
|
||||||
|
|
||||||
def _load_async_markets(self, reload=False) -> None:
|
def _load_async_markets(self, reload: bool = False) -> None:
|
||||||
try:
|
try:
|
||||||
if self._api_async:
|
if self._api_async:
|
||||||
asyncio.get_event_loop().run_until_complete(
|
asyncio.get_event_loop().run_until_complete(
|
||||||
@ -273,7 +277,7 @@ class Exchange:
|
|||||||
except ccxt.BaseError:
|
except ccxt.BaseError:
|
||||||
logger.exception("Could not reload markets.")
|
logger.exception("Could not reload markets.")
|
||||||
|
|
||||||
def validate_stakecurrency(self, stake_currency) -> None:
|
def validate_stakecurrency(self, stake_currency: str) -> None:
|
||||||
"""
|
"""
|
||||||
Checks stake-currency against available currencies on the exchange.
|
Checks stake-currency against available currencies on the exchange.
|
||||||
:param stake_currency: Stake-currency to validate
|
:param stake_currency: Stake-currency to validate
|
||||||
@ -319,7 +323,7 @@ class Exchange:
|
|||||||
f"Please check if you are impacted by this restriction "
|
f"Please check if you are impacted by this restriction "
|
||||||
f"on the exchange and eventually remove {pair} from your whitelist.")
|
f"on the exchange and eventually remove {pair} from your whitelist.")
|
||||||
|
|
||||||
def get_valid_pair_combination(self, curr_1, curr_2) -> str:
|
def get_valid_pair_combination(self, curr_1: str, curr_2: str) -> str:
|
||||||
"""
|
"""
|
||||||
Get valid pair combination of curr_1 and curr_2 by trying both combinations.
|
Get valid pair combination of curr_1 and curr_2 by trying both combinations.
|
||||||
"""
|
"""
|
||||||
@ -373,7 +377,7 @@ class Exchange:
|
|||||||
raise OperationalException(
|
raise OperationalException(
|
||||||
f'Time in force policies are not supported for {self.name} yet.')
|
f'Time in force policies are not supported for {self.name} yet.')
|
||||||
|
|
||||||
def validate_required_startup_candles(self, startup_candles) -> None:
|
def validate_required_startup_candles(self, startup_candles: int) -> None:
|
||||||
"""
|
"""
|
||||||
Checks if required startup_candles is more than ohlcv_candle_limit.
|
Checks if required startup_candles is more than ohlcv_candle_limit.
|
||||||
Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default.
|
Requires a grace-period of 5 candles - so a startup-period up to 494 is allowed by default.
|
||||||
@ -392,7 +396,7 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
return endpoint in self._api.has and self._api.has[endpoint]
|
return endpoint in self._api.has and self._api.has[endpoint]
|
||||||
|
|
||||||
def amount_to_precision(self, pair, amount: float) -> float:
|
def amount_to_precision(self, pair: str, amount: float) -> float:
|
||||||
'''
|
'''
|
||||||
Returns the amount to buy or sell to a precision the Exchange accepts
|
Returns the amount to buy or sell to a precision the Exchange accepts
|
||||||
Reimplementation of ccxt internal methods - ensuring we can test the result is correct
|
Reimplementation of ccxt internal methods - ensuring we can test the result is correct
|
||||||
@ -406,7 +410,7 @@ class Exchange:
|
|||||||
|
|
||||||
return amount
|
return amount
|
||||||
|
|
||||||
def price_to_precision(self, pair, price: float) -> float:
|
def price_to_precision(self, pair: str, price: float) -> float:
|
||||||
'''
|
'''
|
||||||
Returns the price rounded up to the precision the Exchange accepts.
|
Returns the price rounded up to the precision the Exchange accepts.
|
||||||
Partial Reimplementation of ccxt internal method decimal_to_precision(),
|
Partial Reimplementation of ccxt internal method decimal_to_precision(),
|
||||||
@ -494,7 +498,7 @@ class Exchange:
|
|||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
def buy(self, pair: str, ordertype: str, amount: float,
|
def buy(self, pair: str, ordertype: str, amount: float,
|
||||||
rate: float, time_in_force) -> Dict:
|
rate: float, time_in_force: str) -> Dict:
|
||||||
|
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
dry_order = self.dry_run_order(pair, ordertype, "buy", amount, rate)
|
dry_order = self.dry_run_order(pair, ordertype, "buy", amount, rate)
|
||||||
@ -507,7 +511,7 @@ class Exchange:
|
|||||||
return self.create_order(pair, ordertype, 'buy', amount, rate, params)
|
return self.create_order(pair, ordertype, 'buy', amount, rate, params)
|
||||||
|
|
||||||
def sell(self, pair: str, ordertype: str, amount: float,
|
def sell(self, pair: str, ordertype: str, amount: float,
|
||||||
rate: float, time_in_force='gtc') -> Dict:
|
rate: float, time_in_force: str = 'gtc') -> Dict:
|
||||||
|
|
||||||
if self._config['dry_run']:
|
if self._config['dry_run']:
|
||||||
dry_order = self.dry_run_order(pair, ordertype, "sell", amount, rate)
|
dry_order = self.dry_run_order(pair, ordertype, "sell", amount, rate)
|
||||||
@ -519,9 +523,17 @@ class Exchange:
|
|||||||
|
|
||||||
return self.create_order(pair, ordertype, 'sell', amount, rate, params)
|
return self.create_order(pair, ordertype, 'sell', amount, rate, params)
|
||||||
|
|
||||||
def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict:
|
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||||
"""
|
"""
|
||||||
creates a stoploss limit order.
|
Verify stop_loss against stoploss-order value (limit or price)
|
||||||
|
Returns True if adjustment is necessary.
|
||||||
|
"""
|
||||||
|
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||||
|
|
||||||
|
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||||
|
"""
|
||||||
|
creates a stoploss order.
|
||||||
|
The precise ordertype is determined by the order_types dict or exchange default.
|
||||||
Since ccxt does not unify stoploss-limit orders yet, this needs to be implemented in each
|
Since ccxt does not unify stoploss-limit orders yet, this needs to be implemented in each
|
||||||
exchange's subclass.
|
exchange's subclass.
|
||||||
The exception below should never raise, since we disallow
|
The exception below should never raise, since we disallow
|
||||||
@ -529,7 +541,7 @@ class Exchange:
|
|||||||
Note: Changes to this interface need to be applied to all sub-classes too.
|
Note: Changes to this interface need to be applied to all sub-classes too.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
raise OperationalException(f"stoploss_limit is not implemented for {self.name}.")
|
raise OperationalException(f"stoploss is not implemented for {self.name}.")
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def get_balance(self, currency: str) -> float:
|
def get_balance(self, currency: str) -> float:
|
||||||
@ -728,10 +740,11 @@ class Exchange:
|
|||||||
f'Exchange {self._api.name} does not support fetching historical candlestick data.'
|
f'Exchange {self._api.name} does not support fetching historical candlestick data.'
|
||||||
f'Message: {e}') from e
|
f'Message: {e}') from e
|
||||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
raise TemporaryError(f'Could not load ticker history due to {e.__class__.__name__}. '
|
raise TemporaryError(f'Could not load ticker history for pair {pair} due to '
|
||||||
f'Message: {e}') from e
|
f'{e.__class__.__name__}. Message: {e}') from e
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(f'Could not fetch ticker data. Msg: {e}') from e
|
raise OperationalException(f'Could not fetch ticker data for pair {pair}. '
|
||||||
|
f'Msg: {e}') from e
|
||||||
|
|
||||||
@retrier_async
|
@retrier_async
|
||||||
async def _async_fetch_trades(self, pair: str,
|
async def _async_fetch_trades(self, pair: str,
|
||||||
@ -976,8 +989,8 @@ class Exchange:
|
|||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
@retrier
|
@retrier
|
||||||
def get_fee(self, symbol, type='', side='', amount=1,
|
def get_fee(self, symbol: str, type: str = '', side: str = '', amount: float = 1,
|
||||||
price=1, taker_or_maker='maker') -> float:
|
price: float = 1, taker_or_maker: str = 'maker') -> float:
|
||||||
try:
|
try:
|
||||||
# validate that markets are loaded before trying to get fee
|
# validate that markets are loaded before trying to get fee
|
||||||
if self._api.markets is None or len(self._api.markets) == 0:
|
if self._api.markets is None or len(self._api.markets) == 0:
|
||||||
@ -1000,7 +1013,7 @@ def get_exchange_bad_reason(exchange_name: str) -> str:
|
|||||||
return BAD_EXCHANGES.get(exchange_name, "")
|
return BAD_EXCHANGES.get(exchange_name, "")
|
||||||
|
|
||||||
|
|
||||||
def is_exchange_known_ccxt(exchange_name: str, ccxt_module=None) -> bool:
|
def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
|
||||||
return exchange_name in ccxt_exchanges(ccxt_module)
|
return exchange_name in ccxt_exchanges(ccxt_module)
|
||||||
|
|
||||||
|
|
||||||
@ -1008,14 +1021,14 @@ def is_exchange_officially_supported(exchange_name: str) -> bool:
|
|||||||
return exchange_name in ['bittrex', 'binance']
|
return exchange_name in ['bittrex', 'binance']
|
||||||
|
|
||||||
|
|
||||||
def ccxt_exchanges(ccxt_module=None) -> List[str]:
|
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Return the list of all exchanges known to ccxt
|
Return the list of all exchanges known to ccxt
|
||||||
"""
|
"""
|
||||||
return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges
|
return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges
|
||||||
|
|
||||||
|
|
||||||
def available_exchanges(ccxt_module=None) -> List[str]:
|
def available_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Return exchanges available to the bot, i.e. non-bad exchanges in the ccxt list
|
Return exchanges available to the bot, i.e. non-bad exchanges in the ccxt list
|
||||||
"""
|
"""
|
||||||
@ -1075,7 +1088,8 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime:
|
|||||||
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
|
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
def symbol_is_pair(market_symbol: str, base_currency: str = None, quote_currency: str = None):
|
def symbol_is_pair(market_symbol: str, base_currency: str = None,
|
||||||
|
quote_currency: str = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Check if the market symbol is a pair, i.e. that its symbol consists of the base currency and the
|
Check if the market symbol is a pair, i.e. that its symbol consists of the base currency and the
|
||||||
quote currency separated by '/' character. If base_currency and/or quote_currency is passed,
|
quote currency separated by '/' character. If base_currency and/or quote_currency is passed,
|
||||||
@ -1088,7 +1102,7 @@ def symbol_is_pair(market_symbol: str, base_currency: str = None, quote_currency
|
|||||||
(symbol_parts[1] == quote_currency if quote_currency else len(symbol_parts[1]) > 0))
|
(symbol_parts[1] == quote_currency if quote_currency else len(symbol_parts[1]) > 0))
|
||||||
|
|
||||||
|
|
||||||
def market_is_active(market):
|
def market_is_active(market: Dict) -> bool:
|
||||||
"""
|
"""
|
||||||
Return True if the market is active.
|
Return True if the market is active.
|
||||||
"""
|
"""
|
||||||
|
@ -4,7 +4,8 @@ from typing import Dict
|
|||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
|
|
||||||
from freqtrade.exceptions import OperationalException, TemporaryError
|
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||||
|
OperationalException, TemporaryError)
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchange
|
||||||
from freqtrade.exchange.exchange import retrier
|
from freqtrade.exchange.exchange import retrier
|
||||||
|
|
||||||
@ -15,6 +16,7 @@ class Kraken(Exchange):
|
|||||||
|
|
||||||
_params: Dict = {"trading_agreement": "agree"}
|
_params: Dict = {"trading_agreement": "agree"}
|
||||||
_ft_has: Dict = {
|
_ft_has: Dict = {
|
||||||
|
"stoploss_on_exchange": True,
|
||||||
"trades_pagination": "id",
|
"trades_pagination": "id",
|
||||||
"trades_pagination_arg": "since",
|
"trades_pagination_arg": "since",
|
||||||
}
|
}
|
||||||
@ -48,3 +50,51 @@ class Kraken(Exchange):
|
|||||||
f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e
|
f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
except ccxt.BaseError as e:
|
except ccxt.BaseError as e:
|
||||||
raise OperationalException(e) from e
|
raise OperationalException(e) from e
|
||||||
|
|
||||||
|
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
|
||||||
|
"""
|
||||||
|
Verify stop_loss against stoploss-order value (limit or price)
|
||||||
|
Returns True if adjustment is necessary.
|
||||||
|
"""
|
||||||
|
return order['type'] == 'stop-loss' and stop_loss > float(order['price'])
|
||||||
|
|
||||||
|
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
|
||||||
|
"""
|
||||||
|
Creates a stoploss market order.
|
||||||
|
Stoploss market orders is the only stoploss type supported by kraken.
|
||||||
|
"""
|
||||||
|
|
||||||
|
ordertype = "stop-loss"
|
||||||
|
|
||||||
|
stop_price = self.price_to_precision(pair, stop_price)
|
||||||
|
|
||||||
|
if self._config['dry_run']:
|
||||||
|
dry_order = self.dry_run_order(
|
||||||
|
pair, ordertype, "sell", amount, stop_price)
|
||||||
|
return dry_order
|
||||||
|
|
||||||
|
try:
|
||||||
|
params = self._params.copy()
|
||||||
|
|
||||||
|
amount = self.amount_to_precision(pair, amount)
|
||||||
|
|
||||||
|
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
|
||||||
|
amount=amount, price=stop_price, params=params)
|
||||||
|
logger.info('stoploss order added for %s. '
|
||||||
|
'stop price: %s.', pair, stop_price)
|
||||||
|
return order
|
||||||
|
except ccxt.InsufficientFunds as e:
|
||||||
|
raise DependencyException(
|
||||||
|
f'Insufficient funds to create {ordertype} sell order on market {pair}.'
|
||||||
|
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||||
|
f'Message: {e}') from e
|
||||||
|
except ccxt.InvalidOrder as e:
|
||||||
|
raise InvalidOrderException(
|
||||||
|
f'Could not create {ordertype} sell order on market {pair}. '
|
||||||
|
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
|
||||||
|
f'Message: {e}') from e
|
||||||
|
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||||
|
raise TemporaryError(
|
||||||
|
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
|
||||||
|
except ccxt.BaseError as e:
|
||||||
|
raise OperationalException(e) from e
|
||||||
|
@ -265,7 +265,7 @@ class FreqtradeBot:
|
|||||||
|
|
||||||
return used_rate
|
return used_rate
|
||||||
|
|
||||||
def get_trade_stake_amount(self, pair) -> float:
|
def get_trade_stake_amount(self, pair: str) -> float:
|
||||||
"""
|
"""
|
||||||
Calculate stake amount for the trade
|
Calculate stake amount for the trade
|
||||||
:return: float: Stake amount
|
:return: float: Stake amount
|
||||||
@ -427,16 +427,23 @@ class FreqtradeBot:
|
|||||||
Checks depth of market before executing a buy
|
Checks depth of market before executing a buy
|
||||||
"""
|
"""
|
||||||
conf_bids_to_ask_delta = conf.get('bids_to_ask_delta', 0)
|
conf_bids_to_ask_delta = conf.get('bids_to_ask_delta', 0)
|
||||||
logger.info('checking depth of market for %s', pair)
|
logger.info(f"Checking depth of market for {pair} ...")
|
||||||
order_book = self.exchange.get_order_book(pair, 1000)
|
order_book = self.exchange.get_order_book(pair, 1000)
|
||||||
order_book_data_frame = order_book_to_dataframe(order_book['bids'], order_book['asks'])
|
order_book_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
|
bids_ask_delta = order_book_bids / order_book_asks
|
||||||
logger.info('bids: %s, asks: %s, delta: %s', order_book_bids,
|
logger.info(
|
||||||
order_book_asks, bids_ask_delta)
|
f"Bids: {order_book_bids}, Asks: {order_book_asks}, Delta: {bids_ask_delta}, "
|
||||||
|
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 Ask Quantity: {order_book['asks'][0][1]}."
|
||||||
|
)
|
||||||
if bids_ask_delta >= conf_bids_to_ask_delta:
|
if bids_ask_delta >= conf_bids_to_ask_delta:
|
||||||
|
logger.info(f"Bids to asks delta for {pair} DOES satisfy condition.")
|
||||||
return True
|
return True
|
||||||
|
else:
|
||||||
|
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None) -> bool:
|
def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None) -> bool:
|
||||||
@ -532,7 +539,7 @@ class FreqtradeBot:
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _notify_buy(self, trade: Trade, order_type: str):
|
def _notify_buy(self, trade: Trade, order_type: str) -> None:
|
||||||
"""
|
"""
|
||||||
Sends rpc notification when a buy occured.
|
Sends rpc notification when a buy occured.
|
||||||
"""
|
"""
|
||||||
@ -620,7 +627,7 @@ class FreqtradeBot:
|
|||||||
self.dataprovider.ohlcv(trade.pair, self.strategy.ticker_interval))
|
self.dataprovider.ohlcv(trade.pair, self.strategy.ticker_interval))
|
||||||
|
|
||||||
if config_ask_strategy.get('use_order_book', False):
|
if config_ask_strategy.get('use_order_book', False):
|
||||||
logger.info('Using order book for selling...')
|
logger.debug(f'Using order book for selling {trade.pair}...')
|
||||||
# logger.debug('Order book %s',orderBook)
|
# logger.debug('Order book %s',orderBook)
|
||||||
order_book_min = config_ask_strategy.get('order_book_min', 1)
|
order_book_min = config_ask_strategy.get('order_book_min', 1)
|
||||||
order_book_max = config_ask_strategy.get('order_book_max', 1)
|
order_book_max = config_ask_strategy.get('order_book_max', 1)
|
||||||
@ -629,7 +636,7 @@ class FreqtradeBot:
|
|||||||
|
|
||||||
for i in range(order_book_min, order_book_max + 1):
|
for i in range(order_book_min, order_book_max + 1):
|
||||||
order_book_rate = order_book['asks'][i - 1][0]
|
order_book_rate = order_book['asks'][i - 1][0]
|
||||||
logger.info(' order book asks top %s: %0.8f', i, order_book_rate)
|
logger.debug(' order book asks top %s: %0.8f', i, order_book_rate)
|
||||||
sell_rate = order_book_rate
|
sell_rate = order_book_rate
|
||||||
|
|
||||||
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
|
if self._check_and_execute_sell(trade, sell_rate, buy, sell):
|
||||||
@ -651,13 +658,10 @@ class FreqtradeBot:
|
|||||||
Force-sells the pair (using EmergencySell reason) in case of Problems creating the order.
|
Force-sells the pair (using EmergencySell reason) in case of Problems creating the order.
|
||||||
:return: True if the order succeeded, and False in case of problems.
|
:return: True if the order succeeded, and False in case of problems.
|
||||||
"""
|
"""
|
||||||
# Limit price threshold: As limit price should always be below stop-price
|
|
||||||
LIMIT_PRICE_PCT = self.strategy.order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
stoploss_order = self.exchange.stoploss_limit(pair=trade.pair, amount=trade.amount,
|
stoploss_order = self.exchange.stoploss(pair=trade.pair, amount=trade.amount,
|
||||||
stop_price=stop_price,
|
stop_price=stop_price,
|
||||||
rate=rate * LIMIT_PRICE_PCT)
|
order_types=self.strategy.order_types)
|
||||||
trade.stoploss_order_id = str(stoploss_order['id'])
|
trade.stoploss_order_id = str(stoploss_order['id'])
|
||||||
return True
|
return True
|
||||||
except InvalidOrderException as e:
|
except InvalidOrderException as e:
|
||||||
@ -689,8 +693,24 @@ class FreqtradeBot:
|
|||||||
except InvalidOrderException as exception:
|
except InvalidOrderException as exception:
|
||||||
logger.warning('Unable to fetch stoploss order: %s', exception)
|
logger.warning('Unable to fetch stoploss order: %s', exception)
|
||||||
|
|
||||||
|
# We check if stoploss order is fulfilled
|
||||||
|
if stoploss_order and stoploss_order['status'] == 'closed':
|
||||||
|
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
||||||
|
trade.update(stoploss_order)
|
||||||
|
# Lock pair for one candle to prevent immediate rebuys
|
||||||
|
self.strategy.lock_pair(trade.pair,
|
||||||
|
timeframe_to_next_date(self.config['ticker_interval']))
|
||||||
|
self._notify_sell(trade, "stoploss")
|
||||||
|
return True
|
||||||
|
|
||||||
|
if trade.open_order_id or not trade.is_open:
|
||||||
|
# Trade has an open Buy or Sell order, Stoploss-handling can't happen in this case
|
||||||
|
# as the Amount on the exchange is tied up in another trade.
|
||||||
|
# The trade can be closed already (sell-order fill confirmation came in this iteration)
|
||||||
|
return False
|
||||||
|
|
||||||
# If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange
|
# If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange
|
||||||
if (not trade.open_order_id and 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
|
||||||
|
|
||||||
@ -709,16 +729,6 @@ class FreqtradeBot:
|
|||||||
trade.stoploss_order_id = None
|
trade.stoploss_order_id = None
|
||||||
logger.warning('Stoploss order was cancelled, but unable to recreate one.')
|
logger.warning('Stoploss order was cancelled, but unable to recreate one.')
|
||||||
|
|
||||||
# We check if stoploss order is fulfilled
|
|
||||||
if stoploss_order and stoploss_order['status'] == 'closed':
|
|
||||||
trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
|
|
||||||
trade.update(stoploss_order)
|
|
||||||
# Lock pair for one candle to prevent immediate rebuys
|
|
||||||
self.strategy.lock_pair(trade.pair,
|
|
||||||
timeframe_to_next_date(self.config['ticker_interval']))
|
|
||||||
self._notify_sell(trade, "stoploss")
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Finally we check if stoploss on exchange should be moved up because of trailing.
|
# Finally we check if stoploss on exchange should be moved up because of trailing.
|
||||||
if stoploss_order and self.config.get('trailing_stop', False):
|
if stoploss_order and self.config.get('trailing_stop', False):
|
||||||
# if trailing stoploss is enabled we check if stoploss value has changed
|
# if trailing stoploss is enabled we check if stoploss value has changed
|
||||||
@ -728,7 +738,7 @@ class FreqtradeBot:
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def handle_trailing_stoploss_on_exchange(self, trade: Trade, order):
|
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
|
||||||
@ -736,8 +746,7 @@ class FreqtradeBot:
|
|||||||
: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):
|
||||||
if trade.stop_loss > float(order['info']['stopPrice']):
|
|
||||||
# we check if the update is neccesary
|
# we check if the update is neccesary
|
||||||
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:
|
||||||
@ -751,10 +760,8 @@ class FreqtradeBot:
|
|||||||
f"for pair {trade.pair}")
|
f"for pair {trade.pair}")
|
||||||
|
|
||||||
# Create new stoploss order
|
# Create new stoploss order
|
||||||
if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss,
|
if not self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss,
|
||||||
rate=trade.stop_loss):
|
rate=trade.stop_loss):
|
||||||
return False
|
|
||||||
else:
|
|
||||||
logger.warning(f"Could not create trailing stoploss order "
|
logger.warning(f"Could not create trailing stoploss order "
|
||||||
f"for pair {trade.pair}.")
|
f"for pair {trade.pair}.")
|
||||||
|
|
||||||
@ -983,7 +990,7 @@ class FreqtradeBot:
|
|||||||
|
|
||||||
self._notify_sell(trade, order_type)
|
self._notify_sell(trade, order_type)
|
||||||
|
|
||||||
def _notify_sell(self, trade: Trade, order_type: str):
|
def _notify_sell(self, trade: Trade, order_type: str) -> None:
|
||||||
"""
|
"""
|
||||||
Sends rpc notification when a sell occured.
|
Sends rpc notification when a sell occured.
|
||||||
"""
|
"""
|
||||||
@ -1024,7 +1031,7 @@ class FreqtradeBot:
|
|||||||
# Common update trade state methods
|
# Common update trade state methods
|
||||||
#
|
#
|
||||||
|
|
||||||
def update_trade_state(self, trade, action_order: dict = None):
|
def update_trade_state(self, trade: Trade, action_order: dict = None) -> None:
|
||||||
"""
|
"""
|
||||||
Checks trades with open orders and updates the amount if necessary
|
Checks trades with open orders and updates the amount if necessary
|
||||||
"""
|
"""
|
||||||
|
@ -6,6 +6,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
from typing.io import IO
|
from typing.io import IO
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -40,7 +41,7 @@ def datesarray_to_datetimearray(dates: np.ndarray) -> np.ndarray:
|
|||||||
return dates.dt.to_pydatetime()
|
return dates.dt.to_pydatetime()
|
||||||
|
|
||||||
|
|
||||||
def file_dump_json(filename: Path, data, is_zip=False) -> None:
|
def file_dump_json(filename: Path, data: Any, is_zip: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
Dump JSON data into a file
|
Dump JSON data into a file
|
||||||
:param filename: file to create
|
:param filename: file to create
|
||||||
@ -61,7 +62,7 @@ def file_dump_json(filename: Path, data, is_zip=False) -> None:
|
|||||||
logger.debug(f'done json to "{filename}"')
|
logger.debug(f'done json to "{filename}"')
|
||||||
|
|
||||||
|
|
||||||
def json_load(datafile: IO):
|
def json_load(datafile: IO) -> Any:
|
||||||
"""
|
"""
|
||||||
load data with rapidjson
|
load data with rapidjson
|
||||||
Use this to have a consistent experience,
|
Use this to have a consistent experience,
|
||||||
@ -125,11 +126,11 @@ def round_dict(d, n):
|
|||||||
return {k: (round(v, n) if isinstance(v, float) else v) for k, v in d.items()}
|
return {k: (round(v, n) if isinstance(v, float) else v) for k, v in d.items()}
|
||||||
|
|
||||||
|
|
||||||
def plural(num, singular: str, plural: str = None) -> str:
|
def plural(num: float, singular: str, plural: str = None) -> str:
|
||||||
return singular if (num == 1 or num == -1) else plural or singular + 's'
|
return singular if (num == 1 or num == -1) else plural or singular + 's'
|
||||||
|
|
||||||
|
|
||||||
def render_template(templatefile: str, arguments: dict = {}):
|
def render_template(templatefile: str, arguments: dict = {}) -> str:
|
||||||
|
|
||||||
from jinja2 import Environment, PackageLoader, select_autoescape
|
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ from datetime import datetime, timedelta
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Dict, List, NamedTuple, Optional
|
from typing import Any, Dict, List, NamedTuple, Optional
|
||||||
|
|
||||||
|
import arrow
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.configuration import (TimeRange, remove_credentials,
|
from freqtrade.configuration import (TimeRange, remove_credentials,
|
||||||
@ -24,7 +25,7 @@ from freqtrade.optimize.optimize_reports import (
|
|||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||||
from freqtrade.state import RunMode
|
from freqtrade.state import RunMode
|
||||||
from freqtrade.strategy.interface import IStrategy, SellType
|
from freqtrade.strategy.interface import IStrategy, SellCheckTuple, SellType
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -148,7 +149,7 @@ class Backtesting:
|
|||||||
logger.info(f'Dumping backtest results to {recordfilename}')
|
logger.info(f'Dumping backtest results to {recordfilename}')
|
||||||
file_dump_json(recordfilename, records)
|
file_dump_json(recordfilename, records)
|
||||||
|
|
||||||
def _get_ticker_list(self, processed) -> Dict[str, DataFrame]:
|
def _get_ticker_list(self, processed: Dict) -> Dict[str, DataFrame]:
|
||||||
"""
|
"""
|
||||||
Helper function to convert a processed tickerlist into a list for performance reasons.
|
Helper function to convert a processed tickerlist into a list for performance reasons.
|
||||||
|
|
||||||
@ -175,7 +176,8 @@ class Backtesting:
|
|||||||
ticker[pair] = [x for x in ticker_data.itertuples()]
|
ticker[pair] = [x for x in ticker_data.itertuples()]
|
||||||
return ticker
|
return ticker
|
||||||
|
|
||||||
def _get_close_rate(self, sell_row, trade: Trade, sell, trade_dur) -> float:
|
def _get_close_rate(self, sell_row, trade: Trade, sell: SellCheckTuple,
|
||||||
|
trade_dur: int) -> float:
|
||||||
"""
|
"""
|
||||||
Get close rate for backtesting result
|
Get close rate for backtesting result
|
||||||
"""
|
"""
|
||||||
@ -280,7 +282,7 @@ class Backtesting:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def backtest(self, processed: Dict, stake_amount: float,
|
def backtest(self, processed: Dict, stake_amount: float,
|
||||||
start_date, end_date,
|
start_date: arrow.Arrow, end_date: arrow.Arrow,
|
||||||
max_open_trades: int = 0, position_stacking: bool = False) -> DataFrame:
|
max_open_trades: int = 0, position_stacking: bool = False) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Implement backtesting functionality
|
Implement backtesting functionality
|
||||||
@ -426,7 +428,10 @@ class Backtesting:
|
|||||||
results=results))
|
results=results))
|
||||||
|
|
||||||
print(' SELL REASON STATS '.center(133, '='))
|
print(' SELL REASON STATS '.center(133, '='))
|
||||||
print(generate_text_table_sell_reason(data, results))
|
print(generate_text_table_sell_reason(data,
|
||||||
|
stake_currency=self.config['stake_currency'],
|
||||||
|
max_open_trades=self.config['max_open_trades'],
|
||||||
|
results=results))
|
||||||
|
|
||||||
print(' LEFT OPEN TRADES REPORT '.center(133, '='))
|
print(' LEFT OPEN TRADES REPORT '.center(133, '='))
|
||||||
print(generate_text_table(data,
|
print(generate_text_table(data,
|
||||||
@ -436,7 +441,7 @@ class Backtesting:
|
|||||||
print()
|
print()
|
||||||
if len(all_results) > 1:
|
if len(all_results) > 1:
|
||||||
# Print Strategy summary table
|
# Print Strategy summary table
|
||||||
print(' Strategy Summary '.center(133, '='))
|
print(' STRATEGY SUMMARY '.center(133, '='))
|
||||||
print(generate_text_table_strategy(self.config['stake_currency'],
|
print(generate_text_table_strategy(self.config['stake_currency'],
|
||||||
self.config['max_open_trades'],
|
self.config['max_open_trades'],
|
||||||
all_results=all_results))
|
all_results=all_results))
|
||||||
|
@ -59,6 +59,7 @@ class Hyperopt:
|
|||||||
hyperopt = Hyperopt(config)
|
hyperopt = Hyperopt(config)
|
||||||
hyperopt.start()
|
hyperopt.start()
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, config: Dict[str, Any]) -> None:
|
def __init__(self, config: Dict[str, Any]) -> None:
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
@ -117,11 +118,11 @@ class Hyperopt:
|
|||||||
self.print_json = self.config.get('print_json', False)
|
self.print_json = self.config.get('print_json', False)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_lock_filename(config) -> str:
|
def get_lock_filename(config: Dict[str, Any]) -> str:
|
||||||
|
|
||||||
return str(config['user_data_dir'] / 'hyperopt.lock')
|
return str(config['user_data_dir'] / 'hyperopt.lock')
|
||||||
|
|
||||||
def clean_hyperopt(self):
|
def clean_hyperopt(self) -> None:
|
||||||
"""
|
"""
|
||||||
Remove hyperopt pickle files to restart hyperopt.
|
Remove hyperopt pickle files to restart hyperopt.
|
||||||
"""
|
"""
|
||||||
@ -158,7 +159,7 @@ class Hyperopt:
|
|||||||
f"saved to '{self.trials_file}'.")
|
f"saved to '{self.trials_file}'.")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _read_trials(trials_file) -> List:
|
def _read_trials(trials_file: Path) -> List:
|
||||||
"""
|
"""
|
||||||
Read hyperopt trials file
|
Read hyperopt trials file
|
||||||
"""
|
"""
|
||||||
@ -189,7 +190,7 @@ class Hyperopt:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def print_epoch_details(results, total_epochs, print_json: bool,
|
def print_epoch_details(results, total_epochs: int, print_json: bool,
|
||||||
no_header: bool = False, header_str: str = None) -> None:
|
no_header: bool = False, header_str: str = None) -> None:
|
||||||
"""
|
"""
|
||||||
Display details of the hyperopt result
|
Display details of the hyperopt result
|
||||||
@ -218,7 +219,7 @@ class Hyperopt:
|
|||||||
Hyperopt._params_pretty_print(params, 'trailing', "Trailing stop:")
|
Hyperopt._params_pretty_print(params, 'trailing', "Trailing stop:")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _params_update_for_json(result_dict, params, space: str):
|
def _params_update_for_json(result_dict, params, space: str) -> None:
|
||||||
if space in params:
|
if space in params:
|
||||||
space_params = Hyperopt._space_params(params, space)
|
space_params = Hyperopt._space_params(params, space)
|
||||||
if space in ['buy', 'sell']:
|
if space in ['buy', 'sell']:
|
||||||
@ -235,7 +236,7 @@ class Hyperopt:
|
|||||||
result_dict.update(space_params)
|
result_dict.update(space_params)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _params_pretty_print(params, space: str, header: str):
|
def _params_pretty_print(params, space: str, header: str) -> None:
|
||||||
if space in params:
|
if space in params:
|
||||||
space_params = Hyperopt._space_params(params, space, 5)
|
space_params = Hyperopt._space_params(params, space, 5)
|
||||||
if space == 'stoploss':
|
if space == 'stoploss':
|
||||||
@ -251,7 +252,7 @@ class Hyperopt:
|
|||||||
return round_dict(d, r) if r else d
|
return round_dict(d, r) if r else d
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_best_loss(results, current_best_loss) -> bool:
|
def is_best_loss(results, current_best_loss: float) -> bool:
|
||||||
return results['loss'] < current_best_loss
|
return results['loss'] < current_best_loss
|
||||||
|
|
||||||
def print_results(self, results) -> None:
|
def print_results(self, results) -> None:
|
||||||
@ -438,7 +439,7 @@ class Hyperopt:
|
|||||||
random_state=self.random_state,
|
random_state=self.random_state,
|
||||||
)
|
)
|
||||||
|
|
||||||
def fix_optimizer_models_list(self):
|
def fix_optimizer_models_list(self) -> None:
|
||||||
"""
|
"""
|
||||||
WORKAROUND: Since skopt is not actively supported, this resolves problems with skopt
|
WORKAROUND: Since skopt is not actively supported, this resolves problems with skopt
|
||||||
memory usage, see also: https://github.com/scikit-optimize/scikit-optimize/pull/746
|
memory usage, see also: https://github.com/scikit-optimize/scikit-optimize/pull/746
|
||||||
@ -460,7 +461,7 @@ class Hyperopt:
|
|||||||
wrap_non_picklable_objects(self.generate_optimizer))(v, i) for v in asked)
|
wrap_non_picklable_objects(self.generate_optimizer))(v, i) for v in asked)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_previous_results(trials_file) -> List:
|
def load_previous_results(trials_file: Path) -> List:
|
||||||
"""
|
"""
|
||||||
Load data for epochs from the file if we have one
|
Load data for epochs from the file if we have one
|
||||||
"""
|
"""
|
||||||
|
@ -28,18 +28,19 @@ class SharpeHyperOptLoss(IHyperOptLoss):
|
|||||||
|
|
||||||
Uses Sharpe Ratio calculation.
|
Uses Sharpe Ratio calculation.
|
||||||
"""
|
"""
|
||||||
total_profit = results.profit_percent
|
total_profit = results["profit_percent"]
|
||||||
days_period = (max_date - min_date).days
|
days_period = (max_date - min_date).days
|
||||||
|
|
||||||
# adding slippage of 0.1% per trade
|
# adding slippage of 0.1% per trade
|
||||||
total_profit = total_profit - 0.0005
|
total_profit = total_profit - 0.0005
|
||||||
expected_yearly_return = total_profit.sum() / days_period
|
expected_returns_mean = total_profit.sum() / days_period
|
||||||
|
up_stdev = np.std(total_profit)
|
||||||
|
|
||||||
if (np.std(total_profit) != 0.):
|
if (np.std(total_profit) != 0.):
|
||||||
sharp_ratio = expected_yearly_return / np.std(total_profit) * np.sqrt(365)
|
sharp_ratio = expected_returns_mean / up_stdev * np.sqrt(365)
|
||||||
else:
|
else:
|
||||||
# Define high (negative) sharpe ratio to be clear that this is NOT optimal.
|
# Define high (negative) sharpe ratio to be clear that this is NOT optimal.
|
||||||
sharp_ratio = -20.
|
sharp_ratio = -20.
|
||||||
|
|
||||||
# print(expected_yearly_return, np.std(total_profit), sharp_ratio)
|
# print(expected_returns_mean, up_stdev, sharp_ratio)
|
||||||
return -sharp_ratio
|
return -sharp_ratio
|
||||||
|
61
freqtrade/optimize/hyperopt_loss_sharpe_daily.py
Normal file
61
freqtrade/optimize/hyperopt_loss_sharpe_daily.py
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
"""
|
||||||
|
SharpeHyperOptLossDaily
|
||||||
|
|
||||||
|
This module defines the alternative HyperOptLoss class which can be used for
|
||||||
|
Hyperoptimization.
|
||||||
|
"""
|
||||||
|
import math
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pandas import DataFrame, date_range
|
||||||
|
|
||||||
|
from freqtrade.optimize.hyperopt import IHyperOptLoss
|
||||||
|
|
||||||
|
|
||||||
|
class SharpeHyperOptLossDaily(IHyperOptLoss):
|
||||||
|
"""
|
||||||
|
Defines the loss function for hyperopt.
|
||||||
|
|
||||||
|
This implementation uses the Sharpe Ratio calculation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def hyperopt_loss_function(results: DataFrame, trade_count: int,
|
||||||
|
min_date: datetime, max_date: datetime,
|
||||||
|
*args, **kwargs) -> float:
|
||||||
|
"""
|
||||||
|
Objective function, returns smaller number for more optimal results.
|
||||||
|
|
||||||
|
Uses Sharpe Ratio calculation.
|
||||||
|
"""
|
||||||
|
resample_freq = '1D'
|
||||||
|
slippage_per_trade_ratio = 0.0005
|
||||||
|
days_in_year = 365
|
||||||
|
annual_risk_free_rate = 0.0
|
||||||
|
risk_free_rate = annual_risk_free_rate / days_in_year
|
||||||
|
|
||||||
|
# apply slippage per trade to profit_percent
|
||||||
|
results.loc[:, 'profit_percent_after_slippage'] = \
|
||||||
|
results['profit_percent'] - slippage_per_trade_ratio
|
||||||
|
|
||||||
|
# create the index within the min_date and end max_date
|
||||||
|
t_index = date_range(start=min_date, end=max_date, freq=resample_freq)
|
||||||
|
|
||||||
|
sum_daily = (
|
||||||
|
results.resample(resample_freq, on='close_time').agg(
|
||||||
|
{"profit_percent_after_slippage": sum}).reindex(t_index).fillna(0)
|
||||||
|
)
|
||||||
|
|
||||||
|
total_profit = sum_daily["profit_percent_after_slippage"] - risk_free_rate
|
||||||
|
expected_returns_mean = total_profit.mean()
|
||||||
|
up_stdev = total_profit.std()
|
||||||
|
|
||||||
|
if (up_stdev != 0.):
|
||||||
|
sharp_ratio = expected_returns_mean / up_stdev * math.sqrt(days_in_year)
|
||||||
|
else:
|
||||||
|
# Define high (negative) sharpe ratio to be clear that this is NOT optimal.
|
||||||
|
sharp_ratio = -20.
|
||||||
|
|
||||||
|
# print(t_index, sum_daily, total_profit)
|
||||||
|
# print(risk_free_rate, expected_returns_mean, up_stdev, sharp_ratio)
|
||||||
|
return -sharp_ratio
|
@ -19,9 +19,17 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra
|
|||||||
|
|
||||||
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f')
|
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f')
|
||||||
tabular_data = []
|
tabular_data = []
|
||||||
headers = ['pair', 'buy count', 'avg profit %', 'cum profit %',
|
headers = [
|
||||||
f'tot profit {stake_currency}', 'tot profit %', 'avg duration',
|
'Pair',
|
||||||
'profit', 'loss']
|
'Buy Count',
|
||||||
|
'Avg Profit %',
|
||||||
|
'Cum Profit %',
|
||||||
|
f'Tot Profit {stake_currency}',
|
||||||
|
'Tot Profit %',
|
||||||
|
'Avg Duration',
|
||||||
|
'Wins',
|
||||||
|
'Losses'
|
||||||
|
]
|
||||||
for pair in data:
|
for pair in data:
|
||||||
result = results[results.pair == pair]
|
result = results[results.pair == pair]
|
||||||
if skip_nan and result.profit_abs.isnull().all():
|
if skip_nan and result.profit_abs.isnull().all():
|
||||||
@ -58,7 +66,9 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra
|
|||||||
floatfmt=floatfmt, tablefmt="pipe") # type: ignore
|
floatfmt=floatfmt, tablefmt="pipe") # type: ignore
|
||||||
|
|
||||||
|
|
||||||
def generate_text_table_sell_reason(data: Dict[str, Dict], results: DataFrame) -> str:
|
def generate_text_table_sell_reason(
|
||||||
|
data: Dict[str, Dict], stake_currency: str, max_open_trades: int, results: DataFrame
|
||||||
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Generate small table outlining Backtest results
|
Generate small table outlining Backtest results
|
||||||
:param data: Dict of <pair: dataframe> containing data that was used during backtesting.
|
:param data: Dict of <pair: dataframe> containing data that was used during backtesting.
|
||||||
@ -66,13 +76,36 @@ def generate_text_table_sell_reason(data: Dict[str, Dict], results: DataFrame) -
|
|||||||
:return: pretty printed table with tabulate as string
|
:return: pretty printed table with tabulate as string
|
||||||
"""
|
"""
|
||||||
tabular_data = []
|
tabular_data = []
|
||||||
headers = ['Sell Reason', 'Count', 'Profit', 'Loss', 'Profit %']
|
headers = [
|
||||||
|
"Sell Reason",
|
||||||
|
"Sell Count",
|
||||||
|
"Wins",
|
||||||
|
"Losses",
|
||||||
|
"Avg Profit %",
|
||||||
|
"Cum Profit %",
|
||||||
|
f"Tot Profit {stake_currency}",
|
||||||
|
"Tot Profit %",
|
||||||
|
]
|
||||||
for reason, count in results['sell_reason'].value_counts().iteritems():
|
for reason, count in results['sell_reason'].value_counts().iteritems():
|
||||||
result = results.loc[results['sell_reason'] == reason]
|
result = results.loc[results['sell_reason'] == reason]
|
||||||
profit = len(result[result['profit_abs'] >= 0])
|
profit = len(result[result['profit_abs'] >= 0])
|
||||||
loss = len(result[result['profit_abs'] < 0])
|
loss = len(result[result['profit_abs'] < 0])
|
||||||
profit_mean = round(result['profit_percent'].mean() * 100.0, 2)
|
profit_mean = round(result['profit_percent'].mean() * 100.0, 2)
|
||||||
tabular_data.append([reason.value, count, profit, loss, profit_mean])
|
profit_sum = round(result["profit_percent"].sum() * 100.0, 2)
|
||||||
|
profit_tot = result['profit_abs'].sum()
|
||||||
|
profit_percent_tot = round(result['profit_percent'].sum() * 100.0 / max_open_trades, 2)
|
||||||
|
tabular_data.append(
|
||||||
|
[
|
||||||
|
reason.value,
|
||||||
|
count,
|
||||||
|
profit,
|
||||||
|
loss,
|
||||||
|
profit_mean,
|
||||||
|
profit_sum,
|
||||||
|
profit_tot,
|
||||||
|
profit_percent_tot,
|
||||||
|
]
|
||||||
|
)
|
||||||
return tabulate(tabular_data, headers=headers, tablefmt="pipe")
|
return tabulate(tabular_data, headers=headers, tablefmt="pipe")
|
||||||
|
|
||||||
|
|
||||||
@ -88,9 +121,9 @@ def generate_text_table_strategy(stake_currency: str, max_open_trades: str,
|
|||||||
|
|
||||||
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f')
|
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.2f', 'd', '.1f', '.1f')
|
||||||
tabular_data = []
|
tabular_data = []
|
||||||
headers = ['Strategy', 'buy count', 'avg profit %', 'cum profit %',
|
headers = ['Strategy', 'Buy Count', 'Avg Profit %', 'Cum Profit %',
|
||||||
f'tot profit {stake_currency}', 'tot profit %', 'avg duration',
|
f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration',
|
||||||
'profit', 'loss']
|
'Wins', 'Losses']
|
||||||
for strategy, results in all_results.items():
|
for strategy, results in all_results.items():
|
||||||
tabular_data.append([
|
tabular_data.append([
|
||||||
strategy,
|
strategy,
|
||||||
|
@ -7,7 +7,7 @@ Provides lists as configured in config.json
|
|||||||
import logging
|
import logging
|
||||||
from abc import ABC, abstractmethod, abstractproperty
|
from abc import ABC, abstractmethod, abstractproperty
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from freqtrade.exchange import market_is_active
|
from freqtrade.exchange import market_is_active
|
||||||
|
|
||||||
@ -16,7 +16,8 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class IPairList(ABC):
|
class IPairList(ABC):
|
||||||
|
|
||||||
def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict,
|
def __init__(self, exchange, pairlistmanager,
|
||||||
|
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
||||||
pairlist_pos: int) -> None:
|
pairlist_pos: int) -> None:
|
||||||
"""
|
"""
|
||||||
:param exchange: Exchange instance
|
:param exchange: Exchange instance
|
||||||
|
@ -48,10 +48,10 @@ class PrecisionFilter(IPairList):
|
|||||||
"""
|
"""
|
||||||
Filters and sorts pairlists and assigns and returns them again.
|
Filters and sorts pairlists and assigns and returns them again.
|
||||||
"""
|
"""
|
||||||
stoploss = None
|
stoploss = self._config.get('stoploss')
|
||||||
if self._config.get('stoploss') is not None:
|
if stoploss is not None:
|
||||||
# Precalculate sanitized stoploss value to avoid recalculation for every pair
|
# Precalculate sanitized stoploss value to avoid recalculation for every pair
|
||||||
stoploss = 1 - abs(self._config.get('stoploss'))
|
stoploss = 1 - abs(stoploss)
|
||||||
# Copy list since we're modifying this list
|
# Copy list since we're modifying this list
|
||||||
for p in deepcopy(pairlist):
|
for p in deepcopy(pairlist):
|
||||||
ticker = tickers.get(p)
|
ticker = tickers.get(p)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
from typing import Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from freqtrade.pairlist.IPairList import IPairList
|
from freqtrade.pairlist.IPairList import IPairList
|
||||||
|
|
||||||
@ -9,7 +9,8 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
class PriceFilter(IPairList):
|
class PriceFilter(IPairList):
|
||||||
|
|
||||||
def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict,
|
def __init__(self, exchange, pairlistmanager,
|
||||||
|
config: Dict[str, Any], pairlistconfig: Dict[str, Any],
|
||||||
pairlist_pos: int) -> None:
|
pairlist_pos: int) -> None:
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||||
|
|
||||||
|
59
freqtrade/pairlist/SpreadFilter.py
Normal file
59
freqtrade/pairlist/SpreadFilter.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import logging
|
||||||
|
from copy import deepcopy
|
||||||
|
from typing import Dict, List
|
||||||
|
|
||||||
|
from freqtrade.pairlist.IPairList import IPairList
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class SpreadFilter(IPairList):
|
||||||
|
|
||||||
|
def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict,
|
||||||
|
pairlist_pos: int) -> None:
|
||||||
|
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||||
|
|
||||||
|
self._max_spread_ratio = pairlistconfig.get('max_spread_ratio', 0.005)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def needstickers(self) -> bool:
|
||||||
|
"""
|
||||||
|
Boolean property defining if tickers are necessary.
|
||||||
|
If no Pairlist requries tickers, an empty List is passed
|
||||||
|
as tickers argument to filter_pairlist
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
def short_desc(self) -> str:
|
||||||
|
"""
|
||||||
|
Short whitelist method description - used for startup-messages
|
||||||
|
"""
|
||||||
|
return (f"{self.name} - Filtering pairs with ask/bid diff above "
|
||||||
|
f"{self._max_spread_ratio * 100}%.")
|
||||||
|
|
||||||
|
def filter_pairlist(self, pairlist: List[str], tickers: Dict) -> List[str]:
|
||||||
|
|
||||||
|
"""
|
||||||
|
Filters and sorts pairlist and returns the whitelist again.
|
||||||
|
Called on each bot iteration - please use internal caching if necessary
|
||||||
|
:param pairlist: pairlist to filter or sort
|
||||||
|
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
|
||||||
|
:return: new whitelist
|
||||||
|
"""
|
||||||
|
# Copy list since we're modifying this list
|
||||||
|
|
||||||
|
spread = None
|
||||||
|
for p in deepcopy(pairlist):
|
||||||
|
ticker = tickers.get(p)
|
||||||
|
assert ticker is not None
|
||||||
|
if 'bid' in ticker and 'ask' in ticker:
|
||||||
|
spread = 1 - ticker['bid'] / ticker['ask']
|
||||||
|
if not ticker or spread > self._max_spread_ratio:
|
||||||
|
logger.info(f"Removed {ticker['symbol']} from whitelist, "
|
||||||
|
f"because spread {spread * 100:.3f}% >"
|
||||||
|
f"{self._max_spread_ratio * 100}%")
|
||||||
|
pairlist.remove(p)
|
||||||
|
else:
|
||||||
|
pairlist.remove(p)
|
||||||
|
|
||||||
|
return pairlist
|
@ -6,7 +6,7 @@ Provides lists as configured in config.json
|
|||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, List
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.pairlist.IPairList import IPairList
|
from freqtrade.pairlist.IPairList import IPairList
|
||||||
@ -18,7 +18,7 @@ SORT_VALUES = ['askVolume', 'bidVolume', 'quoteVolume']
|
|||||||
|
|
||||||
class VolumePairList(IPairList):
|
class VolumePairList(IPairList):
|
||||||
|
|
||||||
def __init__(self, exchange, pairlistmanager, config, pairlistconfig: dict,
|
def __init__(self, exchange, pairlistmanager, config: Dict[str, Any], pairlistconfig: dict,
|
||||||
pairlist_pos: int) -> None:
|
pairlist_pos: int) -> None:
|
||||||
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
super().__init__(exchange, pairlistmanager, config, pairlistconfig, pairlist_pos)
|
||||||
|
|
||||||
@ -28,6 +28,7 @@ class VolumePairList(IPairList):
|
|||||||
'for "pairlist.config.number_assets"')
|
'for "pairlist.config.number_assets"')
|
||||||
self._number_pairs = self._pairlistconfig['number_assets']
|
self._number_pairs = self._pairlistconfig['number_assets']
|
||||||
self._sort_key = self._pairlistconfig.get('sort_key', 'quoteVolume')
|
self._sort_key = self._pairlistconfig.get('sort_key', 'quoteVolume')
|
||||||
|
self._min_value = self._pairlistconfig.get('min_value', 0)
|
||||||
self.refresh_period = self._pairlistconfig.get('refresh_period', 1800)
|
self.refresh_period = self._pairlistconfig.get('refresh_period', 1800)
|
||||||
|
|
||||||
if not self._exchange.exchange_has('fetchTickers'):
|
if not self._exchange.exchange_has('fetchTickers'):
|
||||||
@ -73,11 +74,13 @@ class VolumePairList(IPairList):
|
|||||||
tickers,
|
tickers,
|
||||||
self._config['stake_currency'],
|
self._config['stake_currency'],
|
||||||
self._sort_key,
|
self._sort_key,
|
||||||
|
self._min_value
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return pairlist
|
return pairlist
|
||||||
|
|
||||||
def _gen_pair_whitelist(self, pairlist, tickers, base_currency: str, key: str) -> List[str]:
|
def _gen_pair_whitelist(self, pairlist: List[str], tickers: Dict,
|
||||||
|
base_currency: str, key: str, min_val: int) -> List[str]:
|
||||||
"""
|
"""
|
||||||
Updates the whitelist with with a dynamically generated list
|
Updates the whitelist with with a dynamically generated list
|
||||||
:param base_currency: base currency as str
|
:param base_currency: base currency as str
|
||||||
@ -96,6 +99,9 @@ class VolumePairList(IPairList):
|
|||||||
# If other pairlist is in front, use the incomming pairlist.
|
# If other pairlist is in front, use the incomming pairlist.
|
||||||
filtered_tickers = [v for k, v in tickers.items() if k in pairlist]
|
filtered_tickers = [v for k, v in tickers.items() if k in pairlist]
|
||||||
|
|
||||||
|
if min_val > 0:
|
||||||
|
filtered_tickers = list(filter(lambda t: t[key] > min_val, filtered_tickers))
|
||||||
|
|
||||||
sorted_tickers = sorted(filtered_tickers, reverse=True, key=lambda t: t[key])
|
sorted_tickers = sorted(filtered_tickers, reverse=True, key=lambda t: t[key])
|
||||||
|
|
||||||
# Validate whitelist to only have active market pairs
|
# Validate whitelist to only have active market pairs
|
||||||
|
@ -64,11 +64,11 @@ def init(db_url: str, clean_open_orders: bool = False) -> None:
|
|||||||
clean_dry_run_db()
|
clean_dry_run_db()
|
||||||
|
|
||||||
|
|
||||||
def has_column(columns, searchname: str) -> bool:
|
def has_column(columns: List, searchname: str) -> bool:
|
||||||
return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1
|
return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1
|
||||||
|
|
||||||
|
|
||||||
def get_column_def(columns, column: str, default: str) -> str:
|
def get_column_def(columns: List, column: str, default: str) -> str:
|
||||||
return default if not has_column(columns, column) else column
|
return default if not has_column(columns, column) else column
|
||||||
|
|
||||||
|
|
||||||
@ -246,14 +246,15 @@ class Trade(_DECL_BASE):
|
|||||||
if self.initial_stop_loss_pct else None),
|
if self.initial_stop_loss_pct else None),
|
||||||
}
|
}
|
||||||
|
|
||||||
def adjust_min_max_rates(self, current_price: float):
|
def adjust_min_max_rates(self, current_price: float) -> None:
|
||||||
"""
|
"""
|
||||||
Adjust the max_rate and min_rate.
|
Adjust the max_rate and min_rate.
|
||||||
"""
|
"""
|
||||||
self.max_rate = max(current_price, self.max_rate or self.open_rate)
|
self.max_rate = max(current_price, self.max_rate or self.open_rate)
|
||||||
self.min_rate = min(current_price, self.min_rate or self.open_rate)
|
self.min_rate = min(current_price, self.min_rate or self.open_rate)
|
||||||
|
|
||||||
def adjust_stop_loss(self, current_price: float, stoploss: float, initial: bool = False):
|
def adjust_stop_loss(self, current_price: float, stoploss: float,
|
||||||
|
initial: bool = False) -> None:
|
||||||
"""
|
"""
|
||||||
This adjusts the stop loss to it's most recently observed setting
|
This adjusts the stop loss to it's most recently observed setting
|
||||||
:param current_price: Current rate the asset is traded
|
:param current_price: Current rate the asset is traded
|
||||||
|
@ -370,7 +370,7 @@ def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame],
|
|||||||
return fig
|
return fig
|
||||||
|
|
||||||
|
|
||||||
def generate_plot_filename(pair, timeframe) -> str:
|
def generate_plot_filename(pair: str, timeframe: str) -> str:
|
||||||
"""
|
"""
|
||||||
Generate filenames per pair/timeframe to be used for storing plots
|
Generate filenames per pair/timeframe to be used for storing plots
|
||||||
"""
|
"""
|
||||||
|
@ -25,7 +25,7 @@ class IResolver:
|
|||||||
initial_search_path: Path
|
initial_search_path: Path
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def build_search_paths(cls, config, user_subdir: Optional[str] = None,
|
def build_search_paths(cls, config: Dict[str, Any], user_subdir: Optional[str] = None,
|
||||||
extra_dir: Optional[str] = None) -> List[Path]:
|
extra_dir: Optional[str] = None) -> List[Path]:
|
||||||
|
|
||||||
abs_paths: List[Path] = [cls.initial_search_path]
|
abs_paths: List[Path] = [cls.initial_search_path]
|
||||||
|
@ -9,10 +9,10 @@ from base64 import urlsafe_b64decode
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from inspect import getfullargspec
|
from inspect import getfullargspec
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from freqtrade.constants import (REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES,
|
from freqtrade.constants import (REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES,
|
||||||
USERPATH_STRATEGY)
|
USERPATH_STRATEGIES)
|
||||||
from freqtrade.exceptions import OperationalException
|
from freqtrade.exceptions import OperationalException
|
||||||
from freqtrade.resolvers import IResolver
|
from freqtrade.resolvers import IResolver
|
||||||
from freqtrade.strategy.interface import IStrategy
|
from freqtrade.strategy.interface import IStrategy
|
||||||
@ -26,11 +26,11 @@ class StrategyResolver(IResolver):
|
|||||||
"""
|
"""
|
||||||
object_type = IStrategy
|
object_type = IStrategy
|
||||||
object_type_str = "Strategy"
|
object_type_str = "Strategy"
|
||||||
user_subdir = USERPATH_STRATEGY
|
user_subdir = USERPATH_STRATEGIES
|
||||||
initial_search_path = Path(__file__).parent.parent.joinpath('strategy').resolve()
|
initial_search_path = Path(__file__).parent.parent.joinpath('strategy').resolve()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def load_strategy(config: Optional[Dict] = None) -> IStrategy:
|
def load_strategy(config: Dict[str, Any] = None) -> IStrategy:
|
||||||
"""
|
"""
|
||||||
Load the custom class from config parameter
|
Load the custom class from config parameter
|
||||||
:param config: configuration dictionary or None
|
:param config: configuration dictionary or None
|
||||||
@ -96,7 +96,8 @@ class StrategyResolver(IResolver):
|
|||||||
return strategy
|
return strategy
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _override_attribute_helper(strategy, config, attribute: str, default):
|
def _override_attribute_helper(strategy, config: Dict[str, Any],
|
||||||
|
attribute: str, default: Any):
|
||||||
"""
|
"""
|
||||||
Override attributes in the strategy.
|
Override attributes in the strategy.
|
||||||
Prevalence:
|
Prevalence:
|
||||||
@ -140,7 +141,7 @@ class StrategyResolver(IResolver):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
abs_paths = StrategyResolver.build_search_paths(config,
|
abs_paths = StrategyResolver.build_search_paths(config,
|
||||||
user_subdir=USERPATH_STRATEGY,
|
user_subdir=USERPATH_STRATEGIES,
|
||||||
extra_dir=extra_dir)
|
extra_dir=extra_dir)
|
||||||
|
|
||||||
if ":" in strategy_name:
|
if ":" in strategy_name:
|
||||||
|
@ -139,7 +139,8 @@ class RPC:
|
|||||||
results.append(trade_dict)
|
results.append(trade_dict)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def _rpc_status_table(self, stake_currency, fiat_display_currency: str) -> Tuple[List, List]:
|
def _rpc_status_table(self, stake_currency: str,
|
||||||
|
fiat_display_currency: str) -> Tuple[List, List]:
|
||||||
trades = Trade.get_open_trades()
|
trades = Trade.get_open_trades()
|
||||||
if not trades:
|
if not trades:
|
||||||
raise RPCException('no active trade')
|
raise RPCException('no active trade')
|
||||||
@ -385,7 +386,7 @@ class RPC:
|
|||||||
|
|
||||||
return {'status': 'No more buy will occur from now. Run /reload_conf to reset.'}
|
return {'status': 'No more buy will occur from now. Run /reload_conf to reset.'}
|
||||||
|
|
||||||
def _rpc_forcesell(self, trade_id) -> Dict[str, str]:
|
def _rpc_forcesell(self, trade_id: str) -> Dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Handler for forcesell <id>.
|
Handler for forcesell <id>.
|
||||||
Sells the given trade at current price
|
Sells the given trade at current price
|
||||||
|
@ -61,7 +61,7 @@ class RPCManager:
|
|||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
logger.error(f"Message type {msg['type']} not implemented by handler {mod.name}.")
|
logger.error(f"Message type {msg['type']} not implemented by handler {mod.name}.")
|
||||||
|
|
||||||
def startup_messages(self, config, pairlist) -> None:
|
def startup_messages(self, config: Dict[str, Any], pairlist) -> None:
|
||||||
if config['dry_run']:
|
if config['dry_run']:
|
||||||
self.send_msg({
|
self.send_msg({
|
||||||
'type': RPCMessageType.WARNING_NOTIFICATION,
|
'type': RPCMessageType.WARNING_NOTIFICATION,
|
||||||
|
@ -180,7 +180,7 @@ class IStrategy(ABC):
|
|||||||
if pair not in self._pair_locked_until or self._pair_locked_until[pair] < until:
|
if pair not in self._pair_locked_until or self._pair_locked_until[pair] < until:
|
||||||
self._pair_locked_until[pair] = until
|
self._pair_locked_until[pair] = until
|
||||||
|
|
||||||
def unlock_pair(self, pair) -> None:
|
def unlock_pair(self, pair: str) -> None:
|
||||||
"""
|
"""
|
||||||
Unlocks a pair previously locked using lock_pair.
|
Unlocks a pair previously locked using lock_pair.
|
||||||
Not used by freqtrade itself, but intended to be used if users lock pairs
|
Not used by freqtrade itself, but intended to be used if users lock pairs
|
||||||
|
4
freqtrade/vendor/qtpylib/indicators.py
vendored
4
freqtrade/vendor/qtpylib/indicators.py
vendored
@ -288,9 +288,9 @@ def rolling_min(series, window=14, min_periods=None):
|
|||||||
def rolling_max(series, window=14, min_periods=None):
|
def rolling_max(series, window=14, min_periods=None):
|
||||||
min_periods = window if min_periods is None else min_periods
|
min_periods = window if min_periods is None else min_periods
|
||||||
try:
|
try:
|
||||||
return series.rolling(window=window, min_periods=min_periods).min()
|
return series.rolling(window=window, min_periods=min_periods).max()
|
||||||
except Exception as e: # noqa: F841
|
except Exception as e: # noqa: F841
|
||||||
return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
|
return pd.Series(series).rolling(window=window, min_periods=min_periods).max()
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
|
@ -30,24 +30,21 @@ class Wallets:
|
|||||||
self._last_wallet_refresh = 0
|
self._last_wallet_refresh = 0
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def get_free(self, currency) -> float:
|
def get_free(self, currency: str) -> float:
|
||||||
|
|
||||||
balance = self._wallets.get(currency)
|
balance = self._wallets.get(currency)
|
||||||
if balance and balance.free:
|
if balance and balance.free:
|
||||||
return balance.free
|
return balance.free
|
||||||
else:
|
else:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def get_used(self, currency) -> float:
|
def get_used(self, currency: str) -> float:
|
||||||
|
|
||||||
balance = self._wallets.get(currency)
|
balance = self._wallets.get(currency)
|
||||||
if balance and balance.used:
|
if balance and balance.used:
|
||||||
return balance.used
|
return balance.used
|
||||||
else:
|
else:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
def get_total(self, currency) -> float:
|
def get_total(self, currency: str) -> float:
|
||||||
|
|
||||||
balance = self._wallets.get(currency)
|
balance = self._wallets.get(currency)
|
||||||
if balance and balance.total:
|
if balance and balance.total:
|
||||||
return balance.total
|
return balance.total
|
||||||
@ -87,7 +84,6 @@ class Wallets:
|
|||||||
self._wallets = _wallets
|
self._wallets = _wallets
|
||||||
|
|
||||||
def _update_live(self) -> None:
|
def _update_live(self) -> None:
|
||||||
|
|
||||||
balances = self._exchange.get_balances()
|
balances = self._exchange.get_balances()
|
||||||
|
|
||||||
for currency in balances:
|
for currency in balances:
|
||||||
|
@ -22,7 +22,7 @@ class Worker:
|
|||||||
Freqtradebot worker class
|
Freqtradebot worker class
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, args: Dict[str, Any], config=None) -> None:
|
def __init__(self, args: Dict[str, Any], config: Dict[str, Any] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Init all variables and objects the bot needs to work
|
Init all variables and objects the bot needs to work
|
||||||
"""
|
"""
|
||||||
@ -56,14 +56,6 @@ class Worker:
|
|||||||
self._sd_notify = sdnotify.SystemdNotifier() if \
|
self._sd_notify = sdnotify.SystemdNotifier() if \
|
||||||
self._config.get('internals', {}).get('sd_notify', False) else None
|
self._config.get('internals', {}).get('sd_notify', False) else None
|
||||||
|
|
||||||
@property
|
|
||||||
def state(self) -> State:
|
|
||||||
return self.freqtrade.state
|
|
||||||
|
|
||||||
@state.setter
|
|
||||||
def state(self, value: State) -> None:
|
|
||||||
self.freqtrade.state = value
|
|
||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
state = None
|
state = None
|
||||||
while True:
|
while True:
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# requirements without requirements installable via conda
|
# requirements without requirements installable via conda
|
||||||
# mainly used for Raspberry pi installs
|
# mainly used for Raspberry pi installs
|
||||||
ccxt==1.21.91
|
ccxt==1.22.30
|
||||||
SQLAlchemy==1.3.13
|
SQLAlchemy==1.3.13
|
||||||
python-telegram-bot==12.3.0
|
python-telegram-bot==12.3.0
|
||||||
arrow==0.15.5
|
arrow==0.15.5
|
||||||
@ -12,7 +12,7 @@ jsonschema==3.2.0
|
|||||||
TA-Lib==0.4.17
|
TA-Lib==0.4.17
|
||||||
tabulate==0.8.6
|
tabulate==0.8.6
|
||||||
coinmarketcap==5.0.3
|
coinmarketcap==5.0.3
|
||||||
jinja2==2.10.3
|
jinja2==2.11.1
|
||||||
|
|
||||||
# find first, C search in arrays
|
# find first, C search in arrays
|
||||||
py_find_1st==1.1.4
|
py_find_1st==1.1.4
|
||||||
|
@ -8,7 +8,7 @@ flake8==3.7.9
|
|||||||
flake8-type-annotations==0.1.0
|
flake8-type-annotations==0.1.0
|
||||||
flake8-tidy-imports==4.0.0
|
flake8-tidy-imports==4.0.0
|
||||||
mypy==0.761
|
mypy==0.761
|
||||||
pytest==5.3.4
|
pytest==5.3.5
|
||||||
pytest-asyncio==0.10.0
|
pytest-asyncio==0.10.0
|
||||||
pytest-cov==2.8.1
|
pytest-cov==2.8.1
|
||||||
pytest-mock==2.0.0
|
pytest-mock==2.0.0
|
||||||
|
@ -4,6 +4,6 @@
|
|||||||
# Required for hyperopt
|
# Required for hyperopt
|
||||||
scipy==1.4.1
|
scipy==1.4.1
|
||||||
scikit-learn==0.22.1
|
scikit-learn==0.22.1
|
||||||
scikit-optimize==0.5.2
|
scikit-optimize==0.7.1
|
||||||
filelock==3.0.12
|
filelock==3.0.12
|
||||||
joblib==0.14.1
|
joblib==0.14.1
|
||||||
|
@ -2,4 +2,4 @@
|
|||||||
-r requirements-common.txt
|
-r requirements-common.txt
|
||||||
|
|
||||||
numpy==1.18.1
|
numpy==1.18.1
|
||||||
pandas==0.25.3
|
pandas==1.0.0
|
||||||
|
@ -7,7 +7,8 @@ import pytest
|
|||||||
from freqtrade.commands import (start_create_userdir, start_download_data,
|
from freqtrade.commands import (start_create_userdir, start_download_data,
|
||||||
start_hyperopt_list, start_hyperopt_show,
|
start_hyperopt_list, start_hyperopt_show,
|
||||||
start_list_exchanges, start_list_markets,
|
start_list_exchanges, start_list_markets,
|
||||||
start_list_strategies, start_list_timeframes,
|
start_list_hyperopts, start_list_strategies,
|
||||||
|
start_list_timeframes,
|
||||||
start_new_hyperopt, start_new_strategy,
|
start_new_hyperopt, start_new_strategy,
|
||||||
start_test_pairlist, start_trading)
|
start_test_pairlist, start_trading)
|
||||||
from freqtrade.configuration import setup_utils_configuration
|
from freqtrade.configuration import setup_utils_configuration
|
||||||
@ -665,6 +666,39 @@ def test_start_list_strategies(mocker, caplog, capsys):
|
|||||||
assert "DefaultStrategy" in captured.out
|
assert "DefaultStrategy" in captured.out
|
||||||
|
|
||||||
|
|
||||||
|
def test_start_list_hyperopts(mocker, caplog, capsys):
|
||||||
|
|
||||||
|
args = [
|
||||||
|
"list-hyperopts",
|
||||||
|
"--hyperopt-path",
|
||||||
|
str(Path(__file__).parent.parent / "optimize"),
|
||||||
|
"-1"
|
||||||
|
]
|
||||||
|
pargs = get_args(args)
|
||||||
|
# pargs['config'] = None
|
||||||
|
start_list_hyperopts(pargs)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "TestHyperoptLegacy" not in captured.out
|
||||||
|
assert "legacy_hyperopt.py" not in captured.out
|
||||||
|
assert "DefaultHyperOpt" in captured.out
|
||||||
|
assert "test_hyperopt.py" not in captured.out
|
||||||
|
|
||||||
|
# Test regular output
|
||||||
|
args = [
|
||||||
|
"list-hyperopts",
|
||||||
|
"--hyperopt-path",
|
||||||
|
str(Path(__file__).parent.parent / "optimize"),
|
||||||
|
]
|
||||||
|
pargs = get_args(args)
|
||||||
|
# pargs['config'] = None
|
||||||
|
start_list_hyperopts(pargs)
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert "TestHyperoptLegacy" not in captured.out
|
||||||
|
assert "legacy_hyperopt.py" not in captured.out
|
||||||
|
assert "DefaultHyperOpt" in captured.out
|
||||||
|
assert "test_hyperopt.py" in captured.out
|
||||||
|
|
||||||
|
|
||||||
def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
|
def test_start_test_pairlist(mocker, caplog, tickers, default_conf, capsys):
|
||||||
patch_exchange(mocker, mock_markets=True)
|
patch_exchange(mocker, mock_markets=True)
|
||||||
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
mocker.patch.multiple('freqtrade.exchange.Exchange',
|
||||||
|
@ -640,6 +640,31 @@ def shitcoinmarkets(markets):
|
|||||||
},
|
},
|
||||||
'info': {},
|
'info': {},
|
||||||
},
|
},
|
||||||
|
'NANO/USDT': {
|
||||||
|
"percentage": True,
|
||||||
|
"tierBased": False,
|
||||||
|
"taker": 0.001,
|
||||||
|
"maker": 0.001,
|
||||||
|
"precision": {
|
||||||
|
"base": 8,
|
||||||
|
"quote": 8,
|
||||||
|
"amount": 2,
|
||||||
|
"price": 4
|
||||||
|
},
|
||||||
|
"limits": {
|
||||||
|
},
|
||||||
|
"id": "NANOUSDT",
|
||||||
|
"symbol": "NANO/USDT",
|
||||||
|
"base": "NANO",
|
||||||
|
"quote": "USDT",
|
||||||
|
"baseId": "NANO",
|
||||||
|
"quoteId": "USDT",
|
||||||
|
"info": {},
|
||||||
|
"type": "spot",
|
||||||
|
"spot": True,
|
||||||
|
"future": False,
|
||||||
|
"active": True
|
||||||
|
},
|
||||||
})
|
})
|
||||||
return shitmarkets
|
return shitmarkets
|
||||||
|
|
||||||
@ -1114,6 +1139,28 @@ def tickers():
|
|||||||
'quoteVolume': 1154.19266394,
|
'quoteVolume': 1154.19266394,
|
||||||
'info': {}
|
'info': {}
|
||||||
},
|
},
|
||||||
|
"NANO/USDT": {
|
||||||
|
"symbol": "NANO/USDT",
|
||||||
|
"timestamp": 1580469388244,
|
||||||
|
"datetime": "2020-01-31T11:16:28.244Z",
|
||||||
|
"high": 0.7519,
|
||||||
|
"low": 0.7154,
|
||||||
|
"bid": 0.7305,
|
||||||
|
"bidVolume": 300.3,
|
||||||
|
"ask": 0.7342,
|
||||||
|
"askVolume": 15.14,
|
||||||
|
"vwap": 0.73645591,
|
||||||
|
"open": 0.7154,
|
||||||
|
"close": 0.7342,
|
||||||
|
"last": 0.7342,
|
||||||
|
"previousClose": 0.7189,
|
||||||
|
"change": 0.0188,
|
||||||
|
"percentage": 2.628,
|
||||||
|
"average": None,
|
||||||
|
"baseVolume": 439472.44,
|
||||||
|
"quoteVolume": 323652.075405,
|
||||||
|
"info": {}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -163,8 +163,8 @@ def test_edge_results(edge_conf, mocker, caplog, data) -> None:
|
|||||||
for c, trade in enumerate(data.trades):
|
for c, trade in enumerate(data.trades):
|
||||||
res = results.iloc[c]
|
res = results.iloc[c]
|
||||||
assert res.exit_type == trade.sell_reason
|
assert res.exit_type == trade.sell_reason
|
||||||
assert arrow.get(res.open_time) == _get_frame_time_from_offset(trade.open_tick)
|
assert res.open_time == np.datetime64(_get_frame_time_from_offset(trade.open_tick))
|
||||||
assert arrow.get(res.close_time) == _get_frame_time_from_offset(trade.close_tick)
|
assert res.close_time == np.datetime64(_get_frame_time_from_offset(trade.close_tick))
|
||||||
|
|
||||||
|
|
||||||
def test_adjust(mocker, edge_conf):
|
def test_adjust(mocker, edge_conf):
|
||||||
|
@ -9,7 +9,7 @@ from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
|||||||
from tests.conftest import get_patched_exchange
|
from tests.conftest import get_patched_exchange
|
||||||
|
|
||||||
|
|
||||||
def test_stoploss_limit_order(default_conf, mocker):
|
def test_stoploss_order_binance(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||||
order_type = 'stop_loss_limit'
|
order_type = 'stop_loss_limit'
|
||||||
@ -28,46 +28,47 @@ def test_stoploss_limit_order(default_conf, mocker):
|
|||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||||
|
|
||||||
with pytest.raises(OperationalException):
|
with pytest.raises(OperationalException):
|
||||||
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200)
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||||
|
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
|
||||||
|
|
||||||
api_mock.create_order.reset_mock()
|
api_mock.create_order.reset_mock()
|
||||||
|
|
||||||
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
assert 'id' in order
|
assert 'id' in order
|
||||||
assert 'info' in order
|
assert 'info' in order
|
||||||
assert order['id'] == order_id
|
assert order['id'] == order_id
|
||||||
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
|
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
|
||||||
assert api_mock.create_order.call_args[0][1] == order_type
|
assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
|
||||||
assert api_mock.create_order.call_args[0][2] == 'sell'
|
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
||||||
assert api_mock.create_order.call_args[0][3] == 1
|
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||||
assert api_mock.create_order.call_args[0][4] == 200
|
assert api_mock.create_order.call_args_list[0][1]['price'] == 220
|
||||||
assert api_mock.create_order.call_args[0][5] == {'stopPrice': 220}
|
assert api_mock.create_order.call_args_list[0][1]['params'] == {'stopPrice': 220}
|
||||||
|
|
||||||
# test exception handling
|
# test exception handling
|
||||||
with pytest.raises(DependencyException):
|
with pytest.raises(DependencyException):
|
||||||
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||||
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
with pytest.raises(InvalidOrderException):
|
with pytest.raises(InvalidOrderException):
|
||||||
api_mock.create_order = MagicMock(
|
api_mock.create_order = MagicMock(
|
||||||
side_effect=ccxt.InvalidOrder("binance Order would trigger immediately."))
|
side_effect=ccxt.InvalidOrder("binance Order would trigger immediately."))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||||
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
with pytest.raises(TemporaryError):
|
with pytest.raises(TemporaryError):
|
||||||
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection"))
|
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection"))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||||
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
with pytest.raises(OperationalException, match=r".*DeadBeef.*"):
|
with pytest.raises(OperationalException, match=r".*DeadBeef.*"):
|
||||||
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
|
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
|
||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||||
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
|
||||||
def test_stoploss_limit_order_dry_run(default_conf, mocker):
|
def test_stoploss_order_dry_run_binance(default_conf, mocker):
|
||||||
api_mock = MagicMock()
|
api_mock = MagicMock()
|
||||||
order_type = 'stop_loss_limit'
|
order_type = 'stop_loss_limit'
|
||||||
default_conf['dry_run'] = True
|
default_conf['dry_run'] = True
|
||||||
@ -77,11 +78,12 @@ def test_stoploss_limit_order_dry_run(default_conf, mocker):
|
|||||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'binance')
|
||||||
|
|
||||||
with pytest.raises(OperationalException):
|
with pytest.raises(OperationalException):
|
||||||
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=190, rate=200)
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||||
|
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
|
||||||
|
|
||||||
api_mock.create_order.reset_mock()
|
api_mock.create_order.reset_mock()
|
||||||
|
|
||||||
order = exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
assert 'id' in order
|
assert 'id' in order
|
||||||
assert 'info' in order
|
assert 'info' in order
|
||||||
@ -90,3 +92,17 @@ def test_stoploss_limit_order_dry_run(default_conf, mocker):
|
|||||||
assert order['type'] == order_type
|
assert order['type'] == order_type
|
||||||
assert order['price'] == 220
|
assert order['price'] == 220
|
||||||
assert order['amount'] == 1
|
assert order['amount'] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_adjust_binance(mocker, default_conf):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id='binance')
|
||||||
|
order = {
|
||||||
|
'type': 'stop_loss_limit',
|
||||||
|
'price': 1500,
|
||||||
|
'info': {'stopPrice': 1500},
|
||||||
|
}
|
||||||
|
assert exchange.stoploss_adjust(1501, order)
|
||||||
|
assert not exchange.stoploss_adjust(1499, order)
|
||||||
|
# Test with invalid order case
|
||||||
|
order['type'] = 'stop_loss'
|
||||||
|
assert not exchange.stoploss_adjust(1501, order)
|
||||||
|
@ -76,9 +76,11 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
|||||||
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
|
||||||
caplog.set_level(logging.INFO)
|
caplog.set_level(logging.INFO)
|
||||||
conf = copy.deepcopy(default_conf)
|
conf = copy.deepcopy(default_conf)
|
||||||
conf['exchange']['ccxt_async_config'] = {'aiohttp_trust_env': True}
|
conf['exchange']['ccxt_async_config'] = {'aiohttp_trust_env': True, 'asyncio_loop': True}
|
||||||
ex = Exchange(conf)
|
ex = Exchange(conf)
|
||||||
assert log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}", caplog)
|
assert log_has(
|
||||||
|
"Applying additional ccxt config: {'aiohttp_trust_env': True, 'asyncio_loop': True}",
|
||||||
|
caplog)
|
||||||
assert ex._api_async.aiohttp_trust_env
|
assert ex._api_async.aiohttp_trust_env
|
||||||
assert not ex._api.aiohttp_trust_env
|
assert not ex._api.aiohttp_trust_env
|
||||||
|
|
||||||
@ -86,6 +88,8 @@ def test_init_ccxt_kwargs(default_conf, mocker, caplog):
|
|||||||
caplog.clear()
|
caplog.clear()
|
||||||
conf = copy.deepcopy(default_conf)
|
conf = copy.deepcopy(default_conf)
|
||||||
conf['exchange']['ccxt_config'] = {'TestKWARG': 11}
|
conf['exchange']['ccxt_config'] = {'TestKWARG': 11}
|
||||||
|
conf['exchange']['ccxt_async_config'] = {'asyncio_loop': True}
|
||||||
|
|
||||||
ex = Exchange(conf)
|
ex = Exchange(conf)
|
||||||
assert not log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}", caplog)
|
assert not log_has("Applying additional ccxt config: {'aiohttp_trust_env': True}", caplog)
|
||||||
assert not ex._api_async.aiohttp_trust_env
|
assert not ex._api_async.aiohttp_trust_env
|
||||||
@ -1758,10 +1762,13 @@ def test_get_fee(default_conf, mocker, exchange_name):
|
|||||||
'get_fee', 'calculate_fee', symbol="ETH/BTC")
|
'get_fee', 'calculate_fee', symbol="ETH/BTC")
|
||||||
|
|
||||||
|
|
||||||
def test_stoploss_limit_order_unsupported_exchange(default_conf, mocker):
|
def test_stoploss_order_unsupported_exchange(default_conf, mocker):
|
||||||
exchange = get_patched_exchange(mocker, default_conf, 'bittrex')
|
exchange = get_patched_exchange(mocker, default_conf, 'bittrex')
|
||||||
with pytest.raises(OperationalException, match=r"stoploss_limit is not implemented .*"):
|
with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"):
|
||||||
exchange.stoploss_limit(pair='ETH/BTC', amount=1, stop_price=220, rate=200)
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException, match=r"stoploss is not implemented .*"):
|
||||||
|
exchange.stoploss_adjust(1, {})
|
||||||
|
|
||||||
|
|
||||||
def test_merge_ft_has_dict(default_conf, mocker):
|
def test_merge_ft_has_dict(default_conf, mocker):
|
||||||
|
@ -3,6 +3,11 @@
|
|||||||
from random import randint
|
from random import randint
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import ccxt
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from freqtrade.exceptions import (DependencyException, InvalidOrderException,
|
||||||
|
OperationalException, TemporaryError)
|
||||||
from tests.conftest import get_patched_exchange
|
from tests.conftest import get_patched_exchange
|
||||||
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
from tests.exchange.test_exchange import ccxt_exceptionhandlers
|
||||||
|
|
||||||
@ -149,3 +154,98 @@ def test_get_balances_prod(default_conf, mocker):
|
|||||||
assert balances['4ST']['used'] == 0.0
|
assert balances['4ST']['used'] == 0.0
|
||||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken",
|
ccxt_exceptionhandlers(mocker, default_conf, api_mock, "kraken",
|
||||||
"get_balances", "fetch_balance")
|
"get_balances", "fetch_balance")
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_order_kraken(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||||
|
order_type = 'stop-loss'
|
||||||
|
|
||||||
|
api_mock.create_order = MagicMock(return_value={
|
||||||
|
'id': order_id,
|
||||||
|
'info': {
|
||||||
|
'foo': 'bar'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
default_conf['dry_run'] = False
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||||
|
|
||||||
|
# stoploss_on_exchange_limit_ratio is irrelevant for kraken market orders
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190,
|
||||||
|
order_types={'stoploss_on_exchange_limit_ratio': 1.05})
|
||||||
|
assert api_mock.create_order.call_count == 1
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert order['id'] == order_id
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['type'] == order_type
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell'
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['amount'] == 1
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['price'] == 220
|
||||||
|
assert api_mock.create_order.call_args_list[0][1]['params'] == {'trading_agreement': 'agree'}
|
||||||
|
|
||||||
|
# test exception handling
|
||||||
|
with pytest.raises(DependencyException):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||||
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
with pytest.raises(InvalidOrderException):
|
||||||
|
api_mock.create_order = MagicMock(
|
||||||
|
side_effect=ccxt.InvalidOrder("kraken Order would trigger immediately."))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||||
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
with pytest.raises(TemporaryError):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError("No connection"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||||
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
with pytest.raises(OperationalException, match=r".*DeadBeef.*"):
|
||||||
|
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError("DeadBeef"))
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||||
|
exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_order_dry_run_kraken(default_conf, mocker):
|
||||||
|
api_mock = MagicMock()
|
||||||
|
order_type = 'stop-loss'
|
||||||
|
default_conf['dry_run'] = True
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y)
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y)
|
||||||
|
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, api_mock, 'kraken')
|
||||||
|
|
||||||
|
api_mock.create_order.reset_mock()
|
||||||
|
|
||||||
|
order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={})
|
||||||
|
|
||||||
|
assert 'id' in order
|
||||||
|
assert 'info' in order
|
||||||
|
assert 'type' in order
|
||||||
|
|
||||||
|
assert order['type'] == order_type
|
||||||
|
assert order['price'] == 220
|
||||||
|
assert order['amount'] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_stoploss_adjust_kraken(mocker, default_conf):
|
||||||
|
exchange = get_patched_exchange(mocker, default_conf, id='kraken')
|
||||||
|
order = {
|
||||||
|
'type': 'stop-loss',
|
||||||
|
'price': 1500,
|
||||||
|
}
|
||||||
|
assert exchange.stoploss_adjust(1501, order)
|
||||||
|
assert not exchange.stoploss_adjust(1499, order)
|
||||||
|
# Test with invalid order case ...
|
||||||
|
order['type'] = 'stop_loss_limit'
|
||||||
|
assert not exchange.stoploss_adjust(1501, order)
|
||||||
|
@ -42,7 +42,13 @@ def hyperopt_results():
|
|||||||
'profit_percent': [-0.1, 0.2, 0.3],
|
'profit_percent': [-0.1, 0.2, 0.3],
|
||||||
'profit_abs': [-0.2, 0.4, 0.6],
|
'profit_abs': [-0.2, 0.4, 0.6],
|
||||||
'trade_duration': [10, 30, 10],
|
'trade_duration': [10, 30, 10],
|
||||||
'sell_reason': [SellType.STOP_LOSS, SellType.ROI, SellType.ROI]
|
'sell_reason': [SellType.STOP_LOSS, SellType.ROI, SellType.ROI],
|
||||||
|
'close_time':
|
||||||
|
[
|
||||||
|
datetime(2019, 1, 1, 9, 26, 3, 478039),
|
||||||
|
datetime(2019, 2, 1, 9, 26, 3, 478039),
|
||||||
|
datetime(2019, 3, 1, 9, 26, 3, 478039)
|
||||||
|
]
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -336,6 +342,24 @@ def test_sharpe_loss_prefers_higher_profits(default_conf, hyperopt_results) -> N
|
|||||||
assert under > correct
|
assert under > correct
|
||||||
|
|
||||||
|
|
||||||
|
def test_sharpe_loss_daily_prefers_higher_profits(default_conf, hyperopt_results) -> None:
|
||||||
|
results_over = hyperopt_results.copy()
|
||||||
|
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
|
||||||
|
results_under = hyperopt_results.copy()
|
||||||
|
results_under['profit_percent'] = hyperopt_results['profit_percent'] / 2
|
||||||
|
|
||||||
|
default_conf.update({'hyperopt_loss': 'SharpeHyperOptLossDaily'})
|
||||||
|
hl = HyperOptLossResolver.load_hyperoptloss(default_conf)
|
||||||
|
correct = hl.hyperopt_loss_function(hyperopt_results, len(hyperopt_results),
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
over = hl.hyperopt_loss_function(results_over, len(hyperopt_results),
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
under = hl.hyperopt_loss_function(results_under, len(hyperopt_results),
|
||||||
|
datetime(2019, 1, 1), datetime(2019, 5, 1))
|
||||||
|
assert over < correct
|
||||||
|
assert under > correct
|
||||||
|
|
||||||
|
|
||||||
def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None:
|
def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results) -> None:
|
||||||
results_over = hyperopt_results.copy()
|
results_over = hyperopt_results.copy()
|
||||||
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
|
results_over['profit_percent'] = hyperopt_results['profit_percent'] * 2
|
||||||
|
@ -21,14 +21,14 @@ def test_generate_text_table(default_conf, mocker):
|
|||||||
)
|
)
|
||||||
|
|
||||||
result_str = (
|
result_str = (
|
||||||
'| pair | buy count | avg profit % | cum profit % | '
|
'| Pair | Buy Count | Avg Profit % | Cum Profit % | Tot Profit BTC '
|
||||||
'tot profit BTC | tot profit % | avg duration | profit | loss |\n'
|
'| Tot Profit % | Avg Duration | Wins | Losses |\n'
|
||||||
'|:--------|------------:|---------------:|---------------:|'
|
'|:--------|------------:|---------------:|---------------:|-----------------:'
|
||||||
'-----------------:|---------------:|:---------------|---------:|-------:|\n'
|
'|---------------:|:---------------|-------:|---------:|\n'
|
||||||
'| ETH/BTC | 2 | 15.00 | 30.00 | '
|
'| ETH/BTC | 2 | 15.00 | 30.00 | 0.60000000 '
|
||||||
'0.60000000 | 15.00 | 0:20:00 | 2 | 0 |\n'
|
'| 15.00 | 0:20:00 | 2 | 0 |\n'
|
||||||
'| TOTAL | 2 | 15.00 | 30.00 | '
|
'| TOTAL | 2 | 15.00 | 30.00 | 0.60000000 '
|
||||||
'0.60000000 | 15.00 | 0:20:00 | 2 | 0 |'
|
'| 15.00 | 0:20:00 | 2 | 0 |'
|
||||||
)
|
)
|
||||||
assert generate_text_table(data={'ETH/BTC': {}},
|
assert generate_text_table(data={'ETH/BTC': {}},
|
||||||
stake_currency='BTC', max_open_trades=2,
|
stake_currency='BTC', max_open_trades=2,
|
||||||
@ -50,13 +50,19 @@ def test_generate_text_table_sell_reason(default_conf, mocker):
|
|||||||
)
|
)
|
||||||
|
|
||||||
result_str = (
|
result_str = (
|
||||||
'| Sell Reason | Count | Profit | Loss | Profit % |\n'
|
'| Sell Reason | Sell Count | Wins | Losses | Avg Profit % |'
|
||||||
'|:--------------|--------:|---------:|-------:|-----------:|\n'
|
' Cum Profit % | Tot Profit BTC | Tot Profit % |\n'
|
||||||
'| roi | 2 | 2 | 0 | 15 |\n'
|
'|:--------------|-------------:|-------:|---------:|---------------:|'
|
||||||
|
'---------------:|-----------------:|---------------:|\n'
|
||||||
|
'| roi | 2 | 2 | 0 | 15 |'
|
||||||
|
' 30 | 0.6 | 15 |\n'
|
||||||
'| stop_loss | 1 | 0 | 1 | -10 |'
|
'| stop_loss | 1 | 0 | 1 | -10 |'
|
||||||
|
' -10 | -0.2 | -5 |'
|
||||||
)
|
)
|
||||||
assert generate_text_table_sell_reason(
|
assert generate_text_table_sell_reason(
|
||||||
data={'ETH/BTC': {}}, results=results) == result_str
|
data={'ETH/BTC': {}},
|
||||||
|
stake_currency='BTC', max_open_trades=2,
|
||||||
|
results=results) == result_str
|
||||||
|
|
||||||
|
|
||||||
def test_generate_text_table_strategy(default_conf, mocker):
|
def test_generate_text_table_strategy(default_conf, mocker):
|
||||||
@ -85,10 +91,10 @@ def test_generate_text_table_strategy(default_conf, mocker):
|
|||||||
)
|
)
|
||||||
|
|
||||||
result_str = (
|
result_str = (
|
||||||
'| Strategy | buy count | avg profit % | cum profit % '
|
'| Strategy | Buy Count | Avg Profit % | Cum Profit % | Tot Profit BTC '
|
||||||
'| tot profit BTC | tot profit % | avg duration | profit | loss |\n'
|
'| Tot Profit % | Avg Duration | Wins | Losses |\n'
|
||||||
'|:-----------|------------:|---------------:|---------------:'
|
'|:-----------|------------:|---------------:|---------------:|-----------------:'
|
||||||
'|-----------------:|---------------:|:---------------|---------:|-------:|\n'
|
'|---------------:|:---------------|-------:|---------:|\n'
|
||||||
'| ETH/BTC | 3 | 20.00 | 60.00 '
|
'| ETH/BTC | 3 | 20.00 | 60.00 '
|
||||||
'| 1.10000000 | 30.00 | 0:17:00 | 3 | 0 |\n'
|
'| 1.10000000 | 30.00 | 0:17:00 | 3 | 0 |\n'
|
||||||
'| LTC/BTC | 3 | 30.00 | 90.00 '
|
'| LTC/BTC | 3 | 30.00 | 90.00 '
|
||||||
|
@ -141,7 +141,7 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
|||||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}],
|
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"}],
|
||||||
"BTC", ['HOT/BTC', 'FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']),
|
"BTC", ['HOT/BTC', 'FUEL/BTC', 'XRP/BTC', 'LTC/BTC', 'TKN/BTC']),
|
||||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}],
|
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}],
|
||||||
"USDT", ['ETH/USDT']),
|
"USDT", ['ETH/USDT', 'NANO/USDT']),
|
||||||
# No pair for ETH ...
|
# No pair for ETH ...
|
||||||
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}],
|
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"}],
|
||||||
"ETH", []),
|
"ETH", []),
|
||||||
@ -160,6 +160,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
|||||||
{"method": "PrecisionFilter"},
|
{"method": "PrecisionFilter"},
|
||||||
{"method": "PriceFilter", "low_price_ratio": 0.02}
|
{"method": "PriceFilter", "low_price_ratio": 0.02}
|
||||||
], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']),
|
], "BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC', 'XRP/BTC']),
|
||||||
|
# HOT and XRP are removed because below 1250 quoteVolume
|
||||||
|
([{"method": "VolumePairList", "number_assets": 5,
|
||||||
|
"sort_key": "quoteVolume", "min_value": 1250}],
|
||||||
|
"BTC", ['ETH/BTC', 'TKN/BTC', 'LTC/BTC']),
|
||||||
# StaticPairlist Only
|
# StaticPairlist Only
|
||||||
([{"method": "StaticPairList"},
|
([{"method": "StaticPairList"},
|
||||||
], "BTC", ['ETH/BTC', 'TKN/BTC']),
|
], "BTC", ['ETH/BTC', 'TKN/BTC']),
|
||||||
@ -167,6 +171,10 @@ def test_VolumePairList_refresh_empty(mocker, markets_empty, whitelist_conf):
|
|||||||
([{"method": "StaticPairList"},
|
([{"method": "StaticPairList"},
|
||||||
{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"},
|
{"method": "VolumePairList", "number_assets": 5, "sort_key": "bidVolume"},
|
||||||
], "BTC", ['TKN/BTC', 'ETH/BTC']),
|
], "BTC", ['TKN/BTC', 'ETH/BTC']),
|
||||||
|
# SpreadFilter
|
||||||
|
([{"method": "VolumePairList", "number_assets": 5, "sort_key": "quoteVolume"},
|
||||||
|
{"method": "SpreadFilter", "max_spread": 0.005}
|
||||||
|
], "USDT", ['ETH/USDT']),
|
||||||
])
|
])
|
||||||
def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers,
|
def test_VolumePairList_whitelist_gen(mocker, whitelist_conf, shitcoinmarkets, tickers,
|
||||||
pairlists, base_currency, whitelist_result,
|
pairlists, base_currency, whitelist_result,
|
||||||
|
@ -797,10 +797,10 @@ def test_process_operational_exception(default_conf, ticker, mocker) -> None:
|
|||||||
worker = Worker(args=None, config=default_conf)
|
worker = Worker(args=None, config=default_conf)
|
||||||
patch_get_signal(worker.freqtrade)
|
patch_get_signal(worker.freqtrade)
|
||||||
|
|
||||||
assert worker.state == State.RUNNING
|
assert worker.freqtrade.state == State.RUNNING
|
||||||
|
|
||||||
worker._process()
|
worker._process()
|
||||||
assert worker.state == State.STOPPED
|
assert worker.freqtrade.state == State.STOPPED
|
||||||
assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]['status']
|
assert 'OperationalException' in msg_mock.call_args_list[-1][0][0]['status']
|
||||||
|
|
||||||
|
|
||||||
@ -1023,8 +1023,8 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None
|
|||||||
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
|
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
|
||||||
return_value=limit_buy_order['amount'])
|
return_value=limit_buy_order['amount'])
|
||||||
|
|
||||||
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
stoploss = MagicMock(return_value={'id': 13434334})
|
||||||
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit)
|
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
@ -1037,13 +1037,13 @@ def test_add_stoploss_on_exchange(mocker, default_conf, limit_buy_order) -> None
|
|||||||
|
|
||||||
freqtrade.exit_positions(trades)
|
freqtrade.exit_positions(trades)
|
||||||
assert trade.stoploss_order_id == '13434334'
|
assert trade.stoploss_order_id == '13434334'
|
||||||
assert stoploss_limit.call_count == 1
|
assert stoploss.call_count == 1
|
||||||
assert trade.is_open is True
|
assert trade.is_open is True
|
||||||
|
|
||||||
|
|
||||||
def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
||||||
limit_buy_order, limit_sell_order) -> None:
|
limit_buy_order, limit_sell_order) -> None:
|
||||||
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
stoploss = MagicMock(return_value={'id': 13434334})
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -1056,7 +1056,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
|||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
stoploss_limit=stoploss_limit
|
stoploss=stoploss
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
@ -1070,7 +1070,7 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
|||||||
trade.stoploss_order_id = None
|
trade.stoploss_order_id = None
|
||||||
|
|
||||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
assert stoploss_limit.call_count == 1
|
assert stoploss.call_count == 1
|
||||||
assert trade.stoploss_order_id == "13434334"
|
assert trade.stoploss_order_id == "13434334"
|
||||||
|
|
||||||
# Second case: when stoploss is set but it is not yet hit
|
# Second case: when stoploss is set but it is not yet hit
|
||||||
@ -1094,10 +1094,10 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
|||||||
|
|
||||||
canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'})
|
canceled_stoploss_order = MagicMock(return_value={'status': 'canceled'})
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_order', canceled_stoploss_order)
|
mocker.patch('freqtrade.exchange.Exchange.get_order', canceled_stoploss_order)
|
||||||
stoploss_limit.reset_mock()
|
stoploss.reset_mock()
|
||||||
|
|
||||||
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
assert stoploss_limit.call_count == 1
|
assert stoploss.call_count == 1
|
||||||
assert trade.stoploss_order_id == "13434334"
|
assert trade.stoploss_order_id == "13434334"
|
||||||
|
|
||||||
# Fourth case: when stoploss is set and it is hit
|
# Fourth case: when stoploss is set and it is hit
|
||||||
@ -1124,9 +1124,10 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
|||||||
assert trade.is_open is False
|
assert trade.is_open is False
|
||||||
|
|
||||||
mocker.patch(
|
mocker.patch(
|
||||||
'freqtrade.exchange.Exchange.stoploss_limit',
|
'freqtrade.exchange.Exchange.stoploss',
|
||||||
side_effect=DependencyException()
|
side_effect=DependencyException()
|
||||||
)
|
)
|
||||||
|
trade.is_open = True
|
||||||
freqtrade.handle_stoploss_on_exchange(trade)
|
freqtrade.handle_stoploss_on_exchange(trade)
|
||||||
assert log_has('Unable to place a stoploss order on exchange.', caplog)
|
assert log_has('Unable to place a stoploss order on exchange.', caplog)
|
||||||
assert trade.stoploss_order_id is None
|
assert trade.stoploss_order_id is None
|
||||||
@ -1134,11 +1135,21 @@ def test_handle_stoploss_on_exchange(mocker, default_conf, fee, caplog,
|
|||||||
# Fifth case: get_order returns InvalidOrder
|
# Fifth case: get_order returns InvalidOrder
|
||||||
# It should try to add stoploss order
|
# It should try to add stoploss order
|
||||||
trade.stoploss_order_id = 100
|
trade.stoploss_order_id = 100
|
||||||
stoploss_limit.reset_mock()
|
stoploss.reset_mock()
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_order', side_effect=InvalidOrderException())
|
mocker.patch('freqtrade.exchange.Exchange.get_order', side_effect=InvalidOrderException())
|
||||||
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_limit)
|
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
|
||||||
freqtrade.handle_stoploss_on_exchange(trade)
|
freqtrade.handle_stoploss_on_exchange(trade)
|
||||||
assert stoploss_limit.call_count == 1
|
assert stoploss.call_count == 1
|
||||||
|
|
||||||
|
# Sixth case: Closed Trade
|
||||||
|
# Should not create new order
|
||||||
|
trade.stoploss_order_id = None
|
||||||
|
trade.is_open = False
|
||||||
|
stoploss.reset_mock()
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.get_order')
|
||||||
|
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss)
|
||||||
|
assert freqtrade.handle_stoploss_on_exchange(trade) is False
|
||||||
|
assert stoploss.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
|
def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
|
||||||
@ -1157,7 +1168,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
|
|||||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
get_order=MagicMock(return_value={'status': 'canceled'}),
|
get_order=MagicMock(return_value={'status': 'canceled'}),
|
||||||
stoploss_limit=MagicMock(side_effect=DependencyException()),
|
stoploss=MagicMock(side_effect=DependencyException()),
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
@ -1165,7 +1176,7 @@ def test_handle_sle_cancel_cant_recreate(mocker, default_conf, fee, caplog,
|
|||||||
freqtrade.enter_positions()
|
freqtrade.enter_positions()
|
||||||
trade = Trade.query.first()
|
trade = Trade.query.first()
|
||||||
trade.is_open = True
|
trade.is_open = True
|
||||||
trade.open_order_id = '12345'
|
trade.open_order_id = None
|
||||||
trade.stoploss_order_id = 100
|
trade.stoploss_order_id = 100
|
||||||
assert trade
|
assert trade
|
||||||
|
|
||||||
@ -1191,7 +1202,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee,
|
|||||||
sell=sell_mock,
|
sell=sell_mock,
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
get_order=MagicMock(return_value={'status': 'canceled'}),
|
get_order=MagicMock(return_value={'status': 'canceled'}),
|
||||||
stoploss_limit=MagicMock(side_effect=InvalidOrderException()),
|
stoploss=MagicMock(side_effect=InvalidOrderException()),
|
||||||
)
|
)
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
patch_get_signal(freqtrade)
|
patch_get_signal(freqtrade)
|
||||||
@ -1221,7 +1232,7 @@ def test_create_stoploss_order_invalid_order(mocker, default_conf, caplog, fee,
|
|||||||
def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
||||||
limit_buy_order, limit_sell_order) -> None:
|
limit_buy_order, limit_sell_order) -> None:
|
||||||
# When trailing stoploss is set
|
# When trailing stoploss is set
|
||||||
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
stoploss = MagicMock(return_value={'id': 13434334})
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
@ -1233,7 +1244,8 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
|||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
stoploss_limit=stoploss_limit
|
stoploss=stoploss,
|
||||||
|
stoploss_adjust=MagicMock(return_value=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
# enabling TSL
|
# enabling TSL
|
||||||
@ -1288,7 +1300,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
|||||||
cancel_order_mock = MagicMock()
|
cancel_order_mock = MagicMock()
|
||||||
stoploss_order_mock = MagicMock()
|
stoploss_order_mock = MagicMock()
|
||||||
mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock)
|
mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_order_mock)
|
mocker.patch('freqtrade.exchange.Exchange.stoploss', stoploss_order_mock)
|
||||||
|
|
||||||
# stoploss should not be updated as the interval is 60 seconds
|
# stoploss should not be updated as the interval is 60 seconds
|
||||||
assert freqtrade.handle_trade(trade) is False
|
assert freqtrade.handle_trade(trade) is False
|
||||||
@ -1307,7 +1319,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
|||||||
cancel_order_mock.assert_called_once_with(100, 'ETH/BTC')
|
cancel_order_mock.assert_called_once_with(100, 'ETH/BTC')
|
||||||
stoploss_order_mock.assert_called_once_with(amount=85.25149190110828,
|
stoploss_order_mock.assert_called_once_with(amount=85.25149190110828,
|
||||||
pair='ETH/BTC',
|
pair='ETH/BTC',
|
||||||
rate=0.00002344 * 0.95 * 0.99,
|
order_types=freqtrade.strategy.order_types,
|
||||||
stop_price=0.00002344 * 0.95)
|
stop_price=0.00002344 * 0.95)
|
||||||
|
|
||||||
# price fell below stoploss, so dry-run sells trade.
|
# price fell below stoploss, so dry-run sells trade.
|
||||||
@ -1322,7 +1334,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
|
|||||||
def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog,
|
def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, caplog,
|
||||||
limit_buy_order, limit_sell_order) -> None:
|
limit_buy_order, limit_sell_order) -> None:
|
||||||
# When trailing stoploss is set
|
# When trailing stoploss is set
|
||||||
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
stoploss = MagicMock(return_value={'id': 13434334})
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
@ -1335,7 +1347,8 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
|
|||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
stoploss_limit=stoploss_limit
|
stoploss=stoploss,
|
||||||
|
stoploss_adjust=MagicMock(return_value=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
# enabling TSL
|
# enabling TSL
|
||||||
@ -1375,12 +1388,12 @@ def test_handle_stoploss_on_exchange_trailing_error(mocker, default_conf, fee, c
|
|||||||
assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog)
|
assert log_has_re(r"Could not cancel stoploss order abcd for pair ETH/BTC.*", caplog)
|
||||||
|
|
||||||
# Still try to create order
|
# Still try to create order
|
||||||
assert stoploss_limit.call_count == 1
|
assert stoploss.call_count == 1
|
||||||
|
|
||||||
# Fail creating stoploss order
|
# Fail creating stoploss order
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_order", MagicMock())
|
cancel_mock = mocker.patch("freqtrade.exchange.Exchange.cancel_order", MagicMock())
|
||||||
mocker.patch("freqtrade.exchange.Exchange.stoploss_limit", side_effect=DependencyException())
|
mocker.patch("freqtrade.exchange.Exchange.stoploss", side_effect=DependencyException())
|
||||||
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
|
freqtrade.handle_trailing_stoploss_on_exchange(trade, stoploss_order_hanging)
|
||||||
assert cancel_mock.call_count == 1
|
assert cancel_mock.call_count == 1
|
||||||
assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog)
|
assert log_has_re(r"Could not create trailing stoploss order for pair ETH/BTC\..*", caplog)
|
||||||
@ -1390,12 +1403,13 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
|||||||
limit_buy_order, limit_sell_order) -> None:
|
limit_buy_order, limit_sell_order) -> None:
|
||||||
|
|
||||||
# When trailing stoploss is set
|
# When trailing stoploss is set
|
||||||
stoploss_limit = MagicMock(return_value={'id': 13434334})
|
stoploss = MagicMock(return_value={'id': 13434334})
|
||||||
patch_RPCManager(mocker)
|
patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
patch_edge(mocker)
|
patch_edge(mocker)
|
||||||
edge_conf['max_open_trades'] = float('inf')
|
edge_conf['max_open_trades'] = float('inf')
|
||||||
edge_conf['dry_run_wallet'] = 999.9
|
edge_conf['dry_run_wallet'] = 999.9
|
||||||
|
edge_conf['exchange']['name'] = 'binance'
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=MagicMock(return_value={
|
fetch_ticker=MagicMock(return_value={
|
||||||
@ -1406,7 +1420,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
|||||||
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
|
||||||
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
sell=MagicMock(return_value={'id': limit_sell_order['id']}),
|
||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
stoploss_limit=stoploss_limit
|
stoploss=stoploss,
|
||||||
)
|
)
|
||||||
|
|
||||||
# enabling TSL
|
# enabling TSL
|
||||||
@ -1459,7 +1473,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
|||||||
cancel_order_mock = MagicMock()
|
cancel_order_mock = MagicMock()
|
||||||
stoploss_order_mock = MagicMock()
|
stoploss_order_mock = MagicMock()
|
||||||
mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock)
|
mocker.patch('freqtrade.exchange.Exchange.cancel_order', cancel_order_mock)
|
||||||
mocker.patch('freqtrade.exchange.Exchange.stoploss_limit', stoploss_order_mock)
|
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss_order_mock)
|
||||||
|
|
||||||
# price goes down 5%
|
# price goes down 5%
|
||||||
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={
|
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={
|
||||||
@ -1492,7 +1506,7 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
|
|||||||
cancel_order_mock.assert_called_once_with(100, 'NEO/BTC')
|
cancel_order_mock.assert_called_once_with(100, 'NEO/BTC')
|
||||||
stoploss_order_mock.assert_called_once_with(amount=2131074.168797954,
|
stoploss_order_mock.assert_called_once_with(amount=2131074.168797954,
|
||||||
pair='NEO/BTC',
|
pair='NEO/BTC',
|
||||||
rate=0.00002344 * 0.99 * 0.99,
|
order_types=freqtrade.strategy.order_types,
|
||||||
stop_price=0.00002344 * 0.99)
|
stop_price=0.00002344 * 0.99)
|
||||||
|
|
||||||
|
|
||||||
@ -2423,7 +2437,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
|
|||||||
default_conf['exchange']['name'] = 'binance'
|
default_conf['exchange']['name'] = 'binance'
|
||||||
rpc_mock = patch_RPCManager(mocker)
|
rpc_mock = patch_RPCManager(mocker)
|
||||||
patch_exchange(mocker)
|
patch_exchange(mocker)
|
||||||
stoploss_limit = MagicMock(return_value={
|
stoploss = MagicMock(return_value={
|
||||||
'id': 123,
|
'id': 123,
|
||||||
'info': {
|
'info': {
|
||||||
'foo': 'bar'
|
'foo': 'bar'
|
||||||
@ -2437,7 +2451,7 @@ def test_execute_sell_with_stoploss_on_exchange(default_conf, ticker, fee, ticke
|
|||||||
get_fee=fee,
|
get_fee=fee,
|
||||||
amount_to_precision=lambda s, x, y: y,
|
amount_to_precision=lambda s, x, y: y,
|
||||||
price_to_precision=lambda s, x, y: y,
|
price_to_precision=lambda s, x, y: y,
|
||||||
stoploss_limit=stoploss_limit,
|
stoploss=stoploss,
|
||||||
cancel_order=cancel_order,
|
cancel_order=cancel_order,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -2482,14 +2496,14 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
|
|||||||
price_to_precision=lambda s, x, y: y,
|
price_to_precision=lambda s, x, y: y,
|
||||||
)
|
)
|
||||||
|
|
||||||
stoploss_limit = MagicMock(return_value={
|
stoploss = MagicMock(return_value={
|
||||||
'id': 123,
|
'id': 123,
|
||||||
'info': {
|
'info': {
|
||||||
'foo': 'bar'
|
'foo': 'bar'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
mocker.patch('freqtrade.exchange.Binance.stoploss_limit', stoploss_limit)
|
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
|
||||||
|
|
||||||
freqtrade = FreqtradeBot(default_conf)
|
freqtrade = FreqtradeBot(default_conf)
|
||||||
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||||
@ -2507,7 +2521,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
|
|||||||
# Assuming stoploss on exchnage is hit
|
# Assuming stoploss on exchnage is hit
|
||||||
# stoploss_order_id should become None
|
# stoploss_order_id should become None
|
||||||
# and trade should be sold at the price of stoploss
|
# and trade should be sold at the price of stoploss
|
||||||
stoploss_limit_executed = MagicMock(return_value={
|
stoploss_executed = MagicMock(return_value={
|
||||||
"id": "123",
|
"id": "123",
|
||||||
"timestamp": 1542707426845,
|
"timestamp": 1542707426845,
|
||||||
"datetime": "2018-11-20T09:50:26.845Z",
|
"datetime": "2018-11-20T09:50:26.845Z",
|
||||||
@ -2525,7 +2539,7 @@ def test_may_execute_sell_after_stoploss_on_exchange_hit(default_conf, ticker, f
|
|||||||
"fee": None,
|
"fee": None,
|
||||||
"trades": None
|
"trades": None
|
||||||
})
|
})
|
||||||
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_limit_executed)
|
mocker.patch('freqtrade.exchange.Exchange.get_order', stoploss_executed)
|
||||||
|
|
||||||
freqtrade.exit_positions(trades)
|
freqtrade.exit_positions(trades)
|
||||||
assert trade.stoploss_order_id is None
|
assert trade.stoploss_order_id is None
|
||||||
@ -3631,7 +3645,7 @@ def test_startup_state(default_conf, mocker):
|
|||||||
}
|
}
|
||||||
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=True))
|
||||||
worker = get_patched_worker(mocker, default_conf)
|
worker = get_patched_worker(mocker, default_conf)
|
||||||
assert worker.state is State.RUNNING
|
assert worker.freqtrade.state is State.RUNNING
|
||||||
|
|
||||||
|
|
||||||
def test_startup_trade_reinit(default_conf, edge_conf, mocker):
|
def test_startup_trade_reinit(default_conf, edge_conf, mocker):
|
||||||
|
@ -20,7 +20,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
|||||||
default_conf['max_open_trades'] = 3
|
default_conf['max_open_trades'] = 3
|
||||||
default_conf['exchange']['name'] = 'binance'
|
default_conf['exchange']['name'] = 'binance'
|
||||||
|
|
||||||
stoploss_limit = {
|
stoploss = {
|
||||||
'id': 123,
|
'id': 123,
|
||||||
'info': {}
|
'info': {}
|
||||||
}
|
}
|
||||||
@ -53,7 +53,7 @@ def test_may_execute_sell_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
|||||||
SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)]
|
SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)]
|
||||||
)
|
)
|
||||||
cancel_order_mock = MagicMock()
|
cancel_order_mock = MagicMock()
|
||||||
mocker.patch('freqtrade.exchange.Binance.stoploss_limit', stoploss_limit)
|
mocker.patch('freqtrade.exchange.Binance.stoploss', stoploss)
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker,
|
||||||
|
@ -11,11 +11,11 @@ from tests.conftest import get_patched_worker, log_has
|
|||||||
def test_worker_state(mocker, default_conf, markets) -> None:
|
def test_worker_state(mocker, default_conf, markets) -> None:
|
||||||
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
|
mocker.patch('freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets))
|
||||||
worker = get_patched_worker(mocker, default_conf)
|
worker = get_patched_worker(mocker, default_conf)
|
||||||
assert worker.state is State.RUNNING
|
assert worker.freqtrade.state is State.RUNNING
|
||||||
|
|
||||||
default_conf.pop('initial_state')
|
default_conf.pop('initial_state')
|
||||||
worker = Worker(args=None, config=default_conf)
|
worker = Worker(args=None, config=default_conf)
|
||||||
assert worker.state is State.STOPPED
|
assert worker.freqtrade.state is State.STOPPED
|
||||||
|
|
||||||
|
|
||||||
def test_worker_running(mocker, default_conf, caplog) -> None:
|
def test_worker_running(mocker, default_conf, caplog) -> None:
|
||||||
@ -41,7 +41,7 @@ def test_worker_stopped(mocker, default_conf, caplog) -> None:
|
|||||||
mock_sleep = mocker.patch('time.sleep', return_value=None)
|
mock_sleep = mocker.patch('time.sleep', return_value=None)
|
||||||
|
|
||||||
worker = get_patched_worker(mocker, default_conf)
|
worker = get_patched_worker(mocker, default_conf)
|
||||||
worker.state = State.STOPPED
|
worker.freqtrade.state = State.STOPPED
|
||||||
state = worker._worker(old_state=State.RUNNING)
|
state = worker._worker(old_state=State.RUNNING)
|
||||||
assert state is State.STOPPED
|
assert state is State.STOPPED
|
||||||
assert log_has('Changing state to: STOPPED', caplog)
|
assert log_has('Changing state to: STOPPED', caplog)
|
||||||
|
Loading…
Reference in New Issue
Block a user