Merge branch 'develop' into BASE64

This commit is contained in:
Gert Wohlgemuth 2018-07-05 14:40:04 -07:00 committed by GitHub
commit 1c48902e64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1135 additions and 1140 deletions

View File

@ -5,7 +5,11 @@
"fiat_display_currency": "USD", "fiat_display_currency": "USD",
"ticker_interval" : "5m", "ticker_interval" : "5m",
"dry_run": false, "dry_run": false,
"unfilledtimeout": 600, "trailing_stop": false,
"unfilledtimeout": {
"buy": 10,
"sell": 30
},
"bid_strategy": { "bid_strategy": {
"ask_last_balance": 0.0 "ask_last_balance": 0.0
}, },

View File

@ -5,6 +5,8 @@
"fiat_display_currency": "USD", "fiat_display_currency": "USD",
"dry_run": false, "dry_run": false,
"ticker_interval": "5m", "ticker_interval": "5m",
"trailing_stop": false,
"trailing_stop_positive": 0.005,
"minimal_roi": { "minimal_roi": {
"40": 0.0, "40": 0.0,
"30": 0.01, "30": 0.01,
@ -12,7 +14,10 @@
"0": 0.04 "0": 0.04
}, },
"stoploss": -0.10, "stoploss": -0.10,
"unfilledtimeout": 600, "unfilledtimeout": {
"buy": 10,
"sell": 30
},
"bid_strategy": { "bid_strategy": {
"ask_last_balance": 0.0 "ask_last_balance": 0.0
}, },

View File

@ -70,6 +70,34 @@ Where `-s TestStrategy` refers to the class name within the strategy file `test_
python3 ./freqtrade/main.py backtesting --export trades python3 ./freqtrade/main.py backtesting --export trades
``` ```
The exported trades can be read using the following code for manual analysis, or can be used by the plotting script `plot_dataframe.py` in the scripts folder.
``` python
import json
from pathlib import Path
import pandas as pd
filename=Path('user_data/backtest_data/backtest-result.json')
with filename.open() as file:
data = json.load(file)
columns = ["pair", "profit", "opents", "closets", "index", "duration",
"open_rate", "close_rate", "open_at_end"]
df = pd.DataFrame(data, columns=columns)
df['opents'] = pd.to_datetime(df['opents'],
unit='s',
utc=True,
infer_datetime_format=True
)
df['closets'] = pd.to_datetime(df['closets'],
unit='s',
utc=True,
infer_datetime_format=True
)
```
#### Exporting trades to file specifying a custom filename #### Exporting trades to file specifying a custom filename
```bash ```bash

View File

@ -1,12 +1,15 @@
# Configure the bot # Configure the bot
This page explains how to configure your `config.json` file. This page explains how to configure your `config.json` file.
## Table of Contents ## Table of Contents
- [Bot commands](#bot-commands) - [Bot commands](#bot-commands)
- [Backtesting commands](#backtesting-commands) - [Backtesting commands](#backtesting-commands)
- [Hyperopt commands](#hyperopt-commands) - [Hyperopt commands](#hyperopt-commands)
## Setup config.json ## Setup config.json
We recommend to copy and use the `config.json.example` as a template We recommend to copy and use the `config.json.example` as a template
for your bot configuration. for your bot configuration.
@ -22,7 +25,10 @@ The table below will list all configuration parameters.
| `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode. | `dry_run` | true | Yes | Define if the bot must be in Dry-run or production mode.
| `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file. | `minimal_roi` | See below | No | Set the threshold in percent the bot will use to sell a trade. More information below. If set, this parameter will override `minimal_roi` from your strategy file.
| `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file. | `stoploss` | -0.10 | No | Value of the stoploss in percent used by the bot. More information below. If set, this parameter will override `stoploss` from your strategy file.
| `unfilledtimeout` | 0 | No | How long (in minutes) the bot will wait for an unfilled order to complete, after which the order will be cancelled. | `trailing_stoploss` | false | No | Enables trailing stop-loss (based on `stoploss` in either configuration or strategy file).
| `trailing_stoploss_positve` | 0 | No | Changes stop-loss once profit has been reached.
| `unfilledtimeout.buy` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled.
| `unfilledtimeout.sell` | 10 | Yes | How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled.
| `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below. | `bid_strategy.ask_last_balance` | 0.0 | Yes | Set the bidding price. More information below.
| `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). | `exchange.name` | bittrex | Yes | Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
| `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode. | `exchange.key` | key | No | API key to use for the exchange. Only required when you are in production mode.
@ -41,10 +47,10 @@ The table below will list all configuration parameters.
| `strategy_path` | null | No | Adds an additional strategy lookup path (must be a folder). | `strategy_path` | null | No | Adds an additional strategy lookup path (must be a folder).
| `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second. | `internals.process_throttle_secs` | 5 | Yes | Set the process throttle. Value in second.
The definition of each config parameters is in The definition of each config parameters is in [misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L205).
[misc.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/misc.py#L205).
### Understand stake_amount ### Understand stake_amount
`stake_amount` is an amount of crypto-currency your bot will use for each trade. `stake_amount` is an amount of crypto-currency your bot will use for each trade.
The minimal value is 0.0005. If there is not enough crypto-currency in The minimal value is 0.0005. If there is not enough crypto-currency in
the account an exception is generated. the account an exception is generated.
@ -52,9 +58,11 @@ To allow the bot to trade all the avaliable `stake_currency` in your account set
In this case a trade amount is calclulated as `currency_balanse / (max_open_trades - current_open_trades)`. In this case a trade amount is calclulated as `currency_balanse / (max_open_trades - current_open_trades)`.
### Understand minimal_roi ### Understand minimal_roi
`minimal_roi` is a JSON object where the key is a duration `minimal_roi` is a JSON object where the key is a duration
in minutes and the value is the minimum ROI in percent. in minutes and the value is the minimum ROI in percent.
See the example below: See the example below:
``` ```
"minimal_roi": { "minimal_roi": {
"40": 0.0, # Sell after 40 minutes if the profit is not negative "40": 0.0, # Sell after 40 minutes if the profit is not negative
@ -69,6 +77,7 @@ value. This parameter is optional. If you use it, it will take over the
`minimal_roi` value from the strategy file. `minimal_roi` value from the strategy file.
### Understand stoploss ### Understand stoploss
`stoploss` is loss in percentage that should trigger a sale. `stoploss` is loss in percentage that should trigger a sale.
For example value `-0.10` will cause immediate sell if the For example value `-0.10` will cause immediate sell if the
profit dips below -10% for a given trade. This parameter is optional. profit dips below -10% for a given trade. This parameter is optional.
@ -77,82 +86,100 @@ Most of the strategy files already include the optimal `stoploss`
value. This parameter is optional. If you use it, it will take over the value. This parameter is optional. If you use it, it will take over the
`stoploss` value from the strategy file. `stoploss` value from the strategy file.
### Understand trailing stoploss
Go to the [trailing stoploss Documentation](stoploss.md) for details on trailing stoploss.
### Understand initial_state ### Understand initial_state
`initial_state` is an optional field that defines the initial application state. `initial_state` is an optional field that defines the initial application state.
Possible values are `running` or `stopped`. (default=`running`) Possible values are `running` or `stopped`. (default=`running`)
If the value is `stopped` the bot has to be started with `/start` first. If the value is `stopped` the bot has to be started with `/start` first.
### Understand process_throttle_secs ### Understand process_throttle_secs
`process_throttle_secs` is an optional field that defines in seconds how long the bot should wait `process_throttle_secs` is an optional field that defines in seconds how long the bot should wait
before asking the strategy if we should buy or a sell an asset. After each wait period, the strategy is asked again for before asking the strategy if we should buy or a sell an asset. After each wait period, the strategy is asked again for
every opened trade wether or not we should sell, and for all the remaining pairs (either the dynamic list of pairs or every opened trade wether or not we should sell, and for all the remaining pairs (either the dynamic list of pairs or
the static list of pairs) if we should buy. the static list of pairs) if we should buy.
### Understand ask_last_balance ### Understand ask_last_balance
`ask_last_balance` sets the bidding price. Value `0.0` will use `ask` price, `1.0` will `ask_last_balance` sets the bidding price. Value `0.0` will use `ask` price, `1.0` will
use the `last` price and values between those interpolate between ask and last use the `last` price and values between those interpolate between ask and last
price. Using `ask` price will guarantee quick success in bid, but bot will also price. Using `ask` price will guarantee quick success in bid, but bot will also
end up paying more then would probably have been necessary. end up paying more then would probably have been necessary.
### What values for exchange.name? ### What values for exchange.name?
Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115 cryptocurrency Freqtrade is based on [CCXT library](https://github.com/ccxt/ccxt) that supports 115 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). However, the bot was tested
with only Bittrex and Binance. with only Bittrex and Binance.
The bot was tested with the following exchanges: 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"
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.
### What values for fiat_display_currency? ### What values for fiat_display_currency?
`fiat_display_currency` set the base currency to use for the conversion from coin to fiat in Telegram. `fiat_display_currency` set the base currency to use for the conversion from coin to fiat in Telegram.
The valid values are: "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD". The valid values are: "AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK", "EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY", "KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN", "RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD".
In addition to central bank currencies, a range of cryto currencies are supported. In addition to central bank currencies, a range of cryto currencies are supported.
The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT". The valid values are: "BTC", "ETH", "XRP", "LTC", "BCH", "USDT".
## Switch to dry-run mode ## Switch to dry-run mode
We recommend starting the bot in dry-run mode to see how your bot will We recommend starting the bot in dry-run mode to see how your bot will
behave and how is the performance of your strategy. In Dry-run mode the behave and how is the performance of your strategy. In Dry-run mode the
bot does not engage your money. It only runs a live simulation without bot does not engage your money. It only runs a live simulation without
creating trades. creating trades.
### To switch your bot in Dry-run mode: ### To switch your bot in Dry-run mode:
1. Edit your `config.json` file 1. Edit your `config.json` file
2. Switch dry-run to true and specify db_url for a persistent db 2. Switch dry-run to true and specify db_url for a persistent db
```json ```json
"dry_run": true, "dry_run": true,
"db_url": "sqlite///tradesv3.dryrun.sqlite", "db_url": "sqlite///tradesv3.dryrun.sqlite",
``` ```
3. Remove your Exchange API key (change them by fake api credentials) 3. Remove your Exchange API key (change them by fake api credentials)
```json ```json
"exchange": { "exchange": {
"name": "bittrex", "name": "bittrex",
"key": "key", "key": "key",
"secret": "secret", "secret": "secret",
... ...
} }
``` ```
Once you will be happy with your bot performance, you can switch it to Once you will be happy with your bot performance, you can switch it to
production mode. production mode.
## Switch to production mode ## Switch to production mode
In production mode, the bot will engage your money. Be careful a wrong In production mode, the bot will engage your money. Be careful a wrong
strategy can lose all your money. Be aware of what you are doing when strategy can lose all your money. Be aware of what you are doing when
you run it in production mode. you run it in production mode.
### To switch your bot in production mode: ### To switch your bot in production mode:
1. Edit your `config.json` file 1. Edit your `config.json` file
2. Switch dry-run to false and don't forget to adapt your database URL if set 2. Switch dry-run to false and don't forget to adapt your database URL if set
```json ```json
"dry_run": false, "dry_run": false,
``` ```
3. Insert your Exchange API key (change them by fake api keys) 3. Insert your Exchange API key (change them by fake api keys)
```json ```json
"exchange": { "exchange": {
"name": "bittrex", "name": "bittrex",
@ -160,10 +187,10 @@ you run it in production mode.
"secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5", "secret": "08a9dc6db3d7b53e1acebd9275677f4b0a04f1a5",
... ...
} }
``` ```
If you have not your Bittrex API key yet, If you have not your Bittrex API key yet, [see our tutorial](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md).
[see our tutorial](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md).
## Next step ## Next step
Now you have configured your config.json, the next step is to
[start your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md). Now you have configured your config.json, the next step is to [start your bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-usage.md).

View File

@ -1,155 +1,114 @@
# Hyperopt # Hyperopt
This page explains how to tune your strategy by finding the optimal This page explains how to tune your strategy by finding the optimal
parameters with Hyperopt. parameters, a process called hyperparameter optimization. The bot uses several
algorithms included in the `scikit-optimize` package to accomplish this. The
search will burn all your CPU cores, make your laptop sound like a fighter jet
and still take a long time.
## Table of Contents ## Table of Contents
- [Prepare your Hyperopt](#prepare-hyperopt) - [Prepare your Hyperopt](#prepare-hyperopt)
- [1. Configure your Guards and Triggers](#1-configure-your-guards-and-triggers) - [Configure your Guards and Triggers](#configure-your-guards-and-triggers)
- [2. Update the hyperopt config file](#2-update-the-hyperopt-config-file) - [Solving a Mystery](#solving-a-mystery)
- [Advanced Hyperopt notions](#advanced-notions) - [Adding New Indicators](#adding-new-indicators)
- [Understand the Guards and Triggers](#understand-the-guards-and-triggers)
- [Execute Hyperopt](#execute-hyperopt) - [Execute Hyperopt](#execute-hyperopt)
- [Understand the hyperopts result](#understand-the-backtesting-result) - [Understand the hyperopts result](#understand-the-backtesting-result)
## Prepare Hyperopt ## Prepare Hyperopting
Before we start digging in Hyperopt, we recommend you to take a look at We recommend you start by taking a look at `hyperopt.py` file located in [freqtrade/optimize](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py)
your strategy file located into [user_data/strategies/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py)
### 1. Configure your Guards and Triggers ### Configure your Guards and Triggers
There are two places you need to change in your strategy file to add a There are two places you need to change to add a new buy strategy for testing:
new buy strategy for testing: - Inside [populate_buy_trend()](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L278-L294).
- Inside [populate_buy_trend()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L278-L294). - Inside [hyperopt_space()](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/optimize/hyperopt.py#L218-L229)
- Inside [hyperopt_space()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297) known as `SPACE`. and the associated methods `indicator_space`, `roi_space`, `stoploss_space`.
There you have two different type of indicators: 1. `guards` and 2. There you have two different type of indicators: 1. `guards` and 2. `triggers`.
`triggers`. 1. Guards are conditions like "never buy if ADX < 10", or "never buy if
1. Guards are conditions like "never buy if ADX < 10", or never buy if current price is over EMA10".
current price is over EMA10.
2. Triggers are ones that actually trigger buy in specific moment, like 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 "buy when EMA5 crosses over EMA10" or "buy when close price touches lower
bollinger band. bollinger band".
HyperOpt will, for each eval round, pick just ONE trigger, and possibly Hyperoptimization will, for each eval round, pick one trigger and possibly
multiple guards. So that 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, ie. changed the contents of
If you have updated the buy strategy, means change the content of
`populate_buy_trend()` method you have to update the `guards` and `populate_buy_trend()` method you have to update the `guards` and
`triggers` hyperopts must used. `triggers` hyperopts must use.
As for an example if your `populate_buy_trend()` method is: ## Solving a Mystery
```python
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
dataframe.loc[
(dataframe['rsi'] < 35) &
(dataframe['adx'] > 65),
'buy'] = 1
return dataframe Let's say you are curious: should you use MACD crossings or lower Bollinger
``` Bands to trigger your buys. And you also wonder should you use RSI or ADX to
help with those buy decisions. If you decide to use RSI or ADX, which values
should I use for them? So let's use hyperparameter optimization to solve this
mystery.
Your hyperopt file must contain `guards` to find the right value for We will start by defining a search space:
`(dataframe['adx'] > 65)` & and `(dataframe['plus_di'] > 0.5)`. That
means you will need to enable/disable triggers.
In our case the `SPACE` and `populate_buy_trend` in your strategy file
will look like:
```python
space = {
'rsi': hp.choice('rsi', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
]),
'adx': hp.choice('adx', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('adx-value', 15, 50, 1)}
]),
'trigger': hp.choice('trigger', [
{'type': 'lower_bb'},
{'type': 'faststoch10'},
{'type': 'ao_cross_zero'},
{'type': 'ema5_cross_ema10'},
{'type': 'macd_cross_signal'},
{'type': 'sar_reversal'},
{'type': 'stochf_cross'},
{'type': 'ht_sine'},
]),
}
...
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
conditions = []
# GUARDS AND TRENDS
if params['adx']['enabled']:
conditions.append(dataframe['adx'] > params['adx']['value'])
if params['rsi']['enabled']:
conditions.append(dataframe['rsi'] < params['rsi']['value'])
# TRIGGERS
triggers = {
'lower_bb': dataframe['tema'] <= dataframe['blower'],
'faststoch10': (crossed_above(dataframe['fastd'], 10.0)),
'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)),
'ema5_cross_ema10': (crossed_above(dataframe['ema5'], dataframe['ema10'])),
'macd_cross_signal': (crossed_above(dataframe['macd'], dataframe['macdsignal'])),
'sar_reversal': (crossed_above(dataframe['close'], dataframe['sar'])),
'stochf_cross': (crossed_above(dataframe['fastk'], dataframe['fastd'])),
'ht_sine': (crossed_above(dataframe['htleadsine'], dataframe['htsine'])),
}
...
```
### 2. Update the hyperopt config file
Hyperopt is using a dedicated config file. Currently hyperopt
cannot use your config file. It is also made on purpose to allow you
testing your strategy with different configurations.
The Hyperopt configuration is located in
[user_data/hyperopt_conf.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopt_conf.py).
## Advanced notions
### Understand the Guards and Triggers
When you need to add the new guards and triggers to be hyperopt
parameters, you do this by adding them into the [hyperopt_space()](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py#L244-L297).
If it's a trigger, you add one line to the 'trigger' choice group and that's it.
If it's a guard, you will add a line like this:
```
'rsi': hp.choice('rsi', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 1)}
]),
```
This says, "*one of the guards is RSI, it can have two values, enabled or
disabled. If it is enabled, try different values for it between 20 and 40*".
So, the part of the strategy builder using the above setting looks like
this:
``` ```
if params['rsi']['enabled']: def indicator_space() -> List[Dimension]:
conditions.append(dataframe['rsi'] < params['rsi']['value']) """
Define your Hyperopt space for searching strategy parameters
"""
return [
Integer(20, 40, name='adx-value'),
Integer(20, 40, name='rsi-value'),
Categorical([True, False], name='adx-enabled'),
Categorical([True, False], name='rsi-enabled'),
Categorical(['bb_lower', 'macd_cross_signal'], name='trigger')
]
``` ```
It checks if Hyperopt wants the RSI guard to be enabled for this Above definition says: I have five parameters I want you to randomly combine
round `params['rsi']['enabled']` and if it is, then it will add a to find the best combination. Two of them are integer values (`adx-value`
condition that says RSI must be smaller than the value hyperopt picked and `rsi-value`) and I want you test in the range of values 20 to 40.
for this evaluation, which is given in the `params['rsi']['value']`. Then we have three category variables. First two are either `True` or `False`.
We use these to either enable or disable the ADX and RSI guards. The last
one we call `trigger` and use it to decide which buy trigger we want to use.
That's it. Now you can add new parts of strategies to Hyperopt and it So let's write the buy strategy using these values:
will try all the combinations with all different values in the search
for best working algo.
```
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
conditions = []
# GUARDS AND TRENDS
if 'adx-enabled' in params and params['adx-enabled']:
conditions.append(dataframe['adx'] > params['adx-value'])
if 'rsi-enabled' in params and params['rsi-enabled']:
conditions.append(dataframe['rsi'] < params['rsi-value'])
### Add a new Indicators # TRIGGERS
If you want to test an indicator that isn't used by the bot currently, if params['trigger'] == 'bb_lower':
you need to add it to the `populate_indicators()` method in `hyperopt.py`. conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
if params['trigger'] == 'macd_cross_signal':
conditions.append(qtpylib.crossed_above(
dataframe['macd'], dataframe['macdsignal']
))
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'buy'] = 1
return dataframe
return populate_buy_trend
```
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
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.
The search for best parameters starts with a few random combinations and then uses a
regressor algorithm (currently ExtraTreesRegressor) to quickly find a parameter combination
that minimizes the value of the objective function `calculate_loss` in `hyperopt.py`.
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
add it to the `populate_indicators()` method in `hyperopt.py`.
## Execute Hyperopt ## Execute Hyperopt
Once you have updated your hyperopt configuration you can run it. Once you have updated your hyperopt configuration you can run it.
@ -164,12 +123,12 @@ python3 ./freqtrade/main.py -c config.json hyperopt -e 5000
The `-e` flag will set how many evaluations hyperopt will do. We recommend The `-e` flag will set how many evaluations hyperopt will do. We recommend
running at least several thousand evaluations. running at least several thousand evaluations.
### Execute hyperopt with different ticker-data source ### Execute Hyperopt with Different Ticker-Data Source
If you would like to hyperopt parameters using an alternate ticker data that If you would like to hyperopt parameters using an alternate ticker data that
you have on-disk, use the `--datadir PATH` option. Default hyperopt will you have on-disk, use the `--datadir PATH` option. Default hyperopt will
use data from directory `user_data/data`. use data from directory `user_data/data`.
### Running hyperopt with smaller testset ### Running Hyperopt with Smaller Testset
Use the `--timeperiod` argument to change how much of the testset Use the `--timeperiod` argument to change how much of the testset
you want to use. The last N ticks/timeframes will be used. you want to use. The last N ticks/timeframes will be used.
Example: Example:
@ -178,7 +137,7 @@ Example:
python3 ./freqtrade/main.py hyperopt --timeperiod -200 python3 ./freqtrade/main.py hyperopt --timeperiod -200
``` ```
### Running hyperopt with smaller search space ### Running Hyperopt with Smaller Search Space
Use the `--spaces` argument to limit the search space used by hyperopt. Use the `--spaces` argument to limit the search space used by hyperopt.
Letting Hyperopt optimize everything is a huuuuge search space. Often it Letting Hyperopt optimize everything is a huuuuge search space. Often it
might make more sense to start by just searching for initial buy algorithm. might make more sense to start by just searching for initial buy algorithm.
@ -193,87 +152,44 @@ Legal values are:
- `stoploss`: search for the best stoploss value - `stoploss`: search for the best stoploss value
- space-separated list of any of the above values for example `--spaces roi stoploss` - space-separated list of any of the above values for example `--spaces roi stoploss`
## Understand the hyperopts result ## Understand the Hyperopts Result
Once Hyperopt is completed you can use the result to adding new buy Once Hyperopt is completed you can use the result to create a new strategy.
signal. Given following result from hyperopt: Given the following result from hyperopt:
```
Best parameters:
{
"adx": {
"enabled": true,
"value": 15.0
},
"fastd": {
"enabled": true,
"value": 40.0
},
"green_candle": {
"enabled": true
},
"mfi": {
"enabled": false
},
"over_sar": {
"enabled": false
},
"rsi": {
"enabled": true,
"value": 37.0
},
"trigger": {
"type": "lower_bb"
},
"uptrend_long_ema": {
"enabled": true
},
"uptrend_short_ema": {
"enabled": false
},
"uptrend_sma": {
"enabled": false
}
}
Best Result: ```
2197 trades. Avg profit 1.84%. Total profit 0.79367541 BTC. Avg duration 241.0 mins. Best result:
135 trades. Avg profit 0.57%. Total profit 0.03871918 BTC (0.7722Σ%). Avg duration 180.4 mins.
with values:
{'adx-value': 44, 'rsi-value': 29, 'adx-enabled': False, 'rsi-enabled': True, 'trigger': 'bb_lower'}
``` ```
You should understand this result like: You should understand this result like:
- You should **consider** the guard "adx" (`"adx"` is `"enabled": true`) - The buy trigger that worked best was `bb_lower`.
and the best value is `15.0` (`"value": 15.0,`) - You should not use ADX because `adx-enabled: False`)
- You should **consider** the guard "fastd" (`"fastd"` is `"enabled": - You should **consider** using the RSI indicator (`rsi-enabled: True` and the best value is `29.0` (`rsi-value: 29.0`)
true`) and the best value is `40.0` (`"value": 40.0,`)
- You should **consider** to enable the guard "green_candle"
(`"green_candle"` is `"enabled": true`) but this guards as no
customizable value.
- You should **ignore** the guard "mfi" (`"mfi"` is `"enabled": false`)
- and so on...
You have to look inside your strategy file into `buy_strategy_generator()` You have to look inside your strategy file into `buy_strategy_generator()`
method, what those values match to. method, what those values match to.
So for example you had `adx:` with the `value: 15.0` so we would look So for example you had `rsi-value: 29.0` so we would look
at `adx`-block, that translates to the following code block: at `rsi`-block, that translates to the following code block:
``` ```
(dataframe['adx'] > 15.0) (dataframe['rsi'] < 29.0)
``` ```
Translating your whole hyperopt result to as the new buy-signal Translating your whole hyperopt result as the new buy-signal
would be the following: would then look like:
``` ```
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
dataframe.loc[ dataframe.loc[
( (
(dataframe['adx'] > 15.0) & # adx-value (dataframe['rsi'] < 29.0) & # rsi-value
(dataframe['fastd'] < 40.0) & # fastd-value dataframe['close'] < dataframe['bb_lowerband'] # trigger
(dataframe['close'] > dataframe['open']) & # green_candle
(dataframe['rsi'] < 37.0) & # rsi-value
(dataframe['ema50'] > dataframe['ema100']) # uptrend_long_ema
), ),
'buy'] = 1 'buy'] = 1
return dataframe return dataframe
``` ```
## Next step ## Next Step
Now you have a perfect bot and want to control it from Telegram. Your Now you have a perfect bot and want to control it from Telegram. Your
next step is to learn the [Telegram usage](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md). next step is to learn the [Telegram usage](https://github.com/freqtrade/freqtrade/blob/develop/docs/telegram-usage.md).

View File

@ -1,4 +1,5 @@
# freqtrade documentation # freqtrade documentation
Welcome to freqtrade documentation. Please feel free to contribute to Welcome to freqtrade documentation. Please feel free to contribute to
this documentation if you see it became outdated by sending us a this documentation if you see it became outdated by sending us a
Pull-request. Do not hesitate to reach us on Pull-request. Do not hesitate to reach us on
@ -6,6 +7,7 @@ Pull-request. Do not hesitate to reach us on
if you do not find the answer to your questions. if you do not find the answer to your questions.
## Table of Contents ## Table of Contents
- [Pre-requisite](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md) - [Pre-requisite](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md)
- [Setup your Bittrex account](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-bittrex-account) - [Setup your Bittrex account](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-bittrex-account)
- [Setup your Telegram bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-telegram-bot) - [Setup your Telegram bot](https://github.com/freqtrade/freqtrade/blob/develop/docs/pre-requisite.md#setup-your-telegram-bot)

48
docs/stoploss.md Normal file
View File

@ -0,0 +1,48 @@
# Stop Loss support
At this stage the bot contains the following stoploss support modes:
1. static stop loss, defined in either the strategy or configuration
2. trailing stop loss, defined in the configuration
3. trailing stop loss, custom positive loss, defined in configuration
## Static Stop Loss
This is very simple, basically you define a stop loss of x in your strategy file or alternative in the configuration, which
will overwrite the strategy definition. This will basically try to sell your asset, the second the loss exceeds the defined loss.
## Trail Stop Loss
The initial value for this stop loss, is defined in your strategy or configuration. Just as you would define your Stop Loss normally.
To enable this Feauture all you have to do is to define the configuration element:
``` json
"trailing_stop" : True
```
This will now activate an algorithm, which automatically moves your stop loss up every time the price of your asset increases.
For example, simplified math,
* you buy an asset at a price of 100$
* your stop loss is defined at 2%
* which means your stop loss, gets triggered once your asset dropped below 98$
* assuming your asset now increases to 102$
* your stop loss, will now be 2% of 102$ or 99.96$
* now your asset drops in value to 101$, your stop loss, will still be 99.96$
basically what this means is that your stop loss will be adjusted to be always be 2% of the highest observed price
### Custom positive loss
Due to demand, it is possible to have a default stop loss, when you are in the red with your buy, but once your buy turns positive,
the system will utilize a new stop loss, which can be a different value. For example your default stop loss is 5%, but once you are in the
black, it will be changed to be only a 1% stop loss
This can be configured in the main configuration file and requires `"trailing_stop": true` to be set to true.
``` json
"trailing_stop_positive": 0.01,
```
The 0.01 would translate to a 1% stop loss, once you hit profit.

View File

@ -1,5 +1,5 @@
""" FreqTrade bot """ """ FreqTrade bot """
__version__ = '0.17.0' __version__ = '0.17.1'
class DependencyException(BaseException): class DependencyException(BaseException):

View File

@ -7,8 +7,8 @@ To launch Freqtrade as a module
""" """
import sys import sys
from freqtrade import main
from freqtrade import main
if __name__ == '__main__': if __name__ == '__main__':
main.set_loggers() main.set_loggers()

View File

@ -12,8 +12,7 @@ from pandas import DataFrame, to_datetime
from freqtrade import constants from freqtrade import constants
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.strategy.resolver import StrategyResolver, IStrategy from freqtrade.strategy.resolver import IStrategy, StrategyResolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -180,7 +179,7 @@ class Analyze(object):
:return: True if trade should be sold, False otherwise :return: True if trade should be sold, False otherwise
""" """
current_profit = trade.calc_profit_percent(rate) current_profit = trade.calc_profit_percent(rate)
if self.stop_loss_reached(current_profit=current_profit): if self.stop_loss_reached(current_rate=rate, trade=trade, current_time=date):
return True return True
experimental = self.config.get('experimental', {}) experimental = self.config.get('experimental', {})
@ -204,12 +203,46 @@ class Analyze(object):
return False return False
def stop_loss_reached(self, current_profit: float) -> bool: def stop_loss_reached(self, current_rate: float, trade: Trade, current_time: datetime) -> bool:
"""Based on current profit of the trade and configured stoploss, decides to sell or not""" """
Based on current profit of the trade and configured (trailing) stoploss,
decides to sell or not
"""
current_profit = trade.calc_profit_percent(current_rate)
trailing_stop = self.config.get('trailing_stop', False)
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
# evaluate if the stoploss was hit
if self.strategy.stoploss is not None and trade.stop_loss >= current_rate:
if trailing_stop:
logger.debug(
f"HIT STOP: current price at {current_rate:.6f}, "
f"stop loss is {trade.stop_loss:.6f}, "
f"initial stop loss was at {trade.initial_stop_loss:.6f}, "
f"trade opened at {trade.open_rate:.6f}")
logger.debug(f"trailing stop saved {trade.stop_loss - trade.initial_stop_loss:.6f}")
if self.strategy.stoploss is not None and current_profit < self.strategy.stoploss:
logger.debug('Stop loss hit.') logger.debug('Stop loss hit.')
return True return True
# update the stop loss afterwards, after all by definition it's supposed to be hanging
if trailing_stop:
# check if we have a special stop loss for positive condition
# and if profit is positive
stop_loss_value = self.strategy.stoploss
if 'trailing_stop_positive' in self.config and current_profit > 0:
# Ignore mypy error check in configuration that this is a float
stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore
logger.debug(f"using positive stop loss mode: {stop_loss_value} "
f"since we have profit {current_profit}")
trade.adjust_stop_loss(current_rate, stop_loss_value)
return False return False
def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool: def min_roi_reached(self, trade: Trade, current_profit: float, current_time: datetime) -> bool:

View File

@ -2,12 +2,13 @@
This module contains the argument manager class This module contains the argument manager class
""" """
import os
import argparse import argparse
import logging import logging
import os
import re import re
from typing import List, NamedTuple, Optional
import arrow import arrow
from typing import List, Optional, NamedTuple
from freqtrade import __version__, constants from freqtrade import __version__, constants
@ -334,3 +335,10 @@ class Arguments(object):
nargs='+', nargs='+',
dest='timeframes', dest='timeframes',
) )
self.parser.add_argument(
'--erase',
help='Clean all existing data for the selected exchange/pairs/timeframes',
dest='erase',
action='store_true'
)

View File

@ -1,18 +1,18 @@
""" """
This module contains the configuration class This module contains the configuration class
""" """
import os
import json import json
import logging import logging
import os
from argparse import Namespace from argparse import Namespace
from typing import Optional, Dict, Any from typing import Any, Dict, Optional
import ccxt
from jsonschema import Draft4Validator, validate from jsonschema import Draft4Validator, validate
from jsonschema.exceptions import ValidationError, best_match from jsonschema.exceptions import ValidationError, best_match
import ccxt
from freqtrade import OperationalException, constants from freqtrade import OperationalException, constants
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -62,8 +62,8 @@ class Configuration(object):
conf = json.load(file) conf = json.load(file)
except FileNotFoundError: except FileNotFoundError:
raise OperationalException( raise OperationalException(
'Config file "{}" not found!' f'Config file "{path}" not found!'
' Please create a config file or check whether it exists.'.format(path)) ' Please create a config file or check whether it exists.')
if 'internals' not in conf: if 'internals' not in conf:
conf['internals'] = {} conf['internals'] = {}
@ -109,7 +109,7 @@ class Configuration(object):
config['db_url'] = constants.DEFAULT_DB_PROD_URL config['db_url'] = constants.DEFAULT_DB_PROD_URL
logger.info('Dry run is disabled') logger.info('Dry run is disabled')
logger.info('Using DB: "{}"'.format(config['db_url'])) logger.info(f'Using DB: "{config["db_url"]}"')
# Check if the exchange set by the user is supported # Check if the exchange set by the user is supported
self.check_exchange(config) self.check_exchange(config)

View File

@ -61,7 +61,15 @@ CONF_SCHEMA = {
'minProperties': 1 'minProperties': 1
}, },
'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True}, 'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True},
'unfilledtimeout': {'type': 'integer', 'minimum': 0}, 'trailing_stop': {'type': 'boolean'},
'trailing_stop_positive': {'type': 'number', 'minimum': 0, 'maximum': 1},
'unfilledtimeout': {
'type': 'object',
'properties': {
'buy': {'type': 'number', 'minimum': 3},
'sell': {'type': 'number', 'minimum': 10}
}
},
'bid_strategy': { 'bid_strategy': {
'type': 'object', 'type': 'object',
'properties': { 'properties': {

View File

@ -7,10 +7,12 @@ import logging
import time import time
from typing import Dict, List from typing import Dict, List
from coinmarketcap import Market
from requests.exceptions import RequestException from requests.exceptions import RequestException
from coinmarketcap import Market
from freqtrade.constants import SUPPORTED_FIAT from freqtrade.constants import SUPPORTED_FIAT
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -7,16 +7,14 @@ import logging
import time import time
import traceback import traceback
from datetime import datetime from datetime import datetime
from typing import Dict, List, Optional, Any, Callable from typing import Any, Callable, Dict, List, Optional
import arrow import arrow
import requests import requests
from cachetools import TTLCache, cached from cachetools import TTLCache, cached
from freqtrade import ( from freqtrade import (DependencyException, OperationalException,
DependencyException, OperationalException, TemporaryError, persistence, __version__, TemporaryError, __version__, constants, persistence)
)
from freqtrade import constants
from freqtrade.analyze import Analyze from freqtrade.analyze import Analyze
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.fiat_convert import CryptoToFiatConverter from freqtrade.fiat_convert import CryptoToFiatConverter
@ -160,7 +158,7 @@ class FreqtradeBot(object):
if 'unfilledtimeout' in self.config: if 'unfilledtimeout' in self.config:
# Check and handle any timed out open orders # Check and handle any timed out open orders
self.check_handle_timedout(self.config['unfilledtimeout']) self.check_handle_timedout()
Trade.session.flush() Trade.session.flush()
except TemporaryError as error: except TemporaryError as error:
@ -277,11 +275,14 @@ class FreqtradeBot(object):
return None return None
min_stake_amounts = [] min_stake_amounts = []
if 'cost' in market['limits'] and 'min' in market['limits']['cost']: limits = market['limits']
min_stake_amounts.append(market['limits']['cost']['min']) if ('cost' in limits and 'min' in limits['cost']
and limits['cost']['min'] is not None):
min_stake_amounts.append(limits['cost']['min'])
if 'amount' in market['limits'] and 'min' in market['limits']['amount']: if ('amount' in limits and 'min' in limits['amount']
min_stake_amounts.append(market['limits']['amount']['min'] * price) and limits['amount']['min'] is not None):
min_stake_amounts.append(limits['amount']['min'] * price)
if not min_stake_amounts: if not min_stake_amounts:
return None return None
@ -492,13 +493,16 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
logger.info('Found no sell signals for whitelisted currencies. Trying again..') logger.info('Found no sell signals for whitelisted currencies. Trying again..')
return False return False
def check_handle_timedout(self, timeoutvalue: int) -> None: def check_handle_timedout(self) -> None:
""" """
Check if any orders are timed out and cancel if neccessary Check if any orders are timed out and cancel if neccessary
:param timeoutvalue: Number of minutes until order is considered timed out :param timeoutvalue: Number of minutes until order is considered timed out
:return: None :return: None
""" """
timeoutthreashold = arrow.utcnow().shift(minutes=-timeoutvalue).datetime buy_timeout = self.config['unfilledtimeout']['buy']
sell_timeout = self.config['unfilledtimeout']['sell']
buy_timeoutthreashold = arrow.utcnow().shift(minutes=-buy_timeout).datetime
sell_timeoutthreashold = arrow.utcnow().shift(minutes=-sell_timeout).datetime
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all(): for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
try: try:
@ -521,10 +525,12 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
if int(order['remaining']) == 0: if int(order['remaining']) == 0:
continue continue
if order['side'] == 'buy' and ordertime < timeoutthreashold: # Check if trade is still actually open
self.handle_timedout_limit_buy(trade, order) if order['status'] == 'open':
elif order['side'] == 'sell' and ordertime < timeoutthreashold: if order['side'] == 'buy' and ordertime < buy_timeoutthreashold:
self.handle_timedout_limit_sell(trade, order) self.handle_timedout_limit_buy(trade, order)
elif order['side'] == 'sell' and ordertime < sell_timeoutthreashold:
self.handle_timedout_limit_sell(trade, order)
# FIX: 20180110, why is cancel.order unconditionally here, whereas # FIX: 20180110, why is cancel.order unconditionally here, whereas
# it is conditionally called in the # it is conditionally called in the
@ -620,12 +626,8 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
# Because telegram._forcesell does not have the configuration # Because telegram._forcesell does not have the configuration
# Ignore the FIAT value and does not show the stake_currency as well # Ignore the FIAT value and does not show the stake_currency as well
else: else:
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f})`'.format( gain = "profit" if fmt_exp_profit > 0 else "loss"
gain="profit" if fmt_exp_profit > 0 else "loss", message += f'` ({gain}: {fmt_exp_profit:.2f}%, {profit_trade:.8f})`'
profit_percent=fmt_exp_profit,
profit_coin=profit_trade
)
# Send the message # Send the message
self.rpc.send_msg(message) self.rpc.send_msg(message)
Trade.session.flush() Trade.session.flush()

View File

@ -1,4 +1,4 @@
from math import exp, pi, sqrt, cos from math import cos, exp, pi, sqrt
import numpy as np import numpy as np
import talib as ta import talib as ta

View File

@ -74,10 +74,7 @@ def reconfigure(freqtrade: FreqtradeBot, args: Namespace) -> FreqtradeBot:
# Create new instance # Create new instance
freqtrade = FreqtradeBot(Configuration(args).get_config()) freqtrade = FreqtradeBot(Configuration(args).get_config())
freqtrade.rpc.send_msg( freqtrade.rpc.send_msg(
'*Status:* `Config reloaded ...`'.format( '*Status:* `Config reloaded {freqtrade.state.name.lower()}...`')
freqtrade.state.name.lower()
)
)
return freqtrade return freqtrade

View File

@ -2,10 +2,10 @@
Various tool function for Freqtrade and scripts Various tool function for Freqtrade and scripts
""" """
import gzip
import json import json
import logging import logging
import re import re
import gzip
from datetime import datetime from datetime import datetime
from typing import Dict from typing import Dict

View File

@ -54,11 +54,8 @@ def load_tickerdata_file(
:return dict OR empty if unsuccesful :return dict OR empty if unsuccesful
""" """
path = make_testdata_path(datadir) path = make_testdata_path(datadir)
pair_file_string = pair.replace('/', '_') pair_s = pair.replace('/', '_')
file = os.path.join(path, '{pair}-{ticker_interval}.json'.format( file = os.path.join(path, f'{pair_s}-{ticker_interval}.json')
pair=pair_file_string,
ticker_interval=ticker_interval,
))
gzipfile = file + '.gz' gzipfile = file + '.gz'
# If the file does not exist we download it when None is returned. # If the file does not exist we download it when None is returned.

View File

@ -7,18 +7,18 @@ import logging
import operator import operator
from argparse import Namespace from argparse import Namespace
from datetime import datetime from datetime import datetime
from typing import Dict, Tuple, Any, List, Optional, NamedTuple from typing import Any, Dict, List, NamedTuple, Optional, Tuple
import arrow import arrow
from pandas import DataFrame from pandas import DataFrame
from tabulate import tabulate from tabulate import tabulate
import freqtrade.optimize as optimize import freqtrade.optimize as optimize
from freqtrade import constants, DependencyException from freqtrade import DependencyException, constants
from freqtrade.exchange import Exchange
from freqtrade.analyze import Analyze from freqtrade.analyze import Analyze
from freqtrade.arguments import Arguments from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration from freqtrade.configuration import Configuration
from freqtrade.exchange import Exchange
from freqtrade.misc import file_dump_json from freqtrade.misc import file_dump_json
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
@ -38,6 +38,8 @@ class BacktestResult(NamedTuple):
close_index: int close_index: int
trade_duration: float trade_duration: float
open_at_end: bool open_at_end: bool
open_rate: float
close_rate: float
class Backtesting(object): class Backtesting(object):
@ -116,11 +118,10 @@ class Backtesting(object):
def _store_backtest_result(self, recordfilename: Optional[str], results: DataFrame) -> None: def _store_backtest_result(self, recordfilename: Optional[str], results: DataFrame) -> None:
records = [(trade_entry.pair, trade_entry.profit_percent, records = [(t.pair, t.profit_percent, t.open_time.timestamp(),
trade_entry.open_time.timestamp(), t.close_time.timestamp(), t.open_index - 1, t.trade_duration,
trade_entry.close_time.timestamp(), t.open_rate, t.close_rate, t.open_at_end)
trade_entry.open_index - 1, trade_entry.trade_duration) for index, t in results.iterrows()]
for index, trade_entry in results.iterrows()]
if records: if records:
logger.info('Dumping backtest results to %s', recordfilename) logger.info('Dumping backtest results to %s', recordfilename)
@ -159,7 +160,9 @@ class Backtesting(object):
trade_duration=(sell_row.date - buy_row.date).seconds // 60, trade_duration=(sell_row.date - buy_row.date).seconds // 60,
open_index=buy_row.Index, open_index=buy_row.Index,
close_index=sell_row.Index, close_index=sell_row.Index,
open_at_end=False open_at_end=False,
open_rate=buy_row.close,
close_rate=sell_row.close
) )
if partial_ticker: if partial_ticker:
# no sell condition found - trade stil open at end of backtest period # no sell condition found - trade stil open at end of backtest period
@ -172,7 +175,9 @@ class Backtesting(object):
trade_duration=(sell_row.date - buy_row.date).seconds // 60, trade_duration=(sell_row.date - buy_row.date).seconds // 60,
open_index=buy_row.Index, open_index=buy_row.Index,
close_index=sell_row.Index, close_index=sell_row.Index,
open_at_end=True open_at_end=True,
open_rate=buy_row.close,
close_rate=sell_row.close
) )
logger.debug('Force_selling still open trade %s with %s perc - %s', btr.pair, logger.debug('Force_selling still open trade %s with %s perc - %s', btr.pair,
btr.profit_percent, btr.profit_abs) btr.profit_percent, btr.profit_abs)

View File

@ -4,22 +4,21 @@
This module contains the hyperopt logic This module contains the hyperopt logic
""" """
import json
import logging import logging
import multiprocessing
import os import os
import pickle
import signal
import sys import sys
from argparse import Namespace from argparse import Namespace
from functools import reduce from functools import reduce
from math import exp from math import exp
from operator import itemgetter from operator import itemgetter
from typing import Dict, Any, Callable, Optional from typing import Any, Callable, Dict, List
import numpy
import talib.abstract as ta import talib.abstract as ta
from hyperopt import STATUS_FAIL, STATUS_OK, Trials, fmin, hp, space_eval, tpe
from pandas import DataFrame from pandas import DataFrame
from sklearn.externals.joblib import Parallel, delayed, dump, load
from skopt import Optimizer
from skopt.space import Categorical, Dimension, Integer, Real
import freqtrade.vendor.qtpylib.indicators as qtpylib import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.arguments import Arguments from freqtrade.arguments import Arguments
@ -29,6 +28,9 @@ from freqtrade.optimize.backtesting import Backtesting
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization
TICKERDATA_PICKLE = os.path.join('user_data', 'hyperopt_tickerdata.pkl')
class Hyperopt(Backtesting): class Hyperopt(Backtesting):
""" """
@ -44,7 +46,6 @@ class Hyperopt(Backtesting):
# to the number of days # to the number of days
self.target_trades = 600 self.target_trades = 600
self.total_tries = config.get('epochs', 0) self.total_tries = config.get('epochs', 0)
self.current_tries = 0
self.current_best_loss = 100 self.current_best_loss = 100
# max average trade duration in minutes # max average trade duration in minutes
@ -56,130 +57,38 @@ class Hyperopt(Backtesting):
# check that the reported Σ% values do not exceed this! # check that the reported Σ% values do not exceed this!
self.expected_max_profit = 3.0 self.expected_max_profit = 3.0
# Configuration and data used by hyperopt # Previous evaluations
self.processed: Optional[Dict[str, Any]] = None self.trials_file = os.path.join('user_data', 'hyperopt_results.pickle')
self.trials: List = []
# Hyperopt Trials def get_args(self, params):
self.trials_file = os.path.join('user_data', 'hyperopt_trials.pickle') dimensions = self.hyperopt_space()
self.trials = Trials() # Ensure the number of dimensions match
# the number of parameters in the list x.
if len(params) != len(dimensions):
raise ValueError('Mismatch in number of search-space dimensions. '
f'len(dimensions)=={len(dimensions)} and len(x)=={len(params)}')
# Create a dict where the keys are the names of the dimensions
# and the values are taken from the list of parameters x.
arg_dict = {dim.name: value for dim, value in zip(dimensions, params)}
return arg_dict
@staticmethod @staticmethod
def populate_indicators(dataframe: DataFrame) -> DataFrame: def populate_indicators(dataframe: DataFrame) -> DataFrame:
"""
Adds several different TA indicators to the given DataFrame
"""
dataframe['adx'] = ta.ADX(dataframe) dataframe['adx'] = ta.ADX(dataframe)
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
dataframe['cci'] = ta.CCI(dataframe)
macd = ta.MACD(dataframe) macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd'] dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal'] dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
dataframe['mfi'] = ta.MFI(dataframe) dataframe['mfi'] = ta.MFI(dataframe)
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
dataframe['roc'] = ta.ROC(dataframe)
dataframe['rsi'] = ta.RSI(dataframe) dataframe['rsi'] = ta.RSI(dataframe)
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
rsi = 0.1 * (dataframe['rsi'] - 50)
dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1)
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
# Stoch
stoch = ta.STOCH(dataframe)
dataframe['slowd'] = stoch['slowd']
dataframe['slowk'] = stoch['slowk']
# Stoch fast
stoch_fast = ta.STOCHF(dataframe) stoch_fast = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch_fast['fastd'] dataframe['fastd'] = stoch_fast['fastd']
dataframe['fastk'] = stoch_fast['fastk'] dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# Stoch RSI
stoch_rsi = ta.STOCHRSI(dataframe)
dataframe['fastd_rsi'] = stoch_rsi['fastd']
dataframe['fastk_rsi'] = stoch_rsi['fastk']
# Bollinger bands # Bollinger bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower'] dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_middleband'] = bollinger['mid']
dataframe['bb_upperband'] = bollinger['upper']
# EMA - Exponential Moving Average
dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
# SAR Parabolic
dataframe['sar'] = ta.SAR(dataframe) dataframe['sar'] = ta.SAR(dataframe)
# SMA - Simple Moving Average
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
# TEMA - Triple Exponential Moving Average
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
# Hilbert Transform Indicator - SineWave
hilbert = ta.HT_SINE(dataframe)
dataframe['htsine'] = hilbert['sine']
dataframe['htleadsine'] = hilbert['leadsine']
# Pattern Recognition - Bullish candlestick patterns
# ------------------------------------
"""
# Hammer: values [0, 100]
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
# Inverted Hammer: values [0, 100]
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
# Dragonfly Doji: values [0, 100]
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
# Piercing Line: values [0, 100]
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
# Morningstar: values [0, 100]
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
# Three White Soldiers: values [0, 100]
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
"""
# Pattern Recognition - Bearish candlestick patterns
# ------------------------------------
"""
# Hanging Man: values [0, 100]
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
# Shooting Star: values [0, 100]
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
# Gravestone Doji: values [0, 100]
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
# Dark Cloud Cover: values [0, 100]
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
# Evening Doji Star: values [0, 100]
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
# Evening Star: values [0, 100]
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
"""
# Pattern Recognition - Bullish/Bearish candlestick patterns
# ------------------------------------
"""
# Three Line Strike: values [0, -100, 100]
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
# Spinning Top: values [0, -100, 100]
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
# Engulfing: values [0, -100, 100]
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
# Harami: values [0, -100, 100]
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
# Three Outside Up/Down: values [0, -100, 100]
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
# Three Inside Up/Down: values [0, -100, 100]
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
"""
# Chart type
# ------------------------------------
# Heikinashi stategy
heikinashi = qtpylib.heikinashi(dataframe)
dataframe['ha_open'] = heikinashi['open']
dataframe['ha_close'] = heikinashi['close']
dataframe['ha_high'] = heikinashi['high']
dataframe['ha_low'] = heikinashi['low']
return dataframe return dataframe
@ -187,15 +96,16 @@ class Hyperopt(Backtesting):
""" """
Save hyperopt trials to file Save hyperopt trials to file
""" """
logger.info('Saving Trials to \'%s\'', self.trials_file) if self.trials:
pickle.dump(self.trials, open(self.trials_file, 'wb')) logger.info('Saving %d evaluations to \'%s\'', len(self.trials), self.trials_file)
dump(self.trials, self.trials_file)
def read_trials(self) -> Trials: def read_trials(self) -> List:
""" """
Read hyperopt trials file Read hyperopt trials file
""" """
logger.info('Reading Trials from \'%s\'', self.trials_file) logger.info('Reading Trials from \'%s\'', self.trials_file)
trials = pickle.load(open(self.trials_file, 'rb')) trials = load(self.trials_file)
os.remove(self.trials_file) os.remove(self.trials_file)
return trials return trials
@ -203,22 +113,27 @@ class Hyperopt(Backtesting):
""" """
Display Best hyperopt result Display Best hyperopt result
""" """
vals = json.dumps(self.trials.best_trial['misc']['vals'], indent=4) results = sorted(self.trials, key=itemgetter('loss'))
results = self.trials.best_trial['result']['result'] best_result = results[0]
logger.info('Best result:\n%s\nwith values:\n%s', results, vals) logger.info(
'Best result:\n%s\nwith values:\n%s',
best_result['result'],
best_result['params']
)
if 'roi_t1' in best_result['params']:
logger.info('ROI table:\n%s', self.generate_roi_table(best_result['params']))
def log_results(self, results) -> None: def log_results(self, results) -> None:
""" """
Log results if it is better than any previous evaluation Log results if it is better than any previous evaluation
""" """
if results['loss'] < self.current_best_loss: if results['loss'] < self.current_best_loss:
current = results['current_tries']
total = results['total_tries']
res = results['result']
loss = results['loss']
self.current_best_loss = results['loss'] self.current_best_loss = results['loss']
log_msg = '\n{:5d}/{}: {}. Loss {:.5f}'.format( log_msg = f'\n{current:5d}/{total}: {res}. Loss {loss:.5f}'
results['current_tries'],
results['total_tries'],
results['result'],
results['loss']
)
print(log_msg) print(log_msg)
else: else:
print('.', end='') print('.', end='')
@ -231,7 +146,8 @@ class Hyperopt(Backtesting):
trade_loss = 1 - 0.25 * exp(-(trade_count - self.target_trades) ** 2 / 10 ** 5.8) trade_loss = 1 - 0.25 * exp(-(trade_count - self.target_trades) ** 2 / 10 ** 5.8)
profit_loss = max(0, 1 - total_profit / self.expected_max_profit) profit_loss = max(0, 1 - total_profit / self.expected_max_profit)
duration_loss = 0.4 * min(trade_duration / self.max_accepted_trade_duration, 1) duration_loss = 0.4 * min(trade_duration / self.max_accepted_trade_duration, 1)
return trade_loss + profit_loss + duration_loss result = trade_loss + profit_loss + duration_loss
return result
@staticmethod @staticmethod
def generate_roi_table(params: Dict) -> Dict[int, float]: def generate_roi_table(params: Dict) -> Dict[int, float]:
@ -247,87 +163,44 @@ class Hyperopt(Backtesting):
return roi_table return roi_table
@staticmethod @staticmethod
def roi_space() -> Dict[str, Any]: def roi_space() -> List[Dimension]:
""" """
Values to search for each ROI steps Values to search for each ROI steps
""" """
return { return [
'roi_t1': hp.quniform('roi_t1', 10, 120, 20), Integer(10, 120, name='roi_t1'),
'roi_t2': hp.quniform('roi_t2', 10, 60, 15), Integer(10, 60, name='roi_t2'),
'roi_t3': hp.quniform('roi_t3', 10, 40, 10), Integer(10, 40, name='roi_t3'),
'roi_p1': hp.quniform('roi_p1', 0.01, 0.04, 0.01), Real(0.01, 0.04, name='roi_p1'),
'roi_p2': hp.quniform('roi_p2', 0.01, 0.07, 0.01), Real(0.01, 0.07, name='roi_p2'),
'roi_p3': hp.quniform('roi_p3', 0.01, 0.20, 0.01), Real(0.01, 0.20, name='roi_p3'),
} ]
@staticmethod @staticmethod
def stoploss_space() -> Dict[str, Any]: def stoploss_space() -> List[Dimension]:
""" """
Stoploss Value to search Stoploss search space
""" """
return { return [
'stoploss': hp.quniform('stoploss', -0.5, -0.02, 0.02), Real(-0.5, -0.02, name='stoploss'),
} ]
@staticmethod @staticmethod
def indicator_space() -> Dict[str, Any]: def indicator_space() -> List[Dimension]:
""" """
Define your Hyperopt space for searching strategy parameters Define your Hyperopt space for searching strategy parameters
""" """
return { return [
'macd_below_zero': hp.choice('macd_below_zero', [ Integer(10, 25, name='mfi-value'),
{'enabled': False}, Integer(15, 45, name='fastd-value'),
{'enabled': True} Integer(20, 50, name='adx-value'),
]), Integer(20, 40, name='rsi-value'),
'mfi': hp.choice('mfi', [ Categorical([True, False], name='mfi-enabled'),
{'enabled': False}, Categorical([True, False], name='fastd-enabled'),
{'enabled': True, 'value': hp.quniform('mfi-value', 10, 25, 5)} Categorical([True, False], name='adx-enabled'),
]), Categorical([True, False], name='rsi-enabled'),
'fastd': hp.choice('fastd', [ Categorical(['bb_lower', 'macd_cross_signal', 'sar_reversal'], name='trigger')
{'enabled': False}, ]
{'enabled': True, 'value': hp.quniform('fastd-value', 15, 45, 5)}
]),
'adx': hp.choice('adx', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('adx-value', 20, 50, 5)}
]),
'rsi': hp.choice('rsi', [
{'enabled': False},
{'enabled': True, 'value': hp.quniform('rsi-value', 20, 40, 5)}
]),
'uptrend_long_ema': hp.choice('uptrend_long_ema', [
{'enabled': False},
{'enabled': True}
]),
'uptrend_short_ema': hp.choice('uptrend_short_ema', [
{'enabled': False},
{'enabled': True}
]),
'over_sar': hp.choice('over_sar', [
{'enabled': False},
{'enabled': True}
]),
'green_candle': hp.choice('green_candle', [
{'enabled': False},
{'enabled': True}
]),
'uptrend_sma': hp.choice('uptrend_sma', [
{'enabled': False},
{'enabled': True}
]),
'trigger': hp.choice('trigger', [
{'type': 'lower_bb'},
{'type': 'lower_bb_tema'},
{'type': 'faststoch10'},
{'type': 'ao_cross_zero'},
{'type': 'ema3_cross_ema10'},
{'type': 'macd_cross_signal'},
{'type': 'sar_reversal'},
{'type': 'ht_sine'},
{'type': 'heiken_reversal_bull'},
{'type': 'di_cross'},
]),
}
def has_space(self, space: str) -> bool: def has_space(self, space: str) -> bool:
""" """
@ -337,17 +210,17 @@ class Hyperopt(Backtesting):
return True return True
return False return False
def hyperopt_space(self) -> Dict[str, Any]: def hyperopt_space(self) -> List[Dimension]:
""" """
Return the space to use during Hyperopt Return the space to use during Hyperopt
""" """
spaces: Dict = {} spaces: List[Dimension] = []
if self.has_space('buy'): if self.has_space('buy'):
spaces = {**spaces, **Hyperopt.indicator_space()} spaces += Hyperopt.indicator_space()
if self.has_space('roi'): if self.has_space('roi'):
spaces = {**spaces, **Hyperopt.roi_space()} spaces += Hyperopt.roi_space()
if self.has_space('stoploss'): if self.has_space('stoploss'):
spaces = {**spaces, **Hyperopt.stoploss_space()} spaces += Hyperopt.stoploss_space()
return spaces return spaces
@staticmethod @staticmethod
@ -361,63 +234,26 @@ class Hyperopt(Backtesting):
""" """
conditions = [] conditions = []
# GUARDS AND TRENDS # GUARDS AND TRENDS
if 'uptrend_long_ema' in params and params['uptrend_long_ema']['enabled']: if 'mfi-enabled' in params and params['mfi-enabled']:
conditions.append(dataframe['ema50'] > dataframe['ema100']) conditions.append(dataframe['mfi'] < params['mfi-value'])
if 'macd_below_zero' in params and params['macd_below_zero']['enabled']: if 'fastd-enabled' in params and params['fastd-enabled']:
conditions.append(dataframe['macd'] < 0) conditions.append(dataframe['fastd'] < params['fastd-value'])
if 'uptrend_short_ema' in params and params['uptrend_short_ema']['enabled']: if 'adx-enabled' in params and params['adx-enabled']:
conditions.append(dataframe['ema5'] > dataframe['ema10']) conditions.append(dataframe['adx'] > params['adx-value'])
if 'mfi' in params and params['mfi']['enabled']: if 'rsi-enabled' in params and params['rsi-enabled']:
conditions.append(dataframe['mfi'] < params['mfi']['value']) conditions.append(dataframe['rsi'] < params['rsi-value'])
if 'fastd' in params and params['fastd']['enabled']:
conditions.append(dataframe['fastd'] < params['fastd']['value'])
if 'adx' in params and params['adx']['enabled']:
conditions.append(dataframe['adx'] > params['adx']['value'])
if 'rsi' in params and params['rsi']['enabled']:
conditions.append(dataframe['rsi'] < params['rsi']['value'])
if 'over_sar' in params and params['over_sar']['enabled']:
conditions.append(dataframe['close'] > dataframe['sar'])
if 'green_candle' in params and params['green_candle']['enabled']:
conditions.append(dataframe['close'] > dataframe['open'])
if 'uptrend_sma' in params and params['uptrend_sma']['enabled']:
prevsma = dataframe['sma'].shift(1)
conditions.append(dataframe['sma'] > prevsma)
# TRIGGERS # TRIGGERS
triggers = { if params['trigger'] == 'bb_lower':
'lower_bb': ( conditions.append(dataframe['close'] < dataframe['bb_lowerband'])
dataframe['close'] < dataframe['bb_lowerband'] if params['trigger'] == 'macd_cross_signal':
), conditions.append(qtpylib.crossed_above(
'lower_bb_tema': (
dataframe['tema'] < dataframe['bb_lowerband']
),
'faststoch10': (qtpylib.crossed_above(
dataframe['fastd'], 10.0
)),
'ao_cross_zero': (qtpylib.crossed_above(
dataframe['ao'], 0.0
)),
'ema3_cross_ema10': (qtpylib.crossed_above(
dataframe['ema3'], dataframe['ema10']
)),
'macd_cross_signal': (qtpylib.crossed_above(
dataframe['macd'], dataframe['macdsignal'] dataframe['macd'], dataframe['macdsignal']
)), ))
'sar_reversal': (qtpylib.crossed_above( if params['trigger'] == 'sar_reversal':
conditions.append(qtpylib.crossed_above(
dataframe['close'], dataframe['sar'] dataframe['close'], dataframe['sar']
)), ))
'ht_sine': (qtpylib.crossed_above(
dataframe['htleadsine'], dataframe['htsine']
)),
'heiken_reversal_bull': (
(qtpylib.crossed_above(dataframe['ha_close'], dataframe['ha_open'])) &
(dataframe['ha_low'] == dataframe['ha_open'])
),
'di_cross': (qtpylib.crossed_above(
dataframe['plus_di'], dataframe['minus_di']
)),
}
conditions.append(triggers.get(params['trigger']['type']))
dataframe.loc[ dataframe.loc[
reduce(lambda x, y: x & y, conditions), reduce(lambda x, y: x & y, conditions),
@ -427,7 +263,9 @@ class Hyperopt(Backtesting):
return populate_buy_trend return populate_buy_trend
def generate_optimizer(self, params: Dict) -> Dict: def generate_optimizer(self, _params) -> Dict:
params = self.get_args(_params)
if self.has_space('roi'): if self.has_space('roi'):
self.analyze.strategy.minimal_roi = self.generate_roi_table(params) self.analyze.strategy.minimal_roi = self.generate_roi_table(params)
@ -437,10 +275,11 @@ class Hyperopt(Backtesting):
if self.has_space('stoploss'): if self.has_space('stoploss'):
self.analyze.strategy.stoploss = params['stoploss'] self.analyze.strategy.stoploss = params['stoploss']
processed = load(TICKERDATA_PICKLE)
results = self.backtest( results = self.backtest(
{ {
'stake_amount': self.config['stake_amount'], 'stake_amount': self.config['stake_amount'],
'processed': self.processed, 'processed': processed,
'realistic': self.config.get('realistic_simulation', False), 'realistic': self.config.get('realistic_simulation', False),
} }
) )
@ -450,30 +289,18 @@ class Hyperopt(Backtesting):
trade_count = len(results.index) trade_count = len(results.index)
trade_duration = results.trade_duration.mean() trade_duration = results.trade_duration.mean()
if trade_count == 0 or trade_duration > self.max_accepted_trade_duration: if trade_count == 0:
print('.', end='')
sys.stdout.flush()
return { return {
'status': STATUS_FAIL, 'loss': MAX_LOSS,
'loss': float('inf') 'params': params,
'result': result_explanation,
} }
loss = self.calculate_loss(total_profit, trade_count, trade_duration) loss = self.calculate_loss(total_profit, trade_count, trade_duration)
self.current_tries += 1
self.log_results(
{
'loss': loss,
'current_tries': self.current_tries,
'total_tries': self.total_tries,
'result': result_explanation,
}
)
return { return {
'loss': loss, 'loss': loss,
'status': STATUS_OK, 'params': params,
'result': result_explanation, 'result': result_explanation,
} }
@ -481,15 +308,37 @@ class Hyperopt(Backtesting):
""" """
Return the format result in a string Return the format result in a string
""" """
return ('{:6d} trades. Avg profit {: 5.2f}%. ' trades = len(results.index)
'Total profit {: 11.8f} {} ({:.4f}Σ%). Avg duration {:5.1f} mins.').format( avg_profit = results.profit_percent.mean() * 100.0
len(results.index), total_profit = results.profit_abs.sum()
results.profit_percent.mean() * 100.0, stake_cur = self.config['stake_currency']
results.profit_abs.sum(), profit = results.profit_percent.sum()
self.config['stake_currency'], duration = results.trade_duration.mean()
results.profit_percent.sum(),
results.trade_duration.mean(), return (f'{trades:6d} trades. Avg profit {avg_profit: 5.2f}%. '
) f'Total profit {total_profit: 11.8f} {stake_cur} '
f'({profit:.4f}Σ%). Avg duration {duration:5.1f} mins.')
def get_optimizer(self, cpu_count) -> Optimizer:
return Optimizer(
self.hyperopt_space(),
base_estimator="ET",
acq_optimizer="auto",
n_initial_points=30,
acq_optimizer_kwargs={'n_jobs': cpu_count}
)
def run_optimizer_parallel(self, parallel, asked) -> List:
return parallel(delayed(self.generate_optimizer)(v) for v in asked)
def load_previous_results(self):
""" read trials file if we have one """
if os.path.exists(self.trials_file) and os.path.getsize(self.trials_file) > 0:
self.trials = self.read_trials()
logger.info(
'Loaded %d previous evaluations from disk.',
len(self.trials)
)
def start(self) -> None: def start(self) -> None:
timerange = Arguments.parse_timerange(None if self.config.get( timerange = Arguments.parse_timerange(None if self.config.get(
@ -503,67 +352,35 @@ class Hyperopt(Backtesting):
if self.has_space('buy'): if self.has_space('buy'):
self.analyze.populate_indicators = Hyperopt.populate_indicators # type: ignore self.analyze.populate_indicators = Hyperopt.populate_indicators # type: ignore
self.processed = self.tickerdata_to_dataframe(data) dump(self.tickerdata_to_dataframe(data), TICKERDATA_PICKLE)
self.exchange = None # type: ignore
self.load_previous_results()
logger.info('Preparing Trials..') cpus = multiprocessing.cpu_count()
signal.signal(signal.SIGINT, self.signal_handler) logger.info(f'Found {cpus} CPU cores. Let\'s make them scream!')
# read trials file if we have one
if os.path.exists(self.trials_file) and os.path.getsize(self.trials_file) > 0:
self.trials = self.read_trials()
self.current_tries = len(self.trials.results)
self.total_tries += self.current_tries
logger.info(
'Continuing with trials. Current: %d, Total: %d',
self.current_tries,
self.total_tries
)
opt = self.get_optimizer(cpus)
EVALS = max(self.total_tries//cpus, 1)
try: try:
best_parameters = fmin( with Parallel(n_jobs=cpus) as parallel:
fn=self.generate_optimizer, for i in range(EVALS):
space=self.hyperopt_space(), asked = opt.ask(n_points=cpus)
algo=tpe.suggest, f_val = self.run_optimizer_parallel(parallel, asked)
max_evals=self.total_tries, opt.tell(asked, [i['loss'] for i in f_val])
trials=self.trials
)
results = sorted(self.trials.results, key=itemgetter('loss')) self.trials += f_val
best_result = results[0]['result'] for j in range(cpus):
self.log_results({
except ValueError: 'loss': f_val[j]['loss'],
best_parameters = {} 'current_tries': i * cpus + j,
best_result = 'Sorry, Hyperopt was not able to find good parameters. Please ' \ 'total_tries': self.total_tries,
'try with more epochs (param: -e).' 'result': f_val[j]['result'],
})
# Improve best parameter logging display except KeyboardInterrupt:
if best_parameters: print('User interrupted..')
best_parameters = space_eval(
self.hyperopt_space(),
best_parameters
)
logger.info('Best parameters:\n%s', json.dumps(best_parameters, indent=4))
if 'roi_t1' in best_parameters:
logger.info('ROI table:\n%s', self.generate_roi_table(best_parameters))
logger.info('Best Result:\n%s', best_result)
# Store trials result to file to resume next time
self.save_trials()
def signal_handler(self, sig, frame) -> None:
"""
Hyperopt SIGINT handler
"""
logger.info(
'Hyperopt received %s',
signal.Signals(sig).name
)
self.save_trials() self.save_trials()
self.log_trials_result() self.log_trials_result()
sys.exit(0)
def start(args: Namespace) -> None: def start(args: Namespace) -> None:

View File

@ -5,12 +5,11 @@ This module contains the class to persist trades into SQLite
import logging import logging
from datetime import datetime from datetime import datetime
from decimal import Decimal, getcontext from decimal import Decimal, getcontext
from typing import Dict, Optional, Any from typing import Any, Dict, Optional
import arrow import arrow
from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String, from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String,
create_engine) create_engine, inspect)
from sqlalchemy import inspect
from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.exc import NoSuchModuleError
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.scoping import scoped_session from sqlalchemy.orm.scoping import scoped_session
@ -22,6 +21,7 @@ from freqtrade import OperationalException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_DECL_BASE: Any = declarative_base() _DECL_BASE: Any = declarative_base()
_SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
def init(config: Dict) -> None: def init(config: Dict) -> None:
@ -46,10 +46,8 @@ def init(config: Dict) -> None:
try: try:
engine = create_engine(db_url, **kwargs) engine = create_engine(db_url, **kwargs)
except NoSuchModuleError: except NoSuchModuleError:
error = 'Given value for db_url: \'{}\' is no valid database URL! (See {}).'.format( raise OperationalException(f'Given value for db_url: \'{db_url}\' '
db_url, 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls' f'is no valid database URL! (See {_SQL_DOCS_URL})')
)
raise OperationalException(error)
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
Trade.session = session() Trade.session = session()
@ -66,6 +64,10 @@ def has_column(columns, searchname: str) -> bool:
return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1 return len(list(filter(lambda x: x["name"] == searchname, columns))) == 1
def get_column_def(columns, column: str, default: str) -> str:
return default if not has_column(columns, column) else column
def check_migrate(engine) -> None: def check_migrate(engine) -> None:
""" """
Checks if migration is necessary and migrates if necessary Checks if migration is necessary and migrates if necessary
@ -73,18 +75,32 @@ def check_migrate(engine) -> None:
inspector = inspect(engine) inspector = inspect(engine)
cols = inspector.get_columns('trades') cols = inspector.get_columns('trades')
tabs = inspector.get_table_names()
table_back_name = 'trades_bak'
for i, table_back_name in enumerate(tabs):
table_back_name = f'trades_bak{i}'
logger.info(f'trying {table_back_name}')
# Check for latest column
if not has_column(cols, 'max_rate'):
open_rate_requested = get_column_def(cols, 'open_rate_requested', 'null')
close_rate_requested = get_column_def(cols, 'close_rate_requested', 'null')
stop_loss = get_column_def(cols, 'stop_loss', '0.0')
initial_stop_loss = get_column_def(cols, 'initial_stop_loss', '0.0')
max_rate = get_column_def(cols, 'max_rate', '0.0')
if not has_column(cols, 'fee_open'):
# Schema migration necessary # Schema migration necessary
engine.execute("alter table trades rename to trades_bak") engine.execute(f"alter table trades rename to {table_back_name}")
# let SQLAlchemy create the schema as required # let SQLAlchemy create the schema as required
_DECL_BASE.metadata.create_all(engine) _DECL_BASE.metadata.create_all(engine)
# Copy data back - following the correct schema # Copy data back - following the correct schema
engine.execute("""insert into trades engine.execute(f"""insert into trades
(id, exchange, pair, is_open, fee_open, fee_close, open_rate, (id, exchange, pair, is_open, fee_open, fee_close, open_rate,
open_rate_requested, close_rate, close_rate_requested, close_profit, open_rate_requested, close_rate, close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id) stake_amount, amount, open_date, close_date, open_order_id,
stop_loss, initial_stop_loss, max_rate
)
select id, lower(exchange), select id, lower(exchange),
case case
when instr(pair, '_') != 0 then when instr(pair, '_') != 0 then
@ -94,21 +110,18 @@ def check_migrate(engine) -> None:
end end
pair, pair,
is_open, fee fee_open, fee fee_close, is_open, fee fee_open, fee fee_close,
open_rate, null open_rate_requested, close_rate, open_rate, {open_rate_requested} open_rate_requested, close_rate,
null close_rate_requested, close_profit, {close_rate_requested} close_rate_requested, close_profit,
stake_amount, amount, open_date, close_date, open_order_id stake_amount, amount, open_date, close_date, open_order_id,
from trades_bak {stop_loss} stop_loss, {initial_stop_loss} initial_stop_loss,
{max_rate} max_rate
from {table_back_name}
""") """)
# Reread columns - the above recreated the table! # Reread columns - the above recreated the table!
inspector = inspect(engine) inspector = inspect(engine)
cols = inspector.get_columns('trades') cols = inspector.get_columns('trades')
if not has_column(cols, 'open_rate_requested'):
engine.execute("alter table trades add open_rate_requested float")
if not has_column(cols, 'close_rate_requested'):
engine.execute("alter table trades add close_rate_requested float")
def cleanup() -> None: def cleanup() -> None:
""" """
@ -151,15 +164,57 @@ class Trade(_DECL_BASE):
open_date = Column(DateTime, nullable=False, default=datetime.utcnow) open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
close_date = Column(DateTime) close_date = Column(DateTime)
open_order_id = Column(String) open_order_id = Column(String)
# absolute value of the stop loss
stop_loss = Column(Float, nullable=True, default=0.0)
# absolute value of the initial stop loss
initial_stop_loss = Column(Float, nullable=True, default=0.0)
# absolute value of the highest reached price
max_rate = Column(Float, nullable=True, default=0.0)
def __repr__(self): def __repr__(self):
return 'Trade(id={}, pair={}, amount={:.8f}, open_rate={:.8f}, open_since={})'.format( open_since = arrow.get(self.open_date).humanize() if self.is_open else 'closed'
self.id,
self.pair, return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
self.amount, f'open_rate={self.open_rate:.8f}, open_since={open_since})')
self.open_rate,
arrow.get(self.open_date).humanize() if self.is_open else 'closed' def adjust_stop_loss(self, current_price: float, stoploss: float, initial: bool = False):
) """this adjusts the stop loss to it's most recently observed setting"""
if initial and not (self.stop_loss is None or self.stop_loss == 0):
# Don't modify if called with initial and nothing to do
return
new_loss = float(current_price * (1 - abs(stoploss)))
# keeping track of the highest observed rate for this trade
if self.max_rate is None:
self.max_rate = current_price
else:
if current_price > self.max_rate:
self.max_rate = current_price
# no stop loss assigned yet
if not self.stop_loss:
logger.debug("assigning new stop loss")
self.stop_loss = new_loss
self.initial_stop_loss = new_loss
# evaluate if the stop loss needs to be updated
else:
if new_loss > self.stop_loss: # stop losses only walk up, never down!
self.stop_loss = new_loss
logger.debug("adjusted stop loss")
else:
logger.debug("keeping current stop loss")
logger.debug(
f"{self.pair} - current price {current_price:.8f}, "
f"bought at {self.open_rate:.8f} and calculated "
f"stop loss is at: {self.initial_stop_loss:.8f} initial "
f"stop at {self.stop_loss:.8f}. "
f"trailing stop loss saved us: "
f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f} "
f"and max observed rate was {self.max_rate:.8f}")
def update(self, order: Dict) -> None: def update(self, order: Dict) -> None:
""" """
@ -167,6 +222,7 @@ class Trade(_DECL_BASE):
:param order: order retrieved by exchange.get_order() :param order: order retrieved by exchange.get_order()
:return: None :return: None
""" """
order_type = order['type']
# Ignore open and cancelled orders # Ignore open and cancelled orders
if order['status'] == 'open' or order['price'] is None: if order['status'] == 'open' or order['price'] is None:
return return
@ -174,16 +230,16 @@ class Trade(_DECL_BASE):
logger.info('Updating trade (id=%d) ...', self.id) logger.info('Updating trade (id=%d) ...', self.id)
getcontext().prec = 8 # Bittrex do not go above 8 decimal getcontext().prec = 8 # Bittrex do not go above 8 decimal
if order['type'] == 'limit' and order['side'] == 'buy': if order_type == 'limit' and order['side'] == 'buy':
# Update open rate and actual amount # Update open rate and actual amount
self.open_rate = Decimal(order['price']) self.open_rate = Decimal(order['price'])
self.amount = Decimal(order['amount']) self.amount = Decimal(order['amount'])
logger.info('LIMIT_BUY has been fulfilled for %s.', self) logger.info('LIMIT_BUY has been fulfilled for %s.', self)
self.open_order_id = None self.open_order_id = None
elif order['type'] == 'limit' and order['side'] == 'sell': elif order_type == 'limit' and order['side'] == 'sell':
self.close(order['price']) self.close(order['price'])
else: else:
raise ValueError('Unknown order type: {}'.format(order['type'])) raise ValueError(f'Unknown order type: {order_type}')
cleanup() cleanup()
def close(self, rate: float) -> None: def close(self, rate: float) -> None:
@ -254,7 +310,8 @@ class Trade(_DECL_BASE):
rate=(rate or self.close_rate), rate=(rate or self.close_rate),
fee=(fee or self.fee_close) fee=(fee or self.fee_close)
) )
return float("{0:.8f}".format(close_trade_price - open_trade_price)) profit = close_trade_price - open_trade_price
return float(f"{profit:.8f}")
def calc_profit_percent( def calc_profit_percent(
self, self,
@ -274,5 +331,5 @@ class Trade(_DECL_BASE):
rate=(rate or self.close_rate), rate=(rate or self.close_rate),
fee=(fee or self.fee_close) fee=(fee or self.fee_close)
) )
profit_percent = (close_trade_price / open_trade_price) - 1
return float("{0:.8f}".format((close_trade_price / open_trade_price) - 1)) return float(f"{profit_percent:.8f}")

View File

@ -3,9 +3,9 @@ This module contains class to define a RPC communications
""" """
import logging import logging
from abc import abstractmethod from abc import abstractmethod
from datetime import datetime, timedelta, date from datetime import date, datetime, timedelta
from decimal import Decimal from decimal import Decimal
from typing import Dict, Tuple, Any, List from typing import Any, Dict, List, Tuple
import arrow import arrow
import sqlalchemy as sql import sqlalchemy as sql
@ -74,34 +74,32 @@ class RPC(object):
# calculate profit and send message to user # calculate profit and send message to user
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid'] current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
current_profit = trade.calc_profit_percent(current_rate) current_profit = trade.calc_profit_percent(current_rate)
fmt_close_profit = '{:.2f}%'.format( fmt_close_profit = (f'{round(trade.close_profit * 100, 2):.2f}%'
round(trade.close_profit * 100, 2) if trade.close_profit else None)
) if trade.close_profit else None market_url = self._freqtrade.exchange.get_pair_detail_url(trade.pair)
message = "*Trade ID:* `{trade_id}`\n" \ trade_date = arrow.get(trade.open_date).humanize()
"*Current Pair:* [{pair}]({market_url})\n" \ open_rate = trade.open_rate
"*Open Since:* `{date}`\n" \ close_rate = trade.close_rate
"*Amount:* `{amount}`\n" \ amount = round(trade.amount, 8)
"*Open Rate:* `{open_rate:.8f}`\n" \ current_profit = round(current_profit * 100, 2)
"*Close Rate:* `{close_rate}`\n" \ open_order = ''
"*Current Rate:* `{current_rate:.8f}`\n" \ if order:
"*Close Profit:* `{close_profit}`\n" \ order_type = order['type']
"*Current Profit:* `{current_profit:.2f}%`\n" \ order_side = order['side']
"*Open Order:* `{open_order}`"\ order_rem = order['remaining']
.format( open_order = f'({order_type} {order_side} rem={order_rem:.8f})'
trade_id=trade.id,
pair=trade.pair, message = f"*Trade ID:* `{trade.id}`\n" \
market_url=self._freqtrade.exchange.get_pair_detail_url(trade.pair), f"*Current Pair:* [{trade.pair}]({market_url})\n" \
date=arrow.get(trade.open_date).humanize(), f"*Open Since:* `{trade_date}`\n" \
open_rate=trade.open_rate, f"*Amount:* `{amount}`\n" \
close_rate=trade.close_rate, f"*Open Rate:* `{open_rate:.8f}`\n" \
current_rate=current_rate, f"*Close Rate:* `{close_rate}`\n" \
amount=round(trade.amount, 8), f"*Current Rate:* `{current_rate:.8f}`\n" \
close_profit=fmt_close_profit, f"*Close Profit:* `{fmt_close_profit}`\n" \
current_profit=round(current_profit * 100, 2), f"*Current Profit:* `{current_profit:.2f}%`\n" \
open_order='({} {} rem={:.8f})'.format( f"*Open Order:* `{open_order}`"\
order['type'], order['side'], order['remaining']
) if order else None,
)
result.append(message) result.append(message)
return result return result
@ -116,11 +114,12 @@ class RPC(object):
for trade in trades: for trade in trades:
# calculate profit and send message to user # calculate profit and send message to user
current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid'] current_rate = self._freqtrade.exchange.get_ticker(trade.pair, False)['bid']
trade_perc = (100 * trade.calc_profit_percent(current_rate))
trades_list.append([ trades_list.append([
trade.id, trade.id,
trade.pair, trade.pair,
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
'{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate)) f'{trade_perc:.2f}%'
]) ])
columns = ['ID', 'Pair', 'Since', 'Profit'] columns = ['ID', 'Pair', 'Since', 'Profit']
@ -148,7 +147,7 @@ class RPC(object):
.all() .all()
curdayprofit = sum(trade.calc_profit() for trade in trades) curdayprofit = sum(trade.calc_profit() for trade in trades)
profit_days[profitday] = { profit_days[profitday] = {
'amount': format(curdayprofit, '.8f'), 'amount': f'{curdayprofit:.8f}',
'trades': len(trades) 'trades': len(trades)
} }

View File

@ -153,7 +153,7 @@ class Telegram(RPC):
try: try:
df_statuses = self._rpc_status_table() df_statuses = self._rpc_status_table()
message = tabulate(df_statuses, headers='keys', tablefmt='simple') message = tabulate(df_statuses, headers='keys', tablefmt='simple')
self._send_msg("<pre>{}</pre>".format(message), parse_mode=ParseMode.HTML) self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e), bot=bot)
@ -166,6 +166,8 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
stake_cur = self._config['stake_currency']
fiat_disp_cur = self._config['fiat_display_currency']
try: try:
timescale = int(update.message.text.replace('/daily', '').strip()) timescale = int(update.message.text.replace('/daily', '').strip())
except (TypeError, ValueError): except (TypeError, ValueError):
@ -173,18 +175,17 @@ class Telegram(RPC):
try: try:
stats = self._rpc_daily_profit( stats = self._rpc_daily_profit(
timescale, timescale,
self._config['stake_currency'], stake_cur,
self._config['fiat_display_currency'] fiat_disp_cur
) )
stats = tabulate(stats, stats = tabulate(stats,
headers=[ headers=[
'Day', 'Day',
'Profit {}'.format(self._config['stake_currency']), f'Profit {stake_cur}',
'Profit {}'.format(self._config['fiat_display_currency']) f'Profit {fiat_disp_cur}'
], ],
tablefmt='simple') tablefmt='simple')
message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'\ message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats}</pre>'
.format(timescale, stats)
self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML) self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e), bot=bot)
@ -198,39 +199,38 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
stake_cur = self._config['stake_currency']
fiat_disp_cur = self._config['fiat_display_currency']
try: try:
stats = self._rpc_trade_statistics( stats = self._rpc_trade_statistics(
self._config['stake_currency'], stake_cur,
self._config['fiat_display_currency']) fiat_disp_cur)
profit_closed_coin = stats['profit_closed_coin']
profit_closed_percent = stats['profit_closed_percent']
profit_closed_fiat = stats['profit_closed_fiat']
profit_all_coin = stats['profit_all_coin']
profit_all_percent = stats['profit_all_percent']
profit_all_fiat = stats['profit_all_fiat']
trade_count = stats['trade_count']
first_trade_date = stats['first_trade_date']
latest_trade_date = stats['latest_trade_date']
avg_duration = stats['avg_duration']
best_pair = stats['best_pair']
best_rate = stats['best_rate']
# Message to display # Message to display
markdown_msg = "*ROI:* Close trades\n" \ markdown_msg = "*ROI:* Close trades\n" \
"∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`\n" \ f"∙ `{profit_closed_coin:.8f} {stake_cur} "\
"∙ `{profit_closed_fiat:.3f} {fiat}`\n" \ f"({profit_closed_percent:.2f}%)`\n" \
"*ROI:* All trades\n" \ f"∙ `{profit_closed_fiat:.3f} {fiat_disp_cur}`\n" \
"∙ `{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`\n" \ f"*ROI:* All trades\n" \
"∙ `{profit_all_fiat:.3f} {fiat}`\n" \ f"∙ `{profit_all_coin:.8f} {stake_cur} ({profit_all_percent:.2f}%)`\n" \
"*Total Trade Count:* `{trade_count}`\n" \ f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n" \
"*First Trade opened:* `{first_trade_date}`\n" \ f"*Total Trade Count:* `{trade_count}`\n" \
"*Latest Trade opened:* `{latest_trade_date}`\n" \ f"*First Trade opened:* `{first_trade_date}`\n" \
"*Avg. Duration:* `{avg_duration}`\n" \ f"*Latest Trade opened:* `{latest_trade_date}`\n" \
"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"\ f"*Avg. Duration:* `{avg_duration}`\n" \
.format( f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"
coin=self._config['stake_currency'],
fiat=self._config['fiat_display_currency'],
profit_closed_coin=stats['profit_closed_coin'],
profit_closed_percent=stats['profit_closed_percent'],
profit_closed_fiat=stats['profit_closed_fiat'],
profit_all_coin=stats['profit_all_coin'],
profit_all_percent=stats['profit_all_percent'],
profit_all_fiat=stats['profit_all_fiat'],
trade_count=stats['trade_count'],
first_trade_date=stats['first_trade_date'],
latest_trade_date=stats['latest_trade_date'],
avg_duration=stats['avg_duration'],
best_pair=stats['best_pair'],
best_rate=stats['best_rate']
)
self._send_msg(markdown_msg, bot=bot) self._send_msg(markdown_msg, bot=bot)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e), bot=bot)

View File

@ -2,8 +2,8 @@
IStrategy interface IStrategy interface
This module defines the interface to apply for strategies This module defines the interface to apply for strategies
""" """
from typing import Dict
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Dict
from pandas import DataFrame from pandas import DataFrame

View File

@ -8,7 +8,7 @@ import inspect
import logging import logging
from base64 import urlsafe_b64decode from base64 import urlsafe_b64decode
from collections import OrderedDict from collections import OrderedDict
from typing import Optional, Dict, Type from typing import Dict, Optional, Type
from freqtrade import constants from freqtrade import constants
from freqtrade.strategy import import_strategy from freqtrade.strategy import import_strategy
@ -17,6 +17,7 @@ import tempfile
import os import os
from pathlib import Path from pathlib import Path
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -82,8 +83,8 @@ class StrategyResolver(object):
# Add extra strategy directory on top of search paths # Add extra strategy directory on top of search paths
abs_paths.insert(0, extra_dir) abs_paths.insert(0, extra_dir)
if ":" in strategy_name and "http" not in strategy_name: if ":" in strategy_name:
print("loading none http based strategy: {}".format(strategy_name)) logger.debug(("loading base64 endocded strategy".)
strat = strategy_name.split(":") strat = strategy_name.split(":")
if len(strat) == 2: if len(strat) == 2:

View File

@ -2,8 +2,8 @@
import json import json
import logging import logging
from datetime import datetime from datetime import datetime
from typing import Dict, Optional
from functools import reduce from functools import reduce
from typing import Dict, Optional
from unittest.mock import MagicMock from unittest.mock import MagicMock
import arrow import arrow
@ -11,8 +11,8 @@ import pytest
from jsonschema import validate from jsonschema import validate
from telegram import Chat, Message, Update from telegram import Chat, Message, Update
from freqtrade.analyze import Analyze
from freqtrade import constants from freqtrade import constants
from freqtrade.analyze import Analyze
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
@ -100,7 +100,10 @@ def default_conf():
"0": 0.04 "0": 0.04
}, },
"stoploss": -0.10, "stoploss": -0.10,
"unfilledtimeout": 600, "unfilledtimeout": {
"buy": 10,
"sell": 30
},
"bid_strategy": { "bid_strategy": {
"ask_last_balance": 0.0 "ask_last_balance": 0.0
}, },

View File

@ -2,16 +2,32 @@
# pragma pylint: disable=protected-access # pragma pylint: disable=protected-access
import logging import logging
from copy import deepcopy from copy import deepcopy
from random import randint
from datetime import datetime from datetime import datetime
from random import randint
from unittest.mock import MagicMock, PropertyMock from unittest.mock import MagicMock, PropertyMock
import ccxt import ccxt
import pytest import pytest
from freqtrade import OperationalException, DependencyException, TemporaryError from freqtrade import DependencyException, OperationalException, TemporaryError
from freqtrade.exchange import Exchange, API_RETRY_COUNT from freqtrade.exchange import API_RETRY_COUNT, Exchange
from freqtrade.tests.conftest import log_has, get_patched_exchange from freqtrade.tests.conftest import get_patched_exchange, log_has
def ccxt_exceptionhandlers(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs):
"""Function to test ccxt exception handling """
with pytest.raises(TemporaryError):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1
with pytest.raises(OperationalException):
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
getattr(exchange, fun)(**kwargs)
assert api_mock.__dict__[mock_ccxt_fun].call_count == 1
def test_init(default_conf, mocker, caplog): def test_init(default_conf, mocker, caplog):
@ -20,7 +36,7 @@ def test_init(default_conf, mocker, caplog):
assert log_has('Instance is running with dry_run enabled', caplog.record_tuples) assert log_has('Instance is running with dry_run enabled', caplog.record_tuples)
def test_init_exception(default_conf): def test_init_exception(default_conf, mocker):
default_conf['exchange']['name'] = 'wrong_exchange_name' default_conf['exchange']['name'] = 'wrong_exchange_name'
with pytest.raises( with pytest.raises(
@ -28,6 +44,13 @@ def test_init_exception(default_conf):
match='Exchange {} is not supported'.format(default_conf['exchange']['name'])): match='Exchange {} is not supported'.format(default_conf['exchange']['name'])):
Exchange(default_conf) Exchange(default_conf)
default_conf['exchange']['name'] = 'binance'
with pytest.raises(
OperationalException,
match='Exchange {} is not supported'.format(default_conf['exchange']['name'])):
mocker.patch("ccxt.binance", MagicMock(side_effect=AttributeError))
Exchange(default_conf)
def test_validate_pairs(default_conf, mocker): def test_validate_pairs(default_conf, mocker):
api_mock = MagicMock() api_mock = MagicMock()
@ -97,6 +120,20 @@ def test_validate_pairs_stake_exception(default_conf, mocker, caplog):
Exchange(conf) Exchange(conf)
def test_exchangehas(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf)
assert not exchange.exchange_has('ASDFASDF')
api_mock = MagicMock()
type(api_mock).has = PropertyMock(return_value={'deadbeef': True})
exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert exchange.exchange_has("deadbeef")
type(api_mock).has = PropertyMock(return_value={'deadbeef': False})
exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert not exchange.exchange_has("deadbeef")
def test_buy_dry_run(default_conf, mocker): def test_buy_dry_run(default_conf, mocker):
default_conf['dry_run'] = True default_conf['dry_run'] = True
exchange = get_patched_exchange(mocker, default_conf) exchange = get_patched_exchange(mocker, default_conf)
@ -216,6 +253,11 @@ def test_get_balance_prod(default_conf, mocker):
exchange.get_balance(currency='BTC') exchange.get_balance(currency='BTC')
with pytest.raises(TemporaryError, match=r'.*balance due to malformed exchange response:.*'):
exchange = get_patched_exchange(mocker, default_conf, api_mock)
mocker.patch('freqtrade.exchange.Exchange.get_balances', MagicMock(return_value={}))
exchange.get_balance(currency='BTC')
def test_get_balances_dry_run(default_conf, mocker): def test_get_balances_dry_run(default_conf, mocker):
default_conf['dry_run'] = True default_conf['dry_run'] = True
@ -243,17 +285,8 @@ def test_get_balances_prod(default_conf, mocker):
assert exchange.get_balances()['1ST']['total'] == 10.0 assert exchange.get_balances()['1ST']['total'] == 10.0
assert exchange.get_balances()['1ST']['used'] == 0.0 assert exchange.get_balances()['1ST']['used'] == 0.0
with pytest.raises(TemporaryError): ccxt_exceptionhandlers(mocker, default_conf, api_mock,
api_mock.fetch_balance = MagicMock(side_effect=ccxt.NetworkError) "get_balances", "fetch_balance")
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_balances()
assert api_mock.fetch_balance.call_count == API_RETRY_COUNT + 1
with pytest.raises(OperationalException):
api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_balances()
assert api_mock.fetch_balance.call_count == 1
def test_get_tickers(default_conf, mocker): def test_get_tickers(default_conf, mocker):
@ -282,15 +315,8 @@ def test_get_tickers(default_conf, mocker):
assert tickers['BCH/BTC']['bid'] == 0.6 assert tickers['BCH/BTC']['bid'] == 0.6
assert tickers['BCH/BTC']['ask'] == 0.5 assert tickers['BCH/BTC']['ask'] == 0.5
with pytest.raises(TemporaryError): # test retrier ccxt_exceptionhandlers(mocker, default_conf, api_mock,
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NetworkError) "get_tickers", "fetch_tickers")
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_tickers()
with pytest.raises(OperationalException):
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_tickers()
with pytest.raises(OperationalException): with pytest.raises(OperationalException):
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported) api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported)
@ -345,15 +371,9 @@ def test_get_ticker(default_conf, mocker):
exchange.get_ticker(pair='ETH/BTC', refresh=False) exchange.get_ticker(pair='ETH/BTC', refresh=False)
assert api_mock.fetch_ticker.call_count == 0 assert api_mock.fetch_ticker.call_count == 0
with pytest.raises(TemporaryError): # test retrier ccxt_exceptionhandlers(mocker, default_conf, api_mock,
api_mock.fetch_ticker = MagicMock(side_effect=ccxt.NetworkError) "get_ticker", "fetch_ticker",
exchange = get_patched_exchange(mocker, default_conf, api_mock) pair='ETH/BTC', refresh=True)
exchange.get_ticker(pair='ETH/BTC', refresh=True)
with pytest.raises(OperationalException):
api_mock.fetch_ticker = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_ticker(pair='ETH/BTC', refresh=True)
api_mock.fetch_ticker = MagicMock(return_value={}) api_mock.fetch_ticker = MagicMock(return_value={})
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
@ -416,17 +436,14 @@ def test_get_ticker_history(default_conf, mocker):
assert ticks[0][4] == 9 assert ticks[0][4] == 9
assert ticks[0][5] == 10 assert ticks[0][5] == 10
with pytest.raises(TemporaryError): # test retrier ccxt_exceptionhandlers(mocker, default_conf, api_mock,
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NetworkError) "get_ticker_history", "fetch_ohlcv",
exchange = get_patched_exchange(mocker, default_conf, api_mock) pair='ABCD/BTC', tick_interval=default_conf['ticker_interval'])
# new symbol to get around cache
exchange.get_ticker_history('ABCD/BTC', default_conf['ticker_interval'])
with pytest.raises(OperationalException): with pytest.raises(OperationalException, match=r'Exchange .* does not support.*'):
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError) api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.NotSupported)
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
# new symbol to get around cache exchange.get_ticker_history(pair='ABCD/BTC', tick_interval=default_conf['ticker_interval'])
exchange.get_ticker_history('EFGH/BTC', default_conf['ticker_interval'])
def test_get_ticker_history_sort(default_conf, mocker): def test_get_ticker_history_sort(default_conf, mocker):
@ -515,24 +532,15 @@ def test_cancel_order(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123 assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123
with pytest.raises(TemporaryError):
api_mock.cancel_order = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.cancel_order(order_id='_', pair='TKN/BTC')
assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder) api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder)
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.cancel_order(order_id='_', pair='TKN/BTC') exchange.cancel_order(order_id='_', pair='TKN/BTC')
assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1 assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1
with pytest.raises(OperationalException): ccxt_exceptionhandlers(mocker, default_conf, api_mock,
api_mock.cancel_order = MagicMock(side_effect=ccxt.BaseError) "cancel_order", "cancel_order",
exchange = get_patched_exchange(mocker, default_conf, api_mock) order_id='_', pair='TKN/BTC')
exchange.cancel_order(order_id='_', pair='TKN/BTC')
assert api_mock.cancel_order.call_count == 1
def test_get_order(default_conf, mocker): def test_get_order(default_conf, mocker):
@ -550,23 +558,15 @@ def test_get_order(default_conf, mocker):
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
assert exchange.get_order('X', 'TKN/BTC') == 456 assert exchange.get_order('X', 'TKN/BTC') == 456
with pytest.raises(TemporaryError):
api_mock.fetch_order = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1
with pytest.raises(DependencyException): with pytest.raises(DependencyException):
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder) api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder)
exchange = get_patched_exchange(mocker, default_conf, api_mock) exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_order(order_id='_', pair='TKN/BTC') exchange.get_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1 assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1
with pytest.raises(OperationalException): ccxt_exceptionhandlers(mocker, default_conf, api_mock,
api_mock.fetch_order = MagicMock(side_effect=ccxt.BaseError) 'get_order', 'fetch_order',
exchange = get_patched_exchange(mocker, default_conf, api_mock) order_id='_', pair='TKN/BTC')
exchange.get_order(order_id='_', pair='TKN/BTC')
assert api_mock.fetch_order.call_count == 1
def test_name(default_conf, mocker): def test_name(default_conf, mocker):
@ -651,19 +651,12 @@ def test_get_trades_for_order(default_conf, mocker):
assert len(orders) == 1 assert len(orders) == 1
assert orders[0]['price'] == 165 assert orders[0]['price'] == 165
# test Exceptions ccxt_exceptionhandlers(mocker, default_conf, api_mock,
with pytest.raises(OperationalException): 'get_trades_for_order', 'fetch_my_trades',
api_mock = MagicMock() order_id=order_id, pair='LTC/BTC', since=since)
api_mock.fetch_my_trades = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_trades_for_order(order_id, 'LTC/BTC', since)
with pytest.raises(TemporaryError): mocker.patch('freqtrade.exchange.Exchange.exchange_has', MagicMock(return_value=False))
api_mock = MagicMock() assert exchange.get_trades_for_order(order_id, 'LTC/BTC', since) == []
api_mock.fetch_my_trades = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_trades_for_order(order_id, 'LTC/BTC', since)
assert api_mock.fetch_my_trades.call_count == API_RETRY_COUNT + 1
def test_get_markets(default_conf, mocker, markets): def test_get_markets(default_conf, mocker, markets):
@ -677,19 +670,8 @@ def test_get_markets(default_conf, mocker, markets):
assert ret[0]["id"] == "ethbtc" assert ret[0]["id"] == "ethbtc"
assert ret[0]["symbol"] == "ETH/BTC" assert ret[0]["symbol"] == "ETH/BTC"
# test Exceptions ccxt_exceptionhandlers(mocker, default_conf, api_mock,
with pytest.raises(OperationalException): 'get_markets', 'fetch_markets')
api_mock = MagicMock()
api_mock.fetch_markets = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_markets()
with pytest.raises(TemporaryError):
api_mock = MagicMock()
api_mock.fetch_markets = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_markets()
assert api_mock.fetch_markets.call_count == API_RETRY_COUNT + 1
def test_get_fee(default_conf, mocker): def test_get_fee(default_conf, mocker):
@ -704,19 +686,8 @@ def test_get_fee(default_conf, mocker):
assert exchange.get_fee() == 0.025 assert exchange.get_fee() == 0.025
# test Exceptions ccxt_exceptionhandlers(mocker, default_conf, api_mock,
with pytest.raises(OperationalException): 'get_fee', 'calculate_fee')
api_mock = MagicMock()
api_mock.calculate_fee = MagicMock(side_effect=ccxt.BaseError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_fee()
with pytest.raises(TemporaryError):
api_mock = MagicMock()
api_mock.calculate_fee = MagicMock(side_effect=ccxt.NetworkError)
exchange = get_patched_exchange(mocker, default_conf, api_mock)
exchange.get_fee()
assert api_mock.calculate_fee.call_count == API_RETRY_COUNT + 1
def test_get_amount_lots(default_conf, mocker): def test_get_amount_lots(default_conf, mocker):

View File

@ -3,19 +3,20 @@
import json import json
import math import math
import random import random
import pytest
from copy import deepcopy from copy import deepcopy
from typing import List from typing import List
from unittest.mock import MagicMock from unittest.mock import MagicMock
import numpy as np import numpy as np
import pandas as pd import pandas as pd
import pytest
from arrow import Arrow from arrow import Arrow
from freqtrade import optimize, constants, DependencyException from freqtrade import DependencyException, constants, optimize
from freqtrade.analyze import Analyze from freqtrade.analyze import Analyze
from freqtrade.arguments import Arguments, TimeRange from freqtrade.arguments import Arguments, TimeRange
from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration from freqtrade.optimize.backtesting import (Backtesting, setup_configuration,
start)
from freqtrade.tests.conftest import log_has, patch_exchange from freqtrade.tests.conftest import log_has, patch_exchange
@ -627,9 +628,13 @@ def test_backtest_record(default_conf, fee, mocker):
Arrow(2017, 11, 14, 22, 10, 00).datetime, Arrow(2017, 11, 14, 22, 10, 00).datetime,
Arrow(2017, 11, 14, 22, 43, 00).datetime, Arrow(2017, 11, 14, 22, 43, 00).datetime,
Arrow(2017, 11, 14, 22, 58, 00).datetime], Arrow(2017, 11, 14, 22, 58, 00).datetime],
"open_rate": [0.002543, 0.003003, 0.003089, 0.003214],
"close_rate": [0.002546, 0.003014, 0.003103, 0.003217],
"open_index": [1, 119, 153, 185], "open_index": [1, 119, 153, 185],
"close_index": [118, 151, 184, 199], "close_index": [118, 151, 184, 199],
"trade_duration": [123, 34, 31, 14]}) "trade_duration": [123, 34, 31, 14],
"open_at_end": [False, False, False, True]
})
backtesting._store_backtest_result("backtest-result.json", results) backtesting._store_backtest_result("backtest-result.json", results)
assert len(results) == 4 assert len(results) == 4
# Assert file_dump_json was only called once # Assert file_dump_json was only called once
@ -640,12 +645,16 @@ def test_backtest_record(default_conf, fee, mocker):
# ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117) # ('UNITTEST/BTC', 0.00331158, '1510684320', '1510691700', 0, 117)
# Below follows just a typecheck of the schema/type of trade-records # Below follows just a typecheck of the schema/type of trade-records
oix = None oix = None
for (pair, profit, date_buy, date_sell, buy_index, dur) in records: for (pair, profit, date_buy, date_sell, buy_index, dur,
openr, closer, open_at_end) in records:
assert pair == 'UNITTEST/BTC' assert pair == 'UNITTEST/BTC'
isinstance(profit, float) assert isinstance(profit, float)
# FIX: buy/sell should be converted to ints # FIX: buy/sell should be converted to ints
isinstance(date_buy, str) assert isinstance(date_buy, float)
isinstance(date_sell, str) assert isinstance(date_sell, float)
assert isinstance(openr, float)
assert isinstance(closer, float)
assert isinstance(open_at_end, bool)
isinstance(buy_index, pd._libs.tslib.Timestamp) isinstance(buy_index, pd._libs.tslib.Timestamp)
if oix: if oix:
assert buy_index > oix assert buy_index > oix

View File

@ -1,6 +1,5 @@
# pragma pylint: disable=missing-docstring,W0212,C0103 # pragma pylint: disable=missing-docstring,W0212,C0103
import os import os
import signal
from copy import deepcopy from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
@ -40,21 +39,11 @@ def create_trials(mocker) -> None:
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=False) mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=False)
mocker.patch('freqtrade.optimize.hyperopt.os.path.getsize', return_value=1) mocker.patch('freqtrade.optimize.hyperopt.os.path.getsize', return_value=1)
mocker.patch('freqtrade.optimize.hyperopt.os.remove', return_value=True) mocker.patch('freqtrade.optimize.hyperopt.os.remove', return_value=True)
mocker.patch('freqtrade.optimize.hyperopt.pickle.dump', return_value=None) mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
return mocker.Mock( return [{'loss': 1, 'result': 'foo', 'params': {}}]
results=[
{
'loss': 1,
'result': 'foo',
'status': 'ok'
}
],
best_trial={'misc': {'vals': {'adx': 999}}}
)
# Unit tests
def test_start(mocker, default_conf, caplog) -> None: def test_start(mocker, default_conf, caplog) -> None:
""" """
Test start() function Test start() function
@ -148,155 +137,18 @@ def test_no_log_if_loss_does_not_improve(init_hyperopt, caplog) -> None:
assert caplog.record_tuples == [] assert caplog.record_tuples == []
def test_fmin_best_results(mocker, init_hyperopt, default_conf, caplog) -> None:
fmin_result = {
"macd_below_zero": 0,
"adx": 1,
"adx-value": 15.0,
"fastd": 1,
"fastd-value": 40.0,
"green_candle": 1,
"mfi": 0,
"over_sar": 0,
"rsi": 1,
"rsi-value": 37.0,
"trigger": 2,
"uptrend_long_ema": 1,
"uptrend_short_ema": 0,
"uptrend_sma": 0,
"stoploss": -0.1,
"roi_t1": 1,
"roi_t2": 2,
"roi_t3": 3,
"roi_p1": 1,
"roi_p2": 2,
"roi_p3": 3,
}
conf = deepcopy(default_conf)
conf.update({'config': 'config.json.example'})
conf.update({'epochs': 1})
conf.update({'timerange': None})
conf.update({'spaces': 'all'})
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value=fmin_result)
patch_exchange(mocker)
StrategyResolver({'strategy': 'DefaultStrategy'})
hyperopt = Hyperopt(conf)
hyperopt.trials = create_trials(mocker)
hyperopt.tickerdata_to_dataframe = MagicMock()
hyperopt.start()
exists = [
'Best parameters:',
'"adx": {\n "enabled": true,\n "value": 15.0\n },',
'"fastd": {\n "enabled": true,\n "value": 40.0\n },',
'"green_candle": {\n "enabled": true\n },',
'"macd_below_zero": {\n "enabled": false\n },',
'"mfi": {\n "enabled": false\n },',
'"over_sar": {\n "enabled": false\n },',
'"roi_p1": 1.0,',
'"roi_p2": 2.0,',
'"roi_p3": 3.0,',
'"roi_t1": 1.0,',
'"roi_t2": 2.0,',
'"roi_t3": 3.0,',
'"rsi": {\n "enabled": true,\n "value": 37.0\n },',
'"stoploss": -0.1,',
'"trigger": {\n "type": "faststoch10"\n },',
'"uptrend_long_ema": {\n "enabled": true\n },',
'"uptrend_short_ema": {\n "enabled": false\n },',
'"uptrend_sma": {\n "enabled": false\n }',
'ROI table:\n{0: 6.0, 3.0: 3.0, 5.0: 1.0, 6.0: 0}',
'Best Result:\nfoo'
]
for line in exists:
assert line in caplog.text
def test_fmin_throw_value_error(mocker, init_hyperopt, default_conf, caplog) -> None:
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mocker.patch('freqtrade.optimize.hyperopt.fmin', side_effect=ValueError())
conf = deepcopy(default_conf)
conf.update({'config': 'config.json.example'})
conf.update({'epochs': 1})
conf.update({'timerange': None})
conf.update({'spaces': 'all'})
patch_exchange(mocker)
StrategyResolver({'strategy': 'DefaultStrategy'})
hyperopt = Hyperopt(conf)
hyperopt.trials = create_trials(mocker)
hyperopt.tickerdata_to_dataframe = MagicMock()
hyperopt.start()
exists = [
'Best Result:',
'Sorry, Hyperopt was not able to find good parameters. Please try with more epochs '
'(param: -e).',
]
for line in exists:
assert line in caplog.text
def test_resuming_previous_hyperopt_results_succeeds(mocker, init_hyperopt, default_conf) -> None:
trials = create_trials(mocker)
conf = deepcopy(default_conf)
conf.update({'config': 'config.json.example'})
conf.update({'epochs': 1})
conf.update({'timerange': None})
conf.update({'spaces': 'all'})
mocker.patch('freqtrade.optimize.hyperopt.os.path.exists', return_value=True)
mocker.patch('freqtrade.optimize.hyperopt.len', return_value=len(trials.results))
mock_read = mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.read_trials',
return_value=trials
)
mock_save = mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.save_trials',
return_value=None
)
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
patch_exchange(mocker)
StrategyResolver({'strategy': 'DefaultStrategy'})
hyperopt = Hyperopt(conf)
hyperopt.trials = trials
hyperopt.tickerdata_to_dataframe = MagicMock()
hyperopt.start()
mock_read.assert_called_once()
mock_save.assert_called_once()
current_tries = hyperopt.current_tries
total_tries = hyperopt.total_tries
assert current_tries == len(trials.results)
assert total_tries == (current_tries + len(trials.results))
def test_save_trials_saves_trials(mocker, init_hyperopt, caplog) -> None: def test_save_trials_saves_trials(mocker, init_hyperopt, caplog) -> None:
create_trials(mocker) trials = create_trials(mocker)
mock_dump = mocker.patch('freqtrade.optimize.hyperopt.pickle.dump', return_value=None) mock_dump = mocker.patch('freqtrade.optimize.hyperopt.dump', return_value=None)
hyperopt = _HYPEROPT hyperopt = _HYPEROPT
mocker.patch('freqtrade.optimize.hyperopt.open', return_value=hyperopt.trials_file) _HYPEROPT.trials = trials
hyperopt.save_trials() hyperopt.save_trials()
trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle') trials_file = os.path.join('freqtrade', 'tests', 'optimize', 'ut_trials.pickle')
assert log_has( assert log_has(
'Saving Trials to \'{}\''.format(trials_file), 'Saving 1 evaluations to \'{}\''.format(trials_file),
caplog.record_tuples caplog.record_tuples
) )
mock_dump.assert_called_once() mock_dump.assert_called_once()
@ -304,8 +156,7 @@ def test_save_trials_saves_trials(mocker, init_hyperopt, caplog) -> None:
def test_read_trials_returns_trials_file(mocker, init_hyperopt, caplog) -> None: def test_read_trials_returns_trials_file(mocker, init_hyperopt, caplog) -> None:
trials = create_trials(mocker) trials = create_trials(mocker)
mock_load = mocker.patch('freqtrade.optimize.hyperopt.pickle.load', return_value=trials) mock_load = mocker.patch('freqtrade.optimize.hyperopt.load', return_value=trials)
mock_open = mocker.patch('freqtrade.optimize.hyperopt.open', return_value=mock_load)
hyperopt = _HYPEROPT hyperopt = _HYPEROPT
hyperopt_trial = hyperopt.read_trials() hyperopt_trial = hyperopt.read_trials()
@ -315,7 +166,6 @@ def test_read_trials_returns_trials_file(mocker, init_hyperopt, caplog) -> None:
caplog.record_tuples caplog.record_tuples
) )
assert hyperopt_trial == trials assert hyperopt_trial == trials
mock_open.assert_called_once()
mock_load.assert_called_once() mock_load.assert_called_once()
@ -333,12 +183,15 @@ def test_roi_table_generation(init_hyperopt) -> None:
assert hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0} assert hyperopt.generate_roi_table(params) == {0: 6, 15: 3, 25: 1, 30: 0}
def test_start_calls_fmin(mocker, init_hyperopt, default_conf) -> None: def test_start_calls_optimizer(mocker, init_hyperopt, default_conf, caplog) -> None:
trials = create_trials(mocker) dumper = mocker.patch('freqtrade.optimize.hyperopt.dump', MagicMock())
mocker.patch('freqtrade.optimize.hyperopt.sorted', return_value=trials.results)
mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock()) mocker.patch('freqtrade.optimize.hyperopt.load_data', MagicMock())
mocker.patch('freqtrade.optimize.hyperopt.multiprocessing.cpu_count', MagicMock(return_value=1))
parallel = mocker.patch(
'freqtrade.optimize.hyperopt.Hyperopt.run_optimizer_parallel',
MagicMock(return_value=[{'loss': 1, 'result': 'foo result', 'params': {}}])
)
patch_exchange(mocker) patch_exchange(mocker)
mock_fmin = mocker.patch('freqtrade.optimize.hyperopt.fmin', return_value={})
conf = deepcopy(default_conf) conf = deepcopy(default_conf)
conf.update({'config': 'config.json.example'}) conf.update({'config': 'config.json.example'})
@ -347,11 +200,13 @@ def test_start_calls_fmin(mocker, init_hyperopt, default_conf) -> None:
conf.update({'spaces': 'all'}) conf.update({'spaces': 'all'})
hyperopt = Hyperopt(conf) hyperopt = Hyperopt(conf)
hyperopt.trials = trials
hyperopt.tickerdata_to_dataframe = MagicMock() hyperopt.tickerdata_to_dataframe = MagicMock()
hyperopt.start() hyperopt.start()
mock_fmin.assert_called_once() parallel.assert_called_once()
assert 'Best result:\nfoo result\nwith values:\n{}' in caplog.text
assert dumper.called
def test_format_results(init_hyperopt): def test_format_results(init_hyperopt):
@ -384,20 +239,6 @@ def test_format_results(init_hyperopt):
assert result.find('Total profit 1.00000000 EUR') assert result.find('Total profit 1.00000000 EUR')
def test_signal_handler(mocker, init_hyperopt):
"""
Test Hyperopt.signal_handler()
"""
m = MagicMock()
mocker.patch('sys.exit', m)
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.save_trials', m)
mocker.patch('freqtrade.optimize.hyperopt.Hyperopt.log_trials_result', m)
hyperopt = _HYPEROPT
hyperopt.signal_handler(signal.SIGTERM, None)
assert m.call_count == 3
def test_has_space(init_hyperopt): def test_has_space(init_hyperopt):
""" """
Test Hyperopt.has_space() method Test Hyperopt.has_space() method
@ -422,8 +263,8 @@ def test_populate_indicators(init_hyperopt) -> None:
# Check if some indicators are generated. We will not test all of them # Check if some indicators are generated. We will not test all of them
assert 'adx' in dataframe assert 'adx' in dataframe
assert 'ao' in dataframe assert 'mfi' in dataframe
assert 'cci' in dataframe assert 'rsi' in dataframe
def test_buy_strategy_generator(init_hyperopt) -> None: def test_buy_strategy_generator(init_hyperopt) -> None:
@ -437,44 +278,15 @@ def test_buy_strategy_generator(init_hyperopt) -> None:
populate_buy_trend = _HYPEROPT.buy_strategy_generator( populate_buy_trend = _HYPEROPT.buy_strategy_generator(
{ {
'uptrend_long_ema': { 'adx-value': 20,
'enabled': True 'fastd-value': 20,
}, 'mfi-value': 20,
'macd_below_zero': { 'rsi-value': 20,
'enabled': True 'adx-enabled': True,
}, 'fastd-enabled': True,
'uptrend_short_ema': { 'mfi-enabled': True,
'enabled': True 'rsi-enabled': True,
}, 'trigger': 'bb_lower'
'mfi': {
'enabled': True,
'value': 20
},
'fastd': {
'enabled': True,
'value': 20
},
'adx': {
'enabled': True,
'value': 20
},
'rsi': {
'enabled': True,
'value': 20
},
'over_sar': {
'enabled': True,
},
'green_candle': {
'enabled': True,
},
'uptrend_sma': {
'enabled': True,
},
'trigger': {
'type': 'lower_bb'
}
} }
) )
result = populate_buy_trend(dataframe) result = populate_buy_trend(dataframe)
@ -503,35 +315,34 @@ def test_generate_optimizer(mocker, init_hyperopt, default_conf) -> None:
MagicMock(return_value=backtest_result) MagicMock(return_value=backtest_result)
) )
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch('freqtrade.optimize.hyperopt.load', MagicMock())
optimizer_param = { optimizer_param = {
'adx': {'enabled': False}, 'adx-value': 0,
'fastd': {'enabled': True, 'value': 35.0}, 'fastd-value': 35,
'green_candle': {'enabled': True}, 'mfi-value': 0,
'macd_below_zero': {'enabled': True}, 'rsi-value': 0,
'mfi': {'enabled': False}, 'adx-enabled': False,
'over_sar': {'enabled': False}, 'fastd-enabled': True,
'roi_p1': 0.01, 'mfi-enabled': False,
'roi_p2': 0.01, 'rsi-enabled': False,
'roi_p3': 0.1, 'trigger': 'macd_cross_signal',
'roi_t1': 60.0, 'roi_t1': 60.0,
'roi_t2': 30.0, 'roi_t2': 30.0,
'roi_t3': 20.0, 'roi_t3': 20.0,
'rsi': {'enabled': False}, 'roi_p1': 0.01,
'roi_p2': 0.01,
'roi_p3': 0.1,
'stoploss': -0.4, 'stoploss': -0.4,
'trigger': {'type': 'macd_cross_signal'},
'uptrend_long_ema': {'enabled': False},
'uptrend_short_ema': {'enabled': True},
'uptrend_sma': {'enabled': True}
} }
response_expected = { response_expected = {
'loss': 1.9840569076926293, 'loss': 1.9840569076926293,
'result': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC ' 'result': ' 1 trades. Avg profit 2.31%. Total profit 0.00023300 BTC '
'(0.0231Σ%). Avg duration 100.0 mins.', '(0.0231Σ%). Avg duration 100.0 mins.',
'status': 'ok' 'params': optimizer_param
} }
hyperopt = Hyperopt(conf) hyperopt = Hyperopt(conf)
generate_optimizer_value = hyperopt.generate_optimizer(optimizer_param) generate_optimizer_value = hyperopt.generate_optimizer(list(optimizer_param.values()))
assert generate_optimizer_value == response_expected assert generate_optimizer_value == response_expected

View File

@ -3,16 +3,19 @@
import json import json
import os import os
import uuid import uuid
import arrow
from shutil import copyfile from shutil import copyfile
import arrow
from freqtrade import optimize from freqtrade import optimize
from freqtrade.misc import file_dump_json
from freqtrade.optimize.__init__ import make_testdata_path, download_pairs, \
download_backtesting_testdata, load_tickerdata_file, trim_tickerlist, \
load_cached_data_for_updating
from freqtrade.arguments import TimeRange from freqtrade.arguments import TimeRange
from freqtrade.tests.conftest import log_has, get_patched_exchange from freqtrade.misc import file_dump_json
from freqtrade.optimize.__init__ import (download_backtesting_testdata,
download_pairs,
load_cached_data_for_updating,
load_tickerdata_file,
make_testdata_path, trim_tickerlist)
from freqtrade.tests.conftest import get_patched_exchange, log_has
# Change this if modifying UNITTEST/BTC testdatafile # Change this if modifying UNITTEST/BTC testdatafile
_BTC_UNITTEST_LENGTH = 13681 _BTC_UNITTEST_LENGTH = 13681

View File

@ -13,7 +13,8 @@ from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc.rpc import RPC, RPCException from freqtrade.rpc.rpc import RPC, RPCException
from freqtrade.state import State from freqtrade.state import State
from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap from freqtrade.tests.test_freqtradebot import (patch_coinmarketcap,
patch_get_signal)
# Functions for recurrent object patching # Functions for recurrent object patching

View File

@ -7,7 +7,7 @@ from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
from freqtrade.rpc.rpc_manager import RPCManager from freqtrade.rpc.rpc_manager import RPCManager
from freqtrade.tests.conftest import log_has, get_patched_freqtradebot from freqtrade.tests.conftest import get_patched_freqtradebot, log_has
def test_rpc_manager_object() -> None: def test_rpc_manager_object() -> None:

View File

@ -11,17 +11,18 @@ from datetime import datetime
from random import randint from random import randint
from unittest.mock import MagicMock from unittest.mock import MagicMock
from telegram import Update, Message, Chat from telegram import Chat, Message, Update
from telegram.error import NetworkError from telegram.error import NetworkError
from freqtrade import __version__ from freqtrade import __version__
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc.telegram import Telegram from freqtrade.rpc.telegram import Telegram, authorized_only
from freqtrade.rpc.telegram import authorized_only
from freqtrade.state import State from freqtrade.state import State
from freqtrade.tests.conftest import get_patched_freqtradebot, patch_exchange, log_has from freqtrade.tests.conftest import (get_patched_freqtradebot, log_has,
from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap patch_exchange)
from freqtrade.tests.test_freqtradebot import (patch_coinmarketcap,
patch_get_signal)
class DummyCls(Telegram): class DummyCls(Telegram):

View File

@ -1,8 +1,9 @@
# pragma pylint: disable=missing-docstring,C0103,protected-access # pragma pylint: disable=missing-docstring,C0103,protected-access
import freqtrade.tests.conftest as tt # test tools
from unittest.mock import MagicMock from unittest.mock import MagicMock
import freqtrade.tests.conftest as tt # test tools
# whitelist, blacklist, filtering, all of that will # whitelist, blacklist, filtering, all of that will
# eventually become some rules to run on a generic ACL engine # eventually become some rules to run on a generic ACL engine
# perhaps try to anticipate that by using some python package # perhaps try to anticipate that by using some python package

View File

@ -12,9 +12,9 @@ import arrow
from pandas import DataFrame from pandas import DataFrame
from freqtrade.analyze import Analyze, SignalType from freqtrade.analyze import Analyze, SignalType
from freqtrade.optimize.__init__ import load_tickerdata_file
from freqtrade.arguments import TimeRange from freqtrade.arguments import TimeRange
from freqtrade.tests.conftest import log_has, get_patched_exchange from freqtrade.optimize.__init__ import load_tickerdata_file
from freqtrade.tests.conftest import get_patched_exchange, log_has
# Avoid to reinit the same object again and again # Avoid to reinit the same object again and again
_ANALYZE = Analyze({'strategy': 'DefaultStrategy'}) _ANALYZE = Analyze({'strategy': 'DefaultStrategy'})
@ -42,6 +42,7 @@ def test_analyze_object() -> None:
assert hasattr(Analyze, 'get_signal') assert hasattr(Analyze, 'get_signal')
assert hasattr(Analyze, 'should_sell') assert hasattr(Analyze, 'should_sell')
assert hasattr(Analyze, 'min_roi_reached') assert hasattr(Analyze, 'min_roi_reached')
assert hasattr(Analyze, 'stop_loss_reached')
def test_dataframe_correct_length(result): def test_dataframe_correct_length(result):

View File

@ -4,18 +4,18 @@
Unit test file for configuration.py Unit test file for configuration.py
""" """
import json import json
from argparse import Namespace
from copy import deepcopy from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
from argparse import Namespace
import pytest import pytest
from jsonschema import ValidationError from jsonschema import ValidationError
from freqtrade import OperationalException
from freqtrade.arguments import Arguments from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration from freqtrade.configuration import Configuration
from freqtrade.constants import DEFAULT_DB_PROD_URL, DEFAULT_DB_DRYRUN_URL from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL
from freqtrade.tests.conftest import log_has from freqtrade.tests.conftest import log_has
from freqtrade import OperationalException
def test_configuration_object() -> None: def test_configuration_object() -> None:

View File

@ -5,7 +5,6 @@ import time
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
from requests.exceptions import RequestException from requests.exceptions import RequestException
from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter

View File

@ -14,11 +14,13 @@ import arrow
import pytest import pytest
import requests import requests
from freqtrade import constants, DependencyException, OperationalException, TemporaryError from freqtrade import (DependencyException, OperationalException,
TemporaryError, constants)
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.state import State from freqtrade.state import State
from freqtrade.tests.conftest import log_has, patch_coinmarketcap, patch_exchange from freqtrade.tests.conftest import (log_has, patch_coinmarketcap,
patch_exchange)
# Functions for recurrent object patching # Functions for recurrent object patching
@ -348,6 +350,34 @@ def test_get_min_pair_stake_amount(mocker, default_conf) -> None:
result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1) result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1)
assert result is None assert result is None
# no cost Min
mocker.patch(
'freqtrade.exchange.Exchange.get_markets',
MagicMock(return_value=[{
'symbol': 'ETH/BTC',
'limits': {
'cost': {"min": None},
'amount': {}
}
}])
)
result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1)
assert result is None
# no amount Min
mocker.patch(
'freqtrade.exchange.Exchange.get_markets',
MagicMock(return_value=[{
'symbol': 'ETH/BTC',
'limits': {
'cost': {},
'amount': {"min": None}
}
}])
)
result = freqtrade._get_min_pair_stake_amount('ETH/BTC', 1)
assert result is None
# empty 'cost'/'amount' section # empty 'cost'/'amount' section
mocker.patch( mocker.patch(
'freqtrade.exchange.Exchange.get_markets', 'freqtrade.exchange.Exchange.get_markets',
@ -1124,7 +1154,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, fe
Trade.session.add(trade_buy) Trade.session.add(trade_buy)
# check it does cancel buy orders over the time limit # check it does cancel buy orders over the time limit
freqtrade.check_handle_timedout(600) freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1 assert rpc_mock.call_count == 1
trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all() trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
@ -1165,7 +1195,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old,
Trade.session.add(trade_sell) Trade.session.add(trade_sell)
# check it does cancel sell orders over the time limit # check it does cancel sell orders over the time limit
freqtrade.check_handle_timedout(600) freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1 assert rpc_mock.call_count == 1
assert trade_sell.is_open is True assert trade_sell.is_open is True
@ -1205,7 +1235,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old
# check it does cancel buy orders over the time limit # check it does cancel buy orders over the time limit
# note this is for a partially-complete buy order # note this is for a partially-complete buy order
freqtrade.check_handle_timedout(600) freqtrade.check_handle_timedout()
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
assert rpc_mock.call_count == 1 assert rpc_mock.call_count == 1
trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all() trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all()
@ -1256,7 +1286,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) -
'recent call last):\n.*' 'recent call last):\n.*'
) )
freqtrade.check_handle_timedout(600) freqtrade.check_handle_timedout()
assert filter(regexp.match, caplog.record_tuples) assert filter(regexp.match, caplog.record_tuples)
@ -1599,6 +1629,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, marke
}), }),
buy=MagicMock(return_value={'id': limit_buy_order['id']}), buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee, get_fee=fee,
get_markets=markets
) )
conf = deepcopy(default_conf) conf = deepcopy(default_conf)
@ -1616,7 +1647,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, marke
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, mocker) -> None: def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, markets, mocker) -> None:
""" """
Test sell_profit_only feature when enabled and we have a loss Test sell_profit_only feature when enabled and we have a loss
""" """
@ -1634,6 +1665,7 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, mocker) ->
}), }),
buy=MagicMock(return_value={'id': limit_buy_order['id']}), buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee, get_fee=fee,
get_markets=markets
) )
conf = deepcopy(default_conf) conf = deepcopy(default_conf)
@ -1654,6 +1686,103 @@ def test_ignore_roi_if_buy_signal(default_conf, limit_buy_order, fee, mocker) ->
assert freqtrade.handle_trade(trade) is True assert freqtrade.handle_trade(trade) is True
def test_trailing_stop_loss(default_conf, limit_buy_order, fee, caplog, mocker) -> None:
"""
Test sell_profit_only feature when enabled and we have a loss
"""
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.00000102,
'ask': 0.00000103,
'last': 0.00000102
}),
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
)
conf = deepcopy(default_conf)
conf['trailing_stop'] = True
print(limit_buy_order)
freqtrade = FreqtradeBot(conf)
freqtrade.create_trade()
trade = Trade.query.first()
trade.update(limit_buy_order)
caplog.set_level(logging.DEBUG)
# Sell as trailing-stop is reached
assert freqtrade.handle_trade(trade) is True
assert log_has(
f'HIT STOP: current price at 0.000001, stop loss is {trade.stop_loss:.6f}, '
f'initial stop loss was at 0.000010, trade opened at 0.000011', caplog.record_tuples)
def test_trailing_stop_loss_positive(default_conf, limit_buy_order, fee, caplog, mocker) -> None:
"""
Test sell_profit_only feature when enabled and we have a loss
"""
buy_price = limit_buy_order['price']
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False)
mocker.patch.multiple(
'freqtrade.exchange.Exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': buy_price - 0.000001,
'ask': buy_price - 0.000001,
'last': buy_price - 0.000001
}),
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
)
conf = deepcopy(default_conf)
conf['trailing_stop'] = True
conf['trailing_stop_positive'] = 0.01
freqtrade = FreqtradeBot(conf)
freqtrade.create_trade()
trade = Trade.query.first()
trade.update(limit_buy_order)
caplog.set_level(logging.DEBUG)
# stop-loss not reached
assert freqtrade.handle_trade(trade) is False
# Raise ticker above buy price
mocker.patch('freqtrade.exchange.Exchange.get_ticker',
MagicMock(return_value={
'bid': buy_price + 0.000003,
'ask': buy_price + 0.000003,
'last': buy_price + 0.000003
}))
# stop-loss not reached, adjusted stoploss
assert freqtrade.handle_trade(trade) is False
assert log_has(f'using positive stop loss mode: 0.01 since we have profit 0.26662643',
caplog.record_tuples)
assert log_has(f'adjusted stop loss', caplog.record_tuples)
assert trade.stop_loss == 0.0000138501
mocker.patch('freqtrade.exchange.Exchange.get_ticker',
MagicMock(return_value={
'bid': buy_price + 0.000002,
'ask': buy_price + 0.000002,
'last': buy_price + 0.000002
}))
# Lower price again (but still positive)
assert freqtrade.handle_trade(trade) is True
assert log_has(
f'HIT STOP: current price at {buy_price + 0.000002:.6f}, '
f'stop loss is {trade.stop_loss:.6f}, '
f'initial stop loss was at 0.000010, trade opened at 0.000011', caplog.record_tuples)
def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order, def test_disable_ignore_roi_if_buy_signal(default_conf, limit_buy_order,
fee, markets, mocker) -> None: fee, markets, mocker) -> None:
""" """

View File

@ -1,6 +1,6 @@
import pandas as pd import pandas as pd
from freqtrade.indicator_helpers import went_up, went_down from freqtrade.indicator_helpers import went_down, went_up
def test_went_up(): def test_went_up():

View File

@ -11,7 +11,7 @@ import pytest
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.arguments import Arguments from freqtrade.arguments import Arguments
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.main import main, set_loggers, reconfigure from freqtrade.main import main, reconfigure, set_loggers
from freqtrade.state import State from freqtrade.state import State
from freqtrade.tests.conftest import log_has, patch_exchange from freqtrade.tests.conftest import log_has, patch_exchange

View File

@ -8,8 +8,8 @@ import datetime
from unittest.mock import MagicMock from unittest.mock import MagicMock
from freqtrade.analyze import Analyze from freqtrade.analyze import Analyze
from freqtrade.misc import (shorten_date, datesarray_to_datetimearray, from freqtrade.misc import (common_datearray, datesarray_to_datetimearray,
common_datearray, file_dump_json, format_ms_time) file_dump_json, format_ms_time, shorten_date)
from freqtrade.optimize.__init__ import load_tickerdata_file from freqtrade.optimize.__init__ import load_tickerdata_file

View File

@ -5,8 +5,9 @@ from unittest.mock import MagicMock
import pytest import pytest
from sqlalchemy import create_engine from sqlalchemy import create_engine
from freqtrade import constants, OperationalException from freqtrade import OperationalException, constants
from freqtrade.persistence import Trade, init, clean_dry_run_db from freqtrade.persistence import Trade, clean_dry_run_db, init
from freqtrade.tests.conftest import log_has
@pytest.fixture(scope='function') @pytest.fixture(scope='function')
@ -400,9 +401,12 @@ def test_migrate_old(mocker, default_conf, fee):
assert trade.stake_amount == default_conf.get("stake_amount") assert trade.stake_amount == default_conf.get("stake_amount")
assert trade.pair == "ETC/BTC" assert trade.pair == "ETC/BTC"
assert trade.exchange == "bittrex" assert trade.exchange == "bittrex"
assert trade.max_rate == 0.0
assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0
def test_migrate_new(mocker, default_conf, fee): def test_migrate_new(mocker, default_conf, fee, caplog):
""" """
Test Database migration (starting with new pairformat) Test Database migration (starting with new pairformat)
""" """
@ -439,6 +443,11 @@ def test_migrate_new(mocker, default_conf, fee):
# Create table using the old format # Create table using the old format
engine.execute(create_table_old) engine.execute(create_table_old)
engine.execute(insert_table_old) engine.execute(insert_table_old)
# fake previous backup
engine.execute("create table trades_bak as select * from trades")
engine.execute("create table trades_bak1 as select * from trades")
# Run init to test migration # Run init to test migration
init(default_conf) init(default_conf)
@ -453,3 +462,54 @@ def test_migrate_new(mocker, default_conf, fee):
assert trade.stake_amount == default_conf.get("stake_amount") assert trade.stake_amount == default_conf.get("stake_amount")
assert trade.pair == "ETC/BTC" assert trade.pair == "ETC/BTC"
assert trade.exchange == "binance" assert trade.exchange == "binance"
assert trade.max_rate == 0.0
assert trade.stop_loss == 0.0
assert trade.initial_stop_loss == 0.0
assert log_has("trying trades_bak1", caplog.record_tuples)
assert log_has("trying trades_bak2", caplog.record_tuples)
def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee):
trade = Trade(
pair='ETH/BTC',
stake_amount=0.001,
fee_open=fee.return_value,
fee_close=fee.return_value,
exchange='bittrex',
open_rate=1,
)
trade.adjust_stop_loss(trade.open_rate, 0.05, True)
assert trade.stop_loss == 0.95
assert trade.max_rate == 1
assert trade.initial_stop_loss == 0.95
# Get percent of profit with a lowre rate
trade.adjust_stop_loss(0.96, 0.05)
assert trade.stop_loss == 0.95
assert trade.max_rate == 1
assert trade.initial_stop_loss == 0.95
# Get percent of profit with a custom rate (Higher than open rate)
trade.adjust_stop_loss(1.3, -0.1)
assert round(trade.stop_loss, 8) == 1.17
assert trade.max_rate == 1.3
assert trade.initial_stop_loss == 0.95
# current rate lower again ... should not change
trade.adjust_stop_loss(1.2, 0.1)
assert round(trade.stop_loss, 8) == 1.17
assert trade.max_rate == 1.3
assert trade.initial_stop_loss == 0.95
# current rate higher... should raise stoploss
trade.adjust_stop_loss(1.4, 0.1)
assert round(trade.stop_loss, 8) == 1.26
assert trade.max_rate == 1.4
assert trade.initial_stop_loss == 0.95
# Initial is true but stop_loss set - so doesn't do anything
trade.adjust_stop_loss(1.7, 0.1, True)
assert round(trade.stop_loss, 8) == 1.26
assert trade.max_rate == 1.4
assert trade.initial_stop_loss == 0.95

View File

@ -1,5 +1,5 @@
ccxt==1.14.256 ccxt==1.15.13
SQLAlchemy==1.2.8 SQLAlchemy==1.2.9
python-telegram-bot==10.1.0 python-telegram-bot==10.1.0
arrow==0.12.1 arrow==0.12.1
cachetools==2.1.0 cachetools==2.1.0
@ -12,14 +12,14 @@ scipy==1.1.0
jsonschema==2.6.0 jsonschema==2.6.0
numpy==1.14.5 numpy==1.14.5
TA-Lib==0.4.17 TA-Lib==0.4.17
pytest==3.6.2 pytest==3.6.3
pytest-mock==1.10.0 pytest-mock==1.10.0
pytest-cov==2.5.1 pytest-cov==2.5.1
hyperopt==0.1
# do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325
networkx==1.11 # pyup: ignore
tabulate==0.8.2 tabulate==0.8.2
coinmarketcap==5.0.3 coinmarketcap==5.0.3
# Required for hyperopt
scikit-optimize==0.5.2
# Required for plotting data # Required for plotting data
#plotly==2.7.0 #plotly==2.7.0

View File

@ -143,15 +143,14 @@ def convert_main(args: Namespace) -> None:
interval = str_interval interval = str_interval
break break
# change order on pairs if old ticker interval found # change order on pairs if old ticker interval found
filename_new = path.join(path.dirname(filename), filename_new = path.join(path.dirname(filename),
"{}_{}-{}.json".format(currencies[1], f"{currencies[1]}_{currencies[0]}-{interval}.json")
currencies[0], interval))
elif ret_string: elif ret_string:
interval = ret_string.group(0) interval = ret_string.group(0)
filename_new = path.join(path.dirname(filename), filename_new = path.join(path.dirname(filename),
"{}_{}-{}.json".format(currencies[0], f"{currencies[0]}_{currencies[1]}-{interval}.json")
currencies[1], interval))
else: else:
logger.warning("file %s could not be converted, interval not found", filename) logger.warning("file %s could not be converted, interval not found", filename)

View File

@ -3,11 +3,14 @@
"""This script generate json data from bittrex""" """This script generate json data from bittrex"""
import json import json
import sys import sys
import os from pathlib import Path
import arrow import arrow
from freqtrade import (arguments, misc) from freqtrade import arguments
from freqtrade.arguments import TimeRange
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.optimize import download_backtesting_testdata
DEFAULT_DL_PATH = 'user_data/data' DEFAULT_DL_PATH = 'user_data/data'
@ -17,25 +20,27 @@ args = arguments.parse_args()
timeframes = args.timeframes timeframes = args.timeframes
dl_path = os.path.join(DEFAULT_DL_PATH, args.exchange) dl_path = Path(DEFAULT_DL_PATH).joinpath(args.exchange)
if args.export: if args.export:
dl_path = args.export dl_path = Path(args.export)
if not os.path.isdir(dl_path): if not dl_path.is_dir():
sys.exit(f'Directory {dl_path} does not exist.') sys.exit(f'Directory {dl_path} does not exist.')
pairs_file = args.pairs_file if args.pairs_file else os.path.join(dl_path, 'pairs.json') pairs_file = Path(args.pairs_file) if args.pairs_file else dl_path.joinpath('pairs.json')
if not os.path.isfile(pairs_file): if not pairs_file.exists():
sys.exit(f'No pairs file found with path {pairs_file}.') sys.exit(f'No pairs file found with path {pairs_file}.')
with open(pairs_file) as file: with pairs_file.open() as file:
PAIRS = list(set(json.load(file))) PAIRS = list(set(json.load(file)))
PAIRS.sort() PAIRS.sort()
since_time = None
timerange = TimeRange()
if args.days: if args.days:
since_time = arrow.utcnow().shift(days=-args.days).timestamp * 1000 time_since = arrow.utcnow().shift(days=-args.days).strftime("%Y%m%d")
timerange = arguments.parse_timerange(f'{time_since}-')
print(f'About to download pairs: {PAIRS} to {dl_path}') print(f'About to download pairs: {PAIRS} to {dl_path}')
@ -59,21 +64,18 @@ for pair in PAIRS:
print(f"skipping pair {pair}") print(f"skipping pair {pair}")
continue continue
for tick_interval in timeframes: for tick_interval in timeframes:
print(f'downloading pair {pair}, interval {tick_interval}')
data = exchange.get_ticker_history(pair, tick_interval, since_ms=since_time)
if not data:
print('\tNo data was downloaded')
break
print('\tData was downloaded for period %s - %s' % (
arrow.get(data[0][0] / 1000).format(),
arrow.get(data[-1][0] / 1000).format()))
# save data
pair_print = pair.replace('/', '_') pair_print = pair.replace('/', '_')
filename = f'{pair_print}-{tick_interval}.json' filename = f'{pair_print}-{tick_interval}.json'
misc.file_dump_json(os.path.join(dl_path, filename), data) dl_file = dl_path.joinpath(filename)
if args.erase and dl_file.exists():
print(f'Deleting existing data for pair {pair}, interval {tick_interval}')
dl_file.unlink()
print(f'downloading pair {pair}, interval {tick_interval}')
download_backtesting_testdata(str(dl_path), exchange=exchange,
pair=pair,
tick_interval=tick_interval,
timerange=timerange)
if pairs_not_available: if pairs_not_available:

View File

@ -25,11 +25,13 @@ Example of usage:
--indicators2 fastk,fastd --indicators2 fastk,fastd
""" """
import logging import logging
import os
import sys import sys
import json
from pathlib import Path
from argparse import Namespace from argparse import Namespace
from typing import Dict, List, Any from typing import Dict, List, Any
import pandas as pd
import plotly.graph_objs as go import plotly.graph_objs as go
from plotly import tools from plotly import tools
from plotly.offline import plot from plotly.offline import plot
@ -37,7 +39,7 @@ from plotly.offline import plot
import freqtrade.optimize as optimize import freqtrade.optimize as optimize
from freqtrade import persistence from freqtrade import persistence
from freqtrade.analyze import Analyze from freqtrade.analyze import Analyze
from freqtrade.arguments import Arguments from freqtrade.arguments import Arguments, TimeRange
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.optimize.backtesting import setup_configuration from freqtrade.optimize.backtesting import setup_configuration
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
@ -46,6 +48,45 @@ logger = logging.getLogger(__name__)
_CONF: Dict[str, Any] = {} _CONF: Dict[str, Any] = {}
def load_trades(args: Namespace, pair: str, timerange: TimeRange) -> pd.DataFrame:
trades: pd.DataFrame = pd.DataFrame()
if args.db_url:
persistence.init(_CONF)
columns = ["pair", "profit", "opents", "closets", "open_rate", "close_rate", "duration"]
trades = pd.DataFrame([(t.pair, t.calc_profit(),
t.open_date, t.close_date,
t.open_rate, t.close_rate,
t.close_date.timestamp() - t.open_date.timestamp())
for t in Trade.query.filter(Trade.pair.is_(pair)).all()],
columns=columns)
if args.exportfilename:
file = Path(args.exportfilename)
# must align with columns in backtest.py
columns = ["pair", "profit", "opents", "closets", "index", "duration",
"open_rate", "close_rate", "open_at_end"]
with file.open() as f:
data = json.load(f)
trades = pd.DataFrame(data, columns=columns)
trades = trades.loc[trades["pair"] == pair]
if timerange:
if timerange.starttype == 'date':
trades = trades.loc[trades["opents"] >= timerange.startts]
if timerange.stoptype == 'date':
trades = trades.loc[trades["opents"] <= timerange.stopts]
trades['opents'] = pd.to_datetime(trades['opents'],
unit='s',
utc=True,
infer_datetime_format=True)
trades['closets'] = pd.to_datetime(trades['closets'],
unit='s',
utc=True,
infer_datetime_format=True)
return trades
def plot_analyzed_dataframe(args: Namespace) -> None: def plot_analyzed_dataframe(args: Namespace) -> None:
""" """
Calls analyze() and plots the returned dataframe Calls analyze() and plots the returned dataframe
@ -102,31 +143,32 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
if tickers == {}: if tickers == {}:
exit() exit()
if args.db_url and args.exportfilename:
logger.critical("Can only specify --db-url or --export-filename")
# Get trades already made from the DB # Get trades already made from the DB
trades: List[Trade] = [] trades = load_trades(args, pair, timerange)
if args.db_url:
persistence.init(_CONF)
trades = Trade.query.filter(Trade.pair.is_(pair)).all()
dataframes = analyze.tickerdata_to_dataframe(tickers) dataframes = analyze.tickerdata_to_dataframe(tickers)
dataframe = dataframes[pair] dataframe = dataframes[pair]
dataframe = analyze.populate_buy_trend(dataframe) dataframe = analyze.populate_buy_trend(dataframe)
dataframe = analyze.populate_sell_trend(dataframe) dataframe = analyze.populate_sell_trend(dataframe)
if len(dataframe.index) > 750: if len(dataframe.index) > args.plot_limit:
logger.warning('Ticker contained more than 750 candles, clipping.') logger.warning('Ticker contained more than %s candles as defined '
'with --plot-limit, clipping.', args.plot_limit)
dataframe = dataframe.tail(args.plot_limit)
trades = trades.loc[trades['opents'] >= dataframe.iloc[0]['date']]
fig = generate_graph( fig = generate_graph(
pair=pair, pair=pair,
trades=trades, trades=trades,
data=dataframe.tail(750), data=dataframe,
args=args args=args
) )
plot(fig, filename=os.path.join('user_data', 'freqtrade-plot.html')) plot(fig, filename=str(Path('user_data').joinpath('freqtrade-plot.html')))
def generate_graph(pair, trades, data, args) -> tools.make_subplots: def generate_graph(pair, trades: pd.DataFrame, data: pd.DataFrame, args) -> tools.make_subplots:
""" """
Generate the graph from the data generated by Backtesting or from DB Generate the graph from the data generated by Backtesting or from DB
:param pair: Pair to Display on the graph :param pair: Pair to Display on the graph
@ -187,8 +229,8 @@ def generate_graph(pair, trades, data, args) -> tools.make_subplots:
) )
trade_buys = go.Scattergl( trade_buys = go.Scattergl(
x=[t.open_date.isoformat() for t in trades], x=trades["opents"],
y=[t.open_rate for t in trades], y=trades["open_rate"],
mode='markers', mode='markers',
name='trade_buy', name='trade_buy',
marker=dict( marker=dict(
@ -199,8 +241,8 @@ def generate_graph(pair, trades, data, args) -> tools.make_subplots:
) )
) )
trade_sells = go.Scattergl( trade_sells = go.Scattergl(
x=[t.close_date.isoformat() for t in trades], x=trades["closets"],
y=[t.close_rate for t in trades], y=trades["close_rate"],
mode='markers', mode='markers',
name='trade_sell', name='trade_sell',
marker=dict( marker=dict(
@ -299,11 +341,17 @@ def plot_parse_args(args: List[str]) -> Namespace:
default='macd', default='macd',
dest='indicators2', dest='indicators2',
) )
arguments.parser.add_argument(
'--plot-limit',
help='Specify tick limit for plotting - too high values cause huge files - '
'Default: %(default)s',
dest='plot_limit',
default=750,
type=int,
)
arguments.common_args_parser() arguments.common_args_parser()
arguments.optimizer_shared_options(arguments.parser) arguments.optimizer_shared_options(arguments.parser)
arguments.backtesting_options(arguments.parser) arguments.backtesting_options(arguments.parser)
return arguments.parse_args() return arguments.parse_args()

View File

@ -36,6 +36,7 @@ setup(name='freqtrade',
'tabulate', 'tabulate',
'cachetools', 'cachetools',
'coinmarketcap', 'coinmarketcap',
'scikit-optimize',
], ],
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,