Merge branch 'develop' into no-percent-1

This commit is contained in:
hroff-1902 2020-03-05 14:27:12 +03:00 committed by GitHub
commit 34093d1208
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 791 additions and 297 deletions

View File

@ -115,7 +115,7 @@ jobs:
strategy: strategy:
matrix: matrix:
os: [ windows-latest ] os: [ windows-latest ]
python-version: [3.7] python-version: [3.7, 3.8]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -130,8 +130,7 @@ jobs:
if: startsWith(runner.os, 'Windows') if: startsWith(runner.os, 'Windows')
with: with:
path: ~\AppData\Local\pip\Cache path: ~\AppData\Local\pip\Cache
key: ${{ runner.os }}-pip key: ${{ matrix.os }}-${{ matrix.python-version }}-pip
restore-keys: ${{ runner.os }}-pip
- name: Installation - name: Installation
run: | run: |

View File

@ -25,7 +25,8 @@ hesitate to read the source code and understand the mechanism of this bot.
## Exchange marketplaces supported ## Exchange marketplaces supported
- [X] [Bittrex](https://bittrex.com/) - [X] [Bittrex](https://bittrex.com/)
- [X] [Binance](https://www.binance.com/) ([*Note for binance users](#a-note-on-binance)) - [X] [Binance](https://www.binance.com/) ([*Note for binance users](docs/exchanges.md#blacklists))
- [X] [Kraken](https://kraken.com/)
- [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_ - [ ] [113 others to tests](https://github.com/ccxt/ccxt/). _(We cannot guarantee they will work)_
## Documentation ## Documentation

Binary file not shown.

View File

@ -3,7 +3,15 @@
# Invoke-WebRequest -Uri "https://download.lfd.uci.edu/pythonlibs/xxxxxxx/TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl" -OutFile "TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl" # Invoke-WebRequest -Uri "https://download.lfd.uci.edu/pythonlibs/xxxxxxx/TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl" -OutFile "TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl"
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install build_helpers\TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl
$pyv = python -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
if ($pyv -eq '3.7') {
pip install build_helpers\TA_Lib-0.4.17-cp37-cp37m-win_amd64.whl
}
if ($pyv -eq '3.8') {
pip install build_helpers\TA_Lib-0.4.17-cp38-cp38-win_amd64.whl
}
pip install -r requirements-dev.txt pip install -r requirements-dev.txt
pip install -e . pip install -e .

View File

@ -25,6 +25,7 @@
"sell": 30 "sell": 30
}, },
"bid_strategy": { "bid_strategy": {
"price_side": "bid",
"use_order_book": false, "use_order_book": false,
"ask_last_balance": 0.0, "ask_last_balance": 0.0,
"order_book_top": 1, "order_book_top": 1,
@ -34,6 +35,7 @@
} }
}, },
"ask_strategy":{ "ask_strategy":{
"price_side": "ask",
"use_order_book": false, "use_order_book": false,
"order_book_min": 1, "order_book_min": 1,
"order_book_max": 9, "order_book_max": 9,

View File

@ -275,7 +275,7 @@ Check the corresponding [Data Downloading](data-download.md) section for more de
## Hyperopt commands ## Hyperopt commands
To optimize your strategy, you can use hyperopt parameter hyperoptimization To optimize your strategy, you can use hyperopt parameter hyperoptimization
to find optimal parameter values for your stategy. to find optimal parameter values for your strategy.
``` ```
usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] usage: freqtrade hyperopt [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
@ -323,7 +323,7 @@ optional arguments:
--print-all Print all results, not only the best ones. --print-all Print all results, not only the best ones.
--no-color Disable colorization of hyperopt results. May be --no-color Disable colorization of hyperopt results. May be
useful if you are redirecting output to a file. useful if you are redirecting output to a file.
--print-json Print best result detailization in JSON format. --print-json Print best results in JSON format.
-j JOBS, --job-workers JOBS -j JOBS, --job-workers JOBS
The number of concurrently running jobs for The number of concurrently running jobs for
hyperoptimization (hyperopt worker processes). If -1 hyperoptimization (hyperopt worker processes). If -1
@ -341,10 +341,11 @@ optional arguments:
class (IHyperOptLoss). Different functions can class (IHyperOptLoss). Different functions can
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:
OnlyProfitHyperOptLoss, SharpeHyperOptLoss, DefaultHyperOptLoss, OnlyProfitHyperOptLoss,
SharpeHyperOptLossDaily.(default: SharpeHyperOptLoss, SharpeHyperOptLossDaily,
`DefaultHyperOptLoss`). SortinoHyperOptLoss, SortinoHyperOptLossDaily.
(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

@ -60,11 +60,13 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean | `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer | `unfilledtimeout.buy` | **Required.** How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer | `unfilledtimeout.sell` | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#buy-price-without-orderbook). | `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).<br> *Defaults to `bid`.* <br> **Datatype:** String (either `ask` or `bid`).
| `bid_strategy.ask_last_balance` | **Required.** Set the bidding price. More information [below](#buy-price-without-orderbook-enabled).
| `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled). <br> **Datatype:** Boolean | `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled). <br> **Datatype:** Boolean
| `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids to buy. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in [Order Book Bids](#buy-price-with-orderbook-enabled). <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer | `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book Bids to buy. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in [Order Book Bids](#buy-price-with-orderbook-enabled). <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
| `bid_strategy. check_depth_of_market.enabled` | Do not buy if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market). <br>*Defaults to `false`.* <br> **Datatype:** Boolean | `bid_strategy. check_depth_of_market.enabled` | Do not buy if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market) <br> *Defaults to `0`.* <br> **Datatype:** Float (as ratio) | `bid_strategy. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market) <br> *Defaults to `0`.* <br> **Datatype:** Float (as ratio)
| `ask_strategy.price_side` | Select the side of the spread the bot should look at to get the sell rate. [More information below](#sell-price-side).<br> *Defaults to `ask`.* <br> **Datatype:** String (either `ask` or `bid`).
| `ask_strategy.use_order_book` | Enable selling of open trades using [Order Book Asks](#sell-price-with-orderbook-enabled). <br> **Datatype:** Boolean | `ask_strategy.use_order_book` | Enable selling of open trades using [Order Book Asks](#sell-price-with-orderbook-enabled). <br> **Datatype:** Boolean
| `ask_strategy.order_book_min` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer | `ask_strategy.order_book_min` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
| `ask_strategy.order_book_max` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer | `ask_strategy.order_book_max` | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
@ -370,16 +372,18 @@ The possible values are: `gtc` (default), `fok` or `ioc`.
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports over 100 cryptocurrency Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports over 100 cryptocurrency
exchange markets and trading APIs. The complete up-to-date list can be found in the exchange markets and trading APIs. The complete up-to-date list can be found in the
[CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python). However, the bot was tested [CCXT repo homepage](https://github.com/ccxt/ccxt/tree/master/python).
with only Bittrex and Binance. However, the bot was tested by the development team with only Bittrex, Binance and Kraken,
so the these are the only officially supported exhanges:
The bot was tested with the following exchanges:
- [Bittrex](https://bittrex.com/): "bittrex" - [Bittrex](https://bittrex.com/): "bittrex"
- [Binance](https://www.binance.com/): "binance" - [Binance](https://www.binance.com/): "binance"
- [Kraken](https://kraken.com/): "kraken"
Feel free to test other exchanges and submit your PR to improve the bot. Feel free to test other exchanges and submit your PR to improve the bot.
Some exchanges require special configuration, which can be found on the [Exchange-specific Notes](exchanges.md) documentation page.
#### Sample exchange configuration #### Sample exchange configuration
A exchange configuration for "binance" would look as follows: A exchange configuration for "binance" would look as follows:
@ -461,23 +465,72 @@ Orderbook `bid` (buy) side depth is then divided by the orderbook `ask` (sell) s
!!! Note !!! Note
A delta value below 1 means that `ask` (sell) orderbook side depth is greater than the depth of the `bid` (buy) orderbook side, while a value greater than 1 means opposite (depth of the buy side is higher than the depth of the sell side). A delta value below 1 means that `ask` (sell) orderbook side depth is greater than the depth of the `bid` (buy) orderbook side, while a value greater than 1 means opposite (depth of the buy side is higher than the depth of the sell side).
#### Buy price side
The configuration setting `bid_strategy.price_side` defines the side of the spread the bot looks for when buying.
The following displays an orderbook.
``` explanation
...
103
102
101 # ask
-------------Current spread
99 # bid
98
97
...
```
If `bid_strategy.price_side` is set to `"bid"`, then the bot will use 99 as buying price.
In line with that, if `bid_strategy.price_side` is set to `"ask"`, then the bot will use 101 as buying price.
Using `ask` price often guarantees quicker filled orders, but the bot can also end up paying more than what would have been necessary.
Taker fees instead of maker fees will most likely apply even when using limit buy orders.
Also, prices at the "ask" side of the spread are higher than prices at the "bid" side in the orderbook, so the order behaves similar to a market order (however with a maximum price).
#### Buy price with Orderbook enabled #### Buy price with Orderbook enabled
When buying with the orderbook enabled (`bid_strategy.use_order_book=True`), Freqtrade fetches the `bid_strategy.order_book_top` entries from the orderbook and then uses the entry specified as `bid_strategy.order_book_top` on the `bid` (buy) side of the orderbook. 1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on. When buying with the orderbook enabled (`bid_strategy.use_order_book=True`), Freqtrade fetches the `bid_strategy.order_book_top` entries from the orderbook and then uses the entry specified as `bid_strategy.order_book_top` on the configured side (`bid_strategy.price_side`) of the orderbook. 1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on.
#### Buy price without Orderbook enabled #### Buy price without Orderbook enabled
When not using orderbook (`bid_strategy.use_order_book=False`), Freqtrade uses the best `ask` (sell) price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `ask` price is not below the `last` price), it calculates a rate between `ask` and `last` price. The following section uses `side` as the configured `bid_strategy.price_side`.
The `bid_strategy.ask_last_balance` configuration parameter controls this. A value of `0.0` will use `ask` price, while `1.0` will use the `last` price and values between those interpolate between ask and last price. When not using orderbook (`bid_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price.
Using `ask` price often guarantees quicker success in the bid, but the bot can also end up paying more than what would have been necessary. The `bid_strategy.ask_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the `last` price and values between those interpolate between ask and last price.
### Sell price ### Sell price
#### Sell price side
The configuration setting `ask_strategy.price_side` defines the side of the spread the bot looks for when selling.
The following displays an orderbook:
``` explanation
...
103
102
101 # ask
-------------Current spread
99 # bid
98
97
...
```
If `ask_strategy.price_side` is set to `"ask"`, then the bot will use 101 as selling price.
In line with that, if `ask_strategy.price_side` is set to `"bid"`, then the bot will use 99 as selling price.
#### Sell price with Orderbook enabled #### Sell price with Orderbook enabled
When selling with the orderbook enabled (`ask_strategy.use_order_book=True`), Freqtrade fetches the `ask_strategy.order_book_max` entries in the orderbook. Then each of the orderbook steps between `ask_strategy.order_book_min` and `ask_strategy.order_book_max` on the `ask` orderbook side are validated for a profitable sell-possibility based on the strategy configuration and the sell order is placed at the first profitable spot. When selling with the orderbook enabled (`ask_strategy.use_order_book=True`), Freqtrade fetches the `ask_strategy.order_book_max` entries in the orderbook. Then each of the orderbook steps between `ask_strategy.order_book_min` and `ask_strategy.order_book_max` on the configured orderbook side are validated for a profitable sell-possibility based on the strategy configuration (`minimal_roi` conditions) and the sell order is placed at the first profitable spot.
!!! Note
Using `order_book_max` higher than `order_book_min` only makes sense when ask_strategy.price_side is set to `"ask"`.
The idea here is to place the sell order early, to be ahead in the queue. The idea here is to place the sell order early, to be ahead in the queue.
@ -488,7 +541,7 @@ A fixed slot (mirroring `bid_strategy.order_book_top`) can be defined by setting
#### Sell price without Orderbook enabled #### Sell price without Orderbook enabled
When not using orderbook (`ask_strategy.use_order_book=False`), the `bid` price from the ticker will be used as the sell price. When not using orderbook (`ask_strategy.use_order_book=False`), the price at the `ask_strategy.price_side` side (defaults to `"ask"`) from the ticker will be used as the sell price.
## Pairlists ## Pairlists

View File

@ -234,7 +234,7 @@ git checkout -b new_release <commitid>
Determine if crucial bugfixes have been made between this commit and the current state, and eventually cherry-pick these. Determine if crucial bugfixes have been made between this commit and the current state, and eventually cherry-pick these.
* Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for July 2019). Minor versions can be `2019.7-1` should we need to do a second release that month. * Edit `freqtrade/__init__.py` and add the version matching the current date (for example `2019.7` for July 2019). Minor versions can be `2019.7.1` should we need to do a second release that month. Version numbers must follow allowed versions from PEP0440 to avoid failures pushing to pypi.
* Commit this part * Commit this part
* push that branch to the remote and create a PR against the master branch * push that branch to the remote and create a PR against the master branch
@ -268,11 +268,6 @@ Once the PR against master is merged (best right after merging):
* Use "master" as reference (this step comes after the above PR is merged). * Use "master" as reference (this step comes after the above PR is merged).
* Use the above changelog as release comment (as codeblock) * Use the above changelog as release comment (as codeblock)
### After-release
* Update version in develop by postfixing that with `-dev` (`2019.6 -> 2019.6-dev`).
* Create a PR against develop to update that branch.
## Releases ## Releases
### pypi ### pypi

View File

@ -62,6 +62,11 @@ res = [ f"{x['MarketCurrency']}/{x['BaseCurrency']}" for x in ct.publicGetMarket
print(res) print(res)
``` ```
## All exchanges
Should you experience constant errors with Nonce (like `InvalidNonce`), it is best to regenerate the API keys. Resetting Nonce is difficult and it's usually easier to regenerate the API keys.
## Random notes for other exchanges ## Random notes for other exchanges
* The Ocean (exchange id: `theocean`) exchange uses Web3 functionality and requires `web3` python package to be installed: * The Ocean (exchange id: `theocean`) exchange uses Web3 functionality and requires `web3` python package to be installed:

View File

@ -31,9 +31,9 @@ This will create a new hyperopt file from a template, which will be located unde
Depending on the space you want to optimize, only some of the below are required: Depending on the space you want to optimize, only some of the below are required:
* fill `buy_strategy_generator` - for buy signal optimization * fill `buy_strategy_generator` - for buy signal optimization
* fill `indicator_space` - for buy signal optimzation * fill `indicator_space` - for buy signal optimization
* fill `sell_strategy_generator` - for sell signal optimization * fill `sell_strategy_generator` - for sell signal optimization
* fill `sell_indicator_space` - for sell signal optimzation * fill `sell_indicator_space` - for sell signal optimization
!!! Note !!! Note
`populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work. `populate_indicators` needs to create all indicators any of thee spaces may use, otherwise hyperopt will not work.
@ -81,11 +81,11 @@ There are two places you need to change in your hyperopt file to add a new buy h
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`.
1. Guards are conditions like "never buy if ADX < 10", or never buy if current price is over EMA10. 1. Guards are conditions like "never buy if ADX < 10", or never buy if current price is over EMA10.
2. Triggers are ones that actually trigger buy in specific moment, like "buy when EMA5 crosses over EMA10" or "buy when close price touches lower bollinger band". 2. Triggers are ones that actually trigger buy in specific moment, like "buy when EMA5 crosses over EMA10" or "buy when close price touches lower Bollinger band".
Hyperoptimization will, for each eval round, pick one trigger and possibly Hyperoptimization will, for each eval round, pick one trigger and possibly
multiple guards. The constructed strategy will be something like multiple guards. The constructed strategy will be something like
"*buy exactly when close price touches lower bollinger band, BUT only if "*buy exactly when close price touches lower Bollinger band, BUT only if
ADX > 10*". ADX > 10*".
If you have updated the buy strategy, i.e. changed the contents of If you have updated the buy strategy, i.e. changed the contents of
@ -172,7 +172,7 @@ So let's write the buy strategy using these values:
Hyperopting will now call this `populate_buy_trend` as many times you ask it (`epochs`) Hyperopting will now call this `populate_buy_trend` as many times you ask it (`epochs`)
with different value combinations. It will then use the given historical data and make with different value combinations. It will then use the given historical data and make
buys based on the buy signals generated with the above function and based on the results buys based on the buy signals generated with the above function and based on the results
it will end with telling you which paramter combination produced the best profits. it will end with telling you which parameter combination produced the best profits.
The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators. The above setup expects to find ADX, RSI and Bollinger Bands in the populated indicators.
When you want to test an indicator that isn't used by the bot currently, remember to When you want to test an indicator that isn't used by the bot currently, remember to
@ -191,8 +191,10 @@ 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 trade returns relative to standard deviation)
* `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on daily trade returns) * `SharpeHyperOptLossDaily` (optimizes Sharpe Ratio calculated on **daily** trade returns relative to standard deviation)
* `SortinoHyperOptLoss` (optimizes Sortino Ratio calculated on trade returns relative to **downside** standard deviation)
* `SortinoHyperOptLossDaily` (optimizes Sortino Ratio calculated on **daily** trade returns relative to **downside** standard deviation)
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.
@ -272,7 +274,7 @@ In some situations, you may need to run Hyperopt (and Backtesting) with the
By default, hyperopt emulates the behavior of the Freqtrade Live Run/Dry Run, where only one By default, hyperopt emulates the behavior of the Freqtrade Live Run/Dry Run, where only one
open trade is allowed for every traded pair. The total number of trades open for all pairs open trade is allowed for every traded pair. The total number of trades open for all pairs
is also limited by the `max_open_trades` setting. During Hyperopt/Backtesting this may lead to is also limited by the `max_open_trades` setting. During Hyperopt/Backtesting this may lead to
some potential trades to be hidden (or masked) by previosly open trades. some potential trades to be hidden (or masked) by previously open trades.
The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times, The `--eps`/`--enable-position-stacking` argument allows emulation of buying the same pair multiple times,
while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades` while `--dmmp`/`--disable-max-market-positions` disables applying `max_open_trades`

View File

@ -196,6 +196,7 @@ The first graph is good to get a grip of how the overall market progresses.
The second graph will show if your algorithm works or doesn't. The second graph will show if your algorithm works or doesn't.
Perhaps you want an algorithm that steadily makes small profits, or one that acts less often, but makes big swings. Perhaps you want an algorithm that steadily makes small profits, or one that acts less often, but makes big swings.
This graph will also highlight the start (and end) of the Max drawdown period.
The third graph can be useful to spot outliers, events in pairs that cause profit spikes. The third graph can be useful to spot outliers, events in pairs that cause profit spikes.

View File

@ -121,7 +121,6 @@ from freqtrade.data.btanalysis import analyze_trade_parallelism
# Analyze the above # Analyze the above
parallel_trades = analyze_trade_parallelism(trades, '5m') parallel_trades = analyze_trade_parallelism(trades, '5m')
parallel_trades.plot() parallel_trades.plot()
``` ```
@ -134,11 +133,14 @@ Freqtrade offers interactive plotting capabilities based on plotly.
from freqtrade.plot.plotting import generate_candlestick_graph from freqtrade.plot.plotting import generate_candlestick_graph
# Limit graph period to keep plotly quick and reactive # Limit graph period to keep plotly quick and reactive
# Filter trades to one pair
trades_red = trades.loc[trades['pair'] == pair]
data_red = data['2019-06-01':'2019-06-10'] data_red = data['2019-06-01':'2019-06-10']
# Generate candlestick graph # Generate candlestick graph
graph = generate_candlestick_graph(pair=pair, graph = generate_candlestick_graph(pair=pair,
data=data_red, data=data_red,
trades=trades, trades=trades_red,
indicators1=['sma20', 'ema50', 'ema55'], indicators1=['sma20', 'ema50', 'ema55'],
indicators2=['rsi', 'macd', 'macdsignal', 'macdhist'] indicators2=['rsi', 'macd', 'macdsignal', 'macdhist']
) )

View File

@ -257,7 +257,8 @@ 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, SharpeHyperOptLossDaily.' 'DefaultHyperOptLoss, OnlyProfitHyperOptLoss, SharpeHyperOptLoss, SharpeHyperOptLossDaily, '
'SortinoHyperOptLoss, SortinoHyperOptLossDaily.'
'(default: `%(default)s`).', '(default: `%(default)s`).',
metavar='NAME', metavar='NAME',
default=constants.DEFAULT_HYPEROPT_LOSS, default=constants.DEFAULT_HYPEROPT_LOSS,

View File

@ -51,7 +51,7 @@ def start_hyperopt_list(args: Dict[str, Any]) -> None:
try: try:
Hyperopt.print_result_table(config, trials, total_epochs, Hyperopt.print_result_table(config, trials, total_epochs,
not filteroptions['only_best'], print_colorized) not filteroptions['only_best'], print_colorized, 0)
except KeyboardInterrupt: except KeyboardInterrupt:
print('User interrupted..') print('User interrupted..')
@ -97,10 +97,10 @@ def start_hyperopt_show(args: Dict[str, Any]) -> None:
if n > trials_epochs: if n > trials_epochs:
raise OperationalException( raise OperationalException(
f"The index of the epoch to show should be less than {trials_epochs + 1}.") f"The index of the epoch to show should be less than {trials_epochs + 1}.")
if n < -trials_epochs: if n < -trials_epochs:
raise OperationalException( raise OperationalException(
f"The index of the epoch to show should be greater than {-trials_epochs - 1}.") f"The index of the epoch to show should be greater than {-trials_epochs - 1}.")
# Translate epoch index from human-readable format to pythonic # Translate epoch index from human-readable format to pythonic
if n > 0: if n > 0:
@ -122,52 +122,52 @@ def _hyperopt_filter_trials(trials: List, filteroptions: dict) -> List:
trials = [x for x in trials if x['results_metrics']['profit'] > 0] trials = [x for x in trials if x['results_metrics']['profit'] > 0]
if filteroptions['filter_min_trades'] > 0: if filteroptions['filter_min_trades'] > 0:
trials = [ trials = [
x for x in trials x for x in trials
if x['results_metrics']['trade_count'] > filteroptions['filter_min_trades'] if x['results_metrics']['trade_count'] > filteroptions['filter_min_trades']
] ]
if filteroptions['filter_max_trades'] > 0: if filteroptions['filter_max_trades'] > 0:
trials = [ trials = [
x for x in trials x for x in trials
if x['results_metrics']['trade_count'] < filteroptions['filter_max_trades'] if x['results_metrics']['trade_count'] < filteroptions['filter_max_trades']
] ]
if filteroptions['filter_min_avg_time'] is not None: if filteroptions['filter_min_avg_time'] is not None:
trials = [x for x in trials if x['results_metrics']['trade_count'] > 0] trials = [x for x in trials if x['results_metrics']['trade_count'] > 0]
trials = [ trials = [
x for x in trials x for x in trials
if x['results_metrics']['duration'] > filteroptions['filter_min_avg_time'] if x['results_metrics']['duration'] > filteroptions['filter_min_avg_time']
] ]
if filteroptions['filter_max_avg_time'] is not None: if filteroptions['filter_max_avg_time'] is not None:
trials = [x for x in trials if x['results_metrics']['trade_count'] > 0] trials = [x for x in trials if x['results_metrics']['trade_count'] > 0]
trials = [ trials = [
x for x in trials x for x in trials
if x['results_metrics']['duration'] < filteroptions['filter_max_avg_time'] if x['results_metrics']['duration'] < filteroptions['filter_max_avg_time']
] ]
if filteroptions['filter_min_avg_profit'] is not None: if filteroptions['filter_min_avg_profit'] is not None:
trials = [x for x in trials if x['results_metrics']['trade_count'] > 0] trials = [x for x in trials if x['results_metrics']['trade_count'] > 0]
trials = [ trials = [
x for x in trials x for x in trials
if x['results_metrics']['avg_profit'] if x['results_metrics']['avg_profit']
> filteroptions['filter_min_avg_profit'] > filteroptions['filter_min_avg_profit']
] ]
if filteroptions['filter_max_avg_profit'] is not None: if filteroptions['filter_max_avg_profit'] is not None:
trials = [x for x in trials if x['results_metrics']['trade_count'] > 0] trials = [x for x in trials if x['results_metrics']['trade_count'] > 0]
trials = [ trials = [
x for x in trials x for x in trials
if x['results_metrics']['avg_profit'] if x['results_metrics']['avg_profit']
< filteroptions['filter_max_avg_profit'] < filteroptions['filter_max_avg_profit']
] ]
if filteroptions['filter_min_total_profit'] is not None: if filteroptions['filter_min_total_profit'] is not None:
trials = [x for x in trials if x['results_metrics']['trade_count'] > 0] trials = [x for x in trials if x['results_metrics']['trade_count'] > 0]
trials = [ trials = [
x for x in trials x for x in trials
if x['results_metrics']['profit'] > filteroptions['filter_min_total_profit'] if x['results_metrics']['profit'] > filteroptions['filter_min_total_profit']
] ]
if filteroptions['filter_max_total_profit'] is not None: if filteroptions['filter_max_total_profit'] is not None:
trials = [x for x in trials if x['results_metrics']['trade_count'] > 0] trials = [x for x in trials if x['results_metrics']['trade_count'] > 0]
trials = [ trials = [
x for x in trials x for x in trials
if x['results_metrics']['profit'] < filteroptions['filter_max_total_profit'] if x['results_metrics']['profit'] < filteroptions['filter_max_total_profit']
] ]
logger.info(f"{len(trials)} " + logger.info(f"{len(trials)} " +
("best " if filteroptions['only_best'] else "") + ("best " if filteroptions['only_best'] else "") +

View File

@ -15,6 +15,7 @@ UNLIMITED_STAKE_AMOUNT = 'unlimited'
DEFAULT_AMOUNT_RESERVE_PERCENT = 0.05 DEFAULT_AMOUNT_RESERVE_PERCENT = 0.05
REQUIRED_ORDERTIF = ['buy', 'sell'] REQUIRED_ORDERTIF = ['buy', 'sell']
REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange'] REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
ORDERBOOK_SIDES = ['ask', 'bid']
ORDERTYPE_POSSIBILITIES = ['limit', 'market'] ORDERTYPE_POSSIBILITIES = ['limit', 'market']
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList',
@ -113,15 +114,16 @@ CONF_SCHEMA = {
'minimum': 0, 'minimum': 0,
'maximum': 1, 'maximum': 1,
'exclusiveMaximum': False, 'exclusiveMaximum': False,
'use_order_book': {'type': 'boolean'}, },
'order_book_top': {'type': 'integer', 'maximum': 20, 'minimum': 1}, 'price_side': {'type': 'string', 'enum': ORDERBOOK_SIDES, 'default': 'bid'},
'check_depth_of_market': { 'use_order_book': {'type': 'boolean'},
'type': 'object', 'order_book_top': {'type': 'integer', 'maximum': 20, 'minimum': 1},
'properties': { 'check_depth_of_market': {
'enabled': {'type': 'boolean'}, 'type': 'object',
'bids_to_ask_delta': {'type': 'number', 'minimum': 0}, 'properties': {
} 'enabled': {'type': 'boolean'},
}, 'bids_to_ask_delta': {'type': 'number', 'minimum': 0},
}
}, },
}, },
'required': ['ask_last_balance'] 'required': ['ask_last_balance']
@ -129,6 +131,7 @@ CONF_SCHEMA = {
'ask_strategy': { 'ask_strategy': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {
'price_side': {'type': 'string', 'enum': ORDERBOOK_SIDES, 'default': 'ask'},
'use_order_book': {'type': 'boolean'}, 'use_order_book': {'type': 'boolean'},
'order_book_min': {'type': 'integer', 'minimum': 1}, 'order_book_min': {'type': 'integer', 'minimum': 1},
'order_book_max': {'type': 'integer', 'minimum': 1, 'maximum': 50}, 'order_book_max': {'type': 'integer', 'minimum': 1, 'maximum': 50},
@ -299,6 +302,7 @@ SCHEMA_TRADE_REQUIRED = [
'last_stake_amount_min_ratio', 'last_stake_amount_min_ratio',
'dry_run', 'dry_run',
'dry_run_wallet', 'dry_run_wallet',
'ask_strategy',
'bid_strategy', 'bid_strategy',
'unfilledtimeout', 'unfilledtimeout',
'stoploss', 'stoploss',

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, Union from typing import Dict, Union, Tuple
import numpy as np import numpy as np
import pandas as pd import pandas as pd
@ -188,3 +188,28 @@ def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
# FFill to get continuous # FFill to get continuous
df[col_name] = df[col_name].ffill() df[col_name] = df[col_name].ffill()
return df return df
def calculate_max_drawdown(trades: pd.DataFrame, *, date_col: str = 'close_time',
value_col: str = 'profitperc'
) -> Tuple[float, pd.Timestamp, pd.Timestamp]:
"""
Calculate max drawdown and the corresponding close dates
:param trades: DataFrame containing trades (requires columns close_time and profitperc)
:param date_col: Column in DataFrame to use for dates (defaults to 'close_time')
:param value_col: Column in DataFrame to use for values (defaults to 'profitperc')
:return: Tuple (float, highdate, lowdate) with absolute max drawdown, high and low time
:raise: ValueError if trade-dataframe was found empty.
"""
if len(trades) == 0:
raise ValueError("Trade dataframe empty.")
profit_results = trades.sort_values(date_col)
max_drawdown_df = pd.DataFrame()
max_drawdown_df['cumulative'] = profit_results[value_col].cumsum()
max_drawdown_df['high_value'] = max_drawdown_df['cumulative'].cummax()
max_drawdown_df['drawdown'] = max_drawdown_df['cumulative'] - max_drawdown_df['high_value']
high_date = profit_results.loc[max_drawdown_df['high_value'].idxmax(), date_col]
low_date = profit_results.loc[max_drawdown_df['drawdown'].idxmin(), date_col]
return abs(min(max_drawdown_df['drawdown'])), high_date, low_date

View File

@ -332,7 +332,8 @@ class Exchange:
logger.warning(f"Pair {pair} is restricted for some users on this exchange." logger.warning(f"Pair {pair} is restricted for some users on this 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.")
if not self.get_pair_quote_currency(pair) == self._config['stake_currency']: if (self._config['stake_currency'] and
self.get_pair_quote_currency(pair) != self._config['stake_currency']):
invalid_pairs.append(pair) invalid_pairs.append(pair)
if invalid_pairs: if invalid_pairs:
raise OperationalException( raise OperationalException(
@ -1023,7 +1024,7 @@ def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = Non
def is_exchange_officially_supported(exchange_name: str) -> bool: def is_exchange_officially_supported(exchange_name: str) -> bool:
return exchange_name in ['bittrex', 'binance'] return exchange_name in ['bittrex', 'binance', 'kraken']
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]: def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:

View File

@ -242,25 +242,25 @@ class FreqtradeBot:
logger.info(f"Using cached buy rate for {pair}.") logger.info(f"Using cached buy rate for {pair}.")
return rate return rate
config_bid_strategy = self.config.get('bid_strategy', {}) bid_strategy = self.config.get('bid_strategy', {})
if 'use_order_book' in config_bid_strategy and\ if 'use_order_book' in bid_strategy and bid_strategy.get('use_order_book', False):
config_bid_strategy.get('use_order_book', False): logger.info(
logger.info('Getting price from order book') f"Getting price from order book {bid_strategy['price_side'].capitalize()} side."
order_book_top = config_bid_strategy.get('order_book_top', 1) )
order_book_top = bid_strategy.get('order_book_top', 1)
order_book = self.exchange.get_order_book(pair, order_book_top) order_book = self.exchange.get_order_book(pair, order_book_top)
logger.debug('order_book %s', order_book) logger.debug('order_book %s', order_book)
# top 1 = index 0 # top 1 = index 0
order_book_rate = order_book['bids'][order_book_top - 1][0] order_book_rate = order_book[f"{bid_strategy['price_side']}s"][order_book_top - 1][0]
logger.info('...top %s order book buy rate %0.8f', order_book_top, order_book_rate) logger.info(f'...top {order_book_top} order book buy rate {order_book_rate:.8f}')
used_rate = order_book_rate used_rate = order_book_rate
else: else:
logger.info('Using Last Ask / Last Price') logger.info(f"Using Last {bid_strategy['price_side'].capitalize()} / Last Price")
ticker = self.exchange.fetch_ticker(pair) ticker = self.exchange.fetch_ticker(pair)
if ticker['ask'] < ticker['last']: ticker_rate = ticker[bid_strategy['price_side']]
ticker_rate = ticker['ask'] if ticker['last'] and ticker_rate > ticker['last']:
else:
balance = self.config['bid_strategy']['ask_last_balance'] balance = self.config['bid_strategy']['ask_last_balance']
ticker_rate = ticker['ask'] + balance * (ticker['last'] - ticker['ask']) ticker_rate = ticker_rate + balance * (ticker['last'] - ticker_rate)
used_rate = ticker_rate used_rate = ticker_rate
self._buy_rate_cache[pair] = used_rate self._buy_rate_cache[pair] = used_rate
@ -617,6 +617,15 @@ class FreqtradeBot:
return trades_closed return trades_closed
def _order_book_gen(self, pair: str, side: str, order_book_max: int = 1,
order_book_min: int = 1):
"""
Helper generator to query orderbook in loop (used for early sell-order placing)
"""
order_book = self.exchange.get_order_book(pair, order_book_max)
for i in range(order_book_min, order_book_max + 1):
yield order_book[side][i - 1][0]
def get_sell_rate(self, pair: str, refresh: bool) -> float: def get_sell_rate(self, pair: str, refresh: bool) -> float:
""" """
Get sell rate - either using get-ticker bid or first bid based on orderbook Get sell rate - either using get-ticker bid or first bid based on orderbook
@ -636,13 +645,12 @@ class FreqtradeBot:
config_ask_strategy = self.config.get('ask_strategy', {}) config_ask_strategy = self.config.get('ask_strategy', {})
if config_ask_strategy.get('use_order_book', False): if config_ask_strategy.get('use_order_book', False):
# This code is only used for notifications, selling uses the generator directly
logger.debug('Using order book to get sell rate') logger.debug('Using order book to get sell rate')
rate = next(self._order_book_gen(pair, f"{config_ask_strategy['price_side']}s"))
order_book = self.exchange.get_order_book(pair, 1)
rate = order_book['bids'][0][0]
else: else:
rate = self.exchange.fetch_ticker(pair)['bid'] rate = self.exchange.fetch_ticker(pair)[config_ask_strategy['price_side']]
self._sell_rate_cache[pair] = rate self._sell_rate_cache[pair] = rate
return rate return rate
@ -672,12 +680,13 @@ class FreqtradeBot:
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)
order_book = self.exchange.get_order_book(trade.pair, order_book_max) order_book = self._order_book_gen(trade.pair, f"{config_ask_strategy['price_side']}s",
order_book_min=order_book_min,
order_book_max=order_book_max)
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] sell_rate = next(order_book)
logger.debug(' order book asks top %s: %0.8f', i, order_book_rate) logger.debug(f" order book {config_ask_strategy['price_side']} top {i}: "
sell_rate = order_book_rate f"{sell_rate:0.8f}")
if self._check_and_execute_sell(trade, sell_rate, buy, sell): if self._check_and_execute_sell(trade, sell_rate, buy, sell):
return True return True

View File

@ -423,28 +423,37 @@ class Backtesting:
strategy if len(self.strategylist) > 1 else None) strategy if len(self.strategylist) > 1 else None)
print(f"Result for strategy {strategy}") print(f"Result for strategy {strategy}")
print(' BACKTESTING REPORT '.center(133, '=')) table = generate_text_table(data, stake_currency=self.config['stake_currency'],
print(generate_text_table(data, max_open_trades=self.config['max_open_trades'],
stake_currency=self.config['stake_currency'], results=results)
max_open_trades=self.config['max_open_trades'], if isinstance(table, str):
results=results)) print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
print(table)
print(' SELL REASON STATS '.center(133, '=')) table = generate_text_table_sell_reason(data,
print(generate_text_table_sell_reason(data, stake_currency=self.config['stake_currency'],
stake_currency=self.config['stake_currency'], max_open_trades=self.config['max_open_trades'],
max_open_trades=self.config['max_open_trades'], results=results)
results=results)) if isinstance(table, str):
print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '='))
print(table)
print(' LEFT OPEN TRADES REPORT '.center(133, '=')) table = generate_text_table(data,
print(generate_text_table(data, stake_currency=self.config['stake_currency'],
stake_currency=self.config['stake_currency'], max_open_trades=self.config['max_open_trades'],
max_open_trades=self.config['max_open_trades'], results=results.loc[results.open_at_end], skip_nan=True)
results=results.loc[results.open_at_end], skip_nan=True)) if isinstance(table, str):
print(' LEFT OPEN TRADES REPORT '.center(len(table.splitlines()[0]), '='))
print(table)
if isinstance(table, str):
print('=' * len(table.splitlines()[0]))
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, '=')) table = 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)) print(' STRATEGY SUMMARY '.center(len(table.splitlines()[0]), '='))
print(table)
print('=' * len(table.splitlines()[0]))
print('\nFor more details, please look at the detail tables above') print('\nFor more details, please look at the detail tables above')

View File

@ -9,6 +9,7 @@ import logging
import random import random
import sys import sys
import warnings import warnings
from math import ceil
from collections import OrderedDict from collections import OrderedDict
from operator import itemgetter from operator import itemgetter
from pathlib import Path from pathlib import Path
@ -21,7 +22,7 @@ from colorama import init as colorama_init
from joblib import (Parallel, cpu_count, delayed, dump, load, from joblib import (Parallel, cpu_count, delayed, dump, load,
wrap_non_picklable_objects) wrap_non_picklable_objects)
from pandas import DataFrame, json_normalize, isna from pandas import DataFrame, json_normalize, isna
from tabulate import tabulate import tabulate
from freqtrade.data.converter import trim_dataframe from freqtrade.data.converter import trim_dataframe
from freqtrade.data.history import get_timerange from freqtrade.data.history import get_timerange
@ -116,6 +117,7 @@ class Hyperopt:
self.config['ask_strategy']['use_sell_signal'] = True self.config['ask_strategy']['use_sell_signal'] = True
self.print_all = self.config.get('print_all', False) self.print_all = self.config.get('print_all', False)
self.hyperopt_table_header = 0
self.print_colorized = self.config.get('print_colorized', False) self.print_colorized = self.config.get('print_colorized', False)
self.print_json = self.config.get('print_json', False) self.print_json = self.config.get('print_json', False)
@ -153,7 +155,7 @@ class Hyperopt:
""" """
num_trials = len(self.trials) num_trials = len(self.trials)
if num_trials > self.num_trials_saved: if num_trials > self.num_trials_saved:
logger.info(f"Saving {num_trials} {plural(num_trials, 'epoch')}.") logger.debug(f"Saving {num_trials} {plural(num_trials, 'epoch')}.")
dump(self.trials, self.trials_file) dump(self.trials, self.trials_file)
self.num_trials_saved = num_trials self.num_trials_saved = num_trials
if final: if final:
@ -272,8 +274,10 @@ class Hyperopt:
if not self.print_all: if not self.print_all:
# Separate the results explanation string from dots # Separate the results explanation string from dots
print("\n") print("\n")
self.print_results_explanation(results, self.total_epochs, self.print_all, self.print_result_table(self.config, results, self.total_epochs,
self.print_colorized) self.print_all, self.print_colorized,
self.hyperopt_table_header)
self.hyperopt_table_header = 2
@staticmethod @staticmethod
def print_results_explanation(results, total_epochs, highlight_best: bool, def print_results_explanation(results, total_epochs, highlight_best: bool,
@ -299,13 +303,15 @@ class Hyperopt:
@staticmethod @staticmethod
def print_result_table(config: dict, results: list, total_epochs: int, highlight_best: bool, def print_result_table(config: dict, results: list, total_epochs: int, highlight_best: bool,
print_colorized: bool) -> None: print_colorized: bool, remove_header: int) -> None:
""" """
Log result table Log result table
""" """
if not results: if not results:
return return
tabulate.PRESERVE_WHITESPACE = True
trials = json_normalize(results, max_level=1) trials = json_normalize(results, max_level=1)
trials['Best'] = '' trials['Best'] = ''
trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count', trials = trials[['Best', 'current_epoch', 'results_metrics.trade_count',
@ -317,35 +323,63 @@ class Hyperopt:
trials['is_profit'] = False trials['is_profit'] = False
trials.loc[trials['is_initial_point'], 'Best'] = '*' trials.loc[trials['is_initial_point'], 'Best'] = '*'
trials.loc[trials['is_best'], 'Best'] = 'Best' trials.loc[trials['is_best'], 'Best'] = 'Best'
trials['Objective'] = trials['Objective'].astype(str)
trials.loc[trials['Total profit'] > 0, 'is_profit'] = True trials.loc[trials['Total profit'] > 0, 'is_profit'] = True
trials['Trades'] = trials['Trades'].astype(str) trials['Trades'] = trials['Trades'].astype(str)
trials['Epoch'] = trials['Epoch'].apply( trials['Epoch'] = trials['Epoch'].apply(
lambda x: "{}/{}".format(x, total_epochs)) lambda x: '{}/{}'.format(str(x).rjust(len(str(total_epochs)), ' '), total_epochs)
)
trials['Avg profit'] = trials['Avg profit'].apply( trials['Avg profit'] = trials['Avg profit'].apply(
lambda x: '{:,.2f}%'.format(x) if not isna(x) else x) lambda x: ('{:,.2f}%'.format(x)).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ')
trials['Profit'] = trials['Profit'].apply( )
lambda x: '{:,.2f}%'.format(x) if not isna(x) else x)
trials['Total profit'] = trials['Total profit'].apply(
lambda x: '{: 11.8f} '.format(x) + config['stake_currency'] if not isna(x) else x)
trials['Avg duration'] = trials['Avg duration'].apply( trials['Avg duration'] = trials['Avg duration'].apply(
lambda x: '{:,.1f}m'.format(x) if not isna(x) else x) lambda x: ('{:,.1f} m'.format(x)).rjust(7, ' ') if not isna(x) else "--".rjust(7, ' ')
)
trials['Objective'] = trials['Objective'].apply(
lambda x: '{:,.5f}'.format(x).rjust(8, ' ') if x != 100000 else "N/A".rjust(8, ' ')
)
trials['Profit'] = trials.apply(
lambda x: '{:,.8f} {} {}'.format(
x['Total profit'], config['stake_currency'],
'({:,.2f}%)'.format(x['Profit']).rjust(10, ' ')
).rjust(25+len(config['stake_currency']))
if x['Total profit'] != 0.0 else '--'.rjust(25+len(config['stake_currency'])),
axis=1
)
trials = trials.drop(columns=['Total profit'])
if print_colorized: if print_colorized:
for i in range(len(trials)): for i in range(len(trials)):
if trials.loc[i]['is_profit']: if trials.loc[i]['is_profit']:
for z in range(len(trials.loc[i])-3): for j in range(len(trials.loc[i])-3):
trials.iat[i, z] = "{}{}{}".format(Fore.GREEN, trials.iat[i, j] = "{}{}{}".format(Fore.GREEN,
str(trials.loc[i][z]), Fore.RESET) str(trials.loc[i][j]), Fore.RESET)
if trials.loc[i]['is_best'] and highlight_best: if trials.loc[i]['is_best'] and highlight_best:
for z in range(len(trials.loc[i])-3): for j in range(len(trials.loc[i])-3):
trials.iat[i, z] = "{}{}{}".format(Style.BRIGHT, trials.iat[i, j] = "{}{}{}".format(Style.BRIGHT,
str(trials.loc[i][z]), Style.RESET_ALL) str(trials.loc[i][j]), Style.RESET_ALL)
trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit']) trials = trials.drop(columns=['is_initial_point', 'is_best', 'is_profit'])
if remove_header > 0:
table = tabulate.tabulate(
trials.to_dict(orient='list'), tablefmt='orgtbl',
headers='keys', stralign="right"
)
print(tabulate(trials.to_dict(orient='list'), headers='keys', tablefmt='psql', table = table.split("\n", remove_header)[remove_header]
stralign="right")) elif remove_header < 0:
table = tabulate.tabulate(
trials.to_dict(orient='list'), tablefmt='psql',
headers='keys', stralign="right"
)
table = "\n".join(table.split("\n")[0:remove_header])
else:
table = tabulate.tabulate(
trials.to_dict(orient='list'), tablefmt='psql',
headers='keys', stralign="right"
)
print(table)
def has_space(self, space: str) -> bool: def has_space(self, space: str) -> bool:
""" """
@ -533,7 +567,7 @@ class Hyperopt:
def start(self) -> None: def start(self) -> None:
self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None)) self.random_state = self._set_random_state(self.config.get('hyperopt_random_state', None))
logger.info(f"Using optimizer random state: {self.random_state}") logger.info(f"Using optimizer random state: {self.random_state}")
self.hyperopt_table_header = -1
data, timerange = self.backtesting.load_bt_data() data, timerange = self.backtesting.load_bt_data()
preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data) preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data)
@ -569,16 +603,21 @@ class Hyperopt:
with Parallel(n_jobs=config_jobs) as parallel: with Parallel(n_jobs=config_jobs) as parallel:
jobs = parallel._effective_n_jobs() jobs = parallel._effective_n_jobs()
logger.info(f'Effective number of parallel workers used: {jobs}') logger.info(f'Effective number of parallel workers used: {jobs}')
EVALS = max(self.total_epochs // jobs, 1) EVALS = ceil(self.total_epochs / jobs)
for i in range(EVALS): for i in range(EVALS):
asked = self.opt.ask(n_points=jobs) # Correct the number of epochs to be processed for the last
# iteration (should not exceed self.total_epochs in total)
n_rest = (i + 1) * jobs - self.total_epochs
current_jobs = jobs - n_rest if n_rest > 0 else jobs
asked = self.opt.ask(n_points=current_jobs)
f_val = self.run_optimizer_parallel(parallel, asked, i) f_val = self.run_optimizer_parallel(parallel, asked, i)
self.opt.tell(asked, [v['loss'] for v in f_val]) self.opt.tell(asked, [v['loss'] for v in f_val])
self.fix_optimizer_models_list() self.fix_optimizer_models_list()
for j in range(jobs):
for j, val in enumerate(f_val):
# Use human-friendly indexes here (starting from 1) # Use human-friendly indexes here (starting from 1)
current = i * jobs + j + 1 current = i * jobs + j + 1
val = f_val[j]
val['current_epoch'] = current val['current_epoch'] = current
val['is_initial_point'] = current <= INITIAL_POINTS val['is_initial_point'] = current <= INITIAL_POINTS
logger.debug(f"Optimizer epoch evaluated: {val}") logger.debug(f"Optimizer epoch evaluated: {val}")

View File

@ -0,0 +1,49 @@
"""
SortinoHyperOptLoss
This module defines the alternative HyperOptLoss class which can be used for
Hyperoptimization.
"""
from datetime import datetime
from pandas import DataFrame
import numpy as np
from freqtrade.optimize.hyperopt import IHyperOptLoss
class SortinoHyperOptLoss(IHyperOptLoss):
"""
Defines the loss function for hyperopt.
This implementation uses the Sortino 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 Sortino Ratio calculation.
"""
total_profit = results["profit_percent"]
days_period = (max_date - min_date).days
# adding slippage of 0.1% per trade
total_profit = total_profit - 0.0005
expected_returns_mean = total_profit.sum() / days_period
results['downside_returns'] = 0
results.loc[total_profit < 0, 'downside_returns'] = results['profit_percent']
down_stdev = np.std(results['downside_returns'])
if np.std(total_profit) != 0.0:
sortino_ratio = expected_returns_mean / down_stdev * np.sqrt(365)
else:
# Define high (negative) sortino ratio to be clear that this is NOT optimal.
sortino_ratio = -20.
# print(expected_returns_mean, down_stdev, sortino_ratio)
return -sortino_ratio

View File

@ -0,0 +1,70 @@
"""
SortinoHyperOptLossDaily
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 SortinoHyperOptLossDaily(IHyperOptLoss):
"""
Defines the loss function for hyperopt.
This implementation uses the Sortino 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 Sortino Ratio calculation.
Sortino Ratio calculated as described in
http://www.redrockcapital.com/Sortino__A__Sharper__Ratio_Red_Rock_Capital.pdf
"""
resample_freq = '1D'
slippage_per_trade_ratio = 0.0005
days_in_year = 365
minimum_acceptable_return = 0.0
# 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,
normalize=True)
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"] - minimum_acceptable_return
expected_returns_mean = total_profit.mean()
sum_daily['downside_returns'] = 0
sum_daily.loc[total_profit < 0, 'downside_returns'] = total_profit
total_downside = sum_daily['downside_returns']
# Here total_downside contains min(0, P - MAR) values,
# where P = sum_daily["profit_percent_after_slippage"]
down_stdev = math.sqrt((total_downside**2).sum() / len(total_downside))
if (down_stdev != 0.):
sortino_ratio = expected_returns_mean / down_stdev * math.sqrt(days_in_year)
else:
# Define high (negative) sortino ratio to be clear that this is NOT optimal.
sortino_ratio = -20.
# print(t_index, sum_daily, total_profit)
# print(minimum_acceptable_return, expected_returns_mean, down_stdev, sortino_ratio)
return -sortino_ratio

View File

@ -66,7 +66,7 @@ def generate_text_table(data: Dict[str, Dict], stake_currency: str, max_open_tra
]) ])
# 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
return tabulate(tabular_data, headers=headers, return tabulate(tabular_data, headers=headers,
floatfmt=floatfmt, tablefmt="pipe") # type: ignore floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
def generate_text_table_sell_reason( def generate_text_table_sell_reason(
@ -112,7 +112,7 @@ def generate_text_table_sell_reason(
profit_percent_tot, profit_percent_tot,
] ]
) )
return tabulate(tabular_data, headers=headers, tablefmt="pipe") return tabulate(tabular_data, headers=headers, tablefmt="orgtbl", stralign="right")
def generate_text_table_strategy(stake_currency: str, max_open_trades: str, def generate_text_table_strategy(stake_currency: str, max_open_trades: str,
@ -146,7 +146,7 @@ def generate_text_table_strategy(stake_currency: str, max_open_trades: str,
]) ])
# 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
return tabulate(tabular_data, headers=headers, return tabulate(tabular_data, headers=headers,
floatfmt=floatfmt, tablefmt="pipe") # type: ignore floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore
def generate_edge_table(results: dict) -> str: def generate_edge_table(results: dict) -> str:
@ -172,4 +172,4 @@ def generate_edge_table(results: dict) -> str:
# 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
return tabulate(tabular_data, headers=headers, return tabulate(tabular_data, headers=headers,
floatfmt=floatfmt, tablefmt="pipe") # type: ignore floatfmt=floatfmt, tablefmt="orgtbl", stralign="right") # type: ignore

View File

@ -5,7 +5,8 @@ from typing import Any, Dict, List
import pandas as pd import pandas as pd
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data.btanalysis import (combine_tickers_with_mean, from freqtrade.data.btanalysis import (calculate_max_drawdown,
combine_tickers_with_mean,
create_cum_profit, create_cum_profit,
extract_trades_of_period, load_trades) extract_trades_of_period, load_trades)
from freqtrade.data.converter import trim_dataframe from freqtrade.data.converter import trim_dataframe
@ -111,6 +112,36 @@ def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> make_sub
return fig return fig
def add_max_drawdown(fig, row, trades: pd.DataFrame, df_comb: pd.DataFrame) -> make_subplots:
"""
Add scatter points indicating max drawdown
"""
try:
max_drawdown, highdate, lowdate = calculate_max_drawdown(trades)
drawdown = go.Scatter(
x=[highdate, lowdate],
y=[
df_comb.loc[highdate, 'cum_profit'],
df_comb.loc[lowdate, 'cum_profit'],
],
mode='markers',
name=f"Max drawdown {max_drawdown:.2f}%",
text=f"Max drawdown {max_drawdown:.2f}%",
marker=dict(
symbol='square-open',
size=9,
line=dict(width=2),
color='green'
)
)
fig.add_trace(drawdown, row, 1)
except ValueError:
logger.warning("No trades found - not plotting max drawdown.")
return fig
def plot_trades(fig, trades: pd.DataFrame) -> make_subplots: def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
""" """
Add trades to "fig" Add trades to "fig"
@ -364,6 +395,7 @@ def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame],
fig.add_trace(avgclose, 1, 1) fig.add_trace(avgclose, 1, 1)
fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit') fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit')
fig = add_max_drawdown(fig, 2, trades, df_comb)
for pair in pairs: for pair in pairs:
profit_col = f'cum_profit_{pair}' profit_col = f'cum_profit_{pair}'

View File

@ -11,6 +11,7 @@
"sell": 30 "sell": 30
}, },
"bid_strategy": { "bid_strategy": {
"price_side": "bid",
"ask_last_balance": 0.0, "ask_last_balance": 0.0,
"use_order_book": false, "use_order_book": false,
"order_book_top": 1, "order_book_top": 1,
@ -20,6 +21,7 @@
} }
}, },
"ask_strategy": { "ask_strategy": {
"price_side": "ask",
"use_order_book": false, "use_order_book": false,
"order_book_min": 1, "order_book_min": 1,
"order_book_max": 9, "order_book_max": 9,

View File

@ -190,7 +190,6 @@
"# Analyze the above\n", "# Analyze the above\n",
"parallel_trades = analyze_trade_parallelism(trades, '5m')\n", "parallel_trades = analyze_trade_parallelism(trades, '5m')\n",
"\n", "\n",
"\n",
"parallel_trades.plot()" "parallel_trades.plot()"
] ]
}, },
@ -212,11 +211,14 @@
"from freqtrade.plot.plotting import generate_candlestick_graph\n", "from freqtrade.plot.plotting import generate_candlestick_graph\n",
"# Limit graph period to keep plotly quick and reactive\n", "# Limit graph period to keep plotly quick and reactive\n",
"\n", "\n",
"# Filter trades to one pair\n",
"trades_red = trades.loc[trades['pair'] == pair]\n",
"\n",
"data_red = data['2019-06-01':'2019-06-10']\n", "data_red = data['2019-06-01':'2019-06-10']\n",
"# Generate candlestick graph\n", "# Generate candlestick graph\n",
"graph = generate_candlestick_graph(pair=pair,\n", "graph = generate_candlestick_graph(pair=pair,\n",
" data=data_red,\n", " data=data_red,\n",
" trades=trades,\n", " trades=trades_red,\n",
" indicators1=['sma20', 'ema50', 'ema55'],\n", " indicators1=['sma20', 'ema50', 'ema55'],\n",
" indicators2=['rsi', 'macd', 'macdsignal', 'macdhist']\n", " indicators2=['rsi', 'macd', 'macdsignal', 'macdhist']\n",
" )\n", " )\n",

View File

@ -1,6 +1,6 @@
# requirements without requirements installable via conda # requirements without requirements installable via conda
# mainly used for Raspberry pi installs # mainly used for Raspberry pi installs
ccxt==1.22.95 ccxt==1.23.30
SQLAlchemy==1.3.13 SQLAlchemy==1.3.13
python-telegram-bot==12.4.2 python-telegram-bot==12.4.2
arrow==0.15.5 arrow==0.15.5

View File

@ -3,7 +3,7 @@
# Required for hyperopt # Required for hyperopt
scipy==1.4.1 scipy==1.4.1
scikit-learn==0.22.1 scikit-learn==0.22.2
scikit-optimize==0.7.4 scikit-optimize==0.7.4
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.5.1 plotly==4.5.2

View File

@ -447,11 +447,6 @@ def test_create_datadir_failed(caplog):
def test_create_datadir(caplog, mocker): def test_create_datadir(caplog, mocker):
# Ensure that caplog is empty before starting ...
# Should prevent random failures.
caplog.clear()
# Added assert here to analyze random test-failures ...
assert len(caplog.record_tuples) == 0
cud = mocker.patch("freqtrade.commands.deploy_commands.create_userdata_dir", MagicMock()) cud = mocker.patch("freqtrade.commands.deploy_commands.create_userdata_dir", MagicMock())
csf = mocker.patch("freqtrade.commands.deploy_commands.copy_sample_files", MagicMock()) csf = mocker.patch("freqtrade.commands.deploy_commands.copy_sample_files", MagicMock())
@ -464,7 +459,6 @@ def test_create_datadir(caplog, mocker):
assert cud.call_count == 1 assert cud.call_count == 1
assert csf.call_count == 1 assert csf.call_count == 1
assert len(caplog.record_tuples) == 0
def test_start_new_strategy(mocker, caplog): def test_start_new_strategy(mocker, caplog):

View File

@ -2,15 +2,17 @@ from unittest.mock import MagicMock
import pytest import pytest
from arrow import Arrow from arrow import Arrow
from pandas import DataFrame, DateOffset, to_datetime from pandas import DataFrame, DateOffset, to_datetime, Timestamp
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data.btanalysis import (BT_DATA_COLUMNS, from freqtrade.data.btanalysis import (BT_DATA_COLUMNS,
analyze_trade_parallelism,
calculate_max_drawdown,
combine_tickers_with_mean, combine_tickers_with_mean,
create_cum_profit, create_cum_profit,
extract_trades_of_period, extract_trades_of_period,
load_backtest_data, load_trades, load_backtest_data, load_trades,
load_trades_from_db, analyze_trade_parallelism) load_trades_from_db)
from freqtrade.data.history import load_data, load_pair_history from freqtrade.data.history import load_data, load_pair_history
from tests.test_persistence import create_mock_trades from tests.test_persistence import create_mock_trades
@ -163,3 +165,17 @@ def test_create_cum_profit1(testdatadir):
assert "cum_profits" in cum_profits.columns assert "cum_profits" in cum_profits.columns
assert cum_profits.iloc[0]['cum_profits'] == 0 assert cum_profits.iloc[0]['cum_profits'] == 0
assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005 assert cum_profits.iloc[-1]['cum_profits'] == 0.0798005
def test_calculate_max_drawdown(testdatadir):
filename = testdatadir / "backtest-result_test.json"
bt_data = load_backtest_data(filename)
drawdown, h, low = calculate_max_drawdown(bt_data)
assert isinstance(drawdown, float)
assert pytest.approx(drawdown) == 0.21142322
assert isinstance(h, Timestamp)
assert isinstance(low, Timestamp)
assert h == Timestamp('2018-01-24 14:25:00', tz='UTC')
assert low == Timestamp('2018-01-30 04:45:00', tz='UTC')
with pytest.raises(ValueError, match='Trade dataframe empty.'):
drawdown, h, low = calculate_max_drawdown(DataFrame())

View File

@ -511,6 +511,22 @@ def test_validate_pairs_stakecompatibility(default_conf, mocker, caplog):
Exchange(default_conf) Exchange(default_conf)
def test_validate_pairs_stakecompatibility_downloaddata(default_conf, mocker, caplog):
api_mock = MagicMock()
default_conf['stake_currency'] = ''
type(api_mock).markets = PropertyMock(return_value={
'ETH/BTC': {'quote': 'BTC'}, 'LTC/BTC': {'quote': 'BTC'},
'XRP/BTC': {'quote': 'BTC'}, 'NEO/BTC': {'quote': 'BTC'},
'HELLO-WORLD': {'quote': 'BTC'},
})
mocker.patch('freqtrade.exchange.Exchange._init_ccxt', MagicMock(return_value=api_mock))
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes')
mocker.patch('freqtrade.exchange.Exchange._load_async_markets')
mocker.patch('freqtrade.exchange.Exchange.validate_stakecurrency')
Exchange(default_conf)
def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog): def test_validate_pairs_stakecompatibility_fail(default_conf, mocker, caplog):
default_conf['exchange']['pair_whitelist'].append('HELLO-WORLD') default_conf['exchange']['pair_whitelist'].append('HELLO-WORLD')
api_mock = MagicMock() api_mock = MagicMock()

View File

@ -369,6 +369,42 @@ def test_sharpe_loss_daily_prefers_higher_profits(default_conf, hyperopt_results
assert under > correct assert under > correct
def test_sortino_loss_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': 'SortinoHyperOptLoss'})
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_sortino_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': 'SortinoHyperOptLossDaily'})
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
@ -390,17 +426,27 @@ def test_onlyprofit_loss_prefers_higher_profits(default_conf, hyperopt_results)
def test_log_results_if_loss_improves(hyperopt, capsys) -> None: def test_log_results_if_loss_improves(hyperopt, capsys) -> None:
hyperopt.current_best_loss = 2 hyperopt.current_best_loss = 2
hyperopt.total_epochs = 2 hyperopt.total_epochs = 2
hyperopt.print_results( hyperopt.print_results(
{ {
'is_best': True,
'loss': 1, 'loss': 1,
'results_metrics':
{
'trade_count': 1,
'avg_profit': 0.1,
'total_profit': 0.001,
'profit': 1.0,
'duration': 20.0
},
'total_profit': 0,
'current_epoch': 2, # This starts from 1 (in a human-friendly manner) 'current_epoch': 2, # This starts from 1 (in a human-friendly manner)
'results_explanation': 'foo.', 'is_initial_point': False,
'is_initial_point': False 'is_best': True
} }
) )
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert ' 2/2: foo. Objective: 1.00000' in out assert all(x in out
for x in ["Best", "2/2", " 1", "0.10%", "0.00100000 BTC (1.00%)", "20.0 m"])
def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None: def test_no_log_if_loss_does_not_improve(hyperopt, caplog) -> None:
@ -422,13 +468,11 @@ def test_save_trials_saves_trials(mocker, hyperopt, testdatadir, caplog) -> None
hyperopt.trials = trials hyperopt.trials = trials
hyperopt.save_trials(final=True) hyperopt.save_trials(final=True)
assert log_has("Saving 1 epoch.", caplog)
assert log_has(f"1 epoch saved to '{trials_file}'.", caplog) assert log_has(f"1 epoch saved to '{trials_file}'.", caplog)
mock_dump.assert_called_once() mock_dump.assert_called_once()
hyperopt.trials = trials + trials hyperopt.trials = trials + trials
hyperopt.save_trials(final=True) hyperopt.save_trials(final=True)
assert log_has("Saving 2 epochs.", caplog)
assert log_has(f"2 epochs saved to '{trials_file}'.", caplog) assert log_has(f"2 epochs saved to '{trials_file}'.", caplog)
@ -466,8 +510,18 @@ def test_start_calls_optimizer(mocker, default_conf, caplog, capsys) -> None:
parallel = mocker.patch( parallel = mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', 'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', MagicMock(return_value=[{
'params': {'buy': {}, 'sell': {}, 'roi': {}, 'stoploss': 0.0}}]) 'loss': 1, 'results_explanation': 'foo result',
'params': {'buy': {}, 'sell': {}, 'roi': {}, 'stoploss': 0.0},
'results_metrics':
{
'trade_count': 1,
'avg_profit': 0.1,
'total_profit': 0.001,
'profit': 1.0,
'duration': 20.0
},
}])
) )
patch_exchange(mocker) patch_exchange(mocker)
# Co-test loading ticker-interval from strategy # Co-test loading ticker-interval from strategy
@ -761,11 +815,23 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None:
parallel = mocker.patch( parallel = mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', 'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}, MagicMock(return_value=[{
'params_details': {'buy': {'mfi-value': None}, 'loss': 1, 'results_explanation': 'foo result', 'params': {},
'sell': {'sell-mfi-value': None}, 'params_details': {
'roi': {}, 'stoploss': {'stoploss': None}, 'buy': {'mfi-value': None},
'trailing': {'trailing_stop': None}}}]) 'sell': {'sell-mfi-value': None},
'roi': {}, 'stoploss': {'stoploss': None},
'trailing': {'trailing_stop': None}
},
'results_metrics':
{
'trade_count': 1,
'avg_profit': 0.1,
'total_profit': 0.001,
'profit': 1.0,
'duration': 20.0
}
}])
) )
patch_exchange(mocker) patch_exchange(mocker)
@ -787,7 +853,11 @@ def test_print_json_spaces_all(mocker, default_conf, caplog, capsys) -> None:
parallel.assert_called_once() parallel.assert_called_once()
out, err = capsys.readouterr() out, err = capsys.readouterr()
assert '{"params":{"mfi-value":null,"sell-mfi-value":null},"minimal_roi":{},"stoploss":null,"trailing_stop":null}' in out # noqa: E501 result_str = (
'{"params":{"mfi-value":null,"sell-mfi-value":null},"minimal_roi"'
':{},"stoploss":null,"trailing_stop":null}'
)
assert result_str in out # noqa: E501
assert dumper.called assert dumper.called
# Should be called twice, once for tickerdata, once to save evaluations # Should be called twice, once for tickerdata, once to save evaluations
assert dumper.call_count == 2 assert dumper.call_count == 2
@ -804,10 +874,22 @@ def test_print_json_spaces_default(mocker, default_conf, caplog, capsys) -> None
parallel = mocker.patch( parallel = mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', 'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}, MagicMock(return_value=[{
'params_details': {'buy': {'mfi-value': None}, 'loss': 1, 'results_explanation': 'foo result', 'params': {},
'sell': {'sell-mfi-value': None}, 'params_details': {
'roi': {}, 'stoploss': {'stoploss': None}}}]) 'buy': {'mfi-value': None},
'sell': {'sell-mfi-value': None},
'roi': {}, 'stoploss': {'stoploss': None}
},
'results_metrics':
{
'trade_count': 1,
'avg_profit': 0.1,
'total_profit': 0.001,
'profit': 1.0,
'duration': 20.0
}
}])
) )
patch_exchange(mocker) patch_exchange(mocker)
@ -846,8 +928,18 @@ def test_print_json_spaces_roi_stoploss(mocker, default_conf, caplog, capsys) ->
parallel = mocker.patch( parallel = mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', 'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}, MagicMock(return_value=[{
'params_details': {'roi': {}, 'stoploss': {'stoploss': None}}}]) 'loss': 1, 'results_explanation': 'foo result', 'params': {},
'params_details': {'roi': {}, 'stoploss': {'stoploss': None}},
'results_metrics':
{
'trade_count': 1,
'avg_profit': 0.1,
'total_profit': 0.001,
'profit': 1.0,
'duration': 20.0
}
}])
) )
patch_exchange(mocker) patch_exchange(mocker)
@ -887,7 +979,16 @@ def test_simplified_interface_roi_stoploss(mocker, default_conf, caplog, capsys)
parallel = mocker.patch( parallel = mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', 'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
MagicMock(return_value=[{ MagicMock(return_value=[{
'loss': 1, 'results_explanation': 'foo result', 'params': {'stoploss': 0.0}}]) 'loss': 1, 'results_explanation': 'foo result', 'params': {'stoploss': 0.0},
'results_metrics':
{
'trade_count': 1,
'avg_profit': 0.1,
'total_profit': 0.001,
'profit': 1.0,
'duration': 20.0
}
}])
) )
patch_exchange(mocker) patch_exchange(mocker)
@ -965,7 +1066,17 @@ def test_simplified_interface_buy(mocker, default_conf, caplog, capsys) -> None:
parallel = mocker.patch( parallel = mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', 'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}]) MagicMock(return_value=[{
'loss': 1, 'results_explanation': 'foo result', 'params': {},
'results_metrics':
{
'trade_count': 1,
'avg_profit': 0.1,
'total_profit': 0.001,
'profit': 1.0,
'duration': 20.0
}
}])
) )
patch_exchange(mocker) patch_exchange(mocker)
@ -1012,7 +1123,17 @@ def test_simplified_interface_sell(mocker, default_conf, caplog, capsys) -> None
parallel = mocker.patch( parallel = mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel', 'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
MagicMock(return_value=[{'loss': 1, 'results_explanation': 'foo result', 'params': {}}]) MagicMock(return_value=[{
'loss': 1, 'results_explanation': 'foo result', 'params': {},
'results_metrics':
{
'trade_count': 1,
'avg_profit': 0.1,
'total_profit': 0.001,
'profit': 1.0,
'duration': 20.0
}
}])
) )
patch_exchange(mocker) patch_exchange(mocker)

View File

@ -22,14 +22,14 @@ def test_generate_text_table(default_conf, mocker):
) )
result_str = ( result_str = (
'| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC |' '| Pair | Buys | Avg Profit % | Cum Profit % | Tot Profit BTC |'
' Tot Profit % | Avg Duration | Wins | Draws | Losses |\n' ' Tot Profit % | Avg Duration | Wins | Draws | Losses |\n'
'|:--------|-------:|---------------:|---------------:|-----------------:|' '|---------+--------+----------------+----------------+------------------+'
'---------------:|:---------------|-------:|--------:|---------:|\n' '----------------+----------------+--------+---------+----------|\n'
'| ETH/BTC | 2 | 15.00 | 30.00 | 0.60000000 |' '| ETH/BTC | 2 | 15.00 | 30.00 | 0.60000000 |'
' 15.00 | 0:20:00 | 2 | 0 | 0 |\n' ' 15.00 | 0:20:00 | 2 | 0 | 0 |\n'
'| TOTAL | 2 | 15.00 | 30.00 | 0.60000000 |' '| TOTAL | 2 | 15.00 | 30.00 | 0.60000000 |'
' 15.00 | 0:20:00 | 2 | 0 | 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,
@ -52,13 +52,13 @@ def test_generate_text_table_sell_reason(default_conf, mocker):
) )
result_str = ( result_str = (
'| Sell Reason | Sells | Wins | Draws | Losses |' '| Sell Reason | Sells | Wins | Draws | Losses |'
' Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % |\n' ' Avg Profit % | Cum Profit % | Tot Profit BTC | Tot Profit % |\n'
'|:--------------|--------:|-------:|--------:|---------:|' '|---------------+---------+--------+---------+----------+'
'---------------:|---------------:|-----------------:|---------------:|\n' '----------------+----------------+------------------+----------------|\n'
'| roi | 2 | 2 | 0 | 0 |' '| roi | 2 | 2 | 0 | 0 |'
' 15 | 30 | 0.6 | 15 |\n' ' 15 | 30 | 0.6 | 15 |\n'
'| stop_loss | 1 | 0 | 0 | 1 |' '| stop_loss | 1 | 0 | 0 | 1 |'
' -10 | -10 | -0.2 | -5 |' ' -10 | -10 | -0.2 | -5 |'
) )
assert generate_text_table_sell_reason( assert generate_text_table_sell_reason(
@ -95,14 +95,14 @@ def test_generate_text_table_strategy(default_conf, mocker):
) )
result_str = ( result_str = (
'| Strategy | Buys | Avg Profit % | Cum Profit % | Tot' '| Strategy | Buys | Avg Profit % | Cum Profit % | Tot'
' Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses |\n' ' Profit BTC | Tot Profit % | Avg Duration | Wins | Draws | Losses |\n'
'|:--------------|-------:|---------------:|---------------:|------' '|---------------+--------+----------------+----------------+------------------+'
'-----------:|---------------:|:---------------|-------:|--------:|---------:|\n' '----------------+----------------+--------+---------+----------|\n'
'| TestStrategy1 | 3 | 20.00 | 60.00 | ' '| TestStrategy1 | 3 | 20.00 | 60.00 | 1.10000000 |'
' 1.10000000 | 30.00 | 0:17:00 | 3 | 0 | 0 |\n' ' 30.00 | 0:17:00 | 3 | 0 | 0 |\n'
'| TestStrategy2 | 3 | 30.00 | 90.00 | ' '| TestStrategy2 | 3 | 30.00 | 90.00 | 1.30000000 |'
' 1.30000000 | 45.00 | 0:20:00 | 3 | 0 | 0 |' ' 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
@ -111,8 +111,7 @@ def test_generate_edge_table(edge_conf, mocker):
results = {} results = {}
results['ETH/BTC'] = PairInfo(-0.01, 0.60, 2, 1, 3, 10, 60) results['ETH/BTC'] = PairInfo(-0.01, 0.60, 2, 1, 3, 10, 60)
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

@ -51,13 +51,13 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'open_date_hum': ANY, 'open_date_hum': ANY,
'close_date': None, 'close_date': None,
'close_date_hum': None, 'close_date_hum': None,
'open_rate': 1.099e-05, 'open_rate': 1.098e-05,
'close_rate': None, 'close_rate': None,
'current_rate': 1.098e-05, 'current_rate': 1.099e-05,
'amount': 90.99181074, 'amount': 91.07468124,
'stake_amount': 0.001, 'stake_amount': 0.001,
'close_profit': None, 'close_profit': None,
'current_profit': -0.59, 'current_profit': -0.41,
'stop_loss': 0.0, 'stop_loss': 0.0,
'initial_stop_loss': 0.0, 'initial_stop_loss': 0.0,
'initial_stop_loss_pct': None, 'initial_stop_loss_pct': None,
@ -78,10 +78,10 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'open_date_hum': ANY, 'open_date_hum': ANY,
'close_date': None, 'close_date': None,
'close_date_hum': None, 'close_date_hum': None,
'open_rate': 1.099e-05, 'open_rate': 1.098e-05,
'close_rate': None, 'close_rate': None,
'current_rate': ANY, 'current_rate': ANY,
'amount': 90.99181074, 'amount': 91.07468124,
'stake_amount': 0.001, 'stake_amount': 0.001,
'close_profit': None, 'close_profit': None,
'current_profit': ANY, 'current_profit': ANY,
@ -121,7 +121,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
assert "Pair" in headers assert "Pair" in headers
assert 'instantly' == result[0][2] assert 'instantly' == result[0][2]
assert 'ETH/BTC' in result[0][1] assert 'ETH/BTC' in result[0][1]
assert '-0.59%' == result[0][3] assert '-0.41%' == result[0][3]
# Test with fiatconvert # Test with fiatconvert
rpc._fiat_converter = CryptoToFiatConverter() rpc._fiat_converter = CryptoToFiatConverter()
@ -130,7 +130,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
assert "Pair" in headers assert "Pair" in headers
assert 'instantly' == result[0][2] assert 'instantly' == result[0][2]
assert 'ETH/BTC' in result[0][1] assert 'ETH/BTC' in result[0][1]
assert '-0.59% (-0.09)' == result[0][3] assert '-0.41% (-0.06)' == result[0][3]
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate', mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_sell_rate',
MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available"))) MagicMock(side_effect=DependencyException(f"Pair 'ETH/BTC' not available")))
@ -245,9 +245,9 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05) assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
assert prec_satoshi(stats['profit_closed_percent'], 6.2) assert prec_satoshi(stats['profit_closed_percent'], 6.2)
assert prec_satoshi(stats['profit_closed_fiat'], 0.93255) assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
assert prec_satoshi(stats['profit_all_coin'], 5.632e-05) assert prec_satoshi(stats['profit_all_coin'], 5.802e-05)
assert prec_satoshi(stats['profit_all_percent'], 2.81) assert prec_satoshi(stats['profit_all_percent'], 2.89)
assert prec_satoshi(stats['profit_all_fiat'], 0.8448) assert prec_satoshi(stats['profit_all_fiat'], 0.8703)
assert stats['trade_count'] == 2 assert stats['trade_count'] == 2
assert stats['first_trade_date'] == 'just now' assert stats['first_trade_date'] == 'just now'
assert stats['latest_trade_date'] == 'just now' assert stats['latest_trade_date'] == 'just now'
@ -668,7 +668,7 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order) -> None
trade = rpc._rpc_forcebuy(pair, None) trade = rpc._rpc_forcebuy(pair, None)
assert isinstance(trade, Trade) assert isinstance(trade, Trade)
assert trade.pair == pair assert trade.pair == pair
assert trade.open_rate == ticker()['ask'] assert trade.open_rate == ticker()['bid']
# Test buy duplicate # Test buy duplicate
with pytest.raises(RPCException, match=r'position for ETH/BTC already open - id: 1'): with pytest.raises(RPCException, match=r'position for ETH/BTC already open - id: 1'):

View File

@ -426,20 +426,20 @@ def test_api_status(botclient, mocker, ticker, fee, markets):
rc = client_get(client, f"{BASE_URI}/status") rc = client_get(client, f"{BASE_URI}/status")
assert_response(rc) assert_response(rc)
assert len(rc.json) == 1 assert len(rc.json) == 1
assert rc.json == [{'amount': 90.99181074, assert rc.json == [{'amount': 91.07468124,
'base_currency': 'BTC', 'base_currency': 'BTC',
'close_date': None, 'close_date': None,
'close_date_hum': None, 'close_date_hum': None,
'close_profit': None, 'close_profit': None,
'close_rate': None, 'close_rate': None,
'current_profit': -0.59, 'current_profit': -0.41,
'current_rate': 1.098e-05, 'current_rate': 1.099e-05,
'initial_stop_loss': 0.0, 'initial_stop_loss': 0.0,
'initial_stop_loss_pct': None, 'initial_stop_loss_pct': None,
'open_date': ANY, 'open_date': ANY,
'open_date_hum': 'just now', 'open_date_hum': 'just now',
'open_order': '(limit buy rem=0.00000000)', 'open_order': '(limit buy rem=0.00000000)',
'open_rate': 1.099e-05, 'open_rate': 1.098e-05,
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'stake_amount': 0.001, 'stake_amount': 0.001,
'stop_loss': 0.0, 'stop_loss': 0.0,

View File

@ -720,13 +720,13 @@ def test_forcesell_handle(default_conf, update, ticker, fee,
'exchange': 'Bittrex', 'exchange': 'Bittrex',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'gain': 'profit', 'gain': 'profit',
'limit': 1.172e-05, 'limit': 1.173e-05,
'amount': 90.99181073703367, 'amount': 91.07468123861567,
'order_type': 'limit', 'order_type': 'limit',
'open_rate': 1.099e-05, 'open_rate': 1.098e-05,
'current_rate': 1.172e-05, 'current_rate': 1.173e-05,
'profit_amount': 6.126e-05, 'profit_amount': 6.314e-05,
'profit_ratio': 0.0611052, 'profit_ratio': 0.0629778,
'stake_currency': 'BTC', 'stake_currency': 'BTC',
'fiat_currency': 'USD', 'fiat_currency': 'USD',
'sell_reason': SellType.FORCE_SELL.value, 'sell_reason': SellType.FORCE_SELL.value,
@ -779,13 +779,13 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
'exchange': 'Bittrex', 'exchange': 'Bittrex',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'gain': 'loss', 'gain': 'loss',
'limit': 1.044e-05, 'limit': 1.043e-05,
'amount': 90.99181073703367, 'amount': 91.07468123861567,
'order_type': 'limit', 'order_type': 'limit',
'open_rate': 1.099e-05, 'open_rate': 1.098e-05,
'current_rate': 1.044e-05, 'current_rate': 1.043e-05,
'profit_amount': -5.492e-05, 'profit_amount': -5.497e-05,
'profit_ratio': -0.05478342, 'profit_ratio': -0.05482878,
'stake_currency': 'BTC', 'stake_currency': 'BTC',
'fiat_currency': 'USD', 'fiat_currency': 'USD',
'sell_reason': SellType.FORCE_SELL.value, 'sell_reason': SellType.FORCE_SELL.value,
@ -827,13 +827,13 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
'exchange': 'Bittrex', 'exchange': 'Bittrex',
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'gain': 'loss', 'gain': 'loss',
'limit': 1.098e-05, 'limit': 1.099e-05,
'amount': 90.99181073703367, 'amount': 91.07468123861567,
'order_type': 'limit', 'order_type': 'limit',
'open_rate': 1.099e-05, 'open_rate': 1.098e-05,
'current_rate': 1.098e-05, 'current_rate': 1.099e-05,
'profit_amount': -5.91e-06, 'profit_amount': -4.09e-06,
'profit_ratio': -0.00589291, 'profit_ratio': -0.00408133,
'stake_currency': 'BTC', 'stake_currency': 'BTC',
'fiat_currency': 'USD', 'fiat_currency': 'USD',
'sell_reason': SellType.FORCE_SELL.value, 'sell_reason': SellType.FORCE_SELL.value,

View File

@ -761,8 +761,8 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order,
assert trade.is_open assert trade.is_open
assert trade.open_date is not None assert trade.open_date is not None
assert trade.exchange == 'bittrex' assert trade.exchange == 'bittrex'
assert trade.open_rate == 0.00001099 assert trade.open_rate == 0.00001098
assert trade.amount == 90.99181073703367 assert trade.amount == 91.07468123861567
assert log_has( assert log_has(
'Buy signal found: about create a new trade with stake_amount: 0.001 ...', caplog 'Buy signal found: about create a new trade with stake_amount: 0.001 ...', caplog
@ -906,20 +906,37 @@ def test_process_informative_pairs_added(default_conf, ticker, mocker) -> None:
assert ("ETH/BTC", default_conf["ticker_interval"]) in refresh_mock.call_args[0][0] assert ("ETH/BTC", default_conf["ticker_interval"]) in refresh_mock.call_args[0][0]
@pytest.mark.parametrize("ask,last,last_ab,expected", [ @pytest.mark.parametrize("side,ask,bid,last,last_ab,expected", [
(20, 10, 0.0, 20), # Full ask side ('ask', 20, 19, 10, 0.0, 20), # Full ask side
(20, 10, 1.0, 10), # Full last side ('ask', 20, 19, 10, 1.0, 10), # Full last side
(20, 10, 0.5, 15), # Between ask and last ('ask', 20, 19, 10, 0.5, 15), # Between ask and last
(20, 10, 0.7, 13), # Between ask and last ('ask', 20, 19, 10, 0.7, 13), # Between ask and last
(20, 10, 0.3, 17), # Between ask and last ('ask', 20, 19, 10, 0.3, 17), # Between ask and last
(5, 10, 1.0, 5), # last bigger than ask ('ask', 5, 6, 10, 1.0, 5), # last bigger than ask
(5, 10, 0.5, 5), # last bigger than ask ('ask', 5, 6, 10, 0.5, 5), # last bigger than ask
('ask', 10, 20, None, 0.5, 10), # last not available - uses ask
('ask', 4, 5, None, 0.5, 4), # last not available - uses ask
('ask', 4, 5, None, 1, 4), # last not available - uses ask
('ask', 4, 5, None, 0, 4), # last not available - uses ask
('bid', 10, 20, 10, 0.0, 20), # Full bid side
('bid', 10, 20, 10, 1.0, 10), # Full last side
('bid', 10, 20, 10, 0.5, 15), # Between bid and last
('bid', 10, 20, 10, 0.7, 13), # Between bid and last
('bid', 10, 20, 10, 0.3, 17), # Between bid and last
('bid', 4, 5, 10, 1.0, 5), # last bigger than bid
('bid', 4, 5, 10, 0.5, 5), # last bigger than bid
('bid', 10, 20, None, 0.5, 20), # last not available - uses bid
('bid', 4, 5, None, 0.5, 5), # last not available - uses bid
('bid', 4, 5, None, 1, 5), # last not available - uses bid
('bid', 4, 5, None, 0, 5), # last not available - uses bid
]) ])
def test_get_buy_rate(mocker, default_conf, caplog, ask, last, last_ab, expected) -> None: def test_get_buy_rate(mocker, default_conf, caplog, side, ask, bid,
last, last_ab, expected) -> None:
default_conf['bid_strategy']['ask_last_balance'] = last_ab default_conf['bid_strategy']['ask_last_balance'] = last_ab
default_conf['bid_strategy']['price_side'] = side
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', mocker.patch('freqtrade.exchange.Exchange.fetch_ticker',
MagicMock(return_value={'ask': ask, 'last': last})) MagicMock(return_value={'ask': ask, 'last': last, 'bid': bid}))
assert freqtrade.get_buy_rate('ETH/BTC', True) == expected assert freqtrade.get_buy_rate('ETH/BTC', True) == expected
assert not log_has("Using cached buy rate for ETH/BTC.", caplog) assert not log_has("Using cached buy rate for ETH/BTC.", caplog)
@ -1317,7 +1334,7 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
stoploss_order_mock.assert_not_called() stoploss_order_mock.assert_not_called()
assert freqtrade.handle_trade(trade) is False assert freqtrade.handle_trade(trade) is False
assert trade.stop_loss == 0.00002344 * 0.95 assert trade.stop_loss == 0.00002346 * 0.95
# setting stoploss_on_exchange_interval to 0 seconds # setting stoploss_on_exchange_interval to 0 seconds
freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0 freqtrade.strategy.order_types['stoploss_on_exchange_interval'] = 0
@ -1325,10 +1342,10 @@ def test_handle_stoploss_on_exchange_trailing(mocker, default_conf, fee, caplog,
assert freqtrade.handle_stoploss_on_exchange(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False
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.32423208191126,
pair='ETH/BTC', pair='ETH/BTC',
order_types=freqtrade.strategy.order_types, order_types=freqtrade.strategy.order_types,
stop_price=0.00002344 * 0.95) stop_price=0.00002346 * 0.95)
# price fell below stoploss, so dry-run sells trade. # price fell below stoploss, so dry-run sells trade.
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={ mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', MagicMock(return_value={
@ -1510,12 +1527,12 @@ def test_tsl_on_exchange_compatible_with_edge(mocker, edge_conf, fee, caplog,
assert freqtrade.handle_stoploss_on_exchange(trade) is False assert freqtrade.handle_stoploss_on_exchange(trade) is False
# stoploss should be set to 1% as trailing is on # stoploss should be set to 1% as trailing is on
assert trade.stop_loss == 0.00002344 * 0.99 assert trade.stop_loss == 0.00002346 * 0.99
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=2132892.491467577,
pair='NEO/BTC', pair='NEO/BTC',
order_types=freqtrade.strategy.order_types, order_types=freqtrade.strategy.order_types,
stop_price=0.00002344 * 0.99) stop_price=0.00002346 * 0.99)
def test_enter_positions(mocker, default_conf, caplog) -> None: def test_enter_positions(mocker, default_conf, caplog) -> None:
@ -2292,12 +2309,12 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'gain': 'profit', 'gain': 'profit',
'limit': 1.172e-05, 'limit': 1.172e-05,
'amount': 90.99181073703367, 'amount': 91.07468123861567,
'order_type': 'limit', 'order_type': 'limit',
'open_rate': 1.099e-05, 'open_rate': 1.098e-05,
'current_rate': 1.172e-05, 'current_rate': 1.173e-05,
'profit_amount': 6.126e-05, 'profit_amount': 6.223e-05,
'profit_ratio': 0.0611052, 'profit_ratio': 0.0620716,
'stake_currency': 'BTC', 'stake_currency': 'BTC',
'fiat_currency': 'USD', 'fiat_currency': 'USD',
'sell_reason': SellType.ROI.value, 'sell_reason': SellType.ROI.value,
@ -2341,12 +2358,12 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker)
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'gain': 'loss', 'gain': 'loss',
'limit': 1.044e-05, 'limit': 1.044e-05,
'amount': 90.99181073703367, 'amount': 91.07468123861567,
'order_type': 'limit', 'order_type': 'limit',
'open_rate': 1.099e-05, 'open_rate': 1.098e-05,
'current_rate': 1.044e-05, 'current_rate': 1.043e-05,
'profit_amount': -5.492e-05, 'profit_amount': -5.406e-05,
'profit_ratio': -0.05478342, 'profit_ratio': -0.05392257,
'stake_currency': 'BTC', 'stake_currency': 'BTC',
'fiat_currency': 'USD', 'fiat_currency': 'USD',
'sell_reason': SellType.STOP_LOSS.value, 'sell_reason': SellType.STOP_LOSS.value,
@ -2397,12 +2414,12 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'gain': 'loss', 'gain': 'loss',
'limit': 1.08801e-05, 'limit': 1.08801e-05,
'amount': 90.99181073703367, 'amount': 91.07468123861567,
'order_type': 'limit', 'order_type': 'limit',
'open_rate': 1.099e-05, 'open_rate': 1.098e-05,
'current_rate': 1.044e-05, 'current_rate': 1.043e-05,
'profit_amount': -1.498e-05, 'profit_amount': -1.408e-05,
'profit_ratio': -0.01493766, 'profit_ratio': -0.01404051,
'stake_currency': 'BTC', 'stake_currency': 'BTC',
'fiat_currency': 'USD', 'fiat_currency': 'USD',
'sell_reason': SellType.STOP_LOSS.value, 'sell_reason': SellType.STOP_LOSS.value,
@ -2587,7 +2604,7 @@ def test_execute_sell_market_order(default_conf, ticker, fee,
freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI) freqtrade.execute_sell(trade=trade, limit=ticker_sell_up()['bid'], sell_reason=SellType.ROI)
assert not trade.is_open assert not trade.is_open
assert trade.close_profit == 0.0611052 assert trade.close_profit == 0.0620716
assert rpc_mock.call_count == 2 assert rpc_mock.call_count == 2
last_msg = rpc_mock.call_args_list[-1][0][0] last_msg = rpc_mock.call_args_list[-1][0][0]
@ -2597,12 +2614,12 @@ def test_execute_sell_market_order(default_conf, ticker, fee,
'pair': 'ETH/BTC', 'pair': 'ETH/BTC',
'gain': 'profit', 'gain': 'profit',
'limit': 1.172e-05, 'limit': 1.172e-05,
'amount': 90.99181073703367, 'amount': 91.07468123861567,
'order_type': 'market', 'order_type': 'market',
'open_rate': 1.099e-05, 'open_rate': 1.098e-05,
'current_rate': 1.172e-05, 'current_rate': 1.173e-05,
'profit_amount': 6.126e-05, 'profit_amount': 6.223e-05,
'profit_ratio': 0.0611052, 'profit_ratio': 0.0620716,
'stake_currency': 'BTC', 'stake_currency': 'BTC',
'fiat_currency': 'USD', 'fiat_currency': 'USD',
'sell_reason': SellType.ROI.value, 'sell_reason': SellType.ROI.value,
@ -3624,13 +3641,20 @@ def test_order_book_ask_strategy(default_conf, limit_buy_order, limit_sell_order
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
def test_get_sell_rate(default_conf, mocker, caplog, ticker, order_book_l2) -> None: @pytest.mark.parametrize('side,ask,bid,expected', [
('bid', 10.0, 11.0, 11.0),
mocker.patch.multiple( ('bid', 10.0, 11.2, 11.2),
'freqtrade.exchange.Exchange', ('bid', 10.0, 11.0, 11.0),
get_order_book=order_book_l2, ('bid', 9.8, 11.0, 11.0),
fetch_ticker=ticker, ('bid', 0.0001, 0.002, 0.002),
) ('ask', 10.0, 11.0, 10.0),
('ask', 10.11, 11.2, 10.11),
('ask', 0.001, 0.002, 0.001),
('ask', 0.006, 1.0, 0.006),
])
def test_get_sell_rate(default_conf, mocker, caplog, side, bid, ask, expected) -> None:
default_conf['ask_strategy']['price_side'] = side
mocker.patch('freqtrade.exchange.Exchange.fetch_ticker', return_value={'ask': ask, 'bid': bid})
pair = "ETH/BTC" pair = "ETH/BTC"
# Test regular mode # Test regular mode
@ -3638,25 +3662,33 @@ def test_get_sell_rate(default_conf, mocker, caplog, ticker, order_book_l2) -> N
rate = ft.get_sell_rate(pair, True) rate = ft.get_sell_rate(pair, True)
assert not log_has("Using cached sell rate for ETH/BTC.", caplog) assert not log_has("Using cached sell rate for ETH/BTC.", caplog)
assert isinstance(rate, float) assert isinstance(rate, float)
assert rate == 0.00001098 assert rate == expected
# Use caching # Use caching
rate = ft.get_sell_rate(pair, False) rate = ft.get_sell_rate(pair, False)
assert rate == 0.00001098 assert rate == expected
assert log_has("Using cached sell rate for ETH/BTC.", caplog) assert log_has("Using cached sell rate for ETH/BTC.", caplog)
caplog.clear()
@pytest.mark.parametrize('side,expected', [
('bid', 0.043936), # Value from order_book_l2 fiture - bids side
('ask', 0.043949), # Value from order_book_l2 fiture - asks side
])
def test_get_sell_rate_orderbook(default_conf, mocker, caplog, side, expected, order_book_l2):
# Test orderbook mode # Test orderbook mode
default_conf['ask_strategy']['price_side'] = side
default_conf['ask_strategy']['use_order_book'] = True default_conf['ask_strategy']['use_order_book'] = True
default_conf['ask_strategy']['order_book_min'] = 1 default_conf['ask_strategy']['order_book_min'] = 1
default_conf['ask_strategy']['order_book_max'] = 2 default_conf['ask_strategy']['order_book_max'] = 2
# TODO: min/max is irrelevant for this test until refactoring
pair = "ETH/BTC"
mocker.patch('freqtrade.exchange.Exchange.get_order_book', order_book_l2)
ft = get_patched_freqtradebot(mocker, default_conf) ft = get_patched_freqtradebot(mocker, default_conf)
rate = ft.get_sell_rate(pair, True) rate = ft.get_sell_rate(pair, True)
assert not log_has("Using cached sell rate for ETH/BTC.", caplog) assert not log_has("Using cached sell rate for ETH/BTC.", caplog)
assert isinstance(rate, float) assert isinstance(rate, float)
assert rate == 0.043936 assert rate == expected
rate = ft.get_sell_rate(pair, False) rate = ft.get_sell_rate(pair, False)
assert rate == 0.043936 assert rate == expected
assert log_has("Using cached sell rate for ETH/BTC.", caplog) assert log_has("Using cached sell rate for ETH/BTC.", caplog)

View File

@ -3,15 +3,16 @@ from copy import deepcopy
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pandas as pd
import plotly.graph_objects as go import plotly.graph_objects as go
import pytest import pytest
from plotly.subplots import make_subplots from plotly.subplots import make_subplots
from freqtrade.commands import start_plot_dataframe, start_plot_profit
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data import history from freqtrade.data import history
from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data from freqtrade.data.btanalysis import create_cum_profit, load_backtest_data
from freqtrade.exceptions import OperationalException from freqtrade.exceptions import OperationalException
from freqtrade.commands import start_plot_dataframe, start_plot_profit
from freqtrade.plot.plotting import (add_indicators, add_profit, from freqtrade.plot.plotting import (add_indicators, add_profit,
create_plotconfig, create_plotconfig,
generate_candlestick_graph, generate_candlestick_graph,
@ -266,6 +267,7 @@ def test_generate_profit_graph(testdatadir):
trades = load_backtest_data(filename) trades = load_backtest_data(filename)
timerange = TimeRange.parse_timerange("20180110-20180112") timerange = TimeRange.parse_timerange("20180110-20180112")
pairs = ["TRX/BTC", "ADA/BTC"] pairs = ["TRX/BTC", "ADA/BTC"]
trades = trades[trades['close_time'] < pd.Timestamp('2018-01-12', tz='UTC')]
tickers = history.load_data(datadir=testdatadir, tickers = history.load_data(datadir=testdatadir,
pairs=pairs, pairs=pairs,
@ -283,13 +285,15 @@ def test_generate_profit_graph(testdatadir):
assert fig.layout.yaxis3.title.text == "Profit" assert fig.layout.yaxis3.title.text == "Profit"
figure = fig.layout.figure figure = fig.layout.figure
assert len(figure.data) == 4 assert len(figure.data) == 5
avgclose = find_trace_in_fig_data(figure.data, "Avg close price") avgclose = find_trace_in_fig_data(figure.data, "Avg close price")
assert isinstance(avgclose, go.Scatter) assert isinstance(avgclose, go.Scatter)
profit = find_trace_in_fig_data(figure.data, "Profit") profit = find_trace_in_fig_data(figure.data, "Profit")
assert isinstance(profit, go.Scatter) assert isinstance(profit, go.Scatter)
profit = find_trace_in_fig_data(figure.data, "Max drawdown 0.00%")
assert isinstance(profit, go.Scatter)
for pair in pairs: for pair in pairs:
profit_pair = find_trace_in_fig_data(figure.data, f"Profit {pair}") profit_pair = find_trace_in_fig_data(figure.data, f"Profit {pair}")

View File

@ -60,7 +60,7 @@ def test_throttle(mocker, default_conf, caplog) -> None:
assert result == 42 assert result == 42
assert end - start > 0.1 assert end - start > 0.1
assert log_has_re(r"Throttling with 'throttled_func\(\)': sleep for 0\.10 s.*", caplog) assert log_has_re(r"Throttling with 'throttled_func\(\)': sleep for \d\.\d{2} s.*", caplog)
result = worker._throttle(throttled_func, throttle_secs=-1) result = worker._throttle(throttled_func, throttle_secs=-1)
assert result == 42 assert result == 42