Merge branch 'develop' into data_handler

This commit is contained in:
Matthias 2020-02-09 15:16:43 +01:00
commit d65a06947d
74 changed files with 1078 additions and 408 deletions

View File

@ -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

View File

@ -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 \

View File

@ -44,7 +44,7 @@
"DASH/BTC", "DASH/BTC",
"ZEC/BTC", "ZEC/BTC",
"XLM/BTC", "XLM/BTC",
"NXT/BTC", "XRP/BTC",
"TRX/BTC", "TRX/BTC",
"ADA/BTC", "ADA/BTC",
"XMR/BTC" "XMR/BTC"

View File

@ -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",

View File

@ -4,6 +4,34 @@ This page explains some advanced Hyperopt topics that may require higher
coding skills and Python knowledge than creation of an ordinal hyperoptimization coding skills and Python knowledge than creation of an ordinal hyperoptimization
class. class.
## Derived hyperopt classes
Custom hyperop classes can be derived in the same way [it can be done for strategies](strategy-customization.md#derived-strategies).
Applying to hyperoptimization, as an example, you may override how dimensions are defined in your optimization hyperspace:
```python
class MyAwesomeHyperOpt(IHyperOpt):
...
# Uses default stoploss dimension
class MyAwesomeHyperOpt2(MyAwesomeHyperOpt):
@staticmethod
def stoploss_space() -> List[Dimension]:
# Override boundaries for stoploss
return [
Real(-0.33, -0.01, name='stoploss'),
]
```
and then quickly switch between hyperopt classes, running optimization process with hyperopt class you need in each particular case:
```
$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt ...
or
$ freqtrade hyperopt --hyperopt MyAwesomeHyperOpt2 ...
```
## Creating and using a custom loss function ## Creating and using a custom loss function
To use a custom loss function class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt loss class. To use a custom loss function class, make sure that the function `hyperopt_loss_function` is defined in your custom hyperopt loss class.

View File

@ -119,40 +119,40 @@ A backtesting result will look like that:
``` ```
========================================================= BACKTESTING REPORT ======================================================== ========================================================= BACKTESTING REPORT ========================================================
| pair | buy count | avg profit % | cum profit % | tot profit BTC | tot profit % | avg duration | profit | loss | | Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses |
|:---------|------------:|---------------:|---------------:|-----------------:|---------------:|:---------------|---------:|-------:| |:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|--------:|
| ADA/BTC | 35 | -0.11 | -3.88 | -0.00019428 | -1.94 | 4:35:00 | 14 | 21 | | ADA/BTC | 35 | -0.11 | -3.88 | -0.00019428 | -1.94 | 4:35:00 | 14 | 0 | 21 |
| ARK/BTC | 11 | -0.41 | -4.52 | -0.00022647 | -2.26 | 2:03:00 | 3 | 8 | | ARK/BTC | 11 | -0.41 | -4.52 | -0.00022647 | -2.26 | 2:03:00 | 3 | 0 | 8 |
| BTS/BTC | 32 | 0.31 | 9.78 | 0.00048938 | 4.89 | 5:05:00 | 18 | 14 | | BTS/BTC | 32 | 0.31 | 9.78 | 0.00048938 | 4.89 | 5:05:00 | 18 | 0 | 14 |
| DASH/BTC | 13 | -0.08 | -1.07 | -0.00005343 | -0.53 | 4:39:00 | 6 | 7 | | DASH/BTC | 13 | -0.08 | -1.07 | -0.00005343 | -0.53 | 4:39:00 | 6 | 0 | 7 |
| ENG/BTC | 18 | 1.36 | 24.54 | 0.00122807 | 12.27 | 2:50:00 | 8 | 10 | | ENG/BTC | 18 | 1.36 | 24.54 | 0.00122807 | 12.27 | 2:50:00 | 8 | 0 | 10 |
| EOS/BTC | 36 | 0.08 | 3.06 | 0.00015304 | 1.53 | 3:34:00 | 16 | 20 | | EOS/BTC | 36 | 0.08 | 3.06 | 0.00015304 | 1.53 | 3:34:00 | 16 | 0 | 20 |
| ETC/BTC | 26 | 0.37 | 9.51 | 0.00047576 | 4.75 | 6:14:00 | 11 | 15 | | ETC/BTC | 26 | 0.37 | 9.51 | 0.00047576 | 4.75 | 6:14:00 | 11 | 0 | 15 |
| ETH/BTC | 33 | 0.30 | 9.96 | 0.00049856 | 4.98 | 7:31:00 | 16 | 17 | | ETH/BTC | 33 | 0.30 | 9.96 | 0.00049856 | 4.98 | 7:31:00 | 16 | 0 | 17 |
| IOTA/BTC | 32 | 0.03 | 1.09 | 0.00005444 | 0.54 | 3:12:00 | 14 | 18 | | IOTA/BTC | 32 | 0.03 | 1.09 | 0.00005444 | 0.54 | 3:12:00 | 14 | 0 | 18 |
| LSK/BTC | 15 | 1.75 | 26.26 | 0.00131413 | 13.13 | 2:58:00 | 6 | 9 | | LSK/BTC | 15 | 1.75 | 26.26 | 0.00131413 | 13.13 | 2:58:00 | 6 | 0 | 9 |
| LTC/BTC | 32 | -0.04 | -1.38 | -0.00006886 | -0.69 | 4:49:00 | 11 | 21 | | LTC/BTC | 32 | -0.04 | -1.38 | -0.00006886 | -0.69 | 4:49:00 | 11 | 0 | 21 |
| NANO/BTC | 17 | 1.26 | 21.39 | 0.00107058 | 10.70 | 1:55:00 | 10 | 7 | | NANO/BTC | 17 | 1.26 | 21.39 | 0.00107058 | 10.70 | 1:55:00 | 10 | 0 | 7 |
| NEO/BTC | 23 | 0.82 | 18.97 | 0.00094936 | 9.48 | 2:59:00 | 10 | 13 | | NEO/BTC | 23 | 0.82 | 18.97 | 0.00094936 | 9.48 | 2:59:00 | 10 | 0 | 13 |
| REQ/BTC | 9 | 1.17 | 10.54 | 0.00052734 | 5.27 | 3:47:00 | 4 | 5 | | REQ/BTC | 9 | 1.17 | 10.54 | 0.00052734 | 5.27 | 3:47:00 | 4 | 0 | 5 |
| XLM/BTC | 16 | 1.22 | 19.54 | 0.00097800 | 9.77 | 3:15:00 | 7 | 9 | | XLM/BTC | 16 | 1.22 | 19.54 | 0.00097800 | 9.77 | 3:15:00 | 7 | 0 | 9 |
| XMR/BTC | 23 | -0.18 | -4.13 | -0.00020696 | -2.07 | 5:30:00 | 12 | 11 | | XMR/BTC | 23 | -0.18 | -4.13 | -0.00020696 | -2.07 | 5:30:00 | 12 | 0 | 11 |
| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 | 23 | | XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 | 0 | 23 |
| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 | 15 | | ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 | 0 | 15 |
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 243 | | TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 0 | 243 |
========================================================= SELL REASON STATS ========================================================= ========================================================= SELL REASON STATS =========================================================
| Sell Reason | Count | Profit | Loss | | Sell Reason | Sells | Wins | Draws | Losses |
|:-------------------|--------:|---------:|-------:| |:-------------------|--------:|------:|-------:|--------:|
| trailing_stop_loss | 205 | 150 | 55 | | trailing_stop_loss | 205 | 150 | 0 | 55 |
| stop_loss | 166 | 0 | 166 | | stop_loss | 166 | 0 | 0 | 166 |
| sell_signal | 56 | 36 | 20 | | sell_signal | 56 | 36 | 0 | 20 |
| force_sell | 2 | 0 | 2 | | force_sell | 2 | 0 | 0 | 2 |
====================================================== LEFT OPEN TRADES REPORT ====================================================== ====================================================== LEFT OPEN TRADES REPORT ======================================================
| pair | buy count | avg profit % | cum profit % | tot profit BTC | tot profit % | avg duration | profit | loss | | Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses |
|:---------|------------:|---------------:|---------------:|-----------------:|---------------:|:---------------|---------:|-------:| |:---------|-------:|---------------:|---------------:|-----------------:|---------------:|:---------------|------:|-------:|--------:|
| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 | 0 | | ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 | 0 | 0 |
| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 | 0 | | LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 | 0 | 0 |
| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 | 0 | | TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 | 0 | 0 |
``` ```
The 1st table contains all trades the bot made, including "left open trades". The 1st table contains all trades the bot made, including "left open trades".
@ -237,11 +237,11 @@ 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 | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | 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 | 0 | 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 | 0 | 825 |
``` ```
## Next step ## Next step

View File

@ -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).

View File

@ -280,7 +280,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$.
@ -505,6 +505,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.
@ -553,6 +554,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%.
@ -604,6 +610,14 @@ Once you will be happy with your bot performance running in the Dry-run mode, yo
!!! Note !!! Note
A simulated wallet is available during dry-run mode, and will assume a starting capital of `dry_run_wallet` (defaults to 1000). A simulated wallet is available during dry-run mode, and will assume a starting capital of `dry_run_wallet` (defaults to 1000).
### Considerations for dry-run
* API-keys may or may not be provided. Only Read-Only operations (i.e. operations that do not alter account state) on the exchange are performed in the dry-run mode.
* Wallets (`/balance`) are simulated.
* Orders are simulated, and will not be posted to the exchange.
* In combination with `stoploss_on_exchange`, the stop_loss price is assumed to be filled.
* Open orders (not trades, which are stored in the database) are reset on bot restart.
## Switch to production mode ## Switch to production mode
In production mode, the bot will engage your money. Be careful, since a wrong In production mode, the bot will engage your money. Be careful, since a wrong

View File

@ -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.

View File

@ -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.
@ -206,7 +207,7 @@ We strongly recommend to use `screen` or `tmux` to prevent any connection loss.
freqtrade hyperopt --config config.json --hyperopt <hyperoptname> -e 5000 --spaces all freqtrade hyperopt --config config.json --hyperopt <hyperoptname> -e 5000 --spaces all
``` ```
Use `<hyperoptname>` as the name of the custom hyperopt used. Use `<hyperoptname>` as the name of the custom hyperopt used.
The `-e` option will set how many evaluations hyperopt will do. We recommend The `-e` option will set how many evaluations hyperopt will do. We recommend
running at least several thousand evaluations. running at least several thousand evaluations.
@ -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,18 +373,19 @@ 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
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 |
| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 | | 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 |
These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the ticker interval used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the ticker interval used. These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the ticker interval used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the ticker interval used.
@ -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

View File

@ -42,7 +42,7 @@ The easiest way to install and run Freqtrade is to clone the bot GitHub reposito
This can be achieved with the following commands: This can be achieved with the following commands:
```bash ```bash
git clone git@github.com:freqtrade/freqtrade.git git clone https://github.com/freqtrade/freqtrade.git
cd freqtrade cd freqtrade
git checkout master # Optional, see (1) git checkout master # Optional, see (1)
./setup.sh --install ./setup.sh --install

View File

@ -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

View File

@ -532,6 +532,27 @@ If you want to use a strategy from a different directory you can pass `--strateg
freqtrade trade --strategy AwesomeStrategy --strategy-path /some/directory freqtrade trade --strategy AwesomeStrategy --strategy-path /some/directory
``` ```
### Derived strategies
The strategies can be derived from other strategies. This avoids duplication of your custom strategy code. You can use this technique to override small parts of your main strategy, leaving the rest untouched:
``` python
class MyAwesomeStrategy(IStrategy):
...
stoploss = 0.13
trailing_stop = False
# All other attributes and methods are here as they
# should be in any custom strategy...
...
class MyAwesomeStrategy2(MyAwesomeStrategy):
# Override something
stoploss = 0.08
trailing_stop = True
```
Both attributes and methods may be overriden, altering behavior of the original strategy in a way you need.
### Common mistakes when developing strategies ### Common mistakes when developing strategies
Backtesting analyzes the whole time-range at once for performance reasons. Because of this, strategy authors need to make sure that strategies do not look-ahead into the future. Backtesting analyzes the whole time-range at once for performance reasons. Because of this, strategy authors need to make sure that strategies do not look-ahead into the future.

View File

@ -108,9 +108,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
@ -133,22 +133,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.

View File

@ -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)

View File

@ -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,9 +68,9 @@ ARGS_HYPEROPT_LIST = ["hyperopt_list_best", "hyperopt_list_profitable", "print_c
ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_show_index", ARGS_HYPEROPT_SHOW = ["hyperopt_list_best", "hyperopt_list_profitable", "hyperopt_show_index",
"print_json", "hyperopt_show_no_header"] "print_json", "hyperopt_show_no_header"]
NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", NO_CONF_REQURIED = ["convert-data", "convert-trade-data", "download-data", "list-timeframes",
"list-timeframes", "list-markets", "list-pairs", "list-markets", "list-pairs", "list-strategies",
"list-strategies", "hyperopt-list", "hyperopt-show", "list-hyperopts", "hyperopt-list", "hyperopt-show",
"plot-dataframe", "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"]
@ -137,9 +139,10 @@ class Arguments:
from freqtrade.commands import (start_create_userdir, start_convert_data, from freqtrade.commands import (start_create_userdir, start_convert_data,
start_download_data, 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_new_hyperopt, start_list_markets, start_list_strategies,
start_new_strategy, start_list_timeframes, start_list_timeframes,
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,
start_test_pairlist, start_trading) start_test_pairlist, start_trading)
@ -203,6 +206,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',

View File

@ -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,

View File

@ -39,26 +39,31 @@ def start_download_data(args: Dict[str, Any]) -> None:
pairs_not_available: List[str] = [] pairs_not_available: List[str] = []
# Init exchange # Init exchange
exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config) exchange = ExchangeResolver.load_exchange(config['exchange']['name'], config, validate=False)
# Manual validations of relevant settings
exchange.validate_pairs(config['pairs'])
for timeframe in config['timeframes']:
exchange.validate_timeframes(timeframe)
try: try:
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")),
data_format=config['dataformat_trades']) data_format=config['dataformat_trades'])
# 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")),
data_format_ohlcv=config['dataformat_ohlcv'], data_format_ohlcv=config['dataformat_ohlcv'],
data_format_trades=config['dataformat_trades'], data_format_trades=config['dataformat_trades'],
) )
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")),
data_format=config['dataformat_ohlcv']) data_format=config['dataformat_ohlcv'])
except KeyboardInterrupt: except KeyboardInterrupt:

View File

@ -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
""" """

View File

@ -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

View File

@ -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` "

View File

@ -10,8 +10,10 @@ def start_trading(args: Dict[str, Any]) -> int:
""" """
Main entry point for trading mode Main entry point for trading mode
""" """
# Import here to avoid loading worker module when it's not used
from freqtrade.worker import Worker from freqtrade.worker import Worker
# Load and run worker
# Create and run worker
worker = None worker = None
try: try:
worker = Worker(args) worker = Worker(args)

View File

@ -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.

View File

@ -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:

View File

@ -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.

View File

@ -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

View File

@ -17,22 +17,24 @@ 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']
AVAILABLE_DATAHANDLERS = ['json', 'jsongz'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz']
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
DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume'] DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume']
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 = [

View File

@ -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.

View File

@ -219,7 +219,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, data_format: str = None) -> List[str]: erase: bool = False, data_format: str = None) -> 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.
@ -290,7 +290,7 @@ def _download_trades_history(exchange: Exchange,
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, timerange: TimeRange, erase: bool = False,
data_format: str = 'jsongz') -> List[str]: data_format: str = 'jsongz') -> List[str]:
""" """
Refresh stored trades data for backtesting and hyperopt operations. Refresh stored trades data for backtesting and hyperopt operations.
@ -318,7 +318,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, datadir: Path, timerange: TimeRange, erase: bool = False,
data_format_ohlcv: str = 'json', data_format_ohlcv: str = 'json',
data_format_trades: str = 'jsongz') -> None: data_format_trades: str = 'jsongz') -> None:
""" """

View File

@ -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
@ -182,7 +182,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
""" """

View File

@ -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

View File

@ -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
@ -282,8 +286,8 @@ class Exchange:
quote_currencies = self.get_quote_currencies() quote_currencies = self.get_quote_currencies()
if stake_currency not in quote_currencies: if stake_currency not in quote_currencies:
raise OperationalException( raise OperationalException(
f"{stake_currency} is not available as stake on {self.name}. " f"{stake_currency} is not available as stake on {self.name}. "
f"Available currencies are: {', '.join(quote_currencies)}") f"Available currencies are: {', '.join(quote_currencies)}")
def validate_pairs(self, pairs: List[str]) -> None: def validate_pairs(self, pairs: List[str]) -> None:
""" """
@ -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(),
@ -460,7 +464,7 @@ class Exchange:
"status": "closed", "status": "closed",
"filled": closed_order["amount"], "filled": closed_order["amount"],
"remaining": 0 "remaining": 0
}) })
if closed_order["type"] in ["stop_loss_limit"]: if closed_order["type"] in ["stop_loss_limit"]:
closed_order["info"].update({"stopPrice": closed_order["price"]}) closed_order["info"].update({"stopPrice": closed_order["price"]})
self._dry_run_open_orders[closed_order["id"]] = closed_order self._dry_run_open_orders[closed_order["id"]] = closed_order
@ -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.
""" """

View File

@ -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

View File

@ -95,6 +95,16 @@ class FreqtradeBot:
# Protect sell-logic from forcesell and viceversa # Protect sell-logic from forcesell and viceversa
self._sell_lock = Lock() self._sell_lock = Lock()
def notify_status(self, msg: str) -> None:
"""
Public method for users of this class (worker, etc.) to send notifications
via RPC about changes in the bot status.
"""
self.rpc.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': msg
})
def cleanup(self) -> None: def cleanup(self) -> None:
""" """
Cleanup pending resources on an already stopped bot Cleanup pending resources on an already stopped bot
@ -255,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
@ -417,17 +427,24 @@ 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
return False else:
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
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:
""" """
@ -522,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.
""" """
@ -610,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)
@ -619,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):
@ -641,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:
@ -679,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
@ -699,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
@ -718,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
@ -726,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:
@ -741,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}.")
@ -973,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.
""" """
@ -1014,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
""" """

View File

@ -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
@ -63,7 +64,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,
@ -133,11 +134,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

View File

@ -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,
@ -25,7 +26,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__)
@ -150,7 +151,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.
@ -177,7 +178,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
""" """
@ -282,7 +284,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
@ -406,12 +408,12 @@ class Backtesting:
) )
# Execute backtest and print results # Execute backtest and print results
all_results[self.strategy.get_strategy_name()] = self.backtest( all_results[self.strategy.get_strategy_name()] = self.backtest(
processed=preprocessed, processed=preprocessed,
stake_amount=self.config['stake_amount'], stake_amount=self.config['stake_amount'],
start_date=min_date, start_date=min_date,
end_date=max_date, end_date=max_date,
max_open_trades=max_open_trades, max_open_trades=max_open_trades,
position_stacking=position_stacking, position_stacking=position_stacking,
) )
for strategy, results in all_results.items(): for strategy, results in all_results.items():
@ -428,7 +430,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,
@ -438,7 +443,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))

View File

@ -60,6 +60,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
@ -91,13 +92,13 @@ class Hyperopt:
# Populate functions here (hasattr is slow so should not be run during "regular" operations) # Populate functions here (hasattr is slow so should not be run during "regular" operations)
if hasattr(self.custom_hyperopt, 'populate_indicators'): if hasattr(self.custom_hyperopt, 'populate_indicators'):
self.backtesting.strategy.advise_indicators = \ self.backtesting.strategy.advise_indicators = \
self.custom_hyperopt.populate_indicators # type: ignore self.custom_hyperopt.populate_indicators # type: ignore
if hasattr(self.custom_hyperopt, 'populate_buy_trend'): if hasattr(self.custom_hyperopt, 'populate_buy_trend'):
self.backtesting.strategy.advise_buy = \ self.backtesting.strategy.advise_buy = \
self.custom_hyperopt.populate_buy_trend # type: ignore self.custom_hyperopt.populate_buy_trend # type: ignore
if hasattr(self.custom_hyperopt, 'populate_sell_trend'): if hasattr(self.custom_hyperopt, 'populate_sell_trend'):
self.backtesting.strategy.advise_sell = \ self.backtesting.strategy.advise_sell = \
self.custom_hyperopt.populate_sell_trend # type: ignore self.custom_hyperopt.populate_sell_trend # type: ignore
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
if self.config.get('use_max_market_positions', True): if self.config.get('use_max_market_positions', True):
@ -118,11 +119,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.
""" """
@ -159,7 +160,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
""" """
@ -190,7 +191,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
@ -219,7 +220,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']:
@ -236,7 +237,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':
@ -252,7 +253,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:
@ -346,15 +347,15 @@ class Hyperopt:
if self.has_space('roi'): if self.has_space('roi'):
self.backtesting.strategy.minimal_roi = \ self.backtesting.strategy.minimal_roi = \
self.custom_hyperopt.generate_roi_table(params_dict) self.custom_hyperopt.generate_roi_table(params_dict)
if self.has_space('buy'): if self.has_space('buy'):
self.backtesting.strategy.advise_buy = \ self.backtesting.strategy.advise_buy = \
self.custom_hyperopt.buy_strategy_generator(params_dict) self.custom_hyperopt.buy_strategy_generator(params_dict)
if self.has_space('sell'): if self.has_space('sell'):
self.backtesting.strategy.advise_sell = \ self.backtesting.strategy.advise_sell = \
self.custom_hyperopt.sell_strategy_generator(params_dict) self.custom_hyperopt.sell_strategy_generator(params_dict)
if self.has_space('stoploss'): if self.has_space('stoploss'):
self.backtesting.strategy.stoploss = params_dict['stoploss'] self.backtesting.strategy.stoploss = params_dict['stoploss']
@ -373,12 +374,12 @@ class Hyperopt:
min_date, max_date = get_timerange(processed) min_date, max_date = get_timerange(processed)
backtesting_results = self.backtesting.backtest( backtesting_results = self.backtesting.backtest(
processed=processed, processed=processed,
stake_amount=self.config['stake_amount'], stake_amount=self.config['stake_amount'],
start_date=min_date, start_date=min_date,
end_date=max_date, end_date=max_date,
max_open_trades=self.max_open_trades, max_open_trades=self.max_open_trades,
position_stacking=self.position_stacking, position_stacking=self.position_stacking,
) )
return self._get_results_dict(backtesting_results, min_date, max_date, return self._get_results_dict(backtesting_results, min_date, max_date,
params_dict, params_details) params_dict, params_details)
@ -439,7 +440,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
@ -461,7 +462,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
""" """
@ -470,8 +471,8 @@ class Hyperopt:
trials = Hyperopt._read_trials(trials_file) trials = Hyperopt._read_trials(trials_file)
if trials[0].get('is_best') is None: if trials[0].get('is_best') is None:
raise OperationalException( raise OperationalException(
"The file with Hyperopt results is incompatible with this version " "The file with Hyperopt results is incompatible with this version "
"of Freqtrade and cannot be loaded.") "of Freqtrade and cannot be loaded.")
logger.info(f"Loaded {len(trials)} previous evaluations from disk.") logger.info(f"Loaded {len(trials)} previous evaluations from disk.")
return trials return trials

View File

@ -207,7 +207,7 @@ class IHyperOpt(ABC):
# so this intermediate parameter is used as the value of the difference between # so this intermediate parameter is used as the value of the difference between
# them. The value of the 'trailing_stop_positive_offset' is constructed in the # them. The value of the 'trailing_stop_positive_offset' is constructed in the
# generate_trailing_params() method. # generate_trailing_params() method.
# # This is similar to the hyperspace dimensions used for constructing the ROI tables. # This is similar to the hyperspace dimensions used for constructing the ROI tables.
Real(0.001, 0.1, name='trailing_stop_positive_offset_p1'), Real(0.001, 0.1, name='trailing_stop_positive_offset_p1'),
Categorical([True, False], name='trailing_only_offset_is_reached'), Categorical([True, False], name='trailing_only_offset_is_reached'),

View File

@ -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

View 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

View File

@ -19,9 +19,18 @@ 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'] 'Buys',
'Avg Profit %',
'Cum Profit %',
f'Tot Profit {stake_currency}',
'Tot Profit %',
'Avg Duration',
'Wins',
'Draws',
'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():
@ -37,6 +46,7 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra
str(timedelta( str(timedelta(
minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00', minutes=round(result.trade_duration.mean()))) if not result.empty else '0:00',
len(result[result.profit_abs > 0]), len(result[result.profit_abs > 0]),
len(result[result.profit_abs == 0]),
len(result[result.profit_abs < 0]) len(result[result.profit_abs < 0])
]) ])
@ -51,6 +61,7 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra
str(timedelta( str(timedelta(
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00', minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
len(results[results.profit_abs > 0]), len(results[results.profit_abs > 0]),
len(results[results.profit_abs == 0]),
len(results[results.profit_abs < 0]) len(results[results.profit_abs < 0])
]) ])
# Ignore type as floatfmt does allow tuples but mypy does not know that # Ignore type as floatfmt does allow tuples but mypy does not know that
@ -58,7 +69,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 +79,39 @@ 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",
"Sells",
"Wins",
"Draws",
"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]) wins = len(result[result['profit_abs'] > 0])
draws = 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,
wins,
draws,
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 +127,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', 'Buys', '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', 'Draws', 'Losses']
for strategy, results in all_results.items(): for strategy, results in all_results.items():
tabular_data.append([ tabular_data.append([
strategy, strategy,
@ -102,6 +141,7 @@ def generate_text_table_strategy(stake_currency: str, max_open_trades: str,
str(timedelta( str(timedelta(
minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00', minutes=round(results.trade_duration.mean()))) if not results.empty else '0:00',
len(results[results.profit_abs > 0]), len(results[results.profit_abs > 0]),
len(results[results.profit_abs == 0]),
len(results[results.profit_abs < 0]) len(results[results.profit_abs < 0])
]) ])
# Ignore type as floatfmt does allow tuples but mypy does not know that # Ignore type as floatfmt does allow tuples but mypy does not know that
@ -113,9 +153,9 @@ def generate_edge_table(results: dict) -> str:
floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', '.d') floatfmt = ('s', '.10g', '.2f', '.2f', '.2f', '.2f', 'd', '.d')
tabular_data = [] tabular_data = []
headers = ['pair', 'stoploss', 'win rate', 'risk reward ratio', headers = ['Pair', 'Stoploss', 'Win Rate', 'Risk Reward Ratio',
'required risk reward', 'expectancy', 'total number of trades', 'Required Risk Reward', 'Expectancy', 'Total Number of Trades',
'average duration (min)'] 'Average Duration (min)']
for result in results.items(): for result in results.items():
if result[1].nb_trades > 0: if result[1].nb_trades > 0:

View File

@ -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

View File

@ -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)

View File

@ -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)

View 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

View File

@ -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

View File

@ -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

View File

@ -374,7 +374,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
""" """

View File

@ -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]
@ -61,7 +61,8 @@ class IResolver:
valid_objects_gen = ( valid_objects_gen = (
obj for name, obj in inspect.getmembers(module, inspect.isclass) obj for name, obj in inspect.getmembers(module, inspect.isclass)
if (object_name is None or object_name == name) and cls.object_type in obj.__bases__ if ((object_name is None or object_name == name) and
issubclass(obj, cls.object_type) and obj is not cls.object_type)
) )
return valid_objects_gen return valid_objects_gen

View File

@ -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:

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -230,7 +230,7 @@ class AdvancedSampleHyperOpt(IHyperOpt):
'stoploss' optimization hyperspace. 'stoploss' optimization hyperspace.
""" """
return [ return [
Real(-0.5, -0.02, name='stoploss'), Real(-0.35, -0.02, name='stoploss'),
] ]
@staticmethod @staticmethod
@ -249,8 +249,15 @@ class AdvancedSampleHyperOpt(IHyperOpt):
# other 'trailing' hyperspace parameters. # other 'trailing' hyperspace parameters.
Categorical([True], name='trailing_stop'), Categorical([True], name='trailing_stop'),
Real(0.02, 0.35, name='trailing_stop_positive'), Real(0.01, 0.35, name='trailing_stop_positive'),
Real(0.01, 0.1, name='trailing_stop_positive_offset'),
# 'trailing_stop_positive_offset' should be greater than 'trailing_stop_positive',
# so this intermediate parameter is used as the value of the difference between
# them. The value of the 'trailing_stop_positive_offset' is constructed in the
# generate_trailing_params() method.
# This is similar to the hyperspace dimensions used for constructing the ROI tables.
Real(0.001, 0.1, name='trailing_stop_positive_offset_p1'),
Categorical([True, False], name='trailing_only_offset_is_reached'), Categorical([True, False], name='trailing_only_offset_is_reached'),
] ]

View File

@ -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()
# --------------------------------------------- # ---------------------------------------------

View File

@ -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:

View File

@ -12,7 +12,6 @@ from freqtrade import __version__, constants
from freqtrade.configuration import Configuration from freqtrade.configuration import Configuration
from freqtrade.exceptions import OperationalException, TemporaryError from freqtrade.exceptions import OperationalException, TemporaryError
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.rpc import RPCMessageType
from freqtrade.state import State from freqtrade.state import State
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -23,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
""" """
@ -57,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:
@ -84,10 +75,8 @@ class Worker:
# Log state transition # Log state transition
if state != old_state: if state != old_state:
self.freqtrade.rpc.send_msg({ self.freqtrade.notify_status(f'{state.name.lower()}')
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': f'{state.name.lower()}'
})
logger.info('Changing state to: %s', state.name) logger.info('Changing state to: %s', state.name)
if state == State.RUNNING: if state == State.RUNNING:
self.freqtrade.startup() self.freqtrade.startup()
@ -136,10 +125,9 @@ class Worker:
except OperationalException: except OperationalException:
tb = traceback.format_exc() tb = traceback.format_exc()
hint = 'Issue `/start` if you think it is safe to restart.' hint = 'Issue `/start` if you think it is safe to restart.'
self.freqtrade.rpc.send_msg({
'type': RPCMessageType.STATUS_NOTIFICATION, self.freqtrade.notify_status(f'OperationalException:\n```\n{tb}```{hint}')
'status': f'OperationalException:\n```\n{tb}```{hint}'
})
logger.exception('OperationalException. Stopping trader ...') logger.exception('OperationalException. Stopping trader ...')
self.freqtrade.state = State.STOPPED self.freqtrade.state = State.STOPPED
@ -159,10 +147,7 @@ class Worker:
# Load and validate config and create new instance of the bot # Load and validate config and create new instance of the bot
self._init(True) self._init(True)
self.freqtrade.rpc.send_msg({ self.freqtrade.notify_status('config reloaded')
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': 'config reloaded'
})
# Tell systemd that we completed reconfiguration # Tell systemd that we completed reconfiguration
if self._sd_notify: if self._sd_notify:
@ -176,8 +161,5 @@ class Worker:
self._sd_notify.notify("STOPPING=1") self._sd_notify.notify("STOPPING=1")
if self.freqtrade: if self.freqtrade:
self.freqtrade.rpc.send_msg({ self.freqtrade.notify_status('process died')
'type': RPCMessageType.STATUS_NOTIFICATION,
'status': 'process died'
})
self.freqtrade.cleanup() self.freqtrade.cleanup()

View File

@ -1,18 +1,18 @@
# 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.76 ccxt==1.22.30
SQLAlchemy==1.3.12 SQLAlchemy==1.3.13
python-telegram-bot==12.3.0 python-telegram-bot==12.3.0
arrow==0.15.5 arrow==0.15.5
cachetools==4.0.0 cachetools==4.0.0
requests==2.22.0 requests==2.22.0
urllib3==1.25.7 urllib3==1.25.8
wrapt==1.11.2 wrapt==1.11.2
jsonschema==3.2.0 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

View File

@ -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.3 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

View File

@ -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

View File

@ -1,5 +1,5 @@
# Include all requirements to run the bot. # Include all requirements to run the bot.
-r requirements.txt -r requirements.txt
plotly==4.4.1 plotly==4.5.0

View File

@ -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

View File

@ -7,10 +7,10 @@ import pytest
from freqtrade.commands import (start_convert_data, start_create_userdir, from freqtrade.commands import (start_convert_data, start_create_userdir,
start_download_data, start_hyperopt_list, start_download_data, start_hyperopt_list,
start_hyperopt_show, start_list_exchanges, start_hyperopt_show, start_list_exchanges,
start_list_markets, start_list_strategies, start_list_hyperopts, start_list_markets,
start_list_timeframes, start_new_hyperopt, start_list_strategies, start_list_timeframes,
start_new_strategy, start_test_pairlist, start_new_hyperopt, start_new_strategy,
start_trading) start_test_pairlist, start_trading)
from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration import setup_utils_configuration
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.state import RunMode from freqtrade.state import RunMode
@ -666,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',

View File

@ -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": {}
},
}) })

View File

@ -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):

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -15,20 +15,21 @@ def test_generate_text_table(default_conf, mocker):
'profit_percent': [0.1, 0.2], 'profit_percent': [0.1, 0.2],
'profit_abs': [0.2, 0.4], 'profit_abs': [0.2, 0.4],
'trade_duration': [10, 30], 'trade_duration': [10, 30],
'profit': [2, 0], 'wins': [2, 0],
'loss': [0, 0] 'draws': [0, 0],
'losses': [0, 0]
} }
) )
result_str = ( result_str = (
'| pair | buy count | avg profit % | cum profit % | ' '| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC |'
'tot profit BTC | tot profit % | avg duration | profit | loss |\n' ' Tot Profit % | Avg Duration | Wins | Draws | 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 | 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 | 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,
@ -43,56 +44,65 @@ def test_generate_text_table_sell_reason(default_conf, mocker):
'profit_percent': [0.1, 0.2, -0.1], 'profit_percent': [0.1, 0.2, -0.1],
'profit_abs': [0.2, 0.4, -0.2], 'profit_abs': [0.2, 0.4, -0.2],
'trade_duration': [10, 30, 10], 'trade_duration': [10, 30, 10],
'profit': [2, 0, 0], 'wins': [2, 0, 0],
'loss': [0, 0, 1], 'draws': [0, 0, 0],
'losses': [0, 0, 1],
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
} }
) )
result_str = ( result_str = (
'| Sell Reason | Count | Profit | Loss | Profit % |\n' '| Sell Reason | Sells | Wins | Draws | Losses |'
'|:--------------|--------:|---------:|-------:|-----------:|\n' ' Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % |\n'
'| roi | 2 | 2 | 0 | 15 |\n' '|:--------------|--------:|-------:|--------:|---------:|'
'| stop_loss | 1 | 0 | 1 | -10 |' '---------------:|---------------:|-----------------:|---------------:|\n'
'| roi | 2 | 2 | 0 | 0 |'
' 15 | 30 | 0.6 | 15 |\n'
'| stop_loss | 1 | 0 | 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):
results = {} results = {}
results['ETH/BTC'] = pd.DataFrame( results['TestStrategy1'] = pd.DataFrame(
{ {
'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'], 'pair': ['ETH/BTC', 'ETH/BTC', 'ETH/BTC'],
'profit_percent': [0.1, 0.2, 0.3], 'profit_percent': [0.1, 0.2, 0.3],
'profit_abs': [0.2, 0.4, 0.5], 'profit_abs': [0.2, 0.4, 0.5],
'trade_duration': [10, 30, 10], 'trade_duration': [10, 30, 10],
'profit': [2, 0, 0], 'wins': [2, 0, 0],
'loss': [0, 0, 1], 'draws': [0, 0, 0],
'losses': [0, 0, 1],
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
} }
) )
results['LTC/BTC'] = pd.DataFrame( results['TestStrategy2'] = pd.DataFrame(
{ {
'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'], 'pair': ['LTC/BTC', 'LTC/BTC', 'LTC/BTC'],
'profit_percent': [0.4, 0.2, 0.3], 'profit_percent': [0.4, 0.2, 0.3],
'profit_abs': [0.4, 0.4, 0.5], 'profit_abs': [0.4, 0.4, 0.5],
'trade_duration': [15, 30, 15], 'trade_duration': [15, 30, 15],
'profit': [4, 1, 0], 'wins': [4, 1, 0],
'loss': [0, 0, 1], 'draws': [0, 0, 0],
'losses': [0, 0, 1],
'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS] 'sell_reason': [SellType.ROI, SellType.ROI, SellType.STOP_LOSS]
} }
) )
result_str = ( result_str = (
'| Strategy | buy count | avg profit % | cum profit % ' '| Strategy | Buys | Avg Profit % | Cum Profit % | Tot'
'| tot profit BTC | tot profit % | avg duration | profit | loss |\n' ' Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses |\n'
'|:-----------|------------:|---------------:|---------------:' '|:--------------|-------:|---------------:|---------------:|------'
'|-----------------:|---------------:|:---------------|---------:|-------:|\n' '-----------:|---------------:|:---------------|-------:|--------:|---------:|\n'
'| ETH/BTC | 3 | 20.00 | 60.00 ' '| TestStrategy1 | 3 | 20.00 | 60.00 | '
'| 1.10000000 | 30.00 | 0:17:00 | 3 | 0 |\n' ' 1.10000000 | 30.00 | 0:17:00 | 3 | 0 | 0 |\n'
'| LTC/BTC | 3 | 30.00 | 90.00 ' '| TestStrategy2 | 3 | 30.00 | 90.00 | '
'| 1.30000000 | 45.00 | 0:20:00 | 3 | 0 |' ' 1.30000000 | 45.00 | 0:20:00 | 3 | 0 | 0 |'
) )
assert generate_text_table_strategy('BTC', 2, all_results=results) == result_str assert generate_text_table_strategy('BTC', 2, all_results=results) == result_str
@ -105,4 +115,4 @@ def test_generate_edge_table(edge_conf, mocker):
assert generate_edge_table(results).count(':|') == 7 assert generate_edge_table(results).count(':|') == 7
assert generate_edge_table(results).count('| ETH/BTC |') == 1 assert generate_edge_table(results).count('| ETH/BTC |') == 1
assert generate_edge_table(results).count( assert generate_edge_table(results).count(
'| risk reward ratio | required risk reward | expectancy |') == 1 '| Risk Reward Ratio | Required Risk Reward | Expectancy |') == 1

View File

@ -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,

View File

@ -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):

View File

@ -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,

View File

@ -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)